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>
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.ts → src/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:
-
The architecture diverged, not just the names. Example: gsd-2 commit
bb747ec57"fix(mcp-server): prevent defaultExecFn stdout-buffer deadlock" — they have adefaultExecFnthat spawns child processes; we have anexecFnparameter 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. -
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). -
We have richer code that the upstream is catching up to. Don't downgrade to upstream's version. Example: our
benchmark-selector.tshas 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_id→sf_milestone_generate_idgsd_plan_slice→sf_plan_slicegsd_decision_save→sf_decision_savegsd_summary_save→sf_summary_savegsd_complete_task→sf_complete_taskgsd_product_audit→sf_product_auditgsd_help→sf_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_reviewtool, 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 whosesf_<verb>equivalent isn't registered?).
For now, manual translation by humans (or by sf with this guide as input) is the workflow.