@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/out/mcp-server.js +1624 -190
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
  4. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
  5. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
  6. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +238 -0
  8. package/resources/claude-customizations/.claude/agents/parallel-planner.md +149 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
  10. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
  15. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
  16. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
  17. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
  18. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
  19. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
  20. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
  21. package/resources/claude-customizations/.claude/settings.json +25 -0
  22. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +148 -0
  23. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
  24. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +960 -0
  25. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +420 -0
  26. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
  27. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
  28. package/resources/claude-customizations/pack-manifests/core.json +19 -1
  29. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  30. package/resources/config/orchestration-routing.json +22 -0
  31. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
@@ -0,0 +1,960 @@
1
+ ---
2
+ name: parallel-orchestrate
3
+ description: Execute a prepared parallel run for the parallel-orchestrator agent — cohort scheduling under a max_concurrency cap, per-item fan-out onto isolated worktrees branched from origin/main, per-item merge to main after durably confirming CI green, worktree cleanup, and the generated parallel-status.md projection. There is no integration branch and no final integration pull request.
4
+ argument-hint: "[parallel-manifest-path | parallel-slug]"
5
+ context: fork
6
+ agent: parallel-orchestrator
7
+ ---
8
+
9
+ # Parallel Orchestrate Skill
10
+
11
+ A user invocation (`/parallel-orchestrate <parallel-manifest-path>`) forks the
12
+ `parallel-orchestrator` agent with this procedure in context. The parallel manifest path (or
13
+ parallel slug) for this run is:
14
+
15
+ $ARGUMENTS
16
+
17
+ This skill frames work for the `parallel-orchestrator` agent, parallel to how
18
+ `.claude/skills/orchestrate/SKILL.md` frames work for `orchestrator`. It documents manifest
19
+ consumption, cohort consumption and ordering, the cohort barrier and `max_concurrency` slot
20
+ filling, the per-item branch and worktree lifecycle, the child kickoff parameter, model selection,
21
+ per-item merge to `main`, per-item merge-conflict handling, worktree cleanup,
22
+ `parallel-status.md` maintenance, checkpoint persistence, and completion, so that the procedure is
23
+ not re-derived ad hoc on each parallel run.
24
+
25
+ Each item on this surface ships independently: an item's pull request targets `main` directly.
26
+ There is no integration branch, no final integration pull request, and no fan-in path anywhere in
27
+ this procedure. Scheduling order comes only from computed blast-radius contention recorded in the
28
+ cohort table; there is no `depends_on` field on this surface.
29
+
30
+ Every section below is self-contained. A cross-reference names another section by its exact
31
+ heading text, never by position or number, so a section can be added or extended without
32
+ reflowing, reordering, or renumbering anything else in this file. The sections
33
+ `## Mutation Protocol (F6)`, `## Enforcement Hooks (F7)`, and `## Radius Drift Detection (F8)`
34
+ are reserved for the wave-4 features named in them: each is appended to by its own feature and
35
+ must not be relocated.
36
+
37
+ ## Prerequisites
38
+
39
+ Before proceeding, `parallel-orchestrator` must:
40
+
41
+ 1. Read `CLAUDE.md` for repository tone policy and architectural context.
42
+ 2. Read the applicable `.claude/rules/` files for the languages in scope, including
43
+ `.claude/rules/parallel-orchestration.md`, which is the schema authority for this surface.
44
+ 3. Read the policy files listed in the compliance reading order section of `CLAUDE.md`.
45
+
46
+ ## Parallel Manifest Consumption
47
+
48
+ The run manifest is the YAML frontmatter of `docs/features/parallel/<slug>/parallel.md`, authored
49
+ by `parallel-planner` on the planner-owned branch `parallel/<slug>-plan`. That branch is not an
50
+ integration branch: no item branch merges into it, it merges into no item branch, and it holds
51
+ only run-level artifacts under `docs/features/parallel/<slug>/`.
52
+
53
+ **This section is not a schema authority.** The manifest schema, the checkpoint schema, and all
54
+ nine parallel enums are defined once as prose invariants in
55
+ `.claude/rules/parallel-orchestration.md` (manifest invariants M1 through M7) and are enforced by
56
+ the F3-owned validators `scripts/dev_tools/parallel_manifest_contract.py` and
57
+ `scripts/dev_tools/validate_parallel_orchestrator_state.py`. This is a deliberate delta from
58
+ `.claude/skills/epic-orchestrate/SKILL.md`, whose manifest section carries its schema inline. Read
59
+ the schema from the rule file and the validators; consume it here and never redefine or extend it.
60
+
61
+ Consumption rules:
62
+
63
+ - `issue_num` is the primary key for every item reference. It is the stable GitHub issue number,
64
+ so it does not drift when an item's folder moves from `docs/features/active/` to
65
+ `docs/features/completed/`.
66
+ - Read `mode` (`closed` or `open`, defaulting to `closed`), `max_concurrency` (an integer from 1
67
+ through 8, defaulting to 4), and each item's identity and state: `feature_folder`, `kind`,
68
+ `state`, and `blast_radius`.
69
+ - The manifest is read-only to `parallel-orchestrator`. It is static input authored by
70
+ `parallel-planner`: never write it, rewrite it, or back-fill a field into it.
71
+ - The manifest carries no `depends_on` key at any level and no top-level `integration_branch` key.
72
+ Presence of either is an explicit rejection, not a tolerated extra field.
73
+ - A malformed manifest is rejected before any kickoff, recorded as a synthetic Blocking finding in
74
+ the checkpoint. Do not guess a repair, do not silently skip the offending item, and do not launch
75
+ a partial cohort. Validate by calling `validate_parallel_manifest_text` from
76
+ `scripts/dev_tools/parallel_manifest_contract.py`, which is a library call and deliberately not
77
+ an MCP artifact type. That module exposes no CLI entry point, so the permitted mechanism for the
78
+ call is the granted interpreter invocation
79
+ `poetry run python -c "import pathlib, sys; from scripts.dev_tools.parallel_manifest_contract import validate_parallel_manifest_text; errors = validate_parallel_manifest_text(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')); print(errors); sys.exit(1 if errors else 0)" docs/features/parallel/<slug>/parallel.md`,
80
+ whose non-zero exit is the rejection signal and whose printed error list is the content of the
81
+ Blocking finding.
82
+
83
+ ## Cohort Consumption and Ordering
84
+
85
+ `cohorts[]` is an input, not a computation. Each entry has the shape
86
+ `{ index, generation, item_keys[] }` and is seeded by `parallel-planner` at `generation: 0` from
87
+ the deterministic Welsh-Powell coloring in `scripts/dev_tools/parallel_cohort_computation.py`.
88
+
89
+ `parallel-orchestrator` consumes the recorded partition and never computes or recolors one:
90
+
91
+ - Cohort computation belongs to the cohort-scheduler library that `parallel-planner` calls.
92
+ - Recoloring after a membership change belongs to F6; recoloring after a drift event belongs to F8.
93
+ - Read `recolor_generation` and schedule only from the cohorts whose `generation` equals it.
94
+
95
+ Items within a cohort are non-conflicting by construction — a cohort is an independent set in the
96
+ conflict graph — so they may branch from the same `main` tip and may merge in any order. After one
97
+ same-cohort item merges, another same-cohort item's pull request, based on the older tip, remains
98
+ mergeable, and GitHub produces a merge commit.
99
+
100
+ The conflict graph the cohorts project was computed by a contention relation that fails closed.
101
+ Since the blast-radius corrections of issue #452 (merged as PR #453), that relation reports more
102
+ contention rather than less: separator-free repository-root shared surfaces are reached from plan
103
+ and specification text, and path comparison honours listed-directory prefixes on both sides. Build
104
+ on that corrected behaviour. Never narrow a radius, re-derive a partition, or reinterpret an edge
105
+ in order to combine two cohorts or widen a launch batch.
106
+
107
+ ## Cohort Barrier and Max-Concurrency Slot Filling
108
+
109
+ Two independent controls govern every launch. The cohort barrier governs when a cohort may start.
110
+ `max_concurrency` governs how many items of a started cohort run at once. Neither substitutes for
111
+ the other.
112
+
113
+ **Cohort barrier.** Cohort `N+1` branches from `main` only after every cohort-`N` item is `merged`
114
+ or `worktree_removed`. Increment `current_cohort` only on durable confirmation from
115
+ `git worktree list --porcelain`, `git branch`, and
116
+ `gh pr view --json state,mergedAt,headRefOid` — never from an in-memory completion notification. A
117
+ blocked item (`blocked_ci_loop_limit` or `blocked_drift`) is neither `merged` nor
118
+ `worktree_removed`, so a blocked item holds the barrier and cohort `N+1` does not start.
119
+
120
+ **`max_concurrency` slot filling.** `max_concurrency` caps the number of simultaneously in-flight
121
+ items independently of cohort size: a cohort of twelve items executes at most `max_concurrency`
122
+ items at a time. Fill slots in ascending item-key order, keyed on `issue_num`, and refill each
123
+ freed slot with the next unstarted item of the current cohort in that same ascending item-key
124
+ order. A cohort larger than `max_concurrency` therefore launches in several batches from the same
125
+ recorded `main` tip. The batching is a pure function:
126
+ `compute_concurrency_batches(cohort_item_keys, max_concurrency)` in
127
+ `scripts/dev_tools/parallel_cohort_computation.py` returns the batches in order and sorts the keys
128
+ itself, so determinism does not depend on caller ordering.
129
+
130
+ **Mechanical enforcement of the barrier is F7 scope, not this feature's.** F7 delivers a two-layer
131
+ design, because no single `PreToolUse` hook can validate a batch of concurrent `Agent` calls: hooks
132
+ fire per call with no cross-call state visibility.
133
+
134
+ - Layer 1, per-call deterrent: `.claude/hooks/enforce-parallel-cohort-barrier.ps1`, a `PreToolUse`
135
+ hook on the `Agent` matcher. It fires when `subagent_type == "orchestrator"` and the serialized
136
+ prompt carries the parallel kickoff marker, resolves the target item, reads
137
+ `artifacts/orchestration/parallel-orchestrator-state.json`, and denies with reason
138
+ `PARALLEL_COHORT_BARRIER_BLOCKED` unless every conflicting item in a prior cohort is `merged` or
139
+ `worktree_removed`.
140
+ - Layer 2, retrospective backstop: a cohort-ordering invariant inside
141
+ `validate_parallel_orchestrator_state_text`, enforced at `parallel-orchestrator` `SubagentStop`
142
+ time, appending `PARALLEL_COHORT_BARRIER_VIOLATION` when the ordering invariant is violated.
143
+
144
+ Neither layer is shipped by this feature; both are named here so the obligation is legible to an
145
+ operator and to the F7 planner. Until F7 lands, the barrier is enforced by this procedure alone.
146
+
147
+ ## Per-Item Branch and Worktree Lifecycle
148
+
149
+ 1. Run one `git fetch origin main` immediately before each cohort launch, so every item in that
150
+ cohort branches from the same current remote `main` tip rather than from a stale local ref.
151
+ Record the fetched tip.
152
+ 2. Each item's worktree is created by that item's delegation spawn,
153
+ `Agent(orchestrator, isolation: "worktree", run_in_background: true)`, branched from
154
+ `origin/main`. Do not create or check out item worktrees by hand.
155
+ 3. `parallel-planner` created and pushed each item's feature branch from `origin/main` at
156
+ preparation time, and that branch is reused unchanged as the item's execution branch, so the
157
+ spawn checks out an existing branch whose base is `origin/main`. Because that base may be older
158
+ than the fetched tip, the item's own run reconciles its branch against the fetched `origin/main`
159
+ tip at execution start. Real path overlap that survives that reconciliation is drift; the
160
+ conflict outcome is handled per `## Per-Item Merge-Conflict Handling`, and drift recording
161
+ itself belongs to F8.
162
+ 4. Each item's pull request base branch is `main`. Record `worktree_path`, `branch_name`,
163
+ `pr_number`, and `pr_url` for the item, and set `merge_status: worktree_created` at spawn.
164
+ 5. Worktree removal is the terminal step of this lifecycle and is specified in
165
+ `## Worktree Cleanup`.
166
+
167
+ No integration branch is created, fetched, pushed, or referenced at any point in this lifecycle.
168
+ There is no final integration pull request and no fan-in path: the epic surface's
169
+ integration-branch lifecycle has no counterpart on this surface, and its absence is structural
170
+ rather than an omission.
171
+
172
+ ## Parallel-Mode Kickoff Parameter
173
+
174
+ When `parallel-orchestrator` delegates an item to `Agent(orchestrator)`, the delegation prompt
175
+ carries exactly these five elements.
176
+
177
+ 1. The literal marker line:
178
+
179
+ > `Parallel mode: true. parallel_slug: <slug>. parallel_checkpoint_path: artifacts/orchestration/parallel-orchestrator-state.json. cohort_index: <n>. PR base branch MUST be main; pass --base main to gh pr create.`
180
+
181
+ The token `Parallel mode: true` must appear exactly: it is the marker F7's Layer 1 barrier hook
182
+ matches on. The clause `PR base branch MUST be main` is the child's explicit base-branch
183
+ instruction, recorded as prompt text rather than left to a base-branch ancestry heuristic.
184
+ 2. The item's active feature folder path, written literally as `docs/features/active/<basename>`.
185
+ The child needs it for its own operation, and F7's Layer 1 hook resolves the target item by
186
+ scanning the prompt for exactly that path shape, so the path is emitted as a bare path token.
187
+ 3. The canonical issue number line, which is the item key.
188
+ 4. The item's committed `plan-path`, together with the resume instruction: resume at atomic
189
+ execution from that plan rather than re-running promotion, research, or planning. The item's
190
+ prepared feature folder and approved atomic plan are already committed and preflight-clear on the
191
+ item's own pushed feature branch.
192
+ 5. The model-budget marker line `model_budget.fable_policy: <disabled|available|preferred>.`
193
+
194
+ Spawn parameters, passed on the `Agent` call and never written into the prompt text:
195
+ `isolation: "worktree"`, `run_in_background: true`, branch base `origin/main`, and `model` equal to
196
+ that item's model routing receipt's resolved model.
197
+
198
+ Negative obligations on the prompt:
199
+
200
+ - It never carries `Preparation mode: true`. Preparation fan-out belongs to `parallel-planner` and
201
+ to F6's admission path; an item that reaches this procedure is already prepared.
202
+ - It never carries the epic-mode marker line that `.claude/skills/epic-orchestrate/SKILL.md`
203
+ emits — the marker whose text is `Epic mode` followed by the value `true` — so the epic
204
+ wave-barrier hook does not fire on a parallel child. That marker's value is deliberately not
205
+ written out anywhere in this file, so no file this feature delivers carries an epic-mode marker
206
+ string.
207
+ - It contains no instruction for the child to merge its own pull request. The parent performs each
208
+ item's merge, as specified in `## Per-Item Merge to Main (Merge-on-Green)`.
209
+
210
+ Excluded from the prompt as parent-side concerns: the item's declared blast radius,
211
+ `max_concurrency`, and `mode`. Keeping the prompt minimal preserves the child contract unchanged.
212
+
213
+ ## Model Selection
214
+
215
+ When `parallel-orchestrator` delegates an item to `Agent(orchestrator)`, the prompt appends the
216
+ session model-budget kickoff marker line, following the existing kickoff-marker pattern:
217
+
218
+ > `model_budget.fable_policy: <disabled|available|preferred>.`
219
+
220
+ The item's own `orchestrator` reads that line and applies the two-axis model-selection mechanism
221
+ documented in `.claude/skills/orchestrate/SKILL.md` (`## Model Selection`): it assesses a
222
+ judgment-based `complexity_band`, records `complexity_assessments[]` and
223
+ `model_routing_receipts[]`, and resolves each of its own delegations' model tier under the given
224
+ `fable_policy`. The canonical, tested reference implementations are `Get-ComplexityFloor` and
225
+ `Resolve-DelegationModel` in `.claude/lib/model-routing/ModelRouting.psm1`. Default `fable_policy`
226
+ is `disabled` when the marker is absent.
227
+
228
+ `parallel-orchestrator` spawns exactly one delegation channel, `Agent(orchestrator)`, one
229
+ delegation per item. It applies the same per-delegation resolution to that channel and passes
230
+ `model` equal to the routing receipt's `model` on the spawn call. It MUST NOT omit `model` — an
231
+ omitted `model` falls back to the delegate's frontmatter default, `opus`, which suppresses a
232
+ `fable` resolution — and MUST NOT hard-code `model=opus` in a way that overrides the resolved
233
+ routing model.
234
+
235
+ `route` is never an input to model selection. `route` remains file-count driven and governs only
236
+ agents, skills, and MCP tools. A skill whose frontmatter `context` field holds the value `fork`
237
+ inherits the parent model and ignores a model override, so model selection applies to agent
238
+ delegations, not to fork-routed skill invocations.
239
+
240
+ ## Per-Item Merge to Main (Merge-on-Green)
241
+
242
+ The parent — `parallel-orchestrator` — performs each item's merge to `main`. The child contract is
243
+ unchanged: `.claude/skills/orchestrate/SKILL.md` is **not modified by this feature**. There is no
244
+ `parallel_mode` clause in its step 9, no `parallel_merge` object in the child checkpoint, and no
245
+ additional condition on the child's PR Creation Gate.
246
+
247
+ Procedure, per item:
248
+
249
+ 1. The item's child orchestration runs unmodified, with `epic_mode` `false` or absent, and finishes
250
+ at its own DONE. The child's PR Creation Gate condition 6 already requires
251
+ `ci_gate.conclusion == "success"`, so at child DONE the item's pull request is open against
252
+ `main` and its checks are green. The child is never instructed to merge its own pull request.
253
+ 2. On child completion, durably confirm pull-request state and check conclusion with
254
+ `gh pr view --json state,mergedAt,headRefOid`, and with `gh pr checks` when the check conclusion
255
+ must be re-read — never from an in-memory completion notification — then record
256
+ `merge_status: ci_green`.
257
+ 3. Execute `gh pr merge --merge <PR>` for that item's pull request, whose base is `main`.
258
+ 4. On success, record `merge_commit_sha`, `merged_at`, and `merge_status: merged`, then regenerate
259
+ `docs/features/parallel/<slug>/parallel-status.md`.
260
+ 5. On a merge failure caused by a conflict, follow `## Per-Item Merge-Conflict Handling`.
261
+
262
+ **F7 dependency.** `.claude/hooks/enforce-epic-merge-gate.ps1` is a project-wide `PreToolUse`
263
+ Bash-matcher hook that denies any `gh pr merge --merge` unless an epic-shaped checkpoint satisfies
264
+ its allow conditions; its block reason is `EPIC_MERGE_GATE_BLOCKED`. A parallel run has no
265
+ epic-shaped checkpoint, so step 3 above is denied until F7 scopes or extends that gate's allow
266
+ conditions for the parallel case. This feature modifies no file under `.claude/hooks/` and does not
267
+ change `.claude/settings.json`, so the parallel surface is not executable end-to-end before F7
268
+ lands. That limitation is documented, not worked around.
269
+
270
+ Branch protection on `main` affects only the pacing of step 3, not its ownership: if `main`
271
+ requires branches to be up to date, an automated `gh pr update-branch` plus re-green cycle is
272
+ inserted between same-cohort merges, which serializes those merges in practice while remaining
273
+ unattended.
274
+
275
+ ## Per-Item Merge-Conflict Handling
276
+
277
+ Remediation is child-owned and parent-initiated. The conflict is always between one item's own
278
+ branch and `origin/main`; there is no integration branch and therefore no fan-in conflict path on
279
+ this surface.
280
+
281
+ 1. On a conflicted `gh pr merge --merge`, the parent detects the failure and re-delegates that item's
282
+ child orchestration, passing the conflict signal and the instruction to resolve against
283
+ `origin/main`. The conflict capture and the finding write both belong to the child's
284
+ `atomic-executor`, which works inside the item's own worktree, the only working tree holding the
285
+ item's branch. It runs `git fetch origin main`, then `git merge --no-commit origin/main`, and on
286
+ non-zero exit captures `git diff --name-only --diff-filter=U` for the conflicted-file list
287
+ together with the raw conflict-marker content of each conflicted file. The child's
288
+ `atomic-executor` then writes that evidence as a synthetic Blocking finding to the item's own
289
+ `remediation-inputs.<timestamp>.md` in the item's active feature folder under
290
+ `docs/features/active/`, not to the run's parallel folder. Assigning both the capture and the
291
+ finding write to the child's chain matches `.claude/skills/epic-orchestrate/SKILL.md`, whose
292
+ equivalent capture and finding write also belong to the child's `atomic-executor`.
293
+ 2. The parent re-delegates that item's child orchestration. The child processes the finding through
294
+ its unmodified R1 through R5 remediation loop exactly as it processes any local Blocking
295
+ finding. No new remediation loop is introduced by this procedure.
296
+ 3. The child's `remediation_pass` counter is shared with its local-finding and CI-failure passes,
297
+ with the cap of 3, unmodified.
298
+ 4. Each remediated pass ends again at child DONE with the pull request open and CI green, after
299
+ which the parent retries the merge per `## Per-Item Merge to Main (Merge-on-Green)`. During
300
+ remediation the item's `merge_status` legitimately remains `pr_open` or `ci_green`: the
301
+ `merge_status` enum is F3-owned, carries no conflict member, and is not extended by this
302
+ feature.
303
+ 5. On loop exhaustion, the parent records the terminal `merge_status: blocked_ci_loop_limit` for the
304
+ item; the child's own checkpoint retains its precise blocked status. A blocked item is neither
305
+ `merged` nor `worktree_removed`, so it holds the cohort barrier defined in
306
+ `## Cohort Barrier and Max-Concurrency Slot Filling`.
307
+
308
+ Boundary with F8: a merge conflict between two same-cohort items is evidence that the declared
309
+ blast radius under-reported, and this feature records the child's blocked or remediated outcome
310
+ only, leaving drift recording in `drift_events[]`, quiesce of admission, conflict recomputation
311
+ against the observed radius, and requeue of the later-started item to F8.
312
+
313
+ ## Worktree Cleanup
314
+
315
+ After an item reaches `merge_status: merged` and that state is durably confirmed by
316
+ `gh pr view --json state,mergedAt,headRefOid`, the parent — running from the main repository
317
+ checkout, never from inside a child worktree — issues `git worktree remove <worktree_path>`. On
318
+ success it records `merge_status: worktree_removed` and `worktree_removed_at`, then regenerates
319
+ `docs/features/parallel/<slug>/parallel-status.md`.
320
+
321
+ Mechanical gating of this command for parallel worktrees is F7 scope.
322
+ `.claude/hooks/enforce-epic-worktree-removal-gate.ps1` is a project-wide `PreToolUse` Bash-matcher
323
+ hook that denies any `git worktree remove` unless the epic checkpoint carries a matching
324
+ `features[]` record whose `merge_status` is `merged` or `worktree_removed`; an unreadable checkpoint
325
+ or an absent record also denies. Its block reason is `EPIC_WORKTREE_REMOVAL_BLOCKED`. A parallel run
326
+ has no epic checkpoint record for its worktrees, so removal is denied until F7 both delivers
327
+ `enforce-parallel-worktree-removal-gate.ps1` and coordinates the epic gate's allow conditions:
328
+ `PreToolUse` denials are conjunctive, so a new allow-hook alone cannot override the existing deny.
329
+ This feature ships no hook file and makes no `.claude/settings.json` change.
330
+
331
+ ## Documentation Maintenance Boundaries
332
+
333
+ `docs/features/parallel/<slug>/parallel-status.md` is a generated projection of
334
+ `artifacts/orchestration/parallel-orchestrator-state.json`. It is regenerated in full, is never
335
+ hand-authored, and is never treated as an input. It is never the source of the cohort table and
336
+ never the source of the schedule: the run manifest and the checkpoint are authoritative. Generate it
337
+ from the template `docs/features/templates/parallel/parallel-status.md`.
338
+
339
+ Header block fields: `parallel_slug`, `mode`, `max_concurrency`, `current_cohort`,
340
+ `recolor_generation`, `last_updated`.
341
+
342
+ Item table: one row per `items[]` entry, carrying `issue_num`, `feature_folder`, cohort index,
343
+ lifecycle `state`, `merge_status`, `pr_url`, `merge_commit_sha`, and the item's lifecycle
344
+ timestamps. The cohort column takes the place of the epic status document's wave column.
345
+
346
+ Cohort table: a projection of `cohorts[] { index, generation, item_keys[] }`, so a recolored
347
+ schedule stays traceable by `generation`.
348
+
349
+ Read-only projections of F3-owned arrays, rendered by this feature but never written by it: section
350
+ `## Conflict Edges` projects `conflict_edges[]`; section `## Mutations` projects `mutations[]`,
351
+ whose rows appear only once F6 populates that array; section `## Drift Events` projects
352
+ `drift_events[]`, which only F8 populates. An empty array renders an empty section rather than an
353
+ omitted one.
354
+
355
+ Regeneration boundaries — regenerate at each of the following, not only at final completion:
356
+
357
+ - Run kickoff, seeding the initial projection from the manifest and the seeded cohorts.
358
+ - Every item `state` or `merge_status` transition.
359
+ - Every cohort transition, meaning every `current_cohort` increment.
360
+ - Every `recolor_generation` increment.
361
+ - Every append to `mutations[]`.
362
+ - Every append to `drift_events[]`.
363
+ - Run completion in `closed` mode, or run close in `open` mode.
364
+
365
+ Defining the `mutations[]` and `drift_events[]` appends as regeneration boundaries here means F6 and
366
+ F8 need no amendment to these projection rules.
367
+
368
+ ## Parallel-Level Checkpoint
369
+
370
+ This section is consumption documentation only. The checkpoint schema is owned by F3, defined once
371
+ as prose invariants in `.claude/rules/parallel-orchestration.md`, and enforced by
372
+ `scripts/dev_tools/validate_parallel_orchestrator_state.py`. Consume that schema; add no field to it
373
+ and extend no enum in it.
374
+
375
+ Fields `parallel-orchestrator` writes to `artifacts/orchestration/parallel-orchestrator-state.json`:
376
+ `objective`, `route_id: "parallel"`, `parallel_slug`, `parallel_manifest_path`,
377
+ `parallel_status_doc_path`, `mode`, `max_concurrency`, `completed_steps`, `next_step`,
378
+ `last_updated`, `current_cohort`, `recolor_generation`, `cohorts[]`, `items[]` — each item entry
379
+ carrying `issue_num`, `feature_folder`, `state`, `blast_radius`, `worktree_path`, `branch_name`,
380
+ `pr_number`, `pr_url`, `merge_status`, `merge_commit_sha`, and the lifecycle timestamps — and the
381
+ three receipt arrays `delegation_receipts[]`, `skill_receipts[]`, and `mcp_call_receipts[]`,
382
+ populated with the `parallel` route's required names from `config/orchestration-routing.json`.
383
+
384
+ The `merge_status` enum has exactly eight members: `not_started`, `worktree_created`, `pr_open`,
385
+ `ci_green`, `merged`, `worktree_removed`, `blocked_drift`, and `blocked_ci_loop_limit`. An absent
386
+ `merge_status` is treated as `not_started`.
387
+
388
+ The transition chain this feature writes: `not_started` to `worktree_created` at the item's
389
+ delegation spawn; `worktree_created` to `pr_open` when the child reports its pull request open;
390
+ `pr_open` to `ci_green` on durable confirmation after child DONE; `ci_green` to `merged` when the
391
+ parent's merge succeeds; and `merged` to `worktree_removed` after gated removal.
392
+ `blocked_ci_loop_limit` is the mapped terminal for an exhausted remediation loop.
393
+
394
+ Never written by this feature: `blocked_drift`, which only F8 writes; `conflict_edges[]`, seeded by
395
+ `parallel-planner` and recomputed only by F8; `mutations[]`, which only F6 appends to; and
396
+ `drift_events[]`, which only F8 appends to. These are read for projection and for scheduling context
397
+ and are otherwise untouched.
398
+
399
+ The checkpoint is a cache of durable state, not the source of truth. Every recorded field is
400
+ re-derivable on resume from `git worktree list --porcelain` for worktree existence and path,
401
+ `git branch` for branch existence and name, and `gh pr view --json state,mergedAt,headRefOid` for
402
+ pull-request state, merge time, and merge commit. Where the checkpoint disagrees with those three
403
+ commands, the commands win and the checkpoint is rewritten from them.
404
+
405
+ Validate through `mcp__drm-copilot__validate_orchestration_artifacts` with
406
+ `artifact_type: "parallel-orchestrator-state"`, or the equivalent CLI invocation
407
+ `poetry run python -m scripts.dev_tools.validate_orchestration_artifacts parallel-orchestrator-state <path>`,
408
+ adding `--require-complete` at the completion gate.
409
+
410
+ ## Completion Requirements
411
+
412
+ Completion is mode-dependent. Read `mode` from the manifest: it is `closed` or `open` and defaults
413
+ to `closed`.
414
+
415
+ In `closed` mode, `parallel-orchestrator` must not report completion until all of the following
416
+ hold:
417
+
418
+ 1. Every non-withdrawn item has `merge_status` of `merged` or `worktree_removed`, each durably
419
+ confirmed by `git` and `gh` rather than by an in-memory completion notification.
420
+ 2. `docs/features/parallel/<slug>/parallel-status.md` has been regenerated a final time and reflects
421
+ the completed state.
422
+ 3. The parallel checkpoint passes validation with `require_complete` for
423
+ `artifact_type: "parallel-orchestrator-state"`.
424
+ 4. Each item's acceptance criteria have been checked off in that item's own acceptance-criteria
425
+ source files by that item's own run, per the `acceptance-criteria-tracking` skill.
426
+
427
+ In `open` mode there is no automatic completion. The run is a standing queue and terminates only via
428
+ `/parallel-close`, which is owned by F6 and is neither specified nor shipped by this feature. Do not
429
+ synthesize a completion condition for an `open`-mode run.
430
+
431
+ No completion condition involves a run-level pull request. There is no final integration pull
432
+ request on this surface, so completion keys on the per-item terminal states above and on nothing
433
+ else.
434
+
435
+ ## Mutation Protocol (F6)
436
+
437
+ A parallel run is mutable while it executes. Three slash commands mutate it, and each one appends
438
+ exactly one `mutations[]` entry to the parallel-orchestrator checkpoint on success:
439
+
440
+ - `/parallel-add <issue|potential-entry>` — `.claude/skills/parallel-add/SKILL.md`. Admits one new
441
+ item: the item enters `proposed`, is prepared through a preparation-mode child
442
+ `Agent(orchestrator)` run reusing the `route_id: preparation` contract unchanged, its conflict
443
+ edges are computed against ALL items including in-flight ones, and the admission decision places
444
+ it in the current cohort only when it conflicts with no member of that cohort, pinned or
445
+ unstarted, and otherwise defers it and recolors the unstarted subgraph.
446
+ - `/parallel-remove <item> [--disposition detach|abandon]` —
447
+ `.claude/skills/parallel-remove/SKILL.md`. Removes one item per the state-dependent behavior
448
+ table: an unstarted item is withdrawn and the unstarted subgraph is recolored; an in-flight item
449
+ requires an explicit disposition and is rejected without one; a merged item is rejected.
450
+ - `/parallel-close <parallel-slug>` — `.claude/skills/parallel-close/SKILL.md`. Terminates an
451
+ `open`-mode run. Rejected while any item is `in_flight`.
452
+
453
+ Every mutation re-derives durable state (`git worktree list --porcelain`, `git branch`,
454
+ `gh pr view`) before it is applied, because the checkpoint is a cache of durable state and not the
455
+ source of truth. A rejected mutation appends no entry and changes no state.
456
+
457
+ The decision logic for all three commands is the pure engine
458
+ `scripts/dev_tools/parallel_mutation_protocol.py`. It decides; it never applies. Item keys are
459
+ integers (`items[].issue_num`) everywhere on this surface.
460
+
461
+ ### Pinning invariant
462
+
463
+ **In-flight items are pinned. Scheduling is recomputed only over the not-yet-started subgraph, and
464
+ recoloring is a pure function of `(remaining subgraph, pinned set, pinned cohort index)`.**
465
+
466
+ The recolor function takes the induced subgraph of unstarted items (states `proposed`, `admitted`,
467
+ `prepared`, `scheduled`), the pinned set (state `in_flight`), the current generation, and — as a
468
+ third scheduling input — the current cohort index `current_cohort` that the pinned items occupy. It
469
+ returns cohort assignments for unstarted items ONLY: the returned mapping's key set equals the
470
+ unstarted set exactly and contains no pinned key. A pinned item is therefore absent from the result
471
+ rather than reassigned, and that absence IS the guarantee that a mutation never moves work already
472
+ running.
473
+
474
+ **Pinned-barrier offset.** The returned indices are ABSOLUTE checkpoint cohort indices at or above
475
+ `current_cohort`, and strictly above `current_cohort` whenever any conflict edge joins an unstarted
476
+ item to a pinned item. When no such edge exists the lowest returned index equals `current_cohort`
477
+ exactly, so unstarted items may share the running cohort and `max_concurrency` slot filling is
478
+ preserved. The offset is a single uniform shift applied to every color class, so F2's distinct color
479
+ classes remain distinct cohort indices and independence within the unstarted set is preserved
480
+ exactly.
481
+
482
+ Write the returned indices VERBATIM into `cohorts[].index`; never re-base them to zero. `cohorts[]`
483
+ carries exactly ONE current-generation entry per index, so returned keys landing on index
484
+ `current_cohort` JOIN the pinned members of that one entry instead of forming a second entry with
485
+ the same index, which F3 invariant 13 rejects.
486
+
487
+ Coloring is delegated in full to the Welsh-Powell entry point `compute_cohorts` in
488
+ `scripts/dev_tools/parallel_cohort_computation.py`. No part of the coloring, the vertex ordering, or
489
+ the tie-break is reimplemented by the mutation engine, and the offset is applied entirely inside the
490
+ mutation engine's own recolor function.
491
+
492
+ **Two design corrections (spec 1.2).** Admission previously checked only the `in_flight` subset, but
493
+ `max_concurrency` caps simultaneously in-flight items independently of cohort size and refills each
494
+ freed slot from the same current cohort — see
495
+ `## Cohort Barrier and Max-Concurrency Slot Filling` — so the current cohort durably holds
496
+ not-yet-launched `scheduled` members that a candidate can contend with. Recoloring previously
497
+ dropped the candidate-to-pinned edges together with the pinned vertices, which discarded the pinned
498
+ CONSTRAINT as well as the pinned VERTICES and returned a deferred candidate to cohort 0, the current
499
+ cohort, whenever the cohort barrier held `current_cohort` at 0.
500
+
501
+ ### Recompute boundary
502
+
503
+ `recolor_generation` increments by exactly one on a recompute and is stamped unchanged on every
504
+ other operation. The boundary is normative:
505
+
506
+ Operations that RECOMPUTE (`recolor_generation` increments by exactly one):
507
+
508
+ 1. **Deferred add** — the candidate conflicts with a member of the current cohort — pinned or
509
+ not-yet-launched — so the unstarted subgraph, including the new item, is recolored.
510
+ 2. **Remove of an unstarted item** — the vertex is dropped and the remaining unstarted subgraph is
511
+ recolored.
512
+ 3. **Drift-induced requeue** — the later-started item of a newly conflicting pair is halted and
513
+ requeued into a future cohort.
514
+
515
+ Operations that DO NOT recompute (generation stamped unchanged):
516
+
517
+ 1. **Admission into the current cohort with no conflict against any current-cohort member** — no
518
+ cohort assignment changes.
519
+ 2. **`detach`** — the detached item was pinned and was never a vertex of the unstarted subgraph, so
520
+ its departure cannot change the induced subgraph.
521
+ 3. **`abandon`** — same rationale as `detach`. An unstarted item previously deferred because of a
522
+ conflict with the now-abandoned item keeps its deferred cohort assignment: the assignment remains
523
+ valid and is at most conservative, and no opportunistic recompute is performed. This keeps
524
+ generation accounting minimal and deterministic.
525
+ 4. **`close`** — run termination changes no cohort assignment.
526
+
527
+ A non-recompute operation still appends exactly one `mutations[]` entry, stamping the current,
528
+ unchanged `recolor_generation`. A sequence of N operations from generation `g` therefore ends at
529
+ exactly `g` plus the number of recompute operations.
530
+
531
+ ### Per-op mutation-log entry contents
532
+
533
+ | Op case | `op` | `item_key` | `prior_state` | `new_state` | `disposition` | `recolor_generation` |
534
+ | --- | --- | --- | --- | --- | --- | --- |
535
+ | Add, no-conflict admit | `add` | item key | null | `scheduled` | null | `g` (unchanged) |
536
+ | Add, deferred | `add` | item key | null | `scheduled` | null | `g` + 1 |
537
+ | Remove, unstarted | `remove` | item key | prior state (`proposed`/`admitted`/`prepared`/`scheduled`) | `withdrawn` | null | `g` + 1 |
538
+ | Remove, `detach` | `remove` | item key | `in_flight` | `withdrawn` | `detach` | `g` (unchanged) |
539
+ | Remove, `abandon` | `remove` | item key | `in_flight` | `withdrawn` | `abandon` | `g` (unchanged) |
540
+ | Close | `close` | null (run-scoped) | null | null | null | `g` (unchanged) |
541
+ | Drift-induced requeue | `requeue` | item key | `in_flight` | `blocked` | null | `g` + 1 |
542
+
543
+ `prior_state` is null on BOTH add rows. The accompanying `prepared` -> `scheduled` transition is not
544
+ lost and is not recorded in the mutation entry: it is recorded as an item-state update in `items[]`
545
+ with the checkpoint's lifecycle timestamps, the same mechanism that records
546
+ `proposed` -> `admitted` -> `prepared` during preparation. `new_state` is null for `close` only, and
547
+ `item_key` is null for `close` only. `disposition` is non-null only on a `remove` entry whose
548
+ `prior_state` is `in_flight`. Every `at` timestamp comes from the engine's injected clock seam; the
549
+ engine never reads the wall clock. No field and no enum member is added to `mutations[]`; the nine
550
+ parallel enums of `.claude/rules/parallel-orchestration.md` are consumed, never extended.
551
+
552
+ Retrospective validation of the log is the helper
553
+ `scripts/dev_tools/_parallel_orchestrator_state_mutations.py`, wired into
554
+ `scripts/dev_tools/validate_parallel_orchestrator_state.py`. It requires every entry to carry all
555
+ seven fields and no eighth, and requires `recolor_generation` to be monotonically non-decreasing in
556
+ append order, which is what makes a lost update detectable after the fact.
557
+
558
+ ### Mode-dependent completion semantics
559
+
560
+ - **`closed` mode (the default).** The completion gate fires when every non-withdrawn item is
561
+ `merged` or `worktree_removed`, evaluated over per-item `merge_status`. Mid-execution mutation
562
+ remains permitted. The predicate is `is_closed_mode_complete` in the mutation engine.
563
+ - **`open` mode.** The run NEVER auto-completes. It is a standing queue and terminates only via
564
+ `/parallel-close`. The close record is the run's final mutation; nothing may be appended to
565
+ `mutations[]` after it. Do not synthesize a completion condition for an `open`-mode run.
566
+
567
+ A withdrawn item is exempt from the predicate in both modes: it left the run before reaching a merge
568
+ outcome, so requiring a terminal merge status of it would make every run that dropped an item
569
+ permanently incompletable. This is also why `detach` records `withdrawn` — the run does not wait for
570
+ a detached item.
571
+
572
+ ### Abandon confirmation-marker contract
573
+
574
+ The `abandon` disposition is destructive: it closes the item's pull request and removes its
575
+ worktree. Both side effects run through ONE deterministic CLI invocation of
576
+ `scripts/dev_tools/parallel_mutation_abandon_cli.py`, documented in full in
577
+ `.claude/skills/parallel-remove/SKILL.md`. Executing the abandon disposition through ad hoc `gh` or
578
+ `git` commands is prohibited, because an ad hoc command is not matchable and would bypass the
579
+ confirmation contract.
580
+
581
+ The contract is enforced by the PreToolUse hook
582
+ `.claude/hooks/enforce-parallel-abandon-gate.ps1` on the `Bash` matcher:
583
+
584
+ - A Bash command carrying the abandon disposition token MUST also carry the explicit confirmation
585
+ marker `--confirm-abandon` in the SAME command.
586
+ - Without the marker, the command is DENIED with a deny reason prefixed
587
+ `PARALLEL_ABANDON_BLOCKED`.
588
+ - With the marker, the command is allowed.
589
+ - A command carrying no abandon disposition token is out of scope and is allowed unchanged.
590
+
591
+ When the gate denies a command, add the confirmation marker deliberately. Do not reformulate the
592
+ command to evade the match: that defeats the only mechanism protecting a destructive operation.
593
+
594
+ ### Drift-requeue append contract
595
+
596
+ Radius drift detection consumes this protocol rather than reimplementing it. When drift halts the
597
+ later-started item of a newly conflicting pair, the requeue is recorded through the mutation
598
+ engine's `build_requeue_entry` constructor and the recolor through `recolor_unstarted`:
599
+
600
+ - Item state becomes `blocked` and per-item `merge_status` becomes `blocked_drift`.
601
+ - Exactly one `mutations[]` entry is appended with `op: requeue`, `prior_state: in_flight`,
602
+ `new_state: blocked`, `disposition: null`, and `recolor_generation` equal to `g` + 1 — the requeue
603
+ is a recompute.
604
+ - The recolor runs over the unstarted subgraph only, so no other in-flight item moves. Its call
605
+ shape is the five-argument form
606
+ `recolor_unstarted(unstarted_items, conflict_edges, pinned, current_generation, current_cohort=current_cohort)`,
607
+ where `current_cohort` is required and keyword-only.
608
+
609
+ The drift event itself is recorded in `drift_events[]`, which this protocol does not write. See
610
+ `## Radius Drift Detection (F8)`.
611
+
612
+ ## Enforcement Hooks (F7)
613
+
614
+ Three hooks enforce this surface mechanically: two new, one an additive extension of an existing epic
615
+ hook. All three fail closed — once a call is in scope, every unresolvable condition (missing or
616
+ malformed checkpoint, unresolvable target item, missing `items[]` record, missing `merge_status`)
617
+ denies. Out-of-scope calls allow without reading the checkpoint.
618
+
619
+ **Layer 1, per-call deterrent.** `.claude/hooks/enforce-parallel-cohort-barrier.ps1`, a `PreToolUse`
620
+ hook on the `Agent` matcher. It activates only when `subagent_type` is `orchestrator` and the
621
+ serialized delegation prompt carries the marker `Parallel mode: true` (emitted per
622
+ `## Parallel-Mode Kickoff Parameter`). It then resolves the target from the
623
+ `docs/features/active/<folder>` token in the prompt matched against `items[].feature_folder`, reads
624
+ `artifacts/orchestration/parallel-orchestrator-state.json`, projects cohorts to the rows whose
625
+ `generation` equals `recolor_generation`, and denies with a reason prefixed
626
+ `PARALLEL_COHORT_BARRIER_BLOCKED` unless every `conflict_edges[]` neighbour in a strictly prior
627
+ current-generation cohort has `merge_status` of `merged` or `worktree_removed`. `ci_green` does not
628
+ satisfy the barrier. Same-cohort and later-cohort neighbours do not block Layer 1.
629
+
630
+ **Layer 2, retrospective backstop.** `validate_cohort_barrier_ordering` in
631
+ `scripts/dev_tools/_parallel_orchestrator_state_cohort_barrier.py`, invoked from
632
+ `validate_parallel_orchestrator_state_text` and reached at `parallel-orchestrator` `SubagentStop` time
633
+ by the `.claude/settings.json` matcher that runs `.claude/hooks/validate-orchestrator-output.ps1` with
634
+ `-ArtifactType parallel-orchestrator-state`. It appends exactly one message per violated edge, in the
635
+ form `PARALLEL_COHORT_BARRIER_VIOLATION: <a> ran concurrently with conflicting <b>`. The check is
636
+ key-gated on `conflict_edges` and `cohorts`, so a checkpoint lacking either key yields zero errors,
637
+ and it adds no checkpoint fields.
638
+
639
+ **Why both layers are required.** A `PreToolUse` hook fires once per tool call with no cross-call or
640
+ conversation-state visibility, so it cannot see the rest of a batch of concurrent `Agent` calls and
641
+ cannot reject the batch as a whole. The retrospective validator does see the recorded batch, but only
642
+ after those calls executed. Layer 1 deters the individual out-of-order launch; Layer 2 detects the
643
+ concurrent batch Layer 1 structurally cannot. Removing either layer reopens the gap, so neither
644
+ substitutes for the other and neither substitutes for the procedure in
645
+ `## Cohort Barrier and Max-Concurrency Slot Filling`.
646
+
647
+ **Worktree removal gate.** `.claude/hooks/enforce-parallel-worktree-removal-gate.ps1`, a `PreToolUse`
648
+ hook on the `Bash` matcher, intercepts `git worktree remove` and matches the normalized target path
649
+ against `items[].worktree_path`. Removal is allowed only when that item's `merge_status` is `merged`
650
+ or `worktree_removed`; anything else — including an unreadable checkpoint or no matching record —
651
+ denies with a reason prefixed `PARALLEL_WORKTREE_REMOVAL_BLOCKED`. Commands that are not
652
+ `git worktree remove` always allow. This is the mechanical counterpart to `## Worktree Cleanup`.
653
+
654
+ **Invocation-origin extension.** `.claude/hooks/enforce-epic-invocation-origin.ps1` was extended
655
+ additively so `$script:GatedSubagentTypes` lists `epic-planner`, `epic-orchestrator`,
656
+ `parallel-planner`, and `parallel-orchestrator`. An `Agent(parallel-planner)` or
657
+ `Agent(parallel-orchestrator)` call whose caller `agent_type` is `orchestrator` is denied with a
658
+ reason prefixed `PARALLEL_INVOCATION_ORIGIN_BLOCKED`, because both parallel personas delegate to
659
+ `Agent(orchestrator)` and an orchestrator-originated invocation would nest `orchestrator` inside its
660
+ own delegation chain; invoke either persona from the main session instead. Main-thread invocations
661
+ (absent or blank caller `agent_type`) and non-orchestrator callers continue to allow. Epic behaviour
662
+ is unchanged: the `EPIC_INVOCATION_ORIGIN_BLOCKED` reason string is byte-identical for epic targets.
663
+
664
+ ## Radius Drift Detection (F8)
665
+
666
+ ### Radius Drift Detection and Drift Gate
667
+
668
+ An in-flight item whose actual diff escapes its declared `blast_radius.paths` invalidates the
669
+ concurrency guarantee for every item running beside it, which is the dominant failure mode of this
670
+ surface and the compensating control for heuristically derived radii. Detection is the
671
+ execution-time half of a paired mitigation: F1's plan-time coverage validation bounds
672
+ under-reporting when the radius is derived, and this procedure bounds it while the item runs.
673
+ Neither half eliminates the risk. Nothing in this section re-derives F1's matcher or F1's
674
+ contention relation; both are imported by the implementation.
675
+
676
+ #### Seven-Step Procedure
677
+
678
+ 1. Compare the observed changed-path list against the item's declared `blast_radius.paths`.
679
+ 2. On escape, record one `drift_events[]` entry and raise a synthetic Blocking finding in the
680
+ child's own `remediation-inputs.<yyyy-MM-ddTHH-mm>.md`.
681
+ 3. Quiesce: suspend admission of new items into the current cohort.
682
+ 4. Recompute conflicts using the observed radius in place of the drifting item's declared radius.
683
+ 5. If the escape newly conflicts with a concurrently in-flight item, halt the later-started item of
684
+ that pair, record `merge_status: blocked_drift` with item `state: blocked`, and requeue it into a
685
+ future cohort.
686
+ 6. The child's existing R1 through R5 remediation loop processes the finding unmodified.
687
+ 7. **Resolve the recorded drift.** Actor: the `parallel-orchestrator`. Trigger: the consuming
688
+ remediation cycle exiting with `blocking_count == 0`. The parent then performs exactly one of two
689
+ writes to the item's `items[].blast_radius`. Either it re-records the radius from the
690
+ post-remediation diff — the library-built value the detecting invocation already emitted as the
691
+ `observed_radius` payload key, carrying `source: observed` and a `computed_at` that must be
692
+ strictly later than the event's `at` — or, when remediation widened the declared radius instead of
693
+ narrowing the diff, it extends `blast_radius.paths` so every `escaped_paths` entry of the latest
694
+ event is covered. The radius is never hand-constructed; it is the value the command line emitted,
695
+ so the module and shared-surface levels are resolved by F1's library. **No other write clears the
696
+ derived unresolved state**: appending an event cannot, because the action enum has no `resolved`
697
+ member and a zero-escape event is rejected, and changing `merge_status` or item `state` cannot,
698
+ because the derivation reads only `blast_radius`.
699
+
700
+ Deferral of admission into a **future** cohort remains allowed during quiesce; only admission into
701
+ the current cohort is suspended.
702
+
703
+ #### Child-Side Evaluation Point
704
+
705
+ Detection is evaluated at each child's pre-feature-review commit: the moment inside the child
706
+ orchestrator's Pre-Feature-Review Commit step between the successful commit and the `feature-review`
707
+ delegation. Evaluating after the commit is what makes the observed diff complete; evaluating before
708
+ the delegation is what lets the synthetic finding reach the same review pass.
709
+
710
+ The evaluation is active only for a child whose delegation prompt carries the `Parallel mode: true`
711
+ marker emitted by `## Parallel-Mode Kickoff Parameter`. A child running outside a parallel run
712
+ carries no such marker, evaluates no drift, and is unaffected.
713
+
714
+ #### CLI Invocation
715
+
716
+ Detection logic is pure and lives in `scripts/dev_tools/parallel_drift_detection.py` (escape
717
+ detection, `drift_events[]` construction, the derived quiesce predicate, and conflict recomputation)
718
+ and `scripts/dev_tools/parallel_drift_halt.py` (halt selection and the requeue seam). All I/O is
719
+ confined to the thin wrapper `scripts/dev_tools/parallel_drift_detection_cli.py`, invoked as:
720
+
721
+ ```
722
+ poetry run python -m scripts.dev_tools.parallel_drift_detection_cli \
723
+ --item-key <issue_num> \
724
+ [--checkpoint artifacts/orchestration/parallel-orchestrator-state.json] \
725
+ [--config config/blast-radius.json] \
726
+ [--at <yyyy-MM-ddTHH-mm>] [--computed-at <yyyy-MM-ddTHH-mm>] \
727
+ <CHANGED_PATH>...
728
+ ```
729
+
730
+ Argument surface: `--item-key` is the only required argument and is the item's `issue_num`;
731
+ `--checkpoint` and `--config` default to the two paths shown; `--at` is the timestamp recorded on
732
+ the `drift_events[]` entry and `--computed-at` the timestamp recorded on the observed radius, each
733
+ defaulting at the I/O boundary so the pure functions never read a clock; and the changed paths are
734
+ positional and variadic. An empty changed-path list is legal and yields `no_escape`.
735
+
736
+ The changed-path list is an argument, not something the module derives: the caller produces it with
737
+ `git diff --name-only <merge-base(origin/main, HEAD)> HEAD` at the child's pre-review commit, and
738
+ the module executes no git command of its own. Merge-base semantics matter — comparing against the
739
+ merge base rather than against the current `origin/main` tip keeps a concurrently merged peer item's
740
+ files from appearing as spurious drift. A rename lists both the old and the new path, and both must
741
+ be covered, which is the fail-closed direction.
742
+
743
+ Stdout carries exactly one JSON object; errors go to stderr only, so stdout is unconditionally
744
+ parseable:
745
+
746
+ ```json
747
+ {
748
+ "result": "no_escape | no_new_conflict | halt_required",
749
+ "item_key": 0,
750
+ "at": "",
751
+ "computed_at": "",
752
+ "escaped_paths": [],
753
+ "newly_conflicting_pairs": [],
754
+ "halted_item_keys": [],
755
+ "drift_event": null,
756
+ "observed_radius": null
757
+ }
758
+ ```
759
+
760
+ `result` is the verdict: `no_escape` means the diff stayed inside the declared radius;
761
+ `no_new_conflict` means paths escaped but the observed radius introduced no contention, so nothing
762
+ is halted; `halt_required` means at least one pair newly conflicts and `halted_item_keys` names the
763
+ later-started item of each. `newly_conflicting_pairs` holds ascending canonical `[a, b]` item-key
764
+ pairs, the same edge identity `conflict_edges[]` records, so a recomputed pair is comparable with a
765
+ recorded one without normalization. `drift_event` is `null` exactly when `result` is `no_escape`,
766
+ because an event with zero
767
+ escaped paths is not a drift event. `observed_radius` is the serialized observed `blast_radius` the
768
+ parent writes back in step 7 of `#### Seven-Step Procedure`: it carries the six invariant-9 keys with
769
+ `source: observed`, is built by F1's library rather than by hand, and is `null` exactly when `result`
770
+ is `no_escape`, on the same precondition as `drift_event`. Exit status is `0` on success, `1` on
771
+ missing or malformed input, and argparse's `2` on a usage error.
772
+
773
+ #### Synthetic Blocking Finding
774
+
775
+ The finding is written to `docs/features/active/<child-slug>/remediation-inputs.<yyyy-MM-ddTHH-mm>.md`
776
+ — the flat form directly in the item's own active feature-folder root, matching the existing
777
+ merge-conflict finding precedent rather than any folder-per-cycle variant.
778
+
779
+ It is written by `parallel-orchestrator`, which detects the escape and owns the checkpoint, reaching
780
+ the child's checkout through the item's recorded `items[].worktree_path`. That field is optional in
781
+ the schema and may be null; an absent value means the finding cannot be written, which the drift
782
+ gate treats as unwritten.
783
+
784
+ The file must contain the literal line `- Severity: Blocking`, matched case-sensitively by the child
785
+ orchestrator's post-review outcome evaluation, together with the escaped paths, the declared
786
+ patterns, and the required action.
787
+
788
+ #### Halt the Later-Started Item
789
+
790
+ The later-started item of a newly conflicting pair is halted, and **the drifting item is never the
791
+ one halted**. Halting the drifting item is not an option: it must not be implemented, and it must not
792
+ be offered as a configuration. The prohibition is unconditional and holds even when the drifting item
793
+ is the later starter by either tie-break. Two reasons make it necessary: the drifting item's work is
794
+ already broader than planned and is more expensive to unwind, and the drifting item is mid-remediation
795
+ on its own R1 through R5 loop for the drift finding, so halting it would deadlock the very remediation
796
+ that resolves the drift.
797
+
798
+ The exclusion is applied **at the call site, before the later-started comparator runs**:
799
+ `halted_item_keys` in `scripts/dev_tools/parallel_drift_detection_cli.py` drops the drifting key from
800
+ each pair's candidate list, then halts the single remaining candidate, or applies the comparator when
801
+ two remain. Because a recomputed pair holds two distinct canonical keys, the candidate list is always
802
+ one or two entries and can never be empty. It remains true, and remains a real structural guarantee,
803
+ that the selection function itself receives only the two start markers and no drift information at
804
+ all, so a caller cannot invert the comparator; the exclusion lives one level up because that is where
805
+ the drifting key is known.
806
+
807
+ Selection among two candidates is `argmax` over `(start_unknown, worktree_created_at, item_key)`, where
808
+ `worktree_created_at` is the adopted start-of-execution marker and `item_key` is the integer
809
+ `issue_num`. The three tie-breaks:
810
+
811
+ - Equal timestamps — the normal case for a same-minute cohort fan-out — deem the larger `issue_num`
812
+ later-started, so the smaller key survives, consistent with the ascending-item-key determinism
813
+ convention used in `## Cohort Barrier and Max-Concurrency Slot Filling`.
814
+ - A start timestamp present on exactly one item makes that timestamped item earlier-started, so the
815
+ item of unknown start is halted.
816
+ - Both timestamps absent falls through to the item-key tie-break.
817
+
818
+ Identical inputs produce identical halt decisions; every timestamp is a function input and no clock
819
+ is read inside the pure functions.
820
+
821
+ #### Quiesce Is Derived State
822
+
823
+ Quiesce is derived, never stored. Admission into the current cohort is suspended while
824
+ `has_unresolved_drift(events, items)` returns `True`. **No quiesce field is written anywhere** — not
825
+ to the checkpoint, not to the manifest, not to the status document. Deriving quiesce from the event
826
+ log and the item records makes it self-clearing on resolution and keeps the F3-owned checkpoint
827
+ contract untouched, consistent with `## Parallel-Level Checkpoint`. That exported predicate is the
828
+ single seam F6's admission control consults.
829
+
830
+ #### Requeue Through the Single Recolor Seam
831
+
832
+ The requeue passes through exactly one seam, `request_requeue_via_recolor`, which **requests** the
833
+ mutation and never performs one. No second recolor implementation exists in this feature: the seam
834
+ contains no coloring, no cohort assignment, and no graph logic. F6 owns the recolor engine that pins
835
+ in-flight items, recolors the unstarted subgraph, and writes the checkpoint.
836
+
837
+ The requested intent is:
838
+
839
+ - the joint item write `merge_status: blocked_drift` **and** item `state: blocked`. Both are
840
+ required together: the schema requires a `blocked_drift` merge status to accompany item state
841
+ `blocked`, so writing the merge status alone produces a checkpoint that fails validation;
842
+ - exactly one `mutations[]` entry
843
+ `{op: "requeue", item_key, at, prior_state: "in_flight", new_state: "blocked", disposition: null,
844
+ recolor_generation: <prior + 1>}`;
845
+ - `recolor_generation` incremented by exactly one.
846
+
847
+ `new_state` is the item-state value `blocked`, **not** `blocked_drift`. `mutations[].new_state` is
848
+ validated against the item-state enum, and `blocked_drift` is a `merge_status`, so it would be
849
+ rejected in that slot. `disposition` is null because a non-null disposition is permitted only on a
850
+ `remove` entry whose `prior_state` is `in_flight`.
851
+
852
+ #### Drift-Event Recording (A8)
853
+
854
+ Exactly one `drift_events[]` entry is appended per drift occurrence, carrying the strongest action
855
+ taken. `halted_later_started_item` subsumes `raised_blocking_finding`, so an occurrence that halted a
856
+ later-started item records that one event and does **not** additionally record a
857
+ `raised_blocking_finding` event for the same occurrence. The entry shape is the six F3-owned fields
858
+ `{item_key, declared, observed, escaped_paths, at, action}`; `escaped_paths` is non-empty by
859
+ definition. `drift_events[]` is append-only, and each append is a regeneration boundary for the
860
+ status document per `## Documentation Maintenance Boundaries`.
861
+
862
+ #### Two-Layer Drift Gate
863
+
864
+ A child's transition to review, and the item's progression toward merge, are gated while an
865
+ unresolved drift event exists for that item. The gate is implemented at both enforcement layers,
866
+ because a `PreToolUse` hook fires per call with no cross-call state visibility and can be bypassed.
867
+
868
+ - **Layer 1 — per-call deterrent.** `.claude/hooks/enforce-parallel-drift-gate.ps1`, registered on
869
+ the `PreToolUse` `Agent` matcher. It fires only when `subagent_type == "feature-review"` and the
870
+ prompt carries the `Parallel mode: true` marker, resolves the target item by scanning the prompt
871
+ for a `docs/features/active/<basename>` path token, reads
872
+ `artifacts/orchestration/parallel-orchestrator-state.json`, and denies with
873
+ `PARALLEL_DRIFT_GATE_BLOCKED` while the item's latest drift event is unresolved **and** its
874
+ synthetic finding has not been written. Requiring both conditions is what keeps the R4 review of
875
+ the remediation loop from being deadlocked: resolution itself requires a review, so once the
876
+ finding file exists, review proceeds. Allowed outright: a non-`feature-review` target, a prompt
877
+ without the marker, and a resolved or never-drifted item. Denied fail-closed: a missing or
878
+ unreadable checkpoint, an unresolvable target item, and an unreadable drift-event log. The hook
879
+ performs presence gating only — checkpoint-state reads plus one finding-file existence check. It
880
+ runs no git command and no path-glob matching, so all path-matching semantics stay in the single
881
+ Python implementation.
882
+ - **Layer 2 — retrospective backstop.** A key-gated invariant in
883
+ `scripts/dev_tools/validate_parallel_orchestrator_state.py`, implemented in the helper
884
+ `scripts/dev_tools/_parallel_orchestrator_state_drift.py`. It emits one
885
+ `PARALLEL_DRIFT_GATE_VIOLATION:` error per item whose latest drift event is unresolved while its
886
+ `merge_status` is in `{pr_open, ci_green, merged, worktree_removed}`. It is additive: a checkpoint
887
+ with no `drift_events` key produces zero new errors. An item resting at `blocked_drift` produces no
888
+ error, which is what makes the gate compatible with the state the halt path writes.
889
+
890
+ The Layer-1 finding-presence check is narrowed to the **current** drift event. A matched
891
+ `remediation-inputs.<yyyy-MM-ddTHH-mm>.md` file opens the gate only when the timestamp embedded in
892
+ its name is ordinally greater than or equal to the item's latest drift event's `at`; a finding
893
+ written by an earlier, unrelated remediation cycle is therefore ignored and does not open the gate
894
+ for drifted, unsurfaced work. The check remains **presence gating only**: the timestamp is taken
895
+ with a fixed-offset substring of the directory entry's name and compared ordinally, and the hook
896
+ performs no path-glob match, no git command, and no read of any file's content. A name whose
897
+ embedded substring is absent or not canonically formatted, and a latest-event `at` that is itself
898
+ not canonically formatted, both leave the gate closed, so an unreadable timestamp on either side
899
+ denies rather than allows. The canonical `yyyy-MM-ddTHH-mm` shape is required on both sides of every
900
+ timestamp comparison the gate makes, in both runtimes: an ungated ordinal comparison fails open,
901
+ because `-` sorts below `:` and a colon-bearing value therefore compares greater than a
902
+ hyphen-bearing value naming the same instant.
903
+
904
+ #### Resolution Semantics
905
+
906
+ This is the least obvious part of the feature and is stated here in full. `drift_events[].action` has
907
+ exactly two members, `raised_blocking_finding` and `halted_later_started_item`, and there is **no
908
+ `resolved` member**. `escaped_paths` must be non-empty, so a clean re-evaluation cannot be recorded
909
+ as a drift event at all. Resolution is therefore **derived** from fields that already exist, and this
910
+ feature adds no schema field and extends no enum.
911
+
912
+ The latest event for an item key is the one with the greatest `at`, ties broken by append order so
913
+ the later-appended record wins. A latest event for item K is **unresolved** unless at least one
914
+ disjunct holds against K's currently recorded `blast_radius`:
915
+
916
+ - **(a) Radius widened to cover the escape.** Every `escaped_paths` entry of K's latest event is
917
+ subsumed by the item's current `blast_radius.paths` under F1's corrected path-subsumption
918
+ predicate, which honours exact match, listed-directory prefix, and glob match symmetrically with
919
+ the contention relation.
920
+ - **(b) Radius re-recorded from a later observed diff.** The item's `blast_radius.source` is
921
+ `observed` **and** its `blast_radius.computed_at` is strictly later than the event's `at`. This
922
+ covers remediation that narrowed the diff instead of widening the radius.
923
+
924
+ The derivation is **fail-closed**: absent an affirmative parent write, neither disjunct holds, drift
925
+ stays unresolved, and the gate keeps denying. A malformed event log is likewise reported as
926
+ unresolved.
927
+
928
+ Each disjunct has a named producer, so the gate has a release path and nothing deadlocks. The
929
+ producer is step 7 of `#### Seven-Step Procedure`: the `parallel-orchestrator` performs one of the two
930
+ writes when the consuming remediation cycle exits with `blocking_count == 0`. Non-deadlock is
931
+ therefore a consequence of that named producer rather than a property asserted without one. For
932
+ disjunct (b) the value written is not hand-constructed: the command line emits it as the
933
+ `observed_radius` key of its stdout payload, documented under `#### CLI Invocation`, and the parent
934
+ applies that library-built value verbatim with a `computed_at` strictly later than the event's `at`.
935
+ For disjunct (a) the parent extends `blast_radius.paths` to cover every escaped path. The R1 through
936
+ R5 loop that drives the remediation preceding either write is **reused
937
+ unmodified**: `atomic-planner` plans the resolution, `atomic-executor` performs preflight then
938
+ resolves, `feature-review` re-audits, and the loop exits on zero blocking findings. No new
939
+ remediation loop is authored, no line of the existing loop is modified, and the shared
940
+ `remediation_pass` cap of 3 applies. `.claude/skills/orchestrate/SKILL.md` is not modified by this
941
+ feature.
942
+
943
+ #### Layer-1 Narrowing — a Documented Limitation
944
+
945
+ The Layer-1 PowerShell hook implements only disjunct **(b)**. Disjunct (a) requires F1's glob
946
+ matcher, and duplicating that matcher in PowerShell would create exactly the divergent-matcher
947
+ failure this feature exists to prevent, so the hook omits it and evaluates the ordinal
948
+ `computed_at > at` comparison alone.
949
+
950
+ The hook is therefore strictly more conservative than the Python derivation: omitting disjunct (a)
951
+ can only report unresolved where Python reports resolved. The direction of the narrowing is
952
+ **deny-only**, never allow-only, and the finding-file allowance above keeps that conservatism from
953
+ deadlocking review. Python remains the single authority on resolution, and a cross-runtime seam test
954
+ in `tests/scripts/claude-hooks/enforce-parallel-drift-gate-helpers.Tests.ps1` runs both runtimes
955
+ over one shared checkpoint-state table and fails when they diverge. This is a real limitation,
956
+ recorded here rather than omitted.
957
+
958
+ Recovery action for a spurious Layer-1 deny — one the operator can always take: re-record the item's
959
+ `blast_radius` from the later observed diff, which satisfies both runtimes because it is disjunct
960
+ (b), the one disjunct the hook evaluates.