You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bootstrap-gate deadlock regression (#1021/v4.4.39): premature inbox witness from TaskUpdate(owner) defeats the secretary-spawn carve-out under standard CLI choreography #1023
PR #1021 (the #1019 config-less Desktop/SDK bootstrap-deadlock fix, v4.4.39) re-introduces a bootstrap-gate deadlock under the standard CLI PACT choreography. The first fresh session to bootstrap under 4.4.39 (session-b6dc25b7) hit it: the canonical secretary spawn was DENIED by bootstrap_gate, and the session only bootstrapped after a manual rm of a platform inbox file.
This is a new regression distinct from #1019 — #1019's original deadlock was Desktop/config-less; this one breaks the ordinary CLI path that worked pre-#1019.
Severity
High — blocks bootstrap on the first turn of every fresh CLI session running 4.4.39. The session self-heals one turn late (marker written via a premature witness on the next user prompt), but the bootstrap turn's secretary spawn fails, and the self-heal path writes a "bootstrap-complete" marker for a secretary that may never have been spawned (secondary concern below).
Mechanism — indirect regression through a shared predicate
#1021 edited onlybootstrap_marker_writer.py (bootstrap_gate.py was untouched), but it widened the semantics of _team_has_secretary() by adding a config-less inbox-witness fallback:
The PACT bootstrap choreography (commands/bootstrap.md Step 2 → Step 3) is:
Step 1 TaskCreate(secretary briefing)
Step 2 TaskUpdate(taskId, owner="secretary") # platform delivers a task_assignment -> CREATES inboxes/secretary.json
Step 3 Agent(name="secretary", ...) # the carve-out must fire HERE
So by Step 3 the inbox already exists → _team_has_secretary returns True (via the new fallback arm, since members[] doesn't yet contain the secretary) → binding 5 (not True) → False → carve-out does not fire → bootstrap_gateDENIES the very spawn the carve-out exists to permit. Deadlock, because no marker exists yet on the bootstrap turn either.
The load-bearing assumption is falsified by ground truth
The #1021 docstring justifying the inbox witness asserts:
"the platform writes the secretary's inbox file only on delivery of a message to an already-dispatched secretary; it cannot predate the spawn within PACT's choreography."
This is false. TaskUpdate(owner=...)is part of the choreography (Step 2) and the platform delivers a task_assignment notification to the assignee's inbox, creating inboxes/secretary.json before Step 3's spawn.
Reproduction
Start a fresh CLI session under v4.4.39 (in-process or tmux).
Run the bootstrap ritual: TaskCreate secretary task → TaskUpdate(owner="secretary") → Agent(name="secretary", subagent_type="pact-secretary").
The Agent call is denied: PACT bootstrap required. Invoke Skill("PACT:bootstrap") first... (the canonical _DENY_REASON).
Evidence (from session-b6dc25b7, before the manual workaround)
inboxes/secretary.json held a task_assignment for task Claude Code: not strictly follow framework #1, "assignedBy":"team-lead", ts 2026-06-24T02:16:59Z — created by TaskUpdate(owner), with no prior successful spawn.
_team_has_secretary('session-b6dc25b7') → True, while members[] was ['team-lead'] only. The True came purely from the inbox arm.
The actual Agent(name="secretary", subagent_type="pact-secretary") call was DENIED with the canonical bootstrap _DENY_REASON.
After rm inboxes/secretary.json → predicate flipped to False → re-spawn succeeded (carve-out fired).
Both teammate modes are affected (TaskUpdate(owner) writes the inbox in both in-process and tmux).
Root cause
A single predicate was widened for consumer 1's correctness question ("was the secretary dispatched?") while consumer 2 needs a different question answered ("has the secretary actually joined the roster yet?"). Dispatch-witness (inbox) ≠ join-witness (members[]). The carve-out needs a join witness; only the marker writer should accept the dispatch/inbox fallback.
Fix direction (for plan-mode / orchestrate — not yet decided)
Decouple the carve-out from the marker writer's widened predicate. Candidate: give bootstrap_gate binding 5 a members[]-only join witness (e.g. a distinct _team_has_secretary_member() or an inline _iter_members check), leaving _team_has_secretary's inbox fallback for the marker writer alone. This restores pre-#1019 carve-out semantics under CLI while preserving #1019's Desktop marker fix.
Interactions to weigh during design:
Dual-mode contract: under config-less Desktop, members[] is always empty → a members-only carve-out would "always allow secretary spawn pre-marker." Need to confirm that's acceptable (the marker write closes the window; spawn is one-shot in practice) and doesn't lose a needed one-shot guarantee.
Empty-SSOT fail-closed gate (get_team_name short-circuit, test_empty_ssot_team_fails_closed_both_modes) must remain intact.
Both-modes test matrix is a standing merge gate.
Secondary concern (corollary — may warrant its own issue)
Because the premature inbox satisfies the marker writer too, on the next user prompt the marker writer can write a "bootstrap-complete" marker for a secretary that was only task-assigned, never spawned/joined. The inbox witness here proves task assignment, which is weaker than even "dispatched." If the Agent(secretary) call is denied and never retried, the gate unblocks (marker present) with no live secretary — memory queries would then hang on an absent secretary (surfaced as a stuck SendMessage reply, not silent corruption, per the liveness contract). Worth deciding whether the marker writer's inbox witness should exclude task-assignment-only inboxes.
Non-vacuity note
A regression test must run the realTaskUpdate(owner) → Agent sequence (or a faithful fixture that creates the inbox via task-assignment), not a synthetic pre-placed inbox — the synthetic shortcut is exactly what let this slip through #1019's verification.
Source: PR #1021 / commit e371ac9 / v4.4.39. Distinct from #1019 (Desktop config-less deadlock) and #1020 (silent HANDOFF-harvest loss).
Summary
PR #1021 (the #1019 config-less Desktop/SDK bootstrap-deadlock fix, v4.4.39) re-introduces a bootstrap-gate deadlock under the standard CLI PACT choreography. The first fresh session to bootstrap under 4.4.39 (
session-b6dc25b7) hit it: the canonical secretary spawn was DENIED bybootstrap_gate, and the session only bootstrapped after a manualrmof a platform inbox file.This is a new regression distinct from #1019 — #1019's original deadlock was Desktop/config-less; this one breaks the ordinary CLI path that worked pre-#1019.
Severity
High — blocks bootstrap on the first turn of every fresh CLI session running 4.4.39. The session self-heals one turn late (marker written via a premature witness on the next user prompt), but the bootstrap turn's secretary spawn fails, and the self-heal path writes a "bootstrap-complete" marker for a secretary that may never have been spawned (secondary concern below).
Mechanism — indirect regression through a shared predicate
#1021 edited only
bootstrap_marker_writer.py(bootstrap_gate.pywas untouched), but it widened the semantics of_team_has_secretary()by adding a config-less inbox-witness fallback:_team_has_secretary()has two consumers:bootstrap_marker_writer— the intended target (Desktop has noconfig.json→ needs the inbox witness). ✓bootstrap_gate._is_canonical_secretary_spawn, binding 5:return not _team_has_secretary(expected_team)— not re-examined by fix(hooks): config-less Desktop/SDK bootstrap deadlock (#1019) #1021.The PACT bootstrap choreography (
commands/bootstrap.mdStep 2 → Step 3) is:So by Step 3 the inbox already exists →
_team_has_secretaryreturnsTrue(via the new fallback arm, sincemembers[]doesn't yet contain the secretary) → binding 5 (not True) →False→ carve-out does not fire →bootstrap_gateDENIES the very spawn the carve-out exists to permit. Deadlock, because no marker exists yet on the bootstrap turn either.The load-bearing assumption is falsified by ground truth
The #1021 docstring justifying the inbox witness asserts:
This is false.
TaskUpdate(owner=...)is part of the choreography (Step 2) and the platform delivers atask_assignmentnotification to the assignee's inbox, creatinginboxes/secretary.jsonbefore Step 3's spawn.Reproduction
TaskCreatesecretary task →TaskUpdate(owner="secretary")→Agent(name="secretary", subagent_type="pact-secretary").Agentcall is denied:PACT bootstrap required. Invoke Skill("PACT:bootstrap") first...(the canonical_DENY_REASON).Evidence (from
session-b6dc25b7, before the manual workaround)inboxes/secretary.jsonheld atask_assignmentfor task Claude Code: not strictly follow framework #1,"assignedBy":"team-lead", ts2026-06-24T02:16:59Z— created byTaskUpdate(owner), with no prior successful spawn._team_has_secretary('session-b6dc25b7')→True, whilemembers[]was['team-lead']only. TheTruecame purely from the inbox arm.Agent(name="secretary", subagent_type="pact-secretary")call was DENIED with the canonical bootstrap_DENY_REASON.rm inboxes/secretary.json→ predicate flipped toFalse→ re-spawn succeeded (carve-out fired)._team_has_secretarywas members-only, the inbox was ignored, and this exact choreography worked.Why it slipped / why this is the first session to hit it
TaskUpdate(owner) → Agentsequence where the platform writes the inbox.session-b6dc25b7is the first fresh session to bootstrap under 4.4.39 — the first to exercise the regressed path.TaskUpdate(owner)writes the inbox in both in-process and tmux).Root cause
A single predicate was widened for consumer 1's correctness question ("was the secretary dispatched?") while consumer 2 needs a different question answered ("has the secretary actually joined the roster yet?"). Dispatch-witness (inbox) ≠ join-witness (
members[]). The carve-out needs a join witness; only the marker writer should accept the dispatch/inbox fallback.Fix direction (for plan-mode / orchestrate — not yet decided)
Decouple the carve-out from the marker writer's widened predicate. Candidate: give
bootstrap_gatebinding 5 amembers[]-only join witness (e.g. a distinct_team_has_secretary_member()or an inline_iter_memberscheck), leaving_team_has_secretary's inbox fallback for the marker writer alone. This restores pre-#1019 carve-out semantics under CLI while preserving #1019's Desktop marker fix.Interactions to weigh during design:
members[]is always empty → a members-only carve-out would "always allow secretary spawn pre-marker." Need to confirm that's acceptable (the marker write closes the window; spawn is one-shot in practice) and doesn't lose a needed one-shot guarantee.get_team_nameshort-circuit,test_empty_ssot_team_fails_closed_both_modes) must remain intact.Secondary concern (corollary — may warrant its own issue)
Because the premature inbox satisfies the marker writer too, on the next user prompt the marker writer can write a "bootstrap-complete" marker for a secretary that was only task-assigned, never spawned/joined. The inbox witness here proves task assignment, which is weaker than even "dispatched." If the
Agent(secretary)call is denied and never retried, the gate unblocks (marker present) with no live secretary — memory queries would then hang on an absent secretary (surfaced as a stuckSendMessagereply, not silent corruption, per the liveness contract). Worth deciding whether the marker writer's inbox witness should exclude task-assignment-only inboxes.Non-vacuity note
A regression test must run the real
TaskUpdate(owner) → Agentsequence (or a faithful fixture that creates the inbox via task-assignment), not a synthetic pre-placed inbox — the synthetic shortcut is exactly what let this slip through #1019's verification.Source: PR #1021 / commit e371ac9 / v4.4.39. Distinct from #1019 (Desktop config-less deadlock) and #1020 (silent HANDOFF-harvest loss).