abd21b2968dda4eedb904f7099b85fb3a0840b4e
322 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
abd21b2968 |
feat(#1680): ADR-1239 Phase C-1 — hook-bus + stateIO seams [AC4] (#1805)
* feat(#1680): ADR-1239 Phase C-1 — hook-bus + stateIO seams [AC4] Phase 3 slice 4 (AC4, final #1680 slice). The last two adapter seams behind the negotiated hookBus/stateIO axes: - src/hook-bus.cts: createHookBus({bus}, {hostEmit?}) -> host/engine/none. engine = in-process pub/sub (handler errors isolated); host = host-owned, fail-closed emit until a host emitter is bound; none = silent no-op (degrade to rule-text). PORTABLE_EVENT_FLOOR = SessionStart/PreToolUse/PostToolUse/ Stop/SessionEnd (the claude dialect all hook hosts share). - src/state-io.cts: createStateIO({io}, {backend?}) -> filesystem (today's behavior — straight fs) / sandboxed-storage / session-log-append (fail-closed seams until a host backend is bound). Proactive CI gates: ADR-457 ignores + INVENTORY-MANIFEST entries for both new .cjs; injection-scan 'act as' substring audit; unused-import check. All clean locally (11 tests + security 15/15 + inventory + eslint 0 problems). Phase 3 (#1680) seam layer now complete. Concrete host binding -> Phase 5 (#1682, D15/D18). * chore(changeset): add Changed fragment for hook-bus + stateIO seams (#1680) |
||
|
|
b152f7e64c |
feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3] (#1804)
* feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3] Phase 3 slice 3 (AC3). Two model-layer adapters selected by the negotiated modelMode axis (host-integration.cts): - passive: formalizes today's tier routing from src/model-resolver.cts — resolveModel delegates straight to resolveModelForTier (byte-for-behavior). This is the CLI runtimes (claude/gemini/codex/opencode/cursor/...): GSD injects prompts / a per-agent model field. - active: a host-supplied sendRequest seam (VS Code vscode.lm / pi providers) — GSD calls the model through the host. Ships as a fail-closed seam (throws until a provider is bound); Phase 5 wires a concrete provider. createModelAdapter({modelMode}, {sendRequest?}) — factory gating throws on invalid mode. Proactive CI gates applied: ADR-457 eslint ignores entry + INVENTORY-MANIFEST cli_modules entry + comment-wording audited for the injection-scan substring trap. * chore(changeset): add Changed fragment for model adapter seam (#1680) |
||
|
|
da368311ea |
feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2] (#1803)
* feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2] Phase 3 slice 2 (AC2). The engine-as-library path: createImperativeAdapter composes loadRegistry({includeInstalled:true}) — first-party-wins + consent + fail-closed gates, identical trust semantics to the CLI — and binds the engine surface behind the SAME HostIntegrationInterface the declarative adapter (AC1) satisfies, plus a registry accessor for the composed capability set. - src/adapter-imperative.cts: createImperativeAdapter({runtime}, {loadOptions}) → ImperativeAdapter (kind:'imperative' + .registry + install/uninstall delegating to install-engine). Thin: delegates the loop, does not reimplement. - tests/adapter-imperative.test.cjs: kind (16 runtimes), registry composition (loadRegistry called with includeInstalled:true), loadOptions pass-through, install/uninstall delegation, fail-closed construction. - eslint.config.mjs + docs/INVENTORY-MANIFEST.json: ADR-457 ignores entry + cli_modules entry for the new emitted .cjs (the two drift gates that bit AC1, applied proactively here). Concrete host binding (OpenCode/VS Code/pi) deferred to Phase 5 (#1682). * test: remove dead readStateMd helper from bug-1760 test readStateMd was defined but never called (writeStateMd is the only state-md helper this test uses). Clears the lone no-unused-vars warning so the repo lints fully clean (0 problems). No behavior change — test still passes 2/2. * chore(changeset): add Changed fragment for imperative embedding adapter (#1680) * fix(adapter-imperative): reword comment to avoid injection-scan substring match The prompt-injection scan regex 'act\s+as\s+(?:a|an|the)' was matching the 'act as the' substring inside 'contract as the declarative adapter' (contrACT AS THE). Reword 'contract as' -> 'shape as' — no 'act' substring, identical meaning. Clears the 'lib source files are clean' + 'codebase prompt injection scan' security-gate failures. |
||
|
|
c642ed0ec5 |
feat(#1680): ADR-1239 Phase C-1 — declarative embedding adapter + minimal HostIntegrationInterface [AC1] (#1802)
* feat(#1680): ADR-1239 Phase C-1 — declarative embedding adapter + minimal HostIntegrationInterface [AC1] Phase 3 slice 1 (AC1). Names + bounds today's projection path behind the common HostIntegrationInterface that both declarative + imperative adapters will satisfy. - src/embedding-adapter.cts: minimal HostIntegrationInterface (kind + runtime + install/uninstall) + ADAPTER_KINDS. The full 6-point binding surface (command/dispatch/model/hooks/state/artifact) is DEFERRED until the imperative adapter (AC2) fixes the shape — ADR-1239 lists the wire-shape as an open question; freezing it now risks rework across Phases 3-6. - src/adapter-declarative.cts: createDeclarativeAdapter({runtime}) factory. Delegates in-process to install-engine installRuntimeArtifacts / uninstallRuntimeArtifacts (the SAME engine functions bin/install.js uses), so output is byte-identical to today's install (gated by golden-install-parity). Module-ref call style = monkeypatch-friendly for tests. Lossy by design: projects files, does not drive the loop (that's the imperative adapter, AC2). - tests/adapter-declarative-equivalence.test.cjs: kind classification (all 16 runtimes), install/uninstall delegation with exact args (the byte-identity link), fail-closed construction (missing/invalid runtime throws). Purely additive — no install.js/install-engine changes. Unblocks AC2 (imperative adapter) + AC3/AC4 (model/hook/state seams) as follow-up slices. * chore(changeset): add Changed fragment for declarative embedding adapter (#1680) * fix(lint): ignore tsc-emitted embedding-adapter/adapter-declarative .cjs (ADR-457) The new src/embedding-adapter.cts + src/adapter-declarative.cts modules' emitted gsd-core/bin/lib/*.cjs artifacts must join the ADR-457 ignores list (lint the src/*.cts source, not the emitted .cjs). Without this, the .cjs is linted under js.recommended where @typescript-eslint/no-require-imports is undefined, so the verbatim-copied eslint-disable directive surfaces as 'Definition for rule not found' — failing the lint-tests CI job. Also restores the clean line-level disable in adapter-declarative.cts (valid in the .cts source context where the rule IS defined). * fix(docs): add adapter-declarative + embedding-adapter to INVENTORY-MANIFEST cli_modules The new ADR-1239 Phase C-1 modules' built .cjs artifacts must be registered in docs/INVENTORY-MANIFEST.json's cli_modules array or the 'docs/INVENTORY-MANIFEST.json matches the filesystem' drift test fails on CI. Mirrors the existing sorted entries. |
||
|
|
4810bfe4df |
refactor(#1679): ADR-1239 Phase B — collapse getConfigDirFromHome chain into getGlobalConfigHomeFragment [AC2 slice 2/6] (#1801)
* refactor(#1679): ADR-1239 Phase B — collapse getConfigDirFromHome chain into getGlobalConfigHomeFragment AC2 slice 2. Collapses the 14-branch runtime->global-config-home source-fragment chain in bin/install.js getConfigDirFromHome (the hook path.join() codegen mapping) into a single getGlobalConfigHomeFragment(runtime) lookup in runtime-name-policy.cts, sibling to getDirName / getRuntimeLabel. The antigravity branch stays dynamic in the caller (resolveAntigravityGlobalDir + path.relative — env-overridable, multi-segment); the prior inner unreachable `if (!isGlobal) return "'agents'"` (dead: !isGlobal returns at the fn top) is dropped. Behavior: byte-identical. Fragments preserved verbatim in the table. - claude/unknown/empty -> default '.claude' fragment - 14 runtimes (copilot..kimi) -> table lookup - antigravity -> dynamic (unchanged) Verification: - TDD: tests/global-config-home-fragment.test.cjs (golden map + 2 drift guards + fallbacks). Red->green. - 16-runtime golden install parity: byte-identical (hook codegen output unchanged) - eslint + test-file-count + regression-names clean - runtime === count in install.js: 115 -> 101 (-14) * chore(changeset): add Changed fragment for getConfigDirFromHome collapse (#1679) |
||
|
|
ad79e99fc9 |
refactor(#1679): ADR-1239 Phase B — collapse runtimeLabel chains into getRuntimeLabel [AC2 slice 1/6] (#1800)
* refactor(#1679): ADR-1239 Phase B — collapse runtimeLabel chains into getRuntimeLabel Collapses the two duplicated runtimeLabel assignment chains in bin/install.js (uninstall() and install()) into a single getRuntimeLabel(runtime) lookup in src/runtime-name-policy.cts — a curated short-form label table, sibling to the registry-derived getDirName precedent. This is slice 1 of AC2 (regional residue-collapse in install.js) under ADR-1239 Phase B / #1679. The install/uninstall console label was the add-a-host tax poster child: a new runtime meant adding a label line to BOTH chains, and they had drifted out of sync: - kimi: install 'Kimi' / uninstall 'Kimi CLI' -> canonical 'Kimi CLI' - cline: install 'Cline' / uninstall (omitted) -> canonical 'Cline' Each canonical value matches the majority chain AND the descriptor title. Behavior: - 14 of 16 runtime labels unchanged in both sites (zero observable change). - 2 unifications (kimi-install, cline-uninstall) move toward consistency. - Unknown/empty runtime id fails closed to 'Claude Code'. - Raw-id lookup only (no alias expansion); callers pass canonicalized ids. Voice: these SHORT UI labels are intentionally distinct from the descriptor title (the long product name) which serves docs/registry display, not the console. A future slice may relocate this to a runtime.label descriptor field. Verification: - TDD: tests/runtime-label-policy.test.cjs (golden map + drift guard + fallbacks) - 16-runtime golden install parity: byte-identical (labels are stdout-only) - 162-test neighbor cluster green; eslint + test-file-count + regression-names clean - runtime === count in install.js: 129 -> 115 (-14, the uninstalled label chain) * chore(changeset): add Changed fragment for runtimeLabel collapse (#1679) PR #1800 touches bin/ → changeset-required gate. Mirrors the sibling ADR-1239 Phase B slice (eager-elks-frolic): type Changed + docs-exempt marker (internal refactor, no user-facing doc surface). |
||
|
|
21f0b4316f |
refactor(#1796): ADR-1769 Path A — finish STATE.md preservation consolidation (#1799)
Extract readModifyWriteStateMd's post-sync preservation block into a pure,
field-classification-table-driven applyStatePreservation in the STATE.md
Transition Module. progress / status / stopped_at now join current_phase_name
as table-governed (getFieldClassification), so a preservation-policy change is
a one-row table edit instead of a per-call-site patch.
This realizes the consolidation ADR-1769 / CONTEXT.md already claimed shipped
('Absorbs readModifyWriteStateMd post-sync preservation block') and routes the
#1264 preservation policy through the single field-classification table — the
bug class is now structurally guarded by the table, not just the call-site
shouldResync flag.
Behavior is byte-identical to the pre-amendment inline block (Hyrum-safe — the
15 readModifyWriteStateMd callers' observable preservation is unchanged):
- state/frontmatter/transition + bug regression suite: 847 pass
- phase/milestone/verify (other RMW consumers): 582 pass
- codex (gpt-5.5/high) adversarial review: CLEAN (58,564-case equiv sweep)
ADR-1769 amendment appended documenting #1796.
Closes #1796
|
||
|
|
79c69d443b |
refactor(#1793): ADR-1769 Phase 7 — sync, prune, update migrations (#1760, #1761) (#1794)
Migrate cmdStateSync, cmdStatePrune, cmdStateUpdate (state.cts) onto the STATE.md Transition Module substrate and close the maintenance bug pair #1760/#1761 (ADR-1769, epic #1769). Completes the substrate: all 10 lifecycle/maintenance transitions now route through transitionCore. - Add {kind: 'sync'|'prune'|'update'} to StateTransitionIntent with syncCore, pruneCore, updateCore in src/state-transition.cts. - updateCore: body-only single-field update (strip/reassemble), mirroring the pre-migration cmdStateUpdate contract. - pruneCore: pure content→content section pruning (Decisions / Recently Completed / resolved Blockers / Performance Metrics rows at or below cutoff), byte-identical tokenizeHeadings splicing. Adapter owns currentPhase, dry-run, and STATE-ARCHIVE.md. - syncCore: body writes (Total Plans in Phase, Progress bar, Last Activity) given disk-derived numbers. Adapter owns the disk scan + roadmap scope. - #1760: cmdStatePrune now derives currentPhase from 'Current Phase' OR 'Phase' (the canonical template emits 'Phase: X of Y'), so prune engages on template-conformant STATE.md instead of bailing 'Only 0 phases'. - #1761: cmdStateSync skips the Progress write (percent=null) when a milestone version is set in frontmatter but the ROADMAP has no versioned heading for it (milestone cannot be bounded). Projects without a milestone version are unaffected. Leaves progress untouched rather than silently writing fallback- derived wrong values. - Regression: bug-1760 (prune engages on template field) + bug-1761 (sync leaves progress untouched when unbounded). ADR-1769 marked Accepted. All 82 transition + 515 regression tests pass. Closes #1793 |
||
|
|
064f63b299 |
refactor(#1791): ADR-1769 Phase 6 — patch migration + curated current_phase_name preserve (#1695) (#1792)
Migrate cmdStatePatch (state.cts) onto the STATE.md Transition Module substrate and close the curated-field clobber bug class #1743/#1695 (ADR-1769, epic #1769). - Add {kind: 'patch'} to StateTransitionIntent, with patchCore in src/state-transition.cts. Applies each caller-supplied {field:value} pair via stateReplaceField over the full content (body + frontmatter), tracking updated vs. failed. data.updated/data.failed mirror the CLI output shape. - Collapse cmdStatePatch to a transitionCore dispatch. Field-name validation (security) and the resync-progress decision stay in the adapter. - #1695/#1743 fix: extend the #1230 delta heuristic in readModifyWriteStateMd to the curated current_phase_name, table-driven via getFieldClassification('current_phase_name').preservation === 'preserve-always'. When a write does NOT change the body Phase: source line, the curated frontmatter value wins over syncStateFrontmatter's body re-derivation (which harvests a wrong parenthetical aside — #1695). begin/planned/complete-phase rewrite their body Phase line, so the delta does not fire for them and current_phase_name still advances. - Regression: bug-1695-state-patch-clobbers-phase-name.test.cjs — unrelated patch preserves curated current_phase_name; patching the Phase source advances. All 70 transition + 493 regression tests pass (state/phase/milestone + #905/#397/#3242 lineages). Closes #1791 |
||
|
|
3ceb83329d |
refactor(#1789): ADR-1769 Phase 5 — milestoneComplete migration (#1790)
Migrate the STATE.md write path inside cmdMilestoneComplete (milestone.cts) onto the STATE.md Transition Module substrate (ADR-1769, epic #1769). - Add {kind: 'milestoneComplete'} to StateTransitionIntent, with milestoneCompleteCore in src/state-transition.cts (consulting the field-classification table). Owns the closure write: Status ('<version> milestone complete'), Last Activity, Last Activity Description, a Current Position reset to 'Awaiting next milestone', and an Operator Next Steps reset pointing at the next-milestone command. - Collapse the inline STATE.md transform in cmdMilestoneComplete to a transitionCore dispatch. The adapter retains writeStateMd (lock + steady-state syncStateFrontmatter post-sync) and resolves the runtime-specific next-milestone slash command, injecting it via intent.nextMilestoneCommand. - The two section resets carry their pre-seam allow-adhoc-markdown waivers (regex semantics pinned by existing tests; pending collectSection #1372). - realClock.today() is byte-identical to milestone.cts's local today (both new Date().toISOString().split('T')[0]). - Characterization tests pin field updates, both section resets (replace + insert paths), and frontmatter #1255 parity. All 66 transition + milestone + state tests pass. Closes #1789 |
||
|
|
91704c9fcf |
refactor(#1786): ADR-1769 Phase 4 — plannedPhase + milestoneSwitch migration (#1788)
Migrate cmdStatePlannedPhase and cmdStateMilestoneSwitch (state.cts) onto the STATE.md Transition Module substrate (ADR-1769, epic #1769). - Add {kind: 'plannedPhase'} and {kind: 'milestoneSwitch'} to StateTransitionIntent, with plannedPhaseCore and milestoneSwitchCore in src/state-transition.cts (consulting the field-classification table). - plannedPhaseCore owns the template-aware Status/Last Activity updates, Total Plans in Phase, Last Activity Description, and the Current Position section (via the inlined mutateCurrentPositionForAdvance twin). The adapter keeps the resync:false readModifyWriteStateMd wrapper (#500 RC1). - milestoneSwitchCore owns the new-milestone reset: rebuilt frontmatter (milestone/name/status='planning'/zeroed progress) + Current Position body reset. The adapter keeps acquireStateLock + platformWriteSync (NOT readModifyWriteStateMd — milestoneSwitch rebuilds frontmatter directly). - Collapse both callbacks to transitionCore dispatches. The now-dead updateCurrentPositionFields helper and KNOWN_STATUS_PATTERNS import are removed (their behavior lives in the transition module's mutateCurrentPositionForAdvance). - Characterization tests pin the template-aware preserve-authored invariant, Total Plans, Last Activity narrative, Current Position update, and the full milestone reset (frontmatter + position body + gsd_state_version preserve + Accumulated Context preserve). All 58 transition + 177 state + phase + bug-2630/bug-905 regression tests pass. Closes #1786 |
||
|
|
6f80524be1 |
refactor(#1784): ADR-1769 Phase 3 — completePhase migration onto Transition Module (#1785)
Migrate the STATE.md write path inside cmdPhaseComplete (phase.cts) onto the STATE.md Transition Module substrate (ADR-1769, epic #1769). - Add {kind: 'completePhase'} to StateTransitionIntent + completePhaseCore in src/state-transition.cts. Pure body field mutations (Current Phase shape/name, Status, Current Plan, Last Activity + Description, Completed/Total Phases + Progress percent), consulting the field-classification table for touched keys. Roadmap progress injected via a new optional deps.roadmapProvider. - Collapse the ~90-line inline STATE.md transform in cmdPhaseComplete to a transitionCore dispatch. The adapter retains updatePerformanceMetricsSection (section table) + syncStateFrontmatter (disk-scan post-sync) and the multi-file atomic transaction (STATE is committed with ROADMAP/REQUIREMENTS, so readModifyWriteStateMd is not used here). - deriveProgressFromRoadmap/clampPercent/stateReplaceFieldWithFallback move from the phase.cts call site into the pure core (no circular dep: phase-lifecycle and state-document don't import state.cts). - Characterization tests in tests/state-transition.test.cjs pin the field updates, roadmap progress derivation, #1255 frontmatter parity, and the 'Phase:' fallback. All 44 transition + 193 phase tests pass. No user-visible behavior change. cmdPhaseComplete CLI output and 'phase complete' behavior unchanged. Closes #1784 |
||
|
|
1512e6f415 |
refactor(#1782): ADR-1769 Phase 2 — advancePlan migration onto Transition Module (#1783)
Migrates cmdStateAdvancePlan (~80-line RMW callback) onto the transitionCore dispatch established in Phase 1 (#1775): - src/state-transition.cts: - Add {kind: 'advancePlan'} to StateTransitionIntent union - Extend StateTransitionResult with optional data field for intent-specific output (advanced, currentPlan, totalPlans) - advancePlanCore: parses legacy + compound plan formats, handles advance vs phase-complete branching, strips frontmatter before body mutation (#1255 pattern — codex Phase 2 HIGH finding), uses stateReplaceFieldIfTemplate for template-default-aware field replacement (Knuth invariant), mutates ## Current Position section via mutateCurrentPositionForAdvance - mutateCurrentPositionForAdvance: inlined section mutation (avoids circular dep with state.cjs's updateCurrentPositionFields) - src/state.cts:cmdStateAdvancePlan: collapsed to 30-line dispatch - tests/state-transition.test.cjs: 5 characterization tests (advance, phase-complete, error, compound format, frontmatter #1255) Codex gpt-5.5/high review: 1 HIGH blocking (frontmatter strip — fixed), 1 medium follow-up (StateTransitionResult.data shape discrimination — noted for Phases 3-7). gsd-test: 21893/21893 PASS (Linux docker). Closes #1782 |
||
|
|
744bb7aaee |
refactor(#1771): ADR-1769 Phase 1 — STATE.md Transition Module substrate + beginPhase (#1775)
* refactor(#1771): ADR-1769 Phase 1 — STATE.md Transition Module substrate + beginPhase Lands the Phase 1 substrate per ADR-1769: - src/state-transition.cts (new Module): - Field-classification table (FieldClass enum + FIELD_CLASSIFICATION rows) - STATE_MD_SECTIONS constants block - Pure transitionCore(content, intent, deps) dispatch - beginPhase intent implementation (first-time + #3127 resume paths) - src/state.cts:cmdStateBeginPhase — collapses ~190 lines to a thin dispatch onto transitionCore via readModifyWriteStateMd. The lock, no-op write guard, and #1230 post-sync delta heuristic stay in the RMW seam; the body-mutation policy moves to transitionCore. - tests/state-transition.test.cjs (24 tests): - Substrate invariants (table enum, section constants) - Characterization: 6 first-time body field updates - Characterization: 5 #3127 idempotency-guard resume behaviors - Characterization: 3 Current Position section mutations - Characterization: Current focus body text line (#1104) - Property (RULESET.TESTS.property-based-testing): beginPhase status propagation + FIELD_CLASSIFICATION own-property contract - Resume Current Position mutation (preserves Plan/Phase/Status) No external behavior change. Full state.test.cjs regression (177 tests) plus bug-3127/#3242/#905/#948 pass. Property tests surfaced two pre-existing quirks (state-document.cjs greedy \s* on whitespace-only field values; Object.prototype method leakage on FIELD_CLASSIFICATION lookups for strings like 'toString') — documented in test comments; fix-out-of-scope for Phase 1. Closes #1771 * refactor(#1771): ADR-1769 Phase 1 codex review corrections Addresses 3 blocking findings from codex gpt-5.5/high review: 1. FIELD_CLASSIFICATION shape (state-transition.cts): - Was flat FieldClass enum (collapsed source + preservation) - Now two-column {source, preservation} rows per ADR-1769 §4 - Added missing fields verified via Memtrace against buildStateFrontmatter (state.cts:1633-1653): gsd_state_version, last_updated, last_activity_desc, progress.{total_phases, completed_phases, total_plans, completed_plans, percent} - Field 2-7 preservation dispatch can now consult the table 2. Prototype-pollution hardening (state-transition.cts): - Table is now Object.freeze(Object.assign(Object.create(null), {...})) - getFieldClassification() helper uses Object.hasOwn; returns null for inherited prototype methods (toString/valueOf/__proto__) - Old code: FIELD_CLASSIFICATION['toString'] returned the function 3. STATE_MD_SECTIONS aligned to canonical template: - Verified against gsd-core/templates/state.md via Memtrace - Was: 8 entries including non-template sections (## Session, ## Decisions, ## Operator Next Steps, ## Session Log, ## Roadmap Evolution) - Now: 6 canonical top-level sections (## Project Reference, ## Current Position, ## Performance Metrics, ## Accumulated Context, ## Deferred Items, ## Session Continuity) Also: beginPhase now consults getFieldClassification() per touched field (codex finding: 'table not consulted by transitionCore'). Unknown fields raise immediately — adding a field without a table row is caught at runtime. Repo-hygiene catches from gsd-test (not node --test, which missed these): - gsd-core/bin/lib/state-transition.cjs added to eslint.config.mjs ignore list (ADR-457 tsc-generated) - docs/INVENTORY-MANIFEST.json regenerated via node scripts/gen-inventory-manifest.cjs --write Property test for Object.prototype leakage tightened to verify getFieldClassification() returns null for toString/valueOf/__proto__. Ref #1771 * fix(#1771): cast Object.create(null) to satisfy @typescript-eslint/no-unsafe-assignment ESLint CI failed on src/state-transition.cts:72:14 — Object.create(null) returns `any`, which leaked through Object.assign to the typed `FIELD_CLASSIFICATION` declaration. Adding an explicit cast to `Record<string, FieldClassification>` eliminates the unsafe-assignment while preserving the null-prototype protection codex review recommended. gsd-test: 21888/21888 PASS. * fix(#1771): add 'see #1771' to allow-test-rule exemption per ADR-456 CI lint-allow-test-rule-refs failed: 'New allow-test-rule exemption without an issue ref — add `see #NNN` per ADR-456'. Updated comment on tests/state-transition.test.cjs to reference the Phase 1 issue. * fix(#1771): remove unnecessary allow-test-rule exemption The exemption was added speculatively. The test file does not use readFileSync + .includes()/.match()/.startsWith() on source content — it calls transitionCore() with string literals and verifies results via stateExtractField() and array .includes() on the updated[] array. No exemption needed. |
||
|
|
4ced0a64cc |
feat(#1561): assumption-delta advisory checkpoint (#1767)
* feat(#1561): assumption-delta advisory checkpoint * chore(#1561): backfill changeset PR number (#1767) --------- Co-authored-by: review-bot <review-bot@gsd> |
||
|
|
b0d5ca3379 |
feat(#1517): support custom reviewer instances for /gsd:review (#1766)
* feat(#1517): support custom reviewer instances for /gsd:review Add a bounded review.reviewer_instances config surface so one model-capable adapter (e.g. opencode) can run as several independent reviewer identities in a single /gsd:review pass. Instances participate only via review.default_reviewers, expand before built-in slugs, are available iff their cli is detected, and a non-matching entry is a hard error (typo must be loud). >=2 same-cli instances emit a shared-adapter caveat in REVIEWS.md. Default path with no instances is byte-for-byte unchanged. Single-source instance->cli resolution lives in resolveReviewerSelection / normalizeReviewerInstances (parity-locked in tests/review-reviewer-instances.test.cjs). cli validated against KNOWN_REVIEWER_SLUGS only (never arbitrary shell); model/agent opaque, never shell-interpolated. Closes #1517 * chore(#1517): backfill changeset pr:1766 --------- Co-authored-by: review-bot <review-bot@gsd> |
||
|
|
ce62f2b68d |
refactor(#1763): ADR-1235 agent migration — cut over the trivial-converter group to the descriptor path (#1764)
ADR-1235 step 1: route the trivial-converter runtime group (cursor, windsurf, augment, trae, codebuddy) off the inline install() agent loop onto the descriptor-driven installRuntimeArtifacts path. Establishes the converter-context foundation (pre-converter cross-cutting + no agent-stamp). Agent install output is byte-identical for all 16 runtimes (golden-parity, global + verified local). cline deliberately excluded (local rules-only). Closes #1763. |
||
|
|
dcd1d7f973 |
fix(#1693): don't double-quote $CLAUDE_PROJECT_DIR-anchored hook paths on Windows (#1746)
* fix(#1693): don't double-quote $CLAUDE_PROJECT_DIR-anchored hook paths on Windows The installer's #2979 legacy-node rewrite ran every managed node hook path through JSON.stringify on Windows. For local installs the path already carries a "$CLAUDE_PROJECT_DIR"-anchored quoted prefix, so stringifying produced "\"$CLAUDE_PROJECT_DIR\"/...". Node then received an argument starting with a literal " , treated it as relative, and failed MODULE_NOT_FOUND — breaking every node managed hook at once (a self-locking PreToolUse-guard deadlock). projectLegacySettingsHookCommand now emits an already-anchored token verbatim and only JSON.stringify-quotes bare absolute paths (which may contain spaces). Scoped to win32 so non-Windows token-shape behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#1693): add changeset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a3d3c2a445 |
refactor(#1756): derive getDirName from a documented runtime.localConfigDir descriptor axis (#1757)
ADR-1239 Phase B (parent #1679). getDirName was a hand-maintained 15-branch if-chain mapping each runtime to its local content-rewrite dot-dir. Relocate those values into a documented runtime.localConfigDir descriptor field; derive getDirName from registry.runtimes[id].runtime.localConfigDir (fallback .claude). - 16 capability.json gain runtime.localConfigDir (byte-identical values) - capability-validator.cjs requires it (non-empty dot-dir); registry regenerated - docs/reference/capability-manifest.md documents the field + the three divergent values (copilot=.github, antigravity=.agents, kimi=.kimi-code) - drift-guard test: golden value map + key-set equality both ways Byte-identical install output for all 16 runtimes (golden-parity harness #1730). Closes #1756 Co-authored-by: review-bot <review-bot@gsd> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e075a41c86 |
feat(#1754): CLI version-skew detection — warn when a global install shadows project-local GSD (#1755)
* feat(#1754): CLI version-skew detection — warn when a global install shadows project-local GSD Addresses #1754 (approved-enhancement). Detects when the running gsd-tools.cjs is outside the project root while a project-local install exists — the shadowing scenario from #1748 where a stale global canary CLI (retired @gsd-build/sdk) silently overrides project-local GSD. Implementation (Node CLI entry-point, not shell snippet — avoids bloating 93 workflow files past their size caps): - src/cli-skew-check.cts: pure function checkCliSkew({resolvedPath, projectRoot, projectLocalExists}) → string|null. Compares paths via path.relative; returns a warning when the resolved CLI is outside the project root AND a project-local install exists. Includes @gsd-build/sdk removal hint when the path matches. No I/O (pure), no gsd-sdk literal (avoids bug-2801 lint). - gsd-core/bin/gsd-tools.cjs: wired at startup via the existing findProjectRoot resolver. Non-blocking (try/catch; advisory stderr warning, never gates). - eslint.config.mjs: registers the new ADR-457 generated artifact in the ignores. - tests: 6-case suite (skew/no-skew/legacy/normalization); all green. - Golden fixtures regenerated (UPDATE_GOLDEN=1) for the new compiled artifact. - docs/how-to/update-gsd.md: Diátaxis reference note for the skew warning. Full suite: 3354 pass, 0 regressions (1 pre-existing local AGENTS.md failure). lint:ci green. Closes #1754 * chore(#1754): backfill changeset pr placeholder * chore(#1754): regenerate INVENTORY-MANIFEST for the new cli-skew-check source module --------- Co-authored-by: review-bot <review-bot@gsd> |
||
|
|
871621c3c8 |
feat(#1740): require-fs-op-fallback production AST rule + Windows transient-lock retry (Phase 6) (#1742)
* feat(#1740): require-fs-op-fallback production AST rule + Windows transient-lock retry (Phase 6) ADR-1703 Phase 6 of the cross-platform portability epic (#1702). Adds the second production-code portability AST rule + the ADR-mandated glob expansion to bin/install.js and scripts/build-hooks.js. - eslint-rules/require-fs-op-fallback.cjs: flags an unguarded fs.rename / fs.renameSync (the atomic-publish primitive named first in DEFECT.WINDOWS-FS-OPS.symptom) that is NOT inside a try/catch whose handler references a transient errno ('EPERM'/'EBUSY'/'EACCES' or a *RETRY_ERRNOS set) AND NOT behind a Windows platform guard. A catch that silently swallows or cleans-up-and-rethrows without an errno check does NOT satisfy the defect's 'never silently swallow' clause. copyFile/unlink are deliberately not flagged (they are the fallback primitives per the defect's own fix-forward). Scope narrowed to rename per Phase 5's precision discipline; documented on #1740. - src/shell-command-projection.cts: export retryRenameSync(from, to) — the drop-in bounded-retry helper over the existing atomicRenameWithRetry. - 27 bare fs.renameSync sites across 11 modules routed through retryRenameSync (capability-lifecycle/lock/source, installer-migrations, milestone, phase, planning-workspace, roadmap-upgrade, runtime-hooks-surface, state, workstream). Idempotent on POSIX; resilient to AV/indexer transient locks on Windows. - eslint.config.mjs: register rule at error on src/**/*.cts; new focused portability-rules block covering bin/install.js + scripts/build-hooks.js (ADR-1703 L124-126 glob expansion — both files are compliant: zero rename violations). - tests: 15-case RuleTester suite; portability-rule-disable-ban extended (PROTECTED_RULES + scans bin/install.js/build-hooks.js with shebang handling); ci-test-scope portability-lint selection rule. - CONTEXT.md DEFECT.WINDOWS-FS-OPS predicate rewritten to point at the rule; docs/contributing/cross-platform-portability-rules.md reference + how-to. Closes #1740 * chore(#1740): backfill changeset pr:1742 * fix(#1740): tighten require-fs-op-fallback precision (codex review HIGH-1/HIGH-2) Addresses two false-negative findings from the codex (gpt-5.5/high) adversarial review of PR #1742: HIGH-1 — a catch that REFERENCES a transient errno but only rethrows (no retry/fallback) was marked compliant. The DEFECT.WINDOWS-FS-OPS fix-forward requires retry, not just recognition. Fix: catchHandlerHasRetrySignal now requires a loop `continue` backedge OR a `return <call>` delegation; a bare rethrow is flagged. The misleading `/* retry logic */` valid test is replaced with a real retry loop, and the rethrow-only shape is added as invalid. HIGH-2 — the nested-try ancestor walk treated an OUTER errno-catch as protecting the rename even when an INNER catch intercepted/swallowed the error (the outer catch is unreachable). Fix: isInsideTransientErrnoTryCatch now stops at the NEAREST enclosing TryStatement WITH A CATCH HANDLER whose block contains the rename (try-finally is skipped — it doesn't catch); outer catches are no longer consulted. The unsound nested-try valid test is converted to invalid, and a try-finally-skipped valid case is added. Verified: 17 RuleTester cases pass; zero new production violations (the 27 fixed sites use retryRenameSync; the real retry loops — atomicRenameWithRetry, capability-ledger/consent, build-hooks — remain compliant via continue/errno); lint:ci green; disable-ban + vocab-drift green. --------- Co-authored-by: review-bot <review-bot@gsd> |
||
|
|
9d52043f50 |
feat(#1733): normalize-path-in-content production AST rule + fix Windows agent-skills content leak (Phase 5) (#1736)
* feat(#1733): normalize-path-in-content production AST rule (Phase 5) ADR-1703 Phase 5 — the first production-code rule. local/normalize-path-in-content (src/**/*.cts, @typescript-eslint/parser): flags a path-returning fn result (path.basename excluded — returns a separator-less filename) interpolated into an @-reference / config-dir markdown body without .replace(/\\/g,'/') normalization, per RULESET.CONTENT-PATH-NORMALIZATION / DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT. Build-and-assess found the canonical defect site (computePathPrefix) already compliant and only 1 src/ hit — a false positive (path.basename in a status message) — eliminated by narrowing (exclude basename; require a real @-ref/ config-dir marker, not bare .md). 0 src/ violations: clean forward-prevention. The out-of-band disable-ban now scans src/**/*.cts too (typescript-estree) so the production rule also cannot be eslint-disabled. Registered (error) + PROTECTED_RULES; CONTEXT.md predicates + how-to doc updated. - RuleTester suite (26 cases) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1733): add changeset for Windows agent-skills path-leak fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: harden mutation-matrix.cjs stdin read against EAGAIN on non-blocking pipe scripts/mutation-matrix.cjs read piped stdin via readFileSync(process.stdin.fd). On macOS libuv marks the stdin pipe fd non-blocking, so a synchronous read can throw EAGAIN before the writer fills the pipe — intermittently, under heavy CI shard load — aborting the script (status 2) and flaking mutation-matrix-ratchet. Replace with readStdinSync(): an fs.readSync loop that retries on EAGAIN (1ms synchronous Atomics.wait yield), stops on 0-byte/EOF, and rethrows other errors. Deterministic regression test injects EAGAIN via an fs.readSync monkeypatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: re-run golden-install-parity on src/lib + installer changes (close drift guard) golden-install-parity hashes every installed bin/lib/*.cjs per runtime, so it must re-run whenever the built lib could change. ci-test-scope selected it for neither src/** nor installer changes, so a source-only edit (e.g. #1691's milestone.cts/roadmap.cts) recompiled bin/lib and silently drifted the golden fixtures past the scoped lane. Add golden-install-parity.test.cjs to both the 'TS runtime sources' and 'installer and package layout' selection rules, with behavioral regression tests for each. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: review-bot <review-bot@gsd> |
||
|
|
b307c4cfde |
refactor(#1734): extract install engine from bin/install.js (ADR-1239 Phase B deep move) (#1735)
* refactor(#1734): extract install engine from bin/install.js (ADR-1239 Phase B deep move) Relocate the runtime-artifact install cluster out of the 12,490-line bin/install.js into a dedicated src/install-engine.cts -> install-engine.cjs: installRuntimeArtifacts, uninstallRuntimeArtifacts, installOpencodeFamilySkills, and their cluster helpers (_copyStaged, snapshot/restore, legacy migration, GSD-entry pruning, preserve/restoreUserArtifacts, OpenCode-family converters, USER_OWNED_ARTIFACTS). - bin/install.js imports the engine and re-exports the moved symbols for back-compat; getCommitAttribution STAYS in install.js (impure config I/O + argv explicitConfigDir global) and is injected via a resolveAttribution param. - 17 test files migrated to import the moved symbols from the engine. - Bookkeeping: eslint built-artifact ignore, .gitignore, INVENTORY manifest+row, CONTEXT.md Install Engine Module glossary seam. Behaviour-preserving: install output is byte-identical for all 16 runtimes (golden-parity harness #1730) — the only delta is the new install-engine.cjs file shipping in the installed gsd-core/bin/lib/ tree. Closes #1734 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1734): backfill changeset PR number (#1735) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: review-bot <review-bot@gsd> |
||
|
|
6414249d25 |
fix(#1580): exclude 0/999 sentinels from milestone-complete guard and roadmap analyze (#1691)
* fix(#1580): exclude 0/999 sentinels from milestone-complete guard and roadmap analyze Closed #1445 added the `^999` backlog-sentinel exclusion to the progress denominators but missed two other resolvers, leaving two user-facing failures live on a milestone whose only directory-less ROADMAP heading is a backlog sentinel: (A) `milestone complete` was blocked by the unstarted-phase guard in src/milestone.cts — it flagged `### Phase 999: Backlog` as an unstarted phase and refused to close a fully-shipped milestone without --force. (B) `roadmap analyze` (src/roadmap.cts) counted the sentinel in phase_count and routed `next_phase` straight into Phase 999. Both now skip Phase 0 (pre-milestone) and Phase 999 (backlog) sentinels, mirroring the engine-wide convention (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS, the #1445 progress filters). Symptom (C) (state.cts total_phases) was already fixed inline by #1445/#1514 and is out of scope here. Regression coverage folded into tests/fix-1445-*.test.cjs (scenarios C and D); updated tests/bug-978 fixture to use a real unstarted phase (Phase 2) instead of a 999 sentinel, since the sentinel is now correctly excluded from that guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#1580): add changeset for 0/999 sentinel exclusion fix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2990305f78 |
refactor(#1727): derive NON_CLAUDE_RUNTIMES from the capability registry (ADR-1239 Phase B) (#1728)
* refactor(#1727): derive NON_CLAUDE_RUNTIMES from the capability registry ADR-1239 Phase B (parent #1679). NON_CLAUDE_RUNTIMES was a hand-maintained 15-element literal whose own doc-comment said "keep in sync with bin/install.js and getDirName()" — a parallel source of truth that can drift from the capability registry. Derive it instead: Object.keys(capabilityRegistry.runtimes).filter(id => id !== 'claude').sort() The exported value is byte-identical to the old literal (the registry's runtimes key set minus claude is exactly the 15 entries), so there is no observable behavior change; the list can no longer drift from the registry. capability-registry.cjs is a committed, dependency-free data module (no cycle). Drift-guard test: golden-oracle deepEqual (non-circular) + a role-based cross-check from the registry metadata + every member must have a non-'.claude' getDirName branch (a registry runtime missing a getDirName branch now fails CI). Closes #1727 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1727): backfill changeset PR number (#1728) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1727): put docs-exempt marker on its own line so parse.cjs extracts it DOCS_EXEMPT_RE is line-anchored (^...$ + m flag); the marker only counts on its own line. It was appended to the end of the body text, so it was never extracted and docs-lint failed in CI (fail_docs_missing). Verified via direct parse.cjs extraction (docsExempt now non-empty, marker stripped from body). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cf2e66b39e |
feat(#1708): typed documentation-sourced #853 dispatch-flatten (ADR-1239 Phase B) (#1719)
* feat(#1708): typed documentation-sourced #853 dispatch-flatten Graduate the #853 orchestrator-backgrounding decision from a scattered RUNTIME==='codex' prose check to a typed, documentation-sourced engine decision. Adds a backgroundDispatch dispatch sub-axis (sourced per host: codex+cursor documented true, 9 documented false, 5 undocumented), shouldFlattenDispatch(dispatch) (inline UNLESS background && backgroundDispatch, fail-closed), and a gsd_run query dispatch-should-flatten the plan/execute workflows call. Cursor is newly background-eligible per its docs (inline->background) — a documentation-justified behavior change. No RUNTIME-name residue for this decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1708): backgroundDispatch citations in matrix + CONTEXT note Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1708): address review findings on typed dispatch-flatten Code/adversarial review: convert the manager.md/autonomous.md Compound Action preamble from hardcoded 'On Codex' to FLATTEN-based branching (the handlers already use the query; the preamble contradicted them and was wrong for cursor); make shouldFlattenDispatch null-safe + type-honest (accepts raw 'undocumented' registry values); make backgroundDispatch a required descriptor field (matching its siblings, all 16 carry it); strengthen the config.runtime behavioral test; update the bug-853 prose-pin test + comment. Security review clean; Codex confirmed no fail-open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): backfill backgroundDispatch in role:runtime test fixtures Making backgroundDispatch a required descriptor field broke role:runtime fixtures in capability-manifest-version/capability-registry/host-integration-descriptors tests that build a dispatch object without it (caught by full gsd-test, not scoped npm test). Backfill backgroundDispatch:false into the well-formed fixtures; the deliberately-malformed 'required-field' test fixture is left malformed by design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): update fix-1521 dispatch-gating assertion to the FLATTEN gate fix-1521 pinned the codex-specific run_in_background prose that #1708 graduated to the typed dispatch-should-flatten/FLATTEN gate. Update its assertions to verify FLATTEN=false gating (not a runtime name) + that the old RUNTIME===codex gate is gone. Caught by full gsd-test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1708): add changeset for typed dispatch-flatten Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1708): remove stray temp PR-body file Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1708): add issue ref to bug-853 allow-test-rule annotations ADR-456 requires every allow-test-rule exemption to carry a see #NNN reference; the source-text-is-the-product annotations added when migrating the prose assertions lacked it (lint-tests CI gate). Add (see #1708). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
fb5f89db10 |
feat(#1704): destSubpath write-confinement (ADR-1239 Phase B) (#1706)
* feat(#1679): confine install writes within configHome ADR-1239 Phase B write-confinement: a pure assertDestWithinConfigHome(configDir, destSubpath) rejects a destSubpath that escapes configHome (path traversal / NUL byte) at plan-build time on BOTH the install and uninstall plan paths; surface.applySurface and installOpencodeFamilySkills route through it, and _copyStaged carries a defense-in-depth containment check. Security-load-bearing for the Phase C third-party-descriptor loader. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1704): add changeset for destSubpath write-confinement Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1704): fix windows path-portability in confinement test The N1 'accepts a true child subpath' assertion compared against path.join (no drive resolution) while the helper uses path.resolve — on Windows that mismatches the C: drive prefix. Compute the expected via path.resolve to mirror the helper. Windows-CI-only failure (local gsd-test is Mac+Linux). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
30d4b85de5 |
feat(#1684): negotiated host-integration interface (ADR-1239 Phase A) (#1690)
* feat(#1684): add negotiated host-integration interface module ADR-1239 Phase A: a pure, additive, no-I/O module exposing PROTOCOL_VERSION, the 8-axis HOST_INTEGRATION_AXES closed vocabulary, the UNDOCUMENTED fail-closed sentinel, negotiateHostCapabilities (effective subset of host-declared and engine-known), a typed degradation ladder, and host-capability profiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1684): validate and document host-integration axes (16 runtimes) Extend validateRuntimeBody to validate the 8 hostIntegration axes (closed enums + undocumented sentinel + dispatch struct + reserved-key guards) and the widened runtime vocabulary; author a documentation-sourced hostIntegration block in all 16 runtime descriptors; regenerate the registry. Every per-CLI value is documented (cited) or the explicit undocumented sentinel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1684): add host-integration capability matrix and adr amendment New per-CLI, per-axis citation reference (value/source/evidence for all 16 CLIs); ADR-1239 Phase-A-implemented amendment; CONTEXT.md glossary seam entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1684): harden dispatch negotiation edge cases Code-review hardening: treat NaN/Infinity maxDepth as missing (fail-closed, +warning); reset nested/background when namedDispatch collapses to false (struct consistency); SAFE_DEFAULTS dispatch floor to read-only; warn on non-finite protocolVersion; symmetric undocumented warnings for dispatch fields. Pure module — no consumers; behaviour fail-closed throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1684): register host-integration.cjs in lint-ignore and inventory New tsc-generated bin/lib artifact: add to the eslint ignore list (ADR-457 — lint the .cts source), regenerate docs/INVENTORY-MANIFEST.json, and add the docs/INVENTORY.md CLI-modules row. Fixes the 3 gsd-test failures (551-eslint-bin-lib-coverage x2 + inventory-manifest-sync). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1684): add changeset fragment for host-integration interface Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1684): add how-to for sourcing a host's integration axes Diataxis how-to guide for adding/updating a host's runtime.hostIntegration axes from authoritative docs, the undocumented-sentinel rule, validation, and extending the closed vocabulary. Completes the Step-5 doc quadrants (reference + explanation + how-to). Indexed in docs/README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cbf7c82841 |
feat(#323): fish-shell support in post-install PATH suggestion (#727)
* feat(#323): fish-shell support in post-install PATH suggestion Two additive changes to the post-install PATH-suggestion seam, both scoped to existing functions. A. Projection: add a fish entry to the persist-mode shell-action list in projectPathActionProjection() (src/shell-command-projection.cts). fish has no `export`/`$PATH`-list syntax, so the existing zsh/bash `export PATH=...` commands are inert when pasted. The new entry emits the fish-native `fish_add_path '<dir>'` (fish 3.2+, persists via the universal-variable store, de-duplicating). The directory is single-quoted with the same POSIX literal escaping as the zsh/bash siblings; verified round-tripping through real fish 3.7.0 for paths containing quotes, spaces, `$`, `*`, backticks and unicode. B. Detection: add homePathCoveredByFishConfig() in bin/install.js, called from maybeSuggestPathExport() alongside homePathCoveredByRc(). fish does not use sh-style `export PATH=` rc files, so a fish user whose fish_user_paths already covers the global bin would otherwise get a false-positive "not on your PATH" warning on every install. Two side-effect-free detection routes (no fish subprocess): 1. The universal-variable store (~/.config/fish/fish_variables). fish serializes this with `full_escape`: every byte outside [A-Za-z0-9/_] becomes `\xHH` (space -> \x20, `-` -> \x2d, `.` -> \x2e, `$` -> \x24, unicode -> \uXXXX) and list elements are joined by the literal 4-char token `\x1e` (NOT a raw 0x1e byte). The detector splits on `\x1e`, decodes the escapes, then compares each as an absolute literal — a decoded `$` is part of the directory name, not an unexpanded variable. Verified against real fish 3.7.0 output. 2. config.fish (`fish_add_path`, `set -gx PATH`, `set -Ux fish_user_paths`) — plain shell tokens: HOME forms ($HOME/${HOME}/~) are expanded and a token still holding `$` (e.g. `$PATH`, `$fish_user_paths`) is skipped. Honours $XDG_CONFIG_HOME and always also checks ~/.config/fish. No behaviour change for bash/zsh/PowerShell/cmd/Git-Bash users: their entries and command strings are unchanged; the fish entry is additive and the fish detector only narrows the set of cases that warn. Tests: update the projection length assertion (2 -> 3) and fish escaping in bug-3441; add fish detection + suppression cases in install-path-detection (uvar store with real fish escaping, dot/hyphen/space/$-literal decode regressions, config.fish routes, commented-out, relative-segment guard, unreadable-file fault injection, suppression and emission via maybeSuggestPathExport). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(changeset): add Changed fragment for #323 fish PATH support (#727) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#323): address review — action-only fish docs, decoder property test, win32 guard Addresses @trek-e's review on #727: - docs (blocker): keep the how-to action-only (Diátaxis). Drop the `# fish — persists via …` comment and the internal-mechanism clause naming fish_variables/config.fish; leave one command + the exec-fish directive. - tests (minor): extract decodeFishUniversalValue to a pure, exported module function and add fast-check round-trip properties (decode(fishEscape(p)) === p over arbitrary unicode, abs-path variant, totality). Consolidated into install-path-detection.test.cjs to respect the install test-file-count ratchet. - tests (follow-up): port #721's win32 negative-projection test (no fish action on win32; persist projection is PowerShell/cmd.exe/Git Bash). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#323): address review — drop unused 'after' import, clarify escaping comment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
1a46109b97 |
enhance(#1579): deterministic gsd-tools query eval.score verb (#1583)
* feat(#1579): deterministic gsd-tools query eval.score verb Split C of #1573 (pure code, lowest risk). Adds an eval.score query verb (coverage*0.6 + infra*0.4; bands 80/60/40) mirroring the verify.* chain; gsd-eval-auditor consumes it instead of doing weighted arithmetic in-prompt. Non-breaking — additive only. arXiv: 2601.15130 (Plausibility Trap/DPDM), 2507.10281 (Table Agent), 2508.15754 (TIR). * fix(#1579): address review — domain guard, property test, glossary, SKIP_ROOT, inventory/baseline - C3 input-domain: reject out-of-domain eval.score (require 0<=covered<=total; was emitting overall_score>100 / negatives) - C1 property test: add tests/eval.property.test.cjs (fast-check) — determinism, band monotonicity, [0,100] bounds, never-throws - C2 glossary: CONTEXT.md "Eval Scoring Module" entry (source-of-truth path + interface) - C4: add `eval` to SKIP_ROOT_RESOLUTION (pure arithmetic; no .planning/ access) - inventory: register generated eval.cjs/eval-command-router.cjs (INVENTORY-MANIFEST.json + INVENTORY.md rows) - size: regen agent-size baseline for gsd-eval-auditor (reused gsd_run shim + eval.score step) - eslint: ignore generated eval*.cjs (ADR-457 bin/lib migration coverage) * fix(#1579): register eval family in alias-drift gates Add EVAL_COMMAND_ALIASES/EVAL_SUBCOMMANDS to scripts/check-alias-drift.cjs families and to familyArrayKeys in the manifest-coverage test, so the eval family lands under the same drift guard as every sibling family (state/verify/init/phase/phases/validate/roadmap). Addresses trek-e review. check:alias-drift ok; feat-3251 coverage 9/9; eval suites 10/10. * docs(#1579): use half-open verdict band ranges in CLI-TOOLS overall_score is fractional and thresholds are >=80/>=60/>=40, so a score in [79,80) is correctly NEEDS WORK despite the old '60-79' label. Relabel bands as 60-<80 / 40-<60 / 0-<40 to match the code. Addresses trek-e nit. * fix(#1579): validate eval.score CLI inputs Reject unknown infra tokens and fractional counts, and pin the 80-point verdict boundary including rounding-before-banding behavior. |
||
|
|
3870fafe74 |
fix(#1571): resolve schema-drift phase by token, not substring (#1640)
* fix(#1571): resolve schema-drift phase by token, not substring verify schema-drift <phase> resolved the phase directory with a naive entry.name.includes(phaseArg) test, so a non-existent phase could silently match a different phase whose directory name merely contained the requested token (e.g. "1" matched "11-expansion"), running the drift gate against the wrong phase. Use the canonical phaseTokenMatches + normalizePhaseName, matching find-phase, verify phase-completeness, and this file's own unstarted-phase check. Regression coverage folded into tests/schema-drift.test.cjs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#1571): add changeset for schema-drift token-match fix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c583bcc02c |
fix(#1659): dedup By-Phase rows across padded/unpadded phase numbers (#1663)
* fix(#1659): dedup By-Phase rows across padded/unpadded phase numbers phaseRowPattern matched the phase number literally (escapeRegex(String(phaseNum))), so a seeded zero-padded row '| 05 |' was not matched by 'phase complete 5' (pattern '| 5 |'), producing a duplicate row that double-counted the phase. Canonicalize a numeric phase to its integer form (Number('05')===Number('5')===5) and match with a 0* prefix so 5/05/005 all collapse to the same row in either direction. Regression folded into state.test.cjs: seeded '| 05 |' + 'phase complete 5' yields exactly one phase-5 row. Non-numeric phase IDs retain the literal escapeRegex match. * chore(#1659): backfill changeset pr ref to 1663 * fix(#1659): add verification fixture to padded-dedup test under #1522 gate |
||
|
|
ff161f2281 |
fix(#1582): derive phase-complete velocity from By-Phase table (idempotent) (#1655)
* fix(#1582): derive phase-complete velocity from By-Phase table (idempotent) updatePerformanceMetricsSection blind-added summaryCount onto the prior velocity total on every phase complete, so re-running phase complete on an already-complete phase incremented the total each time (the sibling of #4, which fixed the Completed Phases counter the same way). The velocity total is now derived as the sum of the By-Phase table's Plans column AFTER the row upsert — re-completing a phase upserts the same row, so the sum is stable; a hand-edited inflated total self-heals downward to the true sum on the next completion. When the By-Phase table is absent the total is left unchanged (no crash). Strengthens the misnamed 'idempotent' test (its comment explicitly declined to assert velocity idempotency — the latent gap) and adds a self-heal regression; corrects the #320 behavior-lock velocity assertion which had encoded the blind-add (3 = 1+2 double-count) — the derived value is 2. * chore(#1582): backfill changeset pr ref to 1655 * fix(#1582): velocity sum tolerates indented By-Phase rows (codex review) Adversarial review (codex, gpt-5.5/high) flagged that byPhaseTablePattern's data-row capture allows leading whitespace ([ \t]*\|), but the derive sum was anchored at ^\| and would skip indented hand-edited/legacy rows — capturing them in the table but silently undercounting. Align the sum regex (^\s*\|) with the table capture's tolerance. Adds an indented-row regression. Two other codex findings are pre-existing and out of scope: padded/unpadded phase dedup (phaseRowPattern, identical in old code — derive yields the same value as the old blind-add) and CRLF tables (the shared byPhaseTablePattern header requires bare \n, so the upsert was already broken on CRLF; the fix changes stale-vs- double-count, does not worsen it). * fix(#1582): add verification fixtures to velocity tests under #1522 gate Post-rebase onto next+#1548, the #1582 velocity tests (self-heal, indented-row) use phase complete, which now fail-closes under #1522's canonical verification gate without a passed *-VERIFICATION.md. Add writePassedVerification(tmpDir,'02-next','02') to both. |
||
|
|
80607bec93 |
fix(#1658): make byPhaseTablePattern CRLF-tolerant on STATE.md tables (#1662)
* fix(#1658): make byPhaseTablePattern CRLF-tolerant on STATE.md tables byPhaseTablePattern required a bare \n after the header and separator rows, so a STATE.md with CRLF (\r\n) line endings (Windows, or hand-edited) had its By-Phase table treated as absent: phase complete never upserted the row (and the velocity-from- table derivation went stale). Make the header/separator terminators and the closing lookahead CRLF-tolerant ([ \t]*\r?\n, (?=\r?\n|$)). Backward-compatible with LF. Regression folded into tests/state.test.cjs: phase complete on a CRLF STATE.md upserts the row and removes the placeholder. CONTRIBUTING's QA matrix lists Mixed CRLF/LF as a required parser case. * chore(#1658): backfill changeset pr ref to 1662 |
||
|
|
e1d768dd78 |
fix(#1660): fail-closed frontmatter set of object-list fields instead of silent no-op (#1664)
* fix(#1660): fail-closed frontmatter set of object-list fields instead of silent no-op cmdFrontmatterSet reported {updated:true} even when spliceFrontmatter returned the content unchanged, which happened whenever the new value's extractFrontmatter projection equalled the original's — notably for object-list fields like must_haves, whose {path,provides} items flatten to scalar strings under the lossy parser. Detect a no-op (newContent === content) for a dict-valued field and surface an error directing the user to edit the file directly, instead of silently accepting a no-op set. Scalars and scalar arrays round-trip faithfully, so idempotent sets of those are intentionally NOT flagged (two precision regression tests lock this). Folded into frontmatter-cli.test.cjs. * chore(#1660): backfill changeset pr ref to 1664 * refactor(#1660): extract noOpObjectListSetError as pure tested helper (Stryker coverage) cmdFrontmatterSet is not in Stryker's property/unit test set, so the inline no-op detection added survivors that dropped the frontmatter module below its 62% mutation threshold. Extract the detection into a pure exported helper noOpObjectListSetError and unit-test every branch directly (changed content, scalar, scalar-array, null, dict no-op). cmdFrontmatterSet now calls the helper. Same pattern as the #1572 spliceFrontmatter coverage fix. |
||
|
|
f615eb9ef3 |
fix(#1572): preserve must_haves object-lists across frontmatter set/merge (#1656)
* fix(#1572): preserve must_haves object-lists across frontmatter set/merge spliceFrontmatter round-tripped the WHOLE frontmatter through extractFrontmatter (a scalar-only parser) then reconstructFrontmatter (a lossy serializer), so any must_haves object-list — artifacts {path, provides}, prohibitions {statement, status} — was flattened to scalar strings and re-emitted as a malformed inline array whenever an UNRELATED field changed, silently dropping every provides:/ status: value. The write now preserves the original raw text for any top-level key whose value is structurally unchanged between the original parse and the new object (generalizing the existing whole-document no-op guard to per-key fidelity), and regenerates only the key that actually changed. The key set is still defined by newObj (the cmdSet/cmdMerge flow always passes the full merged object). spliceFrontmatter's only callers are cmdFrontmatterSet/Merge — the STATE.md read-modify-write family calls reconstructFrontmatter directly and is unaffected. Regression cases folded into tests/frontmatter-cli.test.cjs: artifacts/prohibitions object-lists survive set and merge; idempotent on repeat sets. Asserted via parseMustHavesBlock (the structure-preserving parser). * chore(#1572): backfill changeset pr ref to 1656 * fix(#1572): fail-closed when set/merge would emit [object Object] (codex review) Adversarial review (codex, gpt-5.5/high) flagged that directly setting a must_haves object-list (a CHANGED key) still routed through the lossy reconstructFrontmatter, emitting literal "[object Object]" and destroying the data. The reported case (mutating an UNRELATED field) was already fixed by per-key raw-text preservation, but the changed-object-list path was still silently lossy. Add fail-closed: when a regenerated key's text contains the "[object Object]" sentinel, spliceFrontmatter throws — cmdFrontmatterSet/Merge error out WITHOUT writing, directing the user to edit the file directly. The no-frontmatter (generate-from-scratch) path is guarded the same way. Adds a test that a refused set leaves the file unchanged and the original object-list intact. Codex finding #2 (a contrived flattened-projection no-op) is a deeper limitation noted in the PR — non-destructive, and the fail-closed message already directs users to edit object-list blocks directly. * test(#1572): add spliceFrontmatter per-key preservation + fail-closed unit coverage Stryker mutates gsd-core/bin/lib/frontmatter.cjs against tests/frontmatter.{property,unit}.test.cjs (MinScore 62). The #1572 regression cases live in frontmatter-cli.test.cjs, which is NOT in Stryker's test set, so the new functions (sliceTopLevelFrontmatterSegments, the per-key preserve/regenerate/drop/append loop, regenerateFrontmatterKey's [object Object] fail-closed) had surviving mutants that dropped the module below threshold. Add unit-level coverage in frontmatter.unit.test.cjs exercising every new branch directly via spliceFrontmatter: unchanged object-list preserved (provides survives) when a scalar sibling changes; changed scalar regenerates only that key; orphan keys dropped; new keys appended; indented nested block stays attached to its parent key; whole-document no-op returns input verbatim; both fail-closed paths (changed object-list + no-frontmatter) throw. |
||
|
|
b205e4c2b2 |
fix(#1639): parseDecisions handles the titled-colon bullet form (#1665)
* fix(#1639): parseDecisions handles titled-colon bullet form bulletColonRe anchors on ':**' (colon immediately before close-bold) and bulletEmDashRe requires an em-dash, so the titled-colon form '- **D-NN: Title.** body' (title between the colon and the closing **) matched neither and was dropped by the parse-miss guard. When all decisions used the titled convention, parseDecisions returned 0 and check.decision-coverage- plan passed vacuously — the same false-coverage failure mode as #1343/#1364/#1365. Add a third per-form regex bulletTitledColonRe, checked LAST (strict superset of bulletColonRe, so it only catches bullets the other two miss — minimal blast radius); id + [tags] trackability honored. Regression folded into decisions.test.cjs: titled-colon parses, coexists with colon/em-dash, tags, all-titled-13 no longer vacuously 0. * fix(#1639): tighten titled-colon title to [^:*]* so malformed pre-colon-run bullets still reject The first cut's title run [^*]* was too permissive: it matched a genuinely-malformed bullet with a colon in the pre-separator freeform run (e.g. 'D-07 ratio 3:1:**') by treating the 3:1 colon as the separator, regressing the #1343 parse-miss guard tests. Tighten the title to [^:*]* (no colon, no star) so the separator colon remains the only colon permitted before ** — matching bulletColonRe's existing [^:*]* discipline. Valid titled forms (colon-free titles) still parse; the malformed colon-in-freeform case still falls through to the parse-miss guard. * chore(#1639): backfill changeset pr ref to 1665 |
||
|
|
77c7b4fc9d |
fix(#1522): enforce canonical verification before phase transition (#1548)
* fix: require fresh phase verification before transition * no-mistakes(review): Fix canonical verification closeout gates * no-mistakes(review): Fix verify-work frontmatter promotion command * no-mistakes(review): Fix stale verification gates * no-mistakes(review): Fix canonical verification routing gates * no-mistakes(review): Fix verification dependency and runtime routing gates * no-mistakes(review): Block stale verification bypasses * fix: handle large init manager outputs in verification workflows * chore: update changeset pr number * fix(verify-work): use fresh verification.status for stale gate The stale check after UAT used phase_completion.verification_status from session-start INIT while human_needed promotion already queried fresh verification.status. Align the stale gate with the canonical query so mid-session verification refresh is not ignored. * fix(init): skip roadmap-checked phases when selecting next_phase Roadmap-only phases without a disk directory were still promoted to next_phase when their checkbox was already checked. Exclude checkboxComplete phases so progress routing does not point at work the roadmap already marks done. * fix: gaps_found not overridden by stale, transition uses canonical verification - verification.cts: check gaps_found before stale so gap-closure routing is not masked by a newer summary mtime - phase.cts: remove redundant findStaleVerificationSummary — readVerificationStatus already handles stale detection - transition.md: replace raw grep on file content with verification.status query to avoid false-positive blocks from body text matching * ci: retrigger tests after rebase * fix(transition): replace gsd_run advisory check with awk frontmatter extraction The runtime launcher is not defined until the update_roadmap_and_state step bash block (~line 165). The early verify_completion block used gsd_run to query verification.status, which violated the runtime-launcher-parity test: 'preamble appears AFTER the first gsd_run reference'. Replace the gsd_run call with an awk-based frontmatter extractor that reads only the status: field between the two --- fences. This avoids both the preamble-ordering constraint and the original false-positive grep bug where body text like 'previous_status: gaps_found' would match a full-text regex. The phase.complete gate at update_roadmap_and_state is the canonical enforcement point; this early check is advisory only. Also update workflow-size-baseline.json for the updated transition.md size. Fixes: runtime-launcher-parity test (B) Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * fix: re-check verification under planning lock in phase complete Move readVerificationStatus into withPlanningLock so stale verification cannot slip through when a SUMMARY.md is written between the gate and the roadmap/state mutation. Return the blocked status from the lock callback and emit the error after release to avoid leaving .lock behind. * fix(transition): gate on canonical verification.status including stale Replace awk frontmatter read with verification.status query so transition blocks when summaries are newer than VERIFICATION.md, matching phase.complete and other workflows (autonomous, progress, verify-work). * Fix workflow verification gates for yolo transition and stale routing Require VERIFY_STATUS passed before yolo/interactive transition advance. Route stale verification recovery to verify-work, matching canonical projection. * fix(transition): use verification.status query for stale-aware advisory check The awk-based check read raw frontmatter status: passed, which misses the stale case where summaries are newer than the VERIFICATION.md file even though the frontmatter still says passed. The stale status is computed from file modification times, not stored in frontmatter. Move the preamble to the verify_completion bash block (the first block with a gsd_run call) so gsd_run query verification.status can be used for the advisory check. This gives the full readVerificationStatus logic including mtime-based staleness detection, matching the enforcement gate at phase.complete. Capture full JSON (VERIFY_JSON) so next_action can be included in the advisory output alongside the status. Also update workflow-size-baseline.json for the updated transition.md size. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * ci: trigger test matrix for 525b946 Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * fix(transition): restore awk frontmatter extraction for pre-shim verification check The gsd_run launcher shim is not defined until line ~163 of transition.md, so the verification debt check at line ~80 cannot use gsd_run. Restore the awk-based frontmatter extraction that correctly reads status without needing the runtime, and restore the shim at its proper location before phase.complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(#1522): clarify transition verification gate wording * fix(#1522): update transition workflow size baseline * fix(#1522): update workflow-size-baseline after rebase onto next Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> * fix(#1522): guard findStaleVerificationSummary FS calls + thread opts.fs seam (review) Address review blocker B1 on #1548: findStaleVerificationSummary ran fs.readdirSync and two fs.statSync calls unguarded between readVerificationStatus's try/catch sections, so a TOCTOU race (a SUMMARY listed by scanPhasePlans then removed before statSync) or any FS error threw uncaught into callers NOT under the planning lock (init.manager / init.progress / uat-predicate). Wrap the body in try/catch degrading to 'not stale', and thread the injectable opts.fs seam (add statSync to FsLike, pass fsImpl from the caller) for parity with readVerificationStatus's no-throw contract and testability. Also adds the Verification Module glossary entry to CONTEXT.md (review B3). --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
35478b615e |
refactor(#1646): route capability routers through Command Routing Hub per ADR-959 (#1647)
* refactor(#1646): route capability routers through Command Routing Hub per ADR-959 Phase 2 of parent #1641. Converts graphify, intel, and audit command routers from hand-rolled if/else dispatch to routeHubCommandFamily, implementing the ADR-959 §III(B) line 75 mandate. The three routers now share the uniform dispatch shape with the 14 host routers. src/cjs-command-router-adapter.cts * Imported ERROR_REASON from io.cjs. * UnknownCommand translation now passes ERROR_REASON.SDK_UNKNOWN_COMMAND as the second arg to error() — additive for host routers (their existing one-arg error callbacks ignore the second arg), required for capability routers whose tests assert reason === 'sdk_unknown_command' on the JSON-error envelope. src/graphify-command-router.cts * Replaced 4-branch if/else with routeHubCommandFamily + handlers map. * Validation handlers (missing term, missing/invalid --budget) now return makeInvalidArgs(arg, reason, ERROR_REASON.USAGE) Results instead of calling error() directly (Q2=C, Q4=ii from grilling). * Success handlers keep direct output() calls. * Subcommands array is alphabetical for byte-identical 'Available:' text in the unknown-subcommand message. * The unknown-subcommand path is now owned by the Hub's manifest check (the adapter passes SDK_UNKNOWN_COMMAND). src/intel-command-router.cts * Replaced 9-branch if/else with routeHubCommandFamily + handlers map. * Validation handlers (missing term, missing filePath for patch-meta and extract-exports) return makeInvalidArgs Results. * Preserved the timeAgo mutation in the non-raw status handler. * Preserved the lazy require('./intel.cjs') inside the route function. src/audit-command-router.cts * routeAuditUat: routes through the Hub with a synthetic 'run' defaultSubcommand (no real subcommands). Gives uniform observability. * routeAuditOpen: captures --json in a closure, strips it from args before Hub dispatch (so it isn't mistaken for a subcommand by the manifest check), then branches on wantJson inside the handler to preserve the formatAuditReport success-path quirk. docs/CONFIGURATION.md * Observability section: noted capability commands (graphify, intel, audit-uat, audit-open) now emit DispatchEvent records since #1646. .changeset/capability-routers-via-hub.md * Changed fragment describing the user-visible audit-trail expansion. pr:0 placeholder will be backfilled after gh pr create returns the real PR number (DEFECT.CHANGESET-PR-FIELD-DRIFT). Verification * graphify cutover tests: 119/119 pass (all unit, dispatch, behavior, error path, JSON-errors, and registry assertions) * intel cutover tests: 39/39 pass * audit cutover tests: 24/24 pass * bug-974-graphify-budget-missing-value regression test: pass * npm run test:unit (full suite): 2384 tests, 0 fail * gsd-test-summary on docker: outcome=passed, 0 failures (RULESET.PR-FLOW.docker-before-push) JSON-error envelope parity verified byte-identical: reason values ('usage', 'sdk_unknown_command') and message texts are preserved across all three routers' error paths. * chore(#1646): backfill changeset pr: 1647 (DEFECT.CHANGESET-PR-FIELD-DRIFT) |
||
|
|
6214039358 |
refactor(#1644): Hub extension — exitReason? field on InvalidArgs + adapter honestification (#1645)
Phase 1 of parent #1641. Implements the contract documented in the Phase 0 ADR-0174 §5 amendment (#1642 / #1643). src/command-routing-hub.cts * InvalidArgsResult interface gains optional exitReason?: string (carries an ERROR_REASON enum value, separate from reason which is the explanation text). * makeInvalidArgs(arg, reason, exitReason?) factory conditionally adds the field only when the third arg is truthy — preserves the strict- keys invariant tested at command-routing-hub.test.cjs:444. * _VARIANT_SCHEMA.InvalidArgs.allowed Set extended to include 'exitReason' so the runtime validator does not coerce well-formed extended Results to HandlerFailure. src/cjs-command-router-adapter.cts * Honestified the wrapper comment: the runtime check ('ok' in result) already passes any {ok:*} object through, so the historical {ok:true, data} return type was a lie for err Results. The lying cast is preserved because the Hub's export = syntax doesn't expose HubResult for import; the Hub's _validateErrResult runtime-validates the actual shape. * Result→error() translation branched: when InvalidArgs carries exitReason, the adapter calls error(result.reason, result.exitReason) so the JSON-error envelope (GSD_JSON_ERRORS=1) preserves the typed ERROR_REASON value. When exitReason is absent, error(msg) is called with exactly one arg — byte-identical with prior behavior. * RouteCjsCommandFamilyOptions.error and RouteHubCommandFamilyOptions .error callback types widened from (message) to (message, reason?) to match io.cts's actual error() signature. CONTEXT.md * Command Routing Hub predicate updated to document the new field, factory signature, and dispatcher translation contract. Tests (TDD red→green) * tests/command-routing-hub.test.cjs: 8 new tests covering 2-arg (strict-keys), 3-arg (key present), undefined, empty string, frozen result, hub.dispatch propagation, and validator acceptance. * tests/cjs-command-router-adapter.test.cjs: 2 new tests covering exitReason passed as second arg + byte-identical prior behavior when absent. Verification * npm run test:unit: 2448 tests, 0 fail (no regressions) * gsd-test-summary on docker: outcome=passed, 0 failures (RULESET.PR-FLOW.docker-before-push) Memtrace blast radius: LOW (get_impact makeInvalidArgs → 3 nodes; the optional field is non-breaking for the 1 existing caller routePhaseCommand). |
||
|
|
bcc5a6d1ba |
fix(#1634): honor capability hook matcher and node-prefix command (#1638)
* fix(#1634): honor capability hook matcher and node-prefix command Capability hook install (applyCapabilitySharedEdits) wrote each settings.json hook entry with no `matcher`, so a tool-scoped hook fired on every tool (a fail-closed guard could then block the whole session), and emitted a bare single-quoted script path so a .js-family hook from a git/tarball source without +x failed with Permission denied on every matching call. - Pass through an optional declared `matcher` (entry-level sibling of `hooks`); absent => omitted (match-all), so existing shipped capabilities are unchanged. - Validate `matcher` in the declaration (non-empty string, no control chars). - Emit `node <quoted-path>` for .js/.cjs/.mjs hooks (mirrors first-party); .sh and others keep the bare quoted path (unchanged). Root cause: the manifest hook schema (validator rule C4) was {event, script} only with no matcher, and applyCapabilitySharedEdits never read or wrote one; the command used shellSingleQuote(absScript) with no node prefix. Regression tests fail-first on both defects (matcher dropped; bare path) and pass after the fix; #1460 command assertions updated for the node prefix. * chore(#1634): backfill changeset pr:1638 * fix(#1634): resolve lint and windows CI failures - validator: replace the control-character range regex with a char-code loop. The literal /[\x00-\x1f\x7f]/ tripped ESLint's no-control-regex rule; char codes are equally precise and lint-clean. Behavior unchanged (still rejects matchers containing ASCII control characters incl. DEL). - test: gate the executable-bit precondition on POSIX. Windows fs does not honor POSIX write modes (a 0o644 write reads back as 0o666), so the precondition is meaningless there and failed the windows-latest lane. The node-prefix assertion — the actual fix — is platform-independent and still runs everywhere. * docs(#1634): amend ADR-894 for optional lifecycle hook matcher The `role: "feature"` `hooks[]` entry now carries an optional `matcher` (settings.json tool-scoping pattern: exact/pipe/wildcard/regex). Document the field in the §2 schema table and record a Grilling-amendments entry: the install path projects a declared matcher onto the emitted settings.json hook entry (absent = match-all, so shipped capabilities are unchanged), and per-runtime matcher projection (ADR-857 D8) stays a separate concern. This amendment ships with the fix that introduced the field rather than as a follow-up. * docs(#1634): record WINDOWS-POSIX-MODE-BIT-ASSERT defect in CONTEXT.md Capture the CI failure pattern from #1634/PR #1638 so it is not repeated: a test that writes a file with a POSIX mode and then asserts statSync().mode & 0o777 === <octal> passes on macOS/Linux but fails on windows-latest (Windows fs does not honor POSIX write modes — reads back 0o666). Added as a machine-greppable DEFECT predicate (symptom/examples/detect/fix-forward/ prevention) next to DEFECT.WINDOWS-TEST-PORTABILITY, with the fix-forward: gate the mode-bit precondition on process.platform !== 'win32' and keep the platform-independent behavioral assertion running everywhere. |
||
|
|
9d12725e4e |
fix(#1619): normalize pruned mise node execPath to the stable shim in normalizeNodePath (#1621)
* fix(#1619): normalize pruned mise node execPath to the stable shim resolveNodeRunner() bakes process.execPath into managed .js hook commands. Node realpaths execPath, so under mise it resolves to a concrete <data>/installs/node/<ver>/bin/node that mise prunes on `mise up`, after which every managed hook 404s — the same ephemeral-path failure #977 fixed for fnm and #3181 for Homebrew. normalizeNodePath now rewrites a mise versioned install path to the stable sibling shim <data>/shims/node when it exists (deriving <data> from execPath so a custom MISE_DATA_DIR works), falling back to the raw execPath otherwise. Tests folded into install.test.cjs per the regression test-name lint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(changeset): set pr number to 1621 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Joe Seymour <joese@iarx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
0c4d570541 |
fix(#1628): type-safe config-set validation — close JSON-coercion enum bypass + enforce capability schema
Three related defects in cmdConfigSet, all 'config-set stores invalid values silently': 1. Missing guards: workflow.security_block_on (enum) and workflow.security_asvs_level (integer 1-3) had no store-time validation. 2. Systemic JSON-coercion bypass: every string-enum guard used VALID_X.includes(String(parsedValue)). Because the value is JSON-parsed before validation, String(["member"]) === "member" let a JSON array slip through and an array was stored in a scalar key. Reproduced on human_verify_mode, statusline.context_position, context_guard_mode, fallow.scope/profile, source_grounding_authority, drift_action, context. 3. Unvalidated capability keys: 32 capability-registry-owned keys (4 enum, 25 boolean, 2 number, 1 string) had no hardcoded guard, so any value — including coerced arrays/objects and out-of-enum strings like code_review_depth=garbage — was stored silently. Fix: a type-safe assertEnumValue() helper (requires typeof === 'string' before membership), routed through all nine central string-enum guards (messages preserved byte-for-byte); plus a generic capability-registry validation block that validates every capability key against its declared type/values (enum via the registry's values — single source of truth — boolean, number, string). Behavioral regression tests cover every central enum key and representative capability keys (array + object coercion rejected, out-of-enum rejected, valid accepted) with boundary coverage for the security keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
658ea33cb6 |
fix(#1615): applySurface rewrites commands kind, not just skills
Codex adversarial orthogonal review of PR #1622 surfaced that applySurface (src/surface.cts) only called rewriteStagedSkillBodies for kind='skills', skipping kind='commands'. The gap meant /gsd-surface profile changes on any runtime with commands kinds (windsurf, opencode, kilo, cursor, augment, codebuddy, gemini) wrote raw @~/.claude/... references into synced command/workflow bodies, which fail at invocation time on non-Claude runtimes. For Windsurf specifically, this left workflow files containing @~/.claude/gsd-core/commands/gsd/X.md after a profile change — paths that don't exist on a Windsurf install. Verified by the new regression test which fails before the fix (workflow bodies contained @~/.claude/) and passes after (workflow bodies reference the install target). Captures the return value of rewriteStagedCommandBodies (temp dir path — commands rewrite uses copy-then-rewrite to avoid mutating the package source), syncs from the temp dir, then cleans up. Type annotations satisfy typescript-eslint strict mode. Findings 2 (install ordering) and 3 (legacy .devin cleanup) from the same review are tracked in #1629 — both real but out of scope for #1615. |
||
|
|
4ed208e74b |
fix(#1615): validate commandName to prevent workflow prompt injection
Codex peer review of PR #1622 surfaced that convertClaudeCommandToWindsurfWorkflow interpolated commandName unsanitized into a markdown body that Windsurf loads as an LLM-readable workflow. A plugin author who controls a commands/gsd/*.md filename could inject newlines, markdown structure, or path components (..) to manipulate the workflow body. Validate commandName at function entry against /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ — rejects slashes, backslashes, spaces, dots, control chars, trailing dash. Pattern requires alphanumeric ending so gsd- alone (which would slice to empty stem) is also rejected. Throws with a JSON.stringify-escaped preview (no literal newlines in the error message). Applied to both bin/install.js (where tests import from) and src/runtime-artifact-conversion.cts (production source). 18 positive + 22 negative test cases lock in the validation. |
||
|
|
527142ad2e |
fix(#1615): normalize Windows backslash paths in workflow content
computePathPrefix returned a Windows-style path (with backslashes from path.join) into markdown @-references. Workflow file content on Windows ended up with mixed separators, breaking substring checks in install/install-runtime-artifacts tests on windows-latest CI only. Normalize resolvedTarget and homeDir to forward slashes inside computePathPrefix. The prefix is always substituted into markdown body text, which uses POSIX paths universally. Idempotent on POSIX. Also normalizes the two test assertions to forward-slash form so they pass on Windows. Adds a regression test for backslash-style input. Documents DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT + RULESET.CONTENT-PATH-NORMALIZATION in CONTEXT.md so this anti-pattern stops recurring. |
||
|
|
fc2a7c0555 | fix(#1615): install Windsurf slash workflows | ||
|
|
1b95762661 |
Merge pull request #1568 from behruznassre/fix/1514-retired-phase-total-phases
fix(#1514): exclude retired/folded phases from progress.total_phases |
||
|
|
da2de3a183 | Merge branch 'next' into fix/1514-retired-phase-total-phases | ||
|
|
cbd21092a9 | fix(#1614): install Antigravity skills flat |