* chore(#604): rename get-shit-done/ runtime directory to gsd-core/ Renames the installed runtime directory `get-shit-done/` to `gsd-core/` so the on-disk name matches the package (`@opengsd/gsd-core`), repo, and binary (`gsd-tools`). The npm package name and binary are unchanged; npx/npm consumers are unaffected. Mechanical (bulk, ~90% of the diff): - `git mv get-shit-done gsd-core` - Swept path/identifier references across the repo via `perl -pe 's/get-shit-done(?!-\w)/gsd-core/g'`. The negative lookahead preserves the five legitimate slug variants that are NOT the directory: get-shit-done-{OLD,cc,classic,cli,redux} (old package/repo names). - Build/manifest wiring: package.json (bin, files, coverage globs), tsconfig.build.json (outDir), ~86 .gitignore build-output entries, stryker.config.mjs, scan-ignore files, install.js path strings. - Frozen (not rewritten): CHANGELOG.md history; translated docs (README.<locale>.md and docs/{ja-JP,ko-KR,pt-BR,zh-CN}/). New logic (review here): - src/installer-migrations/003-rename-get-shit-done-to-gsd-core.cts: a proper ADR-0008 installer migration. On upgrade it walks the legacy `~/.claude/get-shit-done/` tree, classifies each file via the prior install manifest, and emits remove-managed / backup-and-remove for managed files while PRESERVING unknown user-added files. Symlink-safe (skips a symlinked root and symlinked entries; bounds-checks every path under configDir). The framework rolls back on install failure. Emptied dirs may remain (framework has no recursive dir-removal primitive) — documented. - scripts/lint-legacy-dir-name.cjs: CI regression guard forbidding the bare `get-shit-done` directory token (split token to avoid self-match; case- insensitive; `(?!-\w)` lookahead allows the slug variants; allowlists CHANGELOG, translated docs, and `gsd-allow-legacy-name` marker lines). Wired into the lint-tests CI job. - Restored scripts/lint-package-identity-drift.cjs detection regexes (the mechanical sweep had wrongly rewritten the old-name patterns it exists to detect) and marked them as intentional legacy references. - TDD tests for the migration and the guard; do.md slash-command guard regex tightened so a `/gsd-core/bin` path segment is not mistaken for a command; changeset + docs/installer-migrations.md row added. Breaking: the installed runtime path moves `~/.claude/get-shit-done/` -> `~/.claude/gsd-core/`. Migration 003 removes the stale legacy dir's managed files (preserving user files) on upgrade. Users with custom hooks/configs hardcoding the old path must update them. Closes #604 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): unsweep pending changesets + allowlist injection-example docs CI fixes for the rename PR: - Do not sweep pending .changeset/*.md (ephemeral release-note fragments, like CHANGELOG); reverted those body edits so 5 pre-existing malformed fragments (missing type/pr) no longer enter the PR diff and trip docs-lint. Allowlisted .changeset/ in the legacy-name guard accordingly. - Allowlisted TEST-EXAMPLES.md and docs/explanation/security-model.md in prompt-injection-scan.sh: they contain intentional injection examples / security-model prose; the path-reference rewrites are kept. CodeQL alerts on this PR are pre-existing (alert lines unchanged by this PR; none in the new migration/guard) and are out of scope for the rename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): resolve CodeQL alerts surfaced on this PR The rename diff touched files carrying pre-existing CodeQL findings; per the no-pre-existing-dismissal rule, fixing every surfaced alert rather than waving them off. All behavior-preserving: - scripts/ci-test-scope.cjs: build the config-path match from string .includes() instead of a RegExp over an arg-derived value (js/regex-injection). - src/profile-output.cts: escape backslashes before pipe-escaping desc/safeName so the table-cell escape is complete (js/incomplete-sanitization). - tests/{bug-2643,bug-2808,docs-parity-live-registry}: two-pass HTML-comment strip so a bare/unclosed `<!--` cannot survive (js/incomplete-multi-character-sanitization). - tests/inline-plan-threshold: drop the no-op `\s`->`\s` identity replace, keep the meaningful POSIX-class conversion (js/identity-replacement). Verified: build:lib green; the touched test files + ci-test-scope + profile-output suites pass; lint:legacy-name clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): correctly resolve remaining CodeQL alerts (regex-injection + sanitization) The prior commit's fixes for two alerts were ineffective: - ci-test-scope.cjs js/regex-injection: the alert is the CLI-arg-derived `file` reaching static regex `.test(file)` calls (not the config rule). Removed ALL regex over file/t — startsWith/includes/=== string checks + an isWindowsHint helper — so there is no regex sink for the tainted value. - js/incomplete-multi-character-sanitization (3 test files): a single `.replace(/<!--...-->/g,'')` can let `<!--` re-form. Replaced with a fixpoint loop (replace until stable) plus a final bare-opener strip. Verified: no regex over file/t remains; ci-test-scope + the 3 test suites pass; lint:legacy-name clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): make ci-test-scope + comment-strippers regex-free to clear CodeQL CodeQL flags the regex PATTERNS syntactically (regex-injection on the --files arg split; incomplete-multi-character-sanitization on the <!--...--> replace), so loop fixes do not satisfy it. Made these paths regex-free: - ci-test-scope.cjs splitFiles: char-by-char separator tokenizer (no /[,\\s]+/). - 3 test files: indexOf/slice HTML-comment stripper (no .replace(/<!--/)). Behavior preserved; ci-test-scope + the 3 suites pass; guard clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): unblock security base64 scan on the large rename diff The security job hit its 10m timeout: base64-scan.sh choked on the binary test fixture tests/feat-3594-parser-property-style.test.cjs (embedded NUL/ non-UTF8 bytes -> thousands of bogus blobs + "ignored null byte" warnings), and the ~800-file rename diff is slow to scan regardless. - scripts/base64-scan.sh: skip binary-by-content files (grep -Iq .) — they can't carry base64-obfuscated *text* and feeding NUL bytes through the per-line scanner is pathologically slow. collect_files already filtered binary *extensions*; this catches binary *content* in text extensions. - .github/workflows/security-scan.yml: raise the security job timeout 10m->30m to accommodate very large diffs (the scan itself is unchanged). Verified locally: scan skips the fixture, 0 "ignored null byte" warnings, 0 findings, exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): sweep get-shit-done refs introduced by merging next The branch was updated with next (#614/#384/#618 etc.), which reference the get-shit-done/ dir (still named that on next). Swept the stale references in the merged files to gsd-core so the rename stays consistent and lint:legacy-name passes: - commands/gsd/discuss-phase.md (runtime-launcher shim paths) - src/core.cts (getAgentsDir layout comments) - tests/bug-384-agents-runtime-aware.test.cjs (require path to runtime lib) Verified: guard 0 violations; build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): exclude gsd-core/ path segments from bug-3683 command cross-ref invariant The #614 runtime-launcher shim added to discuss-phase.md references `${_GSD_RUNTIME_ROOT}/gsd-core/bin/...`. bug-3683's REF_PATTERN excluded path-y refs only via lookbehind, but `}` precedes `/gsd-core/` in the shim, so it mis-read the directory path as a dangling `/gsd-core` command ref (same class as the #604 bug-2954 fix). Added a trailing `(?![\w-]*\/)` so `/gsd-<x>/...` path segments are not treated as slash-command references. Verified locally on BOTH platforms before pushing: - mac (node 26) full suite: 0 failures - gsd-test-runner (linux, node22 image) full suite: 0 failures - bug-3683 + bug-2954 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): lazily resolve findProjectRoot in gsd-tools (harden flaky CI) CI intermittently failed state.test's gsd-tools subprocess with "findProjectRoot is not a function" (flip-flopping across legs; not reproducible on mac full suite, gsd-test linux full suite, test:unit, or state.test x8). findProjectRoot is a re-export from core.cjs (sourced from project-root.cjs); binding it via destructure at module-load can be undefined under a load-ordering edge. Resolve it lazily at call time via a small wrapper so the lookup happens after core.cjs is fully initialized. Verified green on BOTH platforms before pushing: - mac (node 26) full suite: 0 failures - gsd-test-runner (linux, node22) full suite: 0 failures - state.test.cjs: 106/106; gsd-tools loads cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): allowlist verification-patterns.md placeholder examples in secret scan The rename git-mv'd references/verification-patterns.md into gsd-core/, pulling it into the secret-scan diff. It documents stub/placeholder RED-FLAG env-var examples (illustrative Stripe test-key / database-URL / API-key placeholders) — not real credentials. Added it to .secretscanignore with the strict annotation, mirroring the existing gsd-core/workflows/plan-phase.md exception. Verified locally: secret-scan-lint --strict OK; secret-scan --diff origin/next exits 0 with 0 findings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
23 KiB
PRD: CJS↔SDK hard seam — Shared-Module migration
- Status: Superseded by ADR-0174 (2026-05-23) — historical migration plan; the CJS↔SDK seam and its hand-sync tooling were retired with the
@opengsd/gsd-sdkpackage boundary - Date: 2026-05-14
- Tracking issue: #3524
- Related ADR:
docs/adr/3524-cjs-sdk-hard-seam.md
Why this PRD exists
The ADR defines the target architecture — one source of truth per Shared Module, reusing the existing command-aliases.generated.* precedent. This PRD defines how to get there without breaking the running system. The migration is sequenced so the smallest, lowest-risk Shared Module ships first as a working proof of the pattern. Subsequent phases apply the same pattern to higher-stakes Modules. Each phase is independently shippable and independently reversible.
Problem statement
The CJS↔SDK boundary in open-gsd/gsd-core is structurally permeable. Multiple Shared Modules — STATE.md Document Module, Workstream Inventory Module, and several others — exist today as hand-synced pairs of .cjs and .ts files with character-identical implementations. Constants (CONFIG_DEFAULTS, VALID_CONFIG_KEYS) are likewise defined twice. The boundary is policed only by:
- A naming-parity test (
tests/config-schema-sdk-parity.test.cjs) - Output-parity golden tests for read-only handlers (
sdk/src/golden/read-only-parity.integration.test.ts)
These catch some drift but miss:
- Structure drift under defaults (#3523: top-level
branching_strategyreturned as'none'by CJS,'phase'by SDK) - Warning/error-message drift (#3523: CJS warns falsely; SDK silently grafts)
- Mutation-path drift (each side tested separately; no cross-side mutation fixture)
- New-Module drift (a new constant added to one side and not the other is invisible)
Each of #1535, #1542, #2047/#2052, #2638/#2655, #2653/#2670, #2687/#2706, #2798/#2816, #3055/#3116, #3523 fits this shape.
The fix is mechanical: for every hand-synced pair, replace one side with a generated artifact derived from the other side as the source of truth, modeled on the existing sdk/scripts/gen-command-aliases.ts + sdk/scripts/check-command-aliases-fresh.mjs pattern.
Goals
- Eliminate the drift bug class. Concretely: zero new bugs with the
drift-recurrenceretroactive label in the four months following the seam landing. - One source of truth per Shared Module, enforced by per-Module freshness checks at PR time.
- Hand-synced pairs of
.cjs/.tsfiles become impossible to merge (lint gate). - No new build tooling. The existing generator pattern scales.
Non-goals
- Removing the CJS CLI.
gsd-toolscontinues to exist for shell-script back-compat. (Its dispatcher delegates to the SDK runtime bridge after Phase 5; the external CLI contract is unchanged.) - Migrating CJS-only Modules (graphify, gsd2-import, schema-detect, fallow-runner, intel, drift) to SDK handlers.
- Defining a Verify Module before the verify surface has a shared Interface. Verify-surface deepening is precondition work for a future enhancement.
Approach
The repo already has a working precedent for shared CJS/SDK Modules: sdk/scripts/gen-command-aliases.ts emits both sdk/src/query/command-aliases.generated.ts and gsd-core/bin/lib/command-aliases.generated.cjs from a single TypeScript source. sdk/scripts/check-command-aliases-fresh.mjs is the CI freshness gate that fails when either generated file drifts from the source. This PRD generalizes that pattern to every Shared Module.
For each Shared Module being migrated:
- Promote one side to the source of truth (the TS source, because it already carries types).
- Write
sdk/scripts/gen-<module>.tsthat emits both.generated.tsand.generated.cjs. - Write
sdk/scripts/check-<module>-fresh.mjsmodeled oncheck-command-aliases-fresh.mjs. - Replace the hand-authored CJS file with a thin re-export from the generated file.
- Wire the freshness check into CI.
- Once green for one release cycle, delete the now-unreferenced hand-authored content from history's view by removing dead re-exports.
A separate, standing CI lint (scripts/lint-shared-module-handsync.cjs, introduced in Phase 6) blocks any new hand-synced pair from being merged.
Phased plan
Phases are sized to ship in one to two PRs each. Each phase has its own GitHub issue, linked back to #3524, opened only after the previous phase ships.
Phase 1 — STATE.md Document Module (smallest possible proof)
Why first. bin/lib/state-document.cjs and sdk/src/state/index.ts are already a character-identical hand-synced pair of pure transforms (the file headers explicitly say "Pure transforms for STATE.md text. This module does not read the filesystem and does not own persistence or locking."). Deletion test passes on contact: one side can be deleted as soon as the other becomes the generated artifact. This is the safest possible first step and the canonical proof that the generator pattern works for executable logic, not just alias tables.
Scope:
- Promote
sdk/src/query/state-document.tstosdk/src/state/index.ts(implemented). - Write
sdk/scripts/gen-state-document.tsthat emitsgsd-core/bin/lib/state-document.generated.cjs(and optionally re-exports the TS form at its existing location). - Write
sdk/scripts/check-state-document-fresh.mjsmodeled oncheck-command-aliases-fresh.mjs. - Replace
bin/lib/state-document.cjscontent with a thin re-export fromstate-document.generated.cjs. Keep the existing filename so callers (e.g.workstream-inventory.cjs:16) don't need to update imports. - Wire
check-state-document-fresh.mjsinto CI alongsidecheck-command-aliases-fresh.mjs.
Acceptance criteria:
bin/lib/state-document.cjscontains only a re-export fromstate-document.generated.cjs.sdk/scripts/check-state-document-fresh.mjspasses in CI and fails when intentionally desynchronized.- All existing call sites (CJS:
state.cjs,workstream-inventory.cjs; SDK:state-mutation.ts,state-project-load.ts, others importingstate-document) work unchanged. - Existing STATE.md unit tests on both sides pass.
- CONTEXT.md "STATE.md Document Module" entry is amended (one sentence) to note the source-of-truth file path.
Rollback: Revert the branch. Re-importing the deleted CJS file content from git history restores the prior hand-synced shape. No external consumer is broken.
Phase 2 — Configuration Module (closes the #3523 class)
Why second. This is the Module that triggered the work. It is the highest-leverage drift surface and the test of whether the pattern scales from a pure-transform Module to a Module that consumes data manifests.
Scope:
- Add a Configuration Module entry to
CONTEXT.mdfirst. Definition: "Module owning config load, legacy-key normalization, defaults merge, and explicit on-disk migration for.planning/config.json." Interface and invariants per ADR §6. - Extract
CONFIG_DEFAULTS,VALID_CONFIG_KEYS,DYNAMIC_KEY_PATTERNS,RUNTIME_STATE_KEYSto two data manifests:sdk/shared/config-schema.manifest.jsonandsdk/shared/config-defaults.manifest.json. Precedent:sdk/shared/model-catalog.json. - Write the Configuration Module source at
sdk/src/config/index.ts. Implementation imports the two manifests and exportsloadConfig,normalizeLegacyKeys,mergeDefaults,migrateOnDisk. - Write
sdk/scripts/gen-configuration.tsto emitgsd-core/bin/lib/configuration.generated.cjsand (if needed)sdk/src/query/config-schema.generated.ts. - Write
sdk/scripts/check-configuration-fresh.mjs. - Replace the inline implementations in
bin/lib/core.cjs:loadConfig(lines 220–243, 434–449, 485) andbin/lib/config.cjs(the validation surface) with thin Adapters over the generated Module. Delete the inlineCONFIG_DEFAULTS, the false-positive warning atcore.cjs:444-449, and the duplicated_deepMergeConfig. - Replace
sdk/src/config.ts:mergeDefaults(lines 192–218) with a re-export from the new Module. - Extend
sdk/src/golden/read-only-parity.integration.test.tswith a fixture matrix for the four legacy-key normalizations: top-levelbranching_strategy, top-levelsub_repos,multiRepo: true, top-leveldepth.
Acceptance criteria:
CONTEXT.mdcontains a Configuration Module entry with the Interface contract.bin/lib/core.cjsandbin/lib/config.cjscontain no localCONFIG_DEFAULTSorVALID_CONFIG_KEYSliterals; both load from the manifests via the generated Module.- Bug #3523 fixture matrix passes on both CJS and SDK paths; the false-positive warning at the old
core.cjs:444-449site is gone. - Golden parity matrix green for all four legacy-key shapes.
- Bug #3523 closed with a back-reference to this phase.
Rollback: Revert the branch; inline implementations restore from git history. The manifest files remain unreferenced.
Phase 3 — Workstream Inventory Builder + remaining hand-synced pairs
Why third. Phase 1 proves the pattern for pure transforms. Phase 2 proves it for data-manifest-backed logic. Phase 3 generalizes across the remaining hand-synced pairs surfaced by the audit. The Workstream Inventory Module is the headline because it requires the Builder/Reader split — the projection logic is pure and shareable, but the directory traversal is legitimately sync (CJS) vs async (SDK). This is the pattern for every paired Module with mixed pure-and-I/O concerns.
Scope:
- Write the Workstream Inventory Builder source at
sdk/src/workstream/builder.ts. Pure function: takes a list of directory entries plus per-workstream STATE.md text plus plan-scan results and returns the typedWorkstreamPhaseInventory/WorkstreamInventoryprojection. No fs reads. - Write
sdk/scripts/gen-workstream-inventory-builder.tsto emitgsd-core/bin/lib/workstream-inventory-builder.generated.cjsandsdk/src/query/workstream-inventory-builder.generated.ts. - Write
sdk/scripts/check-workstream-inventory-builder-fresh.mjs. - Refactor
bin/lib/workstream-inventory.cjsto a sync Reader Adapter: doesfs.readdirSync+readFileSyncof STATE.md, calls the Builder. The projection logic is removed. - Refactor
sdk/src/query/workstream-inventory.tsto an async Reader Adapter: same shape, async I/O, calls the Builder. - Amend the
CONTEXT.md"Workstream Inventory Module" entry with a sub-paragraph documenting the Builder/Reader split. - Audit remaining likely pairs (
frontmatter.cjs↔frontmatter-mutation.ts,plan-scan.cjs↔plan-scanSDK equivalents) for pure-transform sharability. For each confirmed-shareable pair, apply the same Builder pattern in this phase. For pairs whose duplication is structural (e.g. routing tables, sync vs async with different return shapes), document the decision in the phase issue and defer.
Acceptance criteria:
bin/lib/workstream-inventory.cjsandsdk/src/query/workstream-inventory.tsno longer share projection logic; both call the generated Builder.- CONTEXT.md "Workstream Inventory Module" entry reflects the split.
- Workstream-related golden tests pass on both sides.
- Every additional Module in scope has its own freshness check.
- Each Module not migrated in this phase has a one-paragraph deferral note (in the phase issue, not in the ADR).
Phase 4 — Project-Root Resolution Module
Scope:
- Add a Project-Root Resolution Module entry to
CONTEXT.md. Interface:findProjectRoot(startDir),findEffectiveRoot(startDir, options). - Source at
sdk/src/project-root/index.ts. Pure function: takes a path and an injected fs probe (or just usesnode:fssince both runtimes have it synchronously). - Generator at
sdk/scripts/gen-project-root.ts. - Freshness check at
sdk/scripts/check-project-root-fresh.mjs. - Replace
bin/lib/core.cjs:74-140with a thin Adapter over the generated Module. - Replace
sdk/src/helpers.ts:497-630with a thin Adapter over the same Module. - Extend parity tests for: standalone project, monorepo with
planning.sub_repos, legacymultiRepo: true, deep nesting.
Acceptance criteria:
findProjectRootis defined exactly once in source form.- Both sides import the generated Module.
- Parity tests pass for the four configurations above.
Phase 5 — CJS Command Router Adapter: delegate to the SDK runtime bridge
Why fifth. Phases 1–4 collapse drift in shared logic. Phase 5 collapses drift in parallel logic — the per-side state/verify/init/phase/roadmap/validate handler implementations on the CJS side. After Phase 5, every canonical command running via gsd-tools executes the same SDK handler that gsd-sdk query executes, in-process, with no subprocess hop. The seam becomes a real wall.
Scope:
- Amend the existing
CJS Command Router Adapter ModuleCONTEXT.md entry to document runtime-bridge delegation. - Expose a synchronous-friendly entry on
QueryRuntimeBridgefor CJS callers. TodayQueryRuntimeBridge.execute()is async; the bridge gains aexecuteForCjs(input) → { exitCode, stdoutChunks, stderrLines }synchronous wrapper that runs the dispatch underdeasyncor a controlledrunUntilsemantic. (Toolchain choice resolved in the Phase 5 issue; if synchronous bridging is not viable, fall back toAtomics.waiton a worker channel — nevergsd-sdksubprocess.) - Replace each canonical-family
handlersmap inbin/lib/*-command-router.cjswith a generated delegate emitter that, per subcommand, callsexecuteForCjs({ canonical, argv, env, cwd })and writes the result through the existing CJS output Adapter. - For each canonical command family in order —
state.*,verify.*,phase.*,phases.*,validate.*,roadmap.*,init.*,frontmatter.*,config.*, plus the non-family commands listed insdk/src/query/command-manifest.non-family.ts— migrate one family per sub-PR. Run the golden parity matrix per family before merging. - Delete CJS-side handler files (or shrink to delegates) for each migrated family:
state.cjs,verify.cjs,init.cjs,phase.cjs,phases.cjs,validate.cjs,roadmap.cjs,milestone.cjs,frontmatter.cjs,config.cjswrite paths, plan-scan handlers, etc. The pure-transform Shared Modules from Phases 1–4 remain untouched; only the per-family handler entry points are replaced. - CJS-only Module handlers (
graphify,gsd2-import,schema-detect,fallow-runner,intel,drift,installer-migrations) keep their in-process CJS implementations. They are not in the canonical family registry and do not route through the SDK runtime bridge. - Extend
sdk/src/golden/golden.integration.test.tsto verify identical exit code + stdout chunks + stderr lines betweengsd-tools <family> <subcommand>(now delegated) andgsd-sdk query <canonical>for every canonical command in the manifest.
Acceptance criteria:
- CONTEXT.md "CJS Command Router Adapter Module" entry documents runtime-bridge delegation.
QueryRuntimeBridge.executeForCjs(or equivalent) ships with the synchronous semantics resolved in the phase issue.- Every canonical command family in
command-manifest.*.tsroutes viaexecuteForCjs. CJS-only commands continue to route via the existing CJS handler. - Each per-family CJS handler file (
state.cjs,verify.cjs, …) contains no command-specific logic — only the delegate wiring or has been deleted entirely. - Golden parity matrix verifies output equivalence across
gsd-toolsandgsd-sdkfor every canonical command. No regressions in workflow markdown that callsgsd-tools. - Subprocess overhead per
gsd-toolsinvocation does not increase (the bridge is in-process, not agsd-sdksubprocess).
Rollback (per family): Each family's PR is independently revertible. The CJS handler files for an un-migrated family remain on disk in git history; if a family's delegation regresses, revert that family's PR and the CJS-side handler is restored.
Out-of-scope under Phase 5: The CJS-only Modules (graphify, gsd2-import, etc.) and workflow markdown that calls them — those calls continue to hit the in-process CJS handler, no change. Migrating CJS-only Modules to SDK is a separate enhancement.
Phase 6 — Enforcement hardening + retrospective
Scope:
- Write
scripts/lint-shared-module-handsync.cjs. Greps for any pair of files atgsd-core/bin/lib/<name>.cjsandsdk/src/query/<name>.ts(orsdk/src/<name>.ts) where neither file matches*.generated.*and the pair is not on an explicit allow-list. Allow-list documents the cooperating-sibling exceptions (e.g. routing files where the implementations are structurally different). - Verify each Shared Module from Phases 1–4 has its own freshness check wired to CI.
- Verify Phase 5's golden parity matrix covers every canonical command family.
- Add CODEOWNERS rules for
sdk/src/<module>/**for each Shared Module source-of-truth directory, forsdk/shared/*.manifest.json, and forsdk/src/query-runtime-bridge.ts(the Phase 5 boundary). Architecture-team review required. - Retrospectively walk the recurring-bug list (#1535 ... #3523). For each, document in
docs/agents/cjs-sdk-seam.mdwhich enforcement layer (handsync lint, freshness check, manifest data isolation, per-Module drift lint, runtime-bridge delegation) would have blocked it. - Write
docs/agents/cjs-sdk-seam.mdas a CONTRIBUTING-linked guide for adding a new Shared Module and for adding a new canonical command.
Acceptance criteria:
lint-shared-module-handsync.cjsruns in CI; demonstrated to block an intentional regression PR.- Every Shared Module from Phases 1–4 appears in a freshness-check workflow step.
- Phase 5's golden parity matrix is in CI on every PR that touches
bin/lib/*orsdk/src/query/*. - CODEOWNERS rules in place.
- Retrospective document committed.
- No PR can land that re-introduces the #3523 anti-pattern or that bypasses the runtime-bridge delegation for a canonical command.
Cross-phase concerns
Backwards compatibility
The CJS public CLI surface (gsd-tools <subcommand>) does not change. Flags, exit codes, stdout shapes preserved. Every phase replaces internal implementations behind the existing Module Interfaces; the external contracts are pinned by the existing golden parity suite plus the new fixture matrices.
Performance
No subprocess overhead anywhere. The generated .cjs files are require-able CommonJS modules; the SDK consumes the TS source directly. Module load cost adds ≤ 10 ms per require across all phases combined.
Phase 5 specifically preserves the in-process model: QueryRuntimeBridge.executeForCjs runs the SDK handler in the same Node process as the CJS dispatcher. No gsd-sdk subprocess is invoked. Synchronous bridging adds at most a handful of microseconds per call vs the previous direct CJS handler invocation, dominated by the existing dispatch policy overhead.
Build/install pipeline impact
- Each generator runs at build time on the developer machine (and in CI for the freshness check). No runtime generator execution.
- The published
@opengsd/gsd-corepackage already includes bothgsd-core/bin/andsdk/dist/. The generated.cjsfiles are committed to the repo (likecommand-aliases.generated.cjstoday), so the install flow is unchanged — no on-install code generation. npm run build:sdkcontinues to do what it does. Generators are invoked vianpm run gen:<module>per the existing precedent.
Risks
| Risk | Likelihood | Mitigation |
|---|---|---|
| Generator output drifts from source between commits | Medium | check-<module>-fresh.mjs per Module catches this at PR time. Precedent already in use for command-aliases. |
| A Shared Module's TS source uses features not expressible in CommonJS output | Low | Generator emits a CJS-compatible subset (no ESM-only syntax in source). Existing gen-command-aliases.ts template covers this. |
Phase 2's removal of inline _deepMergeConfig changes a subtle merge semantic |
Medium | Golden parity matrix is the test. If _deepMergeConfig and the new Module disagree on a fixture, the matrix fails and the new Module is amended before merge. |
migrateOnDisk rollout silently changes user-visible behavior on upgrade |
Medium | migrateOnDisk is explicit and opt-in; installer calls it once on next upgrade, with a release-note entry. Standalone command gsd-tools migrate-config for manual invocation. |
| CODEOWNERS rule slows down architecture-team responsiveness | Medium | Apply CODEOWNERS only to source-of-truth directories and manifests. Adapters and .generated.* files remain open. Architecture team commits to a ≤ 24 h SLA. |
| Phase 3's audit surfaces more pairs than expected, scope creeps | Medium | Each non-Phase-1/2 Module is scope-checked in its phase issue. Pairs that don't fit cleanly are deferred with a documented reason. |
Phase 5's synchronous-bridging mechanism (executeForCjs) has no clean shape — deasync is C++-bound, Atomics.wait requires a Worker, refactoring every SDK handler to be sync is huge |
High | Phase 5 spike resolves this before any family migration. If no clean mechanism exists, Phase 5 is descoped to the families whose SDK handlers are already synchronous, and the remainder shift to a follow-up enhancement. |
| Phase 5 family migrations regress observable CJS output (exit codes, stdout/stderr shape) | Medium | Golden parity matrix per family is the gate. A family's PR cannot merge until the matrix is green across every canonical command in that family. |
| Phase 5 changes startup time because the SDK runtime bridge eagerly loads more handlers than the previous CJS routers | Low | Lazy-load handlers behind the bridge (already the SDK's model). Measure time gsd-tools state load before/after migration; fail the family PR if median latency regresses >20 ms. |
Open questions (resolved before the phase that depends on them)
- Phase 1 source location — resolved to
sdk/src/state/index.ts(migrated fromsdk/src/query/state-document.ts). - Phase 2 manifest format — JSON vs JSONC vs TypeScript-as-source. Decided in Phase 2. JSON wins unless we need comments for invariants documentation.
- Phase 3 sibling-Module audit — exact list of pairs that get Builder-split vs deferred. Decided as a deliverable of Phase 3's spike.
- Phase 5 synchronous-bridging mechanism —
executeForCjsimplementation strategy:deasyncnative module (battle-tested but C++ binding),Atomics.waiton a worker channel (zero-binding but spins a Worker), or refactor every async SDK handler to expose a sync entry point (cleanest but largest scope). Decided in the Phase 5 spike issue before any family migration begins. - Phase 5 family migration order — which canonical family migrates first. Recommended order: smallest read-only family first (likely
frontmatter.*orconfig.* read paths) as the proof of pattern, then state/verify/phase/roadmap/validate/init in increasing complexity. Decided in the Phase 5 issue. - Phase 6 retrospective format — table vs prose. Decided when the retrospective document is drafted.
Done when
#3524 is closed when all six phases have shipped, each with its own merged PR closing its own phase issue, and the Phase 6 retrospective confirms every historical drift bug from the recurring list would have been blocked by one of the five enforcement layers (handsync lint, freshness check, manifest data isolation, per-Module drift lint, runtime-bridge delegation).