Parallel Remove Skill
A user invocation (/parallel-remove <item> [--disposition detach|abandon]) forks the
parallel-orchestrator agent with this procedure in context. The item key and optional disposition
are:
$ARGUMENTS
This skill implements the mutation-protocol remove operation (spec FR2). It mutates a parallel run
that is already executing, so the pinning invariant and the recompute boundary in the
## Mutation Protocol (F6) section of .claude/skills/parallel-orchestrate/SKILL.md govern what
this operation may disturb. Read that section before applying anything here.
Prerequisites
- A parallel run is in progress and
artifacts/orchestration/parallel-orchestrator-state.json
tracks its parallel_slug.
$ARGUMENTS names exactly one item key — an integer items[].issue_num. Item keys are integers
throughout this surface; there is no string key.
Re-Derive Durable State Before Applying Anything
The checkpoint is a CACHE of durable state, not the source of truth
(.claude/rules/parallel-orchestration.md, Cache Doctrine). Before deciding a removal, re-derive
the target item's true state and rewrite the checkpoint from it when they disagree:
git worktree list --porcelain — worktree existence and path.
git branch — branch existence and name.
gh pr view <pr> --json state,mergedAt,headRefOid — pull-request state and merge outcome.
The removal branch is selected by the item's state, so a stale state selects the wrong branch. An
item the checkpoint calls scheduled but which is really in_flight would be silently withdrawn
and recolored, moving work that is already running. The re-derivation is therefore mandatory.
State-Dependent Behavior Table (Normative)
Implement this table exactly, one branch per row:
| Item state |
Behavior |
proposed, admitted, prepared, scheduled |
Mark withdrawn, drop the vertex, recolor the unstarted subgraph (recompute). |
in_flight |
Reject unless --disposition is supplied. A default disposition is never inferred. |
in_flight with --disposition detach |
Let the item finish and merge on its own; the run stops tracking it. No recompute. |
in_flight with --disposition abandon |
Close the PR, remove the worktree, mark withdrawn. Destructive; hook-gated. No recompute. |
merged |
Reject; the change is already in main. |
An item already withdrawn or blocked is not a live removal target and is rejected as an unknown
item.
No Default Disposition
Removing an item that is in_flight without an explicit --disposition is REJECTED. No default is
inferred, in either direction. The choice between letting running work finish and destroying it
changes what happens to a pull request and a worktree, so it belongs to the caller and never to
this procedure. Do not guess, do not prompt-and-assume, and do not pick detach because it is the
non-destructive option.
Rejected Removals Change Nothing
A rejected removal — in_flight without a disposition, merged, or an unknown or already-withdrawn
item — fails fast with a specific error, appends NO mutations[] entry, and makes NO state change.
Do not record a partial removal, and do not record the rejection itself in mutations[].
Procedure
Re-derive durable state as above and resolve the item key against items[].
Decide the removal by calling decide_removal(item_key, items, disposition) from
scripts/dev_tools/parallel_mutation_protocol.py. The engine raises the dedicated rejection
exception for every rejected row; surface its message and stop.
Unstarted removal (recompute). Set the item's state to withdrawn, drop its vertex, and
recolor by calling recolor_unstarted(unstarted_items, conflict_edges, pinned, current_generation, current_cohort=current_cohort, highest_pinned_cohort=highest_pinned_cohort). Write
RecolorResult.cohort_assignments into cohorts[] and set the top-level recolor_generation
to RecolorResult.generation; the generation increments by exactly one. The result names no
pinned key, so no in-flight item moves.
current_cohort is F3's top-level field, read from the re-verified durable state: the lowest
current-generation cohort index still holding a non-terminal item. Under the per-edge barrier an
in-flight item is not confined to that index, so highest_pinned_cohort — the highest
current-generation cohort index occupied by any in-flight item — is read from the same
re-verified state. The returned indices are ABSOLUTE and are written VERBATIM into
cohorts[].index, never re-based to zero. Returned keys whose index equals current_cohort are
MERGED into the single existing current-generation cohort entry at that index alongside its
pinned members, never written as a second entry carrying the same index, which F3 invariant 13
rejects.
detach (no recompute). Set the item's state to withdrawn and record
disposition: "detach" in the mutation entry. recolor_generation is UNCHANGED: the detached
item was pinned and was never a vertex of the unstarted subgraph, so its departure cannot change
the induced subgraph. Perform no side effect — the item's own pull request continues to its own
merge outcome and the run simply stops tracking it. The closed-mode completion predicate
excludes withdrawn items, so the run does not wait for a detached item.
abandon (no recompute, destructive, CLI-only). Set the item's state to withdrawn and
record disposition: "abandon" in the mutation entry. recolor_generation is UNCHANGED, for the
same reason as detach. Execute the destructive side effects — closing the pull request and
removing the worktree — through the single deterministic CLI invocation below and through nothing
else:
poetry run python scripts/dev_tools/parallel_mutation_abandon_cli.py --item <key> --disposition abandon --confirm-abandon --pr <pr-number> --worktree <worktree-path>
Executing the abandon disposition through ad hoc gh pr close or git worktree remove commands
is PROHIBITED. The prohibition is not stylistic: the abandon gate matches on the tokens of the
invocation above, so an ad hoc command is not matchable and would bypass the confirmation
contract entirely. One invocation, one item, both side effects.
Append exactly one mutations[] entry for a successful removal, built by
build_remove_entry from scripts/dev_tools/parallel_mutation_protocol.py:
| Case |
op |
item_key |
prior_state |
new_state |
disposition |
recolor_generation |
| Remove, unstarted |
remove |
item key |
prior state (proposed/admitted/prepared/scheduled) |
withdrawn |
null |
g + 1 |
Remove, detach |
remove |
item key |
in_flight |
withdrawn |
detach |
g (unchanged) |
Remove, abandon |
remove |
item key |
in_flight |
withdrawn |
abandon |
g (unchanged) |
disposition is non-null only on an in-flight removal and is null on the unstarted row. The at
timestamp comes from the engine's injected clock seam.
Validate the checkpoint before treating the removal as applied. Run the
validate_orchestration_artifacts MCP tool with artifact_type: "parallel-orchestrator-state". A non-empty error list means the removal was applied incorrectly;
correct the checkpoint rather than proceeding.
Abandon Confirmation-Marker Contract
The abandon path is guarded by the PreToolUse hook
.claude/hooks/enforce-parallel-abandon-gate.ps1 on the Bash matcher. The contract is:
- A Bash command carrying the disposition token for abandon MUST also carry the explicit
confirmation marker
--confirm-abandon in the SAME command.
- A command carrying the abandon disposition token WITHOUT the confirmation marker is DENIED. The
deny reason is prefixed
PARALLEL_ABANDON_BLOCKED.
- A command carrying both tokens is allowed.
- A command carrying neither is out of scope and is allowed unchanged.
The two token values are declared once each in
scripts/dev_tools/parallel_mutation_abandon_cli.py (the producer) and once each in the hook (the
consumer), and the seam test
tests/scripts/dev_tools/test_parallel_abandon_token_seam.py parses all three artifacts — the CLI,
the hook, and the invocation line in step 5 above — at run time to prove they still agree. Renaming
a token in one artifact without the identical rename in the other two fails that test. The
invocation in step 5 is the file's only executable abandon command line, and the seam test parses
that one line; do not add a second one.
When the gate denies a command, the correct response is to add the confirmation marker
deliberately, not to reformulate the command to evade the match. Reformulating to evade the gate
defeats the only mechanism protecting a destructive operation.
Constraints
- One removal per invocation; at most one
mutations[] entry per invocation.
- No field and no enum member is added to
mutations[], items[], or any state or merge-status
enum. The nine parallel enums are owned by .claude/rules/parallel-orchestration.md and are
consumed, never extended.
- Neither
detach nor abandon recomputes. An unstarted item previously deferred because of a
conflict with the removed item keeps its cohort assignment: the assignment stays valid and is at
most conservative, and no opportunistic recompute is performed.
- No in-flight item other than the removal target changes cohort or state.
Completion Requirements
- Report the item key, the branch of the behavior table taken, the disposition recorded, the
resulting
recolor_generation, and the single appended mutations[] entry — or, for a rejected
removal, the rejection and the explicit confirmation that nothing was appended and no state
changed.
- For an abandon, report the CLI's exit code and both side-effect outcomes.
- Report the checkpoint validation result.
1---2name: parallel-remove3description: Remove one item from a running parallel run per the state-dependent removal behavior table — withdraw and recolor for an unstarted item, detach or abandon for an in-flight item, reject for a merged item. No default disposition is ever inferred. The abandon path runs through one deterministic CLI invocation and is hook-gated.4---56# Parallel Remove Skill78A user invocation (`/parallel-remove <item> [--disposition detach|abandon]`) forks the9`parallel-orchestrator` agent with this procedure in context. The item key and optional disposition10are:1112$ARGUMENTS1314This skill implements the mutation-protocol remove operation (spec FR2). It mutates a parallel run15that is already executing, so the pinning invariant and the recompute boundary in the16`## Mutation Protocol (F6)` section of `.claude/skills/parallel-orchestrate/SKILL.md` govern what17this operation may disturb. Read that section before applying anything here.1819## Prerequisites2021- A parallel run is in progress and `artifacts/orchestration/parallel-orchestrator-state.json`22 tracks its `parallel_slug`.23- `$ARGUMENTS` names exactly one item key — an integer `items[].issue_num`. Item keys are integers24 throughout this surface; there is no string key.2526## Re-Derive Durable State Before Applying Anything2728The checkpoint is a CACHE of durable state, not the source of truth29(`.claude/rules/parallel-orchestration.md`, Cache Doctrine). Before deciding a removal, re-derive30the target item's true state and rewrite the checkpoint from it when they disagree:3132- `git worktree list --porcelain` — worktree existence and path.33- `git branch` — branch existence and name.34- `gh pr view <pr> --json state,mergedAt,headRefOid` — pull-request state and merge outcome.3536The removal branch is selected by the item's state, so a stale state selects the wrong branch. An37item the checkpoint calls `scheduled` but which is really `in_flight` would be silently withdrawn38and recolored, moving work that is already running. The re-derivation is therefore mandatory.3940## State-Dependent Behavior Table (Normative)4142Implement this table exactly, one branch per row:4344| Item state | Behavior |45| --- | --- |46| `proposed`, `admitted`, `prepared`, `scheduled` | Mark `withdrawn`, drop the vertex, recolor the unstarted subgraph (recompute). |47| `in_flight` | **Reject** unless `--disposition` is supplied. A default disposition is never inferred. |48| `in_flight` with `--disposition detach` | Let the item finish and merge on its own; the run stops tracking it. No recompute. |49| `in_flight` with `--disposition abandon` | Close the PR, remove the worktree, mark `withdrawn`. Destructive; hook-gated. No recompute. |50| `merged` | Reject; the change is already in `main`. |5152An item already `withdrawn` or `blocked` is not a live removal target and is rejected as an unknown53item.5455## No Default Disposition5657Removing an item that is `in_flight` without an explicit `--disposition` is REJECTED. No default is58inferred, in either direction. The choice between letting running work finish and destroying it59changes what happens to a pull request and a worktree, so it belongs to the caller and never to60this procedure. Do not guess, do not prompt-and-assume, and do not pick `detach` because it is the61non-destructive option.6263## Rejected Removals Change Nothing6465A rejected removal — `in_flight` without a disposition, `merged`, or an unknown or already-withdrawn66item — fails fast with a specific error, appends NO `mutations[]` entry, and makes NO state change.67Do not record a partial removal, and do not record the rejection itself in `mutations[]`.6869## Procedure70711. Re-derive durable state as above and resolve the item key against `items[]`.72732. Decide the removal by calling `decide_removal(item_key, items, disposition)` from74 `scripts/dev_tools/parallel_mutation_protocol.py`. The engine raises the dedicated rejection75 exception for every rejected row; surface its message and stop.76773. **Unstarted removal (recompute).** Set the item's state to `withdrawn`, drop its vertex, and78 recolor by calling `recolor_unstarted(unstarted_items, conflict_edges, pinned,79 current_generation, current_cohort=current_cohort,80 highest_pinned_cohort=highest_pinned_cohort)`. Write81 `RecolorResult.cohort_assignments` into `cohorts[]` and set the top-level `recolor_generation`82 to `RecolorResult.generation`; the generation increments by exactly one. The result names no83 pinned key, so no in-flight item moves.8485 `current_cohort` is F3's top-level field, read from the re-verified durable state: the lowest86 current-generation cohort index still holding a non-terminal item. Under the per-edge barrier an87 in-flight item is not confined to that index, so `highest_pinned_cohort` — the highest88 current-generation cohort index occupied by any in-flight item — is read from the same89 re-verified state. The returned indices are ABSOLUTE and are written VERBATIM into90 `cohorts[].index`, never re-based to zero. Returned keys whose index equals `current_cohort` are91 MERGED into the single existing current-generation cohort entry at that index alongside its92 pinned members, never written as a second entry carrying the same `index`, which F3 invariant 1393 rejects.94954. **`detach` (no recompute).** Set the item's state to `withdrawn` and record96 `disposition: "detach"` in the mutation entry. `recolor_generation` is UNCHANGED: the detached97 item was pinned and was never a vertex of the unstarted subgraph, so its departure cannot change98 the induced subgraph. Perform no side effect — the item's own pull request continues to its own99 merge outcome and the run simply stops tracking it. The `closed`-mode completion predicate100 excludes withdrawn items, so the run does not wait for a detached item.1011025. **`abandon` (no recompute, destructive, CLI-only).** Set the item's state to `withdrawn` and103 record `disposition: "abandon"` in the mutation entry. `recolor_generation` is UNCHANGED, for the104 same reason as `detach`. Execute the destructive side effects — closing the pull request and105 removing the worktree — through the single deterministic CLI invocation below and through nothing106 else:107108 ```bash109 poetry run python scripts/dev_tools/parallel_mutation_abandon_cli.py --item <key> --disposition abandon --confirm-abandon --pr <pr-number> --worktree <worktree-path>110 ```111112 Executing the abandon disposition through ad hoc `gh pr close` or `git worktree remove` commands113 is PROHIBITED. The prohibition is not stylistic: the abandon gate matches on the tokens of the114 invocation above, so an ad hoc command is not matchable and would bypass the confirmation115 contract entirely. One invocation, one item, both side effects.1161176. **Append exactly one `mutations[]` entry** for a successful removal, built by118 `build_remove_entry` from `scripts/dev_tools/parallel_mutation_protocol.py`:119120 | Case | `op` | `item_key` | `prior_state` | `new_state` | `disposition` | `recolor_generation` |121 | --- | --- | --- | --- | --- | --- | --- |122 | Remove, unstarted | `remove` | item key | prior state (`proposed`/`admitted`/`prepared`/`scheduled`) | `withdrawn` | null | `g` + 1 |123 | Remove, `detach` | `remove` | item key | `in_flight` | `withdrawn` | `detach` | `g` (unchanged) |124 | Remove, `abandon` | `remove` | item key | `in_flight` | `withdrawn` | `abandon` | `g` (unchanged) |125126 `disposition` is non-null only on an in-flight removal and is null on the unstarted row. The `at`127 timestamp comes from the engine's injected clock seam.1281297. **Validate the checkpoint** before treating the removal as applied. Run the130 `validate_orchestration_artifacts` MCP tool with `artifact_type:131 "parallel-orchestrator-state"`. A non-empty error list means the removal was applied incorrectly;132 correct the checkpoint rather than proceeding.133134## Abandon Confirmation-Marker Contract135136The abandon path is guarded by the PreToolUse hook137`.claude/hooks/enforce-parallel-abandon-gate.ps1` on the `Bash` matcher. The contract is:138139- A Bash command carrying the disposition token for abandon MUST also carry the explicit140 confirmation marker `--confirm-abandon` in the SAME command.141- A command carrying the abandon disposition token WITHOUT the confirmation marker is DENIED. The142 deny reason is prefixed `PARALLEL_ABANDON_BLOCKED`.143- A command carrying both tokens is allowed.144- A command carrying neither is out of scope and is allowed unchanged.145146The two token values are declared once each in147`scripts/dev_tools/parallel_mutation_abandon_cli.py` (the producer) and once each in the hook (the148consumer), and the seam test149`tests/scripts/dev_tools/test_parallel_abandon_token_seam.py` parses all three artifacts — the CLI,150the hook, and the invocation line in step 5 above — at run time to prove they still agree. Renaming151a token in one artifact without the identical rename in the other two fails that test. The152invocation in step 5 is the file's only executable abandon command line, and the seam test parses153that one line; do not add a second one.154155When the gate denies a command, the correct response is to add the confirmation marker156deliberately, not to reformulate the command to evade the match. Reformulating to evade the gate157defeats the only mechanism protecting a destructive operation.158159## Constraints160161- One removal per invocation; at most one `mutations[]` entry per invocation.162- No field and no enum member is added to `mutations[]`, `items[]`, or any state or merge-status163 enum. The nine parallel enums are owned by `.claude/rules/parallel-orchestration.md` and are164 consumed, never extended.165- Neither `detach` nor `abandon` recomputes. An unstarted item previously deferred because of a166 conflict with the removed item keeps its cohort assignment: the assignment stays valid and is at167 most conservative, and no opportunistic recompute is performed.168- No in-flight item other than the removal target changes cohort or state.169170## Completion Requirements171172- Report the item key, the branch of the behavior table taken, the disposition recorded, the173 resulting `recolor_generation`, and the single appended `mutations[]` entry — or, for a rejected174 removal, the rejection and the explicit confirmation that nothing was appended and no state175 changed.176- For an abandon, report the CLI's exit code and both side-effect outcomes.177- Report the checkpoint validation result.