singularity-forge/UPSTREAM_PORT_GUIDE.md
Mikael Hugo 6031106d93 docs: add UPSTREAM_PORT_GUIDE.md — translation rules for gsd-2 → sf ports
We sync from two upstreams (pi-mono via cherry-pick, gsd-2 via manual
port) and the gsd-2 syncs hit naming/path translation every time.
This guide makes the translation rules explicit and persistent so
future ports (by humans or by sf) don't have to rediscover them.

Covers:
- The naming translations table: gsd_* → sf_*, .gsd/ → .sf/,
  extensions/gsd/ → extensions/sf/, @sf-run/* → @singularity-forge/*,
  GSD_HOME → SF_HOME, etc.
- Default rule: translate naming, keep substance. Includes the
  cautionary tale of my own self-heal rejection (1bbd20bf7) where I
  wrongly skipped a fix because of the path string.
- When a port REALLY doesn't apply (architectural divergence vs naming
  divergence) — three categories with examples.
- Mechanics for pi-mono (cherry-pick) vs gsd-2 (manual) ports.
- Skip-list documentation: when you reject, document why in BUILD_PLAN
  with the upstream SHA and reason.
- Prompt-edit handling: gsd_<verb> → sf_<verb>, register tools before
  porting prompt edits that call them.

Future automation hint at the bottom for a port-translation script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 14:51:19 +02:00

7.5 KiB

Upstream port translation guide

Reference for porting fixes/features from upstream into singularity-forge.

We sync from two upstreams:

Upstream Path When
badlogic/pi-mono remote pi-mono SDK fixes (agent core, AI clients, TUI primitives) — cherry-pick usually works (no namespace divergence)
gsd-build/gsd-2 remote upstream (alias gsd2) Autopilot/harness fixes — manual port required (namespace + path divergence)

This guide covers gsd-2 because it's where the translation work happens. Pi-mono ports are mostly direct cherry-picks.


The naming translations (memorize these)

When porting from gsd-2, mechanically translate every occurrence of these patterns:

gsd-2 singularity-forge Where it appears
gsd_* (tool names) sf_* All sf_milestone_generate_id, sf_plan_slice, sf_decision_save, sf_summary_save, sf_complete_task, sf_product_audit, etc.
gsd_<verb> (in prompts) sf_<verb> Inline tool references in prompt markdown
.gsd/ (project staging dir) .sf/ .gsd/REQUIREMENTS.md.sf/REQUIREMENTS.md, .gsd/DECISIONS.md.sf/DECISIONS.md, .gsd/active/{mid}/.sf/active/{mid}/, etc.
extensions/gsd/ (path) extensions/sf/ src/resources/extensions/gsd/auto-prompts.tssrc/resources/extensions/sf/auto-prompts.ts
@sf-run/* (package scope) @singularity-forge/* npm package imports in TS files
GSD_HOME env var SF_HOME env var lookups in shell, TS, docs
"GSD" / "gsd" (display) "sf" or "Singularity Forge" log lines, error messages, README sections — but only the display strings; structural symbols already covered above
gsd-build/gsd-2 (upstream URL) singularity-ng/singularity-forge nothing to translate; just don't reference upstream URL in our docs except as attribution

Hermes left alone — bunker had a Hermes Plugin Reviewer skill that genuinely targets the Hermes agent platform (different product). The string "Hermes" in that context is correct as-is. Only translate gsd→sf, not other agent names.


The default rule: translate naming, keep substance

When a gsd-2 commit references .gsd/ or gsd_*, the fix is almost always about something other than the literal path string — symlink resilience, race conditions, validation, a security check. The naming is incidental. Translate the names; the substance ports.

Bad rejection example (one I made on 2026-04-29, corrected in 1bbd20bf7):

gsd-2 commit 9340f1e9b "fix(gsd): self-heal symlinked .gsd staging to prevent silent data loss"

My initial call: "doesn't apply because we use .sf/ instead"

Correct call: the fix is symlink resilience. Translate .gsd/.sf/ in the port. The substance ports.

If you ever find yourself typing "doesn't apply because we use X instead of Y" where X and Y are paths or naming conventions — STOP. Re-read the commit. The fix is about the underlying behavior, not the path.


When a port really doesn't apply (architectural divergence)

There are real cases where porting doesn't make sense. Recognize them by their substance, not their names:

  1. The architecture diverged, not just the names. Example: gsd-2 commit bb747ec57 "fix(mcp-server): prevent defaultExecFn stdout-buffer deadlock" — they have a defaultExecFn that spawns child processes; we have an execFn parameter passed in by callers. Their fix is in the spawn implementation that we don't have. The deadlock vector exists for callers but our remediation is different.

  2. The bug is in code we replaced. Example: pi-mono 3e7ffff18 "fix(ai): ignore unknown anthropic sse events" — they own the SSE parser; we use the SDK directly. Their fix patches code we don't have. To get the protection, we'd need to port the entire "own the parser" refactor (multiple commits, ~200 LOC).

  3. We have richer code that the upstream is catching up to. Don't downgrade to upstream's version. Example: our benchmark-selector.ts has more eval types (swe_bench, aime_2026, etc.) than bunker's. Importing bunker's would lose those.

When you reject for one of these reasons, document why in the BUILD_PLAN with the upstream SHA + a one-line explanation of the architectural difference. Future-you (or sf) needs to know it was considered, not just skipped.


Port mechanics

From pi-mono (cherry-pick usually works)

# 1. Read the upstream commit
git show <pi-mono-sha>

# 2. If it touches packages/pi-* equivalents in our tree, try cherry-pick
git cherry-pick <pi-mono-sha>

# 3. If clean, type-check
cd packages/<pkg> && npx tsc --noEmit

# 4. Commit message
# port(pi-mono): <description> (refs <pi-mono-sha>)

If cherry-pick conflicts: read the conflict, resolve manually, commit. Pi-mono conflicts are usually small because we share the same package layout and naming.

From gsd-2 (manual port)

# 1. Read the upstream commit
git show <gsd-2-sha>

# 2. For each file the commit modifies, find our equivalent
# Translation: extensions/gsd/<x> → extensions/sf/<x>
# Translation: gsd_<verb> → sf_<verb>
# Translation: .gsd/<path> → .sf/<path>

# 3. Apply the substance of the change to our equivalent file(s)
# DO NOT use git cherry-pick — it will fail on every file

# 4. Type-check
npx tsc --noEmit -p tsconfig.extensions.json

# 5. Commit message
# port(gsd-2): <description> (refs <gsd-2-sha>)

Skip-list documentation

If you decide a port doesn't apply, add a row to the relevant BUILD_PLAN table with status "SKIP — ". Don't silently drop. Examples:

Status example
<our-sha> — landed
TODO — pending
DEFERRED — applies but needs prerequisite refactor:
SKIP — architectural divergence:
SKIP — already richer locally: see <our-file>

Verifying the translation

For any port, run:

# 1. Type-check the affected packages
npx tsc --noEmit -p tsconfig.extensions.json
cd packages/<pkg> && npx tsc --noEmit

# 2. Run the relevant test suite
npm run test:sf-light    # for sf-extension changes
npm run typecheck:extensions

# 3. If the port changes prompts, hand-verify by reading the diff
#    sf will catch missing template variables at runtime; better to catch
#    at port time

Handling gsd_<command> references in prompts

Our prompts (src/resources/extensions/sf/prompts/*.md) call tools by name. When porting a prompt edit from gsd-2:

  • gsd_milestone_generate_idsf_milestone_generate_id
  • gsd_plan_slicesf_plan_slice
  • gsd_decision_savesf_decision_save
  • gsd_summary_savesf_summary_save
  • gsd_complete_tasksf_complete_task
  • gsd_product_auditsf_product_audit
  • gsd_helpsf_help

If a gsd-2 prompt edit introduces a NEW tool we don't have (e.g., gsd_eval_review from the eval-review feature), the port involves both:

  • registering our equivalent sf_eval_review tool, AND
  • the prompt edit calling it

Don't translate just the prompt without registering the tool — that creates a runtime "unknown tool" error.


Future automation hint

This guide is hand-maintained. Eventually we should:

  • Add a script scripts/port-from-gsd2.sh <gsd-2-sha> that emits a translated patch (sed-pipe through the naming map), checks it for context-line conflicts, and applies what it can.
  • Track translation drift (e.g., did upstream add a new gsd_<verb> tool whose sf_<verb> equivalent isn't registered?).

For now, manual translation by humans (or by sf with this guide as input) is the workflow.