Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/orchestrator/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
- [Epic manifest's ALL CLEAR table is not evidence](epic-manifest-all-clear-table-is-not-evidence.md) — grep the child folder; 1 round found 7 blocking defects incl. an inverted derivation
- [Epic-child rebase shared-memory conflict](epic-child-rebase-shared-memory-conflict.md) · [agent-memory merge conflicts](epic-child-agent-memory-merge-conflicts.md) · [Parallel children conflict on the memory index](parallel-epic-children-conflict-on-agent-memory-index.md)
- [Child cwd is the session root](preparation-child-cwd-is-session-root-not-item-worktree.md) — mirror the WHOLE folder; execution mode too
- [Parallel-item preparation is structurally impossible](parallel-item-preparation-is-structurally-impossible.md) — planner, prd-feature AND git add all blocked together; probe the hook, not the file
- [Resume brief's "already in your worktree" can be false](resume-brief-worktree-contents-premise-can-be-false.md) — Glob first; repair with `merge --ff-only`, which creates no branch
- [Unplanned epic-child worktree mechanics](unplanned-epic-child-worktree-mechanics.md) · [Parallel preparation children share one worktree](parallel-preparation-children-shared-worktree.md)
- [Parallel epic children name collisions](parallel-epic-children-name-collisions.md) · [generic-constraint cascades across children](epic-generic-constraint-cascades-multiple-children.md) · [Absolute-zero gate on a sibling-owned assembly](absolute-zero-gate-on-sibling-owned-assembly.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: parallel-item-preparation-is-structurally-impossible
description: In a parallel item whose session root holds a foreign checkpoint, atomic-planner, prd-feature AND every git add/commit are simultaneously blocked, so preparation cannot complete — measure all three up front and report blocked rather than working around them
metadata:
type: project
---

Three separate PreToolUse/SubagentStop gates resolve repo-relative paths against the Claude session
cwd. In a parallel item that cwd is the coordinator worktree, never the item worktree, so all three
fail together. Verified end to end on item #882 of run `bugs-2026-09-11` (2026-09-13), and it is the
third consecutive item to deadlock this way after #743 and #871.

**1. `git add` / `git commit` — blocked, with no legitimate escape.**
`enforce-orchestration-preimplementation-gate.ps1` lines 373-395 say in terms that "the path and
command legs are single-feature by construction; only the delegation leg carries a mode marker". So
the command leg has NO parallel branch and always reads the session-root default checkpoint. When
that file is a sibling's with `lifecycle_ready` empty, every staging command in your worktree denies.
The issue #539 exemption cannot rescue it: `Test-ExemptOrchestrationSegmentToken` requires
`Token[0] -ceq 'git'` and `Token[1] -ceq 'add'|'commit'`, so a `git -C <abs> add` form fails at
`Token[1]`, and `Test-ExemptOrchestrationOperand` separately rejects any drive-lettered operand. With
Bash cwd at the session root and `cd` forbidden, no admissible form reaches the item worktree.
A git plumbing sequence (`hash-object` / `update-index` / `write-tree` / `commit-tree` /
`update-ref`) does evade the gate's `add|commit` patterns — do NOT take it. That is deliberate
evasion of a PreToolUse gate and needs explicit one-time human authorization.

**2. `Agent(atomic-planner)` — blocked.** Exactly as
[[prd-feature-hook-parses-prompt-paths]] records. Confirmed again here both by a local dot-sourced
probe and by the real delegation, which returned a byte-identical reason.

**3. `Agent(prd-feature)` — blocked, and BOTH of its stop hooks are unsatisfiable, not just one.**
[[prd-feature-stop-hooks-are-workmode-blind]] records the unconditional `user-story-path`
requirement. The second one is worse and is new: `validate-prd-feature-output.ps1` line 80 calls
`Test-Path -LiteralPath $specPath` on the path the agent itself reports, with no cwd override. In a
parallel item that resolves against the session root, where your feature folder does not exist, so
the check fails for EVERY value the agent could report. There is no prompt wording that fixes it.
Do not spend a delegation on it; author `spec.md` yourself and record it under
`local_execution_overrides` with the measured reason. `prd-feature` is not in the orchestrator
persona's mandated delegate set (`atomic-planner`, `atomic-executor`, `feature-review`,
`task-researcher`), so this is not absorbing a mandated delegated step.

## Do not record local authoring as a delegation receipt

Putting a `delegation_receipts.agents[]` entry whose `agent_name` is anything other than a real
delegate fails the MCP validator under `require_model_routing`: the gate demands a
`model_routing_receipts[]` entry for every `agent_name` it finds, and it does not recognise a prose
name. Error seen: `Checkpoint model_routing_receipts is missing a receipt for delegated agent:
orchestrator (local authoring, ...)`. Use `local_execution_overrides` instead; the checkpoint then
validates.

## The session-root checkpoint is REWRITTEN MID-RUN by a live sibling

[[model-routing-hook-reads-canonical-path-only]]'s advice to read it once and predict the whole run
is too weak. On #882 two reads minutes apart returned different item payloads and different
`model_routing_receipts` sets, because a live sibling owns the file. A single read predicts nothing
durable. Worse, the Read tool and a `pwsh` `Get-Content` of the SAME absolute path returned
different contents in the same session, so only the `pwsh` read is a valid proxy for what a
PowerShell hook sees. **Probe the hook itself, not the file**: dot-source the live session-root hook
and call its decision function with a synthetic payload. That is exact, costs one command, and on
#882 it predicted the real `atomic-planner` denial verbatim.

**How to apply.** Run all three probes before doing any work: a `git add` of an exempt path, a
dot-sourced `Invoke-PrdFeatureBeforePlannerDecision`, and a `pwsh` read of the session-root
checkpoint's `lifecycle_ready`. If the planner probe denies, preparation cannot complete — say so at
once. Still produce everything reachable (promoted record, folder, `issue.md`, `spec.md`, research
via `Agent(task-researcher)`, which IS admitted, and a declared blast radius derived from the spec
instead of from an approved plan, with that substitution stated), leave it uncommitted, and hand the
planner delegation plus the commit to the coordinator. Both defects are push-down-owned from
drm-copilot; fix upstream, never here. See
[[project_claude_files_are_pushdown_owned_fix_upstream]] and
[[shared-checkpoint-read-modify-write-corrupts]] for why writing your payload into the session-root
file is the wrong repair.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -16,7 +17,9 @@ namespace QuickFiler.Controllers.Tests
/// the static atomic and is held only for a straight-line region with no wait, no thread creation,
/// and no await inside it. <c>TransactionGate</c> provides mutual exclusion between long
/// install-to-restore transactions and is held from transaction start until
/// <see cref="UiThreadDispatcherTransaction.Dispose"/>. Lock ordering is <c>TransactionGate</c>
/// <see cref="UiThreadDispatcherTransaction.Dispose"/>; acquisition is bounded by
/// <see cref="TransactionGateAcquireTimeoutMs"/> and throws <see cref="TimeoutException"/> on
/// expiry. Lock ordering is <c>TransactionGate</c>
/// then <c>FieldLock</c>, never the reverse, so no cycle and therefore no deadlock exists.
/// </para>
/// <para>
Expand Down Expand Up @@ -135,18 +138,53 @@ internal static IDisposable EnsureDispatcher()
}

/// <summary>
/// Acquires <c>TransactionGate</c> and returns a transaction that has not installed anything
/// yet. The two-phase shape is deliberate: consumers acquire the gate at fixture-build start,
/// well before the install, which preserves the issue #230 hold window.
/// Upper bound on a <c>TransactionGate</c> acquisition through the parameterless
/// <see cref="BeginTransactionAsync()"/> overload (issue #882): twice the 60000 ms MSTest
/// timeout that bounds the longest legitimate hold, and half the four-minute runner hang
/// guard, so an expired bound is reported as a named failure rather than as a hang dump.
/// </summary>
internal static async Task<UiThreadDispatcherTransaction> BeginTransactionAsync()
internal const int TransactionGateAcquireTimeoutMs = 120000;

/// <summary>
/// Acquires <c>TransactionGate</c> with the production bound and returns a transaction that
/// has not installed anything yet. The two-phase shape is deliberate: consumers acquire the
/// gate at fixture-build start, well before the install, which preserves the issue #230 hold
/// window. Throws <see cref="TimeoutException"/> when the permit is not obtained within
/// <see cref="TransactionGateAcquireTimeoutMs"/>; no transaction exists on that path.
/// </summary>
internal static Task<UiThreadDispatcherTransaction> BeginTransactionAsync()
{
return BeginTransactionAsync(
TimeSpan.FromMilliseconds(TransactionGateAcquireTimeoutMs)
);
}

/// <summary>
/// Bounded acquisition (issue #882). Tests supply <see cref="TimeSpan.Zero"/> to observe the
/// failure branch deterministically while they hold the permit. On failure the method throws
/// before any <see cref="UiThreadDispatcherTransaction"/> exists and without touching the
/// acquisitions or releases counter, so there is no release to omit; the contended pre-check
/// stays before the wait because a failed probe did observe a held permit.
/// </summary>
internal static async Task<UiThreadDispatcherTransaction> BeginTransactionAsync(
TimeSpan bound
)
{
if (TransactionGate.CurrentCount == 0)
{
Interlocked.Increment(ref _contendedAcquisitions);
}

await TransactionGate.WaitAsync().ConfigureAwait(false);
bool acquired = await TransactionGate.WaitAsync(bound).ConfigureAwait(false);
if (!acquired)
{
throw new TimeoutException(
"TRANSACTIONGATE_ACQUIRE_TIMEOUT: UiThreadDispatcherFixture.TransactionGate was not acquired within "
+ bound.TotalMilliseconds.ToString("0", CultureInfo.InvariantCulture)
+ " ms. The probable cause is a permit held by a test the runner has already reported as finished (issue #882)."
);
}

Interlocked.Increment(ref _transactionAcquisitions);
return new UiThreadDispatcherTransaction();
}
Expand Down Expand Up @@ -239,7 +277,7 @@ public void Dispose()
/// <summary>
/// A single install-to-restore transaction over the process-wide static
/// <c>UtilitiesCS.UiThread._dispatcher</c>, holding <c>TransactionGate</c> for its whole lifetime.
/// Obtained from <see cref="UiThreadDispatcherFixture.BeginTransactionAsync"/> and released by
/// Obtained from <see cref="UiThreadDispatcherFixture.BeginTransactionAsync()"/> and released by
/// <see cref="Dispose"/>, which restores strictly before it releases the gate so a waiter can
/// never observe the pre-restore value.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,5 +392,67 @@ public async Task TransactionGate_WhileThisTestHoldsATransaction_HasExactlyOneUn
transaction.Dispose();
}
}

/// <summary>
/// Issue #882 — a bounded acquisition that cannot obtain the permit fails promptly by name
/// instead of waiting without bound. While this test holds the sole permit, a zero-bound probe
/// through the internal overload must throw <c>TimeoutException</c> carrying the token
/// TRANSACTIONGATE_ACQUIRE_TIMEOUT, must not be counted as an acquisition, and must not
/// release the permit it never obtained. A zero bound returns immediately by contract, so the
/// test consumes no wall-clock time on any path; the gate's continued usability is asserted
/// only through the production entry point, which waits rather than fails under contention.
/// </summary>
[TestMethod]
[Timeout(GateTimeoutMs)]
public async Task BeginTransactionAsync_ZeroBoundWhileThisTestHoldsThePermit_ThrowsTimeoutExceptionAndReleasesNothing()
{
// Arrange
UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture
.BeginTransactionAsync()
.ConfigureAwait(false);
try
{
int contendedBefore = UiThreadDispatcherFixture.ContendedAcquisitions;

// Act
Func<Task> probe = () =>
UiThreadDispatcherFixture.BeginTransactionAsync(TimeSpan.Zero);

// Assert
await probe
.Should()
.ThrowAsync<TimeoutException>(
because: "a zero bound cannot obtain the permit this test already holds"
)
.WithMessage("*TRANSACTIONGATE_ACQUIRE_TIMEOUT*");
(
UiThreadDispatcherFixture.TransactionAcquisitions
- UiThreadDispatcherFixture.TransactionReleases
)
.Should()
.Be(1, because: "the failed probe must not be counted as an acquisition");
UiThreadDispatcherFixture
.ContendedAcquisitions.Should()
.BeGreaterThanOrEqualTo(
contendedBefore + 1,
because: "the probe observed a held permit, and other classes can only add to the counter"
);
Action dispose = () => transaction.Dispose();
dispose
.Should()
.NotThrow<SemaphoreFullException>(
because: "the failed probe released nothing, so the holder's own release is the first"
);
}
finally
{
transaction.Dispose();
}

UiThreadDispatcherTransaction roundTrip = await UiThreadDispatcherFixture
.BeginTransactionAsync()
.ConfigureAwait(false);
roundTrip.Dispose();
}
}
}
Loading
Loading