@jaggerxtrm/specialists 3.16.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.
- package/README.md +268 -132
- package/config/mandatory-rules/json-only-final-output.md +13 -0
- package/config/mandatory-rules/research-tool-routing.md +4 -0
- package/config/mandatory-rules/service-skills-diff-scan-mandatory.md +26 -0
- package/config/mandatory-rules/service-skills-gitnexus-triage.md +5 -0
- package/config/mandatory-rules/test-runner-execution-scope.md +1 -1
- package/config/skills/setup-specialists/SKILL.md +556 -0
- package/config/skills/specialists-creator/SKILL.md +160 -5
- package/config/skills/specialists-creator/scripts/audit-spec-uniformity.mjs +1 -1
- package/config/skills/using-specialists-auto/SKILL.md +1 -1
- package/config/skills/using-specialists-v2/SKILL.md +7 -7
- package/config/skills/using-specialists-v3/SKILL.md +223 -147
- package/config/specialists/bare.specialist.json +3 -3
- package/config/specialists/changelog-drafter.specialist.json +9 -6
- package/config/specialists/changelog-keeper.specialist.json +10 -7
- package/config/specialists/debugger.specialist.json +8 -6
- package/config/specialists/executor.specialist.json +9 -7
- package/config/specialists/explorer.specialist.json +8 -6
- package/config/specialists/memory-processor.specialist.json +7 -5
- package/config/specialists/node-coordinator.specialist.json +7 -5
- package/config/specialists/obligations-scanner.specialist.json +149 -0
- package/config/specialists/overthinker.specialist.json +7 -5
- package/config/specialists/planner.specialist.json +8 -6
- package/config/specialists/quant-methodologist.specialist.json +145 -0
- package/config/specialists/quant-researcher.specialist.json +144 -0
- package/config/specialists/researcher.specialist.json +14 -9
- package/config/specialists/reviewer.specialist.json +11 -9
- package/config/specialists/seconder.specialist.json +170 -0
- package/config/specialists/security-auditor.specialist.json +6 -4
- package/config/specialists/service-skills-sync.specialist.json +93 -0
- package/config/specialists/specialists-creator.specialist.json +9 -7
- package/config/specialists/sync-docs.specialist.json +6 -4
- package/config/specialists/test-engineer.specialist.json +134 -0
- package/config/specialists/test-runner.specialist.json +11 -9
- package/config/specialists/transcriber.specialist.json +59 -0
- package/config/specialists/xt-merge.specialist.json +7 -5
- package/dist/asset-contract.json +39 -8
- package/dist/index.js +37083 -26912
- package/dist/lib.js +9850 -6147
- package/dist/types/cli/console/components.d.ts +83 -0
- package/dist/types/cli/console/components.d.ts.map +1 -0
- package/dist/types/cli/console/config-source.d.ts +58 -0
- package/dist/types/cli/console/config-source.d.ts.map +1 -0
- package/dist/types/cli/console/forensic.d.ts +11 -0
- package/dist/types/cli/console/forensic.d.ts.map +1 -0
- package/dist/types/cli/console/git.d.ts +28 -0
- package/dist/types/cli/console/git.d.ts.map +1 -0
- package/dist/types/cli/console/help.d.ts +2 -0
- package/dist/types/cli/console/help.d.ts.map +1 -0
- package/dist/types/cli/console/log.d.ts +13 -0
- package/dist/types/cli/console/log.d.ts.map +1 -0
- package/dist/types/cli/console/repo-config.d.ts +26 -0
- package/dist/types/cli/console/repo-config.d.ts.map +1 -0
- package/dist/types/cli/console/repo-discovery.d.ts +12 -0
- package/dist/types/cli/console/repo-discovery.d.ts.map +1 -0
- package/dist/types/cli/console/runtime.d.ts +12 -0
- package/dist/types/cli/console/runtime.d.ts.map +1 -0
- package/dist/types/cli/console/subscribe-prototype.d.ts +18 -0
- package/dist/types/cli/console/subscribe-prototype.d.ts.map +1 -0
- package/dist/types/cli/console/theme.d.ts +91 -0
- package/dist/types/cli/console/theme.d.ts.map +1 -0
- package/dist/types/cli/console/types.d.ts +231 -0
- package/dist/types/cli/console/types.d.ts.map +1 -0
- package/dist/types/cli/console/view-model.d.ts +252 -0
- package/dist/types/cli/console/view-model.d.ts.map +1 -0
- package/dist/types/cli/console.d.ts +2 -0
- package/dist/types/cli/console.d.ts.map +1 -0
- package/dist/types/cli/db.d.ts.map +1 -1
- package/dist/types/cli/doctor.d.ts.map +1 -1
- package/dist/types/cli/edit.d.ts.map +1 -1
- package/dist/types/cli/epic.d.ts.map +1 -1
- package/dist/types/cli/feed.d.ts.map +1 -1
- package/dist/types/cli/forensic.d.ts +2 -0
- package/dist/types/cli/forensic.d.ts.map +1 -0
- package/dist/types/cli/format-helpers.d.ts +4 -2
- package/dist/types/cli/format-helpers.d.ts.map +1 -1
- package/dist/types/cli/help.d.ts.map +1 -1
- package/dist/types/cli/init.d.ts +10 -0
- package/dist/types/cli/init.d.ts.map +1 -1
- package/dist/types/cli/list.d.ts.map +1 -1
- package/dist/types/cli/log.d.ts +2 -0
- package/dist/types/cli/log.d.ts.map +1 -0
- package/dist/types/cli/metrics.d.ts +2 -0
- package/dist/types/cli/metrics.d.ts.map +1 -0
- package/dist/types/cli/ps.d.ts.map +1 -1
- package/dist/types/cli/result.d.ts.map +1 -1
- package/dist/types/cli/resume.d.ts.map +1 -1
- package/dist/types/cli/run.d.ts +1 -0
- package/dist/types/cli/run.d.ts.map +1 -1
- package/dist/types/cli/script.d.ts +3 -0
- package/dist/types/cli/script.d.ts.map +1 -1
- package/dist/types/cli/serve.d.ts.map +1 -1
- package/dist/types/cli/setup.d.ts +19 -1
- package/dist/types/cli/setup.d.ts.map +1 -1
- package/dist/types/cli/status.d.ts.map +1 -1
- package/dist/types/cli/steer.d.ts.map +1 -1
- package/dist/types/cli/version-check.d.ts +1 -0
- package/dist/types/cli/version-check.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/pi/session.d.ts +11 -1
- package/dist/types/pi/session.d.ts.map +1 -1
- package/dist/types/server.d.ts +15 -0
- package/dist/types/server.d.ts.map +1 -1
- package/dist/types/specialist/benchmarks.d.ts +37 -0
- package/dist/types/specialist/benchmarks.d.ts.map +1 -0
- package/dist/types/specialist/chain-identity.d.ts +7 -1
- package/dist/types/specialist/chain-identity.d.ts.map +1 -1
- package/dist/types/specialist/control.d.ts.map +1 -1
- package/dist/types/specialist/forensic-events.d.ts +138 -0
- package/dist/types/specialist/forensic-events.d.ts.map +1 -0
- package/dist/types/specialist/forensic-renderer.d.ts +34 -0
- package/dist/types/specialist/forensic-renderer.d.ts.map +1 -0
- package/dist/types/specialist/git-diff-evidence.d.ts +28 -0
- package/dist/types/specialist/git-diff-evidence.d.ts.map +1 -0
- package/dist/types/specialist/global-config.d.ts +389 -0
- package/dist/types/specialist/global-config.d.ts.map +1 -0
- package/dist/types/specialist/job-file-output.d.ts +2 -0
- package/dist/types/specialist/job-file-output.d.ts.map +1 -1
- package/dist/types/specialist/launch.d.ts +1 -0
- package/dist/types/specialist/launch.d.ts.map +1 -1
- package/dist/types/specialist/live-aggregates.d.ts +46 -0
- package/dist/types/specialist/live-aggregates.d.ts.map +1 -0
- package/dist/types/specialist/loader.d.ts +50 -1
- package/dist/types/specialist/loader.d.ts.map +1 -1
- package/dist/types/specialist/model-chain.d.ts +7 -0
- package/dist/types/specialist/model-chain.d.ts.map +1 -0
- package/dist/types/specialist/model-probes.d.ts +28 -0
- package/dist/types/specialist/model-probes.d.ts.map +1 -0
- package/dist/types/specialist/node-contract.d.ts +18 -18
- package/dist/types/specialist/node-supervisor.d.ts.map +1 -1
- package/dist/types/specialist/observability-db.d.ts +1 -1
- package/dist/types/specialist/observability-db.d.ts.map +1 -1
- package/dist/types/specialist/observability-sqlite.d.ts +25 -0
- package/dist/types/specialist/observability-sqlite.d.ts.map +1 -1
- package/dist/types/specialist/preset-resolver.d.ts +56 -0
- package/dist/types/specialist/preset-resolver.d.ts.map +1 -0
- package/dist/types/specialist/prometheus-projection.d.ts +25 -0
- package/dist/types/specialist/prometheus-projection.d.ts.map +1 -0
- package/dist/types/specialist/runner.d.ts +29 -1
- package/dist/types/specialist/runner.d.ts.map +1 -1
- package/dist/types/specialist/schema.d.ts +181 -63
- package/dist/types/specialist/schema.d.ts.map +1 -1
- package/dist/types/specialist/script-runner.d.ts +5 -1
- package/dist/types/specialist/script-runner.d.ts.map +1 -1
- package/dist/types/specialist/snapshot-diff.d.ts +8 -0
- package/dist/types/specialist/snapshot-diff.d.ts.map +1 -0
- package/dist/types/specialist/source-queue.d.ts +13 -0
- package/dist/types/specialist/source-queue.d.ts.map +1 -0
- package/dist/types/specialist/supervisor.d.ts +39 -2
- package/dist/types/specialist/supervisor.d.ts.map +1 -1
- package/dist/types/specialist/timeline-events.d.ts +88 -2
- package/dist/types/specialist/timeline-events.d.ts.map +1 -1
- package/dist/types/tools/specialist/resume_specialist.tool.d.ts +4 -4
- package/dist/types/tools/specialist/specialist_status.tool.d.ts +1 -1
- package/dist/types/tools/specialist/steer_specialist.tool.d.ts +4 -4
- package/dist/types/tools/specialist/use_specialist.tool.d.ts +16 -16
- package/docs/ARCHITECTURE.md +1176 -0
- package/docs/TODO.md +9 -0
- package/docs/architecture.md +11 -0
- package/docs/archive/2026-03-13-bd-decision-context-template-plan.md +96 -0
- package/docs/archive/AGENT-HANDOFF.md +351 -0
- package/docs/archive/PARITY-ANALYSIS.md +296 -0
- package/docs/archive/SPECIALISTS_REFACTOR.md.md +30 -0
- package/docs/archive/cc-programmatic.md +216 -0
- package/docs/archive/claude-agent-sdk.md +594 -0
- package/docs/archive/decision-specialist-directory.md +41 -0
- package/docs/archive/discoveries.md +148 -0
- package/docs/archive/executor-benchmark-protocol.md +198 -0
- package/docs/archive/future-features.md +66 -0
- package/docs/archive/gzrx-completion-critique.md +183 -0
- package/docs/archive/gzrx-research-notes.md +401 -0
- package/docs/archive/gzrx-tool-catalog.md +760 -0
- package/docs/archive/iron-review-hardening-qa-chain-substrate.md +322 -0
- package/docs/archive/iron-review-hardening.html +1004 -0
- package/docs/archive/issuetracking.md +312 -0
- package/docs/archive/qa-v3.0.2.md +220 -0
- package/docs/archive/restructure.md +231 -0
- package/docs/archive/script-specialists.md +1254 -0
- package/docs/archive/spec-v3.md +792 -0
- package/docs/archive/specialist-stats.md +127 -0
- package/docs/archive/specialists-friction-audit.md +1347 -0
- package/docs/archive/specialists-runtime-critique.md +170 -0
- package/docs/archive/specialists-service-evaluation.md +713 -0
- package/docs/archive/specialists-substrate-alignment.md +255 -0
- package/docs/archive/substrate-review.md +1288 -0
- package/docs/archive/test-writer-specialist.md +254 -0
- package/docs/archive/using-specialists-v3-improvements-2026-05-09.md +600 -0
- package/docs/archive/xtrm-specialists-analysis.md +314 -0
- package/docs/authoring.md +701 -0
- package/docs/background-jobs.md +203 -0
- package/docs/bare-specialists.md +83 -0
- package/docs/benchmarks/executor-benchmark-runner.md +66 -0
- package/docs/bootstrap.md +161 -0
- package/docs/cli-reference.md +1645 -0
- package/docs/deploying-alongside.md +155 -0
- package/docs/design/README.md +36 -0
- package/docs/design/darth-feedor-migration.md +290 -0
- package/docs/design/roadmap/README.md +32 -0
- package/docs/design/roadmap/chain-templates/README.md +146 -0
- package/docs/design/roadmap/chain-templates/code-quick.formula.json +27 -0
- package/docs/design/roadmap/chain-templates/code-standard.formula.json +126 -0
- package/docs/design/roadmap/chain-templates/code-with-advisors.formula.json +128 -0
- package/docs/design/roadmap/chain-templates/code-with-tests.formula.json +76 -0
- package/docs/design/roadmap/chain-templates/debug.formula.json +128 -0
- package/docs/design/roadmap/chain-templates/doc-sync.formula.json +28 -0
- package/docs/design/roadmap/chain-templates/memory-hygiene.formula.json +27 -0
- package/docs/design/roadmap/chain-templates/planning.formula.json +27 -0
- package/docs/design/roadmap/chain-templates/premortem.formula.json +26 -0
- package/docs/design/roadmap/chain-templates/release-prep.formula.json +36 -0
- package/docs/design/roadmap/chain-templates/research-only.formula.json +28 -0
- package/docs/design/roadmap/chain-templates/restitch.formula.json +125 -0
- package/docs/design/roadmap/chain-templates/security-deep.formula.json +161 -0
- package/docs/design/roadmap/chain-templates/test-only.formula.json +52 -0
- package/docs/design/roadmap/chain-templates/triage.formula.json +35 -0
- package/docs/design/roadmap/history/handoff-from-substrate-design.md +87 -0
- package/docs/design/roadmap/history/substrate-reconciliation.md +105 -0
- package/docs/design/roadmap/specialists-roadmap.md +1193 -0
- package/docs/design/sp-console-subscribe-via-materializer.md +231 -0
- package/docs/design/sp-console-tui-mock-v2.html +293 -0
- package/docs/design/sp-console-tui-mock.html +120 -0
- package/docs/design/sp-console-tui.md +340 -0
- package/docs/design/specialist-agentops-suite.md +323 -0
- package/docs/design/substrate/channels.md +14 -0
- package/docs/design/substrate/devops-platform-engineering-prd.md +446 -0
- package/docs/design/substrate/html-design-example.md +339 -0
- package/docs/design/xtrm-tiers-architecture.svg +132 -0
- package/docs/devops/dependency-verdict-materialization.md +46 -0
- package/docs/epic-readiness.md +75 -0
- package/docs/examples/mercury-atomic-summarizer.specialist.json +26 -0
- package/docs/examples/smoke-echo-text-expected-keys.specialist.json +25 -0
- package/docs/examples/smoke-echo.specialist.json +25 -0
- package/docs/features.md +1577 -0
- package/docs/hooks.md +81 -0
- package/docs/installation.md +142 -0
- package/docs/manifest.md +184 -0
- package/docs/mcp-servers.md +73 -0
- package/docs/mcp-tools.md +71 -0
- package/docs/nodes.md +231 -0
- package/docs/observability-metrics.md +152 -0
- package/docs/operator/sp-console-v2-walkthrough.md +410 -0
- package/docs/overrides-guide.md +306 -0
- package/docs/pi-rpc-boundary.md +118 -0
- package/docs/pi-session.md +195 -0
- package/docs/release.md +22 -0
- package/docs/skills.md +132 -0
- package/docs/specialists/handoff-schema.md +181 -0
- package/docs/specialists-catalog.md +99 -0
- package/docs/specialists-service-install.md +226 -0
- package/docs/specialists-service.md +363 -0
- package/docs/surface-ownership.md +138 -0
- package/docs/upgrade-notes/kan-90-global-user-config.md +114 -0
- package/docs/upgrade-notes/kan-91-expanded-overrides.md +261 -0
- package/docs/workflow.md +114 -0
- package/docs/worktree.md +71 -0
- package/docs/worktrees.md +309 -0
- package/package.json +17 -12
- package/config/specialists/code-sanity.specialist.json +0 -108
|
@@ -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 |
|