* refactor(#455): implement typed surfaces to retire grep tests
Production surfaces added:
- hooks/managed-hooks-registry.cjs: new CJS module exporting MANAGED_HOOKS
as a typed array; gsd-check-update-worker.js now requires it instead of
declaring an inline array
- bin/install.js: elevate inline gsdHooks to module-level GSD_UNINSTALL_HOOKS,
export it alongside runtimeMap/allRuntimes (already exported)
- scripts/build-hooks.js: export HOOKS_TO_COPY; guard build() behind
require.main===module so tests can require the file without triggering a build
- get-shit-done/bin/lib/init.cjs: add --json mode to agent-skills command,
emitting typed IR { agent_type, block, skills_count } for test assertions
- get-shit-done/bin/gsd-tools.cjs: wire --json flag for agent-skills dispatch
Category-B source-grep migrations:
- tests/managed-hooks.test.cjs: require MANAGED_HOOKS from registry, drop fs.readFileSync+regex
- tests/orphaned-hooks.test.cjs: require MANAGED_HOOKS+HOOKS_TO_COPY as typed exports
- tests/hooks-opt-in.test.cjs: replace gsdHooks regex-parse with GSD_UNINSTALL_HOOKS import
- tests/install-minimal-hooks.test.cjs: replace gsdHooks regex-parse with GSD_UNINSTALL_HOOKS
- tests/copilot-install.test.cjs: replace src.includes() checks with typed
assertions on runtimeMap, allRuntimes, parseRuntimeInput, buildRuntimePromptText
- tests/agent-skills.test.cjs: migrate to --json typed IR assertions
pending-migration-to-typed-ir token cleared (87 of 87 files):
- 78 files already had source-text-is-the-product; removed duplicate token
- 5 files already used typed assertions; reclassified or annotated
- 4 files required individual reclassification to source-text-is-the-product
or architectural-invariant
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#455): update workflow-guard test to typed GSD_UNINSTALL_HOOKS import; isolate HOME in runtime-launcher (D) test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#455): guard install.js main() behind require.main===module so the typed export is require-safe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(#455): document --json typed surfaces for agent-skills, progress, validate context
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(#455): add changeset fragment for new --json surfaces
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#455): complete grep migration for files flagged by lint-tests
The branch commit 4e630d99 stripped `allow-test-rule: pending-migration-to-typed-ir`
from ~80 test files without replacing their assertions or adding the correct
exemption annotation. The files were NOT source-grep tests — they read .md
workflow/agent/command/reference files (source-text-is-the-product) or hook
source files for structural invariants (structural-regression-guard). No
assertion logic was changed; only the correct allow-test-rule annotation was
added to each file per CONTRIBUTING.md exception matrix.
73 files: `source-text-is-the-product` — workflow/agent/command/reference .md
7 files: `structural-regression-guard` — hook .js / bin/install.js structural checks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(#453): add deterministic clock seam to lock modules
Introduces get-shit-done/bin/lib/clock.cjs exporting realClock with
now() (Date.now) and sleep() (Atomics.wait). acquireStateLock,
writeStateMd, and readModifyWriteStateMd in state.cjs each accept an
optional trailing clock param (default: realClock). withPlanningLock in
planning-workspace.cjs gains the same seam. No production behavior
change — all callers that omit the param continue to use realClock.
Adds tests/helpers/clock.cjs (makeFakeClock) and tests/clock-seam.test.cjs
with 20 deterministic in-process tests covering: lock serialization,
timeout throw at maxWaitMs boundary, stale-lock takeover, lock released
on error path, withPlanningLock timeout recovery, exit-cleanup integration,
readModifyWriteStateMd call-site coverage (7 cmd*), and roadmap analyze
behavioral assertion (50 phases, no elapsed-time gate).
Deletes/converts per research verdicts: removes 11 source-grep/elapsed-time/
non-deterministic-concurrent tests across concurrency-safety.test.cjs,
locking-bugs-1909-1916-1925-1927.test.cjs, and bug-1974-context-exhaustion-
record.test.cjs. All deleted tests have deterministic replacements in
clock-seam.test.cjs or surviving barrier-based tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#453): update module inventory for clock.cjs; make EEXIST-retry assertion behavioral
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#453): satisfy lint-tests — allow-test-rule annotation on readFileSync/includes runtime output check
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(#443): RED unified effort + fast_mode + resolve-execution
All 68 tests failing as expected — no implementation yet.
Covers: effort cascade (tier defaults, overrides, invalid fallthrough),
fast_mode cascade (boolean-only, tier defaults), resolveEffortForTier
escalation, renderEffortForRuntime clamping, resolve-execution CLI,
config schema new keys, QA hostile-input matrix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#443): unified cross-provider effort + fast_mode knobs and resolve-execution query
Adds config-driven effort control (universal ladder: minimal<low<medium<high<xhigh<max)
and fast_mode propagation knobs, with per-runtime rendering that clamps the unique
tail values (max=Anthropic-only clamps to xhigh on Codex; minimal=Codex-only clamps
to low on Claude).
Key changes:
- config-schema.manifest.json: add effort.default, fast_mode.enabled as validKeys;
add 4 dynamicKeyPatterns for effort.routing_tier_defaults, effort.agent_overrides,
fast_mode.routing_tier_defaults, fast_mode.agent_overrides; fix stale _comment
- config-defaults.manifest.json: add effort and fast_mode blocks with tier defaults
- model-catalog.cjs: add EFFORT_RENDERING map, renderEffortForRuntime(), RUNTIMES_WITH_FAST_MODE
- model-profiles.cjs: re-export new catalog exports
- core.cjs: add resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier,
VALID_EFFORTS, EFFORT_SET, nextEffort; pass effort/fast_mode through loadConfig
- commands.cjs: replace reasoning_effort in cmdResolveModel with unified effort;
add cmdResolveExecution (superset command with effort_rendered, effort_param,
effort_propagation, fast_mode, fast_mode_supported)
- gsd-tools.cjs: add resolve-execution case with --effort/--fast-mode/--attempt flags
- tests/feat-443: 69 tests covering cascade, rendering, escalation, CLI, schema, QA matrix
- tests/commands.test.cjs: convert 3 reasoning_effort assertions to unified effort
- docs/CONFIGURATION.md: document effort + fast_mode + resolve-execution sections
- settings-advanced.md: list new effort/fast_mode keys in confirmation table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(#443): remove dead catalog effort lane; unify codex effort through renderEffortForRuntime
- Remove resolveReasoningEffortInternal (catalog-driven effort function) from
core.cjs and its export; remove from commands.cjs destructure import
- Convert tests/issue-2517-runtime-aware-profiles.test.cjs: all 11 effort
assertions now use resolveEffortInternal + renderEffortForRuntime; Claude
effort is first-class (output_config.effort); unknown runtimes assert param===null
- Convert tests/feat-3023-model-phase-types.test.cjs: replace the entire
resolveReasoningEffortInternal describe with unified effort assertions;
effort derives from AGENT_DEFAULT_TIERS routing tier, not phase-type tier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(#443): ADR for unified cross-provider effort + fast-mode routing
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(#443): architecture-level QA invariants + test-strategy doc
Add 48-test integration suite (feat-443-effort-fast-mode.integration.test.cjs)
covering 8 architectural invariants: cross-provider validity (never emit a value
the real API would 400 on), param/channel contract stability, resolve-execution
JSON contract (all 8 keys + correct types), totality across the full 33-agent
registry, fast-mode honesty (claude always fast_mode_supported=false), precedence
first-valid-wins matrix for both effort and fast_mode cascades, dynamic-routing
composition (effort escalation independent of model tier), and config-set round-trip
for all new effort/* and fast_mode/* key namespaces. Append test-strategy section
with invariant rationale and E2E gap documentation to docs/TESTING-SUITES.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#443): add failing install-wiring tests for effort per-runtime injection (RED)
TDD RED: 10 failing tests covering:
- Claude .md gets effort: injected per tier (planner=xhigh, mapper=low, executor=high)
- Gemini .md does NOT get effort: (already passing — Gemini-safe)
- Codex .toml gets model_reasoning_effort via unified resolver
- Config-driven: effort.agent_overrides drives both Claude .md and Codex .toml
- Source purity: agents/*.md have no effort: key (already passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#443): wire effort per-runtime at install (Claude .md frontmatter + Codex .toml unified)
- Import AGENT_DEFAULT_TIERS and renderEffortForRuntime from model-catalog.cjs
- Add readGsdEffectiveEffortConfig(targetDir): reads merged effort config from
.planning/config.json (per-project wins) + ~/.gsd/defaults.json (global fallback),
same probe pattern as readGsdRuntimeProfileResolver
- Add resolveInstallTimeEffort(effortCfg, agentName): pure function matching
resolveEffortInternal() precedence (agent_overrides > routing_tier_defaults > default > 'high')
without loadConfig side-effects (no sub-repo detection, no migration writes)
- Claude agent copy loop: inject `effort: <value>` into frontmatter ONLY for
runtime === 'claude'; all other .md runtimes (Gemini, Qwen, Hermes, etc.) stay
effort-free (Gemini-safe source contract preserved in agents/*.md)
- generateCodexAgentToml: add effortCfg param; emit model_reasoning_effort from
unified resolver (replaces old catalog entry.reasoning_effort); Codex clamps
max → xhigh via renderEffortForRuntime('codex', ...)
- installCodexConfig: pass readGsdEffectiveEffortConfig(targetDir) to
generateCodexAgentToml so per-project config wins for Codex .toml too
- Update failing tests to GREEN: 12/12 pass; all 17 install tests pass;
2847/2848 unit tests pass (1 pre-existing failure: policy-shell-pinning)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(#443): source install effort defaults from manifest (kill drift) + guard test
Replace hardcoded _GSD_EFFORT_MANIFEST_TIER_DEFAULTS and the 'high' fallback in
resolveInstallTimeEffort with values read from config-defaults.manifest.json at
module init, using the same __dirname-relative path install.js already uses for
all shared manifests. Add feat-443-effort-defaults-drift.test.cjs to assert
equality between install.js's runtime constants and the manifest on every CI run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#443): reconcile Codex TOML tests with unified effort design
The #443 unified effort resolver makes generateCodexAgentToml always emit
model_reasoning_effort (driven by resolveInstallTimeEffort, not model_profile_overrides).
The test 'generated TOML omits reasoning_effort when runtime has none' had an
obsolete premise — model_profile_overrides.reasoning_effort:'' no longer suppresses
unified effort. Convert it to assert the new invariant: Codex TOML always carries a
valid model_reasoning_effort from the agent's routing tier (xhigh for gsd-planner,
a heavy-tier agent), while model_profile_overrides model override is still respected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#443): make install.js effort resolution lazy (no load-time side effects breaking launcher-parity)
Replace module-load-time IIFE + hard throw (config-defaults.manifest.json read)
and top-level require of model-catalog.cjs with a lazy _getGsdEffortCatalog()
getter that initialises on first call from resolveInstallTimeEffort /
generateCodexAgentToml / Claude .md effort injection. Requiring install.js in
unrelated test contexts (e.g. runtime-launcher-parity) no longer triggers
manifest IO or throws, eliminating the load-time side effect that changed
subprocess exit codes / stderr on the bench.
Drift-guard exports (_GSD_EFFORT_MANIFEST_TIER_DEFAULTS / _GSD_EFFORT_MANIFEST_DEFAULT)
preserved as lazy getter properties on module.exports so feat-443-effort-defaults-drift
still validates them without forcing eager load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#443): isolate install-wiring test HOME to stop \$HOME/.claude pollution breaking launcher-parity
runGlobalInstall() now redirects HOME to a per-call isolated tmpdir in addition
to the existing runtime-specific env-var redirects (CLAUDE_CONFIG_DIR,
GEMINI_CONFIG_DIR, CODEX_HOME). This ensures install.js code that uses
os.homedir() directly — including the ~/.cache/gsd update-check deletion,
~/.gsd/defaults.json reads, and any HOME-relative npm subprocess writes —
never touches the real \$HOME during the test.
Without the HOME isolation the install test (which is new to this branch and
is now picked up by Docker's raw \`tests/*.test.cjs\` glob) could write or
delete files under the real \$HOME, causing runtime-launcher-parity test (D)
to fail: (D) asserts a loud non-zero exit when \$RUNTIME_DIR/gsd-tools.cjs is
absent and gsd-tools is not on PATH, but the launcher's \$HOME/.claude fallback
arm succeeds if \$HOME/.claude/get-shit-done/bin/gsd-tools.cjs exists.
Also sets GSD_SKIP_STALE_SDK_CHECK=1 to suppress the \`npm ls -g\` subprocess
that the global installer spawns — irrelevant to effort-wiring assertions,
slow, and potentially writes to ~/.npm cache.
All 12 feat-443 install-wiring assertions preserved. Drift-guard 5/5. Unit
suite 2848/2850 (pre-existing policy-shell-pinning.test.cjs failure on next).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(#443): add changeset fragment for effort + fast-mode routing
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(#443): set GSD_TEST_MODE before requiring install.js in drift-guard test to prevent HOME leak
Without GSD_TEST_MODE=1, require('bin/install.js') runs the module's main
install block (guarded by !GSD_TEST_MODE), performing a real global Claude
install into $HOME/.claude/. On CI ubuntu where node is on standard PATH,
the launcher's $HOME/.claude fallback arm then finds gsd-tools.cjs, causing
runtime-launcher-parity test (D) to exit zero when it must exit non-zero.
Root cause: feat-443-effort-defaults-drift.test.cjs (unit suite) runs
alphabetically before runtime-launcher-parity.test.cjs in the same node
--test invocation. Each runs in a separate worker process but shares the
same HOME. The drift test's install leaks gsd-tools.cjs into that HOME,
then the launcher test's bash subprocess finds it via the $HOME/.claude arm.
Fix: add process.env.GSD_TEST_MODE = '1' at the top of the drift-guard
test, before the require(installPath) call. This matches the pattern used
by feat-443-effort-fast-mode.test.cjs and feat-443-effort-install-wiring
.install.test.cjs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#443): deterministic resolve-execution arg parsing + validate install-time effort (Codex adversarial findings)
Finding 1: resolve-execution --effort low gsd-planner misrouted 'low' as the agent.
Replace find(non-dash) with a proper flag-consuming loop that collects a single
positional; validate missing/extra positionals and malformed --attempt values.
Finding 2: resolveInstallTimeEffort returned unvalidated effort strings (e.g. "ultra")
verbatim. Each precedence layer now checks GSD_EFFORT_SET (imported once from
core.cjs) before accepting a value, mirroring resolveEffortInternal exactly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#443): newline-agnostic effort frontmatter injection (Windows CRLF) + CRLF-safe assertions
Extracts injectEffortFrontmatter(content, effortValue) pure helper that detects
EOL (LF vs CRLF) from the opening '---' line and inserts 'effort: <value>'
before the closing '---' delimiter using the same EOL as the surrounding
frontmatter. Regex now uses /^---\r?\n([\s\S]*?)^---\r?$/m instead of the
LF-only /^(---\n[\s\S]*?)(---)(\n|$)/ that silently skipped CRLF files on
Windows (git core.autocrlf=true checkout).
Also adds 7 unit tests covering LF, CRLF, idempotency, no-frontmatter, and
complex frontmatter cases. Exports injectEffortFrontmatter from module.exports.
Fixes 6 CI failures in tests/feat-443-effort-install-wiring.install.test.cjs
on windows-latest runners (lines 138, 145, 152, 261, 345, 356).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces KNOWN_TEMPLATE_DEFAULTS and KNOWN_STATUS_PATTERNS in state-document.cjs
to enumerate every string a GSD handler writes. stateReplaceFieldIfTemplate
consults this table and only replaces the field when the existing value is a known
template default (or absent) — executor-authored values are left untouched.
Wire-in:
- record-session: Resume File now only overwritten when caller passes --resume-file
OR existing value is 'None'. Router no longer defaults resume_file to 'None'
before calling the handler.
- advance-plan (both branches): Status and Last Activity guarded via
stateReplaceFieldIfTemplate.
- updateCurrentPositionFields: Status and Last activity in the Current Position
section guarded; bare ISO date shape is the trigger for replacement, prose
narrative is preserved.
- planned-phase: Status and Last Activity guarded the same way.
Regression tests (7 cases) in tests/bug-397-state-preserve-executor-authored.test.cjs
cover each data-loss shape; all 106 existing state.test.cjs tests still pass.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#416): return null when active milestone has no archive (no fall-through to prior milestone's archive)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#416): handle bold-formatted Milestone: field in archive dir resolver
The STATE.md regex in getActiveMilestoneArchiveDir failed to extract the
version from **Milestone:** vX.Y format (bold wraps the label+colon).
The old pattern captured '**' instead of the version, causing the
milestone→archive lookup to produce a false candidate path, then return
null (post-fix behavior) instead of falling through to the version-sort
fallback — breaking the #3164 consistency scanner tests.
Fix: extend the regex to skip optional trailing '**' after the colon so
both 'milestone: vX.Y' and '**Milestone:** vX.Y' parse correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
cmdWebsearch called fetch() with no timeout and no retry, so a hung
connection blocked indefinitely and transient 429/5xx/network failures
were not recovered. Add AbortSignal.timeout (configurable via
GSD_WEBSEARCH_TIMEOUT_MS, default 10s) and a bounded retry loop
(max 2 retries, exponential backoff + jitter) for 429/5xx/network
errors, honoring Retry-After on 429 (capped at 60s). Non-429 4xx fail
immediately (no wasted retries). Transient-exhausted failures report an
`attempts` count. Worst-case time is bounded by timeout*(1+retries)+backoff.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Static regex literal `byPhaseTablePattern` was recompiled on every call to `updatePerformanceMetricsSection`; hoisted to module scope (compiled once; stateless /i used with .match → safe to share across calls). `phaseRowPattern` uses dynamic interpolation and stays in-function. `Fixes #320`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).