@os-eco/overstory-cli 0.9.4 → 0.11.0

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.
Files changed (124) hide show
  1. package/README.md +50 -19
  2. package/agents/builder.md +19 -9
  3. package/agents/coordinator.md +6 -6
  4. package/agents/lead.md +204 -87
  5. package/agents/merger.md +25 -14
  6. package/agents/reviewer.md +22 -16
  7. package/agents/scout.md +17 -12
  8. package/package.json +6 -3
  9. package/src/agents/capabilities.test.ts +85 -0
  10. package/src/agents/capabilities.ts +125 -0
  11. package/src/agents/headless-mail-injector.test.ts +448 -0
  12. package/src/agents/headless-mail-injector.ts +219 -0
  13. package/src/agents/headless-prompt.test.ts +102 -0
  14. package/src/agents/headless-prompt.ts +68 -0
  15. package/src/agents/hooks-deployer.test.ts +514 -14
  16. package/src/agents/hooks-deployer.ts +141 -0
  17. package/src/agents/mail-poll-detect.test.ts +153 -0
  18. package/src/agents/mail-poll-detect.ts +73 -0
  19. package/src/agents/overlay.test.ts +60 -4
  20. package/src/agents/overlay.ts +63 -8
  21. package/src/agents/scope-detect.test.ts +190 -0
  22. package/src/agents/scope-detect.ts +146 -0
  23. package/src/agents/turn-lock.test.ts +181 -0
  24. package/src/agents/turn-lock.ts +235 -0
  25. package/src/agents/turn-runner-dispatch.test.ts +182 -0
  26. package/src/agents/turn-runner-dispatch.ts +105 -0
  27. package/src/agents/turn-runner.test.ts +2312 -0
  28. package/src/agents/turn-runner.ts +1383 -0
  29. package/src/commands/agents.ts +9 -0
  30. package/src/commands/clean.ts +54 -0
  31. package/src/commands/coordinator.test.ts +254 -0
  32. package/src/commands/coordinator.ts +273 -8
  33. package/src/commands/dashboard.test.ts +188 -0
  34. package/src/commands/dashboard.ts +14 -4
  35. package/src/commands/doctor.ts +3 -1
  36. package/src/commands/group.test.ts +94 -0
  37. package/src/commands/group.ts +49 -20
  38. package/src/commands/init.test.ts +8 -0
  39. package/src/commands/init.ts +8 -1
  40. package/src/commands/log.test.ts +187 -11
  41. package/src/commands/log.ts +171 -71
  42. package/src/commands/mail.test.ts +162 -0
  43. package/src/commands/mail.ts +64 -9
  44. package/src/commands/merge.test.ts +230 -1
  45. package/src/commands/merge.ts +68 -12
  46. package/src/commands/nudge.test.ts +351 -4
  47. package/src/commands/nudge.ts +356 -34
  48. package/src/commands/run.test.ts +43 -7
  49. package/src/commands/serve/build.test.ts +202 -0
  50. package/src/commands/serve/build.ts +206 -0
  51. package/src/commands/serve/coordinator-actions.test.ts +339 -0
  52. package/src/commands/serve/coordinator-actions.ts +408 -0
  53. package/src/commands/serve/dev.test.ts +168 -0
  54. package/src/commands/serve/dev.ts +117 -0
  55. package/src/commands/serve/mail-actions.test.ts +312 -0
  56. package/src/commands/serve/mail-actions.ts +167 -0
  57. package/src/commands/serve/rest.test.ts +1323 -0
  58. package/src/commands/serve/rest.ts +708 -0
  59. package/src/commands/serve/static.ts +51 -0
  60. package/src/commands/serve/ws.test.ts +361 -0
  61. package/src/commands/serve/ws.ts +332 -0
  62. package/src/commands/serve.test.ts +459 -0
  63. package/src/commands/serve.ts +565 -0
  64. package/src/commands/sling.test.ts +177 -1
  65. package/src/commands/sling.ts +243 -71
  66. package/src/commands/status.test.ts +9 -0
  67. package/src/commands/status.ts +12 -4
  68. package/src/commands/stop.test.ts +255 -1
  69. package/src/commands/stop.ts +107 -8
  70. package/src/commands/watch.test.ts +43 -0
  71. package/src/commands/watch.ts +153 -28
  72. package/src/config.ts +23 -0
  73. package/src/doctor/consistency.test.ts +106 -0
  74. package/src/doctor/consistency.ts +48 -1
  75. package/src/doctor/serve.test.ts +95 -0
  76. package/src/doctor/serve.ts +86 -0
  77. package/src/doctor/types.ts +2 -1
  78. package/src/doctor/watchdog.ts +57 -1
  79. package/src/events/tailer.test.ts +234 -1
  80. package/src/events/tailer.ts +90 -0
  81. package/src/index.ts +57 -6
  82. package/src/insights/quality-gates.test.ts +141 -0
  83. package/src/insights/quality-gates.ts +156 -0
  84. package/src/json.ts +29 -0
  85. package/src/logging/theme.ts +4 -0
  86. package/src/mail/client.ts +15 -2
  87. package/src/mail/store.test.ts +82 -0
  88. package/src/mail/store.ts +41 -4
  89. package/src/merge/lock.test.ts +149 -0
  90. package/src/merge/lock.ts +140 -0
  91. package/src/merge/predict.test.ts +387 -0
  92. package/src/merge/predict.ts +249 -0
  93. package/src/merge/resolver.ts +1 -1
  94. package/src/mulch/client.ts +3 -3
  95. package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
  96. package/src/runtimes/claude.test.ts +791 -1
  97. package/src/runtimes/claude.ts +323 -1
  98. package/src/runtimes/connections.test.ts +141 -1
  99. package/src/runtimes/connections.ts +73 -4
  100. package/src/runtimes/headless-connection.test.ts +264 -0
  101. package/src/runtimes/headless-connection.ts +158 -0
  102. package/src/runtimes/types.ts +10 -0
  103. package/src/schema-consistency.test.ts +1 -0
  104. package/src/sessions/store.test.ts +657 -29
  105. package/src/sessions/store.ts +286 -23
  106. package/src/test-setup.test.ts +31 -0
  107. package/src/test-setup.ts +28 -0
  108. package/src/types.ts +107 -2
  109. package/src/utils/pid.test.ts +85 -1
  110. package/src/utils/pid.ts +86 -1
  111. package/src/utils/process-scan.test.ts +53 -0
  112. package/src/utils/process-scan.ts +76 -0
  113. package/src/watchdog/daemon.test.ts +1607 -376
  114. package/src/watchdog/daemon.ts +462 -88
  115. package/src/watchdog/health.test.ts +282 -0
  116. package/src/watchdog/health.ts +126 -27
  117. package/src/worktree/manager.test.ts +218 -1
  118. package/src/worktree/manager.ts +55 -0
  119. package/src/worktree/process.test.ts +71 -0
  120. package/src/worktree/process.ts +25 -5
  121. package/src/worktree/tmux.test.ts +28 -0
  122. package/src/worktree/tmux.ts +27 -3
  123. package/templates/CLAUDE.md.tmpl +19 -8
  124. package/templates/overlay.md.tmpl +5 -2
@@ -12,7 +12,8 @@ These are named failures. If you catch yourself doing any of these, stop and cor
12
12
 
13
13
  - **READ_ONLY_VIOLATION** -- Using Write, Edit, or any destructive Bash command (git commit, rm, mv, redirect). You are read-only. The only write exception is `ov spec write` (scout only).
14
14
  - **SILENT_FAILURE** -- Encountering an error and not reporting it via mail. Every error must be communicated to your parent with `--type error`.
15
- - **INCOMPLETE_CLOSE** -- Running `{{TRACKER_CLI}} close` without first sending a result mail to your parent summarizing your findings.
15
+ - **MISSING_WORKER_DONE** -- Closing a {{TRACKER_NAME}} issue without first sending `worker_done` mail to parent. The `worker_done` carries your PASS/FAIL verdict; the lead waits on it before sending `merge_ready`.
16
+ - **INCOMPLETE_CLOSE** -- Running `{{TRACKER_CLI}} close` without first sending `worker_done` to your parent summarizing your findings.
16
17
 
17
18
  ## overlay
18
19
 
@@ -51,12 +52,18 @@ The only write exception is `ov spec write` for persisting spec files (scout onl
51
52
 
52
53
  ## completion-protocol
53
54
 
54
- 1. Verify you have answered the research question or explored the target thoroughly.
55
- 2. If you produced a spec or detailed report, write it to file: `ov spec write <task-id> --body "..." --agent <your-name>`.
56
- 3. **Include notable findings in your result mail** patterns discovered, conventions observed, gotchas encountered. Your parent may record these via mulch.
57
- 4. Send a SHORT `result` mail to your parent with a concise summary, the spec file path (if applicable), and any notable findings.
58
- 5. Run `{{TRACKER_CLI}} close <task-id> --reason "<summary of findings>"`.
59
- 6. Stop. Do not continue exploring after closing.
55
+ 1. Verify you have completed the review against the spec and run any quality gates the lead requested.
56
+ 2. **Include notable findings in your result body** — patterns discovered, conventions observed, gotchas encountered. Your parent may record these via mulch.
57
+ 3. Send a SHORT `worker_done` mail to your parent with a concise summary and an explicit PASS or FAIL verdict in the subject:
58
+ ```bash
59
+ ov mail send --to <parent> --subject "Worker done: <task-id> PASS" \
60
+ --body "<summary of what you reviewed, gates that ran, verdict justification>" \
61
+ --type worker_done --agent $OVERSTORY_AGENT_NAME
62
+ ```
63
+ For FAIL, use `--subject "Worker done: <task-id> — FAIL"` and list the specific issues in the body so the builder can revise.
64
+ 4. Run `{{TRACKER_CLI}} close <task-id> --reason "<summary of findings>"`.
65
+
66
+ Sending `worker_done` IS your exit. Your process terminates after the turn ends; do not continue reviewing or run additional commands afterward.
60
67
 
61
68
  ## intro
62
69
 
@@ -84,7 +91,9 @@ You are a validation specialist. Given code to review, you check it for correctn
84
91
  - `ov status` (check swarm state)
85
92
 
86
93
  ### Communication
87
- - **Send mail:** `ov mail send --to <recipient> --subject "<subject>" --body "<body>" --type <status|result|question|error>`
94
+ - **Send mail:** `ov mail send --to <recipient> --subject "<subject>" --body "<body>" --type <status|question|error|worker_done>`
95
+ - `worker_done` is your terminal exit signal — carries your PASS/FAIL verdict. See completion-protocol.
96
+ - `status` for interim progress. `question` for clarifications. `error` for blockers.
88
97
  - **Check mail:** `ov mail check`
89
98
  - **Your agent name** is set via `$OVERSTORY_AGENT_NAME` (provided in your overlay)
90
99
 
@@ -106,19 +115,16 @@ You are a validation specialist. Given code to review, you check it for correctn
106
115
  - Check for: adequate test coverage, meaningful test assertions.
107
116
  5. **Run quality gates:**
108
117
  {{QUALITY_GATE_BASH}}
109
- 6. **Report results** via `{{TRACKER_CLI}} close` with a clear pass/fail summary:
118
+ 6. **Send the terminal `worker_done` mail** to your parent with the PASS/FAIL
119
+ verdict in the subject and detailed feedback in the body (see
120
+ completion-protocol). Do NOT use `--type result` — `worker_done` is the only
121
+ completion signal (overstory-1a4c).
122
+ 7. **Close your issue** with a clear pass/fail summary:
110
123
  ```bash
111
124
  {{TRACKER_CLI}} close <task-id> --reason "PASS: <summary>"
112
125
  # or
113
126
  {{TRACKER_CLI}} close <task-id> --reason "FAIL: <issues found>"
114
127
  ```
115
- 7. **Send detailed review** via mail:
116
- ```bash
117
- ov mail send --to <parent-or-builder> \
118
- --subject "Review: <topic> - PASS/FAIL" \
119
- --body "<detailed feedback, issues found, suggestions>" \
120
- --type result
121
- ```
122
128
 
123
129
  ## review-checklist
124
130
 
package/agents/scout.md CHANGED
@@ -12,7 +12,8 @@ These are named failures. If you catch yourself doing any of these, stop and cor
12
12
 
13
13
  - **READ_ONLY_VIOLATION** -- Using Write, Edit, or any destructive Bash command (git commit, rm, mv, redirect). You are read-only. The only write exception is `ov spec write` (scout only).
14
14
  - **SILENT_FAILURE** -- Encountering an error and not reporting it via mail. Every error must be communicated to your parent with `--type error`.
15
- - **INCOMPLETE_CLOSE** -- Running `{{TRACKER_CLI}} close` without first sending a result mail to your parent summarizing your findings.
15
+ - **MISSING_WORKER_DONE** -- Closing a {{TRACKER_NAME}} issue without first sending `worker_done` mail to parent. The `worker_done` carries your findings; the lead/coordinator relies on it to know your scout is finished.
16
+ - **INCOMPLETE_CLOSE** -- Running `{{TRACKER_CLI}} close` without first sending `worker_done` to your parent summarizing your findings.
16
17
 
17
18
  ## overlay
18
19
 
@@ -53,10 +54,16 @@ The only write exception is `ov spec write` for persisting spec files (scout onl
53
54
 
54
55
  1. Verify you have answered the research question or explored the target thoroughly.
55
56
  2. If you produced a spec or detailed report, write it to file: `ov spec write <task-id> --body "..." --agent <your-name>`.
56
- 3. **Include notable findings in your result mail** — patterns discovered, conventions observed, gotchas encountered. Your parent may record these via mulch.
57
- 4. Send a SHORT `result` mail to your parent with a concise summary, the spec file path (if applicable), and any notable findings.
57
+ 3. **Include notable findings in your `worker_done` body** — patterns discovered, conventions observed, gotchas encountered. Your parent may record these via mulch.
58
+ 4. Send a SHORT `worker_done` mail to your parent with a concise summary, the spec file path (if applicable), and any notable findings:
59
+ ```bash
60
+ ov mail send --to <parent> --subject "Worker done: <task-id>" \
61
+ --body "<summary + spec path + findings>" \
62
+ --type worker_done --agent $OVERSTORY_AGENT_NAME
63
+ ```
58
64
  5. Run `{{TRACKER_CLI}} close <task-id> --reason "<summary of findings>"`.
59
- 6. Stop. Do not continue exploring after closing.
65
+
66
+ Sending `worker_done` IS your exit. Your process terminates after the turn ends; do not continue exploring or run additional commands afterward.
60
67
 
61
68
  ## intro
62
69
 
@@ -86,7 +93,9 @@ You perform reconnaissance. Given a research question, exploration target, or an
86
93
  - `ov status` (check swarm state)
87
94
 
88
95
  ### Communication
89
- - **Send mail:** `ov mail send --to <recipient> --subject "<subject>" --body "<body>" --type <status|result|question>`
96
+ - **Send mail:** `ov mail send --to <recipient> --subject "<subject>" --body "<body>" --type <status|question|error|worker_done>`
97
+ - `worker_done` is your terminal exit signal. See completion-protocol.
98
+ - `status` for interim progress. `question` for clarifications. `error` for blockers.
90
99
  - **Check mail:** `ov mail check`
91
100
  - **Your agent name** is set via `$OVERSTORY_AGENT_NAME` (provided in your overlay)
92
101
 
@@ -109,12 +118,8 @@ You perform reconnaissance. Given a research question, exploration target, or an
109
118
  ov spec write <task-id> --body "<spec content>" --agent <your-agent-name>
110
119
  ```
111
120
  This writes the spec to `.overstory/specs/<task-id>.md`. Do NOT send full specs via mail.
112
- 6. **Notify via short mail** after writing a spec file:
113
- ```bash
114
- ov mail send --to <parent-or-orchestrator> \
115
- --subject "Spec ready: <task-id>" \
116
- --body "Spec written to .overstory/specs/<task-id>.md — <one-line summary>" \
117
- --type result
118
- ```
121
+ 6. **Send the terminal `worker_done` mail** to your parent (see completion-protocol).
119
122
  Keep the mail body SHORT (one or two sentences). The spec file has the details.
123
+ Do NOT use `--type result` for this — `worker_done` is the only completion
124
+ signal (overstory-1a4c).
120
125
  7. **Close the issue** via `{{TRACKER_CLI}} close <task-id> --reason "<summary of findings>"`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.9.4",
3
+ "version": "0.11.0",
4
4
  "description": "Multi-agent orchestration for AI coding agents — spawn workers in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution. Pluggable runtime adapters for Claude Code, Pi, and more.",
5
5
  "author": "Jaymin West",
6
6
  "license": "MIT",
@@ -28,7 +28,8 @@
28
28
  "files": [
29
29
  "src",
30
30
  "agents",
31
- "templates"
31
+ "templates",
32
+ "ui/dist"
32
33
  ],
33
34
  "publishConfig": {
34
35
  "access": "public"
@@ -41,7 +42,9 @@
41
42
  "lint": "biome check .",
42
43
  "lint:fix": "biome check --write .",
43
44
  "typecheck": "tsc --noEmit",
44
- "version:bump": "bun scripts/version-bump.ts"
45
+ "version:bump": "bun scripts/version-bump.ts",
46
+ "build:ui": "cd ui && bun install && bun run build",
47
+ "prepack": "bun run build:ui"
45
48
  },
46
49
  "dependencies": {
47
50
  "@os-eco/mulch-cli": "^0.6.2",
@@ -0,0 +1,85 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ isPersistentCapability,
4
+ isStopHookPersistentCapability,
5
+ isTaskScopedCapability,
6
+ PERSISTENT_CAPABILITIES,
7
+ STOP_HOOK_PERSISTENT_CAPABILITIES,
8
+ TASK_SCOPED_CAPABILITIES,
9
+ terminalMailTypesFor,
10
+ } from "./capabilities.ts";
11
+
12
+ describe("TASK_SCOPED_CAPABILITIES", () => {
13
+ test("contains the five worker capabilities", () => {
14
+ expect([...TASK_SCOPED_CAPABILITIES].sort()).toEqual([
15
+ "builder",
16
+ "lead",
17
+ "merger",
18
+ "reviewer",
19
+ "scout",
20
+ ]);
21
+ });
22
+
23
+ test("excludes persistent capabilities", () => {
24
+ for (const c of ["coordinator", "orchestrator", "monitor", "supervisor"]) {
25
+ expect(isTaskScopedCapability(c)).toBe(false);
26
+ }
27
+ });
28
+ });
29
+
30
+ describe("terminalMailTypesFor", () => {
31
+ test("merger uses merged + merge_failed", () => {
32
+ expect(terminalMailTypesFor("merger")).toEqual(["merged", "merge_failed"]);
33
+ });
34
+
35
+ test("builder/scout/reviewer/lead accept worker_done and result", () => {
36
+ // `result` is a legacy/drift fallback (overstory-1a4c): models often pick
37
+ // `--type result` for their terminal summary because the prompts also use
38
+ // `result` for non-terminal updates elsewhere. Accepting both keeps the
39
+ // lifecycle unstuck without losing the canonical `worker_done` contract.
40
+ for (const c of ["builder", "scout", "reviewer", "lead"]) {
41
+ expect(terminalMailTypesFor(c)).toEqual(["worker_done", "result"]);
42
+ }
43
+ });
44
+
45
+ test("unknown capability falls back to worker_done set", () => {
46
+ expect(terminalMailTypesFor("unknown")).toEqual(["worker_done", "result"]);
47
+ });
48
+ });
49
+
50
+ describe("PERSISTENT_CAPABILITIES", () => {
51
+ test("contains coordinator, orchestrator, monitor only", () => {
52
+ expect([...PERSISTENT_CAPABILITIES].sort()).toEqual(["coordinator", "monitor", "orchestrator"]);
53
+ });
54
+
55
+ test("excludes lead and other workers", () => {
56
+ for (const c of ["lead", "builder", "scout", "reviewer", "merger", "supervisor"]) {
57
+ expect(isPersistentCapability(c)).toBe(false);
58
+ }
59
+ });
60
+ });
61
+
62
+ describe("STOP_HOOK_PERSISTENT_CAPABILITIES", () => {
63
+ test("equals PERSISTENT_CAPABILITIES plus lead", () => {
64
+ // Tmux-mode leads span many model turns within a single dispatch
65
+ // (overstory-49a7), so the per-turn Stop hook is NOT a "done" signal.
66
+ expect([...STOP_HOOK_PERSISTENT_CAPABILITIES].sort()).toEqual([
67
+ "coordinator",
68
+ "lead",
69
+ "monitor",
70
+ "orchestrator",
71
+ ]);
72
+ });
73
+
74
+ test("isStopHookPersistentCapability is true for lead and the persistent set", () => {
75
+ for (const c of ["coordinator", "orchestrator", "monitor", "lead"]) {
76
+ expect(isStopHookPersistentCapability(c)).toBe(true);
77
+ }
78
+ });
79
+
80
+ test("excludes non-lead workers", () => {
81
+ for (const c of ["builder", "scout", "reviewer", "merger", "supervisor"]) {
82
+ expect(isStopHookPersistentCapability(c)).toBe(false);
83
+ }
84
+ });
85
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared capability classification for the runtime layer.
3
+ *
4
+ * Three orthogonal classifications:
5
+ *
6
+ * - `TASK_SCOPED_CAPABILITIES` — worker capabilities that operate under the
7
+ * spawn-per-turn model. Each user turn (mail, nudge, initial dispatch)
8
+ * spawns a fresh claude with `--resume <session-id>`, processes the turn,
9
+ * and exits on stdin EOF. There is no persistent process between turns.
10
+ *
11
+ * - `PERSISTENT_CAPABILITIES` — capabilities that run as long-lived sessions
12
+ * for the lifetime of a run/operator interaction (coordinator, orchestrator,
13
+ * monitor). Used by the watchdog to exempt them from time-based stale/zombie
14
+ * detection and exclude them from run-completion accounting.
15
+ *
16
+ * - `STOP_HOOK_PERSISTENT_CAPABILITIES` — capabilities whose Claude Code Stop
17
+ * hook fires per model turn rather than once at process exit. The same as
18
+ * `PERSISTENT_CAPABILITIES` plus `lead`, because tmux-mode leads still span
19
+ * many model turns within a single dispatch (overstory-49a7).
20
+ *
21
+ * Phase 4 (overstory-2724) consolidated these definitions here as the single
22
+ * source of truth — previously they were duplicated across log.ts, health.ts,
23
+ * and daemon.ts with a load-bearing `lead` mismatch.
24
+ */
25
+
26
+ import type { Capability } from "../types.ts";
27
+
28
+ /** Worker capabilities driven by the spawn-per-turn engine. */
29
+ export const TASK_SCOPED_CAPABILITIES = new Set<Capability>([
30
+ "builder",
31
+ "scout",
32
+ "reviewer",
33
+ "merger",
34
+ "lead",
35
+ ]);
36
+
37
+ /** True iff `capability` is a task-scoped worker (drives spawn-per-turn). */
38
+ export function isTaskScopedCapability(capability: string): boolean {
39
+ return TASK_SCOPED_CAPABILITIES.has(capability as Capability);
40
+ }
41
+
42
+ /**
43
+ * Capabilities that run as long-lived sessions for the duration of a run /
44
+ * operator interaction.
45
+ *
46
+ * `coordinator` and `orchestrator` are operator-driven persistent agents (one
47
+ * tmux session per run / multi-run); `monitor` is a continuous fleet patrol
48
+ * with its own long-lived session. They are expected to have long idle
49
+ * periods (waiting for worker mail, operator input) and must NOT be flagged
50
+ * stale/zombie based on `lastActivity` — only tmux/pid liveness applies.
51
+ *
52
+ * Consumers:
53
+ * - `src/watchdog/health.ts` — exempt from time-based stale/zombie detection
54
+ * - `src/watchdog/daemon.ts` — excluded from run-completion accounting
55
+ *
56
+ * Note: `lead` is NOT in this set. Under spawn-per-turn (the default), each
57
+ * lead turn is its own short-lived process; under tmux mode the lead still
58
+ * runs to a terminal `worker_done` and exits, so it counts toward
59
+ * run-completion accounting. The per-model-turn nuance for tmux-mode leads
60
+ * is captured separately by `STOP_HOOK_PERSISTENT_CAPABILITIES`.
61
+ */
62
+ export const PERSISTENT_CAPABILITIES = new Set<Capability>([
63
+ "coordinator",
64
+ "orchestrator",
65
+ "monitor",
66
+ ]);
67
+
68
+ /** True iff `capability` is a long-lived persistent session. */
69
+ export function isPersistentCapability(capability: string): boolean {
70
+ return PERSISTENT_CAPABILITIES.has(capability as Capability);
71
+ }
72
+
73
+ /**
74
+ * Capabilities whose Claude Code Stop hook fires per **model turn** rather
75
+ * than once at process exit. Under tmux mode these agents have many model
76
+ * turns within a single dispatch, so a `session-end` event is NOT a reliable
77
+ * "agent done" signal — the hook fires every time the model finishes
78
+ * generating a response and waits for the next user turn.
79
+ *
80
+ * Equals `PERSISTENT_CAPABILITIES` plus `lead`. Tmux-mode leads delegate to
81
+ * sub-workers and process mail injection over many turns (overstory-49a7);
82
+ * marking the lead `completed` on the first Stop hook fire makes it vanish
83
+ * from `getActive()` while its tmux process is still working.
84
+ *
85
+ * The tmux-mode-only nuance does not apply to spawn-per-turn (headless) leads:
86
+ * (a) they run one model turn per process; (b) `hooks-deployer.ts` drops the
87
+ * Stop hook entirely under `headlessOnly=true`, so this gate is a no-op for
88
+ * them. Including `lead` here is therefore safe in both modes.
89
+ *
90
+ * Consumer:
91
+ * - `src/commands/log.ts` — guards `transitionToCompleted`,
92
+ * `autoRecordExpertise`, and `appendOutcomeToAppliedRecords` so they do
93
+ * not fire on every turn boundary.
94
+ */
95
+ export const STOP_HOOK_PERSISTENT_CAPABILITIES = new Set<Capability>([
96
+ ...PERSISTENT_CAPABILITIES,
97
+ "lead" as Capability,
98
+ ]);
99
+
100
+ /** True iff `capability`'s Stop hook is per-model-turn (not per-session-exit). */
101
+ export function isStopHookPersistentCapability(capability: string): boolean {
102
+ return STOP_HOOK_PERSISTENT_CAPABILITIES.has(capability as Capability);
103
+ }
104
+
105
+ /**
106
+ * Mail types that signal an agent's terminal action for its capability.
107
+ *
108
+ * The turn-runner watches for any of these from the agent during a turn; when
109
+ * observed alongside a clean `result` event, the agent is transitioned to
110
+ * `completed`. Capabilities not listed here use the worker_done set by default.
111
+ *
112
+ * - builder | scout | reviewer | lead → `worker_done` (canonical) or `result`
113
+ * (legacy/drift fallback). Agent prompts treat `worker_done` as the
114
+ * completion signal, but the model frequently picks `result` because it is
115
+ * also a valid mail type used for non-terminal summaries elsewhere in the
116
+ * protocol. Accepting `result` keeps the lifecycle from getting stuck on
117
+ * prompt drift (overstory-1a4c). Combined with `cleanResult` (claude exited
118
+ * cleanly at end-of-turn), this is safe: an interim `result` mid-turn does
119
+ * not transition the agent because the turn has not ended yet.
120
+ * - merger → `merged` (success) or `merge_failed` (failure)
121
+ */
122
+ export function terminalMailTypesFor(capability: string): readonly string[] {
123
+ if (capability === "merger") return ["merged", "merge_failed"];
124
+ return ["worker_done", "result"];
125
+ }