* fix: use active-workstream resolver exported by store module
* fix: wire verify codebase-drift alias and sync inventory docs
* chore: add changeset for next gate regression fixes
* fix: normalize changeset fragment metadata for docs-lint
* test(#178): update DispatchEvent factory tests to propagate parentTraceId
P1.3 test 'parentTraceId is always undefined' replaced with four P1.4
contracts: absent → undefined, string → propagated, null → undefined,
non-string → undefined (defensive normalization policy).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#178): propagate parentTraceId through DispatchEvent factory
Stop ignoring the parentTraceId parameter added as a forward-compat hook
in P1.3. Defensive normalization: only non-null strings are propagated;
null, non-string values, and absent callers all yield undefined, keeping
P1.3 behavior intact for all existing dispatch call sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): add Hub-level parentTraceId propagation tests
Four new assertions: req.parentTraceId propagates to event, absent →
undefined (P1.3 regression), shared parentTraceId across multiple
dispatches, and unique traceId invariant despite shared parentTraceId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#178): plumb parentTraceId through Hub dispatch and _notifyLogger
dispatch() now reads req.parentTraceId and passes it to _notifyLogger,
which forwards it to makeDispatchEvent. Backward-compatible: callers
that omit parentTraceId emit events with parentTraceId: undefined,
identical to P1.3 behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): add trace correlation end-to-end test
Dispatches a root command then 3 children with parentTraceId=rootTraceId.
Reads the real .gsd-trace.jsonl audit file and verifies: 4 events total,
root has no parentTraceId, all children carry rootTraceId, all traceIds
unique, JS filter returns exactly the 3 children given the root's traceId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(#178): document traceId/parentTraceId in audit file
Update Observability section to note that audit events now carry both
traceId and parentTraceId, and explain the correlation filter pattern.
Note that leaf dispatches emit parentTraceId: undefined until the Phase 2
composer wires it automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(#178): add changeset for trace correlation seam
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): cover invalid parentTraceId values in DispatchEvent factory
Adds 9 new test cases for UUID v4 validation of parentTraceId:
empty string, whitespace, non-UUID, oversized, UUID v1, missing-hyphen,
extra-char (all dropped to undefined), plus UPPERCASE and lowercase v4
(both propagated). Tests are intentionally red until the implementation
commit that follows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#178): validate parentTraceId against UUID v4 before propagation
Adds UUID_V4_REGEX constant and isValidParentTraceId() helper to
event.cjs. makeDispatchEvent now silently coerces any parentTraceId that
fails the UUID v4 check (wrong version nibble, wrong variant, missing
hyphens, oversized, empty, etc.) to undefined. No stderr warn is emitted
— the factory remains pure and side-effect-free. Closes the correlation-
poisoning vector identified in the Codex adversarial review of PR #225.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): assert Hub silently drops invalid parentTraceId at the seam
Adds two tests to hub-logger-integration.test.cjs:
1. dispatch with 'junk' parentTraceId emits event with parentTraceId===undefined.
2. The logger-failure warn path is NOT triggered — the factory coerces the bad
value before onEvent is called, confirmed by zero stderr output even when a
logger that would throw on non-undefined parentTraceId is installed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): assert invalid parentTraceId does not poison correlation siblings
Adds one test to trace-correlation.test.cjs: dispatches a root, a valid
child (parentTraceId = rootTraceId), and an invalid child (parentTraceId =
'junk'). Asserts: valid child carries correct parentTraceId, invalid child
has parentTraceId dropped to undefined, filtering by rootTraceId yields
exactly 1 event (the valid child only), and all 3 events have unique
traceIds. Uses an isolated Hub + tmpdir to avoid shared fixture interference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(#178): document UUID v4 contract for parentTraceId
Appends one sentence to the Observability audit-trail paragraph in
CONFIGURATION.md: parentTraceId must be canonical UUID v4 (RFC 4122);
values that don't match are silently dropped from audit output. No section
restructuring — single sentence addition only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#177): add DispatchEvent factory failing tests
Red tests for makeDispatchEvent shape, traceId UUID v4, uniqueness,
parentTraceId-always-undefined (P1.3), args redaction toggle, ISO 8601
timestamp, and all result variant passthrough.
* feat(#177): introduce DispatchEvent factory
makeDispatchEvent produces an immutable event record per dispatch:
- traceId: crypto.randomUUID() (UUID v4)
- parentTraceId: always undefined (P1.4 wires composer)
- command, result, timestamp (ISO 8601)
- args only included when includeArgs === true (default: omitted)
* test(#177): add arg redaction policy failing tests
Red tests for shouldIncludeArgs (GSD_AUDIT_ARGS env gating) and
redactEvent (strips args from frozen events, preserves all other
fields, returns a new object, never mutates the source).
* feat(#177): introduce arg redaction policy
shouldIncludeArgs(): only GSD_AUDIT_ARGS==='1' opts in; all other
values (unset, '', '0', 'true') default to omitting args.
redactEvent(event): returns a shallow copy of the event, dropping the
args field unless opted in. Never mutates the (frozen) source event.
* test(#177): add DispatchLogger interface failing tests
Red tests covering:
- no-op logger: silent on all events, never throws
- default logger: silent on ok, one flattened JSON line to stderr on error
- default logger: audit file creation + append-only + redaction + config gate
- GSD_AUDIT env var and config.audit.enabled config gate
- GSD_AUDIT_ARGS opt-in for args inclusion
All tests use real fs under os.tmpdir() — no mocked appendFileSync.
* feat(#177): introduce DispatchLogger with default and no-op implementations
createNoOpLogger(): silent on all events — Hub default when no logger injected.
createDefaultLogger({ cwd, config }):
- Silent on ok result
- Flattened JSON line to stderr on error: { kind, traceId, ...typedPayload }
- Append-only audit at .planning/.gsd-trace.jsonl when GSD_AUDIT=1 or config.audit.enabled
- Args redacted by default; GSD_AUDIT_ARGS=1 opts in
- Logger errors caught internally; never break dispatch callers
* test(#177): add Hub+logger integration failing tests
Red tests verifying:
- onEvent called exactly once per dispatch (ok, error, handler-throw, unknown)
- DispatchEvent shape: traceId uniqueness, command, result.kind, parentTraceId
- Logger errors contained (dispatch still returns Result, warn line to stderr)
- Hub defaults to no-op when no logger injected
- End-to-end with createDefaultLogger: silent on success, stderr on error, audit file
* feat(#177): wire DispatchLogger into CommandRoutingHub
Add optional logger param to createHub({ ..., logger }).
Defaults to createNoOpLogger() — silent, no behaviour change for callers
that don't inject a logger.
After every dispatch (success and error):
- Normalises HubResult { ok } to DispatchEvent { kind: 'ok'|error-kind }
- Calls makeDispatchEvent({ command, args, result }) to mint the event
- Calls logger.onEvent(event) exactly once
- Wraps in try/catch: logger errors emit { level:'warn', source:'DispatchLogger' }
to stderr but never propagate to dispatch callers
* chore(#177): gitignore .planning/.gsd-trace.jsonl audit file
The audit trail is local-only, append-only, and must never be committed.
Slotted under the existing "Local scratch + Claude-test artifacts" block.
* docs(#177): document GSD_AUDIT, GSD_AUDIT_ARGS, config.audit.enabled
New ## Observability section at end of CONFIGURATION.md covering:
- Default silent/stderr behaviour overview
- Stderr error JSON format
- Audit file opt-in (env var and config key)
- Args redaction policy and GSD_AUDIT_ARGS opt-in
Also slots GSD_AUDIT and GSD_AUDIT_ARGS into the existing
## Environment Variables table (alphabetical order).
* chore(#177): add changeset for observability seam
type: Added — new DispatchLogger seam with default silent/stderr/audit behaviour.
* fix(#167): support query meta-command in gsd-tools
* chore(#167): add changeset for query meta-command fix
* fix(#167): pin claude runtime in local-agent regression tests
* test(#3751): stabilize local-agent CI assertions
* refactor(hub): tighten Result<T> to typed-payload-per-kind discriminated union (#176)
Each Hub error variant now carries only its own typed payload. The generic
`errorKind` field is renamed to `kind`; `message`/`details` escape hatches
are removed from Hub-emitted errors. Factory functions (makeUnknownCommand,
makeInvalidArgs, makeHandlerRefusal, makeHandlerFailure) are exported and
used in phase-command-router.cjs. Callers switch on `result.kind`.
Part of ADR-0174 P1.2.
<!-- docs-exempt: no docs/ changes; API is internal to Hub callers -->
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(hub): act on P1.2 review findings (#176)
Addresses 4 review findings on PR #221:
- Hub now runtime-validates ok:false variants against the typed shape
and coerces malformed returns to HandlerFailure with a contract-
violation message (codex finding #1, code-review finding #1)
- catch path now preserves the original throwable for non-Error
throws via an Error wrapper with .thrown attached (codex finding #2)
- All 4 factory returns are Object.freeze'd (review finding #9)
- makeHandlerFailure validates cause is Error; non-Error causes are
wrapped with .thrown attached (review finding #10)
Tests added for each finding (TDD red → green).
Refs #176. Part of #174.
* fix(docs-lint): add docs-exempt markers to both P1.2 changeset fragments
Both `176-typed-result-discriminated-union.md` and `176-hub-p1.2-review-findings.md`
carry `type: Changed` which triggers the docs-required lint. Neither fragment had
a `<!-- docs-exempt: <reason> -->` marker, causing `docs-lint` to fail with
`FAIL_DOCS_MISSING`. Added the per-fragment exemption marker to both (the repo has
no `no-docs` label). This is a purely internal SDK refactor (ADR-0174 P1.2) with
no public docs surface.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(hub): drop mode/sdkLoader/SdkDispatchFailed (#175)
The Command Routing Hub no longer carries dual-runtime selection.
Removes `mode` and `sdkLoader` constructor parameters and the
`SdkDispatchFailed` and `SdkLoadFailed` errorKind values. The Hub
now routes exclusively through the CJS registry / handler resolution
path. ERROR_KINDS enum shrinks from 6 to 4 values.
phase-command-router.cjs updated to construct the Hub without the
removed params (removes tryLoadSdk, getExecuteForCjs, sdkLoader fn,
mode variable, and the post-dispatch SDK output branch). The 7
remaining family routers (init, phases, roadmap, state, validate,
verify, cjs-command-router-adapter) do not use createHub directly
and require no changes.
The CJS↔SDK bridge (bin/lib/cjs-sdk-bridge.cjs) is unchanged and
remains separately invokable; its removal is tracked in Phase 4 (#190).
ADR-0012 is no longer amended in this PR — the decision is captured
in ADR-0174 (which supersedes ADR-0012 entirely as part of the
SDK-retirement migration). Amending a superseded ADR would be
redundant noise.
Tests:
- Added assertions that Hub rejects/ignores `mode` and `sdkLoader`
- Removed obsolete mode-selection branching tests
- 57/57 local tests pass
Closes#175.
Part of #174 (ADR-0174).
* chore(changeset): add docs-exempt marker (#175)
P1.1's CommandRoutingHub work has no docs/ touchpoints — the
architectural decision is captured in ADR-0174 (merged via PR #198).
Per-phase ADR amendments would create noise; the SDK-retirement
migration's docs land in Phase 6 PRs (#193-#196) once the relevant
state is removed.
Adds the standard <!-- docs-exempt: <reason> --> marker inside the
changeset fragment so lint:docs accepts the PR without forcing a
docs/ touch that would be redundant.
Refs #175. Part of #174 (ADR-0174).
* test(4): reproduce non-idempotent phase complete + unclamped percent in CJS CLI
RED regression tests for issue #4:
- T1: double invocation of cmdPhaseComplete double-increments **Completed Phases:**
in STATE.md body (blind parseInt+1 instead of deriving from ROADMAP)
- T2: progress percent can exceed 100% when Completed Phases > Total Phases
The CJS path (bin/lib/phase.cjs:cmdPhaseComplete) has the bug; the SDK path
(phase-lifecycle.ts:phaseComplete, fixed in ~PR#3520) already derives
completed_phases from ROADMAP Complete-row count, making it idempotent.
References:
- Issue #4 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add sdk/scripts/gen-phase-lifecycle-policy.mjs generator + freshness check placeholder
Generates phase-lifecycle-policy.generated.cjs from sdk/src/query/phase-lifecycle-policy.ts.
All functions in phase-lifecycle-policy.ts are pure transforms (no I/O), directly
serializable via Function.prototype.toString(). The GSDError dependency is replaced
with a lightweight stub that throws plain Error objects — CJS callers that need
process.exit(1) behavior catch these and delegate to error().
This is the "I/O adapter pattern" from ADR-3524 Section 4 applied to pure helpers.
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add sdk/scripts/gen-phase.mjs generator
Generates phase.generated.cjs from sdk/src/query/phase.ts.
Only pure helpers (isCanonicalPlanFile, describeNonCanonicalPlans) are generated;
async query handlers (findPhase, phasePlanIndex) are I/O-bound and remain per-side
per ADR-3524 Section 4.
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add sdk/scripts/gen-phase-lifecycle.mjs + core idempotency fix logic
Generates phase-lifecycle.generated.cjs providing two pure functions that are the
root-cause fix for issue #4:
1. deriveProgressFromRoadmap(roadmapContent): counts Complete rows in ROADMAP
progress table — makes completed_phases idempotent (derived from ground truth
instead of blind +1). Direct transcription of the SDK's "Root cause 1 fix"
block in phase-lifecycle.ts (~line 1644).
2. clampPercent(completed, total): percent capped at 100 — prevents >100% progress
when Completed Phases exceeds Total Phases.
Design note: the full phase lifecycle mutations (add, insert, remove, complete) are
inherently async and I/O-bound. Per ADR-3524 Section 4 ("I/O stays per-side"), those
are NOT generated. Only the pure-computation kernel is extracted, following the
I/O adapter pattern: pure logic shared; each side (CJS sync, SDK async) supplies
its own I/O adapter.
The pure functions are defined in the generator as real JS functions and serialized
via Function.prototype.toString() — same technique as gen-project-root.mjs — rather
than embedded in template literals (which would require double-escaping all regex
backslashes).
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- /tmp/adr-3524-review-findings.md (architectural justification)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): emit phase.generated.cjs, phase-lifecycle.generated.cjs, phase-lifecycle-policy.generated.cjs
Three generated CJS artifacts from their respective generator scripts:
- phase.generated.cjs (1.7K): isCanonicalPlanFile + describeNonCanonicalPlans
from sdk/src/query/phase.ts
- phase-lifecycle.generated.cjs (3.6K): deriveProgressFromRoadmap + clampPercent
— the idempotency+clamp fix for issue #4
- phase-lifecycle-policy.generated.cjs (7.0K): 14 pure phase naming/directory
helpers from sdk/src/query/phase-lifecycle-policy.ts
Run to regenerate:
node sdk/scripts/gen-phase.mjs
node sdk/scripts/gen-phase-lifecycle.mjs
node sdk/scripts/gen-phase-lifecycle-policy.mjs
References:
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- Issue #4 (open-gsd/get-shit-done-redux)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(4): migrate bin/lib/phase.cjs cmdPhaseComplete to use generated helpers
Replace the blind-increment + unclamped percent bug in cmdPhaseComplete with
the idempotent ROADMAP-derived approach: read freshly-updated ROADMAP, call
deriveProgressFromRoadmap() from phase-lifecycle.generated.cjs, fall back to
existing value when ROADMAP is unavailable. clampPercent() prevents >100%.
Root cause fix for issue #4: the original parseInt(completedRaw) + 1 on every
call made phase complete non-idempotent; the missing Math.min(100, ...) clamp
allowed Progress to exceed 100%.
I/O adapter pattern (ADR-3524 §4): pure computation in generated module;
CJS supplies sync readFileSync; SDK supplies async readFile. Same logic, two adapters.
Closes: Tests in 4-phase-complete-cjs-regression.test.cjs go GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add freshness checks + npm scripts for phase generated artifacts (D4/D6)
Add check-phase-fresh.mjs, check-phase-lifecycle-fresh.mjs, and
check-phase-lifecycle-policy-fresh.mjs (same pattern as check-project-root-fresh.mjs:
import buildXCjs() from the generator, regenerate in-memory, byte-compare to committed
file, exit 1 if stale).
Add gen:phase, check:phase-fresh, gen:phase-lifecycle, check:phase-lifecycle-fresh,
gen:phase-lifecycle-policy, check:phase-lifecycle-policy-fresh to sdk/package.json.
Note: gen:phase-lifecycle / check:phase-lifecycle-fresh do not require 'npm run build'
because the generator defines pure functions directly rather than importing dist.
Update shared-module-handsync-allowlist.json: reclassify phase.cjs justification to
reflect that it now consumes phase-lifecycle.generated.cjs for cmdPhaseComplete. The
*.generated.cjs files are excluded by the lint scanner (excludes *.generated.cjs) so
no new allowlist entries are required for the generated artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(4): add freshness-check CI steps for phase, phase-lifecycle, phase-lifecycle-policy
Add three drift-check steps to .github/workflows/test.yml following the same
pattern as the existing freshness checks (ubuntu-latest + node 24 only):
- SDK generated phase artifact drift check
- SDK generated phase-lifecycle artifact drift check
- SDK generated phase-lifecycle-policy artifact drift check
These guard against editors modifying the generated *.cjs files directly.
They run check-phase-fresh.mjs, check-phase-lifecycle-fresh.mjs, and
check-phase-lifecycle-policy-fresh.mjs respectively (added in D4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(4): amend ADR-3524 — phase * I/O adapter pattern for issue #4 (D8)
Append a 2026-05-23 amendment to docs/adr/3524-cjs-sdk-hard-seam.md documenting
the Phase * cooperating-sibling retirement: three new generator scripts extract
pure-computation helpers from phase.ts / phase-lifecycle.ts / phase-lifecycle-policy.ts,
cmdPhaseComplete migrates to deriveProgressFromRoadmap + clampPercent for idempotency,
freshness checks + CI steps added.
Clarifies what is NOT generated (async I/O-bound mutation handlers stay per-side per
Section 4) and notes open drift bugs #6 and #26 for traceability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(4): add changeset fragment for cmdPhaseComplete fix
Refs #4
* fix(154): use canonical /gsd:plan-phase form in phase-lifecycle-policy.ts
Replaces the retired /gsd-plan-phase slash command reference with the
canonical colon-namespaced /gsd:plan-phase in the TS source template
string that feeds the generated CJS roadmap entry helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(154): regenerate phase-lifecycle-policy.generated.cjs after slash-namespace fix
Regenerated via node sdk/scripts/gen-phase-lifecycle-policy.mjs after
fixing /gsd-plan-phase → /gsd:plan-phase in the TS source. Generated
file now contains the canonical colon form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): cross-platform frontmatter regex anchor in 4-phase-complete-cjs-regression.test.cjs
Replaces /^---\n/ with /^---\r?\n/ so the frontmatter extraction helper
in the regression test tolerates Windows CRLF line endings (autocrlf=true
checkout leaves \r before \n, causing /^---\n/ to never match).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(154): add new generated CJS modules to INVENTORY.md and regenerate manifest
Adds three missing rows to the CLI Modules table:
- phase-lifecycle-policy.generated.cjs
- phase-lifecycle.generated.cjs
- phase.generated.cjs
Bumps the headline count from 74 to 77 to match the filesystem.
Also regenerates docs/INVENTORY-MANIFEST.json via
node scripts/gen-inventory-manifest.cjs --write.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): regenerate phase-lifecycle-policy.generated.cjs with hyphen form
Root cause: commit 6cd701f4 regenerated the CJS artifact but at that point
sdk/dist/query/phase-lifecycle-policy.js already had the correct /gsd-plan-phase
(hyphen) form while sdk/src/query/phase-lifecycle-policy.ts still had /gsd:plan-phase
(colon). The generator uses Function.prototype.toString() on the compiled dist, so
the CJS picked up the wrong string from the stale TS source that was compiled into
dist at some earlier point.
Fix: correct the TS source to /gsd-plan-phase and re-run gen-phase-lifecycle-policy.mjs
so that buildPhaseRoadmapEntry in the CJS emits the hyphen form, satisfying the
bug-3584-runtime-slash-emitters.test.cjs assertion at line 179.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): restore /gsd:plan-phase canonical form in phase-lifecycle-policy
Commit 0c3a9c75 incorrectly reverted the slash-namespace fix by misreading
sdk/dist/ (a build artifact in hyphen form for non-Claude runtimes) as the
authoritative source. The canonical form for Claude-facing source is
/gsd:plan-phase (colon-namespaced).
Fix: revert TS source back to /gsd:plan-phase, rebuild dist, regenerate
phase-lifecycle-policy.generated.cjs.
Fixes bug-2543-gsd-slash-namespace test failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): update INVENTORY.md CLI Modules count to 79 after rebase onto main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(154): use hyphen form /gsd-plan-phase in persisted phase section template
- sdk/src/query/phase-lifecycle-policy.ts: use /gsd-plan-phase (routable
hyphen form) in the phase scaffold template that gets persisted to
ROADMAP.md; bug-3584 requires persisted artifacts use the hyphen form
- docs/INVENTORY.md: add missing runtime-name-policy.cjs row in CLI
Modules table
- tests/4-phase-complete-cjs-regression.test.cjs: add maxRetries/retryDelay
to rmSync calls to satisfy Windows parity ratchet (baseline was 95)
- Regenerate phase-lifecycle-policy.generated.cjs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7): raise concurrency-safety roadmap-analyze budget 2000ms → 5000ms
The 50-phase roadmap-analyze wall-clock test consistently flaked on Mac
under realistic load (empirical floor ~2100-3000ms), independently
reproduced by three fixers on PRs #3795, #3792, #3799.
Extract the magic 2000 to named constant ROADMAP_ANALYZE_BUDGET_MS with
an explanatory comment. Raise to 5000ms (2.5x observed worst-case) —
large enough to eliminate Mac flakes without masking real regressions.
Long-term: convert to behavior-anchored assertion per PR #3803 pattern;
tracked as follow-up, out of scope here.
Acceptance criteria from #7:
> "Replace hardcoded 2000ms with higher budget (e.g., 5000ms) or make
> configurable; add comment documenting rationale; confirm no
> false-negative on healthy machine."
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(7): guard executeForCjs with try/catch fallback in all CJS routers
When the synckit bridge throws (worker crash, Atomics failure on
Windows/Node 24, or any other transient OS-level failure), the exception
propagated uncaught through the sdkHandler closure up to Node.js's
unhandled-rejection handler. On Windows, the async stderr write for the
rejection message may not flush before process exit, producing an empty-
stderr non-zero exit that manifests as 'init failed: Command failed: ...'
in workspace.test.cjs.
Wrap getExecuteForCjs() in a try/catch in every CJS router that has this
pattern (gsd-tools.cjs _dispatchNonFamily, init-, roadmap-, state-,
validate-, verify-command-router.cjs). On catch: fall through to the
CJS handler, which is the designed safety net for bridge failures and
produces identical output.
Verified: workspace.test.cjs (26/26), concurrency-safety.test.cjs
(31/31), cjs-sdk-bridge-integration.test.cjs (4/4), and
bug-3631-router-raw-flag.test.cjs (2/2) all pass locally on Mac.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(26): reproduce W005/W006-archived/I001 false positives in CJS validate path
Issue #26 (open-gsd/get-shit-done-redux): three validation drift items from
PR #3479 were hand-ported to verify.cjs via PR #3806 but never routed through
the generator pattern. This means they can drift again whenever validate.ts
changes.
RED tests assert that validate.generated.cjs exports four new items:
- phaseDirNameRe (W005 regex — /^\d{2,}(?:\.\d+)*-[\w-]+$/)
- MILESTONE_ARCHIVE_DIR_RE (W006-archived — /^v\d+.*-phases$/i)
- PHASE_TOKEN_FROM_DIR_RE (W006-archived — phase dir token extractor)
- canonicalPlanStem (I001 — plan/summary stem canonicalization)
Three of four new export tests are RED (exports missing from generated artifact).
Behavioral tests (W005 no-false-positive, W006-archived no-false-positive,
I001 no-false-positive) are GREEN because #3806's hand-ported fixes are present.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port).
* chore(26): extend gen-validate.mjs to export W005/W006-archived/I001 helpers
Issue #26 (open-gsd/get-shit-done-redux): extend gen-validate.mjs (introduced
in PR #156 / issue #6) to also extract the three drift items that PR #3806
hand-ported to verify.cjs but were never routed through the generator.
New helpers added to gen-validate.mjs:
phaseDirNameRe (PHASE_DIR_NAME_RE) — W005 phase directory naming regex.
/^\d{2,}(?:\.\d+)*-[\w-]+$/ accepts multi-digit prefixes (999.1-foo valid).
Requires adding PHASE_DIR_NAME_RE as a named constant to validate.ts so
it appears as an extractable identifier in the compiled output.
PHASE_TOKEN_FROM_DIR_RE — W006-archived regex; extracts phase token from
directory names like "64-auth-service" → "64". Used by
forEachArchivedPhaseToken() and collectDiskPhases() in verify.cjs.
MILESTONE_ARCHIVE_DIR_RE — W006-archived regex; matches milestone archive
directory names like "v1.0-phases". Used by listMilestoneArchiveDirs().
canonicalPlanStem() — I001 PLAN/SUMMARY stem canonicalization.
'68-01-scaffolding' → '68-01'. Top-level named function in compiled output.
Extraction approach: PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE are
module-level const assignments, extracted via extractConstRegExp() (handles both
`const` and `export const` prefixes). PHASE_DIR_NAME_RE is the new named export
added to validate.ts in this commit. canonicalPlanStem is a top-level function,
extracted via extractTopLevelFunction() (brace-balanced).
validate.ts change: inline regex in Check 6 extracted to named constant
PHASE_DIR_NAME_RE (exported) and Check 6 updated to reference it.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
* chore(26): regenerate validate.generated.cjs with W005/W006-archived/I001 helpers
Re-run of sdk/scripts/gen-validate.mjs after extending it in the preceding
commit. The artifact now exports seven items (was three):
New (issue #26):
phaseDirNameRe — /^\d{2,}(?:\.\d+)*-[\w-]+$/ (W005 check)
PHASE_TOKEN_FROM_DIR_RE — phase token extractor regex (W006-archived)
MILESTONE_ARCHIVE_DIR_RE — archive dir name matcher (W006-archived)
canonicalPlanStem() — PLAN/SUMMARY stem canonicalization (I001)
Existing (issue #6):
phaseVariants()
buildRoadmapPhaseVariants()
buildNotStartedPhaseVariants()
Freshness check: node sdk/scripts/check-validate-fresh.mjs → "fresh".
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
* fix(26): migrate verify.cjs W005/W006-archived/I001 call sites to generated helpers
Issue #26 (open-gsd/get-shit-done-redux): three hand-maintained items in
verify.cjs now consumed from validate.generated.cjs (ADR-3524 §4 adapter pattern).
Changes:
- Top-of-file require(): extend to also destructure phaseDirNameRe,
PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem
from validate.generated.cjs (issue #26 exports).
- Remove inline PHASE_TOKEN_FROM_DIR_RE and MILESTONE_ARCHIVE_DIR_RE constants
(lines ~403-404). Now sourced from generated artifact. listMilestoneArchiveDirs
and forEachArchivedPhaseToken pick them up via the require() at top of file.
- Check 6 (W005): replace inline regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ with
phaseDirNameRe from validate.generated.cjs. No behavior change.
- Remove inline canonicalPlanStem() function (~8 lines). Now sourced from
validate.generated.cjs. Check 7 (I001) continues to call it as before.
Public API of verify.cjs unchanged. Same migration shape as PR #156's Check 8.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port that #26 supersedes).
* docs(26): extend ADR-3524 2026-05-23 amendment with #26 scope
Extends the existing 2026-05-23 amendment (not a new dated section) to document
the W005/W006-archived/I001 generator migration introduced by issue #26.
Key points documented:
- Four new exports added to validate.generated.cjs (phaseDirNameRe,
PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem)
- W006-archived coverage note: both fixes were already in verify.cjs from
#3806; the gap was generator coverage of the regex constants
- Extraction methods: extractConstRegExp() and extractTopLevelFunction()
- Parity tests: tests/26-w005-w006-i001-cjs-drift-regression.test.cjs (7 tests)
- Cross-reference: issue #26 completes the validate.ts ↔ verify.cjs migration
scope started by issue #6
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6),
PR #3479 (original fix), PR #3806 (hand-port).
* chore(26): add changeset fragment for W005/W006-archived/I001 generator migration
Touches get-shit-done/bin/lib/validate.generated.cjs and verify.cjs which
match USER_FACING_PREFIXES. Required by the fix-template checklist + the
changeset-lint CI workflow.
References: issue #26, ADR-3524, PR #154 (issue #4), PR #156 (issue #6).
* test(6): reproduce W007 + phaseVariants + W006 drift between CJS verify and SDK validate
Adds tests/6-validate-cjs-drift-regression.test.cjs with 5 RED tests covering the
three drift items from issue #6 between verify.cjs (Check 8) and validate.ts (Check 8):
1. W007 activeDiskPhases — verify.cjs uses diskPhases (includes archived) for W007;
archived phase "1" absent from current ROADMAP fires false W007.
validate.ts: activeDiskPhases (active phasesDir only) correctly excludes archives.
2. phaseVariants() normalization — ROADMAP says "01A", disk has "1A-foo".
verify.cjs parseInt("01A")=1 → padded "01" (drops letter suffix) → miss.
validate.ts phaseVariants("01A") = {"01A","1A","01A"} → "1A" matched.
Both W006 and W007 fire as false positives in verify.cjs.
3. W006 letter-suffix padding mismatch — ROADMAP says "3B", disk has "03B-foo".
verify.cjs parseInt("3B")=3 → padded "03" (drops "B") → diskPhases.has("03B") missed.
W006 and W007 fire as false positives.
All 5 tests RED on origin/main. Will turn GREEN after generator + verify.cjs migration.
References:
- Issue #6 (open-gsd/get-shit-done-redux) — maintainer acceptance criteria:
"Port all three items to verify.cjs; add parity tests confirming identical output
for all three cases on both paths"
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- PR #154 (issue #4) — precedent for the generator pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(6): add sdk/scripts/gen-validate.mjs generator
Extracts phaseVariants() from sdk/dist/query/validate.js via brace-balanced
source-text parsing (phaseVariants is a closure inside validateHealth, not a
module export, so Function.prototype.toString() is unavailable).
Emits get-shit-done/bin/lib/validate.generated.cjs with three pure helpers:
- phaseVariants(phase): normalized Set of padded/unpadded/letter-suffix variants
- buildRoadmapPhaseVariants(content): {roadmapPhases, roadmapPhaseVariants}
- buildNotStartedPhaseVariants(content): Set of unchecked-phase variants
These three helpers directly address the three drift items in issue #6.
Follows the gen-phase-lifecycle-policy.mjs extraction pattern from PR #154.
References:
- Issue #6 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- PR #154 (issue #4) — generator pattern precedent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(6): add sdk/scripts/check-validate-fresh.mjs freshness check
Mirrors check-phase-lifecycle-policy-fresh.mjs from PR #154: imports
buildValidateCjs() directly, regenerates in-memory, and diffs against the
committed validate.generated.cjs. Exits 1 if stale (CI gate).
References:
- Issue #6 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- PR #154 (issue #4) — precedent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(6): emit validate.generated.cjs from validate.ts
Generated by: node sdk/scripts/gen-validate.mjs
Exports three pure helpers extracted from sdk/src/query/validate.ts Check 8:
- phaseVariants(phase): Set of normalized variants {"01A","1A"} etc.
- buildRoadmapPhaseVariants(content): {roadmapPhases, roadmapPhaseVariants}
- buildNotStartedPhaseVariants(content): Set of unchecked-phase variants
Freshness check: node sdk/scripts/check-validate-fresh.mjs → FRESH
References:
- Issue #6 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- PR #154 (issue #4)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(6): migrate verify.cjs to consume validate.generated.cjs helpers (GREEN)
Check 8 in verify.cjs now uses three generated helpers from validate.generated.cjs:
1. buildRoadmapPhaseVariants(roadmapContent) — replaces hand-rolled roadmapPhases
Set. Produces both roadmapPhases (raw, for W006 message) and roadmapPhaseVariants
(all variants, for W007 membership check). Fixes false W007 for letter-suffix
phases with padding mismatch.
2. activeDiskPhases — now uses collectDiskPhases() WITHOUT forEachArchivedPhaseToken.
W007 iterates activeDiskPhases, not diskPhases, so archived phases absent from
current ROADMAP no longer trigger false W007.
3. buildNotStartedPhaseVariants(roadmapContent) — replaces raw+parseInt-padded
notStartedPhases population. Uses phaseVariants() expansion so zero-padded
letter-suffix unchecked entries (e.g. "03B") correctly suppress W006 for
their un-padded counterpart ("3B") and vice versa.
4. phaseVariants() in W006 loop — replaces parseInt-padded disk-existence check.
"3B" now matches disk dir "03B-foo" via variant expansion.
Also updates test fixture for drift item 1 to use two milestone archives (v1.0 + v1.1),
accurately reproducing the scenario where forEachArchivedPhaseToken walks ALL archives
while getActiveMilestoneArchiveDir returns only the most recent one.
All 5 tests GREEN. Confirmed RED on pre-fix code (git stash test).
References:
- Issue #6 (open-gsd/get-shit-done-redux) — maintainer acceptance criteria:
"Port all three items to verify.cjs; add parity tests confirming identical output"
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- PR #154 (issue #4) — generator pattern precedent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci(6): wire validate freshness check into test workflow
Adds 'SDK generated validate artifact drift check' step to .github/workflows/test.yml,
mirroring the pattern used by all PR #154 generator freshness checks.
Runs on ubuntu-latest/node-24 only (same as other artifact drift checks).
Placement: after workstream-name-policy check, before Shared Module hand-sync drift check.
References:
- Issue #6 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- PR #154 (issue #4) — precedent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(6): wire gen:validate into sdk/package.json, root package.json, and allowlist
sdk/package.json: adds gen:validate and check:validate-fresh npm scripts.
package.json: adds check:validate-fresh script (mirrors other check:*-fresh entries).
scripts/shared-module-handsync-allowlist.json: updates verify.cjs justification to
note that Check 8 W006/W007 helpers are now generated from validate.ts via
gen-validate.mjs (issue #6), with freshness check at check-validate-fresh.mjs.
References:
- Issue #6 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(6): amend ADR-3524 — validate.ts now uses generator pattern
Adds 2026-05-23 amendment section to docs/adr/3524-cjs-sdk-hard-seam.md documenting:
- Generator/artifact/freshness-check/CI paths
- Three drift items resolved (W007 activeDiskPhases, phaseVariants normalization,
W006 unchecked-phase variant skip)
- phaseVariants extraction technique (brace-balanced source-text parsing)
- Parity test coverage (5 tests, RED→GREEN)
- Allowlist classification preserved (cooperating-sibling)
References:
- Issue #6 (open-gsd/get-shit-done-redux)
- ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
- PR #154 (issue #4)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(6): add changeset fragment for validate.ts/verify.cjs generator migration
Touches get-shit-done/bin/lib/validate.generated.cjs and verify.cjs which
match USER_FACING_PREFIXES. Required by the fix-template checklist + the
changeset-lint CI workflow.
Refs #6#156
* docs(6): register validate.generated.cjs in INVENTORY + manifest
INVENTORY parity test demanded a row for the new generated CJS surface
and a matching entry in INVENTORY-MANIFEST.json. Headline count bumped
from 74 → 75.
Refs #6
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(113): add per-rule failing tests + hostile fixture for markdown link payloads
RED phase for issue #113 — scanForInjection() currently returns { clean: true }
for markdown links containing javascript:, data:text/html, userinfo credentials,
and token-in-query payloads.
Changes:
- tests/fixtures/adversarial/security/context-malicious-markdown-link.md:
Extended to contain one hostile example per rule class (MD-LINK-JS-SCHEME,
MD-LINK-DATA-SCHEME, MD-LINK-USERINFO, MD-LINK-TOKEN-IN-QUERY) plus benign
negative controls (data:image/png, mailto:, https://github.com, port-only URL).
- tests/security-prompt-injection.test.cjs:
- Flipped PINNED "malicious-markdown-link fixture is NOT flagged" assertion
to "malicious-markdown-link fixture is flagged by scanner" (forward-looking).
- Added 4×positive + 4×negative per-rule unit tests asserting structuredFindings
with ruleId, file, line, match fields.
- Added parity guard: every MARKDOWN_LINK_PATTERNS source string from
security.cjs must appear in gsd-read-injection-scanner.js hook source.
D3 false-positive grep: 0 legitimate matches — no allowlist entries needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(113): detect javascript:/data:/userinfo/token-in-query in markdown links (security.cjs + hook)
GREEN phase for issue #113.
Rule details (all with primary source citations):
MD-LINK-JS-SCHEME
Flags ](javascript:...) regardless of case.
Source: OWASP XSS Prevention Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
MD-LINK-DATA-SCHEME
Flags data: URIs NOT in the explicit safe-list.
Safe-list: image/(png|jpeg|gif|webp|bmp|ico|avif|heic) and font/(woff2?|otf|ttf).
data:image/svg+xml is intentionally BLOCKED — SVG can host <script>.
Source: OWASP File Upload Cheat Sheet — SVG Files
https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html#svg-files
MD-LINK-USERINFO
Flags https?://user:pass@host in markdown link targets.
Does NOT fire on: mailto:user@host (no :// before user) or https://host:443/path (port, not userinfo).
Source: RFC 3986 §3.2.1 (userinfo syntax)
https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1
RFC 9110 §4.2.4 (HTTP deprecates userinfo)
https://www.rfc-editor.org/rfc/rfc9110#section-4.2.4
MD-LINK-TOKEN-IN-QUERY
Flags key NAMES: token, access_token, id_token, refresh_token, api_key, apikey,
secret, password, client_secret, code — regardless of value.
Source: RFC 9700 OAuth 2.0 Security BCP §4.3.1
https://www.rfc-editor.org/rfc/rfc9700#section-4.3.1
D3 false-positive grep: 0 legitimate matches in codebase — no allowlist needed.
Architecture:
- scripts/security.cjs: canonical MARKDOWN_LINK_PATTERNS export, scanForInjection()
extended with structuredFindings (ruleId, file, line, match) via opts.file.
- hooks/gsd-read-injection-scanner.js: patterns inlined for hook independence
(same pattern sources, verified by parity test).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(113): flip PINNED malicious-markdown-link assertion and add parity guard
REFACTOR phase — tightening test rigor after test-rigor skill review:
1. Fixture assertion now enumerates all 4 expected ruleIds explicitly:
[MD-LINK-JS-SCHEME, MD-LINK-DATA-SCHEME, MD-LINK-USERINFO, MD-LINK-TOKEN-IN-QUERY].
Previously findings.length > 0 would pass even if 3 of 4 rules were broken.
2. line field assertions tightened: `f.line >= 1` (meaningful lower bound for
1-based line numbers) instead of `typeof f.line === 'number'` (vacuous).
3. match field assertions tightened to check the hostile content is present:
- MD-LINK-JS-SCHEME: /javascript:/i in match
- MD-LINK-DATA-SCHEME: /data:/i in match
- MD-LINK-USERINFO: /@/ in match (the @ character is the definitive userinfo marker)
- MD-LINK-TOKEN-IN-QUERY: /token=/i in match
4. Parity test checks actual RegExp .source strings (not just lengths), verifying
the hook contains the exact canonical pattern sources character-for-character.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#113): add changeset fragment + Windows/Node 24 state.test compatibility
1. .changeset/113-malicious-markdown-links.md — required Security fragment
for the user-facing markdown-link scanner changes in this PR (changeset-lint
was failing with FAIL_MISSING_FRAGMENT).
2. get-shit-done/bin/lib/state-command-router.cjs — add OUTPUT_ON_SDK_ERROR
set for mutation state subcommands whose CJS contract is always exit-0.
On Windows/Node 24 the SDK bridge returns result.ok===false for validation
failures (e.g. state record-metric --phase 1 with no --plan/--duration),
causing dispatchViaSdk() to call error() (exit 1) instead of output({error})
(exit 0). The fix maps SDK non-ok results to JSON output for the affected
mutation commands (record-metric, advance-plan, record-session, add-decision,
add-blocker, resolve-blocker, update-progress), restoring the exit-0 CJS
contract on all platforms.
tests/state.test.cjs:1161 "returns error when required fields missing" passes
locally (104/104 pass).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3785): case-insensitive depends_on resolution in phase resolver
planMap, canonicalToId, and shortFormToId in phasePlanIndex used strict
Map.has() with no case normalization. A depends_on ref in mixed/lowercase
against an uppercase-suffix plan ID (e.g. '20-01-auth' → '20-01-Auth')
dropped the DAG edge, assigning the dependent plan to wave 1 instead of
wave 2. Fix: normalize all keys and lookup values to lowercase so the
three-tier resolution is case-insensitive. Adds regression test (#3785).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add PR 3798 changelog fragment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3785): detect case-fold collisions; apply lowercase-both to CJS path
- Add collision guard in both sdk/src/query/phase.ts (phasePlanIndex)
and get-shit-done/bin/lib/phase.cjs (cmdPhasePlanIndex): when two plan
IDs in the same phase are identical after toLowerCase(), throw/error
immediately with a clear message naming both files instead of silently
overwriting one in planMap and misrouting depends_on edges.
- Apply the same lowercase-both normalization (#3785) to cmdPhasePlanIndex
in phase.cjs, which was missing from the original PR — planMap and
canonicalToId keys are now lowercased on write; dep strings are
lowercased before lookup.
- Add regression tests to tests/phase.test.cjs: case-insensitive
resolution test (runs on all platforms) and collision-detection test
(skipped on macOS/Windows where FS is case-insensitive).
- Update changeset to describe both the SDK and CJS fixes.
Identified via Codex adversarial review of PR #3798.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3785): address review — KNOWN GAP comment for CJS shortFormToId, canonical-casing tests, depends_on output normalization
- F1: Reword changeset for accuracy (plannerID drift trigger; two-tier CJS gap honest).
Add KNOWN GAP comment in phase.cjs before Kahn's loop noting CJS lacks shortFormToId
(tracked as follow-up parity gap, out of scope for #3785).
- F2: Add strict planA.id === '20-01-Auth' assertions in both SDK (vitest) and CJS (node --test)
tests — a future regression that silently lowercases stored IDs would now fail the test.
- F3: Normalize depends_on output to canonical plan IDs in both SDK phase.ts and CJS phase.cjs.
User-typed '20-01-auth' in depends_on resolves to '20-01-Auth' in output via planMap lookup.
Add planB.depends_on === ['20-01-Auth'] assertions in both test suites.
- F5: Add seenLower guard-scope comment (full-ID collisions only; shared-prefix collisions
handled by first-write-wins from sorted planFiles).
- F6: Add ASCII-safe toLowerCase comment at first call site in both SDK and CJS.
- F7: Add intentional-separation comment on seenLower vs planMap in both SDK and CJS.
Reviewers: gsd-code-reviewer (MN-01/NT-1) + sonnet adversarial.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3785): cover case-insensitive depends_on resolution branches
Add 4 focused test cases exercising branches introduced by #3785:
- All-uppercase depends_on ref resolving to lowercase plan ID via planMap
- External cross-phase dep preserved as-is in Pass 3 output (planMap miss)
- Mixed-case short canonical prefix resolving via canonicalToId
- Plans with undefined/empty depends_on emit empty array correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(npm): rebrand packages to @opengsd scope
Rename:
- get-shit-done-redux → @opengsd/get-shit-done-redux
- @gsd-redux/sdk → @opengsd/gsd-sdk
Add publishConfig.access=public for first-time scoped publish.
CLI binary names (get-shit-done-redux, gsd-sdk, gsd-tools) unchanged.
Sweeps install commands, npx invocations, CI publish/version-check
workflows, tests, docs, READMEs (all translations), and the
PACKAGE_NAME constant in check-latest-version.
Bumps qs 6.15.1 → 6.15.2 to clear a moderate advisory surfaced by
the audit-clean test (GHSA-q8mj-m7cp-5q26).
Closes#126
* chore: pin 2.0.0 release + remove canary workflow
- Bump both packages 1.50.0-canary.0 → 2.0.0 for first @opengsd publish
- Remove .github/workflows/canary.yml and canary dist-tag handling in
release.yml / release-sdk.yml
- Drop canary section from VERSIONING.md
Refs #126
* chore: address review findings + harden tarball-smoke timeout
- .changeset/opengsd-org-rename.md: match project's custom
parse.cjs frontmatter (type: Changed / pr: 127); the scoped
@changesets/cli keys were silently rejected.
- CONTEXT.md: drop two canary-stream policy lines and a dangling
DEFECT.CANARY-VERSION-LEAK.cross-ref now that canary.yml is gone.
- tests/release-tarball-smoke.install.test.cjs: pass
timeout: 600_000 for npm pack + global install; the 3-minute
runNpm default was timing out on slower Docker hosts (cartographer).
Refs #126
* fix(sdk): add missing type/runtime devDependencies for build
prepublishOnly invokes tsc which couldn't resolve @types/node,
@types/ws, or synckit. They had been hoisted from root but were
not declared in sdk/'s own package.json — first publish from a
clean SDK tree failed.
Refs #126
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): use npm pack stdout instead of glob to find tarball
`npm pack --silent` for a scoped package (@opengsd/get-shit-done-redux)
produces `opengsd-get-shit-done-redux-*.tgz`, not `get-shit-done-redux-*.tgz`.
Capture the filename from stdout instead of a hardcoded glob so the step
works regardless of package name format.
Fixes smoke (ubuntu-latest, 22, false) CI failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: treat workflow-file changes as test-skip eligible
`.github/workflows/install-smoke.yml` (and other workflow files)
were in neither `test.yml` paths nor `test-skip.yml` paths-ignore,
so neither workflow ran on a workflow-only commit — leaving the
required test-skip check perpetually missing.
Refs #126
* chore: reset version to 1.0.0 for first @opengsd publish
Nothing has been published yet under the @opengsd scope, so the
inaugural release uses 1.0.0 rather than 2.0.0. The "major bump"
in the changeset reflects the breaking install-command change for
users migrating from the prior unscoped `get-shit-done-redux`, not
a numeric continuation from a 1.x line under the new identity.
Refs #126
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3774): treat 999 as exact sentinel, not lower bound, in phase-lifecycle-policy
scanSequentialMaxPhaseFromMilestone and scanSequentialMaxPhaseFromDirs used
`num >= 999` to skip the backlog lane, but this incorrectly excluded every
phase ≥ 1000, causing computeNextSequentialPhaseId to return 1 for projects
using canonical phase IDs in the 1000+ range. Change both guards to
`num === 999` so only the backlog sentinel is skipped.
Adds regression test: project with phases 1000–1500 must produce 1501, not 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for fix#3792 (phase.add returns 1 on 1000+ projects)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3774): address review — fix 4 CJS scanner twins + tighten regression test
Addresses gsd-code-reviewer BLOCKER (4 CJS scanner twins in phase.cjs:610,624,688,698 still carried >= 999, reachable via GSD_WORKSTREAM / absent SDK build) and MAJOR (regression test couldn't distinguish === 999 from === 1000 — added [999, 1000] fixture asserting result === 1001). Decrement helpers at :893, :922, :930, :936 left unchanged — intentional 999-lane protection per dual-review analysis.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses gsd-code-reviewer (Bug 2 no-op proven empirically; unanchored Plans regex) and
sonnet adversarial (leading-dot silent wave-1 default; multi-decimal + bare-bold test gaps).
- F1: Drop "Bug 2" nextPhaseOffset regex change (\d[\d.]* → \d): confirmed no-op by
reverting and verifying all 7 existing tests still pass — phase headings always start
with a digit so \d already matches decimal phases like 02.3.
- F2: Anchor plansBlockMatch to start-of-line via (?:^|\n) prefix so mid-line occurrences
like `***Plans:***` in prose or `OpenPlans:` prefixes do not produce false matches.
- F3: Add leading-dot plan ID validation guard before planData.find() — malformed IDs
that fail /^\w[\w.-]*$/ are skipped rather than silently defaulting to wave 1.
- F4: Add adversarial test cases for 001.10-PLAN.md (multi-decimal leading-zero ID) and
**Plans:** bare-bold (no trailing text); delete vacuous Bug 2 describe block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three regex defects in cmdRoadmapAnnotateDependencies (roadmap.cjs):
1. Plans-block detection (line ~553): `Plans:\s*\n` required no text after
the colon, silently skipping `Plans: 3 plans\n` and `**Plans:** N\n`.
Fixed: `\*{0,2}Plans\*{0,2}:[^\n]*\n` + require `+` checklist lines so
a bold summary line above a bare `Plans:` block doesn't consume the match.
2. Phase-section boundary (line ~542): `\d` matched only one digit, so
`### Phase 02.3:` was not recognised as a section terminator, allowing
plan-list content from adjacent decimal phases to bleed in. Fixed with
`\d[\d.]*`. Same one-digit boundary also patched in roadmap.cjs (analyze
path), phase.cjs (insert-after path), and init.cjs (section-slice path).
3. Plan-ID extraction (line ~566): `[\w-]+?` excluded `.`, capturing `02`
from `02.3-01-PLAN.md` instead of `02.3-01`, so planData.find never
resolved and every plan defaulted to wave 1. Fixed: `[\w.-]+?`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3815): phase.insert parser handles checked-bullet ROADMAP format
phaseInsert (TS) and cmdPhaseInsert (CJS) previously used a heading-only
regex (#{2,4}\s*Phase\s+N:) to locate the target phase. On projects whose
ROADMAP uses the checked-bullet format (- [ ] **Phase N: name** or
- [ ] Phase N: name), the lookup always failed with "Phase N not found".
Extend the locator to also accept the bullet form — mirroring the patterns
already used by phaseRemove and phaseComplete. When bullet-style is
detected, insert a new bullet entry after the matched line (preserving
bold/plain style to match surrounding entries). The heading-style code
path is unchanged.
Also fix a pre-existing test timeout: the first registry-integration test in
phase-lifecycle.test.ts was failing with STACK_TRACE_ERROR (masked timeout)
because the cold import of index.js takes >5 s. Added { timeout: 30_000 }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3815): refine hybrid-ROADMAP detection, preserve #3098 parity
Tighten the bullet-style branch guard: only treat a ROADMAP as
bullet-style (and apply the bullet-insert path) when it contains
ZERO heading-style phase entries (anyHeadingPattern test). A
mixed (hybrid) ROADMAP — headings for some phases, bullet summaries
for others — is the #3098 case where the detail section is absent;
that path must still error with "missing a detail section".
Adds a regression test (#3098 preserved) in both TS and CJS to
confirm that a heading-style ROADMAP with a bullet-only entry for
the target phase still fires the "missing a detail section" error,
not the bullet-insert path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for #3815 phase.insert bullet-roadmap fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #3479 fixed three false-positive classes in sdk/src/query/validate.ts
but the fixes never propagated to get-shit-done/bin/lib/verify.cjs —
the hand-maintained CJS runtime bundle that gsd-tools.cjs actually executes.
W005: widened phase-dir regex from \d{2} to \d{2,} so 3+-digit prefixes
like 999.1-foo are accepted.
W006: adds forEachArchivedPhaseToken call after collectDiskPhases so phases
whose directories live in a milestone archive are not flagged as missing.
I001: adds canonicalPlanStem helper and uses it in summaryBases Set
construction so 68-01-scaffolding-PLAN.md correctly matches 68-01-SUMMARY.md.
Adds five regression tests (TDD red→green) covering each false-positive path.
Fixes#3806
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3804): rescue uncommitted SUMMARY.md in executeWorktreeWaveCleanupPlan
Ports the shell-fallback SUMMARY rescue logic from quick.md into
executeWorktreeWaveCleanupPlan. Before the dirty-state check, all
*SUMMARY.md files under <worktree>/.planning/ are copied to the main
tree (if absent or divergent), then filtered out of the git-status
porcelain output. A worktree whose only dirty file is the executor's
uncommitted SUMMARY.md now proceeds to merge+remove instead of
returning cleanup_blocked/worktree_dirty.
Adds two TDD tests (#3804):
- Rescue-only dirty state (SUMMARY.md alone) → cleanup succeeds
- SUMMARY + non-SUMMARY dirty files → cleanup still blocks
Refs: #2296, #2070, #2838, #3804
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3804): normalize relPath to forward slashes for Windows porcelain match
On Windows, `path.join` produces backslash separators while `git status
--porcelain` always emits forward slashes. The rescued-paths Set would
never match porcelain output, causing the dirty-check filter to ignore
SUMMARY rescue and block cleanup on Windows.
Also normalize the test assertion for `rescued[0].dest` to use
forward slashes so the test passes on both platforms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirror of code, issues, and PRs from the upstream gsd-build/get-shit-done,
which appears compromised or abandoned (maintainer unreachable since
2026-04-01; $GSD token linked to rug-pull).
- Adds rebrand notice block at top of English README
- Removes $GSD token badge and @gsd_foundation X badge (keeps Discord)
- Renames npm packages: get-shit-done-cc -> get-shit-done-redux,
@gsd-build/sdk -> @gsd-redux/sdk
- Updates all repo URLs across docs, workflows, package.json, bin/
- Updates ci@gsd-build -> ci@gsd-redux in workflow git identities
- Leaves CHANGELOG and .changeset/* alone (historical, time-stamped)
* feat(routing): add CommandRoutingHub with behavioral test suite (#3788)
Introduces createHub({ mode, sdkLoader, cjsRegistry, manifest }) and
hub.dispatch({ family, subcommand, args, cwd, raw }) -> Result with a
closed 6-value ERROR_KINDS frozen enum. Hub never throws, never prints,
and enforces no transparent fallback between sdk/cjs modes. 34 behavioral
tests cover all errorKind values, mode fixation, and the no-throw contract.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(routing): migrate phase-command-router to CommandRoutingHub (#3788)
Rewrites phase-command-router.cjs to dispatch through CommandRoutingHub.
Public entry point routePhaseCommand({ phase, args, cwd, raw, error }) is
unchanged. The adapter determines mode (sdk/cjs) from env + tryLoadSdk(),
constructs a hub, dispatches, and translates the pure Result back to
output()/error() calls. New behavioral test suite (23 tests) replaces the
old mock-heavy approach and includes two integration tests through the real hub.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(routing): ADR + glossary + changeset for CommandRoutingHub (#3788)
Adds ADR-3788 documenting the hub's design contract (pure result, fixed mode,
closed 6-value errorKind enum, no transparent fallback). Adds Command Routing
Hub glossary entry to CONTEXT.md and a one-paragraph reference to
ARCHITECTURE.md. Changeset fragment records the Changed entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(docs): rename ADR to sequential convention 0012 (#3788)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(inventory): register CommandRoutingHub in INVENTORY (#3788)
Add command-routing-hub.cjs row to docs/INVENTORY.md CLI Modules table,
bump headline count from 72 to 73, and regenerate INVENTORY-MANIFEST.json
via scripts/gen-inventory-manifest.cjs --write.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(adr): add 0012 to ADR index (#3788)
Add entry for 0012-command-routing-hub.md to the index table in
docs/adr/README.md so the enh-3271-sdk-adr-structure lint passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(lint): bump phase test-file ceiling to accommodate command-router suite (#3788)
phase-command-router.test.cjs added by the CommandRoutingHub migration
pushes the phase prefix cluster from 4 to 5 test files. Bump the allowlist
ceiling from 4 to 5 (issue 3788) so lint-test-file-count passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(routing): preserve phase.mvp-mode JSON error and ROADMAP scan through hub (#3788)
mvp-mode was never registered in the SDK; the pre-#3788 CJS router
always dispatched it via the CJS handler even when sdkAvailable was
true. After the hub migration, SDK-mode hubs (Docker, where the SDK
build exists) sent mvp-mode to the SDK bridge, which returned
SdkDispatchFailed with reason 'unknown' instead of the expected
'usage' code, and failed ROADMAP lookups. Fix by short-circuiting
mvp-mode to the CJS handler before hub construction, matching the
pre-migration observable behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(adr): note SDK-incomplete subcommand limitation in ADR-0012 (#3788)
* fix(inventory): bump CLI Modules headline to 74 after rebase onto main (#3788)
Upstream added code-review-flags.cjs (72→73) at the same time our branch
added command-routing-hub.cjs. After rebase both modules exist (74 total)
but the headline stayed at 73; bump to 74.
* fix(routing): remove dead mvp-mode handler from cjsRegistry (#3788)
The cjsRegistry['phase']['mvp-mode'] handler (previously lines 65–68)
was unreachable: the early-return bypass at line 56 intercepts mvp-mode
before hub construction in CJS mode, and in SDK mode cjsRegistry is
passed as undefined. Remove the dead handler; all 57 tests still pass.
* docs(adr): correct router count in ADR-0012 (#3788)
The context section cited "eight" routers including "frontmatter" but
there is no frontmatter-command-router.cjs. The actual count is seven:
phase, phases, roadmap, state, verify, validate, init.
* fix(routing): guard missing subcommand + use ERROR_KINDS constant (#3788)
Two fixes in phase-command-router.cjs:
1. Add early-return for missing subcommand before hub construction.
Pre-#3788 the routeCjsCommandFamily fell through to error() for
undefined args[1]; post-#3788 the hub's manifest check skips falsy
subcommands, which would have sent bare 'phase' into SDK dispatch
in SDK mode instead of the expected "Available: ..." error message.
2. Switch on ERROR_KINDS.UnknownCommand instead of bare 'UnknownCommand'
string, per ADR-0012's closed-enum contract ("callers switch on
ERROR_KINDS values, not bare string literals").
* docs(routing): fix factual errors in ARCHITECTURE, ADR-0012, changeset (#3788)
Three corrections:
1. ARCHITECTURE.md: softened "All CJS command family routers dispatch
through CommandRoutingHub" — only phase-command-router.cjs is
migrated in this PR; remaining routers still use routeCjsCommandFamily
and migrate in follow-up issues.
2. ADR-0012: corrected the SDK mvp-mode claim. The ADR said "the SDK
has no equivalent entry" but sdk/src/query/command-static-catalog-
domain.ts:104-105 registers phase.mvp-mode. The actual reason for
the early-return bypass is divergent ROADMAP scan behaviour and
error reason codes, not SDK absence.
3. .changeset/mellow-tigers-gather.md: corrected pr: 1 → pr: 3828.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#3426): add RED test for Codex Windows hooks .cmd shim requirement
Drive buildCodexHookWindowsShimIR (typed IR) + ensureCodexHooksJsonSessionStart
integration against mocked win32 platform. Counter-tests confirm darwin/linux
paths remain unchanged.
NOTE: Windows wall-clock verification depends on Docker matrix Windows
runners. Local test exercises the generator IR shape only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#3426): Codex Windows hooks use .cmd shim to avoid bash.exe POSIX-exec failure
Root cause: Codex on Windows runs hook commands from PowerShell/cmd. The previous
hooks.json command format was `"node.exe" "script.js"`. Codex's hook-dispatch shell
(Git Bash / MSYS) tried to POSIX-exec node.exe (a Windows PE binary) via execvp(),
which fails with ENOEXEC — reported as `bash.exe: cannot execute binary file`.
Fix: `ensureCodexHooksJsonSessionStart` now calls `buildCodexHookWindowsShimIR` on
win32 to write a .cmd shim alongside the .js hook file. cmd.exe executes .cmd files
natively via CreateProcess, bypassing the POSIX exec layer entirely. Non-Windows
paths (darwin, linux) are unchanged: they continue to use the node-runner command.
Also adds `gsd-check-update.cmd` to the codex-hooks-json managed-basename set so
reconcileCodexHooksJsonSessionStart correctly replaces stale node-runner entries on
reinstall.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3426): update changeset to reference PR #3768
* fix(3426): fail-loud on Codex Windows shim-write failure instead of silently restoring broken command
Replace the silent fallback to `projectManagedHookCommand` (the old
`node.exe script.js` form) with an explicit warn-and-skip path.
When `atomicWriteFileSync` fails to write the `.cmd` shim, the previous
code silently called `reconcileCodexHooksJsonSessionStart` with the
legacy node-runner command. That command triggers the exact
`bash.exe: cannot execute binary file` POSIX-exec failure that #3426
exists to fix — so a successful-looking install was secretly restoring
the original bug.
New behaviour:
- Emit `console.warn` with the failure reason and a remediation hint,
matching the `${yellow}⚠${reset} Skipped …` idiom used at line 9098.
- Return `{ changed: false, wrote: false }` to skip registration for
this runtime entirely, so the outer caller can surface "NOT installed"
instead of "installed (but broken)".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3426): typed-IR assertions on .cmd shim eol/quoting/passthrough + IR extension
Extend `buildCodexHookWindowsShimIR` to expose two new typed fields on
the returned IR object (CONTRIBUTING.md L558-L565 IR-first discipline):
eol: { cmd: '\r\n' } — CRLF is canonical for cmd.exe .cmd files
passthroughArgs: true — shim forwards all args via %*
Add a new describe block (Step 2b) with three IR-level assertions:
1. `eol.cmd === '\r\n'` — prevents silent EOL regression that could
break parsing on Windows versions that require CRLF.
2. `invocation.target` is the raw unquoted path (no shell-metachar
leakage) — quoting happens only at render time.
3. `passthroughArgs === true` — the %* forwarding contract is
explicitly typed so regressions fail before the text is rendered.
All assertions operate on the typed IR returned by the generator, NOT
on the rendered `.cmd` file content — text-matching is the anti-pattern
CONTRIBUTING.md L522-582 prohibits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3426): fix Windows CI failures — update hook-command filter patterns
Four test files filtered for managed hooks in hooks.json using the
literal string `gsd-check-update.js`. On Windows the PR-introduced
.cmd shim changes the hooks.json command to
`"path/gsd-check-update.cmd"` (no node prefix, .cmd extension), so
those filters matched 0 entries and 24 Windows subtests failed.
Fixes:
- bug-2760-codex-install-defensive.test.cjs (7 filters): change
`/gsd-check-update\.js/` → `/gsd-check-update/` to match both
.js (POSIX) and .cmd (Windows) commands.
- bug-3357-codex-legacy-hooks-json-migration.test.cjs (3 filters):
same `.js` → no-extension change.
- bug-3427-3433-codex-install-shape.test.cjs (2 filters): same fix;
add explanatory comment to uninstall assertion.
- codex-config.test.cjs (9 filters + 1 exact-command assertion):
bulk-replace all `hooksJsonCommands.filter(cmd => cmd.includes('gsd-check-update.js'))`
with `gsd-check-update`; make the `fresh CODEX_HOME` test platform-
aware — on win32 assert `.cmd` shim path, on POSIX assert the
existing `"runner" "script.js"` form (#3017).
All four suites pass locally (macOS / darwin). Windows subtests
verified against the Windows CI failure log patterns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3426): address pr-review-toolkit + codex review findings
- fix(uninstall): add gsd-check-update.cmd to gsdHooks cleanup list so
the .cmd shim is removed from disk on Windows uninstall (was left as
orphan artifact — silent failure post-uninstall)
- test(3426): add uninstall test asserting gsd-check-update.cmd is
deleted from hooks dir after `uninstall(true, 'codex')` (no coverage existed)
- fix(comment): correct JSDoc on buildCodexHookWindowsShimIR — shim
content is three-line @ECHO OFF/@SETLOCAL/@runner snippet, not bare
`@node "script.js" %*` as the old comment claimed
- fix(comment): update stale assertion message in codex-config.test.cjs
L1457 — said "config.toml references it" but the hook is in hooks.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3657): RED+shape-lock for verify-reapply-patches pristine-drift
Adds tests/bug-3657-verify-reapply-patches-pristine-drift.test.cjs:
- Core regression: exits 0 with reason=OK_PRISTINE_DRIFT_DETECTED when
on-disk gsd-pristine/ hash does not match backup-meta.json.pristine_hashes
- Counter-tests: real FAIL_USER_LINES_MISSING still caught when hashes match;
over-broad mode unchanged when backup-meta.json is absent; clean run
reports 0 failures when everything matches
- Multi-file: drift + real-failure handled independently per file
Updates tests/bug-2969-verify-reapply-patches.test.cjs REASON shape-lock to
include OK_PRISTINE_DRIFT_DETECTED (added by the #3657 fix).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3657): verify-reapply-patches skips pristine when hash drifts
When gsd-pristine/ is refreshed to a newer GSD version after a backup is
captured, the on-disk pristine's SHA-256 no longer matches the hash recorded
in backup-meta.json.pristine_hashes. Using the wrong-version pristine as the
diff baseline inverts the delta: every line the upstream removed between the
two versions appears as a "user-added line that must survive", producing
spurious FAIL_USER_LINES_MISSING false positives (Bug #3657).
Fix:
- Add sha256() and readPristineHashes() helpers to verify-reapply-patches.cjs
- In verifyFile(), when a pristine_hashes entry exists for the file, compare
the on-disk pristine's SHA-256 against it before accepting the baseline
- On hash mismatch, return immediately with status=ok and the new
REASON.OK_PRISTINE_DRIFT_DETECTED code, skipping the diff rather than
false-failing
- When no pristine_hashes entry exists (older installer / absent backup-meta),
fall through to the pre-fix behaviour (use on-disk pristine as-is)
- Export sha256, readPristineHashes, and OK_PRISTINE_DRIFT_DETECTED
New REASON code OK_PRISTINE_DRIFT_DETECTED is added to the frozen enum.
Exit code contract is unchanged: 0 for gate pass (including skipped-due-drift
files), 1 for real user-content failures, 2 for structural errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(3657): update changeset to reference PR #3767
* fix(3657): surface drifted_files in verify-reapply-patches JSON report
Extend the top-level JSON report shape with two additive fields:
- `drifted: N` — count of files skipped due to pristine-snapshot drift
- `drifted_files: [...]` — relative paths of those files
Per-file shape is unchanged (status:'ok' + reason:OK_PRISTINE_DRIFT_DETECTED)
for backward compat. Drift still exits 0; `failures` count is unaffected.
This gives workflow Step 5a structured data to gate on so drifted files are
no longer silently treated as a full PASS (codex adversarial-review finding 1).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3657): reapply-patches workflow halts on drifted files instead of silent pass
Insert a "Step 5a: drift check" block between the exit-code check and the
failures check in workflows/reapply-patches.md Step 5a. The new block:
1. Parses `drifted` + `drifted_files` from the JSON report (added in the
companion prod-code commit).
2. When DRIFTED_COUNT > 0, emits a formatted HALT message naming each
drifted file and instructs the user to re-baseline before re-running.
3. Sets DRIFT_DETECTED=true and exits non-zero so subsequent steps cannot
execute while drift is unresolved.
Drift is a distinct third state: it is not a failure (no missing user lines
were proven) but it is also not a clean pass (the diff was skipped entirely).
Existing pass/fail logic for VERIFY_STATUS and failures count is unchanged.
Closes the silent-skip gap identified in codex adversarial-review finding 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(3657): assert drifted_files report shape + workflow Step 5a drift check
Finding 1 (BLOCKER) — three new tests in bug-3657 test file:
- Single drifted file: drifted=1, drifted_files contains the file path,
failures=0, per-file shape unchanged (backward compat).
- Multi-file drift: drifted=2, drifted_files lists both paths, clean file
absent from array.
- No-drift baseline: drifted=0, drifted_files=[] always present in output.
Finding 2 (WARNING) — structural test on workflow source:
- Asserts Step 5a contains "Step 5a: drift check" heading.
- Asserts DRIFTED_COUNT, drifted_files, and DRIFT_DETECTED are referenced
(confirming the gate exists and uses the structured report fields).
- Asserts drift-check block appears before VERIFY_STATUS check (exit-code
is 0 for drift, so the drift check must precede the non-zero gate).
Also updates bug-2969 shape-lock to include the two new additive fields
(drifted, drifted_files) per the contract change in the prod-code commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3657): address pr-review-toolkit + codex review + CI failures
- lint-tests: add // allow-test-rule: source-text-is-the-product at file
top of bug-3657 test file; the inline comment at line 477 was a prose
sentence, not a file-level annotation, so the lint scanner did not
recognise it as the bypass token
- Windows test failures (4 subtests): normalize relPath to forward slashes
before pristineHashes key lookup in verifyFile(); on Windows path.join
produces backslash-separated relPath values but backup-meta.json stores
keys with forward slashes, causing the hash lookup to silently return
undefined, falling through to use-as-is mode and producing the same
false FAIL_USER_LINES_MISSING that the fix was meant to prevent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(tests): bump verify allowlist ceiling 9→10 for bug-3657 test file
Adding tests/bug-3657-verify-reapply-patches-pristine-drift.test.cjs in
the previous commit pushed the verify module from 9 to 10 test files.
The lint-test-file-count gate (added in #6313baad on main) enforces that
modules cannot exceed their allowlist ceiling, so all 6 test platforms
plus lint-tests and coverage failed with:
FAIL_EXCEEDS_ALLOWLIST: verify count=10 ceiling=9
The fix is to raise the ceiling from 9 to 10 and set issue=3767.
This file does not exist on this branch yet (introduced on main after
the branch diverged) so we add it here. The merged CI state will see
the bumped ceiling and pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>