Commit Graph

5 Commits

Author SHA1 Message Date
Tom Boucher
2f64e6230a feat(#3676): quick-batch command, workflow, and isolation integration (#4212)
* test(#3676): add failing tests for quick-batch dispatch core

Failing-first tests for Phase 4 of epic #3344 (ADR-1239 "Quick-batch
binding"): quick-batch-dispatch.test.cjs / .property.test.cjs cover the
new pure decision-logic module (arg validation, effective concurrency,
deterministic merge order, spawn backpressure, verification/merge
routing, cleanup-entry construction — design doc rows 3-15,24,26-28,
30-36,39; property rows 51-53). quick-batch-update-items.test.cjs
covers the new updateBatchItems export on src/quick-batch.cts (rows
15,22-23, including the negative cycle-rejection case).
quick-batch-command-router.test.cjs covers the new
gsd-tools quick-batch CLI family (rows 46-47). These reference modules/
exports that do not exist yet.

* feat(#3676): implement quick-batch dispatch core, updateBatchItems, and command router

Phase 4 of epic #3344 (ADR-1239 "Quick-batch binding") CORE decision
layer — CLI verbs and pure orchestration logic only; no workflow
markdown, no Agent()/git-worktree I/O.

- src/quick-batch-dispatch.cts (new): pure decision functions consumed
  by the (separate, follow-up) /gsd:quick-batch workflow markdown —
  parseQuickBatchArgs, computeEffectiveConcurrency, computeMergeOrder,
  computeSpawnPlan, routeVerificationOutcome, routeMergeOutcome,
  buildCleanupManifestEntry (the last parses caller-supplied plan text
  via the existing parsePlanDocument; no filesystem access).

- src/quick-batch.cts: adds updateBatchItems, resolving the design
  doc's Open Question 1 as ONE additive export on this module instead
  of the second, independent BATCH.json writer the design doc
  originally proposed. Reuses the same withPlanningLock transaction
  shape, computeWaves, and platformWriteSync call resumeBatch/
  completeQuickItem already use; fails closed without persisting on
  an unknown item, an unknown/self dependency, or an introduced cycle.

- src/quick-batch-command-router.cts (new): gsd-tools quick-batch CLI
  family, wired into HOST_COMMAND_ROUTERS (gsd-core/bin/gsd-tools.cjs)
  as a first-party always-on command (like /gsd:quick), not the opt-in
  capability-registry path graphify uses. Verbs: create/update/resume/
  complete (wrap quick-batch.cts) and effective-concurrency/
  merge-eligible/spawn-plan/verification-routing/merge-routing/
  cleanup-entry/parse-args (wrap quick-batch-dispatch.cts).

Design doc rows covered: 3-15, 22-24, 26-28, 30-39, 46-47. Property
rows 51-53. Rows covering workflow markdown / Agent() dispatch /
`git worktree` behavior (16-21, 25, 29, 40-45, 48-50) remain for the
follow-up markdown-authoring pass, per the phase brief's explicit
scope boundary.

* docs(#3676): register quick-batch-dispatch/command-router modules in bookkeeping surfaces

New-.cts-module ripple for the two Phase 4 modules (epic #3344,
ADR-1239 "Quick-batch binding"): .gitignore (compiled .cjs artifacts,
ADR-457 build-at-publish), eslint.config.mjs (lint the .cts source,
not the emitted .cjs), docs/INVENTORY.md + docs/INVENTORY-MANIFEST.json
(via `node scripts/gen-inventory-manifest.cjs --write`, after
`npm run build:lib`), and CONTEXT.md glossary entries for
"Quick-Batch Dispatch Core Module" and "Quick-Batch Command Router
Module", plus an update to the existing "Quick-Batch Core Primitives
Module" entry documenting the new updateBatchItems export.

* test(#3676): fold updateBatchItems tests into quick-batch.test.cjs (fix lint-test-file-count)

scripts/lint-test-file-count.cjs buckets any quick-batch-*.test.cjs
file under the quick-batch production module by longest-prefix match,
and that module is already at its 2-file cap (quick-batch.test.cjs +
quick-batch.property.test.cjs). The standalone
tests/quick-batch-update-items.test.cjs added in the prior commit
pushed it to 3 and failed `npm run lint:ci`. Fold its content into
quick-batch.test.cjs (append-only — no existing test in that file is
modified) and update the CONTEXT.md glossary reference to match.

Surfaced while re-running `GITHUB_BASE_REF=next npm run lint:ci` after
`npm ci` (this worktree previously had no local node_modules, which
also made gen-scripts-cli-exit/gen-hooks-cli-exit/gen-exit-code-*
unable to resolve typescript — resolved by npm ci, no code change
needed there). `npm run lint:ci` and
`npx tsc -p tsconfig.build.json --noEmit` are both green after this
fix.

* test(#3676): add failing tests for the quick-batch command/workflow markdown

Failing-first tests for Phase 4's markdown-authoring pass (epic #3344,
ADR-1239 "Quick-batch binding"): gsd-quick-batch-workflow.test.cjs
covers commands/gsd/quick-batch.md's frontmatter/objective/process,
gsd-core/workflows/quick-batch.md's byte-size boundary (row 49, ADR
1610 NEW_FILE_CAP) and step-fragment count, the isolation model
(rows 20-22), the executor single-writer invariant (row 18), merge
validation reusing the existing bounded primitive (row 25), the
optional research/plan-checker/verification leaves (rows 16,17,19,
30,31), planning-failure blocking execution (row 29), the submodule
guard (rows 36,44), and the new agents/gsd-planner.md quick-batch
mode (rows 13-15). gsd-quick-batch-quick-regression.test.cjs covers
row 48 (ordinary /gsd:quick stays byte-identical). Named
`gsd-quick-batch-*` (not `quick-batch-*`) so lint-test-file-count's
longest-prefix bucketing doesn't fold these markdown-only tests into
the already-capped quick-batch/quick-batch-dispatch/
quick-batch-command-router production-module buckets from the CORE
pass. These reference files that do not exist yet.

* feat(#3676): author the quick-batch command, workflow, and planner mode

Phase 4 markdown-authoring pass (epic #3344, ADR-1239 "Quick-batch
binding") — the orchestration layer that calls into Pass 1's CLI
verbs (src/quick-batch-command-router.cts).

- commands/gsd/quick-batch.md (new): frontmatter/objective/process,
  delegates argument validation to `quick-batch parse-args`
  (parseQuickBatchArgs) rather than re-deriving the grammar.

- gsd-core/workflows/quick-batch.md (new, 11843 bytes — under ADR
  1610's 32768-byte NEW_FILE_CAP for a brand-new file) + 9 lazy-loaded
  step fragments under gsd-core/workflows/quick-batch/steps/:
  resume-mode, batch-init, research-phase (flag:--research),
  planner-wave (+ nested plan-checker-loop when --validate),
  worktree-dispatch, merge-wave, verification-wave (flag:--validate),
  completion. Covers design doc rows 3-45: capacity/isolation
  resolution (reusing dispatch-isolation-gate.md verbatim), per-DAG-
  layer planning with full-task-catalog prompts and always-required
  depends_on/files_modified frontmatter, serialized worktree create/
  merge/cleanup via the existing worktree.cleanup-wave primitive,
  deterministic wave-order merging, verification routing
  (human_needed/gaps_found), the executor single-writer invariant,
  submodule fail-loud guard, and #1941 fork-base auto-degrade.

- agents/gsd-planner.md: additive new `load_mode_context` bullet for
  `**Mode:** quick-batch`, pointing at the new
  gsd-core/references/planner-quick-batch.md reference (documents the
  always-required depends_on/files_modified contract, reusing the
  existing frontmatter grammar — no new keys). Existing modes
  byte-identical, only a new bullet added.

- src/init.cts (+init-command-router.cts, +command-aliases.cts):
  cmdInitQuickBatch / `init.quick-batch` — model profiles,
  commit_docs, roadmap/planning existence checks, and the
  section_manifest field gating research-phase/verification-wave
  (reuses the existing flag:--research/flag:--validate WHEN_VOCABULARY
  atoms — no new atom needed).

Rows 16-21, 25, 29, 36, 38, 39, 44, 46-50 covered structurally by the
prior test(#3676) commit; rows 3-15, 22-24, 26-28, 30-35, 37, 40-43,
45 covered by construction (verb wiring, single-writer prompt
constraints, crash-window resume via unmodified Phase 3 primitives).

* docs(#3676): regenerate skills/inventory/section-manifest/install-tree; baseline the intentional word-splitting pattern

npm run regen:derived output for the new command/workflow/reference
(epic #3344, ADR-1239 "Quick-batch binding"):
- skills/gsd-quick-batch/SKILL.md (generated from commands/gsd/quick-batch.md)
- docs/INVENTORY.md rows for /gsd-quick-batch, quick-batch.md,
  planner-quick-batch.md, and the quick-batch-dispatch.cjs/
  quick-batch-command-router.cjs CLI-module rows' now-live
  `/gsd-quick-batch` cross-reference (was "(separate, follow-up)")
  + docs/INVENTORY-MANIFEST.json (`node scripts/gen-inventory-manifest.cjs --write`)
- gsd-core/workflows/section-manifest.json (`npm run gen:section-manifest`)
  — research-phase/verification-wave gsd:section entries for the new
  quick-batch workflow
- tests/fixtures/install-tree/*.json (`npm run gen:install-tree`) —
  the new command/workflow/skill/reference files now ship to every
  runtime

scripts/lint-workflow-shellcheck-baseline.json: 3 new entries for
gsd-core/workflows/quick-batch.md's intentional flag-token/$ARGUMENTS
word-splitting (SC2046/SC2086) — the same deliberate unquoted-optional-
flag pattern gsd-core/workflows/quick.md already carries baselined
(e.g. `$DISCUSS_PARAM $RESEARCH_PARAM` in quick.md's own Step 2);
quoting would break the intended "omit this arg when the flag is
false" splitting.

* fix(#3676): close prompt-injection and argv/glob-injection gaps in quick-batch leaf dispatch

Security review pass findings, both confirmed real:

1. HIGH — prompt injection, no boundaries. Every leaf-dispatch fragment
   interpolated the raw, attacker-influenced task ${description} (and
   the shared ${TASK_CATALOG_TABLE}, broadcasting every item's raw
   description into every planner's prompt in the layer) straight into
   Agent() prompt bodies with no boundary. Fixed by wrapping every such
   interpolation in a <security_context> + DATA_START/DATA_END
   boundary, matching the CONCRETE convention already implemented in
   this repo (agents/gsd-debug-session-manager.md, agents/gsd-debugger.md,
   gsd-core/workflows/debug.md) — commands/gsd/quick.md's own
   <security_notes> only asserts this convention in prose, so the
   debug-agent files are the real precedent followed here. Added a new
   <security_notes> block to commands/gsd/quick-batch.md (it had none)
   documenting both this fix and the one below.

2. MEDIUM — unquoted $ARGUMENTS -> argv/glob injection.
   gsd-core/workflows/quick-batch.md and commands/gsd/quick-batch.md both
   ran `gsd_run quick-batch parse-args --raw -- $ARGUMENTS` UNQUOTED,
   causing shell word-splitting and pathname expansion on raw task-list
   text before the parser ever saw it. Fixed at the source: added a
   `--text <string>` form to the `parse-args` verb
   (src/quick-batch-command-router.cts) that accepts the ENTIRE
   $ARGUMENTS as ONE quoted argv element and does the whitespace split
   itself, in Node — which is never glob-aware, unlike the shell.
   Both call sites now use `--text "$ARGUMENTS"`. The `-- <tokens>` form
   is kept for direct/test callers that already have a real argv array.

The SC2086 baseline entry added for the original unquoted line is now
stale (`node scripts/lint-workflow-shellcheck.cjs` no longer reports
it) and has been removed; the two SC2046 entries for the UNRELATED,
still-unquoted `$([ "$VALIDATE_MODE" = true ] && echo --validate)`-style
conditional-flag splitting remain — that line only ever expands to one
of a few known-safe literal strings (never raw user text), matching
quick.md's own already-baselined convention exactly.

Tests: quick-batch-command-router.test.cjs covers the new --text form
(token splitting, glob-shaped text passing through literally
unexpanded, whitespace-only input). gsd-quick-batch-workflow.test.cjs
asserts the DATA_START/DATA_END boundary on every leaf prompt
(research-phase/planner-wave/plan-checker-loop/verification-wave,
including the shared task catalog) and the quoted --text call sites.

* fix(#3676): strengthen test-depth gaps in rows 9, 18, 24, 34, 35

Spec review pass findings — the test matrix claimed "yes" coverage
these assertions did not actually support:

- Row 9 (--jobs 0/-1/abc hostile case): previously asserted rejection
  only. Added an end-to-end assertion (tests/quick-batch-command-router.test.cjs,
  committed alongside the security fix that touches the same file) that
  .planning/quick-batches/ is never created for any rejected value —
  createBatch is genuinely never reached.
- Row 18 (--resume <unknown-batch-id>): previously only exercised a
  hand-corrupted BATCH.json, never a genuinely nonexistent batch
  directory. Added the real nonexistent-id case (also in
  quick-batch-command-router.test.cjs).
- Row 24 (post-planning updateBatchItems racing a concurrent
  completeQuickItem for a different item, both through
  withPlanningLock): zero test existed. Added a property test
  (tests/quick-batch.property.test.cjs, appended — Phase 3's own file,
  no existing test touched) exercising both call orders and asserting
  no lost update in the final on-disk manifest — the same technique
  Phase 3's own row-15 lock-contention property test uses (sequential
  calls through the real lock; a working mutex makes any interleaving
  equivalent to some serial order, so this is the same claim a literal
  concurrent-thread test would make without OS-level threading).
- Row 34 (worktree preserved on merge_failed) and row 35 (undeclared-
  deletion detection): both were previously asserted only at the pure
  routeMergeOutcome level. Added tests/gsd-quick-batch-merge-integration.test.cjs
  using the SAME real-git-fixture pattern tests/worktree-safety.test.cjs
  already establishes for executeWorktreeWaveCleanupPlan (real repo,
  real worktree, a REAL merge conflict / a REAL file deletion diffed
  against declared_deletions) — asserting the actual worktree directory
  survives on disk, not just that a pure function returns a
  preserveWorktree:true field. Named gsd-quick-batch-* so lint-test-
  file-count's bucketing doesn't fold it into any capped module bucket.

Row 48 (/gsd:quick regression) intentionally left as-is per the
reviewer's own framing: the byte-identity claim is already
mechanically proven by the changed-path diff (git diff --name-only
empty on those two paths IS byte-identity), and a genuine execution-
level regression test would require actually running the workflow —
out of scope for this repo's unit-test model (no other quick.md
regression test in this repo does that either).

* docs(#3676): add the changeset and user-facing docs the command needed

Standards review pass findings — both HARD:

- Missing changeset. None of the 6 prior #3676 commits touched
  .changeset/*. /gsd-quick-batch is a new user-facing command;
  CLAUDE.md/CONTRIBUTING.md require one. Added
  .changeset/silly-rams-caper.md (type: Added, pr: 0 placeholder —
  backfilled after the PR opens, matching CLAUDE.md's own documented
  convention and Phase 3's own precedent, #4190's
  .changeset/mellow-yaks-squeak.md). Uses the docs-convention hyphen
  form `/gsd-quick-batch` throughout, never the source-artifact colon
  form (`scripts/lint-docs-command-form.cjs` confirms 0 violations;
  that check scans docs/**, not .changeset/, so it was never actually
  in scope for the fragment itself, but the wording still follows the
  doc convention for consistency, matching how Phase 3's own fragment
  named the not-yet-shipped command).
- Missing docs. Added docs/how-to/batch-quick-tasks.md (Diátaxis
  how-to, matching docs/how-to/handle-quick-and-fast-tasks.md's
  existing convention for /gsd-quick /gsd-fast) covering --jobs,
  --validate, --research, --resume, --file, the capacity/isolation
  interaction, and resume/failure recovery. Cross-linked from
  docs/README.md's how-to index and from handle-quick-and-fast-tasks.md's
  own "Related" section. Added a /gsd-quick-batch section to
  docs/COMMANDS.md (same table format as the existing /gsd-quick
  entry) and docs/features/quick-batch.md (REQ-QB-01..12, same
  frontmatter shape as docs/features/quick-mode.md) — regenerated
  docs/FEATURES.md (179 features) and skills/gsd-quick-batch/SKILL.md
  via the standard generators.

* fix(#3676): close docs-parity, attribution, and generated-registry gaps gsd-test caught

gsd-test's real run against 155e8975b3 found 43 failures, all rooted in
this phase's own new command/workflow never being registered across
~10 independent generated/hand-maintained registries this repo keeps
in parity by convention. Root-caused each, no test weakened or
special-cased.

- help.md ↔ commands/gsd/ bidirectional parity (docs-parity-live-
  registry.test.cjs): added a /gsd:quick-batch entry to
  gsd-core/workflows/help/modes/full.md (the real help.md content;
  gsd-core/workflows/help.md is a thin dispatcher) documenting every
  flag (--file/--jobs/--validate/--research/--resume), matching the
  existing /gsd:quick entry's format.

- gen-section-manifest.test.cjs: quick-batch.md's
  `gsd_run query init.quick-batch` invocation used inline
  `$([ ... ] && echo --flag)` substitutions, which never satisfy the
  test's exact-whitespace-token / assigned-variable detection (the
  trailing `))` glued onto `--research` in the compound substitution
  broke the "exact token" match). Rewrote to the same
  VALIDATE_PARAM/RESEARCH_PARAM two-line pattern
  gsd-core/workflows/quick.md's own Step 2 already uses.

- runtime-launcher-parity.test.cjs: the 8 quick-batch/steps/*.md
  fragments that call gsd_run each needed their OWN embedded copy of
  the canonical shim preamble (every workflow .md that calls gsd_run
  carries its own copy — reading one file does not persist shell state
  into another). Ran `node scripts/sync-runtime-launcher.cjs`, which
  inserted it before each file's first gsd_run call.
  plan-checker-loop.md correctly has none — it never calls gsd_run
  directly.

- Namespace routing (skill-manifest.test.cjs, install-nested-
  layout.test.cjs, runtime-artifact-layout-surface.test.cjs): added
  `quick-batch` to commands/gsd/ns-workflow.md's `requires:` array and
  routing table (same namespace `quick` already routes through), and
  to src/clusters.cts's `utility` cluster (same cluster `quick`
  already belongs to). Verified by hand-running installRuntimeArtifacts
  + applySurface for augment/cline against a real temp install: exactly
  6 top-level gsd-ns-* router dirs, gsd-quick-batch correctly nested
  under gsd-ns-workflow/skills/, never re-flattened.

- mcp-server-catalog.test.cjs: hardcoded command count 71 -> 72 (a
  brand-new command is a real count change, not a bug this test should
  hide).

- model-omit-when-inherit-guard.test.cjs: added the canonical
  `<!-- #2517 model-omit-on-inherit -->` marker block to
  gsd-core/workflows/quick-batch.md (every leaf dispatch — planner/
  researcher/checker/executor/verifier — lives in a steps/ fragment,
  read combined with the host by this test's own readWorkflowCombined,
  same as quick.md's own research-phase.md carries it for its gated
  section). Also fixed a genuine pre-existing inconsistency in the
  test's own "#2711: the guarded set is derived from dispatch sites"
  check: its `nonDispatching` computation read the BARE host file while
  `derived` (the set it's checked against) reads the combined
  host+steps content — inconsistent with that same test file's own
  #2994 doc comment explaining why the combined read is necessary.
  quick-batch.md is the first workflow whose EVERY model="{...}"
  dispatch site lives in a mandatory (never gated) steps/ fragment —
  extracted to stay under ADR-1610's tighter NEW_FILE_CAP for a
  brand-new file — which is what exposed the mismatch. Fixed by using
  the same readWorkflowCombined read in both places.

- skill-frontmatter-contract.test.cjs: shortened
  commands/gsd/quick-batch.md's frontmatter `description` from 107 to
  91 chars (<=100 budget), and added `quick-batch.md` to the hand-
  maintained KNOWN_SKILLS consolidation allowlist with a #3676
  justification comment (a genuinely new first-party command, not a
  consolidation of an existing skill).

- workflow-fragments-emission.install.test.cjs: added `quick-batch.md`
  to the hand-maintained MARKED_WORKFLOWS set (composeWorkflow is
  deliberately NOT a no-op for it — its research-phase/verification-
  wave sections are gated).

- Regenerated all downstream artifacts (npm run build:lib && npm run
  regen:derived && npm run gen:plugin-skills -- --write && npm run
  gen:features -- --write): skills/gsd-quick-batch/SKILL.md,
  skills/gsd-ns-workflow/SKILL.md, install-tree fixtures for
  augment/cline/hermes/qwen/trae/zcode.

- emitted-attribution.test.cjs: agents/gsd-planner.md's #3676 addition
  (one new `load_mode_context` bullet pointing at the new
  gsd-core/references/planner-quick-batch.md reference) grew the file
  124 bytes without an acknowledgment trailer. Acknowledged below —
  the growth is the deliberate, additive, single-bullet change from
  the earlier feat(#3676) commit, not drift.

Verified: npm run build:lib clean, npx tsc -p tsconfig.build.json
--noEmit clean, GITHUB_BASE_REF=next npm run lint:ci fully green
(includes lint-workflow-shellcheck, lint-test-file-count,
lint-docs-command-form). The deep install/spawn/registry tests gsd-test
actually runs (docs-parity-live-registry, gen-section-manifest,
runtime-launcher-parity, install-nested-layout,
runtime-artifact-layout-surface, skill-manifest, skill-frontmatter-
contract, mcp-server-catalog, model-omit-when-inherit-guard,
workflow-fragments-emission) are not part of lint:ci — each fix above
was independently verified by hand-invoking the exact production
function the failing test calls (installRuntimeArtifacts, applySurface,
composeWorkflow, the CLUSTERS union, the section-manifest forwarding
regex) against the real repo tree and confirming the expected shape.

Emitted-Drift-Ack-Growth: gsd-planner.md — additive #3676 quick-batch mode bullet in load_mode_context (one new line pointing at gsd-core/references/planner-quick-batch.md); not drift.

* fix(#3676): trim the /gsd:quick-batch help.md entry to fit the LARGE tier line budget

skill-frontmatter-contract.test.cjs's "feature #3039: tiered help —
size budgets" enforces a SEPARATE line-count ceiling for
gsd-core/workflows/help/modes/full.md (FULL_BUDGET = 844 lines,
tighten-only ratchet, scripts/lib/allowlist-ratchet.cjs's
assertTightCeiling) — independent of the skill-frontmatter description-
length budget and consolidation allowlist I touched in the prior round;
those are unrelated checks in the same test FILE, not the same check.

Root cause: the /gsd:quick-batch entry I added to full.md in the
docs-parity fix round was 17 lines, pushing the file from 834 to 851
lines — 7 over the 844 ceiling. Condensed the entry (merged the
per-flag bullet list into one dense "Flags:" line, dropped from 3
Usage examples to 1) to 844 lines exactly — at the ceiling with zero
slack, which assertTightCeiling accepts (it only fails on
actualMax > ceiling, or on slack > grace when the ceiling is too
LOOSE — zero slack triggers neither).

Verified after trimming: full.md still contains a live /gsd:quick-batch
reference (bidirectional parity) and all 5 argument-hint flags
(--jobs/--validate/--research/--resume/--file) still appear as literal
tokens (docs-parity-live-registry.test.cjs's own flag-coverage check,
re-run by hand against the trimmed content).

Verified: npm run build:lib clean, npx tsc -p tsconfig.build.json
--noEmit clean, GITHUB_BASE_REF=next npm run lint:ci fully green.

* docs(#3676): backfill changeset pr number to 4212

Follow-up to fix(#3676) commits — .changeset/silly-rams-caper.md's
pr:0 placeholder backfilled with the real PR number now that
gh api POST /pulls has returned it (#4212). Matches CLAUDE.md's PR
Number Handling convention and Phase 3's own #4190 precedent
(708c5a3f8c). Doc-only (root-level .changeset/*.md fragment), exempt
from a fresh gsd-test run per pre-pr-gate.sh's DOC_ONLY_RE.

* fix(#3676): resolve prompt-injection-scan false positive on test fixture

tests/quick-batch.test.cjs:232's row 11b regression proves the task-list
parser carries a prompt-injection-shaped task description through
createBatch as inert data, never interpreted. The fixture has to be a
real "ignore all previous instructions..." phrase or the test asserts
nothing, but the full-file --diff scan flagged it once unrelated edits
in the same file pulled it into the changed-file set.

Add the file to prompt-injection-scan.sh's ALLOWLIST, matching the
sanctioned, precedented exemption already used for other legitimate
security-regression fixtures (tests/windsurf-conversion.test.cjs,
tests/health-validation.test.cjs, tests/continuation-grammar-parity.test.cjs)
per DEFECT.PROMPT-INJECTION-SCAN-COLLISION.

---------

Co-authored-by: sim <sim@local>
2026-09-02 22:38:31 -04:00
Tom Boucher
ff4a57b78c chore(#1671): migrate the remaining 13 LARGE/XL workflows to the fragment model — Phase 6.3 (#3030)
* chore(#2994): fragmentize progress.md forensic audit onto the fragment model

Extract the --forensic-gated forensic_audit step to
workflows/progress/steps/forensic-audit.md behind a section marker, and
repair progress.md's init line to forward --forensic so the atom is
actually true in production rather than only under direct CLI tests.

progress.md shrinks 32630 -> 27207 bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2994): fragmentize the four manifest-wired workflows

new-project, quick, new-milestone and progress each already had a
dedicated cmdInit* entry point but zero marked sections. Extract nine
gated bodies to workflows/<wf>/steps/ behind section markers and repair
each init line to forward its flags.

Fold --full into the discuss/research/validate facts inside cmdInitQuick
so the when= grammar never sees an OR, per the chunked-mode precedent.

Fixes found while working, per the no-defer rule:
- cmdInitProgress passed no phase info to buildSectionManifestField, so
  state:phase-mvp-mode was permanently false — an atom in the vocabulary
  whose fact could never be computed.
- the quick init router folded flag tokens into the free-text
  description, which the new forwarding would have corrupted.
- a #2508 dispatch note was nested inside quick.md's Agent(prompt=)
  fence, leaking orchestrator guidance into the subagent prompt.
- progress.md had a 3-vs-4 backtick outer-fence imbalance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2994): fragmentize verify-work.md and admit state:ui-phase-active

Wire cmdInitVerifyWork to buildSectionManifestField — it was a dedicated
entry point that never emitted a manifest — and mark two sections.

state:ui-phase-active folds (plan:pre hooks include an active ui step) OR
(the phase dir holds a *-UI-SPEC.md) into one boolean in init.cts, so the
grammar still sees a single operator-free atom. The inner Playwright-MCP
check stays as prose inside the fragment: it is live session state and no
init seam can precompute it.

The MVP false-branch note is a real fallback, not redundant prose, so it
sits outside the marker — gating it away would delete the text needed
precisely when MVP mode is off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(#2994): follow moved workflow content in drift guards

Retarget every guard that asserted on content this branch moved into
workflows/<wf>/steps/, mirroring 815b3d897. Each retargeted assertion was
verified to still fail when its step file is blanked, so none was
weakened into vacuity.

Three assertions in verify-mvp-uat were genuinely red. Three more were
worse than red — passing for the wrong reason:
- quick-commit-boundary and worktree-cleanup anchored on indexOf('Step
  5.6'), which matched a later cross-reference and sliced 16069 chars
  that coincidentally held the asserted substrings. Replaced with an
  expandWorkflowSections helper that splices step content back in place.
- phase6-review-capabilities lost its end boundary and widened to EOF.
- playwright-ui-verify matched 'UI' in an unrelated bullet and 'fall
  back' in a subagent-dispatch line after the real content moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2994): fragmentize code-review and complete-milestone, admit three atoms

Add dedicated cmdInitCodeReview and cmdInitCompleteMilestone entry points
alongside the shared generic ones rather than modifying them — init.phase-op
and init.manager carry a CRITICAL blast radius (179 dependents, 24
processes) and stay byte-identical for their other callers.

Admit flag:--fix, state:fallow-enabled and state:git-create-tag, each with
a consuming section and a fact its own entry point computes.

Both sections had the resolver-in-body hazard: the fallow config-gate and
the git.create_tag check each sat inside the very block being gated, so
gating would have disabled the resolver that decides the gate. Both are
hoisted into init and the bodies now consume the resolved fact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(#2994): retarget code-review and milestone drift guards, fix two red tests

Retarget guards that asserted on content moved into steps/, proving
non-vacuity by blanking each step file and confirming failure.

Also fixes two genuinely red tests found while working, per the no-defer
rule:
- workflow-fragments' frozen-vocabulary lock was missing
  state:ui-phase-active, so commit 7ef7f8336 shipped red. Lint and build
  both passed over it, which is why neither is sufficient verification.
- code-review's quick.md capability-hook assertion carried a stale
  delimiter after the 18ff35d20 extraction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2994): fragmentize autonomous.md and admit state:plan-strategy-converge

Five sections share one atom, the pattern plan-phase already uses for
flag:--research-phase. The atom folds --converge OR --cross-ai into a
single boolean in cmdInitAutonomous so the grammar stays operator-free.

cmdInitAutonomous is additive; init.milestone-op, init.manager and
init.phase-op are untouched and still consumed. The $PLAN_STRATEGY bash
resolver is deliberately retained — ungated local-planning bullets still
read it, so the init-side fact supplements it rather than replacing it.

converge-fail-fast required splitting one bash fence so the always-run
CONVERGENCE_ARGS construction stays outside the marker. All three
flag-absent fallbacks were left outside their markers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2994): fragmentize review and discuss-phase-assumptions

Admit state:reviewer-instances-configured (two peripheral notes share it;
the core reviewer-lane dispatch stays unmarked — it is the workflow's
primary always-evaluated logic, not an optional branch) and
state:auto-advance-active, which folds --auto OR two config keys into one
boolean so the grammar stays operator-free.

discuss-phase-assumptions was the highest-risk edit in this PR. Its
auto_advance step is a full if/elif/else; gating it whole would have
deleted the flag-absent fallback needed exactly when --auto is off. Split
verified exact: resolvers 636-651 and the 'End here' fallback 668-669 both
stay outside the marker; only 653-667 is gated.

Adds emitted-drift acks for the two files that grew — review.md (+55 B)
and autonomous.md (+737 B from 80799211c, which had none and would have
red-gated the push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2994): fragmentize docs-update, update, transition and new-milestone Part A

Completes the 13-workflow rollout. Three of these had no init call at all
and gained a dedicated entry point plus their first gsd_run query line.

Admits state:is-monorepo and adds state:next-channel, state:workstream-active
and state:flat-mode. Vocabulary 26 -> 30 atoms.

Part A of new-milestone applies when NO workstream is active — the negation
of state:workstream-active. Rather than teach the grammar negation, which is
the Greenspun drift the frozen list exists to prevent, it gets a separate
positively-phrased atom whose fact is the inverse. Part B, which always runs,
stays outside the marker.

flag:--verify-only is deliberately NOT admitted: docs-update has no
contiguous purely-additive region for it, and an atom without a consuming
section is dead vocabulary. Evidence recorded in the slice report.

update.md reuses its existing resolved $GSD_TOOLS rather than prepending the
canonical preamble, which would have clobbered it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2994): stop automated-ui-verification re-resolving its own gate, retire dead vocabulary

Two defects the new tests caught.

The automated-ui-verification step re-ran gsd_run loop render-hooks and
recomputed UI_PHASE_ACTIVE inside a body that is only read when that fact
is already true — the circular self-disabling pattern this design forbids,
introduced by 3c654b168. cmdInitVerifyWork now exposes ui_phase_active and
the step consumes it. Its launcher preamble goes too: no gsd_run remains.
The Playwright-MCP check stays as prose — that is live session state.

Dead vocabulary predating this PR: flag:--full and state:needs-codebase-map
were admitted with a gate-1 claim that never materialized. flag:--full is
removed, redundant once quick folds it into discuss/research/validate.
state:needs-codebase-map gets the real consumer it always lacked, gating
new-project's codebase-map offer. Vocabulary 30 -> 29, and no atom is now
without a consuming section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(#2994): add the atom-admission, inversion and resolver-hoist gates

The two existing parity guards prove vocabulary/predicate symmetry but
never that a fact is computed — an atom no cmdInit* assembles evaluates
false forever. These close that hole:

- per-atom satisfiability for all 29 atoms, plus an anti-vacuity assertion
  so the loop cannot silently cover zero atoms
- dead-vocabulary check against the shipped manifest
- inversion guard: the flag-absent fallbacks in discuss-phase-assumptions
  and verify-work must stay outside their markers
- data-driven resolver-hoist guard over the shipped manifest, so a future
  extraction cannot reintroduce the circular class
- compound-fold coverage (--full, --cross-ai, --rc, config-only --auto)
- null-vs-[] degraded/computed distinction, and flag value shapes

Also repairs the frozen-vocabulary lock, which was stale and red for the
seven atoms earlier commits on this branch shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(#2994): add changeset for the fragment-model rollout

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(#2994): cite the issue on the two new allow-test-rule exemptions

ADR-456 requires an issue ref on the same line as the annotation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(#2994): correct the atom-count claims after retiring flag:--full

The vocabulary doc comments still said 30 entries; it is 29 since
flag:--full was removed as dead vocabulary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2994): dedupe the phase-fallback block and harden --ws parsing

Review findings.

MAJOR: the three new init entry points each pasted a verbatim copy of the
guardedFindPhase/guardedGetRoadmapPhase fallback, taking the repo from four
copies to seven — DEFECT.GENERATIVE-FIX. Extracted applyRoadmapFallback and
folded six of the seven; each call site keeps its own field-set via a
closure. Duplication removed rather than papered over with a parity test.
cmdInitPhaseOp stays out: its fallback omits has_reviews, so it is not a
byte-identical copy, and it is CRITICAL-radius.

LOW, pre-existing: GSD_WS captured [^[:space:]]+ and expands unquoted, so a
workstream name holding glob metacharacters would expand against the
filesystem. Narrowed to [A-Za-z0-9._-]+. The unquoted expansion is kept —
it must word-split into two args and vanish when empty.

Also restores the vocabulary ordering convention, and fixes a masked test
bug the mandated run surfaced: the flag-forwarding guard checked only the
first init line per workflow, but new-milestone has two, so a real failure
was reporting exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2994): drop the stale new-milestone emitted-drift ack

new-milestone.md was acked for a +406 B growth measured against an
intermediate commit. Net against origin/next it SHRANK by 8 bytes, so
nothing needed the ack and it explained nothing — which the differential
attribution check reports as a stale acknowledgment, not a pass.

update.md's entry stays: it genuinely grew +703 B.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2994): resolve the 15 failures from the full matrix run

All 15 were real and identical on both lanes.

REAL REGRESSION: autonomous.md hit 41479 chars against the #2196 guard's
40960 cap — a CHARS cap distinct from the LARGE tier byte cap, which the
five section stubs pushed it over. Extracted the 3a.5 UI Design Contract
body to references/; now 39968 chars, and the file nets -795 B vs base, so
its growth ack is deleted rather than left stale.

REAL DEFECT: docs referenced /gsd-transition, which is not a live
registered command. Reworded.

STALE FIXTURE: the emission byte-identity test hardcoded two marked
workflows; this branch legitimately marks fifteen. Fixture corrected — the
source was right.

The rest were drift guards over the eight workflows the earlier sweep did
not cover, retargeted at where the content now lives with non-vacuity
proven by blanking each step file and confirming failure. The GSD_WS
forwarding guard was checked as a possible real break and is not one: the
charclass narrowing is intact and forwarding works end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2994): drop the ack for a newly-added reference file

A new file's emitted ripple is attributable to the diff that adds it, so
the acknowledgment explained nothing and the differential check reports it
as stale. Removing the last entry removes the fragment — an empty one
signals nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#2994): retarget the UI-contract guards and clear two transitive advisories

The §3a.5 extraction that brought autonomous.md under the #2196 char cap
moved its body to references/autonomous-ui-design-contract.md, so ten
guards in autonomous-ui-steps and check-ui-safety-gate were asserting it
against the host. Retargeted via a combined read, each proven non-vacuous
by blanking the reference file and confirming failure.

This class had already bitten twice on this branch because each sweep was
scoped to the workflows touched at that moment, so this one was
exhaustive: ~70 test files across all 13 workflows, zero further broken or
vacuous assertions found.

Also clears two high transitive advisories the matrix flagged on one lane
— fast-uri GHSA-7p8r-x3mc-p8w7 and three ip-address SSRF/trust-boundary
issues. Both pre-date this branch: package-lock.json was untouched until
now, so the production tree was byte-identical to the base. Lockfile-only,
package.json unchanged, verified against a real npm ci install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(#2994): backfill changeset pr number to 3030

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:59:58 -04:00
Tom Boucher
ad3b9ec486 chore(#1671): fragmentize plan-phase.md and repair flag forwarding to the init bundle — Phase 6.2 (#3019)
* chore(#2993): fragmentize plan-phase.md onto the fragment model

Epic #1671 Phase 6.2. plan-phase.md is the largest workflow in the repo and
carried zero markers; it was deferred out of the Phase 3 pilot for two
reasons, both now dead. The 36-byte PRE_PHASE6 headroom was never the
blocker it looked like — fragmentizing is net-negative on host source, so
the trim is what creates the room. The --mvp interleaving was resolved by
measurement in #2992 and no sub-line mechanism is built.

- widen WHEN_VOCABULARY 14 -> 19 via a second coordinated ADR-1671
  amendment: flag:--ingest, flag:--prd, flag:--research-phase,
  flag:--reviews, state:chunked-mode
- state:chunked-mode is `--chunked` OR config workflow.plan_chunked, and
  that disjunction is resolved in the FACT, never in the grammar, so a
  compound condition never becomes an operator
- parse the new flags on the plan-phase route; extract six gated bodies to
  gsd-core/workflows/plan-phase/steps/ behind manifest-gated stubs
- prd-express-path.md was already extracted but read unconditionally; its
  wrapper is now gated, so the existing extraction finally pays off

plan-phase.md 94,483 -> 87,575 bytes (cap 94,519): headroom goes from 36
bytes to 6,944.

Also closes a surfaced docs gap: five real plan-phase flags (--chunked,
--skip-ui, --bounce, --skip-bounce, --granularity) were documented in
neither the argument-hint nor help. Making --chunked load-bearing without
fixing its siblings would leave the defect class half-open.

Refs #2993

* fix(#2993): forward flags to the init bundle so section gating actually fires

Blocker found by the correctness review, confirmed directly, and missed by
both the isolated reviewer and every test in this branch.

Neither workflow forwarded its flags to the init CLI:

  plan-phase.md:71    INIT=$(gsd_run query init.plan-phase "$PHASE" $GRAN_PARAM)
  execute-phase.md:84 INIT=$(gsd_run query init.execute-phase "${PHASE_ARG}")

So every flag: atom was permanently false in production and its section
permanently excluded. For plan-phase that made the PRD express path
UNREACHABLE — a regression, since it was an unconditional read before.
For execute-phase this is PRE-EXISTING: #2932 shipped `flag:--wave` gating
that has never once been true, so `--wave` silently dropped its own
wave-filtering guidance. Fixed here under the no-defer rule.

Why every test missed it: they drive the init CLI directly with flags,
which works. Production goes through the workflow's bash line, which did
not pass them — the exact "assert against the shape production uses" trap
this branch's own test matrix warns about.

- parse and forward --prd/--ingest/--research-phase/--reviews/--chunked
  (plan-phase) and --wave (execute-phase), using the anchored regex idiom
  the neighbouring GRAN_PARAM line already uses
- add a regression guard DERIVED FROM THE MANIFEST: for every flag:--X
  section, the owning workflow's init line must forward --X. It fails
  against the pre-fix files and covers any future atom, rather than
  spot-checking today's six.

Verified through the workflow shape, not the CLI shape: `3 --prd spec.md`
now yields ["prd-express-gate"] (was []), `2 --wave 2` yields
["partial-wave"] (was []).

Refs #2993

* test(#2993): acknowledge the execute-phase ripple and regenerate install-tree fixtures

Remote matrix was red with 46 unique failures, identical on both lanes.
Both causes are mechanical consequences of changing shipped workflow
content, and neither is visible to any local gate.

- emitted-attribution: execute-phase.md grew 163 bytes from the WAVE_PARAM
  forwarding fix and was unacknowledged, while the ack fragment named
  plan-phase.md, which SHRANK and therefore needed no ack at all — a stale
  entry is itself a failure. The reason now names the real ripple.
  The entry had to merge into the existing 2930 fragment: the ack linter
  does unconditional cross-fragment duplicate-key detection with no
  spent/live exception, so a second fragment declaring execute-phase.md
  collides even when the first is already merged and inert. Resolved per
  the linter's own guidance and that file's precedent of appending
  successive ripple reasons to one entry.
- golden-install-tree: tests/fixtures/install-tree/*.json are committed and
  deliberately excluded from the ADR-2719 attribution cutover, so they must
  be regenerated when shipped tree content changes. Regenerated after
  build:lib per the ordering landmine. 19 runtimes each gained exactly the
  six new plan-phase step files; zero paths removed, which is the absolute
  failure shape those fixtures exist to catch.

Refs #2993

* fix(#2993): restore the launcher preamble in an extracted step and follow moved content in its drift guards

Second red run: 26 unique failures, identical on both lanes, in two classes.

RUNTIME BUG (runtime-launcher-parity, 7 failures) — chunked-planning-mode.md
calls gsd_run but carried no canonical launcher preamble, which is what
DEFINES gsd_run(). On any non-Claude runtime that step would fail outright.
The preamble is now copied verbatim from the canonical source of truth,
gsd-core/workflows/_runtime-launcher.snippet.sh, and the fence dedented to
column 0 to match the prd-express-path.md sibling (a list-continuation
indent breaks the byte-equal preamble match). prd-express-path.md already
had a correct one. This is the same defect #2932 hit when it extracted
steps; the parity test caught a real bug, not a stale assertion.

DRIFT GUARDS (plan-phase-drift-guard, issue-2762-plan-reviews-chunked,
skill-frontmatter-contract) — these assert plan-phase.md contains content
this branch moved into step files. Retargeted at where the content now
lives, with the asserted property unchanged; the ALL-RUNTIMES label COUNT
test now reads host + every step file so the count is preserved across the
split rather than reduced. Each retargeted guard was verified to still fail
when its step file is stripped, so none was weakened into vacuity.

No emitted-drift ack was needed: currentSizes() enumerates
gsd-core/workflows/*.md non-recursively, so files under
plan-phase/steps/ are never in the size ratchet's scope.

Refs #2993

* chore(#2993): backfill changeset pr number to 3019

---------

Co-authored-by: sim <sim@local>
2026-08-03 09:38:58 -04:00
Tom Boucher
f1af47766a chore(#1671): widen the when= grammar and key the section manifest per workflow — Phase 6.1 (#3013)
* chore(#2992): widen the when= grammar and key the section manifest per workflow

Epic #1671 Phase 6.1. Two blockers stopped the fragment model reaching any
file beyond execute-phase.md: the when= vocabulary was frozen at 4 atoms
(3 execute-phase-specific), and the section manifest was single-workflow by
construction with 'execute-phase' hardcoded into buildSectionManifestField.

- widen WHEN_VOCABULARY 4 -> 14 via a coordinated ADR-1671 amendment; the
  grammar stays CLOSED (one atom, no operators, negation or nesting) and
  WHEN_PREDICATES stays a hand-written literal map, never deriving a
  predicate from its atom string
- InvocationFacts gains flags: ReadonlySet<string> plus three computed state
  booleans; add the missing reverse vocabulary/predicate parity guard
- key the manifest artifact per workflow; a stale flat {sections:[...]}
  artifact now fails shape validation instead of being misattributed
- wire the field into six init entry points and parse the flags each needs

An atom ships only with both a real consuming section and a fact the init
seam actually computes. Six surveyed atoms are withheld because their
workflows have no dedicated init entry point; an atom without a computed
fact evaluates false forever and silently disables its own section.

Fixes a defect found while wiring: parseNamedArgs always materializes a
boolean flag key, so folding its false into the absent sentinel is required
or every flag reads as present and gating is silently always-on.

Also resolves ADR-1671:194 by measurement: --mvp stays unmarkable, because
its interleaved sites are always-run flag resolution and a ~340 byte block
that already delegates lazily.

Refs #2992

* fix(#2992): treat any falsy option value as an absent flag and reject unsafe manifest read paths

Findings from two orthogonal reviews (Claude /code-review + an isolated
adversarial pass); both independently reproduced the first one.

- MAJOR: the flags-builder treated only `undefined` as absent, but
  parseNamedArgs yields `null` for an absent value-flag and `false` for an
  absent boolean-flag, so `--granularity` read as present on every
  plan-phase invocation. Fixed at the root: a flag is present iff its
  option value is truthy. The six per-handler `|| undefined` folds are now
  redundant and removed, which also closes the duplicate-translation and
  missed-onboard-handler findings.
- MAJOR: state:needs-codebase-map had zero coverage. Added unit, property
  and real-CLI integration tests.
- MINOR: reject absolute, UNC/drive and `..`-traversing `read` paths in the
  manifest, degrading the whole load to null like every other shape
  violation. Verified: `/etc/passwd` previously reached section_manifest.read.
- MINOR: corrected a stale "4 to 20" doc comment; the vocabulary is 14.

Refs #2992

* test(#2992): update the generator suite for the per-workflow manifest shape

The remote matrix went red with 5 unique failures, identical on
linux-node22 and linux-node24, all in tests/gen-section-manifest.test.cjs.
Re-keying the artifact to {workflows:{...}} left this suite asserting the
old flat {sections:[...]} shape; nothing else in the tree still does.

- three tests read manifest.sections.length, now undefined; retargeted at
  workflows.<name> with their original intent preserved (a fenced or
  loop-host marker still asserts NO section is produced, not merely a
  changed count)
- the stale-manifest test wrote its fixture in the OLD shape, so it tripped
  shape validation and stopped exercising staleness at all. Its fixture is
  now valid-but-mismatched so FAIL_STALE is genuinely reached again.
- added the coverage that exposed: a pre-6.1 flat artifact must report
  FAIL_MANIFEST_MALFORMED_SHAPE. That is the real upgrade path for an
  installed tree and nothing covered it.

Refs #2992

* chore(#2992): backfill changeset pr number to 3013

---------

Co-authored-by: sim <sim@local>
2026-08-02 22:36:45 -04:00
Tom Boucher
a987cf2731 chore(#2932): emit a per-invocation section manifest from the init bundle (#2987)
* chore(#2932): emit a per-invocation section manifest from init

Extends the init bundle with a typed per-invocation section manifest so an
invocation loads only the branch guidance it will actually take.

The three flag/state-gated branches in execute-phase.md move into their own
step files; the parent keeps its gsd:section markers wrapping a one-line
on-demand reference, so each section's prose lives in exactly one file and
the parent shrinks 93369 -> 89507 bytes. A new drift-guarded generator
derives the shipped section manifest from those markers, and a new pure
evaluator maps invocation facts to applicable section ids.

The evaluator is a lookup over the frozen WHEN_VOCABULARY, never a parser
(Greenspun's Tenth Rule, ADR-1671:69); a parity test asserts the vocabulary
and the predicate map stay exhaustively in sync.

Closes #2932

* fix(#2932): fail closed on prototype-chain when values

An isolated adversarial review found WHEN_PREDICATES[section.when] was a
bracket lookup on a plain-prototype object, so inherited Object.prototype
members resolved as predicates: "constructor"/"toString"/"valueOf"/
"hasOwnProperty" returned truthy and SILENTLY INCLUDED the section, and
"__proto__" threw an untyped TypeError carrying no .reason. Both violate
the module's documented fail-closed contract, and the manifest is read from
disk at run time so it cannot be assumed trustworthy.

Builds the predicate map on a null prototype and guards the lookup with an
explicit Object.hasOwn check. Adds table-driven coverage for nine
Object.prototype-shaped keys asserting the TYPED reason (asserting only
that it throws would still pass while broken) plus a fast-check property
injecting a hostile value at an arbitrary document position.

* test(#2932): retarget execute-phase step assertions at extracted step files

* fix(#2932): emit typed reasons for generator lib-load and write failures

* fix(#2932): restore launcher preamble in extracted steps and refresh derived fixtures

* chore(#2932): backfill changeset pr number to 2987

---------

Co-authored-by: sim <sim@local>
2026-08-02 12:34:41 -04:00