Skip to content

docs(wish): perfect-spawn-hierarchy — plan ghost-approval deadlock fix - #1133

Merged
namastex888 merged 1 commit into
devfrom
feat/perfect-spawn-hierarchy
Apr 10, 2026
Merged

namastex888 merged 1 commit into
devfrom
feat/perfect-spawn-hierarchy

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

Authoritative plan for our worst active bug: native-team spawns persist leadSessionId: "pending", permission_requests pile up in unreadable inboxes (44 unanswered since 2026-03-26), teammates see "The user doesn't want to proceed with this tool use" and silently give up.

This PR is a DRAFT wish — no code changes. Awaiting /review plan-gate approval before /work dispatches execution groups.

What this wish delivers

  • Kill every 'pending' literal in production spawn paths (team-auto-spawn.ts:144, session.ts:77)
  • New helper resolveSpawnerSessionId(cwd) — single source of truth for caller's real Claude Code session ID. Fail loud, no fake "genie-${team}" fallbacks.
  • Spawner-as-leader in every spawn path (genie spawn, team-auto-spawn, session, protocol-router-spawn) — the caller automatically becomes the spawnee's team-lead at spawn time
  • Three-layer hierarchy via nearest-live-ancestor routing, so the master session isn't flooded when task-leads spawn their own underlings
  • genie doctor Team Health — 4 new checks (stale leadSessionId, inbox backlog, teammateMode, pane liveness) + --fix auto-remediation
  • Auto-approver daemon with trust-on-team-membership + audit log at ~/.genie/auto-approve.log
  • Migration backfill for existing teams with leadSessionId: "pending" (idempotent)
  • Regression tests incl. end-to-end 3-layer integration test + anti-regression lint sentinel

Why a wish (not a direct fix)

Third instance of this bug class in 8 days — commit 7c21301a6 (2026-04-02 teammateMode bypass), issue #1094 (2026-04-09 claude-sdk.ts spread override), and this new one all share the "ghost/self approval" symptom through different code paths. We keep patching individual leaks while the architecture leaks. This wish is the architecture fix.

Full trace + primary/secondary/tertiary root causes + 8 decisions with rationale are documented in the WISH.md.

Execution plan

3 waves, 8 groups — Wave 1 has 3 independent streams that can start in parallel (Groups 1, 4, 8). Waves 2-3 build on the foundation. Review gate between waves.

See .genie/wishes/perfect-spawn-hierarchy/WISH.md Execution Strategy section.

Related

Test plan

This PR only adds a markdown planning document — no code changes. CI should be green on the existing full suite (2436 tests).

  • Local bun run check — 2436 pass / 0 fail
  • WISH.md follows the /wish skill template
  • Scope OUT is explicit (7 exclusions)
  • Every execution group has acceptance criteria + validation command
  • /review plan-gate approval before moving to /work

Authoritative wish for our worst active bug — native-team spawns persist
`leadSessionId: "pending"`, permission_requests pile up in an unreadable
inbox (44 unanswered since 2026-03-26), teammates see "The user doesn't
want to proceed" and silently give up.

Plan covers:
- Kill every `'pending'` literal in production spawn paths
  (team-auto-spawn.ts:144, session.ts:77)
- New helper `resolveSpawnerSessionId(cwd)` — fail loud, no fake fallbacks
- Spawner-as-leader in every spawn path (genie spawn, team-auto-spawn,
  session, protocol-router-spawn)
- Three-layer hierarchy via nearest-live-ancestor lookup so master doesn't
  drown when task-leads spawn underlings
- `genie doctor` Team Health section (4 checks) + `--fix` auto-remediation
- Auto-approver daemon with trust-on-team-membership + audit log
- Migration backfill for existing "pending" teams
- Regression tests incl. end-to-end 3-layer test + anti-regression lint sentinel

Third instance of this bug class in 8 days (7c21301, #1094, this one).
Architecture fix, not another patch.

Status: DRAFT — pending /review plan gate.
@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8c06a715-19e2-4778-9f53-8140d9ed770e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/perfect-spawn-hierarchy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@namastex888
namastex888 merged commit b3b6fd0 into dev Apr 10, 2026
5 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive plan to resolve the 'Ghost-Approval Deadlock' in Claude Code by establishing a robust three-layer spawn hierarchy (master → task-lead → underlings). The changes involve eliminating 'pending' session placeholders, implementing a real-time parent session resolution helper, and adding an auto-approver daemon for non-TUI leaders. Additionally, it enhances 'genie doctor' with team health checks and automated remediation. Feedback focuses on correcting the environment variable precedence in session resolution to ensure correct hierarchy routing, clarifying the relationship between new and existing team configuration fields, and suggesting liveness checks for session heuristics to prevent selecting crashed sessions.

- Priority 3: `resolveSpawnerSessionId()` (the new helper).
- Priority 4: **throw** — no fake string fallback.
3. `GENIE_PARENT_SESSION_ID` env var is set on every spawned subprocess (`buildTeamLeadCommand`, `buildSpawnParams`, and any other launcher) so grandchildren can read it for their own resolution.
4. `resolveSpawnerSessionId()` checks `GENIE_PARENT_SESSION_ID` before it checks `CLAUDE_CODE_SESSION_ID` — this is how grandchildren find their direct parent instead of the root.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a logic error in the proposed resolution order. If resolveSpawnerSessionId checks GENIE_PARENT_SESSION_ID before CLAUDE_CODE_SESSION_ID, a task-lead (which has both env vars) will return its own parent's ID instead of its own. This would cause the spawnee to route to the master session, defeating the "three-layer hierarchy" goal where underlings should route to their immediate task-lead. CLAUDE_CODE_SESSION_ID must take precedence to ensure the immediate spawner becomes the parent.

Suggested change
4. `resolveSpawnerSessionId()` checks `GENIE_PARENT_SESSION_ID` before it checks `CLAUDE_CODE_SESSION_ID` — this is how grandchildren find their direct parent instead of the root.
4. resolveSpawnerSessionId() checks CLAUDE_CODE_SESSION_ID before it checks GENIE_PARENT_SESSION_ID — this is how grandchildren find their direct parent (the task-lead) instead of the root master session.

**Goal:** Make every production spawn path resolve a real session ID before calling `ensureNativeTeam()`. Eliminate the `'pending'` literal.

**Deliverables:**
1. New exported function `resolveSpawnerSessionId(cwd?: string): Promise<string>` in `src/lib/claude-native-teams.ts`. Strategy: `CLAUDE_CODE_SESSION_ID` env var → newest `.jsonl` in `~/.claude/projects/<sanitized-cwd>/` → throw `GhostLeaderError` with an actionable message. (Do NOT fall back to `"genie-${team}"` or any other synthetic string.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The strategy for resolveSpawnerSessionId relies on the "newest" .jsonl file. However, if the newest session is dead (e.g., crashed) and an older session is still alive, this heuristic will pick the dead one, potentially leading to another ghost-approval deadlock. Consider adding a liveness check (e.g., verifying the associated tmux pane is still active) before selecting the session ID.

**Deliverables:**
1. When the inbox-watcher or any process resolves "who is the team-lead for this team", it walks the hierarchy by reading team configs and finding the nearest ancestor whose session is still alive (JSONL exists and was modified recently, AND its tmux pane is alive per `isPaneAlive`).
2. New helper `resolveNearestLiveAncestor(childSessionId: string): Promise<string>` in `src/lib/claude-native-teams.ts`. Walks `parentSessionId` → `grandparentSessionId` until it finds one that's alive. Throws if none are.
3. Team config schema gains an optional `taskLeadSessionId` field. When set, the inbox-watcher treats THAT as the effective leader for the team, not the master. Set by the spawn layer whenever a task-lead is introduced (i.e. whenever a spawned worker itself spawns workers).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Group 3 introduces taskLeadSessionId, but Group 2 and the existing TeamConfig (in src/lib/team-manager.ts) use nativeTeamParentSessionId. It is unclear if these are intended to be distinct fields or if taskLeadSessionId is a rename. Clarifying the schema change here will prevent implementation drift during Wave 2.

Suggested change
3. Team config schema gains an optional `taskLeadSessionId` field. When set, the inbox-watcher treats THAT as the effective leader for the team, not the master. Set by the spawn layer whenever a task-lead is introduced (i.e. whenever a spawned worker itself spawns workers).
3. Team config schema gains an optional taskLeadSessionId field (distinct from the root nativeTeamParentSessionId). When set, the inbox-watcher treats THAT as the effective leader for the team, not the master. Set by the spawn layer whenever a task-lead is introduced (i.e. whenever a spawned worker itself spawns workers).

4. **Default-on for non-TUI leaders.** A leader is "non-TUI" if its session JSONL does NOT contain a `custom-title` entry or its agent-type is not `"team-lead"`. TUI sessions surface prompts to the user via Claude Code's built-in UI and must not be overridden.
5. **Config toggle:** `genie config set autoApprove.enabled false` disables it globally. Default: `true`.
6. **Audit log** at `~/.genie/auto-approve.log` — one line per decision: timestamp, team, request_id, agent_id, decision (approved/skipped), reason.
7. **Rate limit:** no more than 100 approvals per minute per team (guardrail against runaway loops).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The rate limit of 100 approvals per minute is a good safety measure, but the plan doesn't specify where the rate-limit state is persisted. If it is purely in-memory, a daemon restart or multiple watchers could bypass it. Consider specifying if this should be persisted or if in-memory is sufficient for the expected failure modes.

Suggested change
7. **Rate limit:** no more than 100 approvals per minute per team (guardrail against runaway loops).
7. Rate limit: no more than 100 approvals per minute per team (in-memory counter; guardrail against runaway loops).

namastex888 added a commit that referenced this pull request Apr 11, 2026
docs(wish): perfect-spawn-hierarchy — plan ghost-approval deadlock fix
namastex888 pushed a commit that referenced this pull request Apr 12, 2026
All six wishes have their deliverables merged to dev via the PRs listed
below. Moving them from active tracking into _archive/ keeps the
wish directory scoped to in-flight and draft work.

| Wish                        | Shipped via            |
|-----------------------------|------------------------|
| velocity-dashboard          | #1142 + #1143 (hotfix) |
| fix-ghost-approval-p0       | #1134                  |
| perfect-spawn-hierarchy     | #1133                  |
| brain-help-passthrough      | #1129                  |
| scaffold-auto-memory        | #1128                  |
| unify-bridge-revamp-skills  | #1125 + #1126 + #1127  |

No code changes — purely a directory reorganization. Wish artifacts
remain readable at .genie/wishes/_archive/<slug>/WISH.md for historical
reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
automagik-genie pushed a commit that referenced this pull request Apr 17, 2026
Adds an optional `team` field to `DirectoryEntry` and populates it in
`resolve()` via a direct lookup against the `agent_templates` PG row.
This makes the template-pinned team available to spawn-time resolvers
without requiring them to re-query the templates table themselves.

This is tier 2 of the tui-spawn-dx Group 1 four-tier team-resolution
precedence. `agent.entry.team` is an authoritative PG lookup — NOT a
synthetic tmux/env fallback — so it does not violate the UUID-first
principle established by the perfect-spawn-hierarchy wish (PR #1133
merge 8a78346, PR #1134 merge 6921574, commits 6389f39, d44a6b9,
0b98bec, 4100d29).

Refs: tui-spawn-dx (Group 1)
automagik-genie pushed a commit that referenced this pull request Apr 17, 2026
…AndResume

Replaces the two-tier fallback (`options.team || discoverTeamName()`)
with an explicit four-tier precedence that restores the canonical-UUID-
per-agent invariant when spawning from a tmux session that does not
match the agent's home team.

Precedence (highest wins):
  1. `options.team` — the `--team` flag. Only tier that flips
     `teamWasExplicit` to true.
  2. `agent.entry?.team` — template-pinned team from the
     `agent_templates` PG row. Authoritative PG lookup, NOT a synthetic
     tmux/env fallback.
  3. `process.env.GENIE_TEAM` — session-scoped env var (short-circuit
     before the heavier discovery path).
  4. `await discoverTeamName()` — retained PR #1164 fallback (JSONL
     leadSessionId match → tmux session name). Fires only when every
     higher-priority tier is empty.

Extracts the pure precedence logic into `resolveTeamName(opts)` and
makes it exported for unit testing. `resolveTeamAndResume` now takes
the resolved agent as a parameter so `agent.entry.team` is in scope.

Reproducer now fixed: running `genie spawn simone` from inside the
`genie` tmux session (where `discoverTeamName` would return 'genie'
via PR #1164's tmux fallback) now resolves to `simone` because the
template pins it. Full regression test covers this case.

Authority: perfect-spawn-hierarchy wish — PR #1133 merge 8a78346,
PR #1134 merge 6921574 (commits 6389f39 kill-pending,
d44a6b9 remove-hardcoded-genie-from-team-flag,
0b98bec remove-genie-literal-from-dispatch,
4100d29 remove-genie-literal-from-resume).

Refs: tui-spawn-dx (Group 1)
automagik-genie pushed a commit that referenced this pull request Apr 17, 2026
Adds the three-branch state machine that gates canonical vs parallel row
creation based on pane liveness — no new flags, semantics are invisible to
the caller.

State machine (in handleWorkerSpawn, after resolveTeamAndResume):
  - No row with id=<name>   → create canonical (workerId=<name>)
  - Alive canonical         → create parallel (workerId=<name>-<sN>)
  - Dead canonical          → findDeadResumable handles it (unchanged)

Parallels mint their OWN fresh Claude session UUID and derive a
deterministic short-id (<sN>) from that UUID's hex prefix — never a random
or minted token. On collision, the slice extends one char at a time
(s5, s6, …) of the SAME UUID until unique. This keeps row ids in lockstep
with session UUIDs and makes parallels resumable only by their full id.

Canonical rows are never clobbered by parallel creation: parallels get
their own row with their own UUID, preserving the perfect-spawn-hierarchy
canonical-UUID invariant (PR #1133/#1134 merge 6921574).

Helpers introduced (exported for unit testing):
  - resolveSpawnIdentity(name, team, uuidFactory?, isAliveFn?)
  - pickParallelShortId(baseName, team, uuid) — rejects non-UUID input
  - findDeadResumable now exported (for off-auto-resume-path tests)

rejectDuplicateRole is retained for the legacy explicit --role path;
for single-verb spawns it no longer fires (alive collision = parallel).

Authority: tui-spawn-dx wish, Group 2. Builds on Wave 1 (merge 9321dd6,
commits 79cbe06 + 898c219).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant