@jaggerxtrm/specialists 3.17.0 → 3.18.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 (246) hide show
  1. package/README.md +268 -134
  2. package/config/mandatory-rules/json-only-final-output.md +13 -0
  3. package/config/mandatory-rules/service-skills-diff-scan-mandatory.md +26 -0
  4. package/config/mandatory-rules/service-skills-gitnexus-triage.md +5 -0
  5. package/config/skills/setup-specialists/SKILL.md +556 -0
  6. package/config/skills/specialists-creator/SKILL.md +132 -4
  7. package/config/skills/specialists-creator/scripts/audit-spec-uniformity.mjs +1 -1
  8. package/config/specialists/bare.specialist.json +3 -3
  9. package/config/specialists/changelog-drafter.specialist.json +5 -4
  10. package/config/specialists/changelog-keeper.specialist.json +7 -6
  11. package/config/specialists/debugger.specialist.json +2 -2
  12. package/config/specialists/executor.specialist.json +2 -2
  13. package/config/specialists/explorer.specialist.json +4 -4
  14. package/config/specialists/memory-processor.specialist.json +3 -3
  15. package/config/specialists/node-coordinator.specialist.json +3 -3
  16. package/config/specialists/obligations-scanner.specialist.json +67 -17
  17. package/config/specialists/overthinker.specialist.json +3 -3
  18. package/config/specialists/planner.specialist.json +4 -4
  19. package/config/specialists/quant-methodologist.specialist.json +145 -0
  20. package/config/specialists/quant-researcher.specialist.json +144 -0
  21. package/config/specialists/researcher.specialist.json +5 -5
  22. package/config/specialists/reviewer.specialist.json +1 -1
  23. package/config/specialists/seconder.specialist.json +2 -2
  24. package/config/specialists/security-auditor.specialist.json +2 -2
  25. package/config/specialists/service-skills-sync.specialist.json +90 -75
  26. package/config/specialists/specialists-creator.specialist.json +4 -4
  27. package/config/specialists/sync-docs.specialist.json +4 -4
  28. package/config/specialists/test-engineer.specialist.json +3 -3
  29. package/config/specialists/test-runner.specialist.json +7 -7
  30. package/config/specialists/transcriber.specialist.json +3 -3
  31. package/config/specialists/xt-merge.specialist.json +4 -4
  32. package/dist/asset-contract.json +21 -2
  33. package/dist/index.js +25704 -16376
  34. package/dist/lib.js +9849 -6147
  35. package/dist/types/cli/console/components.d.ts +83 -0
  36. package/dist/types/cli/console/components.d.ts.map +1 -0
  37. package/dist/types/cli/console/config-source.d.ts +58 -0
  38. package/dist/types/cli/console/config-source.d.ts.map +1 -0
  39. package/dist/types/cli/console/forensic.d.ts +11 -0
  40. package/dist/types/cli/console/forensic.d.ts.map +1 -0
  41. package/dist/types/cli/console/git.d.ts +28 -0
  42. package/dist/types/cli/console/git.d.ts.map +1 -0
  43. package/dist/types/cli/console/help.d.ts +2 -0
  44. package/dist/types/cli/console/help.d.ts.map +1 -0
  45. package/dist/types/cli/console/log.d.ts +13 -0
  46. package/dist/types/cli/console/log.d.ts.map +1 -0
  47. package/dist/types/cli/console/repo-config.d.ts +26 -0
  48. package/dist/types/cli/console/repo-config.d.ts.map +1 -0
  49. package/dist/types/cli/console/repo-discovery.d.ts +12 -0
  50. package/dist/types/cli/console/repo-discovery.d.ts.map +1 -0
  51. package/dist/types/cli/console/runtime.d.ts +12 -0
  52. package/dist/types/cli/console/runtime.d.ts.map +1 -0
  53. package/dist/types/cli/console/subscribe-prototype.d.ts +18 -0
  54. package/dist/types/cli/console/subscribe-prototype.d.ts.map +1 -0
  55. package/dist/types/cli/console/theme.d.ts +91 -0
  56. package/dist/types/cli/console/theme.d.ts.map +1 -0
  57. package/dist/types/cli/console/types.d.ts +231 -0
  58. package/dist/types/cli/console/types.d.ts.map +1 -0
  59. package/dist/types/cli/console/view-model.d.ts +252 -0
  60. package/dist/types/cli/console/view-model.d.ts.map +1 -0
  61. package/dist/types/cli/console.d.ts +2 -0
  62. package/dist/types/cli/console.d.ts.map +1 -0
  63. package/dist/types/cli/db.d.ts.map +1 -1
  64. package/dist/types/cli/doctor.d.ts.map +1 -1
  65. package/dist/types/cli/edit.d.ts.map +1 -1
  66. package/dist/types/cli/epic.d.ts.map +1 -1
  67. package/dist/types/cli/feed.d.ts.map +1 -1
  68. package/dist/types/cli/forensic.d.ts +2 -0
  69. package/dist/types/cli/forensic.d.ts.map +1 -0
  70. package/dist/types/cli/format-helpers.d.ts +4 -2
  71. package/dist/types/cli/format-helpers.d.ts.map +1 -1
  72. package/dist/types/cli/help.d.ts.map +1 -1
  73. package/dist/types/cli/init.d.ts +10 -0
  74. package/dist/types/cli/init.d.ts.map +1 -1
  75. package/dist/types/cli/list.d.ts.map +1 -1
  76. package/dist/types/cli/log.d.ts.map +1 -1
  77. package/dist/types/cli/metrics.d.ts +2 -0
  78. package/dist/types/cli/metrics.d.ts.map +1 -0
  79. package/dist/types/cli/ps.d.ts.map +1 -1
  80. package/dist/types/cli/result.d.ts.map +1 -1
  81. package/dist/types/cli/run.d.ts +1 -0
  82. package/dist/types/cli/run.d.ts.map +1 -1
  83. package/dist/types/cli/script.d.ts +3 -0
  84. package/dist/types/cli/script.d.ts.map +1 -1
  85. package/dist/types/cli/serve.d.ts.map +1 -1
  86. package/dist/types/cli/setup.d.ts +19 -1
  87. package/dist/types/cli/setup.d.ts.map +1 -1
  88. package/dist/types/cli/status.d.ts.map +1 -1
  89. package/dist/types/cli/version-check.d.ts +1 -0
  90. package/dist/types/cli/version-check.d.ts.map +1 -1
  91. package/dist/types/index.d.ts +1 -1
  92. package/dist/types/pi/session.d.ts +11 -1
  93. package/dist/types/pi/session.d.ts.map +1 -1
  94. package/dist/types/server.d.ts +15 -0
  95. package/dist/types/server.d.ts.map +1 -1
  96. package/dist/types/specialist/benchmarks.d.ts +37 -0
  97. package/dist/types/specialist/benchmarks.d.ts.map +1 -0
  98. package/dist/types/specialist/chain-identity.d.ts +7 -1
  99. package/dist/types/specialist/chain-identity.d.ts.map +1 -1
  100. package/dist/types/specialist/control.d.ts.map +1 -1
  101. package/dist/types/specialist/forensic-events.d.ts +138 -0
  102. package/dist/types/specialist/forensic-events.d.ts.map +1 -0
  103. package/dist/types/specialist/forensic-renderer.d.ts +34 -0
  104. package/dist/types/specialist/forensic-renderer.d.ts.map +1 -0
  105. package/dist/types/specialist/git-diff-evidence.d.ts +28 -0
  106. package/dist/types/specialist/git-diff-evidence.d.ts.map +1 -0
  107. package/dist/types/specialist/global-config.d.ts +389 -0
  108. package/dist/types/specialist/global-config.d.ts.map +1 -0
  109. package/dist/types/specialist/launch.d.ts +1 -0
  110. package/dist/types/specialist/launch.d.ts.map +1 -1
  111. package/dist/types/specialist/live-aggregates.d.ts +46 -0
  112. package/dist/types/specialist/live-aggregates.d.ts.map +1 -0
  113. package/dist/types/specialist/loader.d.ts +50 -1
  114. package/dist/types/specialist/loader.d.ts.map +1 -1
  115. package/dist/types/specialist/model-chain.d.ts +7 -0
  116. package/dist/types/specialist/model-chain.d.ts.map +1 -0
  117. package/dist/types/specialist/model-probes.d.ts +28 -0
  118. package/dist/types/specialist/model-probes.d.ts.map +1 -0
  119. package/dist/types/specialist/node-contract.d.ts +18 -18
  120. package/dist/types/specialist/node-supervisor.d.ts.map +1 -1
  121. package/dist/types/specialist/observability-db.d.ts +1 -1
  122. package/dist/types/specialist/observability-db.d.ts.map +1 -1
  123. package/dist/types/specialist/observability-sqlite.d.ts +25 -0
  124. package/dist/types/specialist/observability-sqlite.d.ts.map +1 -1
  125. package/dist/types/specialist/preset-resolver.d.ts +56 -0
  126. package/dist/types/specialist/preset-resolver.d.ts.map +1 -0
  127. package/dist/types/specialist/prometheus-projection.d.ts +25 -0
  128. package/dist/types/specialist/prometheus-projection.d.ts.map +1 -0
  129. package/dist/types/specialist/runner.d.ts +26 -1
  130. package/dist/types/specialist/runner.d.ts.map +1 -1
  131. package/dist/types/specialist/schema.d.ts +163 -54
  132. package/dist/types/specialist/schema.d.ts.map +1 -1
  133. package/dist/types/specialist/script-runner.d.ts +5 -1
  134. package/dist/types/specialist/script-runner.d.ts.map +1 -1
  135. package/dist/types/specialist/snapshot-diff.d.ts +8 -0
  136. package/dist/types/specialist/snapshot-diff.d.ts.map +1 -0
  137. package/dist/types/specialist/source-queue.d.ts +13 -0
  138. package/dist/types/specialist/source-queue.d.ts.map +1 -0
  139. package/dist/types/specialist/supervisor.d.ts +15 -1
  140. package/dist/types/specialist/supervisor.d.ts.map +1 -1
  141. package/dist/types/specialist/timeline-events.d.ts +68 -1
  142. package/dist/types/specialist/timeline-events.d.ts.map +1 -1
  143. package/dist/types/tools/specialist/resume_specialist.tool.d.ts +4 -4
  144. package/dist/types/tools/specialist/specialist_status.tool.d.ts +1 -1
  145. package/dist/types/tools/specialist/steer_specialist.tool.d.ts +4 -4
  146. package/dist/types/tools/specialist/use_specialist.tool.d.ts +16 -16
  147. package/docs/ARCHITECTURE.md +1176 -0
  148. package/docs/TODO.md +9 -0
  149. package/docs/architecture.md +11 -0
  150. package/docs/archive/2026-03-13-bd-decision-context-template-plan.md +96 -0
  151. package/docs/archive/AGENT-HANDOFF.md +351 -0
  152. package/docs/archive/PARITY-ANALYSIS.md +296 -0
  153. package/docs/archive/SPECIALISTS_REFACTOR.md.md +30 -0
  154. package/docs/archive/cc-programmatic.md +216 -0
  155. package/docs/archive/claude-agent-sdk.md +594 -0
  156. package/docs/archive/decision-specialist-directory.md +41 -0
  157. package/docs/archive/discoveries.md +148 -0
  158. package/docs/archive/executor-benchmark-protocol.md +198 -0
  159. package/docs/archive/future-features.md +66 -0
  160. package/docs/archive/gzrx-completion-critique.md +183 -0
  161. package/docs/archive/gzrx-research-notes.md +401 -0
  162. package/docs/archive/gzrx-tool-catalog.md +760 -0
  163. package/docs/archive/iron-review-hardening-qa-chain-substrate.md +322 -0
  164. package/docs/archive/iron-review-hardening.html +1004 -0
  165. package/docs/archive/issuetracking.md +312 -0
  166. package/docs/archive/qa-v3.0.2.md +220 -0
  167. package/docs/archive/restructure.md +231 -0
  168. package/docs/archive/script-specialists.md +1254 -0
  169. package/docs/archive/spec-v3.md +792 -0
  170. package/docs/archive/specialist-stats.md +127 -0
  171. package/docs/archive/specialists-friction-audit.md +1347 -0
  172. package/docs/archive/specialists-runtime-critique.md +170 -0
  173. package/docs/archive/specialists-service-evaluation.md +713 -0
  174. package/docs/archive/specialists-substrate-alignment.md +255 -0
  175. package/docs/archive/substrate-review.md +1288 -0
  176. package/docs/archive/test-writer-specialist.md +254 -0
  177. package/docs/archive/using-specialists-v3-improvements-2026-05-09.md +600 -0
  178. package/docs/archive/xtrm-specialists-analysis.md +314 -0
  179. package/docs/authoring.md +701 -0
  180. package/docs/background-jobs.md +203 -0
  181. package/docs/bare-specialists.md +83 -0
  182. package/docs/benchmarks/executor-benchmark-runner.md +66 -0
  183. package/docs/bootstrap.md +161 -0
  184. package/docs/cli-reference.md +1645 -0
  185. package/docs/deploying-alongside.md +155 -0
  186. package/docs/design/README.md +36 -0
  187. package/docs/design/darth-feedor-migration.md +290 -0
  188. package/docs/design/roadmap/README.md +32 -0
  189. package/docs/design/roadmap/chain-templates/README.md +146 -0
  190. package/docs/design/roadmap/chain-templates/code-quick.formula.json +27 -0
  191. package/docs/design/roadmap/chain-templates/code-standard.formula.json +126 -0
  192. package/docs/design/roadmap/chain-templates/code-with-advisors.formula.json +128 -0
  193. package/docs/design/roadmap/chain-templates/code-with-tests.formula.json +76 -0
  194. package/docs/design/roadmap/chain-templates/debug.formula.json +128 -0
  195. package/docs/design/roadmap/chain-templates/doc-sync.formula.json +28 -0
  196. package/docs/design/roadmap/chain-templates/memory-hygiene.formula.json +27 -0
  197. package/docs/design/roadmap/chain-templates/planning.formula.json +27 -0
  198. package/docs/design/roadmap/chain-templates/premortem.formula.json +26 -0
  199. package/docs/design/roadmap/chain-templates/release-prep.formula.json +36 -0
  200. package/docs/design/roadmap/chain-templates/research-only.formula.json +28 -0
  201. package/docs/design/roadmap/chain-templates/restitch.formula.json +125 -0
  202. package/docs/design/roadmap/chain-templates/security-deep.formula.json +161 -0
  203. package/docs/design/roadmap/chain-templates/test-only.formula.json +52 -0
  204. package/docs/design/roadmap/chain-templates/triage.formula.json +35 -0
  205. package/docs/design/roadmap/history/handoff-from-substrate-design.md +87 -0
  206. package/docs/design/roadmap/history/substrate-reconciliation.md +105 -0
  207. package/docs/design/roadmap/specialists-roadmap.md +1193 -0
  208. package/docs/design/sp-console-subscribe-via-materializer.md +231 -0
  209. package/docs/design/sp-console-tui-mock-v2.html +293 -0
  210. package/docs/design/sp-console-tui-mock.html +120 -0
  211. package/docs/design/sp-console-tui.md +340 -0
  212. package/docs/design/specialist-agentops-suite.md +323 -0
  213. package/docs/design/substrate/channels.md +14 -0
  214. package/docs/design/substrate/devops-platform-engineering-prd.md +446 -0
  215. package/docs/design/substrate/html-design-example.md +339 -0
  216. package/docs/design/xtrm-tiers-architecture.svg +132 -0
  217. package/docs/devops/dependency-verdict-materialization.md +46 -0
  218. package/docs/epic-readiness.md +75 -0
  219. package/docs/examples/mercury-atomic-summarizer.specialist.json +26 -0
  220. package/docs/examples/smoke-echo-text-expected-keys.specialist.json +25 -0
  221. package/docs/examples/smoke-echo.specialist.json +25 -0
  222. package/docs/features.md +1577 -0
  223. package/docs/hooks.md +81 -0
  224. package/docs/installation.md +142 -0
  225. package/docs/manifest.md +184 -0
  226. package/docs/mcp-servers.md +73 -0
  227. package/docs/mcp-tools.md +71 -0
  228. package/docs/nodes.md +231 -0
  229. package/docs/observability-metrics.md +152 -0
  230. package/docs/operator/sp-console-v2-walkthrough.md +410 -0
  231. package/docs/overrides-guide.md +306 -0
  232. package/docs/pi-rpc-boundary.md +118 -0
  233. package/docs/pi-session.md +195 -0
  234. package/docs/release.md +22 -0
  235. package/docs/skills.md +132 -0
  236. package/docs/specialists/handoff-schema.md +181 -0
  237. package/docs/specialists-catalog.md +99 -0
  238. package/docs/specialists-service-install.md +226 -0
  239. package/docs/specialists-service.md +363 -0
  240. package/docs/surface-ownership.md +138 -0
  241. package/docs/upgrade-notes/kan-90-global-user-config.md +114 -0
  242. package/docs/upgrade-notes/kan-91-expanded-overrides.md +261 -0
  243. package/docs/workflow.md +114 -0
  244. package/docs/worktree.md +71 -0
  245. package/docs/worktrees.md +309 -0
  246. package/package.json +6 -3
@@ -0,0 +1,1176 @@
1
+ ---
2
+ title: Specialists Runtime Architecture
3
+ scope: architecture
4
+ category: reference
5
+ version: 3.4.2
6
+ updated: 2026-06-23
7
+ synced_at: bf6baf7a
8
+ description: Event pipeline, Pi RPC adapter boundaries, Supervisor lifecycle ownership, schema v1→v4 migration chain, JSON-first dual-write persistence, node runtime tables, context window tracking, job lineage fields, context denormalization, sp ps CLI surface, worktree/bead ownership semantics, and worktree write-boundary enforcement via generated Pi extensions.
9
+ source_of_truth_for:
10
+ - "src/specialist/job-root.ts"
11
+ - "src/specialist/worktree.ts"
12
+ - "src/specialist/timeline-events.ts"
13
+ - "src/pi/session.ts"
14
+ - "src/specialist/supervisor.ts"
15
+ - "src/cli/ps.ts"
16
+ - "src/cli/merge.ts"
17
+ - "src/cli/epic.ts"
18
+ - "src/cli/end.ts"
19
+ - "src/specialist/epic-lifecycle.ts"
20
+ - "src/specialist/chain-identity.ts"
21
+ - "src/specialist/epic-readiness.ts"
22
+ - "pi/rpc/"
23
+ domain:
24
+ - architecture
25
+ - rpc
26
+ - supervisor
27
+ - timeline
28
+ - worktrees
29
+ - jobs
30
+ ---
31
+
32
+ # Specialists Runtime Architecture
33
+
34
+ This document defines the runtime boundary between:
35
+
36
+ - **Pi RPC protocol** (`pi/rpc/`) — canonical transport and event contract
37
+ - **RPC adapter** (`src/pi/session.ts`) — process bridge + request/response correlation
38
+ - **Lifecycle owner** (`src/specialist/supervisor.ts`) — durable state, persistence, completion semantics, GitNexus tracking
39
+ - **Timeline model** (`src/specialist/timeline-events.ts`) — persisted event vocabulary for feed v2
40
+ - **Worktree isolation** (`src/specialist/worktree.ts`) — isolated git workspaces per executor
41
+ - **Job registry anchor** (`src/specialist/job-root.ts`) — git-common-root-anchored job state
42
+
43
+ ## 0) Runner context injection at spawn
44
+
45
+ `src/specialist/runner.ts` injects context into the specialist's first-turn prompt before spawning the Pi session. The injection pipeline uses keyword-filtered memory retrieval from a local FTS cache, replacing the previous full `bd prime` dump.
46
+
47
+ ### Injection pipeline (in order)
48
+
49
+ | # | Source | Tokens | Condition | Purpose |
50
+ |---|--------|--------|-----------|--------|
51
+ | 0 | Caveman-micro output directive | ~80 | Always | Terse agent-to-agent output style (+26pp accuracy, ~65% token reduction) |
52
+ | 1 | GitNexus workflow mandate | ~200 | `.gitnexus/meta.json` exists | Mandatory code intelligence usage rules |
53
+ | — | `.xtrm/memory.md` | — | **Not injected by runner** | Injected by xtrm-loader Pi extension (`before_agent_start`) — saves ~800 tokens |
54
+ | 2 | Static workflow rules block | ~60 | Always | `STATIC_WORKFLOW_RULES_BLOCK` from `memory-retrieval.ts` (bead claim/close/remember commands) |
55
+ | 3 | Keyword-filtered memories | ~0-600 | `--bead <id>` provided | `buildFilteredMemoryInjection()` from `memory-retrieval.ts` — FTS query using bead title/description keywords |
56
+ | 4 | GitNexus pre-query snapshot | ~0-200 | `.gitnexus/meta.json` exists + symbol-like tokens in bead title | Pre-resolved caller/callee/process summaries for top 2 CamelCase symbols |
57
+
58
+ ### Keyword-filtered memory retrieval (`memory-retrieval.ts`)
59
+
60
+ Replaced the previous full `bd prime` dump (~3000 tokens) with targeted retrieval:
61
+
62
+ ```typescript
63
+ import { buildFilteredMemoryInjection, STATIC_WORKFLOW_RULES_BLOCK } from './memory-retrieval.js';
64
+
65
+ const memoryInjection = buildFilteredMemoryInjection({
66
+ cwd: runCwd,
67
+ beadTitle: beadForMemory.title,
68
+ beadDescription: beadForMemory.description,
69
+ });
70
+ // Returns: { block: string, memories: MemoryRecord[], estimatedTokens: number }
71
+ ```
72
+
73
+ Key parameters:
74
+ - `MAX_KEYWORDS = 6` — max search tokens extracted from bead context
75
+ - `MAX_MEMORIES = 10` — max matching memories returned
76
+ - `MAX_MEMORY_TOKENS = 600` — token budget ceiling
77
+ - `CACHE_MAX_AGE_MS = 3600000` (1h) — FTS cache staleness threshold
78
+
79
+ The FTS cache is a SQLite table (`specialist_memories_cache`) populated from `bd memories` output. Cache sync triggers:
80
+ - `specialists init` — full bootstrap sync
81
+ - PostToolUse hook (`specialists-memory-cache-sync.mjs`) — incremental sync after memory mutations
82
+ - `sp memory sync` / `sp memory refresh` — manual CLI
83
+
84
+ ### Extension opt-out
85
+
86
+ Specialists can opt out of specific npm extensions via `execution.extensions`:
87
+
88
+ ```typescript
89
+ const excludeExtensions = [
90
+ execution.extensions?.serena === false ? 'pi-serena-tools' : undefined,
91
+ execution.extensions?.gitnexus === false ? 'pi-gitnexus' : undefined,
92
+ ].filter(Boolean);
93
+ ```
94
+
95
+ Excluded extensions are passed to `PiAgentSession` via `excludeExtensions` option and skipped during `-e` assembly.
96
+
97
+ ### serena-pool pre-spawn hook
98
+
99
+ Before spawning the `pi` child, `session.ts` dynamically imports `ensureSerenaForRoot` from the globally installed `@jaggerxtrm/pi-extensions/extensions/serena-pool`. The pool:
100
+
101
+ 1. Hashes the absolute `sessionCwd` (git root or fallback) to a deterministic port in 40000–44999.
102
+ 2. Reuses an already-listening daemon on that port, or acquires a per-port file lock, reaps orphaned LSP children left by a dead prior daemon (PGID-only, ownership-verified), spawns a fresh `uvx serena start-mcp-server --transport streamable-http --project <root>` in its own process group, and persists `{ pid, pgid, startTime, instanceId, projectRoot, port }` to `/tmp/serena-pool/pool-<port>.json`.
103
+ 3. Returns the port, which `session.ts` injects as `SERENA_MCP_PORT` in `baseEnv`.
104
+
105
+ `pi-serena-tools` reads `SERENA_MCP_PORT` at extension construction time and reuses the shared daemon (its `isServerAvailable()` check sees the port live, so it does not spawn its own). The daemon survives Pi exit (`detached: true`, parent unrefs), and the next session reuses or reaps it.
106
+
107
+ The hook is skipped when `pi-serena-tools` is excluded (i.e. specialists with `execution.extensions.serena=false`). Requires Bun (the `sp` shebang) for the `.ts` dynamic import.
108
+
109
+ ### `memory_injection` timeline event
110
+
111
+ Supervisor records token accounting for each specialist spawn:
112
+
113
+ ```json
114
+ {
115
+ "type": "meta",
116
+ "model": "memory_injection",
117
+ "backend": "injected",
118
+ "memory_injection": {
119
+ "static_tokens": 60,
120
+ "memory_tokens": 400,
121
+ "gitnexus_tokens": 150,
122
+ "total_tokens": 610
123
+ }
124
+ }
125
+ ```
126
+
127
+ This enables post-hoc analysis of context budget allocation across runs.
128
+
129
+ ### Non-fatal behavior
130
+
131
+ All injection sources are optional and non-blocking:
132
+ - Missing FTS cache → no keyword-filtered memories (static rules still inject)
133
+ - `.gitnexus/meta.json` missing → no GitNexus mandate or pre-query
134
+ - GitNexus CLI unavailable → pre-query skipped silently
135
+ - Extension opt-out → extension simply not loaded (no error)
136
+
137
+ This ensures specialist runs work in minimal environments (fresh clones, CI) while benefiting from full context in mature setups.
138
+
139
+ ---
140
+
141
+ ## 1) Canonical protocol boundary: `pi/rpc/`
142
+
143
+ `pi/rpc/` is the protocol source of truth for:
144
+
145
+ - JSONL framing (`jsonl.ts`)
146
+ - command/response/event types (`rpc-types.ts`)
147
+ - runtime behavior (`rpc-mode.ts`)
148
+ - typed client semantics (`rpc-client.ts`)
149
+
150
+ Specialists does **not** redefine protocol semantics. It consumes Pi events and commands through an adapter layer.
151
+
152
+ ## 2) `src/pi/session.ts` = RPC adapter (not lifecycle owner)
153
+
154
+ `PiAgentSession` is an in-memory adapter over `pi --mode rpc`.
155
+
156
+ ### Responsibilities
157
+
158
+ - Spawns Pi in RPC mode and parses stdout as NDJSON lines
159
+ - Sends commands over stdin with unique request IDs
160
+ - Correlates `response` events back to pending promises via `_pendingRequests`
161
+ - Emits normalized callbacks for Supervisor/Runner (`onEvent`, `onToolStart`, `onToolEnd`, `onMeta`)
162
+ - Enforces liveness timeout (`stallTimeoutMs`) at session level
163
+ - Pins absolute cwd at spawn time to prevent TMUX path drift in worktrees
164
+ - Resolves npm package extensions (gitnexus, serena) from global node_modules
165
+ - Supports per-specialist extension opt-out via `excludeExtensions` option
166
+ - Injects caveman extension for terse agent-to-agent output
167
+ - Sets `CAVEMAN_LEVEL=full` environment variable
168
+
169
+ ### ID-mapped dispatch + ack checks
170
+
171
+ - `sendCommand()` assigns incrementing IDs (`_nextRequestId`) and stores resolver/rejecter in `_pendingRequests`
172
+ - `_handleEvent()` matches `type === "response"` with `event.id` and resolves the matching pending request
173
+ - timeouts reject outstanding calls with `RPC timeout...` (default timeout: 30s)
174
+ - command methods enforce explicit ack success:
175
+ - `prompt()` throws if `response.success === false`
176
+ - `steer()` throws if `response.success === false`
177
+
178
+ ### Extension resolution
179
+
180
+ npm package extensions (gitnexus, serena) are resolved from global node_modules:
181
+ - gitnexus: `~/.nvm/versions/node/<version>/lib/node_modules/pi-gitnexus`
182
+ - serena: `~/.nvm/versions/node/<version>/lib/node_modules/pi-serena-tools`
183
+
184
+ Extension opt-out: `excludeExtensions` string array filters packages before `-e` assembly.
185
+
186
+ Caveman extension: loaded from `~/.pi/agent/extensions/caveman` if present. Sets `CAVEMAN_LEVEL=full` env.
187
+
188
+ This is the key adapter contract: **transport-level correctness and command acknowledgement**, not durable job semantics.
189
+
190
+ ## 3) Job registry anchored to git common root (`job-root.ts`)
191
+
192
+ `src/specialist/job-root.ts` ensures all worktrees converge on the same job registry.
193
+
194
+ ### `resolveJobsDir()` — common-root anchoring
195
+
196
+ ```typescript
197
+ export function resolveCommonGitRoot(cwd: string): string | undefined {
198
+ const result = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd, encoding: 'utf-8' });
199
+ // Returns the main repo root from any worktree
200
+ return dirname(resolve(cwd, result.stdout.trim()));
201
+ }
202
+
203
+ export function resolveJobsDir(cwd = process.cwd()): string {
204
+ const commonRoot = resolveCommonGitRoot(cwd) ?? cwd;
205
+ return join(commonRoot, '.specialists', 'jobs');
206
+ }
207
+ ```
208
+
209
+ **Note:** `resolveCommonGitRoot` is exported for reuse (e.g., worktree.ts deduplication).
210
+
211
+ **Why this matters:** In a worktree, `git rev-parse --git-common-dir` returns the shared `.git/` directory in the main checkout. Taking `dirname` gives us the common project root, so all worktrees read/write `.specialists/jobs/` at the same absolute path.
212
+
213
+ ### `resolveCurrentBranch()` — branch detection
214
+
215
+ Returns the current branch name, or `undefined` when HEAD is detached. Used by Supervisor to persist `branch` in `status.json`.
216
+
217
+ ## Specialist configuration merge contract (KAN-90)
218
+
219
+ Specialist resolution is implemented in `src/specialist/loader.ts` through a deterministic 3-layer merge used by all runtime consumers.
220
+
221
+ ### Layer order
222
+
223
+ 1. **Package canonical** (`config/specialists/<name>.specialist.json`)
224
+ - Upstream default for every specialist.
225
+ 2. **Global user** (`~/.config/specialists/user.json`)
226
+ - Sparse override surface initialized by `sp init --global`.
227
+ - Merge path supports allowlisted execution/prompt/metadata fields and `skills.paths` append semantics.
228
+ 3. **Repo user** (`.specialists/user/<name>.specialist.json`)
229
+ - Full override file in repository scope.
230
+ - Same allowlisted merge semantics as global for safe fields.
231
+
232
+ ### Field governance
233
+
234
+ - **Allowed override fields (global/repo):** execution allowlist leaves (`model`, `fallback_model`, `fallback_models`, `timeout_ms`, `stall_timeout_ms`, `interactive`, `thinking_level`, `max_retries`, `prompt_limit_bytes`, `stdout_limit_bytes`), execution extension leaves, `prompt.system_prompt_mode`, `stall_detection.waiting_auto_close_ms`, `beads_write_notes`, `notes_mode`, `output_file`, and `skills.paths`.
235
+ - **Blocked fields:** `execution.permission_required`, `execution.auto_commit`, `prompt.system`, `prompt.output_schema`, `skills.scripts`, `mandatory_rules`, `capabilities`.
236
+ - **Severity model:**
237
+ - Global blocked attempt: stripped (`severity: strip`) and surfaced only in diagnostic metadata.
238
+ - Repo/user blocked attempt: retained in warning stream (`severity: warn`) and surfaced by `sp doctor --specialists`.
239
+
240
+ ### Health checks and failure path
241
+
242
+ - `sp init --global` creates and seeds global override files from shipped catalog.
243
+ - `sp edit --global` mutates the shared layer.
244
+ - `sp doctor --specialists` evaluates global-model coverage, blocked fields, and model completeness.
245
+ - `SpecialistLoader.get()` throws `SpecialistMissingModelError` when a fully merged specialist still has no model after all layers.
246
+
247
+ ### Why this contract exists
248
+
249
+ This contract centralizes user intent (`~/.config/specialists/user.json`) without forking canonical package assets, while preserving safety on policy-sensitive fields. It also keeps repo-local overrides composable with one machine-wide baseline and one per-repo layer.
250
+
251
+ ## 4) Worktree isolation (`worktree.ts`)
252
+
253
+ `src/specialist/worktree.ts` provisions isolated git workspaces for edit-permission specialists.
254
+
255
+ ### Key constraints
256
+
257
+ - Shells out to `bd worktree create` exclusively — no silent git fallback
258
+ - Fails loud: throws on bd error instead of degrading silently
259
+ - No Pi bootstrap logic (extensions are global via `~/.pi/`)
260
+
261
+ ### Branch and path derivation
262
+
263
+ ```typescript
264
+ // Convention: feature/<beadId>-<specialist-slug>
265
+ export function deriveBranchName(beadId: string, specialistName: string): string {
266
+ return `feature/${beadId}-${slugify(specialistName)}`;
267
+ }
268
+
269
+ // Convention: <beadId>-<specialist-slug>
270
+ export function deriveWorktreeName(beadId: string, specialistName: string): string {
271
+ return `${beadId}-${slugify(specialistName)}`;
272
+ }
273
+ ```
274
+
275
+ ### `provisionWorktree()` — creation and reuse
276
+
277
+ 1. Derives canonical branch name and worktree path
278
+ 2. Checks `git worktree list --porcelain` for existing worktree on that branch
279
+ 3. If exists: returns `reused: true`
280
+ 4. If not: calls `bd worktree create <path> --branch <branch>` (hard — throws on failure)
281
+
282
+ ### `listWorktrees()` / `findExistingWorktree()` — discovery
283
+
284
+ Parses `git worktree list --porcelain` output into a `Map<branch, absolute-path>`. Detached-HEAD worktrees are omitted.
285
+
286
+ ## 5) Supervisor is the sole durable lifecycle source
287
+
288
+ `src/specialist/supervisor.ts` owns persisted lifecycle and job state.
289
+
290
+ ### Durable artifacts (authoritative)
291
+
292
+ For each run (`.specialists/jobs/<id>/` legacy/operator mirror):
293
+
294
+ - `status.json` — mutable current state (`starting/running/waiting/done/error`, pid, last event timestamps, model/backend, worktree_path, branch, **`node_id`**)
295
+ - `events.jsonl` — append-only canonical timeline stream (JSON-first source of truth)
296
+ - `result.txt` — final assistant output text
297
+
298
+ ### JSON-first storage + atomic dual-write
299
+
300
+ Persistence is **JSON-first**:
301
+
302
+ - SQLite is the canonical runtime store for listing/querying and node-level analytics.
303
+ - Files under `.specialists/jobs/<id>/` are legacy/operator mirrors for recovery and debugging.
304
+
305
+ Dual-write behavior is intentionally split by durability role:
306
+
307
+ 1. Write canonical file artifact (`status.json`, `events.jsonl`, `result.txt`).
308
+ 2. Best-effort mirror into SQLite.
309
+
310
+ For coupled SQLite rows, writes are atomic inside a DB transaction:
311
+
312
+ - `upsertStatusWithEvent(...)` → status + event in one transaction
313
+ - `upsertStatusWithEventAndResult(...)` → status + event + result in one transaction
314
+
315
+ This yields: canonical durability from files, atomic relational consistency inside SQLite, and resilient operation when SQLite is unavailable.
316
+
317
+ ### SQLite integration
318
+
319
+ Supervisor optionally uses `ObservabilitySqliteClient` for:
320
+
321
+ - Status mirror (`upsertStatus`) — indexed reads by status/bead/node
322
+ - Event mirror (`appendEvent`) — ordered timeline queries from `event_json`
323
+ - Result mirror (`upsertResult`) — quick result retrieval without reading `result.txt`
324
+ - Transactional compound updates (`upsertStatusWithEvent*`) — single-commit relational state changes
325
+
326
+ File-based storage remains authoritative and always available.
327
+
328
+ ### Observability schema evolution (`schema_version` v1 → v4)
329
+
330
+ `src/specialist/observability-sqlite.ts` initializes and migrates schema idempotently through:
331
+
332
+ - **v1**: base observability tables (`schema_version`, `specialist_jobs`, `specialist_events`, `specialist_results`) + v1 rebuild of `specialist_jobs` to normalized columns (`worktree_column`, `last_output`).
333
+ - **v2**: bead-aware indexing (`bead_id` in jobs + `idx_jobs_bead`).
334
+ - **v3**: explicit job lifecycle indexing (`status`, `node_id`, `idx_jobs_status_updated`) and status denormalization for faster list/filter operations.
335
+ - **v4**: node-runtime observability tables:
336
+ - `node_runs`
337
+ - `node_members`
338
+ - `node_events`
339
+ - `node_memory`
340
+
341
+ Migrations are safe to rerun: each step checks `schema_version`, applies forward-only DDL, and recreates required indexes with `IF NOT EXISTS`.
342
+
343
+ ### Node runtime tables (v4)
344
+
345
+ v4 adds first-class storage for multi-member node orchestration state:
346
+
347
+ - `node_runs` — coordinator-level run status (`node_name`, `status`, `coordinator_job_id`, `waiting_on`, `memory_namespace`, `status_json`)
348
+ - `node_members` — per-member participation (`member_id`, linked `job_id`, `specialist`, `model`, `role`, `status`, `enabled`)
349
+ - `node_events` — node-scoped timeline stream (`type`, `event_json`, ordered by `t,id`)
350
+ - `node_memory` — node memory/materialization (`namespace`, `entry_type`, `entry_id`, `summary`, `source_member_id`, `confidence`, `provenance_json`)
351
+
352
+ ### Lifecycle ownership rules
353
+
354
+ Supervisor determines and persists:
355
+
356
+ - job creation and initial `starting` state
357
+ - transitions to `running`, `waiting`, `done`, `error`
358
+ - run completion and terminal event emission
359
+ - crash recovery and stale-state reconciliation
360
+
361
+ **Design rule:** completion and state are read from Supervisor files, not inferred directly from raw Pi adapter callbacks.
362
+
363
+ ### GitNexus tracking accumulator
364
+
365
+ Supervisor accumulates GitNexus usage across a run:
366
+
367
+ ```typescript
368
+ const gitnexusAccumulator = {
369
+ files_touched: new Set<string>(),
370
+ symbols_analyzed: new Set<string>(),
371
+ highest_risk: undefined as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | undefined,
372
+ tool_invocations: 0,
373
+ };
374
+ ```
375
+
376
+ - `edit`/`write` tool results: extract `path` → add to `files_touched`
377
+ - `gitnexus_*` tool results: extract `files`, `symbols_analyzed`, `risk_level`
378
+ - Emits `gitnexus_summary` in `run_complete` event
379
+
380
+ ### FIFO-based steering
381
+
382
+ Supervisor creates a named FIFO (`steer.pipe`) for cross-process steering:
383
+
384
+ ```typescript
385
+ const fifoPath = join(dir, 'steer.pipe');
386
+ execFileSync('mkfifo', [fifoPath]);
387
+ ```
388
+
389
+ **Synchronous fd closing:** The FIFO fd is opened with `'r+'` (O_RDWR) to prevent blocking, and closed synchronously in the `finally` block before destroying the read stream. This prevents event loop hangs in batch test suites.
390
+
391
+ Message types:
392
+ - `{ type: 'steer', message: '...' }` — steer running session
393
+ - `{ type: 'resume', task: '...' }` — resume waiting keep-alive session
394
+ - `{ type: 'close' }` — close keep-alive session
395
+ - `{ type: 'prompt', message: '...' }` — DEPRECATED, use `resume`
396
+
397
+ ### Keep-alive session support
398
+
399
+ Supervisor supports non-streaming keep-alive sessions via `onResumeReady` callback:
400
+
401
+ 1. Session completes first turn → transitions to `waiting` status
402
+ 2. Session stays alive (not killed) awaiting explicit `resume` or `close`
403
+ 3. Orchestrator sends `{ type: 'resume', task: '...' }` via FIFO
404
+ 4. Session processes next turn → returns to `waiting` or `done`
405
+
406
+ **State machine:**
407
+ - `running` → actively processing
408
+ - `waiting` → alive, awaiting next-turn action (valid: `resume`, `close`)
409
+ - `done` → terminal, session closed
410
+ - `error` → terminal, session closed with error
411
+
412
+ ### Worktree write-boundary enforcement
413
+
414
+ Supervisor propagates `worktreeBoundary` to the Runner when a job has an active worktree:
415
+
416
+ ```typescript
417
+ const runOptionsWithBoundary = runOptions.workingDirectory
418
+ ? { ...runOptions, worktreeBoundary: runOptions.workingDirectory }
419
+ : runOptions;
420
+ ```
421
+
422
+ The resolver walks the target job's `status.json`: if it already carries a `worktree_owner_job_id`, that value is inherited; otherwise the target job's own `id` becomes the owner. This keeps ownership consistent across arbitrarily deep reuse chains.
423
+
424
+ These fields are the primary inputs for `sp ps` tree construction — they replace fragile `worktree_path` inference.
425
+
426
+ ### Context denormalization in `status.json`
427
+
428
+ On every `turn_summary` metric event, Supervisor writes `context_pct` and `context_health` directly into `status.json` via `setStatus()`:
429
+
430
+ ```typescript
431
+ setStatus({
432
+ context_pct: contextUtilization?.context_pct,
433
+ context_health: contextUtilization?.context_health,
434
+ });
435
+ ```
436
+
437
+ This avoids event-log scanning for any consumer that only needs the latest value (e.g. `sp ps` reads `status.json` and displays a `ctx%` column without touching `events.jsonl`).
438
+
439
+ ### Current tool staleness fix (April 2026)
440
+
441
+ Prior to April 2026, `current_tool` in `status.json` was stale because it was set on `tool_execution_start` but never cleared on `tool_execution_end`. `sp ps` read from `status_json` snapshot and showed stale values.
442
+
443
+ **Fix (unitAI-66xn, unitAI-yke7):**
444
+
445
+ 1. **Supervisor clears on tool end**: `onToolEndCallback` sets `current_tool: undefined` via `setStatus()` on every `tool_execution_end` event.
446
+
447
+ ```typescript
448
+ onToolEndCallback: (toolCallId, toolName, result, resultRaw, isError) => {
449
+ setStatus({ current_tool: undefined }); // Clear stale tool
450
+ // ... rest of callback
451
+ }
452
+ ```
453
+
454
+ 2. **ps.ts derives from event stream**: Instead of reading `status_json.current_tool`, `sp ps` queries `specialist_events` for the latest tool phase:
455
+
456
+ ```typescript
457
+ // readLatestToolEvent() in observability-sqlite.ts
458
+ const latestTool = db.query(`
459
+ SELECT json_extract(event_json, '$.phase') as phase
460
+ FROM specialist_events
461
+ WHERE job_id = ? AND type = 'tool'
462
+ ORDER BY t DESC, id DESC LIMIT 1
463
+ `).get(jobId);
464
+ // If phase === 'end', current_tool is null; if 'start'/'update', it's active
465
+ ```
466
+
467
+ This prevents false-positive "hung job" diagnoses where `sp ps` showed a stale tool (e.g., `gitnexus_context`) while the model was actively streaming text.
468
+
469
+ ### Context window tracking
470
+
471
+ Supervisor tracks context utilization for long-running sessions:
472
+
473
+ ```typescript
474
+ type ContextHealth = 'OK' | 'MONITOR' | 'WARN' | 'CRITICAL';
475
+
476
+ const MODEL_CONTEXT_WINDOWS: Array<{ matcher: (model: string) => boolean; windowTokens: number }> = [
477
+ { matcher: (model) => model.includes('gemini-3.1-pro'), windowTokens: 1_000_000 },
478
+ { matcher: (model) => model.includes('qwen3.5') || model.includes('glm-5'), windowTokens: 128_000 },
479
+ { matcher: (model) => model.includes('claude'), windowTokens: 200_000 },
480
+ ];
481
+
482
+ function getContextHealth(contextPct: number): ContextHealth {
483
+ if (contextPct < 40) return 'OK';
484
+ if (contextPct <= 65) return 'MONITOR';
485
+ if (contextPct <= 80) return 'WARN';
486
+ return 'CRITICAL';
487
+ }
488
+ ```
489
+
490
+ Context utilization (`context_pct`) is captured on every `turn_summary` event, rounded/validated into status snapshots, and surfaced by CLI/status views for long-run monitoring and compaction risk detection.
491
+
492
+ **Per-turn text accumulation**:
493
+ - `turnTextAccumulator` collects streamed `text` deltas per assistant message
494
+ - Emits as `text_content` on `turn_summary` events (survives crashes via JSON persistence in `event_json`)
495
+ - Feed displays 80-char preview on `TURN+` lines
496
+ - Context health warnings shown at WARN (80%) and CRITICAL (95%) thresholds
497
+
498
+ ### Stuck detection model
499
+
500
+ Stall/staleness is enforced at two layers:
501
+
502
+ #### Session-level liveness (`session.ts`)
503
+
504
+ - `_markActivity()` resets a timer on each parsed event
505
+ - if no activity for `stallTimeoutMs`, session throws `StallTimeoutError` and kills Pi
506
+
507
+ **Test-aware stall detection:** PiAgentSession extends the stall timeout window when bash tool commands match test runner patterns:
508
+
509
+ ```typescript
510
+ const TEST_COMMAND_PATTERNS = [
511
+ /(?:^|\s)(?:bun\s+--bun\s+)?vitest(?:\s|$)/i,
512
+ /(?:^|\s)bun\s+test(?:\s|$)/i,
513
+ /(?:^|\s)npm\s+test(?:\s|$)/i,
514
+ // ... npm/pnpm/yarn test, jest, pytest
515
+ ];
516
+ const TEST_COMMAND_STALL_TIMEOUT_MS = 300_000; // 5 minutes
517
+ ```
518
+
519
+ When a test command is detected:
520
+ - Effective timeout = `max(base_timeout, test_timeout)`
521
+ - Stall watchdog still fires for actual hangs
522
+ - Window restored after `tool_execution_end`
523
+
524
+ This prevents false-positive kills during vitest's tinypool worker initialization, which can exceed the standard 30-120s stall window.
525
+
526
+ #### Supervisor-level staleness (`supervisor.ts`)
527
+
528
+ Defaults (`STALL_DETECTION_DEFAULTS`):
529
+
530
+ | Threshold | Default | Action |
531
+ |-----------|---------|--------|
532
+ | `running_silence_warn_ms` | 60s | Emit `stale_warning` event |
533
+ | `running_silence_error_ms` | 300s | Transition to `error`, kill session |
534
+ | `waiting_stale_ms` | 1h | Emit `stale_warning` event |
535
+ | `waiting_auto_close_ms` | disabled | Graceful close first; forced termination fallback only if close hangs |
536
+ | `tool_duration_warn_ms` | 120s | Emit `stale_warning` with tool name |
537
+
538
+ Periodic checker (10s interval) monitors silence duration and tool execution time.
539
+
540
+ ### Crash recovery
541
+
542
+ On `run()`, Supervisor scans job dirs for:
543
+
544
+ - `running`/`starting` jobs with dead PID → mark as `error`
545
+ - `running` jobs with prolonged silence → mark as `error`
546
+ - `waiting` jobs with prolonged silence → emit `stale_warning` event (preserve state)
547
+
548
+ ### Liveness checks (`isJobDead`)
549
+
550
+ `Supervisor.isJobDead()` cross-checks PID + tmux session to determine if a job is dead:
551
+
552
+ ```typescript
553
+ function isJobDead(status: SupervisorStatus): boolean {
554
+ if (!status.pid) return true; // no pid recorded = dead
555
+ if (!isProcessAlive(status.pid)) return true;
556
+ if (status.tmux_session && !isTmuxSessionAlive(status.tmux_session)) return true;
557
+ return false;
558
+ }
559
+ ```
560
+
561
+ `is_dead` is **computed at read time**, never persisted. This prevents stale state where:
562
+ - A dead job is marked alive because its `status.json` wasn't updated
563
+ - An alive job is marked dead because `is_dead` was persisted before the process recovered
564
+
565
+ **`isTmuxSessionAlive()`** (in `src/cli/tmux-utils.ts`) uses a 2000ms timeout and returns false on timeout or non-zero exit. This prevents hangs on tmux socket issues.
566
+
567
+ ### Async dispose + pending-ops tracker
568
+
569
+ Supervisor's `dispose()` is now async to prevent "Cannot use a closed database" errors:
570
+
571
+ ```typescript
572
+ async dispose(): Promise<void> {
573
+ this._disposed = true;
574
+ await this._pendingOpsTracker.flush(); // wait for in-flight SQLite ops
575
+ await this.closeActiveSession(); // close active pi/Serena session
576
+ this.sqliteClient?.close();
577
+ }
578
+ ```
579
+
580
+ Root cause: async operations (stall detection interval, FIFO callbacks, Promise microtasks) fired **after** `dispose()` closed the SQLite connection. The retry loop in observability-sqlite.ts never helped because "Cannot use a closed database" wasn't retryable.
581
+
582
+ Solution: a pending-operations tracker that:
583
+ 1. Wraps every SQLite operation in `_pendingOpsTracker.run(op)`
584
+ 2. `dispose()` awaits the tracker's flush before closing the active session and SQLite client
585
+ 3. CLI entry points (`run`, `status`, `resume`, `steer`, `stop`) await `supervisor.dispose()` before exit
586
+
587
+ **Active session lifecycle**: Supervisor tracks the active Pi session via `setActiveSession()`. During `dispose()`, `closeActiveSession()` attempts graceful `session.close()` first; on failure, it falls back to `session.kill()` to ensure the pi/Serena MCP server is reaped before process exit.
588
+
589
+ ### Job reuse concurrency guard
590
+
591
+ When `--job <id>` is passed, `resolveWorkingDirectory()` enforces a concurrency guard for MEDIUM/HIGH specialists:
592
+
593
+ ```typescript
594
+ const BLOCKED_JOB_REUSE_STATUSES = new Set(['starting', 'running']);
595
+
596
+ if (editCapable && !args.forceJob && BLOCKED_JOB_REUSE_STATUSES.has(targetJobStatus)) {
597
+ // Block: cannot enter an active worktree
598
+ process.exit(1);
599
+ }
600
+ ```
601
+
602
+ - `starting`/`running`: blocked for MEDIUM/HIGH (can corrupt files)
603
+ - `waiting`/`done`/`error`/`cancelled`: allowed for all
604
+ - Unknown status: blocked conservatively (unless `--force-job`)
605
+ - `--force-job`: bypass guard at caller's risk
606
+
607
+ READ_ONLY and LOW specialists bypass the guard entirely — they cannot corrupt files.
608
+
609
+ ### Job lineage fields
610
+
611
+ When `--job <id>` is passed at run time, Supervisor persists two lineage fields in `status.json` (and mirrors them to SQLite):
612
+
613
+ ```typescript
614
+ interface SupervisorStatus {
615
+ reused_from_job_id?: string; // the job whose workspace was borrowed via --job
616
+ worktree_owner_job_id?: string; // the transitive root owner of the worktree
617
+ }
618
+ ```
619
+
620
+ ### Stale-base guard (dispatch-time + merge-time)
621
+
622
+ Commit: `4c3eeb36`
623
+
624
+ Two-layer protection against parallel-chain divergence:
625
+
626
+ **Layer 1: Dispatch-time guard** (run.ts)
627
+
628
+ When `--worktree` provisions a new worktree for a bead belonging to an epic, the stale-base guard checks for sibling chains with unmerged substantive commits:
629
+
630
+ ```typescript
631
+ function assertNoStaleBaseSiblings(beadId: string, forceStaleBase: boolean): void {
632
+ const sqliteClient = createObservabilitySqliteClient();
633
+ if (!sqliteClient) return;
634
+
635
+ const epicId = resolveEpicIdForBead(sqliteClient, beadId);
636
+ if (!epicId) return;
637
+
638
+ const siblingChains = sqliteClient
639
+ .listEpicChainsWithLatestJob(epicId)
640
+ .filter((chain) => chain.chain_root_bead_id !== beadId && Boolean(chain.branch));
641
+
642
+ // Check each sibling branch for substantive commits vs master
643
+ const staleSiblings = detectSubstantiveCommits(siblingChains, baseBranch);
644
+
645
+ if (staleSiblings.length > 0 && !forceStaleBase) {
646
+ console.error(`Error: Epic '${epicId}' has sibling chains with unmerged changes.`);
647
+ process.exit(1);
648
+ }
649
+ }
650
+ ```
651
+
652
+ Bypass with `--force-stale-base`.
653
+
654
+ **Layer 2: Merge-time rebase** (merge.ts)
655
+
656
+ Before merging each chain (via `sp merge` or `sp epic merge`), the branch is rebased onto master:
657
+
658
+ ```typescript
659
+ export function rebaseBranchOntoMaster(branch: string, worktreePath: string): void {
660
+ const baseBranch = resolveDefaultBranchName(worktreePath);
661
+ const rebase = runCommand('git', ['rebase', baseBranch], worktreePath);
662
+ if (rebase.status === 0) return;
663
+
664
+ // On failure: abort rebase and report conflicting files
665
+ tryAbortRebase(worktreePath);
666
+ const conflicts = getConflictFiles(worktreePath);
667
+ throw new Error(`Rebase failed for '${branch}' onto '${baseBranch}'...`);
668
+ }
669
+ ```
670
+
671
+ Called in `runMergePlan()` before `mergeBranch()` for each chain.
672
+
673
+ **Why this matters**: Parallel chains branched from the same base diverge. Wave A's merge would appear as reversions in Wave B's diff. Rebase incorporates earlier waves' changes before publication.
674
+
675
+ ### Bead ownership and lifecycle semantics
676
+
677
+ Ownership comes from Runner + Supervisor behavior:
678
+
679
+ - If `inputBeadId` is provided, that bead is orchestrator-owned (inherited)
680
+ - If no input bead and creation policy permits, Runner creates an owned bead
681
+
682
+ Supervisor post-run policy:
683
+
684
+ - always persists bead ID in status when available
685
+ - **Auto-append**: on every `run_complete` event, full specialist output is appended to the **input bead** (all specialists, not just READ_ONLY)
686
+ - **Auto-commit**: if `auto_commit` policy is set, substantive worktree changes are checkpointed at waiting/terminal transitions
687
+ - **Owned beads**: closed with full reason (COMPLETE/duration/model) on terminal status
688
+ - **Input beads**: auto-closed via `closeBeadIfInProgress()` on terminal status (DONE) — closes only if still `open` or `in_progress`, preserving existing reasons if already closed
689
+
690
+ Commit: `83b5986a` (unitAI-9truh)
691
+
692
+ This eliminates stale `in_progress` drift without overwriting closed beads' reasons.
693
+
694
+ ### Auto-append bead notes
695
+
696
+ Commit: `428cd7f7`
697
+
698
+ `appendResultToInputBead()` is called on every `run_complete` event (per-turn for keep-alive, once for one-shot):
699
+
700
+ ```typescript
701
+ const notes = formatBeadNotes({
702
+ output: params.output,
703
+ promptHash: params.promptHash,
704
+ durationMs: params.durationMs,
705
+ model: params.model,
706
+ backend: params.backend,
707
+ specialist: runOptions.name,
708
+ jobId: id,
709
+ status: params.status, // 'waiting' | 'done' | 'error'
710
+ timestamp: new Date().toISOString(),
711
+ });
712
+ ```
713
+
714
+ Status-aware headers:
715
+ - `[WAITING — more output may follow]` — keep-alive awaiting resume
716
+ - `[DONE]` — terminal completion
717
+
718
+ `BeadsClient.updateBeadNotes()` now returns `{ ok: boolean; error?: string }` for error handling.
719
+
720
+ ### Auto-commit checkpoint policy
721
+
722
+ Commit: `11e9b016`
723
+
724
+ Specialists with `execution.auto_commit` policy automatically checkpoint worktree changes:
725
+
726
+ | Policy | Trigger |
727
+ |--------|---------|
728
+ | `checkpoint_on_waiting` | Every turn entering `waiting` |
729
+ | `checkpoint_on_terminal` | Terminal completion (`done`/`error`) |
730
+
731
+ Implementation:
732
+
733
+ ```typescript
734
+ function runAutoCommitCheckpoint(options: {
735
+ autoCommitPolicy: 'never' | 'checkpoint_on_waiting' | 'checkpoint_on_terminal';
736
+ target: 'waiting' | 'terminal';
737
+ worktreePath: string | undefined;
738
+ specialist: string;
739
+ beadId: string | undefined;
740
+ turnNumber: number;
741
+ }): { status: 'skipped' | 'success' | 'failed'; ... }
742
+ ```
743
+
744
+ Noise filtering: `.xtrm/`, `.wolf/`, `.specialists/jobs/`, `.beads/` are ignored. `.specialists/jobs/` is legacy/operator-only.
745
+
746
+ Timeline events: `auto_commit_success`, `auto_commit_skipped`, `auto_commit_failed`.
747
+
748
+ Status fields: `auto_commit_count`, `last_auto_commit_sha`, `last_auto_commit_at_ms`.n
749
+ ## 6) Timeline event model (`timeline-events.ts`)
750
+
751
+ `src/specialist/timeline-events.ts` defines the canonical feed v2 event vocabulary.
752
+
753
+ ### Event layers
754
+
755
+ 1. **Message construction layer** (nested under `message_update`):
756
+ - `text_start`, `text_delta`, `text_end`
757
+ - `thinking_start`, `thinking_delta`, `thinking_end`
758
+ - `toolcall_start`, `toolcall_delta`, `toolcall_end`
759
+ - `done` (message-level completion)
760
+ - `error` (message-level failure)
761
+
762
+ 2. **Tool execution layer** (top-level):
763
+ - `tool_execution_start`
764
+ - `tool_execution_update` (optional, streaming)
765
+ - `tool_execution_end`
766
+
767
+ 3. **Tool result layer** (message role: `toolResult`):
768
+ - `message_start` (role: `toolResult`)
769
+ - `message_end`
770
+
771
+ 4. **Turn boundary layer**:
772
+ - `turn_start`
773
+ - `turn_end` (includes assistant message + `toolResults[]`)
774
+
775
+ 5. **Run boundary layer**:
776
+ - `agent_start`
777
+ - `agent_end` (run completion, contains all `messages[]`)
778
+
779
+ ### Canonical timeline events (persisted to `events.jsonl`)
780
+
781
+ | Event | When emitted | Key fields |
782
+ |-------|-------------|------------|
783
+ | `run_start` | Job begins | `specialist`, `bead_id`, **`startup_snapshot`** |
784
+
785
+ **`startup_snapshot` fields** (on `run_start`):
786
+
787
+ | Field | Source |
788
+ |-------|--------|
789
+ | `job_id` | Supervisor run ID |
790
+ | `specialist_name` | `runOptions.name` |
791
+ | `bead_id` | `runOptions.inputBeadId` |
792
+ | `reused_from_job_id` | `runOptions.reusedFromJobId` |
793
+ | `worktree_owner_job_id` | `runOptions.worktreeOwnerJobId` |
794
+ | `chain_id` / `chain_root_job_id` | Derived from worktree owner or self |
795
+ | `chain_root_bead_id` | `variables.chain_root_bead_id` |
796
+ | `worktree_path` | `runOptions.workingDirectory` |
797
+ | `branch` | `resolveCurrentBranch()` |
798
+ | `variables_keys` | `Object.keys(runOptions.variables)` |
799
+ | `reviewed_job_id_present` | Bool — `reviewed_job_id` in variables |
800
+ | `reused_worktree_awareness_present` | Bool — `reused_worktree_awareness` in variables |
801
+ | `bead_context_present` | Bool — `bead_context` in variables |
802
+ | `memory_injection` | Token counts from `meta` event (backfilled post-emission) |
803
+ | `skills` | `{ count, activated[] }` from `activated_skills` variable |
804
+
805
+ This snapshot is persisted both in `status.json.startup_context` and in the `run_start` timeline event. `sp result` merges both sources + `meta.memory_injection` into a unified startup context block.
806
+ | `meta` | Model/backend known | `model`, `backend`, `memory_injection` |
807
+ | `thinking` | Reasoning detected | `char_count` |
808
+ | `tool` (start/update/end) | Tool execution | `tool`, `phase`, `tool_call_id`, `args`, `result_summary`, `result_raw`, `is_error` |
809
+ | `text` | Text output detected | `char_count` |
810
+ | `message` (start/end) | Message boundary | `phase`, `role` |
811
+ | `turn` (start/end) | Turn boundary | `phase` |
812
+ | `token_usage` | Token metrics from RPC | `token_usage`, `source` |
813
+ | `finish_reason` | Finish reason from RPC | `finish_reason`, `source` |
814
+ | `turn_summary` | Turn completion | `turn_index`, `token_usage`, `finish_reason`, **`context_pct`**, **`text_content`** |
815
+ | `compaction` (start/end) | Context compaction | `phase` |
816
+ | `retry` | Auto-retry event | `phase` |
817
+ | `stale_warning` | Stuck detection | `reason`, `silence_ms`, `threshold_ms`, `tool` |
818
+ | `run_complete` | **THE canonical completion** | `status`, `elapsed_s`, `model`, `backend`, `bead_id`, `error`, `output`, `output_type`, `metrics`, `gitnexus_summary` |
819
+
820
+ ### Completion semantic
821
+
822
+ > ⚠️ **BREAKING CHANGE:** `run_complete` is now emitted **per turn** for keep-alive sessions, not once per job lifecycle. Consumers that previously treated the first `run_complete` as terminal must now gate completion on terminal job status (`done`/`error`/`cancelled`) for keep-alive flows.
823
+
824
+ For feed v2, `run_complete` is the canonical per-turn completion event. In single-turn runs this is emitted once; in keep-alive runs it is emitted after each completed turn.
825
+ This resolves the historical ambiguity between:
826
+
827
+ - callback-level `done` (synthetic, from `agent_end`)
828
+ - persisted `agent_end` (added after runner returns)
829
+
830
+ Each `run_complete` event contains:
831
+ - final status (`COMPLETE` | `ERROR` | `CANCELLED`)
832
+ - elapsed time
833
+ - model/backend
834
+ - output type (from specialist execution config: codegen/analysis/review/synthesis/orchestration/workflow/research/custom)
835
+ - error message if applicable
836
+ - aggregated metrics (`token_usage`, `finish_reason`, `tool_calls`, `exit_reason`)
837
+ - GitNexus summary if any `gitnexus_*` tools were invoked
838
+
839
+ Legacy completion events (`done`, `agent_end`) are parse-compatible for old history but ignored on the write path.
840
+
841
+ ### Bun SQLite loading model
842
+
843
+ `ObservabilitySqliteClient` is Bun-aware and lazy-loaded:
844
+
845
+ - `bun:sqlite` is required dynamically (`require('bun:sqlite')`) only on first probe.
846
+ - Under Node/vitest (where `bun:sqlite` is unavailable), the probe returns `null` and runtime continues file-only.
847
+ - If SQLite exists, schema init (`initSchema`) runs first, then a persistent client is opened with WAL + busy timeout.
848
+
849
+ This keeps tests/tooling portable while enabling SQLite acceleration in Bun environments.
850
+
851
+ ### `mapCallbackEventToTimelineEvent()` — mapping table
852
+
853
+ | Callback event | Timeline event | Notes |
854
+ |---------------|----------------|-------|
855
+ | `thinking` | `thinking` | — |
856
+ | `tool_execution_start` | `tool` (start) | Includes `args`, `started_at` |
857
+ | `tool_execution_update` | `tool` (update) | — |
858
+ | `tool_execution_end` | `tool` (end) | Includes `result_summary`, `result_raw`, `is_error` |
859
+ | `text` | `text` | Presence only, not deltas |
860
+ | `message_start_assistant` | `message` (start, assistant) | — |
861
+ | `message_end_assistant` | `message` (end, assistant) | — |
862
+ | `message_start_tool_result` | `message` (start, toolResult) | — |
863
+ | `message_end_tool_result` | `message` (end, toolResult) | — |
864
+ | `turn_start` | `turn` (start) | — |
865
+ | `turn_end` | `turn` (end) | — |
866
+ | `auto_compaction_start` | `compaction` (start) | — |
867
+ | `auto_compaction_end` | `compaction` (end) | — |
868
+ | `auto_retry` | `retry` (end) | — |
869
+ | `memory_injection` | `meta` (model=`memory_injection`) | Token accounting for context budget analysis |
870
+ | `agent_end`, `done`, `message_done` | **IGNORED** | Supervisor emits `run_complete` instead |
871
+
872
+ ## 7) Pi session extensions for tool interception
873
+
874
+ `PiAgentSession` can generate and inject Pi extensions at spawn time for policy enforcement. The primary use is **worktree write-boundary enforcement** — preventing specialists in isolated worktrees from writing outside their boundary.
875
+
876
+ ### Extension generation pattern
877
+
878
+ When `worktreeBoundary` is provided in session options:
879
+
880
+ 1. `getWorktreeBoundaryExtensionPath(boundary)` generates a temporary extension file
881
+ 2. Extension lives in `$TMPDIR/specialists-pi-extensions/worktree-boundary-<hash>.mjs`
882
+ 3. Hash is derived from SHA256 of the resolved boundary path (first 16 chars)
883
+ 4. Extension is passed to Pi via `-e <path>` argument
884
+
885
+ ### Extension behavior
886
+
887
+ The generated extension hooks `tool_call` events for write-side tools (`edit`, `write`, `multiEdit`, `notebookEdit`):
888
+
889
+ ```javascript
890
+ export default function(pi) {
891
+ pi.on('tool_call', (event) => {
892
+ if (!WRITE_TOOLS.has(event.toolName)) return undefined;
893
+
894
+ const rawPath = extractPathFromInput(event.input);
895
+ if (!rawPath || !isAbsolute(rawPath)) return undefined;
896
+
897
+ if (isPathWithinBoundary(rawPath, worktreeBoundary)) return undefined;
898
+
899
+ return { block: true, reason: `Path '${rawPath}' is outside worktree boundary...` };
900
+ });
901
+ }
902
+ ```
903
+
904
+ - **Relative paths**: always allowed (resolve within worktree cwd)
905
+ - **Absolute paths inside boundary**: allowed
906
+ - **Absolute paths outside boundary**: blocked with error message
907
+
908
+ ### Tmp-fs fallback behavior
909
+
910
+ If the extension directory (`$TMPDIR/specialists-pi-extensions/`) cannot be created or the extension file cannot be written:
911
+
912
+ 1. Logs warning to stderr: `[session] Failed to write worktree boundary extension: <error>`
913
+ 2. Returns `null` from `getWorktreeBoundaryExtensionPath()`
914
+ 3. Session proceeds **without** the boundary extension (unprotected mode)
915
+ 4. Specialist can still write anywhere — relies on orchestrator vigilance
916
+
917
+ This fail-soft behavior ensures sessions don't crash on tmpdir issues (e.g. read-only filesystem, permissions) but surfaces the degradation clearly via stderr.
918
+
919
+ ### Boundary propagation flow
920
+
921
+ ```
922
+ Supervisor.run()
923
+ ↓ detects workingDirectory (worktree path)
924
+ ↓ adds worktreeBoundary: workingDirectory to runOptions
925
+ Runner.startSession()
926
+ ↓ passes worktreeBoundary to PiSessionOptions
927
+ PiAgentSession.start()
928
+ ↓ generates extension via getWorktreeBoundaryExtensionPath()
929
+ ↓ passes -e <ext-path> to Pi spawn args
930
+ ↓ sets WORKTREE_BOUNDARY env var for extension to read
931
+ Pi extension (inside pi process)
932
+ ↓ hooks tool_call events
933
+ ↓ blocks write tools with out-of-bounds paths
934
+ ```
935
+
936
+ ## 8) How Session, Timeline, and Supervisor connect
937
+
938
+ End-to-end flow:
939
+
940
+ 1. Supervisor allocates job ID and writes initial `status.json`
941
+ 2. Supervisor starts Runner; Runner starts `PiAgentSession`
942
+ 3. Session parses Pi RPC stream and emits normalized callbacks
943
+ 4. Supervisor maps callbacks through `mapCallbackEventToTimelineEvent(...)`
944
+ 5. Supervisor appends normalized timeline records to `events.jsonl` (and SQLite when available)
945
+ 6. Supervisor updates `status.json` on every lifecycle change
946
+ 7. On each completed turn, Supervisor writes `result.txt` and emits `run_complete`
947
+
948
+ Result: **Pi provides protocol events; Session adapts transport; Supervisor persists lifecycle truth.**
949
+
950
+ ## 9) Edit gate bead-claim KV pattern
951
+
952
+ The beads edit gate hooks (`beads-edit-gate`) check two KV keys before allowing file edits.
953
+
954
+ ### Primary path: session-scoped claim
955
+
956
+ ```bash
957
+ bd kv set "claimed:<session-id>" "<bead-id>"
958
+ ```
959
+
960
+ Set by Claude Code hooks when an agent claims a bead via `bd update <id> --claim`. Session-bound, cleared on session end.
961
+
962
+ ### Fallback path: bead-claim
963
+
964
+ ```bash
965
+ bd kv set "bead-claim:<bead-id>" "active"
966
+ ```
967
+
968
+ Set by Runner **before spawning a specialist** when `--bead <id>` is provided in `src/cli/run.ts`:
969
+
970
+ ```typescript
971
+ // Before specialist spawn
972
+ if (args.beadId && workingDirectory) {
973
+ execSync(`bd kv set "bead-claim:${args.beadId}" "active"`, { cwd: workingDirectory });
974
+ }
975
+
976
+ // After run completes (success or error)
977
+ if (args.beadId && workingDirectory) {
978
+ execSync(`bd kv clear "bead-claim:${args.beadId}"`, { cwd: workingDirectory });
979
+ }
980
+ ```
981
+
982
+ ### Why this matters
983
+
984
+ Worktree specialists run in subprocesses without session context. The bead-claim pattern provides an edit gate entry that:
985
+ 1. Is independent of Claude Code session IDs
986
+ 2. Is scoped to the specific bead being worked on
987
+ 3. Is automatically cleaned up when the run completes
988
+ 4. Enables MEDIUM/HIGH specialists to edit files in worktrees without blocking
989
+
990
+ ### Edit gate check order
991
+
992
+ ```bash
993
+ # Hook checks in order:
994
+ 1. claimed:<session-id> → session claim (Claude Code)
995
+ 2. bead-claim:<bead-id> → bead-scoped claim (specialist runner)
996
+ ```
997
+
998
+ If neither key exists, the edit is blocked.
999
+
1000
+ ---
1001
+
1002
+ ## 10) Epic lifecycle model (`epic-lifecycle.ts`, `epic-readiness.ts`)
1003
+
1004
+ Epic lifecycle is **derived live** from chain readiness, not driven by an operator-managed state machine. Only two states are persisted as terminal markers; everything else is recomputed each read.
1005
+
1006
+ ### Derived state model
1007
+
1008
+ ```
1009
+ ┌── merged (persisted, terminal)
1010
+
1011
+ chains in flight ──── derive ──── blocked / failed / merge_ready
1012
+
1013
+ └── abandoned (persisted, terminal)
1014
+ ```
1015
+
1016
+ | State | Persisted? | Meaning | Per-chain merge | Batch merge |
1017
+ |-------|:----------:|---------|:----------:|:----------:|
1018
+ | `blocked` | No (derived) | Some chain pending or has no reviewer verdict | — | ✗ |
1019
+ | `failed` | Soft marker only | A chain reviewer returned PARTIAL/FAIL with no fix-loop, OR a previous publish attempt failed transiently | per-chain on PASS chains | ✗ until cleared |
1020
+ | `merge_ready` | No (derived) | All chains pass and no active jobs | ✓ | ✓ |
1021
+ | `merged` | Yes (terminal) | Publication complete | — | — |
1022
+ | `abandoned` | Yes (terminal) | Operator-cancelled via `sp epic abandon` | — | — |
1023
+
1024
+ ### Recovery rule
1025
+
1026
+ `validateEpicMergeReadiness` (`src/cli/epic.ts`) refuses merge **only** for the two truly-terminal states:
1027
+
1028
+ ```typescript
1029
+ if (epicState === 'merged' || epicState === 'abandoned') throw 'No further merges allowed';
1030
+ ```
1031
+
1032
+ A persisted `failed` row from a prior transient publication failure (e.g. rebase conflict, dirty worktree) is treated as legacy/non-terminal — readiness is recomputed live, so the next `sp epic merge` retries fresh once the operator clears the conflict source.
1033
+
1034
+ ### SQLite persistence
1035
+
1036
+ `epic_runs` table:
1037
+ - `epic_id` — bead epic ID
1038
+ - `status` — `merged` | `abandoned` for terminal records; soft `failed` markers may exist from prior transient failures and are recoverable
1039
+ - `status_json` — audit trail (transitions, reasons)
1040
+ - `updated_at_ms` — last write timestamp
1041
+
1042
+ ### Per-chain merge
1043
+
1044
+ `sp merge <chain-root>` is allowed for any PASS chain regardless of sibling-epic state. The original "refuse if epic unresolved" inverted gate was removed in the chain-lifecycle redesign — chains publish individually whenever their reviewer returns PASS and their executor is finalized. Use `sp epic merge` only when batching all epic chains together (atomic, topological order, tsc gate per merge).
1045
+
1046
+ ## 11) Chain identity model (`chain-identity.ts`)
1047
+
1048
+ Chain identity distinguishes worktree lineages from standalone prep jobs.
1049
+
1050
+ ### Chain kinds
1051
+
1052
+ ```typescript
1053
+ export const CHAIN_KINDS = ['chain', 'prep'] as const;
1054
+ export type ChainKind = (typeof CHAIN_KINDS)[number];
1055
+ ```
1056
+
1057
+ | Kind | Definition | Has worktree? |
1058
+ |------|------------|:-------------:|
1059
+ | `chain` | Worktree lineage seeded by edit-capable specialist | Yes |
1060
+ | `prep` | Standalone job without worktree lineage | No |
1061
+
1062
+ ### Identity derivation
1063
+
1064
+ `derivePersistedChainIdentity()` computes `chain_kind` from SupervisorStatus:
1065
+
1066
+ ```typescript
1067
+ // Deterministic fallback:
1068
+ // - missing chain markers + no worktree lineage => prep
1069
+ // - any lineage marker/worktree => chain rooted at owner/id
1070
+ const isChainJob = Boolean(
1071
+ status.worktree_path || status.worktree_owner_job_id || status.chain_id || status.chain_root_job_id
1072
+ );
1073
+ ```
1074
+
1075
+ Chain identity fields:
1076
+ - `chain_kind` — 'chain' or 'prep'
1077
+ - `chain_id` — unique identifier (job ID for prep, owner job ID for chain)
1078
+ - `chain_root_job_id` — root job that owns the worktree
1079
+ - `chain_root_bead_id` — bead that seeded the chain
1080
+
1081
+ ### Chain membership tracking
1082
+
1083
+ `epic_chain_membership` table links chains to epics:
1084
+ - `chain_id` — unique chain identifier
1085
+ - `epic_id` — parent epic ID
1086
+ - `chain_root_bead_id` — optional explicit bead linkage
1087
+ - `chain_root_job_id` — optional job linkage
1088
+
1089
+ ## 12) Epic readiness evaluation (`epic-readiness.ts`)
1090
+
1091
+ Epic readiness determines merge eligibility from chain/prep job states.
1092
+
1093
+ ### Readiness states
1094
+
1095
+ | State | Condition |
1096
+ |-------|----------|
1097
+ | `unresolved` | Open epic with active work |
1098
+ | `resolving` | Resolving epic with active work or blockers |
1099
+ | `merge_ready` | Prep terminal + all chains PASS |
1100
+ | `blocked` | Non-terminal, missing reviewer/fix-loop closure |
1101
+ | `failed` | Prep error or chain review failure |
1102
+ | `merged` / `abandoned` | Terminal passthrough |
1103
+
1104
+ ### Chain readiness per chain
1105
+
1106
+ | State | Condition |
1107
+ |-------|----------|
1108
+ | `pending` | Active jobs (`starting|running|waiting`) |
1109
+ | `blocked` | No reviewer verdict or fix-loop incomplete |
1110
+ | `failed` | Latest reviewer verdict is PARTIAL/FAIL |
1111
+ | `pass` | Latest reviewer verdict is PASS |
1112
+
1113
+ ### Prep semantics
1114
+
1115
+ Prep jobs (`chain_kind !== 'chain'`) affect readiness:
1116
+ - Running prep → blocks merge
1117
+ - Errored prep → fails epic
1118
+ - Done prep → satisfies prep completion
1119
+
1120
+ ### Auto-transition logic
1121
+
1122
+ `syncEpicStateFromReadiness()` persists state transitions automatically:
1123
+ - `open → resolving` when unresolved work exists
1124
+ - `resolving → merge_ready` when all conditions met
1125
+ - `merge_ready → resolving` if blockers reappear
1126
+ - `resolving|merge_ready → failed` on fatal failure
1127
+
1128
+ See `docs/epic-readiness.md` for full evaluator specification.
1129
+
1130
+ ## 13) `sp end` — Epic-aware session close (`end.ts`)
1131
+
1132
+ `sp end` integrates with epic lifecycle for publication:
1133
+
1134
+ ### Synopsis
1135
+
1136
+ ```bash
1137
+ specialists end [--bead <id>|--epic <id>] [--pr] [--rebuild]
1138
+ ```
1139
+
1140
+ ### Flags
1141
+
1142
+ - `--epic <id>`: Redirect to `sp epic merge <id>` (canonical publication path)
1143
+ - `--pr`: Create pull request instead of direct merge
1144
+ - `--rebuild`: Run build after merge
1145
+
1146
+ ### Behavior
1147
+
1148
+ 1. **Epic detection**: If current chain belongs to unresolved epic, redirects to `sp epic merge`
1149
+ 2. **Chain guard**: `checkEpicUnresolvedGuard()` checks epic membership
1150
+ 3. **Auto-redirect**: Prints redirect message, delegates to epic merge handler
1151
+
1152
+ Example:
1153
+ ```bash
1154
+ sp end --epic unitAI-3f7b --pr
1155
+ # → redirects to: sp epic merge unitAI-3f7b --pr
1156
+ ```
1157
+
1158
+ ### Workspace inference
1159
+
1160
+ If no `--bead` or `--epic` provided, `detectCurrentBeadIdFromWorkspace()`:
1161
+ 1. Queries SQLite for job with matching `worktree_path` and `chain_root_bead_id`
1162
+ 2. Falls back to branch name parsing (`feature/unitAI-xxx-...`)
1163
+
1164
+ ## 14) Canonical references
1165
+
1166
+ | Component | Path | Responsibility |
1167
+ |-----------|------|----------------|
1168
+ | Protocol | `pi/rpc/` | JSONL framing, RPC types, client semantics |
1169
+ | Protocol boundary docs | `docs/pi-rpc-boundary.md` | Ownership boundary between pi RPC protocol and Specialists adaptation |
1170
+ | RPC adapter | `src/pi/session.ts` | Spawns Pi, parses NDJSON, correlates requests |
1171
+ | Job registry | `src/specialist/job-root.ts` | Git-common-root-anchored jobs dir |
1172
+ | Worktree isolation | `src/specialist/worktree.ts` | Provisioning, branch naming, reuse detection |
1173
+ | Durable lifecycle | `src/specialist/supervisor.ts` | Status, events, results, GitNexus tracking, FIFO steering, lineage fields, context denorm |
1174
+ | Timeline schema | `src/specialist/timeline-events.ts` | Feed v2 event vocabulary, mapping, constructors |
1175
+ | Process snapshot CLI | `src/cli/ps.ts` | Job tree view, context%, bead titles, urgency sort, JSON output |
1176
+ | Worktree docs | `docs/worktrees.md` | Operator-facing worktree isolation reference |