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