refactor: hard-fork GSD -> MSD (Make Software Done)

Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
Jakub Zych
2026-10-06 01:47:40 +02:00
parent fe069b2a56
commit a9a7a328e6
2763 changed files with 78465 additions and 78434 deletions

View File

@@ -0,0 +1,18 @@
{
"phase": "feat-3677-quick-batch-hardening-acceptance",
"issue": 3677,
"epic": 3344,
"epic_phase": 5,
"branch": "feat/3677-quick-batch-hardening-acceptance",
"base_revision": "114dfcb739",
"base_ref": "origin/next",
"note": "Design artifacts rebuilt after a prior research-pass worktree was garbage-collected (no commits ever landed, so gitignored .msd/ content was lost with it). This directory is force-added via a one-off checkpoint commit so a future resume can read it even though .msd/ is normally gitignored project-wide.",
"prior_summary_reverified": true,
"step1_conclusion": "crash-window gap CONFIRMED REAL — see 40-design.md Open Question 1 / Resolution",
"dependencies": {
"phase1": "feat/3673-dispatch-maxconcurrency-axis (merged, #4190 area)",
"phase2": "feat/3674-shared-wave-partitioner (merged)",
"phase3": "feat/3675-quick-batch-core-primitives (merged, PR #4190)",
"phase4": "feat/3676-quick-batch-command-workflow (merged, PR #4212)"
}
}

View File

@@ -0,0 +1,436 @@
# Phase 5 (#3677) design — quick-batch hardening and acceptance
Epic #3344, Phase 5 (final). Base: `origin/next` @ `114dfcb739` (Phases 1-4
merged: #4190 core primitives, #4212 command/workflow/isolation).
## 0. Provenance note
A prior research pass produced a design doc + test matrix in this same
directory, but its worktree was garbage-collected before those files were
committed (`.msd/` is gitignored repo-wide; "no commits" was treated as "no
changes" by worktree auto-cleanup). This document is a from-scratch rebuild,
grounded in an independent re-read of the actual workflow steps and source,
not a transcription of the lost files. Where it agrees with the prior
agent's SUMMARY, that is re-verified agreement, not inherited trust.
## 1. Open Question 1 — crash-window duplicate dispatch (RESOLVED: real gap, fix proposed)
**Claim under test:** a coordinator crash between Step 6 (executor returns,
`SUMMARY.md` written, real commit in a real worktree) and Step 7 (merge)
leaves the item's `BATCH.json` status at `pending`, and `--resume` would
dispatch a SECOND executor into a NEW worktree for the same item, orphaning
the first.
**Trace performed independently** (not reusing the prior agent's citations):
- `src/quick-batch.cts:894-899` — `resumeBatch`'s ONLY crash-window
detection completes an item early via `hasQuickTaskRow(stateContent,
it.quick_id)` — a check against `.planning/STATE.md`. The eligibility
filter at `src/quick-batch.cts:926-928` is otherwise pure
status/depends_on logic; it has no awareness of `PLAN.md`/`SUMMARY.md` on
disk.
- `msd-core/workflows/quick-batch/steps/completion.md:1-8` — confirms
`completeQuickItem` (Step 9) is "the ONLY writer of a 'Quick Tasks
Completed' STATE.md row" — i.e. the STATE.md row `hasQuickTaskRow` checks
for is written only AFTER Step 7 (merge) and, when `--validate`, Step 8
(verification) both succeed. A crash between Step 6 and Step 7 crashes
before any STATE.md row exists, so `resumeBatch`'s only recovery signal
never fires for this window.
- `msd-core/workflows/quick-batch/steps/resume-mode.md:23-27,45-49` — on
`--resume`, `eligible` items flow straight into the SAME per-round loop
Step 3/planner-wave/worktree-dispatch use for a fresh batch — "the DAG-
layer loop in `planner-wave.md` reads `$BATCH_MANIFEST_JSON`/`$BATCH_ID`
exactly the same way whether this batch was just created or just resumed."
- `msd-core/workflows/quick-batch/steps/planner-wave.md:15-19` — the
planning loop's eligibility for THIS layer is `status == "pending"` AND
`${item_dir}/${quick_id}-PLAN.md` does **not** yet exist on disk. An item
whose PLAN.md already exists (planned before the crash) is correctly
skipped here — planning is NOT re-run. This is the file-existence guard
the prior summary described as absent; it is present, but only at the
PLANNING layer.
- `msd-core/workflows/quick-batch/steps/worktree-dispatch.md:29-59` (Step 6,
"Dispatch rounds") — re-derives eligibility every round via `quick-batch
resume --batch "$BATCH_ID" --raw` (same primitive as above, same blind
spot), then spawns an executor for every item in `$spawn` (backpressure-
limited `eligible`). **There is no analogous check here for "does
`${item_dir}/${quick_id}-SUMMARY.md` already exist" before spawning.** The
only SUMMARY.md check in this file is AFTER dispatch, at
`worktree-dispatch.md:118-121` ("After every item dispatched this round
returns: verify SUMMARY.md exists") — too late to prevent the duplicate
dispatch itself, and it doesn't even apply here since the crashed
coordinator never issued this round's dispatch in the first place; a NEW
coordinator process re-deriving eligibility from scratch has no round
history to consult.
- `msd-core/workflows/quick-batch/steps/merge-wave.md:10-15` (Step 7) — its
OWN mergeable-wave criterion is independent of the `eligible`/`spawn` list
entirely: "items that are `status == 'pending'` with a `SUMMARY.md` on
disk... and NOT yet merged." This is correct and sufficient FOR MERGE — it
would find and merge the original (first) worktree's completed work fine,
IF nothing else raced it. The bug is that Step 6 runs before Step 7 in
every pass (including a resumed one) and would try to spawn a second
executor for the same item first.
**Conclusion: CONFIRMED REAL.** Nothing in the resume/dispatch path checks
for a pre-existing `SUMMARY.md` before dispatching an executor. An item that
crashed after Step 6 but before Step 9's STATE.md write is indistinguishable,
to `worktree-dispatch.md`'s dispatch loop, from an item that was never
started — it re-enters `$spawn` and gets a brand-new `git worktree add` +
`msd-executor` dispatch. The original worktree (with its real commit and
`SUMMARY.md`) is never referenced again by anything — not cleaned up, not
merged via its own record, simply orphaned. If both executors happen to
run to completion, Step 7's merge-eligibility check (`SUMMARY.md` exists on
disk, "not yet merged") only knows about ONE `$item_dir` per `quick_id`
(directories are keyed by `quick_id`+slug, not by worktree), so the SECOND
executor's `SUMMARY.md` write clobbers the first at the same path, and only
the SECOND worktree's `WT_PATH`/`WT_BRANCH` (recorded in
`$QUICK_BATCH_WORKTREE_MANIFEST` under the second dispatch's `agent_id`) is
ever merged — the FIRST worktree's real, already-committed work is never
merged, never cleaned up, and never surfaced as an error. This is a silent
work-loss bug, not merely a resource leak.
**Proposed fix (confidence: HIGH — narrow, mechanical, directly mirrors an
existing, already-reviewed pattern in the same file family):**
Add a SUMMARY.md-existence exclusion to `worktree-dispatch.md`'s Step 6,
substep 1 (the per-round eligibility re-derivation), symmetric to
`planner-wave.md`'s PLAN.md-existence exclusion: after parsing `eligible`
from `quick-batch resume`, drop any `quick_id` whose
`${item_dir}/${quick_id}-SUMMARY.md` already exists on disk from the set
passed to `spawn-plan` (never dispatch it) — those items fall through
untouched to Step 7, whose own mergeable-wave criterion already picks them
up correctly by the same file check. This is a workflow-layer (prose/bash)
fix, not a change to `resumeBatch` or any other already-merged, already-
reviewed `.cts` module — see rejected alternatives below for why.
**Fix location decision — workflow layer, not `resumeBatch` core primitive:**
`resumeBatch` (`src/quick-batch.cts:863-939`) is a shared primitive called
from two call sites with different needs (`resume-mode.md`'s one-shot resume
report AND `worktree-dispatch.md`'s per-round re-derivation). Teaching
`resumeBatch` about `${item_dir}/${quick_id}-SUMMARY.md` paths would require
it to either accept a slug-generation dependency it doesn't currently have,
or have its `eligible` return value silently diverge in meaning between the
two callers (a `resume-mode.md` "eligible" count that excludes
in-flight-but-uncommitted-to-merge items reads correctly for a resume
report; a "the item exists in $item_dir so don't re-plan/re-dispatch it"
check is a workflow-execution concern). Filtering in
`worktree-dispatch.md`, immediately before the existing `spawn-plan` call,
keeps the fix local to the one place that actually dispatches executors,
touches zero already-reviewed `.cts` files, and is directly analogous to
the file-existence check `planner-wave.md` already does one step earlier in
the same pipeline for the same reason.
## 2. Coverage table (#3677 acceptance criteria → status)
| # | AC bullet (verbatim, abbreviated) | Existing coverage | Gap | Action |
|---|---|---|---|---|
| 1 | Security: traversal, symlink escape, special files, prompt-injection, shell-metacharacter task text, manifest tampering, **arbitrary-worktree ownership attempts** | `tests/quick-batch.test.cjs` + `tests/quick-batch.property.test.cjs` cover traversal/symlink/special-file `--file` rejection (Phase 3/4 `60-review.json` fixed real findings); `msd-quick-batch-workflow.test.cjs` covers the DATA_START/DATA_END prompt-injection boundary and quoted `$ARGUMENTS`. **Arbitrary-worktree-ownership tampering is NOT covered** — no test constructs a cleanup-wave manifest entry naming a worktree path/branch this batch never created. | Real gap | New hostile test |
| 2 | Capacity precedence/backpressure | `quick-batch-dispatch.test.cjs` + `.property.test.cjs` cover `effective-concurrency`/`spawn-plan` exhaustively (Phase 3). | None found | none |
| 3 | Scheduling: cycles, unknown deps, deterministic waves, isolation modes, serialized lifecycle, deterministic merge, conflicts, **scope drift**, stale bases, **submodules** | `quick-batch.test.cjs`/`.property.test.cjs` cover cycle/unknown-dep rejection and wave determinism. `msd-quick-batch-merge-integration.test.cjs` covers a real merge conflict and a real undeclared deletion end-to-end. `worktree-safety.test.cjs:5716-6313` covers the `.gitmodules`/`SUBMODULE_PATHS` isolation-disable gate and the executor's own pre-commit submodule guard extensively — but always in the `execute-phase`/`quick.md` context, never through quick-batch's OWN merge/cleanup call path with a real `.gitmodules` file in the repo. `planWaveScopeConformance` (`src/worktree-safety.cts:921-...`) is unit-tested for its advisory `SCOPE_OUT_OF_DECLARED` warning, but not exercised end-to-end through a real git diff via `executeWorktreeWaveCleanupPlan`/`worktree.cleanup-wave` the way merge_failed/scope_violation already are. | Two real gaps | New real-git submodule integration test; new real-git advisory scope-drift test |
| 4 | Fault-injection: every durable manifest/STATE crash window, resume exactly-once | STATE-row crash window (Step 9) is covered. **The Step-6→Step-7 crash window (this doc §1) was UNCOVERED and is the one genuine functional gap in this phase.** | Real gap (now understood + fixed) | Fix + regression test |
| 5 | Outcome propagation (blocked/independent-continue) | Covered by `resumeBatch`'s blocked-propagation fixed-point tests and `quick-batch-dispatch.test.cjs`'s routing tests. | None found | none |
| 6 | Docs: v1 limits (`--discuss`/`--full`, no gap-fix loop, `none`=sequential) | `docs/how-to/batch-quick-tasks.md` states all of these already (lines 52-55, 74, 113). | None found | none |
| 7 | Generated artifact sync (command/skill/registry/inventory/matrix/install-tree) | Enforced by existing repo-wide generated-sync lint (not quick-batch-specific); Phase 4 already ran `regen:derived`. | None found (no new command surface added this phase) | none |
| 8 | `/msd:quick` regression + quick-ID grammar green | `msd-quick-batch-quick-regression.test.cjs` exists explicitly for this. | None found | none |
| 9 | Docs: preserved-worktree diagnosis | `docs/how-to/batch-quick-tasks.md:114` — exactly one sentence ("its worktree is preserved (never deleted) so you can inspect what happened"), no path, no diagnostic steps, no recovery procedure. | Real gap (thin, not absent) | Doc extension |
| 10 | Final acceptance evidence mapped to #3344 | Not yet produced this phase. | Real gap | New doc artifact |
| 11 | RED/GREEN/REFACTOR commit discipline, closes #3677 only | Process requirement, not a test | N/A | Followed in implementation |
## 3. Prior-art grounding for new tests
- Hostile worktree-ownership test: follows the REAL-git-fixture pattern
already established by `tests/msd-quick-batch-merge-integration.test.cjs`
(`initRepo`/`addWorktree`, real `executeWorktreeWaveCleanupPlan`,
`WORKTREE_AGENT_BRANCH_RE` branch-name validation already enforced at
`src/worktree-safety.cts:512`) — a manifest entry naming a path/branch
never created by this batch (e.g. a sibling repo's worktree, or a
plausible-looking but foreign branch) must be rejected/blocked, never
merged or deleted.
- Advisory scope-drift test: exercises `planWaveScopeConformance`
(`src/worktree-safety.cts:921-...`, `WAVE_CLEANUP_WARNING.SCOPE_OUT_OF_DECLARED`
at `:879-884`) end-to-end through a REAL git diff via
`executeWorktreeWaveCleanupPlan`, mirroring the merge-conflict/undeclared-
deletion pattern in `msd-quick-batch-merge-integration.test.cjs` — commit a
path outside `files_modified` and assert the merge still SUCCEEDS
(advisory, never blocking) while a warning with the frozen code is
produced.
- Submodule integration test: reuses `writeGitmodulesWithSubmodule`'s
fixture shape from `tests/worktree-safety.test.cjs:5879-5886` but drives
it through the quick-batch merge/cleanup call path
(`executeWorktreeWaveCleanupPlan`) instead of the isolation-decision gate
that file already covers — proves a repo containing `.gitmodules` merges
cleanly through quick-batch's own primitive when the plan doesn't touch
the submodule path, and that a plan touching the submodule path still
merges (worktree isolation's own submodule-intersection gate is a
separate, already-covered pre-dispatch decision, not a merge-time block).
- Crash-window regression test: structural assertion on
`worktree-dispatch.md`'s prose, following the SAME established convention
`tests/msd-quick-batch-workflow.test.cjs` already uses for every other
workflow-file behavior (e.g. its own "planner-wave.md marks a missing
PLAN.md item failed" test at line 253-256) — this repo's blessed pattern
for testing markdown-as-source (see that file's own header comment,
lines 16-21).
## 4. Not-corruption
None of the new tests duplicate existing coverage:
- The merge-conflict/undeclared-deletion real-git tests already in
`msd-quick-batch-merge-integration.test.cjs` are untouched; the new tests
add sibling `describe` blocks for DIFFERENT manifest-entry shapes
(foreign ownership, out-of-scope-but-declared-nothing-wrong path,
submodule-bearing repo) using the same helpers, not modifying existing
assertions.
- `worktree-safety.test.cjs`'s extensive `.gitmodules` coverage (isolation
ENABLE/DISABLE decision, executor pre-commit shell guard) is left
entirely alone — the new test targets a different function
(`executeWorktreeWaveCleanupPlan`, the merge primitive) that file never
exercises with a `.gitmodules` fixture.
- The crash-window fix touches only `worktree-dispatch.md`; it does not
modify `resumeBatch`, `planner-wave.md`, or `merge-wave.md`, all of which
keep their existing, already-reviewed behavior and tests unchanged.
## 5. Blast radius
- `msd-core/workflows/quick-batch/steps/worktree-dispatch.md` — additive
SUMMARY.md-existence filter, Step 6 substep 1 only.
- `tests/msd-quick-batch-workflow.test.cjs` — one new `describe` block
(structural regression test for the filter above).
- `tests/msd-quick-batch-merge-integration.test.cjs` — new `describe`
blocks: worktree-ownership tampering, advisory scope-drift, submodule
integration.
- `docs/how-to/batch-quick-tasks.md` — extend the "Resuming and failure
recovery" section with a preserved-worktree diagnosis subsection.
- New doc artifact: `.msd/phase/feat-3677-quick-batch-hardening-acceptance/60-acceptance-evidence.md`
mapping every #3344 AC bullet to its evidence.
- No `src/*.cts` production module changes required — the one functional
fix lands entirely in workflow prose.
## 6. Laws (invariants that must keep holding)
- Single-writer invariant (STATE.md/BATCH.json/ROADMAP.md owned only by the
coordinator) — untouched.
- `resumeBatch`'s existing crash-window detection (STATE.md row) and
blocked/failed propagation fixed-point — untouched, still the sole
authority for `complete`/`blocked` transitions.
- Merge scope validation (`partitionDeclaredDeletions`,
`planWaveScopeConformance`) stays advisory-for-scope /
blocking-for-undeclared-deletion exactly as already implemented — the new
scope-drift test asserts this distinction, it does not change it.
- `WORKTREE_AGENT_BRANCH_RE` remains the sole gate on what counts as a
batch-owned branch name — the ownership-tampering test asserts against
this existing regex, does not introduce a second one.
## 7. Rejected alternatives
1. **Fix the crash window inside `resumeBatch`** (teach it about
`SUMMARY.md`/item directories). Rejected — see §1 fix-location decision;
would touch already-merged, already-reviewed code for a fix whose
natural home is one step later in the pipeline, and would overload
`resumeBatch`'s single-purpose eligibility contract for two callers with
different needs.
2. **Mark the item `blocked`/`human_needed` when SUMMARY.md exists but
status is still `pending`.** Rejected — this is not a failure or an
ambiguous state; it's a known, recoverable mid-flight condition. Silently
skipping re-dispatch and letting Step 7's existing merge criterion pick
it up is strictly less invasive and requires no new status value or
routing branch.
3. **Add a lock file per in-flight worktree instead of a file-existence
check.** Rejected — `SUMMARY.md`'s existence already IS the durable,
crash-safe signal (it's written by the executor as its last real action,
same as everywhere else in this design uses artifact-existence over
process state); a separate lock file adds a second source of truth that
itself needs crash-recovery semantics.
4. **Skip the submodule/scope-drift/ownership tests as "already implied" by
unit-level coverage.** Rejected per this repo's own established
pattern (`msd-quick-batch-merge-integration.test.cjs`'s own header
comment): pure-function assertions on `routeMergeOutcome`/
`planWaveScopeConformance` were previously judged insufficient without a
REAL git fixture proving the underlying primitive agrees; the same
standard applies to the three new gaps here.
## 8. Known limits (v1, documented, not fixed here)
- A crash DURING Step 7 (merge in progress) still relies on
`executeWorktreeWaveCleanupPlan`'s own mid-merge halt behavior
(`merge-wave.md:56-59`) — unchanged by this phase, already covered by
Phase 3/4's own worktree-safety suite.
- No automatic gap-fix retry after a `gaps_found` verification outcome
(documented v1 exclusion, unchanged).
- The new SUMMARY.md-existence filter does not distinguish "crashed after
writing SUMMARY.md" from "executor is still mid-write" (a partial file);
this is the same trust boundary the rest of the pipeline already accepts
(`worktree-dispatch.md:118-121`'s own post-dispatch check treats
SUMMARY.md existence as the completion signal, no partial-write handling
anywhere in this design).
## 9. Review Pass 2 addendum — orthogonal Spec + Security findings, both closed
Two isolated review passes (neither authored the original diff) found two
real test-quality gaps in §1's implementation and the ownership-tampering
tests in §3. Both are closed here, WITHOUT deferral, per this repo's
absolute no-defer rule — including a THIRD, self-discovered defect surfaced
while fixing the first finding (see §9.3).
### 9.1 Spec finding — crash-window test was a prose proxy, not behavioral proof
The original `tests/msd-quick-batch-workflow.test.cjs` regression tests for
§1's fix only asserted `readStep('worktree-dispatch.md')` + regex matches
against the MARKDOWN — proving the documentation says the right thing,
never that the runtime condition (pending status + on-disk SUMMARY.md +
absent STATE row) is actually handled correctly. #3677's own "Alternatives
considered" explicitly rejects "document recovery without fault injection."
**Fix:** the filtering decision itself — "drop items whose SUMMARY.md
exists from the eligible-for-dispatch set" — is now a pure, independently
testable function, `filterAlreadyExecuted(eligibleIds, executedIds)` in
`src/quick-batch-dispatch.cts`, wired to a new `quick-batch filter-executed`
CLI verb (`src/quick-batch-command-router.cts`), following the SAME
pure-decision-then-CLI-wired pattern `computeSpawnPlan`/`computeMergeOrder`
already establish in that module. `worktree-dispatch.md` now calls this
verb explicitly instead of describing the decision only in prose.
A genuine fixture-based test in `tests/quick-batch.test.cjs` (new describe
block "crash-window duplicate-dispatch guard (#3677)") constructs a REAL
`BATCH.json` via `createBatch`, writes a REAL `SUMMARY.md` file on disk at
the item's real `item_dir` (via `generateSlugInternal`, the same slug
derivation every workflow step uses), calls the REAL `resumeBatch`, and
proves `resumeBatch` alone still reports the item eligible — then proves
`filterAlreadyExecuted`, fed a real filesystem check, correctly excludes
it. A second test proves an item with no real SUMMARY.md is NOT excluded
(boundary case — the guard must not over-fire). The prior prose-assertion
tests are KEPT (they still prove the workflow markdown is correctly WIRED
to call the new verb, in the right order) but are no longer the only proof.
Pure-function boundary tests (empty/all-executed/order-preserving/Set vs
array/phantom-id) live in `tests/quick-batch-dispatch.test.cjs`; CLI-wiring
tests live in `tests/quick-batch-command-router.test.cjs`.
### 9.2 Security finding — ownership-tampering tests didn't test ownership
The original two tests (branch-name-fails-shape-check; wholly-foreign
never-registered repo) proved real things, but neither exercised what
"arbitrary-worktree ownership" actually names: a manifest entry whose
`worktree_path`/`branch` are swapped to point at a DIFFERENT,
GENUINELY-REGISTERED sibling worktree of the SAME `repoRoot` (a concurrent
batch's own agent worktree, or a stale worktree from a prior crashed run),
with a branch name that passes `WORKTREE_AGENT_BRANCH_RE`'s shape check and
a base that is legitimately in `allowed_bases`.
**Investigation conclusion: ALREADY SAFE — not a reachable gap.** Traced
`executeWorktreeWaveCleanupPlan` (`src/worktree-safety.cts:1005-1217`)
directly against two REAL, concurrently-alive sibling worktrees of one
repo. Git itself enforces branch-per-worktree uniqueness — the same branch
cannot be checked out in two worktrees of one repo at once — so
`worktree_path`'s ACTUAL checked-out branch
(`git -C worktree_path rev-parse --abbrev-ref HEAD`, `:1047`) can only
equal a swapped-in `entry.branch` if that `entry.branch` is the SIBLING's
own real, uniquely-generated branch name. Manifest tampering confined to
ONE batch's own record has no way to know that name: branch names are
`agent-<quick_id>[-<timestamp>]`-shaped (`execute-phase`'s own
`executor-isolation-dispatch.md:269-270` convention, reused verbatim per
`worktree-dispatch.md`'s own text), and `quick_id` allocation is
collision-checked GLOBALLY across every existing quick task AND batch
(`src/quick-batch.cts` `collectExistingBatchQuickIds`) — not merely
within one batch. Constructing a passing swap therefore requires ALSO
having legitimate read access to the sibling's own worktree/branch record,
which is a strictly larger compromise than "tamper with this batch's own
manifest," the scope the AC bullet actually names.
Empirically verified (not merely reasoned about) with two real
`git worktree add`-created siblings sharing one merge-base: a manifest
entry naming item 2's real path but item 1's real branch name (the direct
swap), and the reverse, both come back `status: 'blocked',
reason: 'branch_mismatch'` — never `merge_failed`, never a wrongly
successful merge. Both real worktrees, their branches, and item 2's real
uncommitted-to-main commit survive completely untouched by either attempt.
**Fix:** a new, stronger test in `tests/msd-quick-batch-merge-integration.test.cjs`
("a manifest entry with one sibling worktree's real PATH but the OTHER
sibling's real BRANCH name is blocked") supplements (does not replace) the
original two tests, which still prove real, distinct boundaries
(branch-shape rejection at the manifest-normalization layer; a wholly
foreign, never-registered repo blocked via `base_mismatch`).
**Explicitly documented trust boundary (not a gap, not fixed):**
`executeWorktreeWaveCleanupPlan` defends against fabricated/mismatched
`{worktree_path, branch, base}` triples; it does NOT defend against a
CALLER bug that correctly copies a real-but-wrong-item's triple into the
wrong manifest entry (i.e. gets `agent_id` attribution wrong while every
git-verifiable field is internally consistent for SOME real worktree). This
is why §9.3's durable `dispatched_worktree`/`dispatched_branch`/
`dispatched_base` fields are stored PER quick_id (a direct keyed lookup),
never via a shared array requiring an `agent_id`-matching search — the
storage shape itself avoids the one class of caller-side attribution bug
this primitive cannot see.
### 9.3 Self-discovered defect (not deferred) — durable worktree recovery was missing entirely
While building §9.1's real fixture, tracing `merge-wave.md` substep 3
("`$WT_PATH`/`$WT_BRANCH`/`$EXPECTED_BASE` per item come from the recorded
`$QUICK_BATCH_WORKTREE_MANIFEST` entry Step 6 wrote for that `agent_id`")
against `/msd:quick`'s own prior art (`msd-core/workflows/quick.md:415`:
`QUICK_WORKTREE_MANIFEST=$(mktemp ...)`) revealed that
`$QUICK_BATCH_WORKTREE_MANIFEST` — quick-batch explicitly models it on the
SAME mechanism — is a fresh, PER-PROCESS `mktemp` file, not a durable
record. §1's own fix (never re-dispatch an item whose SUMMARY.md already
exists) means a RESUMED coordinator process's Step 6 correctly does NOT
create a fresh manifest entry for that item — but nothing else durably
recorded that item's `worktree_path`/`branch`/`expected_base` either, so
Step 7 in the resumed process would have had NO data to build that item's
cleanup-wave entry from. §1's original claim ("not lost: merge-wave.md's
own criterion already picks it up") was therefore ACCURATE about
merge-eligibility ("should this item be merged") but WRONG about data
availability ("with what worktree/branch"). Before §1's fix, this never
surfaced as a visible bug because the old (harmful) re-dispatch behavior
always populated a fresh manifest entry for whichever (wrong, duplicate)
worktree it just created — the durable-recovery gap was latent, masked by
the duplicate-dispatch bug itself.
**Fix:** three new fields on `QuickBatchItem`
(`dispatched_worktree`/`dispatched_branch`/`dispatched_base`,
`src/quick-batch.cts`) — deliberately NOT a reuse of the pre-existing
`worktree` field, whose `loadBatch` validation requires the path to exist
on disk (verified empirically: reusing it made the batch permanently
unloadable the moment a legitimately-merged worktree was removed, since
`updateBatchItems` itself calls `loadBatch` first). The three new fields
carry no existence check — their entire purpose is to stay readable (and
clearable) after a legitimate post-merge removal. `updateBatchItems`
(`QuickBatchItemUpdate`) gained matching optional `dispatchedWorktree`/
`dispatchedBranch`/`dispatchedBase` fields (explicit `null` clears,
`undefined` leaves untouched — same convention as `dependsOn`/
`plannedFiles`). `worktree-dispatch.md` persists the triple immediately
after recording the ephemeral entry; `merge-wave.md` falls back to it when
the ephemeral manifest lacks an entry, clears it after a successful merge,
and fails closed (`merge_failed: "missing durable worktree record"`) rather
than guessing if somehow all three are still null.
Verified end-to-end (persist → fresh `loadBatch` recovers the triple →
clear → a SUBSEQUENT `loadBatch` still succeeds even though the path no
longer exists) in `tests/quick-batch.test.cjs`'s new "durable
worktree-recovery fields (#3677)" describe block, plus structural wiring
tests in `tests/msd-quick-batch-workflow.test.cjs` for both workflow files.
### 9.4 Revised blast radius (supersedes §5 for the fields below)
- `src/quick-batch.cts` — `QuickBatchItem`/`QuickBatchItemInput`/
`QuickBatchItemUpdate` gain `dispatched_worktree`/`dispatched_branch`/
`dispatched_base` (+ camelCase input/update equivalents);
`validateBatchSchema` gains matching type checks (no existence check).
- `src/quick-batch-dispatch.cts` — new `filterAlreadyExecuted` pure
function + `FilterAlreadyExecutedResult` type.
- `src/quick-batch-command-router.cts` — new `filter-executed` CLI verb.
- `msd-core/workflows/quick-batch/steps/worktree-dispatch.md` — the guard
now calls `quick-batch filter-executed` instead of describing the split
only in prose; a new durable-persistence step after recording the
ephemeral manifest entry.
- `msd-core/workflows/quick-batch/steps/merge-wave.md` — durable fallback
for `$WT_PATH`/`$WT_BRANCH`/`$EXPECTED_BASE`; clears the triple on a
successful merge; fails closed when no record exists anywhere.
- Test files: `tests/quick-batch.test.cjs`, `tests/quick-batch-dispatch.test.cjs`,
`tests/quick-batch-command-router.test.cjs`,
`tests/msd-quick-batch-workflow.test.cjs`,
`tests/msd-quick-batch-merge-integration.test.cjs` — all gain new,
independently-verified (real fixture / real git, not merely prose-proxy)
coverage per §9.1-9.3.
- §5's "No `src/*.cts` production module changes required" is SUPERSEDED —
three `.cts` modules changed, all additive (new fields/functions/verbs,
zero changes to existing field shapes, existing function signatures, or
existing CLI verb behavior).

View File

@@ -0,0 +1,41 @@
# Phase 5 (#3677) test matrix
Companion to `40-design.md`. Only NEW tests this phase adds — pre-existing
coverage is cited in `40-design.md` §2/§3, not re-listed here.
| Row | Test file | Test name (abbreviated) | Proves | #3677 AC bullet | #3344 AC bullet |
|---|---|---|---|---|---|
| 1 | `tests/msd-quick-batch-merge-integration.test.cjs` | "a manifest entry naming a worktree/branch this batch never created is blocked, never merged or deleted" | `executeWorktreeWaveCleanupPlan` rejects a cleanup-wave entry whose `branch` fails `WORKTREE_AGENT_BRANCH_RE`, or whose `worktree_path` was never actually created by `git worktree add` for this repo — real fixture, real git, asserts the foreign path/branch survives untouched | Security bullet: "arbitrary-worktree ownership attempts" | "the command never sweeps or deletes an unowned worktree" |
| 2 | `tests/msd-quick-batch-merge-integration.test.cjs` | "a committed path outside declared files_modified merges successfully with an advisory SCOPE_OUT_OF_DECLARED warning, never blocked" | `planWaveScopeConformance`'s advisory (non-blocking) behavior holds end-to-end through a REAL git diff via `executeWorktreeWaveCleanupPlan` — distinguishes advisory scope drift from the blocking undeclared-DELETION case already covered | Scheduling bullet: "scope drift" | "reports scope drift without silently merging it" |
| 3 | `tests/msd-quick-batch-merge-integration.test.cjs` | "a repo with `.gitmodules` and an unrelated plan merges cleanly through the quick-batch cleanup primitive" + "...and a plan touching the submodule path also merges (isolation-disable is a separate, already-covered pre-dispatch decision)" | `executeWorktreeWaveCleanupPlan` handles a real `.gitmodules`-bearing repo without special-casing gitlink entries (mode 160000) incorrectly as an undeclared deletion or scope violation | Scheduling bullet: "submodules" | "submodule-touch... paths produce recoverable diagnostics" |
| 4 | `tests/msd-quick-batch-workflow.test.cjs` | "worktree-dispatch.md excludes an item whose SUMMARY.md already exists from this round's spawn set (crash-window duplicate-dispatch guard)" | Structural regression test: the Step-6→Step-7 crash window (design §1) cannot silently regress — asserts the new SUMMARY.md-existence filter text exists in the prose, mirroring the file's own established pattern for asserting `planner-wave.md`'s PLAN.md-existence filter | Fault-injection bullet: "every durable manifest/STATE crash window" | "Base divergence, merge conflict, stale worktree, submodule-touch, and interrupted cleanup paths produce recoverable diagnostics" |
| 5 | `docs/how-to/batch-quick-tasks.md` (doc, not test) | "Diagnosing a preserved worktree" subsection | Extends the one-sentence mention into: where the worktree lives, what to check (`git log`, `git status`, the item's `SUMMARY.md`), how to manually merge/discard, how to re-run `--resume` afterward | Docs bullet: "preserved-worktree diagnosis" | "Base divergence... produce recoverable diagnostics" |
| 6 | `.msd/phase/feat-3677-quick-batch-hardening-acceptance/60-acceptance-evidence.md` (doc, not test) | Full AC-to-evidence mapping | Every #3344 AC bullet cited against the specific test/doc/commit that satisfies it | "Final verification maps evidence to every acceptance criterion in #3344" | (self) |
## Boundary coverage note
Row 1 (ownership tampering) exercises the boundary at the branch-name
regex: a NEAR-miss branch name (fails `WORKTREE_AGENT_BRANCH_RE` by one
character / wrong prefix) is the primary hostile case — the "arbitrary"
part of "arbitrary-worktree ownership attempts" means attacker-controlled
manifest content, not merely a wrong-but-well-formed path, so the test
constructs BOTH a plausible-but-foreign branch name (passes the regex, but
was never created by `git worktree add` in this fixture) and a
regex-rejected name, asserting each is handled (rejected, not silently
adopted).
Row 2 (scope drift) exercises limit-1/limit/limit+1 analogously to the
existing undeclared-deletion test's two-case (declared vs undeclared)
shape: `files_modified` declares path A only; the real commit touches A
(no warning), A+B (one warning, B), and touches ONLY B (declared list
non-empty but wrong — still advisory, still merges, still warns).
## Property-based testing note
No new `fast-check` property test is added this phase — `40-design.md` §2
found no bijective/parser/budget-limit contract among the new gaps (an
existing-fixture merge/cleanup primitive, a doc extension, and a structural
workflow-prose assertion). `quick-batch.property.test.cjs` and
`quick-batch-dispatch.property.test.cjs` already carry this epic's `fast-check`
coverage for the parsing/budget/wave-partition contracts these new tests
build on top of; none of that is touched or needs new properties.

View File

@@ -0,0 +1,92 @@
# Final acceptance evidence — epic #3344 (`/msd:quick-batch`)
Maps every acceptance-criterion bullet in #3344 to its evidence. Phases 1-4
(#4190, #4212, and their prerequisite dispatch/wave-partitioner PRs) are
cited by test file — those PRs' own `60-review.json` verdicts are the
authoritative record for anything not re-verified in this phase. Phase 5
(#3677, this PR) entries are marked **NEW** and cite the file/test added
here. #3344 itself stays OPEN after this PR merges, per its own instruction
("Epic #3344 closes only after final maintainer acceptance") — this table
is the evidence submitted for that acceptance, not a closure action.
## Command and artifacts
| AC bullet | Evidence |
|---|---|
| Accepts ≥2 inline tasks or a validated repo-relative task file | `tests/quick-batch.test.cjs` (`parseTaskList`/`parseTaskListFromFile`), `tests/msd-quick-batch-workflow.test.cjs` |
| `--jobs auto\|N`, `--validate`, `--research`, `--resume <batch-id>` documented | `commands/msd/quick-batch.md` frontmatter tests; `docs/how-to/batch-quick-tasks.md` "Flags" |
| V1 rejects `--discuss`/`--full` before dispatch | `tests/msd-quick-batch-workflow.test.cjs` "objective documents --discuss/--full as rejected" |
| No automatic broad-prompt decomposition | `docs/how-to/batch-quick-tasks.md` "Basic use" (explicit list only) |
| Collision-safe quick ID/directory even for duplicates | `tests/quick-batch.test.cjs` (`allocateQuickIds`/`allocateIdsGivenUsed`), `quick-batch.property.test.cjs` |
| Normal quick PLAN + `status: complete` SUMMARY under `.planning/quick/`, audit-scanner-recognized | `tests/msd-quick-batch-quick-regression.test.cjs` |
| Batch control state under `.planning/quick-batches/<batch-id>/`, not a fake quick dir | `tests/quick-batch.test.cjs` (`createBatch`/`batchManifestPath`) |
| Coordinator dispatches leaves directly; no child invokes `/msd:quick`; no nested/background delegation required | `tests/msd-quick-batch-workflow.test.cjs` "single-writer invariant on the executor" (`NEVER invoke /msd:quick`) |
## Capacity and scheduling
| AC bullet | Evidence |
|---|---|
| Normalized worker-capacity signal; `dispatch-capacity` single consumer | Phase 1/2 (#3673/#3674) host-integration + `quick-batch-dispatch.test.cjs` |
| Effective concurrency bounded by task count / `--jobs` / capacity | `tests/quick-batch-dispatch.test.cjs`, `.property.test.cjs` (`effective-concurrency`) |
| Missing/invalid/zero/negative/`undocumented` capacity fails closed to 1 | `tests/quick-batch-dispatch.test.cjs` |
| No runtime-name literal branches in scheduler | `src/quick-batch.cts`/`src/quick-batch-dispatch.cts` — no `RUNTIME ===`/`runtime name` conditionals (grep-verified during Phase 3/4 review) |
| Backpressure preserves pending state, never overspawns | `tests/quick-batch-dispatch.test.cjs` (`spawn-plan`) |
| Declared dependencies honored; overlapping file sets never co-wave | `tests/quick-batch.test.cjs` (`computeWaves`), `.property.test.cjs` |
| Independent non-overlapping tasks run concurrently when capacity allows | `tests/quick-batch.test.cjs` wave tests |
| Deterministic wave/merge ordering for identical input | `tests/quick-batch.test.cjs` + `tests/msd-quick-batch-merge-integration.test.cjs` (wave-order-preserving merge) |
## Isolation and Git safety
| AC bullet | Evidence |
|---|---|
| `harness-worktree`/`orchestrator-worktree`/`none` each behaviorally tested, fail-closed degradation | `tests/msd-quick-batch-workflow.test.cjs` "isolation model coverage"; `msd-core/workflows/quick-batch/steps/worktree-dispatch.md` (row 38 stale-base auto-degrade) |
| Mutating wave forced to 1 worker when isolation is `none` | `tests/quick-batch-dispatch.test.cjs` (`--mutating` forcing) |
| Worktree create/merge/cleanup serialized and manifest-scoped; never sweeps/deletes an unowned worktree | `tests/msd-quick-batch-merge-integration.test.cjs` — **NEW/REVISED (review pass 2)**: "arbitrary-worktree ownership tampering" describe block, 3 tests: (1) tampered branch name silently dropped at normalization; (2) foreign, never-registered repo blocked via base_mismatch; (3) the STRONGER proof — two REAL, concurrently-alive sibling worktrees of the SAME repo, path/branch swapped between them, blocked via branch_mismatch in both directions, both survive untouched. Investigation concluded the swap is not a reachable gap (git's own branch-per-worktree uniqueness + globally collision-checked quick_id-derived branch names) — see `40-design.md` §9.2. |
| Later waves start from the current merged batch base | `msd-core/workflows/quick-batch/steps/merge-wave.md` Step 7 design; `tests/quick-batch.test.cjs` wave recompute tests |
| Merge applies committed-diff-vs-declared-scope validation; reports scope drift without silently merging it | `tests/msd-quick-batch-merge-integration.test.cjs` (undeclared-DELETION blocking, Phase 4) **+ NEW**: "advisory scope drift merges but warns" describe block (drift produces `scope_out_of_declared` warning + still merges; exact-match boundary case produces zero warnings) |
| Child workers never concurrently switch the main checkout; ≤1 aggregate branch | `msd-core/workflows/quick-batch/steps/worktree-dispatch.md` "ONE AT A TIME" serialization test |
| Each merged item retains atomic implementation commit history | `tests/msd-quick-batch-merge-integration.test.cjs` (`--no-ff` real-git merges preserve commit history) |
| Base divergence, merge conflict, stale worktree, submodule-touch, interrupted cleanup produce recoverable diagnostics | Base divergence: `resumeBatch`'s `currentBaseRevision` check (`tests/quick-batch.test.cjs`). Merge conflict: `tests/msd-quick-batch-merge-integration.test.cjs` (Phase 4). Stale worktree / interrupted cleanup: `worktree-safety.test.cjs`'s existing mid-merge halt coverage (unchanged this phase) + `docs/how-to/batch-quick-tasks.md` **NEW** "Diagnosing a preserved worktree" subsection. Submodule-touch: `tests/msd-quick-batch-merge-integration.test.cjs` **NEW** ".gitmodules submodule integration" describe block (3 tests: unrelated plan merges cleanly with `.gitmodules` present; a real gitlink pointer bump merges and the superproject tree reflects the new pinned commit; an undeclared bump is advisory-only and surfaces a `vendor/sub` scope warning) |
## State, failure, and resume
| AC bullet | Evidence |
|---|---|
| Leaves cannot write shared STATE.md/ROADMAP.md; coordinator is sole writer | `tests/msd-quick-batch-workflow.test.cjs` single-writer invariant test |
| Completed rows appended atomically, exactly once, including after resume | `src/quick-batch.cts` `completeQuickItem`/`hasQuickTaskRow`; `tests/quick-batch.test.cjs` idempotency tests |
| Failure of one item doesn't roll back others | `tests/quick-batch-dispatch.test.cjs` routing isolation tests; `worktree-safety.cts`'s `#2852` per-entry isolation (`blockEntry`/`continue`) |
| Failed item recorded failed; dependents recorded blocked; unrelated pending continue | `tests/quick-batch.test.cjs` `resumeBatch` blocked-propagation fixed-point tests |
| `--resume` skips complete, retries only eligible non-complete, idempotent | `tests/quick-batch.test.cjs` `resumeBatch` tests **+ NEW fix**: `msd-core/workflows/quick-batch/steps/worktree-dispatch.md` Step 6 substep 1 crash-window guard (a `pending` item whose `SUMMARY.md` already exists on disk — executor finished, coordinator crashed before Step 7's merge — is no longer re-dispatched into a second worktree on `--resume`; it falls through to Step 7's existing SUMMARY.md-on-disk merge criterion instead). Regression-tested structurally in `tests/msd-quick-batch-workflow.test.cjs` "crash-window duplicate-dispatch guard" describe block (3 tests: guard text present, guard positioned before `spawn-plan` is computed, guard documents the merge-wave.md recovery path). **This was the one genuine functional gap found in this phase** — see `.msd/phase/feat-3677-quick-batch-hardening-acceptance/40-design.md` §1 for the full trace. |
| Invalid/corrupt batch manifest fails closed, no guessing | `tests/quick-batch.test.cjs` `loadBatch` malformed-JSON tests |
## Security, portability, and project quality
| AC bullet | Evidence |
|---|---|
| `--file` rejects traversal/symlink-escape/special files/outside-root; task text untrusted in prompts | `tests/quick-batch.test.cjs`/`.property.test.cjs` (Phase 3/4 review-fixed findings); `tests/msd-quick-batch-workflow.test.cjs` DATA_START/DATA_END boundary tests |
| Paths with spaces, duplicate slugs, Windows separators, BSD/GNU differences, non-ASCII descriptions | `tests/quick-batch.test.cjs`/`.property.test.cjs` (slug generation, cross-platform path tests) |
| Workflow stays within size budget via shared fragments | `tests/msd-quick-batch-workflow.test.cjs` byte-size boundary describe block (NEW_FILE_CAP); this phase's `worktree-dispatch.md` addition re-verified at 9.8KB, well under cap |
| Capacity axis backward-compatible, fails closed when absent | Phase 1/2 host-integration descriptor tests |
| #2652/PR #2728 resolved; no `RUNTIME != "claude"` gate copied | `msd-core/references/dispatch-isolation-gate.md` reuse (Phase 4 design) |
| Canonical command Markdown / generated skill byte-for-byte synced | Repo-wide generated-sync lint (unchanged this phase; no new command surface) |
| Utility-cluster/profile/surface/inventory/installer/generated-artifact/doc parity | Repo-wide parity tests (unchanged this phase) |
| TDD/failing-test-first; Markdown behavior tested where applicable | Every new test in this phase written RED-first per its own commit; `tests/msd-quick-batch-workflow.test.cjs`'s established structural-assertion convention for workflow prose |
| `npm test`, `npm run lint:ci`, generated-sync, required platform lanes pass | Verified via `msd-test` before this PR ships (not run as part of this dispatch — reported separately) |
## #3677's own acceptance criteria (this phase issue)
| AC bullet | Evidence |
|---|---|
| Security: traversal/symlink/special-file/prompt-injection/shell-metachar/manifest-tampering/**arbitrary-worktree-ownership** | Manifest tampering + arbitrary-worktree ownership: **NEW**, this phase (see table above). Rest: Phase 3/4, unchanged. |
| Capacity: precedence/modes/backpressure | Phase 3, unchanged (`quick-batch-dispatch.test.cjs`) |
| Scheduling/execution: cycles/unknown-deps/waves/capacity/isolation/lifecycle/merge-order/conflicts/**scope-drift**/stale-bases/**submodules** | Scope drift + submodules: **NEW**, this phase. Rest: Phase 3/4, unchanged. |
| Fault-injection: every durable manifest/STATE crash window, resume exactly-once | **NEW fix + test**, this phase (crash-window guard, see above) — the STATE-row crash window was already covered in Phase 3. Review pass 2 upgraded the regression test from a prose-only proxy to a real fixture (`tests/quick-batch.test.cjs`, real `BATCH.json`+`SUMMARY.md`+`resumeBatch`), extracted the decision into a pure `filterAlreadyExecuted` function + `quick-batch filter-executed` CLI verb, and closed a self-discovered follow-on gap (durable worktree-path/branch recovery for a resumed coordinator process — `dispatched_worktree`/`dispatched_branch`/`dispatched_base`). See `40-design.md` §9.1/§9.3. |
| Outcome propagation; blocked dependents; independent continuation | Phase 3, unchanged |
| Docs: v1 limits | Phase 4, unchanged (`docs/how-to/batch-quick-tasks.md`) |
| Generated artifact sync | Repo-wide, unchanged (no new command surface this phase) |
| `/msd:quick` regression + quick-ID grammar green | `tests/msd-quick-batch-quick-regression.test.cjs`, unchanged |
| Docs: **preserved-worktree diagnosis** | **NEW**, this phase — `docs/how-to/batch-quick-tasks.md` "Diagnosing a preserved worktree" subsection |
| Focused tests / `npm test` / `npm run lint:ci` / generated-sync / install / platform lanes green | Verified via `msd-test` before this PR ships |
| Real-PR-number changeset; no hand-edited CHANGELOG | Added after PR creation, per repo convention |
| Final verification maps evidence to #3344 | This document |
| RED/GREEN/REFACTOR commits; closes #3677 only | This PR's commit history; PR body closes #3677, references #3344 without closing it |