The gsd_run resolver preamble now probes
<repo-root>/.claude/get-shit-done/bin/${_GSD_SHIM_NAME} as the second
check — immediately after the existing get-shit-done/bin/ check and
before command -v / $HOME/.claude fallbacks. This covers the install
layout produced by npx @opengsd/get-shit-done-redux@latest --claude --local.
A _GSD_RUNTIME_ROOT variable is introduced to bind the repo-root
expression once and reuse it for both checks without repeating the
git rev-parse subshell.
76 workflow files regenerated via node scripts/sync-runtime-launcher.cjs.
All parity, size-budget, and new regression tests pass.
Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* enh: bump opus-tier model IDs to current GA (Opus 4.8 / codex gpt-5.5)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(#466): changeset for opus-tier model-ID refresh
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
Extends the canonical runtime-launcher snippet with a third resolution arm
that probes $HOME/.claude/get-shit-done/bin/${_GSD_SHIM_NAME} between the
PATH check and the hard-error exit. Global Claude-Code installs (--claude
without --local) with no PATH wiring and no RUNTIME_DIR no longer hit the
hard-error path.
Resolution order: local/RUNTIME_DIR -> PATH -> ~/.claude/... -> hard error.
Propagated to 76 workflow .md files via sync-runtime-launcher.cjs. Parity
test (G) and bug-211 regression test (4 assertions) added.
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>
config-get calls lacked --default, causing stderr noise ("Key not found") and a fragile empty-variable fallback when workflow.nyquist_validation was absent; added --default true (matching schema default). Fixes#138.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a session dies mid-execution (hang, token exhaustion, API drop),
STATE.md's current_phase can be advanced past a phase that still has
PLAN.md files without matching SUMMARY.md files. Without this fix,
/gsd-next and /gsd-progress would route by current_phase and silently
skip the partially-executed phase, producing a data-loss-shape outcome.
Adds a Route 0 cross-phase incomplete-execution scan to both
next.md and progress.md. Before any current_phase-based routing,
the scan finds the lowest-numbered phase where plans outnumber
summaries and routes to /gsd-execute-phase <that-phase> to resume it.
Opt out with --no-resume to fall back to the prior-phase defer prompt;
--force bypasses all gates as before.
Rework (codex review):
- Route 0 now ordered AFTER Gates 1-3 (repo/state validity always run)
but BEFORE the prior-phase completeness-scan defer prompt — eliminating
the double-decision where the default path would both prompt the user
(C/S/F) AND resume the phase anyway. Prior-phase defer prompt moved to
a new prior_phase_completeness step; only reached via --no-resume.
- --force flow made coherent across all three steps: safety_gates jumps
directly to determine_next_action, skipping Gates, Route 0, AND
prior_phase_completeness. resume_incomplete_phase and prior_phase_completeness
now correctly state --force never reaches them. success_criteria entry
updated to reflect --force → determine_next_action (not prior_phase_completeness).
- Scan uses $GSD_SDK (canonical resolver form) throughout next.md, matching
the file's existing convention. progress.md uses $ROADMAP already loaded
by analyze_roadmap. Neither file uses bare gsd-sdk.
- Errors are surfaced rather than suppressed: removed 2>/dev/null on the
main roadmap.analyze call; added explicit WARNING emission when the scan
cannot run, so the invariant fails closed instead of failing open.
- Predicate aligned to plans-without-summaries (plans.length > summaries.length)
in both files, consistent with determine_next_action Route 4.
- command references use canonical /gsd: namespace form
Fixes#160
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(#373): replace unquoted $GSD_SDK with space-safe gsd_run launcher
Workflow bash blocks resolved the runtime as GSD_SDK="node $GSD_TOOLS" and
invoked it unquoted ($GSD_SDK query ...). On install paths containing spaces
(e.g. /Volumes/Mini Me/...) the unquoted expansion word-split into
`node /Volumes/Mini gsd-tools.cjs ...`, failing with "Cannot find module
'/Volumes/Mini'" and getting masked by `2>/dev/null || echo "{}"` into a
silent empty state.
Replace the string variable with a single-line shell launcher that defines a
gsd_run function, invokes the runtime with a fully-quoted path and "$@", and
preserves the local-cjs / installed-gsd-tools-on-PATH fallback (#3668) plus
the loud not-found error and install hint. The launcher uses _GSD_SHIM_NAME
indirection so no workflow emits the /gsd-tools substring that the do.md
dispatcher-parity scanner would misread, and is single-line to stay within
the per-file progressive-disclosure line budgets (#2551).
The canonical launcher lives in
get-shit-done/workflows/_runtime-launcher.snippet.sh, is propagated once per
file by scripts/sync-runtime-launcher.cjs, and is locked by
tests/runtime-launcher-parity.test.cjs (fails CI on drift, on a reappearing
$GSD_SDK token, or on a /gsd-tools substring). Dependent workflow-assertion
tests are updated from $GSD_SDK to gsd_run, and the runtime launcher is
registered in CONTEXT.md.
Fixes#373
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(#373): add changeset fragment and allow-test-rule for parity guard
The runtime-launcher parity test is a structural drift guard that reads
workflow markdown to assert the canonical launcher is present and the
retired $GSD_SDK / /gsd-tools tokens are absent; annotate it with
allow-test-rule per the no-source-grep lint escape hatch. Add the required
.changeset fragment for this user-facing fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(#373): make parity PATH-fallback assertion cross-platform (Windows)
Subtest (E) compared GSD_TOOLS against the Node-side absolute temp path,
but the value originates from git-bash which reports the POSIX form, so the
prefix comparison failed on windows-latest while the launcher itself worked
(the installed stub was invoked). Assert the resolved binary by normalized
suffix (/bin/gsd-tools, not .cjs) instead of the absolute prefix; the
behavioral stub-invocation assertion is unchanged.
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