@automagik/genie 4.260410.4 → 4.260411.1
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.genie/wishes/fix-ghost-approval-p0/REPRO.md +123 -0
- package/.genie/wishes/fix-ghost-approval-p0/WISH.md +215 -0
- package/.genie/wishes/perfect-spawn-hierarchy/WISH.md +15 -5
- package/dist/genie.js +16 -15
- package/package.json +2 -2
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/genie-commands/session.ts +89 -33
- package/src/lib/claude-native-teams.test.ts +138 -0
- package/src/lib/claude-native-teams.ts +151 -0
- package/src/lib/team-auto-spawn.ts +21 -7
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "genie",
|
|
13
|
-
"version": "4.
|
|
13
|
+
"version": "4.260411.1",
|
|
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,123 @@
|
|
|
1
|
+
# REPRO: fix-ghost-approval-p0
|
|
2
|
+
|
|
3
|
+
End-to-end validation steps for the P0 fix. Run these after the fix ships via `@next` (`genie update --next`) to prove the ghost-approval deadlock is gone.
|
|
4
|
+
|
|
5
|
+
## Preconditions
|
|
6
|
+
|
|
7
|
+
- A recent `@next` build of genie that contains the P0 fix (`resolveOrMintLeadSessionId` landed, `'pending'` literal removed from `team-auto-spawn.ts` and `session.ts`).
|
|
8
|
+
- Claude Code CLI installed (the `claude` binary on `$PATH`).
|
|
9
|
+
- tmux installed.
|
|
10
|
+
- `jq` installed.
|
|
11
|
+
- `~/.claude/settings.json` has `teammateMode: "bypassPermissions"` (set by `ensureTeammateBypassPermissions()` — verify with `jq '.teammateMode' ~/.claude/settings.json`).
|
|
12
|
+
|
|
13
|
+
## Repro setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Fresh workspace
|
|
17
|
+
mkdir -p /tmp/ghost-repro && cd /tmp/ghost-repro
|
|
18
|
+
rm -rf ~/.claude/teams/ghost-repro
|
|
19
|
+
rm -f .test-marker .gitmodules
|
|
20
|
+
|
|
21
|
+
# Scaffold a minimal agent so `genie` doesn't ask
|
|
22
|
+
cat > AGENTS.md <<'EOF'
|
|
23
|
+
# Repro agent
|
|
24
|
+
|
|
25
|
+
Testing fix-ghost-approval-p0.
|
|
26
|
+
EOF
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Baseline: the bug before the fix
|
|
30
|
+
|
|
31
|
+
On a pre-fix build these steps produced:
|
|
32
|
+
1. `~/.claude/teams/ghost-repro/config.json` with `"leadSessionId": "pending"`.
|
|
33
|
+
2. A teammate spawned via `genie spawn` that tried to `Write .test-marker` at cwd root.
|
|
34
|
+
3. The permission request landing in `~/.claude/teams/ghost-repro/inboxes/team-lead.json` with no matching response.
|
|
35
|
+
4. The teammate responding with `"The user doesn't want to proceed with this tool use. The tool use was rejected."`.
|
|
36
|
+
|
|
37
|
+
## Reproduction (post-fix)
|
|
38
|
+
|
|
39
|
+
Step 1 — launch the team-lead session:
|
|
40
|
+
```bash
|
|
41
|
+
cd /tmp/ghost-repro
|
|
42
|
+
genie # launches Claude Code in a tmux pane, creates native team "ghost-repro"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Step 2 — from inside the team-lead CC session, verify the team config:
|
|
46
|
+
```bash
|
|
47
|
+
cat ~/.claude/teams/ghost-repro/config.json | jq '.leadSessionId'
|
|
48
|
+
# EXPECTED: a UUID matching /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
49
|
+
# FAILURE MODE: the string "pending" or "genie-ghost-repro"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Step 3 — verify the UUID matches a real JSONL file:
|
|
53
|
+
```bash
|
|
54
|
+
LEAD_ID=$(jq -r '.leadSessionId' ~/.claude/teams/ghost-repro/config.json)
|
|
55
|
+
ENC_CWD=$(echo -n "/tmp/ghost-repro" | tr -c 'a-zA-Z0-9' '-')
|
|
56
|
+
ls -la ~/.claude/projects/"$ENC_CWD"/"$LEAD_ID".jsonl
|
|
57
|
+
# EXPECTED: file exists
|
|
58
|
+
# FAILURE MODE: "No such file or directory"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Step 4 — spawn a teammate and have it write a new cwd-root file:
|
|
62
|
+
```bash
|
|
63
|
+
genie spawn engineer --team ghost-repro
|
|
64
|
+
# (inside the engineer session)
|
|
65
|
+
# Ask the engineer: please Write a file at /tmp/ghost-repro/.test-marker with content "hello"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Step 5 — verify the write succeeded:
|
|
69
|
+
```bash
|
|
70
|
+
cat /tmp/ghost-repro/.test-marker
|
|
71
|
+
# EXPECTED: "hello"
|
|
72
|
+
# FAILURE MODE: file doesn't exist AND the engineer output "The user doesn't want to proceed with this tool use. The tool use was rejected."
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Step 6 — inbox sanity check (no permission_request accumulated):
|
|
76
|
+
```bash
|
|
77
|
+
jq '[.[] | select(.type == "permission_request")] | length' \
|
|
78
|
+
~/.claude/teams/ghost-repro/inboxes/team-lead.json
|
|
79
|
+
# EXPECTED: 0 (or the same count as before step 4)
|
|
80
|
+
# FAILURE MODE: count increased by 1+ (ghost request still landing in the inbox)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Healing check (stale config upgraded in place)
|
|
84
|
+
|
|
85
|
+
Prove the fix also heals machines that already have a broken config:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cd /tmp/ghost-repro-heal && rm -rf ~/.claude/teams/ghost-repro-heal
|
|
89
|
+
mkdir -p ~/.claude/teams/ghost-repro-heal/inboxes
|
|
90
|
+
cat > ~/.claude/teams/ghost-repro-heal/config.json <<'EOF'
|
|
91
|
+
{
|
|
92
|
+
"name": "ghost-repro-heal",
|
|
93
|
+
"description": "Pre-seeded stale config",
|
|
94
|
+
"createdAt": 1700000000000,
|
|
95
|
+
"leadAgentId": "ghost-repro-heal@ghost-repro-heal",
|
|
96
|
+
"leadSessionId": "pending",
|
|
97
|
+
"members": []
|
|
98
|
+
}
|
|
99
|
+
EOF
|
|
100
|
+
|
|
101
|
+
# Trigger a respawn via `genie team ensure` or `genie` in that folder
|
|
102
|
+
mkdir -p /tmp/ghost-repro-heal && cd /tmp/ghost-repro-heal
|
|
103
|
+
cat > AGENTS.md <<'EOF'
|
|
104
|
+
# Heal test
|
|
105
|
+
EOF
|
|
106
|
+
genie team ensure ghost-repro-heal # or just `genie`
|
|
107
|
+
|
|
108
|
+
# Verify the upsert
|
|
109
|
+
jq '.leadSessionId' ~/.claude/teams/ghost-repro-heal/config.json
|
|
110
|
+
# EXPECTED: a real UUID (stale "pending" was replaced in place)
|
|
111
|
+
# FAILURE MODE: still "pending"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Cleanup
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
rm -rf /tmp/ghost-repro /tmp/ghost-repro-heal
|
|
118
|
+
rm -rf ~/.claude/teams/ghost-repro ~/.claude/teams/ghost-repro-heal
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Pass criteria
|
|
122
|
+
|
|
123
|
+
The P0 fix is proven when **every** check above returns its EXPECTED value on a `@next` build.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Wish: Fix Ghost-Approval P0 — Kill the `'pending'` leadSessionId Literal
|
|
2
|
+
|
|
3
|
+
| Field | Value |
|
|
4
|
+
|-------|-------|
|
|
5
|
+
| **Status** | DRAFT |
|
|
6
|
+
| **Slug** | `fix-ghost-approval-p0` |
|
|
7
|
+
| **Date** | 2026-04-10 |
|
|
8
|
+
| **Priority** | P0 — worst active bug in the product |
|
|
9
|
+
| **Parent** | Split out of `.genie/wishes/perfect-spawn-hierarchy/WISH.md` |
|
|
10
|
+
| **Related** | Commit `7c21301a6` (2026-04-02 partial fix), Issue #1094 (2026-04-09 SDK-path fix) |
|
|
11
|
+
|
|
12
|
+
## Summary
|
|
13
|
+
|
|
14
|
+
Two spawn paths in genie (`team-auto-spawn.ts` and `session.ts`) create a native team config with `leadSessionId: "pending"` — a literal placeholder that nothing ever reconciles. When a teammate later writes 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 looks up the leader by session ID. The ghost leader never answers. Result: the teammate sees `"The user doesn't want to proceed with this tool use"` and silently gives up. This wish is a **surgical minimal fix** that (a) mints or discovers a real Claude Code session UUID before the new CC process launches, (b) writes that UUID to the team config, (c) launches CC with `--session-id <uuid>` so the config and the CC process agree, and (d) proves the fix with a real reproducer. Three-layer hierarchy, auto-approver daemon, and `genie doctor` Team Health are explicitly DEFERRED to a follow-up wish.
|
|
15
|
+
|
|
16
|
+
## Context
|
|
17
|
+
|
|
18
|
+
See `.genie/wishes/perfect-spawn-hierarchy/WISH.md` for the full trace, the 44-request-backlog evidence, and the broader architectural diagnosis. This wish is the **narrow surgical bite** that can ship today and prove the theory before we invest in the bigger hierarchy/doctor/migration work.
|
|
19
|
+
|
|
20
|
+
**Primary root cause — two code sites hardcode the `'pending'` literal:**
|
|
21
|
+
- `src/lib/team-auto-spawn.ts:144` — Omni recovery path: `ensureNativeTeam(teamName, ..., 'pending', leaderName)`
|
|
22
|
+
- `src/genie-commands/session.ts:77` — interactive session path: `ensureNativeTeam(teamName, ..., 'pending', leaderName)` with a comment that falsely claims "CC updates it internally once started".
|
|
23
|
+
|
|
24
|
+
**Why the naive fix of "resolve the caller's session ID first" doesn't work here:** both paths are launching a **new** Claude Code process in tmux. At `ensureNativeTeam()` call time, the new CC process doesn't exist yet — there's no JSONL file and no `CLAUDE_CODE_SESSION_ID` env var to read. We have to either (a) pre-mint a UUID and force CC to use it via `--session-id`, or (b) defer the write until after CC boots and self-registers. Approach (a) is already supported by the existing `buildTeamLeadCommand()` helper in `src/lib/team-lead-command.ts:70-72`, so we use it.
|
|
25
|
+
|
|
26
|
+
**The resume case works the same way by reading the existing JSONL:** `sessionExists()` already scans `~/.claude/projects/<encoded-cwd>/*.jsonl` for a `custom-title` match. We extend that helper (or add a sibling) to return the matching JSONL's UUID so we can write it to the team config before passing `--resume <name>` to CC.
|
|
27
|
+
|
|
28
|
+
## Scope
|
|
29
|
+
|
|
30
|
+
### IN
|
|
31
|
+
|
|
32
|
+
- Replace the hardcoded `'pending'` literal in `src/lib/team-auto-spawn.ts:144` with a real session UUID.
|
|
33
|
+
- Replace the hardcoded `'pending'` literal in `src/genie-commands/session.ts:77` with a real session UUID.
|
|
34
|
+
- Add a helper that, given a team name + working directory, returns:
|
|
35
|
+
- `{ sessionId, shouldResume: true }` if an existing `.jsonl` for this team is found (reuse its UUID, CC will `--resume` by name).
|
|
36
|
+
- `{ sessionId, shouldResume: false }` if no prior session exists (mint a fresh `crypto.randomUUID()`, CC will `--session-id` into it).
|
|
37
|
+
- Ensure `ensureNativeTeam()` **upserts** a stale `leadSessionId` (`"pending"` or any non-UUID) with the newly resolved ID, so machines that already have the broken config get healed on the next spawn.
|
|
38
|
+
- Pass the resolved `sessionId` through to `buildTeamLeadCommand()` so the new CC process boots with that exact UUID (or resumes by name into the existing JSONL, whose UUID we also wrote to the config).
|
|
39
|
+
- One new unit test file covering the helper (fresh mint path, resume-by-existing-jsonl path, stale config upsert path).
|
|
40
|
+
- One integration-ish test that exercises `ensureTeamLead()` end-to-end with a pre-seeded stale config and asserts the resulting config has a UUID, not `"pending"`.
|
|
41
|
+
- A reproducer script (or manual steps captured in a markdown checklist) that matches the 2026-04-10 failure: fresh team → spawn a teammate → teammate writes `.test-marker` at cwd root → succeeds.
|
|
42
|
+
- Update the lying comment at `src/genie-commands/session.ts:63`.
|
|
43
|
+
|
|
44
|
+
### OUT
|
|
45
|
+
|
|
46
|
+
- **Three-layer hierarchy** (master → task-lead → underling routing). Deferred to the parent wish.
|
|
47
|
+
- **Auto-approver daemon.** Deferred.
|
|
48
|
+
- **`genie doctor` Team Health section.** Deferred — the P0 must land first; doctor coverage gets reevaluated after.
|
|
49
|
+
- **Migration script / `genie doctor --fix` auto-remediation** for existing broken machines. Deferred. (In-process upsert on the next spawn will heal live-in-use teams as a side-effect; explicit migration is only needed for teams that are never respawned.)
|
|
50
|
+
- **Changes to `src/lib/protocol-router-spawn.ts`.** That path already calls `discoverClaudeParentSessionId()` for its parent session; it's a separate class of bug. Hardening its `?? \`genie-${team}\`` fallback is part of the deferred residual wish.
|
|
51
|
+
- **Architecture docs / troubleshooting write-up.** Deferred.
|
|
52
|
+
- **Any `genie spawn`-side hierarchy work.** The P0 only fixes the two `ensureNativeTeam('pending', ...)` sites that run before CC boots.
|
|
53
|
+
- **Editing the `'pending'` fixture in `src/lib/team-auto-spawn.test.ts:58`** — that test intentionally seeds a broken config to exercise detection. Leave it alone.
|
|
54
|
+
|
|
55
|
+
## Decisions
|
|
56
|
+
|
|
57
|
+
| Decision | Rationale |
|
|
58
|
+
|----------|-----------|
|
|
59
|
+
| **Pre-mint the session UUID and pass it via `--session-id`, don't wait for CC to self-register.** | `buildTeamLeadCommand()` already supports `--session-id`. The "wait for CC to self-register" model is exactly what the lying comment at `session.ts:63` claims today, and it's been wrong for weeks. Pre-minting is synchronous, deterministic, and race-free. |
|
|
60
|
+
| **In the resume case, read the UUID from the existing JSONL filename.** | Claude Code stores sessions as `~/.claude/projects/<encoded-cwd>/<uuid>.jsonl`. When we pass `--resume <name>`, CC loads the JSONL whose `custom-title` matches. The filename UUID IS the session ID. We scan for the match and write the UUID to the config before launching CC. No race. |
|
|
61
|
+
| **Force-upsert stale `leadSessionId`.** | Without this, a machine that already has `"pending"` in its config from a previous broken run will hit the `if (existing) return existing;` short-circuit in `ensureNativeTeam()` and keep the broken value. Upserting in place heals those machines on the next spawn, no migration script needed. |
|
|
62
|
+
| **Ship two tests only.** | One helper unit test and one config-upsert integration test. This is a P0 surgical fix — the test bar is "prove the fix works", not "achieve full coverage". Full regression sweeps are part of the residual wish. |
|
|
63
|
+
| **Ship a reproducer, not a mock.** | A live test that spawns a teammate and writes a new cwd-root file is the only proof that actually matters. If the automated integration test and the manual reproducer both pass on the `@next` publish, the P0 is done. |
|
|
64
|
+
| **Explicitly keep the `'pending'` fixture in `team-auto-spawn.test.ts`.** | That test is validating detection of broken configs. The fixture is intentional. Don't regress the test. |
|
|
65
|
+
| **No `GhostLeaderError` or throw-on-miss.** | The original parent-wish design wanted to throw if nothing resolved. For the P0, we just mint a fresh UUID — there's no "miss" case because we own the ID. Throwing is deferred to `protocol-router-spawn.ts` hardening in the residual wish. |
|
|
66
|
+
|
|
67
|
+
## Success Criteria
|
|
68
|
+
|
|
69
|
+
- [ ] **Zero `'pending'` literals in production code paths.** `grep -n "'pending'" src/lib/team-auto-spawn.ts src/genie-commands/session.ts` returns zero matches.
|
|
70
|
+
- [ ] **`ensureNativeTeam()` is never called with a literal or synthetic session ID** from `team-auto-spawn.ts` or `session.ts`. Both sites pass a value that is either a freshly-minted `crypto.randomUUID()` or a UUID read from an existing JSONL filename.
|
|
71
|
+
- [ ] **Stale `leadSessionId` is force-upserted.** A config with `leadSessionId: "pending"` on disk before the spawn has a real UUID after the spawn (verified by reading `~/.claude/teams/<name>/config.json`).
|
|
72
|
+
- [ ] **The new CC process boots with the same UUID that was written to the config.** Verified by: spawning, reading the config, finding the corresponding `~/.claude/projects/<cwd>/<uuid>.jsonl` on disk.
|
|
73
|
+
- [ ] **Unit test for the resolver helper** covers: (a) no prior JSONL → mints a UUID, (b) prior JSONL exists → returns its UUID, (c) stale `"pending"` config → upserts with the resolved UUID.
|
|
74
|
+
- [ ] **Integration test** pre-seeds a stale config, calls the ensure-team path, and asserts the config's `leadSessionId` is a UUID (matches `/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i`) — never `"pending"`.
|
|
75
|
+
- [ ] **`bun run typecheck` is clean.**
|
|
76
|
+
- [ ] **`bun test` passes** — the existing `team-auto-spawn.test.ts` fixture still works, new tests pass, nothing else regresses.
|
|
77
|
+
- [ ] **`bun run lint` is clean** (biome check).
|
|
78
|
+
- [ ] **Reproducer passes on the `@next` build.** After `genie update --next` pulls in the fix: in a fresh cwd, run `genie` → inside Claude Code, spawn a teammate via `genie spawn engineer` → instruct the teammate to `Write` a new file `.test-marker` at cwd root → the write must succeed with no "user rejected" error.
|
|
79
|
+
- [ ] **No permission_request accumulates in the team-lead inbox** during the reproducer run. `jq 'map(select(.type=="permission_request"))|length' ~/.claude/teams/<team>/inboxes/team-lead.json` reads the same value before and after the teammate's write.
|
|
80
|
+
|
|
81
|
+
## Execution Strategy
|
|
82
|
+
|
|
83
|
+
One wave. One group. One engineer. Ship it.
|
|
84
|
+
|
|
85
|
+
### Wave 1 (single group, sequential within)
|
|
86
|
+
|
|
87
|
+
| Group | Agent | Description |
|
|
88
|
+
|-------|-------|-------------|
|
|
89
|
+
| 1 | engineer | Implement the helper, fix both call sites, force-upsert stale configs, write 2 tests, update the lying comment. |
|
|
90
|
+
| review | reviewer | Verify acceptance criteria + run the reproducer on `@next`. |
|
|
91
|
+
|
|
92
|
+
## Execution Groups
|
|
93
|
+
|
|
94
|
+
### Group 1: Kill the `'pending'` literal + real session ID at spawn time
|
|
95
|
+
|
|
96
|
+
**Goal:** Every native team config written by `ensureTeamLead()` or `ensureNativeTeamForLeader()` contains a real Claude Code session UUID, matching the UUID the newly-launched CC process actually uses.
|
|
97
|
+
|
|
98
|
+
**Deliverables:**
|
|
99
|
+
|
|
100
|
+
1. **New helper `resolveOrMintLeadSessionId(teamName, cwd): Promise<{ sessionId: string; shouldResume: boolean }>`** in `src/lib/claude-native-teams.ts` (or a sibling file — engineer's call based on minimum-diff principle):
|
|
101
|
+
- Scan `~/.claude/projects/<sanitizePath(cwd)>/*.jsonl` for a JSONL whose `custom-title` matches `sanitizeTeamName(teamName)` (the same lookup `sessionExists()` in `team-lead-command.ts` already does).
|
|
102
|
+
- If a match is found → extract the UUID from the filename (`<uuid>.jsonl`) and return `{ sessionId, shouldResume: true }`.
|
|
103
|
+
- Otherwise → return `{ sessionId: crypto.randomUUID(), shouldResume: false }`.
|
|
104
|
+
- Exposes the underlying file scan so it can be unit-tested without spawning CC.
|
|
105
|
+
|
|
106
|
+
2. **Upsert logic for stale `leadSessionId`** — either as a new helper `hydrateLeadSessionId(teamName, realSessionId)` that:
|
|
107
|
+
- Loads the existing config (if any).
|
|
108
|
+
- If config exists and `leadSessionId !== realSessionId` (e.g. the stale value is `"pending"`), updates it and saves.
|
|
109
|
+
- If no config exists, calls `ensureNativeTeam()` with `realSessionId`.
|
|
110
|
+
- **OR** modify `ensureNativeTeam()` to take an `upsertLeadSessionId: boolean` flag. Engineer's call — whatever produces the smaller diff and clearer call sites.
|
|
111
|
+
|
|
112
|
+
3. **`src/lib/team-auto-spawn.ts:ensureTeamLead()`** — replace the call at line 144:
|
|
113
|
+
```typescript
|
|
114
|
+
await ensureNativeTeam(teamName, `Genie team: ${teamName}`, 'pending', leaderName);
|
|
115
|
+
```
|
|
116
|
+
with something like:
|
|
117
|
+
```typescript
|
|
118
|
+
const { sessionId, shouldResume } = await resolveOrMintLeadSessionId(teamName, workingDir);
|
|
119
|
+
await hydrateLeadSessionId(teamName, `Genie team: ${teamName}`, sessionId, leaderName);
|
|
120
|
+
// ... later when building the CC launch command ...
|
|
121
|
+
const cmd = buildTeamLeadCommand(teamName, {
|
|
122
|
+
systemPromptFile: systemPromptFile ?? undefined,
|
|
123
|
+
leaderName,
|
|
124
|
+
...(shouldResume
|
|
125
|
+
? { continueName: sanitizeTeamName(teamName) }
|
|
126
|
+
: { sessionId }),
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
Note: drop the `sessionExists()`-based decision at line 166 because the new helper already computed `shouldResume`.
|
|
130
|
+
|
|
131
|
+
4. **`src/genie-commands/session.ts:ensureNativeTeamForLeader()`** — replace the call at line 77 with the same pattern. Accept the new session ID as a parameter (the outer caller is `createSession()` / `focusTeamWindow()` / `launchInsideTmux()`, which already compute resume information). Thread the `sessionId` through so all three launchers use the same value. Update `buildClaudeCommand()` signature if needed to take `sessionId` in addition to `continueName`.
|
|
132
|
+
|
|
133
|
+
5. **Delete the lying comment at `src/genie-commands/session.ts:63`** — rewrite it to describe the real flow: "The leadSessionId is resolved at spawn time by `resolveOrMintLeadSessionId()` and matches the UUID passed to CC via `--session-id` (or discovered from the existing JSONL in the resume path)."
|
|
134
|
+
|
|
135
|
+
6. **New test file `src/lib/claude-native-teams.lead-session-id.test.ts`** (or extend the existing `claude-native-teams.test.ts` if that's more idiomatic):
|
|
136
|
+
- **Test 1:** With an isolated `CLAUDE_CONFIG_DIR` and an empty `~/.claude/projects/<cwd>/`, `resolveOrMintLeadSessionId("my-team", cwd)` returns `{ shouldResume: false, sessionId: <valid UUID> }`. Assert the UUID matches the UUID v4 regex.
|
|
137
|
+
- **Test 2:** With a pre-seeded `<cwd>/.claude/projects/<encoded>/abc-123-....jsonl` containing a `custom-title` entry matching `my-team`, the helper returns `{ shouldResume: true, sessionId: "abc-123-..." }`.
|
|
138
|
+
- **Test 3:** Pre-seed a team config with `leadSessionId: "pending"`, call `hydrateLeadSessionId("my-team", desc, "fresh-uuid-xxx", "genie")`, then load the config from disk — `leadSessionId` must be `"fresh-uuid-xxx"`.
|
|
139
|
+
- **Test 4 (bonus):** Same as Test 3 but with `leadSessionId: "genie-my-team"` (the synthetic fallback) — also gets upserted.
|
|
140
|
+
|
|
141
|
+
7. **Reproducer script / manual checklist** at `.genie/wishes/fix-ghost-approval-p0/REPRO.md` that documents the exact steps to prove the fix post-publish:
|
|
142
|
+
- `cd /tmp/test-repro && rm -rf ~/.claude/teams/test-repro`
|
|
143
|
+
- `genie` (opens CC as team-lead)
|
|
144
|
+
- Inside CC: spawn a teammate via `genie spawn engineer --team test-repro`
|
|
145
|
+
- In the teammate: `Write` to `.test-marker` at cwd root
|
|
146
|
+
- Expected: write succeeds, no "user rejected" error, `jq '.leadSessionId' ~/.claude/teams/test-repro/config.json` returns a real UUID.
|
|
147
|
+
|
|
148
|
+
**Acceptance Criteria:**
|
|
149
|
+
- [ ] `grep -n "'pending'" src/lib/team-auto-spawn.ts src/genie-commands/session.ts` returns zero matches.
|
|
150
|
+
- [ ] `resolveOrMintLeadSessionId` exists, is exported, and is exercised by all four new tests.
|
|
151
|
+
- [ ] Spawning a team writes a real UUID to `config.json` (not `"pending"`, not `"genie-<team>"`).
|
|
152
|
+
- [ ] Spawning a team whose config already has `"pending"` heals it to a real UUID on the next call.
|
|
153
|
+
- [ ] `bun run typecheck` passes.
|
|
154
|
+
- [ ] `bun test src/lib/claude-native-teams.test.ts src/lib/claude-native-teams.lead-session-id.test.ts src/lib/team-auto-spawn.test.ts` passes.
|
|
155
|
+
- [ ] `bun run lint` clean.
|
|
156
|
+
- [ ] REPRO.md exists at `.genie/wishes/fix-ghost-approval-p0/REPRO.md` with copy-pasteable steps.
|
|
157
|
+
|
|
158
|
+
**Validation:**
|
|
159
|
+
```bash
|
|
160
|
+
cd /home/genie/workspace/repos/genie
|
|
161
|
+
bun run typecheck
|
|
162
|
+
bun run lint
|
|
163
|
+
bun test src/lib/claude-native-teams.test.ts src/lib/claude-native-teams.lead-session-id.test.ts src/lib/team-auto-spawn.test.ts
|
|
164
|
+
! grep -n "'pending'" src/lib/team-auto-spawn.ts src/genie-commands/session.ts
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**depends-on:** none
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Dependencies
|
|
172
|
+
|
|
173
|
+
- `depends-on`: none — this wish is a self-contained surgical fix.
|
|
174
|
+
- `blocks`: `.genie/wishes/perfect-spawn-hierarchy/WISH.md` (residual) — that wish is explicitly DEFERRED until this P0 lands and the reproducer passes.
|
|
175
|
+
|
|
176
|
+
## QA Criteria
|
|
177
|
+
|
|
178
|
+
_What must be verified on dev after merge. The QA agent (or the human) tests each criterion on the `@next` build._
|
|
179
|
+
|
|
180
|
+
- [ ] **Reproducer check:** on a fresh checkout of `dev` built via `genie update --next`, run the steps in `REPRO.md`. The teammate's `Write` to `.test-marker` at cwd root must succeed without any "user rejected" error.
|
|
181
|
+
- [ ] **Config check:** after the reproducer runs, `jq '.leadSessionId' ~/.claude/teams/<team>/config.json` returns a UUID (not `"pending"`, not `"genie-<team>"`).
|
|
182
|
+
- [ ] **No new permission_requests in the inbox:** `jq 'map(select(.type=="permission_request"))|length' ~/.claude/teams/<team>/inboxes/team-lead.json` reads the same value before and after the teammate's write.
|
|
183
|
+
- [ ] **Healing check:** pre-seed a team config with `leadSessionId: "pending"`, then trigger `ensureTeamLead()` (via `genie team ensure <name>` or a spawn that routes through it). The config now has a UUID.
|
|
184
|
+
- [ ] **No regressions:** `bun test` passes with the same or higher count than pre-wish baseline. `bun run typecheck` clean.
|
|
185
|
+
|
|
186
|
+
## Assumptions / Risks
|
|
187
|
+
|
|
188
|
+
| Risk | Severity | Mitigation |
|
|
189
|
+
|------|----------|------------|
|
|
190
|
+
| `--session-id` behavior in Claude Code may not actually honor the pre-assigned UUID (e.g. CC might still mint its own internally). | High | `buildTeamLeadCommand()` at `src/lib/team-lead-command.ts:70-72` already constructs `--session-id ${shellQuote(options.sessionId)}` and has been shipped. If CC didn't honor it, that code would already be broken in another place. The integration test (Criterion 4 above) verifies agreement between the config and the JSONL filename — this is the gate. |
|
|
191
|
+
| Concurrent `ensureTeamLead` calls race to mint different UUIDs. | Low | `ensureNativeTeam()` has a `loadConfig → short-circuit-if-exists` pattern. The loser of the race reads the winner's config; our upsert only replaces truly stale values (like `"pending"`). Two live UUIDs won't fight each other. |
|
|
192
|
+
| Resume-by-filename scan picks the wrong JSONL if two sessions in the same cwd have the same team name. | Low | Pick the most recently modified match (same tiebreaker `sessionExists()` effectively uses). Document in the helper. |
|
|
193
|
+
| `bun test` pre-existing test-pollution bug re-surfaces during the pre-push hook. | Medium | Per the last compaction: stop the local `genie serve` daemon before `git push`. Alternatively, sandbox `GENIE_HOME` in the tests we add (we already have that pattern in `omni-bridge-pidfile.test.ts` — reuse it). |
|
|
194
|
+
| The reproducer can't run in CI (no tmux, no interactive CC). | Medium | CI runs the unit + integration tests. The reproducer is a manual end-to-end gate on the `@next` build, tracked in `REPRO.md`. |
|
|
195
|
+
| We ship a UUID the config but CC resumes into a different JSONL because of a stale `custom-title`. | Low | The `shouldResume=true` path extracts the session ID from the matching JSONL's filename and writes THAT to the config. By construction, config and JSONL agree. |
|
|
196
|
+
|
|
197
|
+
## Review Results
|
|
198
|
+
|
|
199
|
+
_Populated by `/review` after execution completes._
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Files to Create/Modify
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
# New files
|
|
207
|
+
.genie/wishes/fix-ghost-approval-p0/WISH.md # this file
|
|
208
|
+
.genie/wishes/fix-ghost-approval-p0/REPRO.md # reproducer steps
|
|
209
|
+
src/lib/claude-native-teams.lead-session-id.test.ts # unit+integration tests (or inline into existing test file)
|
|
210
|
+
|
|
211
|
+
# Modified files
|
|
212
|
+
src/lib/claude-native-teams.ts # add resolveOrMintLeadSessionId + hydrateLeadSessionId (or upsert flag on ensureNativeTeam)
|
|
213
|
+
src/lib/team-auto-spawn.ts # kill 'pending' (line 144); thread sessionId through buildTeamLeadCommand
|
|
214
|
+
src/genie-commands/session.ts # kill 'pending' (line 77); thread sessionId through; rewrite lying comment (line 63)
|
|
215
|
+
```
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
| Field | Value |
|
|
4
4
|
|-------|-------|
|
|
5
|
-
| **Status** |
|
|
5
|
+
| **Status** | DEFERRED — reevaluate after `fix-ghost-approval-p0` lands and the reproducer passes |
|
|
6
6
|
| **Slug** | `perfect-spawn-hierarchy` |
|
|
7
7
|
| **Date** | 2026-04-10 |
|
|
8
|
-
| **Priority** |
|
|
8
|
+
| **Priority** | P1 — architectural hardening (the P0 root-cause surgery was split out) |
|
|
9
9
|
| **Trace** | See Context section below |
|
|
10
10
|
| **Related** | Commit `7c21301a6` (2026-04-02 partial fix), Issue #1094 (2026-04-09 SDK-path fix) |
|
|
11
|
+
| **Split** | The P0 surgical fix (kill the `'pending'` literal, mint a real session ID at spawn time) was split into `.genie/wishes/fix-ghost-approval-p0/WISH.md` on 2026-04-10. That wish must ship and pass its reproducer before this one is resumed — and this wish will be re-scoped at that point, because the rest of the plan may change based on what we learn from the P0. |
|
|
11
12
|
|
|
12
13
|
## Summary
|
|
13
14
|
|
|
15
|
+
> **2026-04-10 update:** This wish's P0 surgical bite — "eliminate the `'pending'` literal and put a real session ID in the team config at spawn time" — has been split into `.genie/wishes/fix-ghost-approval-p0/WISH.md` so it can ship TODAY and be proven against the 2026-04-10 reproducer. The sections below are preserved as the architectural context and the residual backlog (three-layer hierarchy, auto-approver, `genie doctor` Team Health, migration script, comprehensive regression suite, docs). **Do not execute this wish until the P0 fix lands.** After the P0 lands we reevaluate: some groups here may become unnecessary, others may need re-scoping based on what we learned in production.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
14
19
|
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
20
|
|
|
16
21
|
## Context — What We Know (from the 2026-04-10 trace)
|
|
@@ -40,10 +45,13 @@ No corresponding `permission_response` exists. Inbox-wide: **44 permission_reque
|
|
|
40
45
|
|
|
41
46
|
## Scope
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
> **Scope will be re-cut when this wish is resumed.** The P0 surgery (kill the `'pending'` literal at the two spawn sites) is handled by `fix-ghost-approval-p0`. Everything below is the residual architectural work that may need re-scoping after the P0 lands.
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
### IN (subject to re-scope post-P0)
|
|
51
|
+
|
|
52
|
+
- ~~Eliminate every `'pending'` literal passed to `ensureNativeTeam()` in the production spawn paths.~~ **Moved to `fix-ghost-approval-p0`.**
|
|
53
|
+
- ~~Establish a single helper — `resolveSpawnerSessionId(cwd)` — that every spawn path uses to get a real Claude Code session ID for the CURRENT caller.~~ **Partially moved to `fix-ghost-approval-p0` as `resolveOrMintLeadSessionId` — the broader "every spawn path" sweep (including `protocol-router-spawn.ts`) stays here.**
|
|
54
|
+
- Harden `resolveParentSession()` in `src/lib/protocol-router-spawn.ts` to never return `"genie-<team>"` — throw on miss instead of silently falling back to a synthetic string.
|
|
47
55
|
- `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
56
|
- 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
57
|
- 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.
|
|
@@ -99,6 +107,8 @@ No corresponding `permission_response` exists. Inbox-wide: **44 permission_reque
|
|
|
99
107
|
|
|
100
108
|
## Execution Strategy
|
|
101
109
|
|
|
110
|
+
> **Execution is on hold.** Do not dispatch any group until `fix-ghost-approval-p0` lands, its reproducer passes on a `@next` build, and this wish has been re-scoped with any learnings. Group 1 in particular has been partially subsumed by the P0 fix.
|
|
111
|
+
|
|
102
112
|
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
113
|
|
|
104
114
|
### Wave 1 (parallel — 3 independent streams)
|