acquireStateLock was allocating a fresh SharedArrayBuffer on every retry
iteration via Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), ...).
The buffer is never mutated and never escapes, so hoisting it before the loop
is a provably-equivalent transformation — Atomics.wait always sees value 0
whether the buffer is fresh or reused.
Fixes#316
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
loadConfig called detectSubRepos(cwd) at up to 3 sites per invocation (root-config
requiresFilesystem migration, workstream-config requiresFilesystem migration, and the
planning.sub_repos filesystem re-sync) — all with the same cwd, yielding identical
results. Introduce a per-call lazy memo (getDetectedSubRepos) so the directory scan
runs at most once per loadConfig call while preserving all conditional logic. Fixes#315.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hot path in cmdRoadmapAnnotateDependencies called planData.find() on every
checklist line. Replaced with a first-wins Map built once before the loop so
each line resolves in O(1); first-wins preserves exact .find() semantics and
null-on-miss → wave-1 default is unchanged.
Fixes#314
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intelExtractExports deduped export names with `if (!arr.includes(x)) arr.push(x)`
across ~8 extraction loops (one doubly-nested over an export block) — O(n^2).
Accumulate into Sets (add/has/size) and materialize to an array once at return.
Set dedups by value and preserves insertion order, so the returned export list
and its first-seen order are identical. Adds behavior-lock tests for dedup + order.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseNamedArgs re-scanned argv with indexOf/includes once per flag —
O(flags * argv) — on the command-dispatch hot path (24 call sites across
gsd-tools + init/state/validate routers). Build a first-index Map of argv
tokens in a single pass and use it for the flag lookups, dropping it to
O(argv + flags). Semantics are identical: firstIndex.get(t)??-1 === indexOf(t),
firstIndex.has(t) === includes(t); first-occurrence-wins and the value-token
rejection are preserved. Adds the first behavior-lock tests for the module.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmdCommitToSubrepo routed each changed file to a sub-repo via
subRepos.find(...) inside the file loop — O(files * repos). Extract a pure
groupFilesBySubrepo() that buckets sub-repos by first path segment and scans
only the matching bucket, dropping it to expected O(files + repos). First-
match-in-array-order semantics (incl. multi-segment sub-repos) are preserved
exactly. Adds the first behavior-lock test for the routing path.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
learningsCopyFromProject called learningsWrite K times in one process,
and each call re-scanned the entire learnings store to dedupe — O(K*N).
Build the content_hash -> id index once at the start of the bulk import
and thread it through; single-write behavior and the return contract are
unchanged (no caller reads `id` on the created:false branch). O(K*N) ->
O(N+K). Adds a regression test asserting store scan count is independent
of import size.
The larger persistent on-disk index (atomic updates, corruption rebuild,
cross-process dedupe) is deferred — needs design decisions and is not
required to resolve the bulk-import scan this issue reports.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Pass-2 topological level assignment in cmdPhasePlanIndex dequeued its
Kahn's-algorithm queue with Array.shift(), which is O(n) per call in V8, so the
BFS was O(V^2) and slowed superlinearly on deep queues (wide fan-in plan
graphs). Extract the traversal into a pure, exported computeDependencyLevels
(rawPlans, planMap, canonicalToId) and dequeue via a head index (queue[head++])
-> O(V+E). Behavior is identical: same FIFO order, same longest-path levels,
same visited-count cycle detection. A complexity-contract comment above the loop
documents why shift() must not be reintroduced.
Adds tests/phase-dependency-levels.test.cjs with deterministic behavior and
edge-case coverage (linear chain, diamond longest-path, independent set, cycle,
canonical-prefix resolution, empty, self-loop, duplicate edge, external dep). A
timing-based complexity guard was intentionally omitted: the O(V+E) Map-build
constant dilutes the O(V^2) signal until impractical N (~1e6), so an empirical
guard is inherently flaky on contended CI — the contract is enforced by the
inline comment and correctness tests instead.
Fixes#307
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#145): extractCurrentMilestone selects active sub-milestone over closed sibling
extractCurrentMilestone used a non-global regex with content.match()
(first-match) to locate the milestone section for the STATE.md version.
When the milestone is a shared semver prefix (e.g. v8.0) and ROADMAP.md
holds both a closed sub-milestone (## v8.0 ... CLOSED/FAIL) and an active
one (## v8.0-B ... STARTED), the first match was always the closed
heading, so the active section and its phases were excised from the slice
and downstream phase ops failed with "Phase N not found in current
milestone".
Switch to a global matchAll over candidate headings, skip headings
carrying a closed marker (CLOSED/ARCHIVED/ABANDONED/SHIPPED/FAILED/FAIL/
✅/🗄️), and select the first non-closed match (falling back to the first
match when every candidate is closed, preserving legacy behavior). Anchor
the preamble slice to the first heading index so a closed sibling's body
no longer leaks into the preamble when the selected section is later.
Harden version matching with a trailing word boundary (so v8.0-B does not
match v8.0-Beta), narrow the FAIL marker to FAILED, match a bare 🗄, and
add an active-marker override (STARTED/🚧/ACTIVE) so a heading carrying an
explicit active status is never treated as closed even if its name contains
a completion word. Also anchor the preamble at the first any-version
milestone heading so unmatched sibling sections do not leak into the
preamble.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(#145): add changeset fragment for milestone-selection fix
Adds the required .changeset/*.md fragment for this user-facing fix
(changeset-lint gate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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.
* 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>