* test(#178): update DispatchEvent factory tests to propagate parentTraceId
P1.3 test 'parentTraceId is always undefined' replaced with four P1.4
contracts: absent → undefined, string → propagated, null → undefined,
non-string → undefined (defensive normalization policy).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#178): propagate parentTraceId through DispatchEvent factory
Stop ignoring the parentTraceId parameter added as a forward-compat hook
in P1.3. Defensive normalization: only non-null strings are propagated;
null, non-string values, and absent callers all yield undefined, keeping
P1.3 behavior intact for all existing dispatch call sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): add Hub-level parentTraceId propagation tests
Four new assertions: req.parentTraceId propagates to event, absent →
undefined (P1.3 regression), shared parentTraceId across multiple
dispatches, and unique traceId invariant despite shared parentTraceId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#178): plumb parentTraceId through Hub dispatch and _notifyLogger
dispatch() now reads req.parentTraceId and passes it to _notifyLogger,
which forwards it to makeDispatchEvent. Backward-compatible: callers
that omit parentTraceId emit events with parentTraceId: undefined,
identical to P1.3 behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): add trace correlation end-to-end test
Dispatches a root command then 3 children with parentTraceId=rootTraceId.
Reads the real .gsd-trace.jsonl audit file and verifies: 4 events total,
root has no parentTraceId, all children carry rootTraceId, all traceIds
unique, JS filter returns exactly the 3 children given the root's traceId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(#178): document traceId/parentTraceId in audit file
Update Observability section to note that audit events now carry both
traceId and parentTraceId, and explain the correlation filter pattern.
Note that leaf dispatches emit parentTraceId: undefined until the Phase 2
composer wires it automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(#178): add changeset for trace correlation seam
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): cover invalid parentTraceId values in DispatchEvent factory
Adds 9 new test cases for UUID v4 validation of parentTraceId:
empty string, whitespace, non-UUID, oversized, UUID v1, missing-hyphen,
extra-char (all dropped to undefined), plus UPPERCASE and lowercase v4
(both propagated). Tests are intentionally red until the implementation
commit that follows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#178): validate parentTraceId against UUID v4 before propagation
Adds UUID_V4_REGEX constant and isValidParentTraceId() helper to
event.cjs. makeDispatchEvent now silently coerces any parentTraceId that
fails the UUID v4 check (wrong version nibble, wrong variant, missing
hyphens, oversized, empty, etc.) to undefined. No stderr warn is emitted
— the factory remains pure and side-effect-free. Closes the correlation-
poisoning vector identified in the Codex adversarial review of PR #225.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): assert Hub silently drops invalid parentTraceId at the seam
Adds two tests to hub-logger-integration.test.cjs:
1. dispatch with 'junk' parentTraceId emits event with parentTraceId===undefined.
2. The logger-failure warn path is NOT triggered — the factory coerces the bad
value before onEvent is called, confirmed by zero stderr output even when a
logger that would throw on non-undefined parentTraceId is installed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#178): assert invalid parentTraceId does not poison correlation siblings
Adds one test to trace-correlation.test.cjs: dispatches a root, a valid
child (parentTraceId = rootTraceId), and an invalid child (parentTraceId =
'junk'). Asserts: valid child carries correct parentTraceId, invalid child
has parentTraceId dropped to undefined, filtering by rootTraceId yields
exactly 1 event (the valid child only), and all 3 events have unique
traceIds. Uses an isolated Hub + tmpdir to avoid shared fixture interference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(#178): document UUID v4 contract for parentTraceId
Appends one sentence to the Observability audit-trail paragraph in
CONFIGURATION.md: parentTraceId must be canonical UUID v4 (RFC 4122);
values that don't match are silently dropped from audit output. No section
restructuring — single sentence addition only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#177): add DispatchEvent factory failing tests
Red tests for makeDispatchEvent shape, traceId UUID v4, uniqueness,
parentTraceId-always-undefined (P1.3), args redaction toggle, ISO 8601
timestamp, and all result variant passthrough.
* feat(#177): introduce DispatchEvent factory
makeDispatchEvent produces an immutable event record per dispatch:
- traceId: crypto.randomUUID() (UUID v4)
- parentTraceId: always undefined (P1.4 wires composer)
- command, result, timestamp (ISO 8601)
- args only included when includeArgs === true (default: omitted)
* test(#177): add arg redaction policy failing tests
Red tests for shouldIncludeArgs (GSD_AUDIT_ARGS env gating) and
redactEvent (strips args from frozen events, preserves all other
fields, returns a new object, never mutates the source).
* feat(#177): introduce arg redaction policy
shouldIncludeArgs(): only GSD_AUDIT_ARGS==='1' opts in; all other
values (unset, '', '0', 'true') default to omitting args.
redactEvent(event): returns a shallow copy of the event, dropping the
args field unless opted in. Never mutates the (frozen) source event.
* test(#177): add DispatchLogger interface failing tests
Red tests covering:
- no-op logger: silent on all events, never throws
- default logger: silent on ok, one flattened JSON line to stderr on error
- default logger: audit file creation + append-only + redaction + config gate
- GSD_AUDIT env var and config.audit.enabled config gate
- GSD_AUDIT_ARGS opt-in for args inclusion
All tests use real fs under os.tmpdir() — no mocked appendFileSync.
* feat(#177): introduce DispatchLogger with default and no-op implementations
createNoOpLogger(): silent on all events — Hub default when no logger injected.
createDefaultLogger({ cwd, config }):
- Silent on ok result
- Flattened JSON line to stderr on error: { kind, traceId, ...typedPayload }
- Append-only audit at .planning/.gsd-trace.jsonl when GSD_AUDIT=1 or config.audit.enabled
- Args redacted by default; GSD_AUDIT_ARGS=1 opts in
- Logger errors caught internally; never break dispatch callers
* test(#177): add Hub+logger integration failing tests
Red tests verifying:
- onEvent called exactly once per dispatch (ok, error, handler-throw, unknown)
- DispatchEvent shape: traceId uniqueness, command, result.kind, parentTraceId
- Logger errors contained (dispatch still returns Result, warn line to stderr)
- Hub defaults to no-op when no logger injected
- End-to-end with createDefaultLogger: silent on success, stderr on error, audit file
* feat(#177): wire DispatchLogger into CommandRoutingHub
Add optional logger param to createHub({ ..., logger }).
Defaults to createNoOpLogger() — silent, no behaviour change for callers
that don't inject a logger.
After every dispatch (success and error):
- Normalises HubResult { ok } to DispatchEvent { kind: 'ok'|error-kind }
- Calls makeDispatchEvent({ command, args, result }) to mint the event
- Calls logger.onEvent(event) exactly once
- Wraps in try/catch: logger errors emit { level:'warn', source:'DispatchLogger' }
to stderr but never propagate to dispatch callers
* chore(#177): gitignore .planning/.gsd-trace.jsonl audit file
The audit trail is local-only, append-only, and must never be committed.
Slotted under the existing "Local scratch + Claude-test artifacts" block.
* docs(#177): document GSD_AUDIT, GSD_AUDIT_ARGS, config.audit.enabled
New ## Observability section at end of CONFIGURATION.md covering:
- Default silent/stderr behaviour overview
- Stderr error JSON format
- Audit file opt-in (env var and config key)
- Args redaction policy and GSD_AUDIT_ARGS opt-in
Also slots GSD_AUDIT and GSD_AUDIT_ARGS into the existing
## Environment Variables table (alphabetical order).
* chore(#177): add changeset for observability seam
type: Added — new DispatchLogger seam with default silent/stderr/audit behaviour.
* fix(#167): support query meta-command in gsd-tools
* chore(#167): add changeset for query meta-command fix
* fix(#167): pin claude runtime in local-agent regression tests
* test(#3751): stabilize local-agent CI assertions
* refactor(hub): tighten Result<T> to typed-payload-per-kind discriminated union (#176)
Each Hub error variant now carries only its own typed payload. The generic
`errorKind` field is renamed to `kind`; `message`/`details` escape hatches
are removed from Hub-emitted errors. Factory functions (makeUnknownCommand,
makeInvalidArgs, makeHandlerRefusal, makeHandlerFailure) are exported and
used in phase-command-router.cjs. Callers switch on `result.kind`.
Part of ADR-0174 P1.2.
<!-- docs-exempt: no docs/ changes; API is internal to Hub callers -->
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(hub): act on P1.2 review findings (#176)
Addresses 4 review findings on PR #221:
- Hub now runtime-validates ok:false variants against the typed shape
and coerces malformed returns to HandlerFailure with a contract-
violation message (codex finding #1, code-review finding #1)
- catch path now preserves the original throwable for non-Error
throws via an Error wrapper with .thrown attached (codex finding #2)
- All 4 factory returns are Object.freeze'd (review finding #9)
- makeHandlerFailure validates cause is Error; non-Error causes are
wrapped with .thrown attached (review finding #10)
Tests added for each finding (TDD red → green).
Refs #176. Part of #174.
* fix(docs-lint): add docs-exempt markers to both P1.2 changeset fragments
Both `176-typed-result-discriminated-union.md` and `176-hub-p1.2-review-findings.md`
carry `type: Changed` which triggers the docs-required lint. Neither fragment had
a `<!-- docs-exempt: <reason> -->` marker, causing `docs-lint` to fail with
`FAIL_DOCS_MISSING`. Added the per-fragment exemption marker to both (the repo has
no `no-docs` label). This is a purely internal SDK refactor (ADR-0174 P1.2) with
no public docs surface.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#170): update workflow fallback install hint package
* chore(#170): add changeset for workflow fallback hint migration
* fix(#170): mark workflow-hint test as structural text contract
* fix(#166): omit bash.exe wrapper for windows claude sh hooks
* chore(#166): add changeset for windows claude sh hook fix
* fix(#166): reset exitCode in sdk bridge integration test
* fix(#33): add regression test asserting adaptive is reachable in settings.md UI
The schema (sdk/shared/model-catalog.json) defines 5 model_profile values
(quality, balanced, budget, adaptive, inherit). The settings.md AskUserQuestion
previously omitted `adaptive`; the fix (two-question split, #3784) is already
applied. This test locks the schema/UI contract so the gap cannot regress.
Closes#33
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(#33): add changeset fragment
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(#33): address review feedback — presence not count, drop structural caps
Replace exact-count assertion (profiles.length === 5) with presence
assertion (profiles.includes('adaptive')). Remove the options-per-question
≤4 cap guard — that is a structural implementation detail, not a
behavioural regression guard for issue #33.
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).
Under Node 24 on Windows, running node --test with --test-concurrency=4
causes 4 concurrent gsd-tools subprocesses to each spawn a synckit
worker_threads worker for the SDK bridge. The 4 workers simultaneously
contend on SharedArrayBuffer + Atomics.wait under Windows Defender
scanning and NTFS latency, triggering OS-level resource exhaustion that
kills worker processes with empty stderr before any output is flushed.
The symptom: intermittent exit 1 with 0 test failures, varying affected
test files per run, all sharing the pattern of invoking gsd-tools as a
subprocess. Empty stderr distinguishes OS crash from gsd-tools app error
(the error() path writes to stderr before exiting).
Fix: platform-aware concurrency default — 2 on win32, 4 on Linux/macOS.
The existing TEST_CONCURRENCY env-var override is preserved. Also adds
a [stderr: (empty) exit:N] diagnostic note in helpers.cjs runGsdTools
catch block so future empty-stderr crashes are visible in CI logs.
Fixesgsd-build/get-shit-done#3869
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>