Commit Graph

2080 Commits

Author SHA1 Message Date
Tom Boucher
8e4ebb49e4 fix(#2128): address shared-seam review regressions (#557, action-scan, comment-strip)
Final convergence review found the shared <tag> seam introduced 3 behavior
regressions; fixed all + locked with tests:

- #557 REGRESSION: stripTaggedBlocks's attribute-tolerance stripped `<details open>`
  (the ACTIVE-milestone marker) that the old `<details>`-only regex preserved. The
  seam now takes `allowAttributes` (default false) — details/decisions strip is
  attr-INTOLERANT (preserves `<details open>`); only `<task type="…">` opts in.
  Regression test added to roadmap-parser + markdown-sectionizer suites.
- verify.cts actionZones (negative-grep-echo security scan): reverted to a bounded
  to-first-close scan `<action>([\s\S]{0,20000}?)</action>` so a grep-echo trick
  can't hide behind an unterminated inner <action> (the seam's stop-at-next-open
  would drop it). ReDoS-safe via the cap.
- check-command-router HTML-comment strip: `(?:-->|$)` fallback wiped to EOF
  (fail-closed spurious gate block) — replaced with stop-at-next-open so an
  unclosed `<!--` leaves downstream tags intact.
- Updated the extractTaggedBlocks nested-tag tests to the new (stop-at-next-open)
  behavior: `<x><x>inner</x></x>` -> ['inner'].

All vectors still linear; every fix verified in-process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:12:45 -04:00
Tom Boucher
b321bc04f4 fix(#2128): bound the sibling bracket-prefix clause — complete the ReDoS fix
Review caught that the prior commit bounded only the paren tag clause and left
the SIBLING bracket-prefix `(?:\[[^\]]+\]\s*)?` (same host regexes, before Phase)
UNBOUNDED — the identical quadratic reachable via a `[...]` run (measured ~16s at
1.7MB). Bound `[^\]]+`/`[^\]]*` -> {1,200}/{0,200} across all 19 phase/milestone
heading prefixes. Comprehensive re-measurement now shows EVERY vector linear
(bracket/paren/id/name/milestone all ~2-44ms at 2.45MB; bracket scaling
2k->2ms, 4k->5ms, 8k->10ms). Also: update the #1729 literal-mirror parity test
off its stale unbounded constant, and add limit-1 (199) boundary coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:44:49 -04:00
Tom Boucher
c1cd43a39f fix(#2128): bound the phase-tag clause to {0,200} — kill quadratic ReDoS
The canonical OPTIONAL_PHASE_TAG_SOURCE tag clause `(?:\s*\([^)\n]*\))?` (and its
inlined literal mirrors across 11 modules) had an UNBOUNDED body, making the
optional-group + /g header scan quadratic on adversarial ROADMAP.md/STATE.md — a
long run of `(` after a header ran ~18.8s at 1.7MB. Bound the body to {0,200} in
the constant AND every mirror in lockstep (the #1729 "both forms change together"
contract), so the scan is linear: the same 1.7MB input now resolves in ~9ms
(measured), while real tags (a handful of chars) still match and a 201-char tag
is rejected. Added a #2128 boundary regression to the #1729 suite.

Pre-existing (byte-identical before/after the Phase 4 migrations); folded in at
maintainer direction rather than deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:31:17 -04:00
Tom Boucher
a1de52d71b fix(#2128): sanctions must be a dedicated // comment line (decoy-proof)
Re-review found the `// phase-id-owner:` suppression treated a `//` embedded in a
string literal as a comment — help/doc text quoting the sanction syntax (the exact
string the scanner's own main() prints) would silently suppress a real
re-derivation. Require the marker to LEAD its own comment line (`^\s*//…`), so a
`//` inside a string or trailing a code line never counts. All 5 real sanctions
are already dedicated lines (scanRepo stays green); trailing same-line sanctions
are no longer honored — put the comment on the line directly above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:24:49 -04:00
Tom Boucher
e2eaa5b046 fix(#2128): address review — migrate 9 mis-allowlisted sites, harden scanner + guards
Correctness review of the Phase 4 guard found the allowlist over-broad and the
scanner/guards evadable. Fixed all findings:

- Migrate 9 sites that were wrongly sanctioned: their regex is the PURE canonical
  token (`\d+[A-Z]?(?:\.\d+)*`, no variant), byte-identical to already-migrated
  siblings. The old justification argued against swapping to the extractPhaseToken()
  FUNCTION (behavior-risky) — but the guard only wants the same regex built from
  the SOURCE string (byte-equal, zero risk). Coverage is now 32 migrated / 5
  sanctioned, not the overstated 23 / 14 (audit.cts x3, uat.cts, init.cts x4,
  roadmap-upgrade.cts). Each conversion proven byte-equal (.source + .flags).
- Harden the drift detector: also catch the `[0-9]`-in-place-of-`\d` variant;
  document the accepted limits (cross-line split, semantic restructuring —
  covered by the identity guard + review, not a text scan).
- Sanction robustness: a `phase-id-owner:` marker now counts only inside a `//`
  comment (a bare substring in a string no longer suppresses a real flag), and
  the preceding-line window skips blank lines (an auto-formatter's blank line no
  longer reactivates the flag).
- roadmap-parser.cts:462 comment: corrected — that regex carries no /i flag, so
  its [A-Za-z] class does real case work (matches state.cts:1409's rationale).
- Identity guard: surface require failures instead of silently skipping, and
  floor coverage at >75% of consumer modules (inspects 156/157).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:14:15 -04:00
Tom Boucher
09be501eb7 feat(#2128): phase-id anti-divergence guard — canonical token source + drift scanner + guards
Phase 4 of epic #2121 (ADR-2121 Decision 7), closing the recurrence loop that
produced #2111 / #2114 / #2104: no module outside src/phase-id.cts may
re-implement phase-ID parsing without failing CI.

- phase-id.cts: add PHASE_NUMBER_TOKEN_SOURCE — the canonical phase-number-token
  grammar (\d+[A-Z]?(?:\.\d+)*) for enumeration/scan call sites, the ANY-phase
  counterpart to phaseMarkdownRegexSource(n)'s known-number lookup. Extend-only
  (never touches normalizePhaseName; blast radius 79 fns / CRITICAL).
- scripts/lint-phase-id-drift.cjs: pure findPhaseIdRegexDrift(text) + scanRepo(root),
  wired to `npm run check:phase-id-drift`. Flags a literal re-derivation of the
  canonical token (both /\d/ and new-RegExp `\\d` escaping, plus the [A-Za-z] and
  [.-] near-variants) anywhere in src/** outside phase-id.cts, unless sanctioned
  with `// phase-id-owner: <reason>`. Narrow by design: bare \d+, digits-only
  captures, \w ids, status-message text and pipe-tables are not flagged.
- tests/phase-id-drift-guard.test.cjs: fail-first drift cases (AC1) + live
  scanRepo(ROOT) zero-drift (AC3) + identity guard — phase-id.cjs exports the
  complete locked surface and no consumer re-exports a divergent copy (AC2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:49:36 -04:00
Tom Boucher
a22602e276 docs(#2126): correct malformed_roadmap prior-behavior note (re-review)
Re-review found the test comment + changeset prose inaccurately claimed a bare
query "always" surfaced malformed_roadmap. Empirically, on origin/next a
project-code-prefixed checklist entry was a silent {found:false} for BOTH query
forms — the prefixed pass discarded its malformed candidate and the bare regex
could not match the PROJ- prefix at all. The unified 3-source lookup newly grants
the diagnostic to both forms; correct the prose to say so. No logic change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 01:07:00 -04:00
Tom Boucher
16b61d437f test(#2126): resolve review findings — fold dedup, harness fidelity, malformed-parity lock
Adversarial review of the Phase 3 branch surfaced three verified defects; fix
all three in place (no defer):

- install-runtime-artifacts.test.cjs: finish the fold-triplication dedup started
  earlier (only enh-1511 had been collapsed). 11 B1-batch __foldDescribe blocks
  were byte-identical triplicates (~5.9k lines, ~49% of the file), tripling the
  subprocess-spawning installer suites under --test-concurrency — the same
  starvation that produced the temp-dir races this branch fixes. Byte-identity
  verified per block before removal; 230 distinct test/it titles preserved
  (origin/next: 230 -> 230), interleaved B3/B5/B6 singletons untouched.
- config-get-default.test.cjs: make runExpectError faithful to production. The
  throwing process.exit seam was caught by cmdConfigGet's "No config.json"
  guard and reclassified into a spurious 2nd error() with the wrong reason
  (CONFIG_PARSE_FAILED). Drive io.setJsonErrorMode + carry the original message
  on the sentinel so the guard re-throws (single fire), assert exitCount===1,
  and strengthen both probes to assert the typed reason (CONFIG_NO_FILE /
  CONFIG_KEY_NOT_FOUND).
- roadmap.test.cjs: lock the #2121/#2114 malformed_roadmap parity — a
  project-code-prefixed query against a checklist-only roadmap now surfaces the
  same diagnostic a bare query always did (fails on prior silent-empty behavior).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:53:00 -04:00
Tom Boucher
6e7e3111fb test(#2126): fix #1259 real-eslint CPU starvation — lint a non-type-aware .cjs clean target
The prohibition-enforcement real-runner tests linted src/clock.cts (a .cts) as
their clean target. Under eslint.config.mjs's type-aware block for src/**/*.cts
(recommendedTypeChecked + parserOptions.project: tsconfig.build.json), each eslint
spawn loaded the WHOLE tsconfig.build.json program (~2s, CPU-heavy). The
real-runner tests spawn eslint repeatedly; under --test-concurrency those
full-program type-checks oversubscribed the bench CPU and blew the 60s subprocess
bound -> fail-closed (intermittent, load-dependent — passed 24241/24241 in an
earlier run, failed here).

Root fix (not a retry/timeout bandaid; measured projectService = no faster since
a single-file .cts lint still loads type info): add tests/_ff_lint_clean.cjs, a
KNOWN-CLEAN lint-scoped .cjs companion to _ff_lint_violation.cjs, with a
flat-config block enabling local/no-source-grep so the clean pass stays
non-vacuous. Repoint the 6 src/clock.cts real-runner usages (5 targets + the FF-02
toothless violationFixture) at it. Each spawn is now ~0.8s non-type-aware (no
whole-program load) — starvation removed. All 6 tests' semantics verified
in-process (SF-01 greens; toothless/fail-closed stay unverified); full-repo
`eslint .` green.

Refs #2126, #1259

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:43:18 -04:00
Tom Boucher
119702ff29 test(#2126): fix os.tmpdir() cross-file race + dedup folds surfaced by gsd-test (no-defer)
Phase 3's gsd-test surfaced 8 pre-existing test-isolation races (in #2090's test
files now on next). Per CLAUDE.md's no-defer rule these are fixed inline in the
current change. Root-caused via /qa-test-architect — all bad-test (the
rewrite-engine production code is race-free):

- install-runtime-artifacts.test.cjs: the "rmSync when readFileSync throws" test
  diffed the SHARED os.tmpdir() for gsd-cmd-rewrites-* dirs and force-deleted any
  new one with no ownership check. Under --test-concurrency it deleted a sibling
  test file's LIVE tempDir mid-copy (the #1575 "ENOENT .../graphify.md") and
  misattributed it as its own leak. Fixed: capture the exact tempDir THIS call
  creates (fs.mkdtempSync monkeypatch, restored in finally) and assert only on
  that — never sweep/delete the shared os.tmpdir(). Also deduped the enh-1511
  block the #1969 consolidation folded in 3x byte-identically (#1970/#1974/#1975)
  down to 1 copy; 308 unique test titles unchanged (verified).
- issue-1575-agent-descriptor-parity.test.cjs: a missing }); nested the M2
  'cursor attribution' test inside the per-runtime loop so it ran 7x (widening
  the tempDir window). Fixed the brace -> runs once as a describe sibling.
- config-get-default.test.cjs: local run()/runRaw() spawned node via
  execFileSync with a fixed 5s timeout and no retry -> ETIMEDOUT under Docker
  load. Redesigned to call cmdConfigGet in-process (fs.writeSync fd-capture +
  process.exit sentinel, both restored in finally) — no subprocess, no wall clock.
- runtime-artifact-conversion.cts: fixed the stale "No production caller today"
  JSDoc on rewriteStagedCommandBodies (real callers: applySurface,
  createRuntimeArtifactInstallPlan) — the false doc invited the bad test.

Refs #2126, #2090

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:43:17 -04:00
Tom Boucher
61f3cafc70 fix(#2126): route roadmap.cts CLI resolvers through shared lookup sources (drives #2114)
Phase 3 of epic #2121. cmdRoadmapGetPhase and getRoadmapPhaseWithFallback now
iterate the shared roadmapPhaseLookupSources (exact -> numeric -> prefix-tolerant,
owned by phase-id.cts since Phase 1) instead of a hand-rolled 2-source lookup, so
all three roadmap resolvers share one resolution contract.

Drives #2114: `roadmap get-phase <bare-N>` now resolves a drifted
`### Phase AB-29:` heading (matching getRoadmapPhaseInternal / init.phase-op),
previously EMPTY from the CLI. The malformed_roadmap checklist-fallback and the
milestone-then-full precedence are preserved (a milestone checklist never blocks
a full-roadmap header match).

Behavior reversal (approved in-session): a bare query now also resolves a
*drifted-only* prefixed heading when no bare sibling exists, reversing the #3599
counter-test's expectation. #3599's real anti-steal intent (a bare sibling wins
over a distinct prefixed one) is preserved by the exact->numeric->prefix-tolerant
ordering and re-asserted in the updated test; a new #2114 block covers the
drifted-only case. Fail-first demonstrated.

Closes #2126
Refs #2121

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:43:17 -04:00
Tom Boucher
4048d90860 fix(#2091): remove unused import + sync capability-registry for CI lint 2026-07-09 22:26:13 -04:00
Tom Boucher
d10f9c675e test(#2091): update closed-vocab assertions for hermes extensionEvents dialect 2026-07-09 21:21:49 -04:00
Tom Boucher
f152e9a069 feat(#2091): migrate hermes onto EoS imperative adapter + extensionEvents dialect (ADR-1239)
- Fold 7 hardcoded isHermes/runtime === 'hermes' branches in bin/install.js into
  descriptor-driven _hostBehaviors lookups (skillFrontmatterVersion,
  skillsManifestPrefix, trackCategoryDescription, writeCategoryDescription,
  reportSkillsCount, legacyCommandsGsdCleanup, brandingRewrites)
- Add runtime.hostBehaviors block to capabilities/hermes/capability.json
- Register EXTENSION_EVENT_SURFACES.hermes (13 real plugin hook events) in
  src/host-integration.cts — replaces the borrowed hookEvents:'claude' 6-event
  surface that silently never fired on Hermes
- Add extensionEvents:'hermes' to the descriptor
- Add 'hermes' to VALID_EXTENSION_EVENTS in capability-validator.cjs
- Regenerate capability-registry.cjs
- Tests: hermes-imperative-reference (negotiation, axes, fail-closed, source-guard),
  hermes-dispatch-upgrade (dispatch posture, degradation, fail-closed)
- Changeset + docs update
2026-07-09 20:47:35 -04:00
Tom Boucher
68535d3011 fix(#2090): correct beforeTool test to match parity-faithful path-key contract 2026-07-09 19:38:27 -04:00
Tom Boucher
9e7f80ea69 fix(#2090): resolve lint — drop unnecessary type assertion + unused vars 2026-07-09 19:38:27 -04:00
Tom Boucher
a9f9c130ef test(#2090): add cline EoS migration test scaffolding (red) 2026-07-09 19:38:27 -04:00
Tom Boucher
f50ad1d7a6 Merge remote-tracking branch 'origin/next' into chore/2125-migrate-state-prose 2026-07-09 19:10:31 -04:00
Tom Boucher
ae1bd14691 Merge branch 'next' into fix/2073-antigravity-reviewer-block 2026-07-09 18:55:02 -04:00
Tom Boucher
bd5fcfceeb fix(#2125): route complete-phase resolver through canonical parser (review)
Orthogonal review surfaced that resolvePhaseIdForCompletePhase (state.cts) and
cmdStateCompletePhase's idempotency check still used an unanchored
/(\d+[A-Z]?(?:\.\d+)*)/i — even more permissive than the parseProsePhaseField
regex this phase fixes. Reachable corruption: after `milestone complete v0.5`,
`state complete-phase` (no --phase) mined "0.5" from the body line
"Phase: Milestone v0.5 complete" and rewrote STATE.md as "Phase 0.5 complete".

Both sites now delegate to phase-id.cts:parsePhaseFromProse (the same anchored
parser), so a milestone-closure line yields no token and the existing
"unable to resolve" guard fires instead of corrupting. Canonical tokens
(3, 03, 3A, 3.3, "3 of 5", "1 — Setup") are preserved unchanged.

Regression (tests/state.test.cjs, complete-phase suite): `state complete-phase`
on a "Milestone v0.5 complete" STATE.md now rejects and does not mine "0.5".
Demonstrated fail-first.

Refs #2125, #2121

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:11:55 -04:00
Tom Boucher
e7ff7be1a4 fix(#2125): migrate state.cts prose parsing to canonical parser (drives #2111)
Phase 2 of epic #2121. state.cts:parseProsePhaseField now delegates to the
anchored phase-id.cts:parsePhaseFromProse (built in Phase 1), removing this
module's independent prose phase-id regex.

Drives #2111: `milestone complete vX.Y` no longer corrupts current_phase. The
body line "Phase: Milestone v0.5 complete" previously had "5" mined from it by
the unanchored regex; the anchored parser returns { phase: null }, so
syncStateFrontmatter's #905 guard preserves the real current_phase. This also
fixes the broader family the review surfaced — every milestone completion
(e.g. v1.0 -> "0") was silently corrupting current_phase, not just .5-versions.

Regression (tests/milestone.test.cjs, in the milestone-complete suite, #2111):
`milestone complete v0.5` on a project with current_phase: "19" now preserves
"19". Demonstrated fail-first end-to-end: reverting the migration reproduces
current_phase = "5".

Closes #2125
Refs #2121

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 17:57:47 -04:00
Tom Boucher
80923244b1 fix(#2124): harden parsePhaseFromProse per orthogonal review (ReDoS + coercion)
Orthogonal security review of the Phase 1 surface found two issues; both fixed
and regression-tested:

- MEDIUM ReDoS: the name-extraction regexes /\(([^)]+)\)/ and
  /—\s*([^(\n]+?).../ backtrack O(n^2) on a crafted STATE.md field value with a
  long unterminated "(" / "—" run (reviewer measured ~38s at 320k chars).
  Length-bound both quantifiers to {1,200} -> linear (320k now ~100ms). A real
  phase name is far shorter than the cap.
- LOW: parsePhaseFromProse threw on non-string truthy input, unlike its three
  sibling #2121 functions. Coerce via String(value) up front.

The identical ReDoS regexes are copied verbatim from the pre-existing
state.cts:parseProsePhaseField; per the no-defer rule that surfaced defect is
fixed inline there too (Phase 2 / #2125 later supersedes that function by
delegating to the bounded phase-id.cts parser).

Adds a behavioral bound-guard regression test (a >200-char parenthetical is not
extracted) and a non-string-coercion test.

Refs #2124, #2121

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:33:39 -04:00
Tom Boucher
bf25e0b435 chore(#2124): build canonical phase-id.cts surface (Phase 1 of #2121)
Add the ADR-2121-locked canonical functions to src/phase-id.cts. No consumer
behavior changes — Phases 2-4 migrate the divergent call sites against them.

- parsePhaseFromProse: anchored prose parser. A phase is returned only when the
  STATE.md "Phase:" field VALUE begins with a phase token, so
  "Milestone v0.5 complete" yields { phase: null } instead of "5" (the #2111
  root cause: the old unanchored \b(\d+..)\b mined the minor-version digit).
  Name extraction (parenthetical / em-dash tail, minus status words) unchanged.
- stripConfiguredProjectCodePrefix / isForeignPrefixedPhaseQuery: config-aware
  prefix policy. A foreign prefix (MEM-01 when the configured code is LKML) is
  preserved rather than collapsed to a bare numeric phase — the #2104 fix's
  canonical home (consumed later, outside this epic's critical path).
- roadmapPhaseLookupSources: moved from roadmap-parser.cts so phase-id.cts is
  the single owner of the exact -> numeric -> prefix-tolerant ordering.
  roadmap-parser.cts now imports it (behavior-identical); its two now-unused
  imports (phaseMarkdownRegexSourceExact, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE)
  are dropped.

Tests: subject-named suites in tests/phase-id.test.cjs covering the ADR
boundary set (v0.5, v1.0, MEM-01, AB-29, bare 29, zero-padded 029) plus two
fast-check properties: the #2111 "Milestone vX.Y complete never yields a phase"
invariant and a parse/normalize property.

Extend-never-mutate: the 12 pre-existing phase-id.cts exports are unchanged.

Closes #2124
Refs #2121

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:13:06 -04:00
Tom Boucher
45f3a2a5f9 fix(#2089): wire adapter into install path + address all review findings
MEDIUM fixes (code review):
- Wire resolveManagedHookEvents + resolveHookScripts + buildHookBusEntries
  from imperative-hook-bus.cts into writeCursorHooksJson — the install path
  is now truly descriptor-driven (reads hostBehaviors.managedHookEvents),
  not a hardcoded constant that happens to match the descriptor. bin/install.js
  passes the descriptor list via opts.managedHookEvents.
- buildHookBusEntries is now consumed (was dead code); entry-building is no
  longer duplicated inline.
- Remove try/finally from cursor-hook-bus-upgrade.test.cjs test bodies
  (violated CONTRIBUTING.md L342; redundant with t.after cleanup).

LOW fixes:
- Remove dead require('fs')/require('path') from gsd-cursor-pre-tool.js
- Fix resolveManagedHookEvents docstring (all-invalid fallback behavior)
- Add src/runtime-hooks-surface.cts to the AC2 source-guard file list

Security review: no CRITICAL/HIGH/MEDIUM findings (3 LOW are pre-existing
#777 baseline patterns, not regressions).
2026-07-09 13:17:04 -04:00
Tom Boucher
fad5094587 fix(#2089): redesign scanner property test — logic/results, not wall-clock
Replaces the timing-dependent execFileSync(timeout:5000) approach with a
spawnSync-based runHook that tests the scanner's RESULT (exit code + output
shape), never how long it takes.

Root design flaw in the prior approach: execFileSync's timeout (5000ms)
was identical to the scanner's own internal setTimeout(5000ms), creating a
non-repeatable race (F.I.R.S.T. violation: not Repeatable). Under concurrent
test-chunk load — which #2089's 3 new cursor test files redistribute —
node22's event-loop scheduling let execFileSync's SIGTERM win the race,
producing err.status=null → exitCode=1 → spurious property-test failure.

Redesign (F.I.R.S.T.):
- spawnSync (not execFileSync): non-zero exits return a result object,
  not an exception — cleaner for property tests
- Non-serializable payloads (BigInt, circular refs, Symbol) are SKIPPED:
  the scanner receives JSON via stdin, so these values are outside its
  protocol — JSON.stringify throwing is a test-harness artifact, not a
  scanner defect
- 30s safety-net timeout is NOT a test assertion: scanner exits in <100ms;
  30s only catches a genuinely hung process (6x the scanner's own 5s
  internal timer → no race possible)
- Assertions check exit===0 and output structure, never timing

qa-test-architect pipeline: risk=HIGH (security boundary); automation=
subprocess (real shipped hook); test-cases cover happy/boundary/negative/
independence; verified via gsd-test.
2026-07-09 09:36:33 -04:00
Tom Boucher
a8d9dbe02a fix(#2089): widen read-injection-scanner property test timeout to avoid node22 race
The property test's execFileSync timeout (5000ms) was identical to the
scanner's own internal stdin-timeout (hooks/gsd-read-injection-scanner.js:109,
also 5000ms). Under concurrent test-chunk load on linux-node22 — which #2089's
3 new cursor test files redistribute — the scanner subprocess's stdin 'end'
event can fire late enough that execFileSync's SIGTERM arrives before the
scanner's own process.exit(0), producing err.status=null → exitCode=1 →
spurious property-test failure.

The scanner has no process.exit(N!=0) paths; the only non-zero exit is from
the signal-kill race. Doubling the test ceiling to 10000ms gives the scanner's
5000ms internal exit a 5s buffer to win the race deterministically on every
node version.
2026-07-09 09:16:37 -04:00
Tom Boucher
a53c5462e6 chore(#2089): gitignore compiled imperative-hook-bus adapter + fix Context7 typo
- Add /gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs to
  .gitignore (tsc-emitted build artifact per ADR-457 convention; matches the
  sibling adapter entries at .gitignore:70-89). The subagent authored the
  .cts source but missed this entry, leaving the compiled output untracked.
- Fix cosmetic 'Context3' -> 'Context7' typo in test section header comment.
2026-07-09 02:07:08 -04:00
Tom Boucher
c68bca55cf test(#2089): regenerate all golden fixtures for managed-hooks-registry + cursor hook additions 2026-07-09 01:19:25 -04:00
Tom Boucher
303a796579 docs(changeset): #2089 cursor host-integration migration + golden fixture 2026-07-09 00:21:51 -04:00
Tom Boucher
c5e5211db0 test(#2089): add cursor EoS migration test scaffolding (red) 2026-07-09 00:21:28 -04:00
Tom Boucher
8b99f4f3c3 fix(#2088): weight install-heavy test files so they spread across chunks
The targeted CI lane runs changed files UNSHARDED; #2088 touched 13 install-heavy
test files that all landed in one chunk, blowing the 600s per-chunk backstop on
the slow Windows runner (pure slowness, not a leak — per run-tests.cjs's own
comment). Weight install*/codex-* files (~10x a unit file) toward the per-chunk
budget so they spread across chunks instead of clustering; light-file chunking is
unchanged (weight 1). Adds harness regression tests (heavy split vs light control).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:49:25 -04:00
Tom Boucher
6e773d97df feat(#2088): migrate Codex onto the Embeddable Orchestration System (ADR-1239)
Drive Codex install/uninstall through the descriptor-driven Host-Integration
Interface (declarative embedding adapter → engine surface dispatch) and fold
every positive `runtime === 'codex'` / `isCodex` projection into descriptor-driven
`runtime.hostBehaviors`. Install/uninstall output stays byte-parity-gated
(tests/fixtures/golden-install-parity/codex.json); no other runtime changes.

Three Context7-verified upgrades, each with a test on the user-reachable surface:
- Skill root → canonical $HOME/.agents/skills via a skills-kind `home` override,
  with pre-move migration cleanup (stale ~/.codex/skills/gsd-* removed on install
  and uninstall; user content preserved). Fixes getGlobalSkillsBase, writeManifest,
  and the skill-manifest inventory to honor the override so --skills-root /
  sync-skills / the manifest report the real location.
- Six new hooks.json lifecycle events (PreToolUse, PermissionRequest, PreCompact,
  PostCompact, SubagentStop, UserPromptSubmit) shared by install + uninstall;
  extendedHookEvents reconciled [] -> the schema-valid wired subset.
- Explicit `[agents] max_depth = 1` in the managed config.toml block, pinning the
  negotiated dispatch.maxDepth:1 axis. validateCodexConfigSchema now permits a
  known-scalar-only bare `[agents]` AgentsToml table (still rejects [[agents]] and
  unknown-key break-forms, #2760); mergeCodexConfig preserves the user's own
  AgentsToml scalars (max_threads etc.) instead of dropping them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:42:11 -04:00
Tom Boucher
88d008553d Merge remote-tracking branch 'origin/next' into fix/2073-antigravity-reviewer-block 2026-07-08 20:00:25 -04:00
Tom Boucher
dc5b89f401 test(#2073): add see #2073 ref to allow-test-rule exemption (ADR-456) 2026-07-08 18:56:27 -04:00
Tom Boucher
dbc730d8de fix(#2073): capability-probe external killer (timeout/gtimeout) for macOS
Code review (HIGH): a hardcoded 'timeout 600 agy' fails with rc 127 on stock
macOS (no GNU timeout/gtimeout), silently losing the agy reviewer. Probe for
'timeout'/'gtimeout' via command -v and fall back to agy's native --print-timeout
alone when neither exists (mirrors scripts/base64-scan.sh). External cap (600s)
stays >= --print-timeout (540s) so it only backstops a pre-session stall. Factor
the prompt into _AGY_PROMPT to avoid duplicating the long -p string across both
branches. Update the agy + #687 tests to assert the probe + bound + fallback,
regen the 17 goldens + size baseline, refresh the maintainer-note version stamp
to 1.0.16.
2026-07-08 17:53:28 -04:00
Tom Boucher
396f44bd0b feat(architecture): [EoS/opencode] Migrate OpenCode onto the Embeddable Orchestration System (ADR-1239, #2087)
Route OpenCode (and its Kilo sibling) through the public Host-Integration Interface and
land two Context7-verified capability upgrades. Byte-identical install output for all 16
runtimes (golden parity asserted).

Through the interface (AC2):
- OpenCode/Kilo's bespoke commands+skills+plugin install (the inline
  `else if (isOpencode || isKilo)` block) moves into the engine
  (installOpencodeFamilyCommands/Artifacts in src/install-engine.cts), dispatched by
  installRuntimeArtifacts when the descriptor declares hostBehaviors.combinedFamilyInstall.
  opencode/kilo now flow CLI -> _runtimeAdapter -> installRuntimeArtifacts like the skills
  runtimes. _isSkillsRuntime no longer excludes them; the bespoke block + dead
  copyFlattenedCommands are removed.
- Every hardcoded `runtime === 'opencode'`/`isOpencode` branch is folded into
  descriptor-driven runtime.hostBehaviors. ZERO `runtime === 'opencode'`/`'kilo'`
  string-equality remain in bin/install.js / install-engine.cts / runtime-artifact-conversion.cts.

Upgrades (AC4):
- Background dispatch: OpenCode shipped experimental background subagents in v1.15 and
  made them default-on in v1.17 -> dispatch.background/backgroundDispatch flip to true;
  shouldFlattenDispatch(opencode) now returns false (behavioral change; type: Changed).
- Expanded event surface: the OpenCode plugin subscribes permission.asked/replied +
  session.error.

Tests: opencode-imperative-reference (adapter/profile, shouldFlattenDispatch pin,
fail-closed negotiate, hostBehaviors, AC2 source-guard) + extended plugin surface test.
Docs: capability matrix v1.15/v1.17 citations. Changeset (Changed). gitignore .memdb//.memtrace/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:17:05 -04:00
Tom Boucher
e0f245a6b2 fix(#2073): regen claude-local golden; reword changeset (no product parenthetical) 2026-07-08 17:15:48 -04:00
Tom Boucher
7abe6e34ac chore(#2073): regen workflow-size baseline for review.md growth
The agy block grew (~file-reference prompt instruction, external-timeout
rationale, --model wiring, richer Step 3 diagnostic) — all load-bearing
content fixing 3 production failure modes (#2073), not bloat. Growth
justified in the PR.
2026-07-08 16:47:02 -04:00
Tom Boucher
6ae23ffc21 fix(#2073): supersede #687 contract; regen golden install-parity baselines
#687 encoded 'agy bounded ONLY by --print-timeout, no external killer' and
'inline -p "$(cat)"'. Documentation since then (see PR description) shows:
  * agy's own print-mode guidance pairs --print-timeout with an external
    terminal 'timeout' (it cannot fire pre-session);
  * agy gained --model in ~1.0.3 (#3782's 'no --model' note was correct then,
    stale now);
  * inline "$(cat)" overflows the exec arg list on a large review prompt.
Rewrite the #687 describe block to the new contract (file-reference prompt +
--print-timeout PAIRED with a >= external timeout + --model + discard-on-
nonzero), and regenerate the 16 golden install-parity fixtures (only the
review.md hash line changed per runtime).
2026-07-08 16:47:02 -04:00
Tom Boucher
6dc4676d92 test: add failing regression for #2073 agy reviewer invocation shape
The agy block in /gsd-review overflows the exec arg list (inline "$(cat)"),
has no external timeout (pre-session stall hangs past --print-timeout), no
--model escape hatch for a 404'd pinned model, a generic empty-output stub,
and a stale 'no --model flag' note. These tests pin the corrected shape in
gsd-core/workflows/review.md; they fail on next.
2026-07-08 16:47:02 -04:00
Tom Boucher
eeec6b512e fix(#2086): AC2 source-guard must ignore comments/backtick prose, not just code
The #338 fail-safe commit added a comment containing the literal `runtime === 'claude'`
(explaining what the data lookup is NOT), which the AC2 source-grep test matched as a
false positive (the test read the whole file, prose included). Strip block/line comments
+ backtick spans before matching so the guard flags only LIVE code, and reword the
comment. CRLF-safe line-comment strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:26:19 -04:00
Tom Boucher
102ffa0f9e fix(#2086): #338 fail-safe floor for reference-host behaviors on registry-load failure
Reviewer (PR #2106, elevated): if capability-registry.cjs fails to load,
_hostBehaviors('claude') returned {} — silently routing a claude LOCAL install to
the repo-shared settings.json instead of the gitignored settings.local.json (#338),
skipping mergeClaudePermissions + the .gsd-source marker. The migration is what
introduced that registry dependency (pre-PR the path had none).

Add FALLBACK_HOST_BEHAVIORS (keyed by runtime id — a data lookup, not a
runtime==='claude' branch) mirroring the reference host's #338-privacy-critical keys
(settingsFileByScope, permissionsSchema, sourceMarkerFile), consulted only when the
registry (or the descriptor) is unavailable. Behavior degrades CLOSED, never open;
the live descriptor stays the source of truth. Normal (registry-present) output is
unchanged (golden parity preserved). Pinned by tests via a registry-injected
_resolveHostBehaviors helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:19:12 -04:00
Tom Boucher
ff6e928530 fix(#2086): normalize realpath temp root in golden parity manifest (macOS /private)
The claude LOCAL install resolves its config dir via realpath, which on macOS
prepends /private to the temp root and embeds it in projected agents/commands/
workflows (@ references). buildParityManifest normalized only `root` (/var/folders/…),
leaving the /private prefix on macOS while Linux has none — so the mac-generated
claude-local fixture failed the Linux CI leg (198 files). Normalize the realpath
form too; no-op for the global fixtures (literal --config-dir, never realpath-resolved).
Regenerated claude-local.json now matches the Linux hashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:49:58 -04:00
Tom Boucher
fdd5e401eb feat(architecture): [EoS/claude] drive claude through the imperative adapter + descriptor-driven hostBehaviors (#2086)
Fold Claude Code's install/uninstall onto the Embeddable Orchestration System
(ADR-1239 Phase D). claude is GSD's tier-1 reference host, but its install path
was still driven by 13 hardcoded `runtime === 'claude'` string-equality branches
scattered across bin/install.js rather than the public Host-Integration Interface.

- Route install()/uninstall() through `createImperativeAdapter({runtime})` — the
  adapter delegates to the SAME installRuntimeArtifacts/uninstallRuntimeArtifacts
  engine calls, so output is byte-identical (proven pre/post, both scopes).
- Replace all 13 `runtime === 'claude'` / `runtime !== 'claude'` branches with
  descriptor-driven `runtime.hostBehaviors` lookups on capabilities/claude/
  capability.json (attributionSource, authorsCanonicalWorkflow, localInstallStyle,
  permissionsSchema, settingsFileByScope, sourceMarkerFile, agentFrontmatterExtensions,
  ownsClaudePaths, nativeModelAliases, skillsGlobalOnboarding). Behavior is
  identical; the brittle string-equality coupling (the add-a-host tax) is gone.
- Single-source the scattered literal 'claude' defaults/rosters behind DEFAULT_RUNTIME.
- Extend golden-install-parity to assert the claude LOCAL legacy layout is
  byte-identical too (AC1 "both scopes"); exclude the platform-varying
  settings.local.json (same reason settings.json is excluded).
- New tests/claude-imperative-reference.test.cjs: adapter kind, programmatic-cli
  profile, fail-closed negotiation on a corrupted/partial descriptor, and an AC2
  source guard that no `runtime === 'claude'` branch remains.

No user-visible install-output change (internal architecture only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 13:31:53 -04:00
Tom Boucher
597ee3248a test: add failing regression for #2067 phase-complete checkbox regex
The checkbox regex in cmdPhaseComplete uses a greedy .* between ] and
'Phase N'. Completing an already-checked phase (idempotent re-run) wrongly
matches a later phase whose description mentions the target phase. These
tests encode the exact repro from #2067; they fail on next @ origin/next.
2026-07-08 10:52:55 -04:00
Tom Boucher
49d320a972 test: stabilize perf-316 lock-holder holdMs race under load
Worker A (lock holder) used holdMs=1000 as a safety cap on its
Atomics.wait for the writer's contention signal. Under container load
(full suite, node24) Worker B's spawn + require('state.cjs') + stub
installation can exceed 1s, so A's wait timed out and removed the lock
before B ever contended. B's first atomic-create then succeeded
(lockAttempts:1), failing the retry-path witness and red-flagging the
gsd-test gate on otherwise-green branches.

The handshake is the real release trigger; holdMs is only a safety cap
for a dead/hung writer, so it must be large enough to never elapse
during B's spawn+init. Bump to 30s (bounded; afterEach terminate()s A
on the normal path, so no added latency) and widen the per-test timeout
to 15s for spawn headroom under heavy parallel load.
2026-07-08 10:25:28 -04:00
Tom Boucher
0e039e6dfb Merge branch 'next' into enh/1578-critic-self-check 2026-07-08 08:58:57 -04:00
Alex V.
f15c25867d docs(#1578): rebase B2+B3 agent prompt changes 2026-07-08 12:57:49 +03:00
Tom Boucher
8ffb1261d1 Merge branch 'next' into fix/2028-phase-complete-milestone-end-and-workstream-guard 2026-07-07 23:26:39 -04:00
Tom Boucher
5aa7771dd3 Merge branch 'next' into fix/2009-load-failed-capability-injects-blocking- 2026-07-07 23:12:09 -04:00