@automagik/genie 4.260410.3 → 4.260410.4

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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "genie",
13
- "version": "4.260410.3",
13
+ "version": "4.260410.4",
14
14
  "source": "./plugins/genie",
15
15
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
16
16
  }
@@ -0,0 +1,423 @@
1
+ # Wish: Perfect Spawn Hierarchy — End the Ghost-Approval Deadlock
2
+
3
+ | Field | Value |
4
+ |-------|-------|
5
+ | **Status** | DRAFT |
6
+ | **Slug** | `perfect-spawn-hierarchy` |
7
+ | **Date** | 2026-04-10 |
8
+ | **Priority** | P0 — worst active bug in the product |
9
+ | **Trace** | See Context section below |
10
+ | **Related** | Commit `7c21301a6` (2026-04-02 partial fix), Issue #1094 (2026-04-09 SDK-path fix) |
11
+
12
+ ## Summary
13
+
14
+ Every time a teammate in a native team tries to write a new file at its project cwd root, Claude Code's permission gate routes the request to `~/.claude/teams/<team>/inboxes/team-lead.json` — and because two of the three team-spawn paths create the team with `leadSessionId: "pending"` (a literal placeholder that nothing ever reconciles), the request lands on a ghost leader and never gets a response. The teammate sees `"The user doesn't want to proceed with this tool use"` and silently gives up. 44 unanswered requests have piled up in a single inbox since 2026-03-26. This wish kills the class of bug by making **every spawn establish a real parent session at the moment of spawn**, enforcing a three-layer hierarchy (master → task-lead → underlings) so approval requests always route to a live ancestor, and teaching `genie doctor` to catch the next variant before users do.
15
+
16
+ ## Context — What We Know (from the 2026-04-10 trace)
17
+
18
+ **Rejected Write that triggered the investigation:**
19
+ ```
20
+ /home/genie/.claude/teams/genie/inboxes/team-lead.json:15469
21
+ → {"type":"permission_request","request_id":"perm-1775838084154-39n5jxl",
22
+ "agent_id":"genie","tool_name":"Write",
23
+ "input":{"file_path":"/home/genie/workspace/agents/genie/.gitmodules",...}}
24
+ ```
25
+ No corresponding `permission_response` exists. Inbox-wide: **44 permission_requests, 1 permission_response** (the response is from 2026-03-26).
26
+
27
+ **Primary root cause — two code sites still hardcode `'pending'`:**
28
+ - `src/lib/team-auto-spawn.ts:144` — Omni recovery path: `ensureNativeTeam(teamName, ..., 'pending', leaderName)`
29
+ - `src/genie-commands/session.ts:77` — interactive session path: `ensureNativeTeam(teamName, ..., 'pending', leaderName)` with a comment that claims "CC updates it internally once started" — **this claim is false**; nothing updates it.
30
+
31
+ **The third path already does it right** — `src/lib/protocol-router-spawn.ts:269-270` resolves a real `parentSessionId` via `resolveParentSession()` (line 43-47). But even that helper has a fallback to `"genie-${team}"` (line 46) — a fake string that won't route either.
32
+
33
+ **The fix that shipped on 2026-04-02** (commit `7c21301a6`) added `ensureTeammateBypassPermissions()` in `src/lib/claude-settings.ts:63` to force the global `teammateMode: "bypassPermissions"`. It closed one escape path. The commit message literally calls the failure "deadlock where subagents hang forever". But my global settings confirm `teammateMode: "bypassPermissions"` is set — and the Write to `.gitmodules` **still routed to the ghost leader**. Claude Code has at least one write-gate code path that ignores the global bypass (exact path unknown — the gate lives in the closed-source `claude` binary).
34
+
35
+ **Issue #1094** (closed 2026-04-09, yesterday) fixed the `claude-sdk.ts:183-196` spread-override bug: `...translatedSdk` was clobbering `permissionMode: 'bypassPermissions'`. Different code path, same user-visible symptom: "The agent gets stuck in a self-approval loop."
36
+
37
+ **Three instances of the same class of bug in 8 days.** We keep patching individual leaks while the architecture leaks. The durable fix is to make the parent chain unbreakable and stop trusting Claude Code's internal bypass.
38
+
39
+ **`genie doctor` is blind to all of this.** `src/genie-commands/doctor.ts` checks tmux/jq/bun/claude prereqs, genie config existence, Claude settings existence, tmux server, worker profiles, and the Omni bridge. **Zero team-health checks.** No `leadSessionId === "pending"` detector. No inbox backlog check. No pane liveness check. No `teammateMode` verifier. Users hit this bug, run `genie doctor`, see "All checks passed!", and lose trust.
40
+
41
+ ## Scope
42
+
43
+ ### IN
44
+
45
+ - Eliminate every `'pending'` literal passed to `ensureNativeTeam()` in the production spawn paths.
46
+ - Establish a single helper — `resolveSpawnerSessionId(cwd)` — that every spawn path uses to get a real Claude Code session ID for the CURRENT caller. Strategy: `CLAUDE_CODE_SESSION_ID` env var → newest JSONL in `~/.claude/projects/<sanitized-cwd>/` → **fail loud** (do not silently fall back to fake strings).
47
+ - `genie spawn` (CLI and internal callers) always registers the caller's session ID as the spawnee's `parentSessionId`. If the caller is running inside Claude Code, the spawner becomes the team-lead for the spawnee **at the moment of spawn**.
48
+ - Three-layer hierarchy enforcement: every spawn uses the NEAREST live ancestor as `parentSessionId`, not the root. When a task-lead (a worker that was itself spawned) spawns its own underlings, those underlings route to the task-lead — not to the master. The master session only receives permission requests from its direct children.
49
+ - Auto-approver daemon: the inbox watcher gains a "permission responder" component that, when the team-lead is an automated (non-TUI) Claude session, auto-emits `permission_response {subtype: "success"}` for any `permission_request` whose `agent_id` is a known member of the team. Trust-on-team-membership.
50
+ - `genie doctor` gains a **Team Health** section with four checks: (1) stale `leadSessionId`, (2) inbox permission-request backlog, (3) `teammateMode` verifier, (4) team-lead pane liveness.
51
+ - `genie doctor --fix` extends to auto-remediate: respawn missing team-leads, patch stale `leadSessionId`, drain stuck inbox backlog by issuing synthetic `permission_response` entries (with audit log).
52
+ - Migration script: one-shot backfill for existing teams with `leadSessionId: "pending"` on machines that upgrade to the new version. Runs from `genie setup` and `genie doctor --fix`.
53
+ - Regression tests: unit tests for `resolveSpawnerSessionId`, `resolveParentSession` hardened path, the auto-approver's trust-on-membership logic, doctor checks, and the migration script. Integration test that spawns a three-layer hierarchy and writes a new-file-at-cwd-root from a level-3 agent end-to-end on dev.
54
+ - Documentation: update `src/genie-commands/AGENTS.md` (if it exists) or the top-level docs with the hierarchy model, add a troubleshooting section to the docs describing the bug + fix, and cross-reference commit `7c21301a6` and issue #1094 so the pattern is recorded.
55
+
56
+ ### OUT
57
+
58
+ - **Fixing Claude Code's internal write-gate.** The exact path inside the `claude` binary that routes writes-at-cwd-root through the team inbox despite `bypassPermissions` is closed-source and out of our control. This wish works *around* it by ensuring the leader is always real and always responsive.
59
+ - **Removing native-teams support entirely.** Some folks have considered "just disable it for non-TUI leaders". This wish preserves native teams and makes them actually work, rather than ripping them out.
60
+ - **Rewriting the permission-request inbox protocol.** We stick with the current JSON-append-with-read-flag format.
61
+ - **Changing the SendMessage-based in-process communication.** Different subsystem, not part of this fix.
62
+ - **Fixing the related `claude-sdk.ts` spread order.** Already fixed in #1094.
63
+ - **Adding per-request approval UX to the TUI.** Out of scope — the auto-approver handles the daemon case; TUI leaders already surface prompts interactively.
64
+ - **Backporting to versions older than the currently-supported release.** We fix forward on `dev`.
65
+
66
+ ## Decisions
67
+
68
+ | Decision | Rationale |
69
+ |----------|-----------|
70
+ | **Every spawn resolves `parentSessionId` at spawn time — no deferred "CC will update it" assumption.** | The deferred model is the exact assumption that broke production. The comment at `session.ts:63` literally says "CC updates it internally once started" and that's been wrong for weeks. Resolve eagerly, fail loudly if we can't. |
71
+ | **Trust-on-team-membership for the auto-approver.** | If a request's `agent_id` matches a member in the team config, it's trusted. Alternative (require a signed token per member) is overkill for a local-process setup and delays shipping. |
72
+ | **Three-layer hierarchy via nearest-live-ancestor lookup, not a recorded tree.** | A recorded tree would need migration, persistence, and garbage-collection. Nearest-live-ancestor is computed at spawn time from the current process env, is stateless, and naturally handles death/respawn. |
73
+ | **`genie doctor` gains a Team Health section, not a new `genie teams doctor` subcommand.** | Users already run `genie doctor` when things break. Adding a new subcommand means they won't find it. Put the checks where users look. |
74
+ | **Auto-approver writes a `permission_response` with `subtype: "success"` — does not try to "grant" via the Claude Code API.** | The Claude Code side only reads `permission_response` entries from the inbox. Writing one is the same mechanism the TUI leader already uses (see inbox evidence: one real response from 2026-03-26 used exactly this shape). |
75
+ | **Migration is read-only scan + write-replace, not a schema version bump.** | The team config file is JSON with a flat `leadSessionId` field. Replacing `"pending"` with a real value is a string-level edit. No schema bump needed. |
76
+ | **Fail loud on `resolveSpawnerSessionId()` returning null.** | Silent fallback to `"genie-${team}"` (current behavior at `protocol-router-spawn.ts:46`) is exactly how we got here. If we can't find a real session, throw — the caller either passes a session ID explicitly or the CLI exits with an actionable error. |
77
+ | **Do not remove the `'pending'` literal from the test file.** | `src/lib/team-auto-spawn.test.ts:58` uses `'pending'` as fixture data that exercises the "config-exists-but-not-hydrated" detection. Keep the fixture, fix the production sites. |
78
+
79
+ ## Success Criteria
80
+
81
+ - [ ] **No production code path passes the literal `'pending'` to `ensureNativeTeam()`.** Grep `src/` excluding tests for `ensureNativeTeam.*pending` returns zero matches.
82
+ - [ ] **`resolveSpawnerSessionId(cwd)` exists** as a single exported helper in `src/lib/claude-native-teams.ts` and is the only way any spawn path looks up the caller's session ID. Throws on not-found.
83
+ - [ ] **`genie spawn <role>` sets the spawnee's `parentSessionId`** to the caller's real Claude Code session ID (verified by reading `~/.claude/teams/<team>/config.json` after the spawn and checking it's a UUID, not `"pending"` and not `"genie-<team>"`).
84
+ - [ ] **Three-layer hierarchy works.** Integration test: spawn agent A from a master session, spawn agent B from A, spawn agent C from B. Then have C attempt a Write to a new cwd-root file. The permission request must land in **B's** inbox, not A's and not the master's. B's auto-approver must respond within 2 seconds and C's write must succeed.
85
+ - [ ] **Auto-approver ships and is on by default** for non-TUI leaders, runs as part of the inbox watcher, and writes a `permission_response` for each `permission_request` it can validate as coming from a known team member. An audit log is written to `~/.genie/auto-approve.log` with one line per decision.
86
+ - [ ] **`genie doctor` reports Team Health.** Running `genie doctor` on a machine with a team config that has `leadSessionId: "pending"` produces a FAIL line with the exact team name and a one-line fix suggestion. Running it on a machine with a backlog of 10+ unanswered requests produces a FAIL line. Running it without `teammateMode: "bypassPermissions"` in `~/.claude/settings.json` produces a FAIL line.
87
+ - [ ] **`genie doctor --fix` auto-remediates.** Running on a machine with the exact state we see on 2026-04-10 (leadSessionId pending, 44-entry backlog, teammateMode set, pane alive) must: (a) patch `leadSessionId` to a real session ID if one can be discovered, (b) drain the backlog by writing synthetic `permission_response` entries, (c) log every action to `~/.genie/doctor-fix.log`.
88
+ - [ ] **Migration runs once on upgrade** and logs a summary: `[migrate] perfect-spawn-hierarchy: patched N teams, drained M requests`. Idempotent — running a second time is a no-op with summary `0 teams, 0 requests`.
89
+ - [ ] **`bun test` passes with no regressions** and includes:
90
+ - `src/lib/claude-native-teams.test.ts` — new tests for `resolveSpawnerSessionId`.
91
+ - `src/lib/team-auto-spawn.test.ts` — updated to assert no `'pending'` literal is written.
92
+ - `src/lib/inbox-watcher.test.ts` — new tests for the auto-approver.
93
+ - `src/genie-commands/doctor.test.ts` — new file, covers the four Team Health checks.
94
+ - `src/lib/spawn-hierarchy.integration.test.ts` — new file, end-to-end three-layer test (may be marked `.skip` if tmux isn't available in CI).
95
+ - [ ] **`bun run typecheck` is clean.**
96
+ - [ ] **`bun run lint` is clean** (or worsens no further than baseline, with justification if it does).
97
+ - [ ] **`genie doctor` exit code is 0** on a machine in the fixed state.
98
+ - [ ] **Docs updated.** A troubleshooting section exists in the README or docs/ that explains the bug, the fix, and cross-references `7c21301a6` and #1094.
99
+
100
+ ## Execution Strategy
101
+
102
+ Three waves. Wave 1 is parallel foundation work. Wave 2 builds on Wave 1. Wave 3 is the daemon + tests + migration. Review gates between waves.
103
+
104
+ ### Wave 1 (parallel — 3 independent streams)
105
+
106
+ | Group | Agent | Description |
107
+ |-------|-------|-------------|
108
+ | 1 | engineer | Kill the `'pending'` literal in `team-auto-spawn.ts:144` + `session.ts:77`. Add `resolveSpawnerSessionId()` helper. Harden `resolveParentSession()` to never return `"genie-<team>"`. |
109
+ | 4 | engineer | Add **Team Health** section to `genie doctor` with the four checks (stale leadSessionId, inbox backlog, teammateMode, pane liveness). Read-only — no fix wiring yet. |
110
+ | 8 | engineer | Docs: hierarchy model write-up, troubleshooting section, cross-references to prior fixes. Can start immediately, no code dependencies. |
111
+
112
+ ### Wave 2 (after Wave 1 review)
113
+
114
+ | Group | Agent | Description |
115
+ |-------|-------|-------------|
116
+ | 2 | engineer | `genie spawn` wires the spawner's session ID as `parentSessionId`. Every spawn path (session.ts, protocol-router-spawn.ts, team-auto-spawn.ts) calls the single helper from Group 1. Hard-fail if no session ID can be resolved. |
117
+ | 3 | engineer | Three-layer hierarchy: when a spawned worker itself spawns, the nearest-live-ancestor (its own session) becomes the parent. Verify via env propagation (`GENIE_PARENT_SESSION_ID`) and filesystem introspection. |
118
+ | 5 | engineer | Migration + `genie doctor --fix` auto-remediation. Runs the backfill for stale configs and drains the inbox backlog with synthetic responses. Must be idempotent. |
119
+
120
+ ### Wave 3 (after Wave 2 review)
121
+
122
+ | Group | Agent | Description |
123
+ |-------|-------|-------------|
124
+ | 6 | engineer | Auto-approver daemon integrated into inbox-watcher. Trust-on-team-membership. Audit log at `~/.genie/auto-approve.log`. Default-on for non-TUI leaders. |
125
+ | 7 | qa | Regression tests — unit for every new helper + integration test that spawns a 3-layer hierarchy and writes a new cwd-root file successfully. |
126
+ | review | reviewer | Review Groups 1-7 against acceptance criteria. Verify the exact reproducer from the 2026-04-10 trace (writing `.gitmodules` at cwd root from a teammate) now succeeds on dev. |
127
+
128
+ ## Execution Groups
129
+
130
+ ### Group 1: Kill `'pending'` + introduce `resolveSpawnerSessionId()`
131
+
132
+ **Goal:** Make every production spawn path resolve a real session ID before calling `ensureNativeTeam()`. Eliminate the `'pending'` literal.
133
+
134
+ **Deliverables:**
135
+ 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.)
136
+ 2. `src/lib/team-auto-spawn.ts:144` — replace `'pending'` with `await resolveSpawnerSessionId(workingDir)`. Wrap in try/catch; on failure, log clearly and propagate — do not silently continue with a broken team.
137
+ 3. `src/genie-commands/session.ts:77` — same replacement. Update the comment on line 63 that currently claims "CC updates it internally once started" (it doesn't) to instead reference this wish + the real resolution flow.
138
+ 4. `src/lib/protocol-router-spawn.ts:46` — remove the `?? \`genie-${team}\`` fallback. Throw instead. The ONLY legal return is a real session UUID.
139
+ 5. `src/lib/team-manager.ts:332-336` — stop silently swallowing `registerAsTeamLead` errors. Log to stderr with `[team]` prefix. Still non-blocking, but visible.
140
+
141
+ **Acceptance Criteria:**
142
+ - [ ] `grep -rn "'pending'" src/lib/team-auto-spawn.ts src/genie-commands/session.ts src/lib/protocol-router-spawn.ts` returns zero matches.
143
+ - [ ] `resolveSpawnerSessionId()` exists, is exported, has at least 4 unit tests (env var hit, filesystem hit, both miss, malformed jsonl).
144
+ - [ ] Unit test asserts `GhostLeaderError` is thrown when nothing resolves, with a message containing "CLAUDE_CODE_SESSION_ID" and a link to this wish.
145
+ - [ ] `bun test src/lib/claude-native-teams.test.ts src/lib/team-auto-spawn.test.ts` passes.
146
+ - [ ] `bun run typecheck` clean.
147
+
148
+ **Validation:**
149
+ ```bash
150
+ cd /path/to/genie && \
151
+ bun run typecheck && \
152
+ bun test src/lib/claude-native-teams.test.ts src/lib/team-auto-spawn.test.ts && \
153
+ ! grep -rn "'pending'" src/lib/team-auto-spawn.ts src/genie-commands/session.ts src/lib/protocol-router-spawn.ts
154
+ ```
155
+
156
+ **depends-on:** none
157
+
158
+ ---
159
+
160
+ ### Group 2: Spawner-as-leader in every spawn path
161
+
162
+ **Goal:** Every spawn (CLI, TUI, inbox-watcher recovery, protocol router) establishes the caller's session as the spawnee's parent at the moment of spawn.
163
+
164
+ **Deliverables:**
165
+ 1. `genie spawn <role>` (entry in `src/genie-commands/*`) calls `resolveSpawnerSessionId(process.cwd())` at the top and passes the result through to whichever backend spawn function it uses (likely `spawnWorkerFromTemplate`).
166
+ 2. `resolveParentSession()` in `src/lib/protocol-router-spawn.ts:43-47` is refactored:
167
+ - Priority 1: explicit `parentSessionId` argument (passed from the CLI layer).
168
+ - Priority 2: stored `nativeTeamParentSessionId` in team config.
169
+ - Priority 3: `resolveSpawnerSessionId()` (the new helper).
170
+ - Priority 4: **throw** — no fake string fallback.
171
+ 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.
172
+ 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.
173
+
174
+ **Acceptance Criteria:**
175
+ - [ ] Integration test: spawn a worker from a test harness; read the resulting team config; `leadSessionId` must match the harness's `CLAUDE_CODE_SESSION_ID` (or, in a mock, the injected value).
176
+ - [ ] Integration test: spawn-within-spawn — spawner S1 spawns S2 which spawns S3. S3's team config `leadSessionId` must equal S2's session ID, **not** S1's.
177
+ - [ ] `resolveParentSession` has zero occurrences of `genie-${team}` or any other synthetic fallback.
178
+ - [ ] `GENIE_PARENT_SESSION_ID` is documented in `src/lib/team-lead-command.ts` (or equivalent) as part of the launcher env.
179
+
180
+ **Validation:**
181
+ ```bash
182
+ bun test src/lib/protocol-router-spawn.test.ts src/lib/spawn-hierarchy.integration.test.ts
183
+ bun run typecheck
184
+ ```
185
+
186
+ **depends-on:** Group 1
187
+
188
+ ---
189
+
190
+ ### Group 3: Three-layer hierarchy — nearest-live-ancestor routing
191
+
192
+ **Goal:** When a task-lead (a worker that was itself spawned by the master) spawns its own underlings, those underlings route to the task-lead, not the master. The master receives only requests from its direct children.
193
+
194
+ **Deliverables:**
195
+ 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`).
196
+ 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.
197
+ 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).
198
+ 4. Permission-request routing respects `taskLeadSessionId` — requests from that task-lead's subtree route to the task-lead's inbox, not the master's.
199
+ 5. If a task-lead dies, its subtree falls back to the nearest live ancestor (the master, eventually).
200
+
201
+ **Acceptance Criteria:**
202
+ - [ ] Three-layer integration test passes: master M spawns task-lead T spawns underling U. U issues a permission request. The request lands in T's inbox (not M's). T auto-approves. U's tool call succeeds.
203
+ - [ ] Kill-the-task-lead test: after T dies, U's next permission request falls back to M and M auto-approves. No request is ever lost.
204
+ - [ ] `resolveNearestLiveAncestor` has unit tests for all branches (direct parent alive, parent dead and grandparent alive, all dead → throw).
205
+
206
+ **Validation:**
207
+ ```bash
208
+ bun test src/lib/claude-native-teams.test.ts src/lib/spawn-hierarchy.integration.test.ts
209
+ bun run typecheck
210
+ ```
211
+
212
+ **depends-on:** Group 2
213
+
214
+ ---
215
+
216
+ ### Group 4: `genie doctor` Team Health checks
217
+
218
+ **Goal:** Give users (and agents) a single command that catches this class of bug in 10 seconds instead of 15 days.
219
+
220
+ **Deliverables:**
221
+ 1. New `checkTeamHealth()` function in `src/genie-commands/doctor.ts` returning `CheckResult[]`.
222
+ 2. **Check 1 — Stale leadSessionId:** walk `~/.claude/teams/*/config.json`. For each team with `leadSessionId === "pending"` or a value that doesn't correspond to an existing `.jsonl` file, FAIL with `Team '<name>' has a ghost leader. Fix: genie doctor --fix or genie team respawn-lead <name>`.
223
+ 3. **Check 2 — Inbox backlog:** for each team, count `permission_request` entries minus `permission_response` entries in `inboxes/team-lead.json`. If (requests − responses) > 5 AND the oldest unanswered request is >30 minutes old, FAIL with `Team '<name>' has N unanswered permission_requests (oldest: X min). Leader is probably dead or unresponsive.`
224
+ 4. **Check 3 — teammateMode verifier:** read `~/.claude/settings.json`. If `teammateMode !== "bypassPermissions"` while native teams are in use, FAIL with the suggestion to run `genie setup` or the specific jq command to fix it.
225
+ 5. **Check 4 — Team-lead pane liveness:** for each team with native-teams enabled, check if the team-lead's tmux pane is alive. If not, WARN (not FAIL — might just be a closed window) with `genie doctor --fix` as the remediation.
226
+ 6. Wire `checkTeamHealth` into `doctorCommand()` as a new section.
227
+ 7. This group is **read-only** — it only detects. Remediation is Group 5.
228
+
229
+ **Acceptance Criteria:**
230
+ - [ ] Running `bun run src/bin.ts doctor` on my current machine (team config has `leadSessionId: "pending"`, 44 unanswered requests) produces four distinct FAIL lines in a Team Health section.
231
+ - [ ] `checkTeamHealth` is covered by unit tests in `src/genie-commands/doctor.test.ts`, using tmp filesystems and mock team configs.
232
+ - [ ] Each check has a one-line suggestion that's a runnable command.
233
+
234
+ **Validation:**
235
+ ```bash
236
+ bun run typecheck
237
+ bun test src/genie-commands/doctor.test.ts
238
+ # Manual: bun run src/bin.ts doctor # must show Team Health section with FAILs
239
+ ```
240
+
241
+ **depends-on:** none
242
+
243
+ ---
244
+
245
+ ### Group 5: Migration + `genie doctor --fix` auto-remediation
246
+
247
+ **Goal:** Make `genie doctor --fix` idempotently repair the exact state we see on 2026-04-10: stale `leadSessionId: "pending"`, 44-entry inbox backlog.
248
+
249
+ **Deliverables:**
250
+ 1. Extend `doctorFix()` in `src/genie-commands/doctor.ts` with a new step `fixTeamHealth()`.
251
+ 2. **Patch stale leadSessionId:** for each team with `leadSessionId === "pending"`, call `resolveSpawnerSessionId()`. If one is found, write it to the config. If not, spawn a new team-lead via `ensureTeamLead()` (the same path Omni uses) — but NOW with the Group 1 fix so it writes a real session ID.
252
+ 3. **Drain inbox backlog:** for each unanswered `permission_request` in `inboxes/team-lead.json`, write a matching `permission_response` with `subtype: "success"` **only if** the request's `agent_id` is a known member of the team. For unknown agents, log a warning and leave the request in place (safer default).
253
+ 4. **Migration on upgrade:** add a one-shot migration `migrations/2026-04-10-perfect-spawn-hierarchy.ts` (or similar location — follow the existing migration pattern) that runs `fixTeamHealth()` once per machine, tracked via a marker file at `~/.genie/migrations/perfect-spawn-hierarchy.done`.
254
+ 5. **Audit log:** every change goes to `~/.genie/doctor-fix.log` with one line per action (timestamp, team, action, agent_id, result).
255
+ 6. Idempotency test: run the migration twice; second run must report `0 teams patched, 0 requests drained`.
256
+
257
+ **Acceptance Criteria:**
258
+ - [ ] Running `genie doctor --fix` on a machine matching the 2026-04-10 state successfully (a) patches leadSessionId or respawns, (b) drains the backlog, (c) writes the audit log. Subsequent `genie doctor` shows all Team Health checks PASS.
259
+ - [ ] The auto-drain only responds to requests from known members. A synthetic request with `agent_id: "unknown"` is left in place and logged.
260
+ - [ ] Migration marker prevents double-execution on boot. Unit test covers this.
261
+
262
+ **Validation:**
263
+ ```bash
264
+ bun run typecheck
265
+ bun test src/genie-commands/doctor.test.ts migrations/2026-04-10-perfect-spawn-hierarchy.test.ts
266
+ # Manual dry-run on a copy of current state.
267
+ ```
268
+
269
+ **depends-on:** Group 1, Group 4
270
+
271
+ ---
272
+
273
+ ### Group 6: Auto-approver daemon
274
+
275
+ **Goal:** Prevent future regressions by making the leader **actively responsive**, not passively pollable. When a non-TUI leader receives a permission request from a known team member, it responds automatically within seconds.
276
+
277
+ **Deliverables:**
278
+ 1. New component `src/lib/permission-auto-approver.ts` with an exported function `runAutoApprover(teamName, opts)`.
279
+ 2. Integrated into `src/lib/inbox-watcher.ts` — every poll cycle, the watcher scans `inboxes/team-lead.json` for unanswered `permission_request` entries (not just unread generic messages) and invokes the auto-approver.
280
+ 3. **Trust model:** the auto-approver compares `request.agent_id` against the team config's `members[]` list. If the agent is a known member, write a `permission_response` with `subtype: "success"` to the inbox. Otherwise skip and log.
281
+ 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.
282
+ 5. **Config toggle:** `genie config set autoApprove.enabled false` disables it globally. Default: `true`.
283
+ 6. **Audit log** at `~/.genie/auto-approve.log` — one line per decision: timestamp, team, request_id, agent_id, decision (approved/skipped), reason.
284
+ 7. **Rate limit:** no more than 100 approvals per minute per team (guardrail against runaway loops).
285
+
286
+ **Acceptance Criteria:**
287
+ - [ ] Unit tests cover: known-member-approved, unknown-member-skipped, rate-limited, config-disabled.
288
+ - [ ] Integration test: a request lands in an inbox, the auto-approver responds within 2 seconds, the response is well-formed and readable by whatever format Claude Code expects (match the shape of the one historical successful response at team-lead.json entry from 2026-03-26).
289
+ - [ ] Audit log entries are parseable and include all required fields.
290
+
291
+ **Validation:**
292
+ ```bash
293
+ bun run typecheck
294
+ bun test src/lib/permission-auto-approver.test.ts src/lib/inbox-watcher.test.ts
295
+ ```
296
+
297
+ **depends-on:** Group 2, Group 3
298
+
299
+ ---
300
+
301
+ ### Group 7: Regression tests — the reproducer must pass
302
+
303
+ **Goal:** Ensure the exact failure from 2026-04-10 can never recur silently. Lock it in with an end-to-end test.
304
+
305
+ **Deliverables:**
306
+ 1. `src/lib/spawn-hierarchy.integration.test.ts` — end-to-end test:
307
+ - Create a temporary team via the same API genie uses in production.
308
+ - Spawn two layers of agents.
309
+ - Have the level-3 agent write a **new file at its cwd root** (the exact failure mode from the trace).
310
+ - Assert the write succeeds, the team config has a real `leadSessionId`, the permission request landed in the correct inbox (Group 3's routing), and the auto-approver (Group 6) wrote a response within 2 seconds.
311
+ 2. `src/lib/claude-native-teams.test.ts` — expanded tests for `resolveSpawnerSessionId` and `resolveNearestLiveAncestor`.
312
+ 3. `src/genie-commands/doctor.test.ts` — tests for the four Team Health checks.
313
+ 4. `src/lib/permission-auto-approver.test.ts` — already delivered in Group 6; this group re-verifies edge cases + adds a load test.
314
+ 5. **Anti-regression sentinel:** a simple lint rule or grep-based CI check that fails the build if `'pending'` is reintroduced into any `ensureNativeTeam(` call site in production code. Script lives in `scripts/check-no-pending-literal.sh`.
315
+
316
+ **Acceptance Criteria:**
317
+ - [ ] All new tests pass locally and in CI.
318
+ - [ ] The lint sentinel fires on a deliberately-reintroduced `'pending'` in a throwaway branch (prove it works).
319
+ - [ ] `bun test` runs in under 3 minutes on CI.
320
+
321
+ **Validation:**
322
+ ```bash
323
+ bun test
324
+ bun run typecheck
325
+ bash scripts/check-no-pending-literal.sh # must exit 0
326
+ ```
327
+
328
+ **depends-on:** Group 1, Group 2, Group 3, Group 4, Group 5, Group 6
329
+
330
+ ---
331
+
332
+ ### Group 8: Docs + hierarchy model write-up
333
+
334
+ **Goal:** Document the hierarchy so the next engineer who touches this code doesn't reintroduce the bug.
335
+
336
+ **Deliverables:**
337
+ 1. New file `docs/architecture/spawn-hierarchy.md` (or similar — match existing doc layout). Explains the three-layer model (master → task-lead → underling), how `parentSessionId` flows through spawns, and where permission requests route.
338
+ 2. Troubleshooting section in the top-level README (or docs/troubleshooting.md): "My agent says 'user rejected this tool use' and I didn't reject anything — what's going on?" Explain the bug class, link to `genie doctor`, link to the fix commit.
339
+ 3. Cross-reference prior fixes: commit `7c21301a6` and issue #1094. Explicitly note this is the third instance of the same class of bug.
340
+ 4. Update `src/genie-commands/session.ts:63` comment (the "CC updates it internally once started" lie) to reference the truth and this wish.
341
+ 5. If `AGENTS.md` exists at the repo root or under `src/`, update it with a brief hierarchy note so agents loading the file understand the model.
342
+
343
+ **Acceptance Criteria:**
344
+ - [ ] Docs file exists, is linked from the top-level README index or docs index.
345
+ - [ ] The troubleshooting section quotes the exact user-visible error message `"The user doesn't want to proceed with this tool use. The tool use was rejected."` so people can find it via search.
346
+ - [ ] Zero broken links.
347
+
348
+ **Validation:**
349
+ ```bash
350
+ # Markdown link check (use whatever the repo uses)
351
+ # Or manual review during /review.
352
+ ```
353
+
354
+ **depends-on:** none (can be written in parallel with Group 1)
355
+
356
+ ---
357
+
358
+ ## Dependencies
359
+
360
+ - `depends-on`: none external — this wish is self-contained within the genie repo.
361
+ - `blocks`: any wish that relies on native teams working reliably (currently nothing is explicitly blocked, but every agent-spawning wish is implicitly at risk until this ships).
362
+
363
+ ## QA Criteria
364
+
365
+ _What must be verified on dev after merge. The QA agent tests each criterion._
366
+
367
+ - [ ] **Reproducer check:** on a fresh checkout of `dev`, start `genie setup`, spawn an engineer via `genie spawn engineer`, and have the engineer create a new file at the cwd root (e.g. `.test-marker`). The write must succeed without any "user rejected" error.
368
+ - [ ] **Three-layer check:** spawn a task-lead that itself spawns a worker. The worker creates a new file at cwd root. Succeeds. The master's inbox contains **zero** new entries — all routing stopped at the task-lead.
369
+ - [ ] **Doctor pass:** on a fresh checkout after the fix, `genie doctor` exits 0 and shows a Team Health section with all checks PASS.
370
+ - [ ] **Doctor catch:** manually corrupt a team config back to `leadSessionId: "pending"`. `genie doctor` must exit non-zero with a FAIL line naming the team. `genie doctor --fix` must repair it.
371
+ - [ ] **Backlog drain:** manually append 5 fake `permission_request` entries to a team inbox. `genie doctor --fix` must drain them (writing synthetic responses), log to `~/.genie/doctor-fix.log`, and leave the inbox in a consistent state.
372
+ - [ ] **Idempotency:** `genie doctor --fix` run twice in a row on a healthy machine must report `0 patched, 0 drained` the second time.
373
+ - [ ] **No `'pending'` literals in production code:** `grep -rn "'pending'" src/lib/team-auto-spawn.ts src/genie-commands/session.ts src/lib/protocol-router-spawn.ts` returns zero matches.
374
+ - [ ] **Regression sentinel works:** deliberately reintroduce a `'pending'` literal in a test branch; CI must fail.
375
+ - [ ] **No regressions:** `bun test` passes with the same or higher count than pre-wish baseline. `bun run typecheck` clean. `bun run lint` clean or unchanged baseline.
376
+ - [ ] **Documented:** README troubleshooting section exists and is discoverable.
377
+
378
+ ## Assumptions / Risks
379
+
380
+ | Risk | Severity | Mitigation |
381
+ |------|----------|------------|
382
+ | Claude Code's internal write-gate may change shape in a future version and bypass our auto-approver. | Medium | The regression test (Group 7) will catch breakage on upgrade. Document the exact Claude Code version tested. |
383
+ | `discoverClaudeSessionId` heuristic (newest JSONL in project dir) may pick the wrong session if the user has multiple concurrent Claude sessions in the same cwd. | Medium | Prefer the `CLAUDE_CODE_SESSION_ID` env var path; fall back to JSONL only when env is missing. Test explicitly with concurrent sessions. |
384
+ | Auto-approver could approve malicious requests if a non-member somehow writes to a team inbox. | Low | Trust-on-team-membership + audit log. The team inbox dir has user-only permissions on Unix. If an attacker can write there, they already have the user's shell. |
385
+ | Migration might mis-identify legitimate placeholder values if someone extends the schema later. | Low | Match the literal string `"pending"` exactly, case-sensitive. Document the sentinel value in the code. |
386
+ | Three-layer hierarchy lookup could be slow on large teams. | Low | Cache `resolveNearestLiveAncestor` for the lifetime of an inbox-watcher poll cycle. O(hierarchy depth) lookups, not O(N members). |
387
+ | Fix-forward on `dev` leaves users on older versions broken. | Medium | Document the minimum safe version in the README troubleshooting section. `genie doctor` on old versions won't show Team Health but will at least not claim everything is fine. |
388
+ | The user's current machine has 44 stale requests in its inbox from before the fix. | Confirmed | Group 5 explicitly drains existing backlogs via `doctor --fix`. This wish fixes the bug AND cleans up after it. |
389
+
390
+ ## Review Results
391
+
392
+ _Populated by `/review` after execution completes._
393
+
394
+ ---
395
+
396
+ ## Files to Create/Modify
397
+
398
+ ```
399
+ # New files
400
+ src/lib/permission-auto-approver.ts
401
+ src/lib/permission-auto-approver.test.ts
402
+ src/lib/spawn-hierarchy.integration.test.ts
403
+ src/genie-commands/doctor.test.ts
404
+ migrations/2026-04-10-perfect-spawn-hierarchy.ts
405
+ migrations/2026-04-10-perfect-spawn-hierarchy.test.ts
406
+ scripts/check-no-pending-literal.sh
407
+ docs/architecture/spawn-hierarchy.md
408
+
409
+ # Modified files
410
+ src/lib/claude-native-teams.ts # add resolveSpawnerSessionId, resolveNearestLiveAncestor
411
+ src/lib/claude-native-teams.test.ts # new tests for the helpers
412
+ src/lib/team-auto-spawn.ts # kill 'pending' literal (line 144)
413
+ src/lib/team-auto-spawn.test.ts # update assertions; keep fixture 'pending' for config-detection test
414
+ src/lib/protocol-router-spawn.ts # harden resolveParentSession; remove fake-string fallback
415
+ src/lib/team-manager.ts # stop swallowing registerAsTeamLead errors (line 332-336)
416
+ src/lib/team-lead-command.ts # propagate GENIE_PARENT_SESSION_ID env
417
+ src/lib/inbox-watcher.ts # integrate auto-approver
418
+ src/lib/inbox-watcher.test.ts # tests for the new auto-approve path
419
+ src/genie-commands/session.ts # kill 'pending' literal (line 77); update stale comment (line 63)
420
+ src/genie-commands/doctor.ts # add Team Health section + fixTeamHealth
421
+ README.md # troubleshooting section
422
+ # Possibly AGENTS.md if present
423
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260410.3",
3
+ "version": "4.260410.4",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260410.3",
3
+ "version": "4.260410.4",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260410.3",
3
+ "version": "4.260410.4",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",