From 6031106d935c073c628b02d40920dc0c2760a6de Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Wed, 29 Apr 2026 14:51:19 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20add=20UPSTREAM=5FPORT=5FGUIDE.md=20?= =?UTF-8?q?=E2=80=94=20translation=20rules=20for=20gsd-2=20=E2=86=92=20sf?= =?UTF-8?q?=20ports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_ → sf_, 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 --- UPSTREAM_PORT_GUIDE.md | 167 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 UPSTREAM_PORT_GUIDE.md diff --git a/UPSTREAM_PORT_GUIDE.md b/UPSTREAM_PORT_GUIDE.md new file mode 100644 index 000000000..4b4437989 --- /dev/null +++ b/UPSTREAM_PORT_GUIDE.md @@ -0,0 +1,167 @@ +# 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_` (in prompts) | `sf_` | 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: + +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) + +```bash +# 1. Read the upstream commit +git show + +# 2. If it touches packages/pi-* equivalents in our tree, try cherry-pick +git cherry-pick + +# 3. If clean, type-check +cd packages/ && npx tsc --noEmit + +# 4. Commit message +# port(pi-mono): (refs ) +``` + +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) + +```bash +# 1. Read the upstream commit +git show + +# 2. For each file the commit modifies, find our equivalent +# Translation: extensions/gsd/ → extensions/sf/ +# Translation: gsd_ → sf_ +# Translation: .gsd/ → .sf/ + +# 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): (refs ) +``` + +### 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 | +|---| +| ✅ `` — landed | +| TODO — pending | +| **DEFERRED** — applies but needs prerequisite refactor: | +| **SKIP** — architectural divergence: | +| **SKIP** — already richer locally: see `` | + +--- + +## Verifying the translation + +For any port, run: + +```bash +# 1. Type-check the affected packages +npx tsc --noEmit -p tsconfig.extensions.json +cd packages/ && 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_` 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_id` +- `gsd_plan_slice` → `sf_plan_slice` +- `gsd_decision_save` → `sf_decision_save` +- `gsd_summary_save` → `sf_summary_save` +- `gsd_complete_task` → `sf_complete_task` +- `gsd_product_audit` → `sf_product_audit` +- `gsd_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_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 ` 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_` tool whose `sf_` equivalent isn't registered?). + +For now, manual translation by humans (or by sf with this guide as input) is the workflow.