@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,713 @@
|
|
|
1
|
+
# specialists-service — Production Evaluation
|
|
2
|
+
|
|
3
|
+
> Anchor for revising `docs/specialists-service.md`. Treats the existing draft as poorly written and grounds every decision in actual schema/runtime evidence.
|
|
4
|
+
> Sources: explorer `unitAI-hr6mm` (runtime map), overthinker `unitAI-nauhr` (first-pass critique), overthinker `unitAI-f8hii` (second-pass critique). Files: `src/specialist/{schema,loader,runner,supervisor,observability-sqlite,mandatory-rules,memory-retrieval}.ts`, `src/cli/{edit,run}.ts`, `src/pi/session.ts`, `src/index.ts`, `package.json`.
|
|
5
|
+
|
|
6
|
+
## Open blockers (must resolve before implementation)
|
|
7
|
+
|
|
8
|
+
A second-pass critique (`unitAI-f8hii`) found unresolved contradictions and gaps. Each is patched inline below; the table here is the decision-forcing index.
|
|
9
|
+
|
|
10
|
+
| # | Blocker | Resolution | Section |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| B1 | Loader scans `.specialists/default`, `config/specialists`, legacy dirs — service must not | Service-only loader mode: single root, no fallback, fail-closed on invalid spec | §9.5, §11.1 |
|
|
13
|
+
| B2 | `skills.paths` + `prompt.skill_inherit` inject host files into pi prompts — prompt-injection vector | Force-empty on script surface unless trusted-mode flag set; provenance-log every resolved skill source | §11.1 |
|
|
14
|
+
| B3 | `execution.max_retries` exists in schema/runner — script surface must force `0` | Compatibility validator forces `0`; HTTP/CLI response exposes `meta.attempts` | §11.1 |
|
|
15
|
+
| B4 | `sp serve`, `sp script`, `sp validate --target`, `runScript()` export do not exist in `src/index.ts` or `package.json` today | Required-implementation surface enumerated; image cannot ship before these land | §12 |
|
|
16
|
+
| B5 | Read-only `~/.pi` mount conflicts with pi's oauth refresh write-back path | Two supported modes: (a) read-only mount + host-side token freshness (refresh unsupported in container), (b) writable `auth.json` cache scoped to `/state/pi-tokens` with strict file-mode guard | §9.5, §11.2 |
|
|
17
|
+
| B6 | Strict-validation verdict in §29 was wrong — engine exists at `runner.ts:689-789` and warns; enforcement mode is what's missing | Reword to "convert configurable validation failures from warning to typed error per spec" | §29 |
|
|
18
|
+
| B7 | Cache invalidation only handles modify-time; deletes, renames, atomic-saves serve ghost specs | Watcher emits delete/rename → cache purge; per-request snapshot capture so in-flight requests are immune to mid-request reload | §9.8 |
|
|
19
|
+
| B8 | DB-write failure mid-request is undefined | Model success + audit-write failure → response succeeds with `meta.trace_persisted=false`; structured error log; readiness toggles `degraded` if failure rate > threshold | §9.9 |
|
|
20
|
+
| B9 | SIGTERM mid-request orphans pi subprocess and breaks client socket | Drain contract: on SIGTERM, stop accepting, finish in-flight up to grace, SIGTERM children, SIGKILL after grace+5s, return `503 shutting_down` to queued | §9.13 |
|
|
21
|
+
| B10 | Credential model contradicted itself — old secret-file text remained after pi-delegation pass | All secret-file/`--api-key-*` references removed; pi delegation is the only path | §9.3, §9.15 |
|
|
22
|
+
| B11 | Multi-replica shared-state, supply-chain, and transport-parity-with-`--no-trace` claims unaddressed | New §11 "Unsupported topologies and supply-chain requirements"; transport-parity rephrased as "same audit schema when tracing enabled" | §10.1, §11 |
|
|
23
|
+
|
|
24
|
+
## TL;DR
|
|
25
|
+
|
|
26
|
+
- **Boundary cuts (no orchestration, no keep-alive, no worktree ownership, no Supervisor file lifecycle): mostly right.** They fit a synchronous HTTP boundary.
|
|
27
|
+
- **Mechanism (a separate "script class" with its own forbidden-fields list): wrong.** The doc forbids names that don't exist in the schema (`tools`, `permissions`, `keep_alive`, `worktree`, top-level `scripts`) and outlaws fields that do exist and are load-bearing (`skills`, `capabilities`, `beads_integration`).
|
|
28
|
+
- **Schema fork risk is the headline.** The fix is one schema with a `class` discriminator + per-surface compatibility validators. Anything else guarantees drift between `sp run` and the HTTP service.
|
|
29
|
+
- **Two latent bugs the spec ignores but production will surface:** Zod object schemas strip unknown keys on parse → `sp edit` already loses fields (`communication.publishes` in `config/specialists/explorer.specialist.json`). **Verified by direct round-trip test** against the compiled schema. And `renderTemplate()` doesn't fail on missing `$vars` — the promised `template_variable_missing` error has no implementation path.
|
|
30
|
+
- **Distribution model is sidecar-per-service.** A consumer repo ships only a pinned `docker-compose.yml`, a `.specialists/user/` directory of JSON specs, and a bind-mount of `~/.pi`. **No secret file, no `--api-key-*` flag, no env-var key plumbing** — pi already owns multi-provider credentials in `~/.pi/agent/auth.json`, the service delegates entirely. The published image carries `sp serve`, `sp script`, and `sp validate` so authoring and non-HTTP usage work without the specialists source. Section 9 spells out the mount contract, version policy, hot reload semantics, and air-gap support. Section 10 covers `sp script` CLI, programmatic embedding, and cron.
|
|
31
|
+
|
|
32
|
+
## Why we are dropping functionality (justified cuts)
|
|
33
|
+
|
|
34
|
+
| Drop | Justified because… | Risk if we don't drop |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Multi-stage orchestration | Sync HTTP one-shot can't own pipeline state, idempotency, saga semantics, partial replay. | Service becomes a queue/orchestrator with no leader, no retry contract; debuggability collapses. |
|
|
37
|
+
| Keep-alive / steer / resume | HTTP request cannot hold a long-lived pi RPC session across LBs/timeouts/retries. | Leaked sessions, hung clients, stateful retries that fight load balancers. |
|
|
38
|
+
| Worktree ownership | A stateless service should not branch git. | Branch leaks, GC complexity, multi-tenant cross-talk on shared FS. |
|
|
39
|
+
| Supervisor file lifecycle (`status.json`, `events.jsonl`, `result.txt`, `jobs/<id>/`) | Designed for long-running async jobs. One sync request doesn't need a job dir. | Disk churn per request; concurrency bugs on shared dir; pointless I/O. |
|
|
40
|
+
|
|
41
|
+
These match real failure modes. They are not "simpler is better"; they're cost-of-failure cuts.
|
|
42
|
+
|
|
43
|
+
## Why we are NOT dropping functionality (unjustified cuts in the spec)
|
|
44
|
+
|
|
45
|
+
Each row is a "non-goal" or "forbidden field" the doc proposes that we should reshape, not drop.
|
|
46
|
+
|
|
47
|
+
### §7 non-goals
|
|
48
|
+
|
|
49
|
+
| Doc says | Reality | Verdict |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Nested / strict JSON schema validation — non-goal | **Engine already exists** at `runner.ts:689-789` (recursive object/array/type validation). What's missing is the *enforcement mode*: failures are emitted as warnings only. The doc's `schema_required_missing` enum is unreachable today. | **RESHAPE.** Keep the engine. Add an `output_validation` mode in the spec (`warn` default, `fail_request` opt-in). When `fail_request`, validation failures map to the typed error `output_validation_failed` with detail. Response always includes `meta.validation_mode` so consumers can detect what was applied. |
|
|
52
|
+
| Backend-only swap — non-goal | For first migration, sure. As a product rule it's too rigid: many projects want transport-only swap while keeping local prompt logic. | **RESHAPE.** Split transport policy from rendering policy; default to named-spec rendering, but don't pin the product to "you must give us the template too". |
|
|
53
|
+
| New specialist categories beyond `class: "script"` — non-goal | The doc invents `class: "script"` while declaring no future classes — guaranteeing a second schema later. Hostile to forward compatibility. | **DROP.** Add the discriminator now; allow future classes via per-surface validators. |
|
|
54
|
+
| Tool use — blanket non-goal | Justified by default for shared multi-tenant. But the doc enforces it via a *fake schema field* instead of runtime policy on `execution.permission_required`. | **RESHAPE.** Default `permission_required: READ_ONLY` for the script surface; runtime gate, not schema fiction. |
|
|
55
|
+
| Beads — non-goal | Owning beads in the service is correct (no `bd create/close`). But cross-system correlation IDs are operationally needed. | **RESHAPE.** Service must not write beads; should accept a `correlation` field (incl. caller-supplied bead ref) and emit it in the audit row. |
|
|
56
|
+
| File-based observability — never | SQLite primary is fine. Forbidding any optional debug sink loses forensic capability for air-gapped/sidecar deployments. | **RESHAPE.** SQLite required, optional file sink behind a flag. |
|
|
57
|
+
|
|
58
|
+
### §3 "forbidden fields"
|
|
59
|
+
|
|
60
|
+
The doc lists fields by names that don't all exist. The corrected mapping:
|
|
61
|
+
|
|
62
|
+
| Doc forbids | Actual schema field | Verdict | Reason |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `tools` | not a field; real control = `execution.permission_required` + `capabilities.required_tools` | **RESHAPE** | Attack the right knob. Service surface caps `permission_required: READ_ONLY` and rejects incompatible `required_tools` values. |
|
|
65
|
+
| `skills` | `specialist.skills.{paths,scripts}` (real, used in runner.ts:897-904 and via `--skill`) | **DON'T forbid** | Hard-forbidding the field breaks shared-spec reuse. Forbid the *behavior* (script execution; arbitrary skill injection) per surface policy. |
|
|
66
|
+
| `scripts` | not top-level; real = `skills.scripts` | **RESHAPE** | Service surface always rejects `skills.scripts` (shell exec); no trusted-mode bypass until a separate sandboxed design exists. |
|
|
67
|
+
| `keep_alive` | not a field; real = `execution.interactive` | **RESHAPE** | Service surface requires `execution.interactive: false`. Name the actual field. |
|
|
68
|
+
| `worktree` | not a field; real = `execution.requires_worktree` | **RESHAPE** | Service surface requires `requires_worktree: false`. |
|
|
69
|
+
| `beads_integration` | exists, used by runner.ts:1160-1167 | **DON'T forbid** | Field stays in shared schema. Service forces effective behavior to `never` at runtime. Loader doesn't reject it. |
|
|
70
|
+
| `capabilities` | exists, validated pre-run at runner.ts:205-216 | **DON'T forbid** | `external_commands` is unsafe in service → reject those values. `required_tools` is metadata → keep. Field-level forbid is too coarse. |
|
|
71
|
+
| `permissions` | not a field; real = `execution.permission_required` | **DELETE the entry** | Proves the doc author wasn't reading the schema. |
|
|
72
|
+
|
|
73
|
+
**Why this matters in production:** every "forbidden field" in the doc that doesn't match a real schema name signals a service that won't actually run real specialist files. Every forbid on a real field forks the schema and breaks `sp edit` round-tripping for any spec touched by both surfaces.
|
|
74
|
+
|
|
75
|
+
## Latent bugs the spec inherits (must fix before shipping)
|
|
76
|
+
|
|
77
|
+
### Bug 1 — Silent unknown-key stripping (already broken today, **verified**)
|
|
78
|
+
|
|
79
|
+
`SpecialistSchema` (schema.ts) uses default Zod `.object({...})` which strips unknown keys. Verified with a direct round-trip test against the compiled schema:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
input: specialist.communication.publishes = ["foo"]
|
|
83
|
+
specialist.UNKNOWN_TOP = "should-survive"
|
|
84
|
+
output: communication = { next_specialists: "x" } // publishes gone
|
|
85
|
+
UNKNOWN_TOP = undefined // top-level gone
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Real production example: `config/specialists/explorer.specialist.json` ships with `communication.publishes`; after `parseSpecialist()` the field is gone.
|
|
89
|
+
|
|
90
|
+
This means:
|
|
91
|
+
- `sp edit` that re-serializes from a parsed object **already loses fields** today.
|
|
92
|
+
- Any future class discriminator or per-class extension will silently disappear under parse → save.
|
|
93
|
+
|
|
94
|
+
**Fix:** make every nested object in `SpecialistSchema` use `.passthrough()`. Round-trip becomes lossless. This is a precondition for the discriminator design, not optional cleanup.
|
|
95
|
+
|
|
96
|
+
### Bug 2 — `template_variable_missing` is unimplemented
|
|
97
|
+
|
|
98
|
+
The spec promises this error. `templateEngine.ts` `renderTemplate()` leaves unmatched `$var` literally in the prompt and returns success. The error type as written can never fire.
|
|
99
|
+
|
|
100
|
+
**Fix:** pre-scan rendered template for un-substituted `$ident` tokens and raise the typed error before invoking pi. Otherwise this is a documentation lie.
|
|
101
|
+
|
|
102
|
+
### Bug 3 — Closed error enum, missing real conditions
|
|
103
|
+
|
|
104
|
+
The §2 enum lacks: `circuit_open`, `provider_overloaded`, `output_too_large`, `prompt_too_large`, `model_not_allowed`, `concurrency_limit`, `pi_crash`, `reload_in_progress`, `validation_mode_mismatch`. These are concrete failure modes already present (or about to be) in the runtime path; mapping them to `internal` destroys observability.
|
|
105
|
+
|
|
106
|
+
**Fix:** keep a stable top-level enum *and* add a structured `error_detail.{code, provider_code, validation_detail}` payload.
|
|
107
|
+
|
|
108
|
+
### Bug 4 — Model resolution is undocumented
|
|
109
|
+
|
|
110
|
+
Real order in runner.ts:867-874 is:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
backendOverride (caller) → execution.model (primary)
|
|
114
|
+
→ if circuitBreaker.isAvailable(primary) === false → execution.fallback_model
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Plus `execution.thinking_level` is forwarded as `pi --thinking <level>` (runner.ts:1196). Plus the circuit breaker is global, not request-scoped — a request that "didn't ask for fallback" still gets it.
|
|
118
|
+
|
|
119
|
+
The spec mentions only `model_override`. That is materially wrong in production: the caller will see model swaps it can't predict.
|
|
120
|
+
|
|
121
|
+
**Fix:** document the full resolution order; add `meta.resolved_model`, `meta.fallback_used`, `meta.breaker_state`, `meta.thinking_level` to the response; restrict `model_override` to a per-specialist allowlist (otherwise it bypasses cost/safety/provider pinning).
|
|
122
|
+
|
|
123
|
+
## The schema decision — single SSOT with class discriminator
|
|
124
|
+
|
|
125
|
+
This is the architectural choice the spec gets wrong. Two paths:
|
|
126
|
+
|
|
127
|
+
**Bad (the current draft):** new "script class" gets its own field rules, separate validation surface, hard-forbids on real fields. Repo now has two specialist languages. `sp edit` either loses round-trip or has to know about both.
|
|
128
|
+
|
|
129
|
+
**Good (what we should do):**
|
|
130
|
+
|
|
131
|
+
1. Base `SpecialistSchema` becomes a **superset** with `.passthrough()` on every nested object.
|
|
132
|
+
2. Add `specialist.class: "agent" | "script"` (default `"agent"`).
|
|
133
|
+
3. Two **runtime compatibility validators**, layered on top of base parse:
|
|
134
|
+
- `validateForAgent(spec)` — current `sp run` behavior.
|
|
135
|
+
- `validateForScriptService(spec)` — applies script-surface rules below.
|
|
136
|
+
4. `sp edit` continues to operate on raw JSON (it already does — see edit.ts) and now accepts the `class` field. No round-trip changes.
|
|
137
|
+
|
|
138
|
+
### Script-surface compatibility rules (replaces the §3 forbidden-fields list)
|
|
139
|
+
|
|
140
|
+
For `class: "script"` requested by the HTTP endpoint, all CLI `sp script` invocations, and all programmatic `runScript()` calls:
|
|
141
|
+
|
|
142
|
+
- `execution.interactive` MUST be `false`.
|
|
143
|
+
- `execution.requires_worktree` MUST be `false`.
|
|
144
|
+
- `execution.permission_required` MUST be `READ_ONLY` (no tools).
|
|
145
|
+
- `execution.max_retries` MUST be `0`. Retries are owned by the caller (HTTP client, cron wrapper, breaker). Internal retries amplify cost and duplicate side effects on non-idempotent downstreams. Response exposes `meta.attempts: 1` for transport parity. *(B3)*
|
|
146
|
+
- **`skills.paths` MUST be empty** unless launched in trusted mode (`--allow-skills`). Reason: loader resolves these to host-FS files (`loader.ts:182-189`) and pi injects them into the prompt — a spec author can prompt-inject from any host path the service UID can read. In trusted mode, every resolved skill path is logged in the audit row (`meta.skill_sources`) with its sha256. *(B2)*
|
|
147
|
+
- **`prompt.skill_inherit` MUST be absent** unless trusted mode. Same reason. *(B2)*
|
|
148
|
+
- `skills.scripts` (local shell hooks) MUST be empty. `--allow-local-scripts` is unsupported; script execution and prompt injection remain separate trust decisions.
|
|
149
|
+
- `beads_integration` is forced to `never` at runtime regardless of value.
|
|
150
|
+
- `capabilities.external_commands` MUST be empty (host commands unsupported).
|
|
151
|
+
- `prompt.task_template` MUST be present.
|
|
152
|
+
- All variables referenced by `$varname` in the chosen template MUST be in `variables` at call time → `template_variable_missing`.
|
|
153
|
+
|
|
154
|
+
These run as a compatibility check at request time and on `sp serve` startup (validates every spec in `--user-dir`). Failures return `specialist_load_error` with structured detail. **Boot is fail-closed**: any invalid spec in the authoritative user-dir prevents `/readyz` from going green. *(B1)*
|
|
155
|
+
|
|
156
|
+
### Why this preserves `sp edit`
|
|
157
|
+
|
|
158
|
+
`sp edit` works on raw JSON, drives validation through `parseSpecialist()`. With passthrough enabled, every field — including `class`, future class-only fields, and any unknown legacy keys — round-trips byte-for-byte. The user can swap models, change `thinking_level`, toggle `class`, and the file shape stays intact. Compatibility errors surface at validation time with a clear message, not silently via field drop.
|
|
159
|
+
|
|
160
|
+
## Pi spawn contract (production-grade)
|
|
161
|
+
|
|
162
|
+
Replaces the doc's offhand `pi --mode json --no-session --no-extensions --no-tools` line.
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
pi --mode json --no-session --no-extensions --no-tools \
|
|
166
|
+
--model <resolved-model> \
|
|
167
|
+
[--thinking <level>] \
|
|
168
|
+
[--skill <path>...] \
|
|
169
|
+
-- <rendered-prompt>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Pi reads provider credentials from `$HOME/.pi/agent/auth.json` (or `$PI_HOME/agent/auth.json` if set). The service never opens that file. This keeps key rotation, oauth refresh, and provider proliferation entirely inside pi — exactly where it already works.
|
|
173
|
+
|
|
174
|
+
Hardening rules:
|
|
175
|
+
|
|
176
|
+
- **`spawn` with arg array, never `shell: true`.** No template interpolation reaches a shell.
|
|
177
|
+
- **Credential handling delegated to pi.** Pi reads `~/.pi/agent/auth.json` itself; the service never reads, stores, copies, or logs API keys. `--model anthropic/claude-sonnet-4-5` is resolved by pi against the user's auth file at spawn time, supporting oauth, api_key, and any provider type pi already understands. The service surface drops every `--api-key-*` flag from the original spec — they are unnecessary and add an attack surface we don't need.
|
|
178
|
+
- **Wall-clock kill:** SIGTERM at `timeout_ms`, SIGKILL at `timeout_ms + 5s`. Distinguishes `timeout` (graceful) from `pi_crash` (hard kill).
|
|
179
|
+
- **Bounded I/O:** stdout cap (e.g. 4 MB) → `output_too_large`; stderr cap (last 4 KB) included in audit row.
|
|
180
|
+
- **Stdin closed after writing key + prompt.** No interactive surprises.
|
|
181
|
+
- **Process group isolation:** `detached: false`, but explicit `setsid` so SIGKILL sweeps grandchildren.
|
|
182
|
+
- **Exit-code mapping:** non-zero with no final `agent_end` JSON → `pi_crash`, distinct from `network`.
|
|
183
|
+
- **Concurrency:** semaphore at `--request-concurrency`; queue with bounded wait → `concurrency_limit` (HTTP 429), not `internal`.
|
|
184
|
+
- **Pre-flight:** rendered prompt size cap → `prompt_too_large` before spawn.
|
|
185
|
+
|
|
186
|
+
## Top 10 caveats the spec ignores
|
|
187
|
+
|
|
188
|
+
| # | Caveat | One-line fix |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| 1 | Concurrency / backpressure | Bounded queue with wait timeout; per-backend caps; HTTP 429/503. |
|
|
191
|
+
| 2 | Secrets handling | **Delegated to pi.** Bind-mount `~/.pi` (read-only). The service never reads keys; pi handles oauth refresh, multi-provider auth, rotation. Document log redaction for the `pi` subprocess stderr only. |
|
|
192
|
+
| 3 | Prompt injection via `$vars` | Single-pass renderer, no recursive expansion; record rendered-prompt SHA256 in audit. |
|
|
193
|
+
| 4 | `model_override` allowlist | Per-specialist allowlist; deny by default; log override source. |
|
|
194
|
+
| 5 | Output bounds | Per-spec and global token/char/JSON-size caps; explicit `output_truncated`. |
|
|
195
|
+
| 6 | Hot reload | Versioned cache, atomic swap, in-flight requests use snapshot, reload audit event. |
|
|
196
|
+
| 7 | Audit completeness | Add `resolved_model`, `fallback_used`, `breaker_state`, `validation_mode`, `prompt_sha`, `caller_id`, `override_source`. |
|
|
197
|
+
| 8 | Version pinning | Support `name@version` or `name#sha`; always log resolved version. |
|
|
198
|
+
| 9 | Multi-tenant isolation | Tenant auth, namespace, per-tenant secret scope, per-tenant quotas — not name prefixes. |
|
|
199
|
+
| 10 | Failure-mode taxonomy | Add the missing error_types (Bug 3); structured `error_detail` payload. |
|
|
200
|
+
|
|
201
|
+
## Migration story (single SSOT, no breakage)
|
|
202
|
+
|
|
203
|
+
1. **Phase A — additive:** add optional `specialist.class` (default `"agent"`); flip nested objects to `.passthrough()`.
|
|
204
|
+
2. **Phase B — validators:** add `validateForAgent` (current behavior) and `validateForScriptService`. Both run on top of the base parse.
|
|
205
|
+
3. **Phase C — tooling:** `specialists-creator` emits `class`; `sp edit` schema-paths know about `class`; `specialists validate --target service|agent`.
|
|
206
|
+
4. **Phase D — service launch:** `sp serve` validates user-dir against script-service rules at boot; HTTP requests revalidate per call.
|
|
207
|
+
5. **Phase E — hardening:** require explicit `class` for new files; legacy default stays for old files until migration is complete.
|
|
208
|
+
|
|
209
|
+
## 9. Packaging, distribution, and consumer-repo layout
|
|
210
|
+
|
|
211
|
+
The original spec describes how the service runs but not how it ships. A consumer repo without the specialists source code must be able to install, configure, validate, and operate the service from a published artifact alone. This section is the contract for that.
|
|
212
|
+
|
|
213
|
+
### 9.0 Image composition (read this before anything else)
|
|
214
|
+
|
|
215
|
+
The service is **TypeScript/Node** — same stack as the rest of this repo. The original spec is misleading on this point because §4 ships a Python reference client snippet; that snippet shows how a *Python consumer* calls the HTTP endpoint, not what runs inside the container.
|
|
216
|
+
|
|
217
|
+
| Layer | What | Why |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| Base image | `node:<lts>-slim` (or `gcr.io/distroless/nodejs<lts>` for prod) | Matches the runtime everything in this repo already targets. No Python interpreter, no `pip`, no extra language runtimes. |
|
|
220
|
+
| Compiled service | `dist/` from this repo, bundled with esbuild or shipped as the published npm package | Same code path as `sp serve` / `sp script` / `sp validate` on a host. |
|
|
221
|
+
| `pi` binary | Bundled into the image (also Node) | Pi is the model client; it spawns as a subprocess per request. |
|
|
222
|
+
| `sp` CLI | Bundled and on `$PATH` | Healthcheck shim, in-image `validate`, ad-hoc `script` invocations. |
|
|
223
|
+
| Process model | One Node process listening on `--port`, spawns one short-lived `pi` subprocess per request | No daemon, no worker pool inside the container. Concurrency comes from the request semaphore (§9.11). |
|
|
224
|
+
| User | Non-root UID (e.g. `10001`), declared in image label `org.specialists.uid` | Required for the `~/.pi` and `.specialists/user` bind mounts to work safely on shared hosts. |
|
|
225
|
+
| Filesystem | `read_only: true` root, `tmpfs:/tmp`, writes only under `/state` | Anything that wants to write outside `/state` is a bug. |
|
|
226
|
+
|
|
227
|
+
**Languages NOT in the image:** Python, Ruby, Go, shell beyond `sh`/`busybox`. If a consumer needs to call the service from Python, that Python lives in the consumer's own image; the HTTP boundary is the entire point of language decoupling.
|
|
228
|
+
|
|
229
|
+
**For Node consumers specifically:** skip the container entirely. Import `@<org>/specialists` and call `runScript()` directly (§10.3). Same spec format, same audit row, no subprocess except the `pi` it would have spawned anyway.
|
|
230
|
+
|
|
231
|
+
### 9.1 What the specialists repo publishes
|
|
232
|
+
|
|
233
|
+
| Artifact | Where | Content | Purpose |
|
|
234
|
+
|---|---|---|---|
|
|
235
|
+
| Container image | `ghcr.io/<org>/specialists-service:<version>` | Compiled JS, bundled `pi` binary, `sp` CLI, default config, no project files | Run the HTTP service |
|
|
236
|
+
| Slim image | `:<version>-slim` | Same minus optional dev tooling | Production deployments |
|
|
237
|
+
| Multi-arch | `linux/amd64`, `linux/arm64` | — | Apple Silicon, Graviton |
|
|
238
|
+
| npm package | `@<org>/specialists` (existing) | Source of truth, includes `sp serve` and `sp validate` | Local dev, CI authoring |
|
|
239
|
+
| Compose template | `docker-compose.example.yml` in the npm package and on the release page | Reference layout | Copy-paste starting point |
|
|
240
|
+
|
|
241
|
+
**Image must include** the `sp validate` and `sp serve` subcommands so the container itself is the authoring/validation tool — consumers do not need npm or the source repo.
|
|
242
|
+
|
|
243
|
+
**Image must not include** `.specialists/`, project secrets, or any consumer data. Mounts only.
|
|
244
|
+
|
|
245
|
+
### 9.2 Versioning and tag policy
|
|
246
|
+
|
|
247
|
+
| Tag | Mutable? | Use |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `:v1.2.3` | immutable | Production. Pin this. |
|
|
250
|
+
| `:v1.2` | floating to latest patch | Stage / dev where patch updates are safe |
|
|
251
|
+
| `:v1` | floating to latest minor | Not recommended for prod |
|
|
252
|
+
| `:latest` | floating | Never in production |
|
|
253
|
+
|
|
254
|
+
Image tags follow the npm package semver. Schema-breaking changes require a major bump. Each release publishes a `compat.json` to the GitHub release page documenting which `class` discriminator values and which schema fields the image understands — consumers diff this on upgrade.
|
|
255
|
+
|
|
256
|
+
### 9.3 Consumer repo layout (canonical)
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
my-project/
|
|
260
|
+
├── docker-compose.yml # references ghcr image, pinned tag
|
|
261
|
+
├── .specialists/
|
|
262
|
+
│ └── user/
|
|
263
|
+
│ ├── echo-summarizer.specialist.json
|
|
264
|
+
│ └── classify-intent.specialist.json
|
|
265
|
+
├── .specialists-service/ # gitignored runtime state
|
|
266
|
+
│ ├── observability.db # SQLite, mounted into container
|
|
267
|
+
│ └── pi-tokens/ # only if writable-token mode (§11.2); empty otherwise
|
|
268
|
+
├── .gitignore # ignores .specialists-service/
|
|
269
|
+
└── src/ # consumer's own code
|
|
270
|
+
└── client.ts # calls http://specialists:8000/v1/generate
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Rules:** *(B10)*
|
|
274
|
+
|
|
275
|
+
- `.specialists/user/` is the **authoritative authoring surface**. The service loads from this path only — it does *not* fall back to `.specialists/default/`, `config/specialists/`, or any legacy location. Loader runs in service-isolation mode (§11.1).
|
|
276
|
+
- `.specialists-service/` is **runtime state**, gitignored, written by the container.
|
|
277
|
+
- **No secret file**, no API key file, no `--api-key-*` flag. Pi reads `~/.pi/agent/auth.json` from the bind-mounted host directory; the service never opens credential files. See §11.2 for the two supported oauth-refresh modes.
|
|
278
|
+
- Consumer service talks to the specialists container over the docker network, never over a public address.
|
|
279
|
+
|
|
280
|
+
### 9.4 Reference docker-compose.yml
|
|
281
|
+
|
|
282
|
+
```yaml
|
|
283
|
+
version: "3.9"
|
|
284
|
+
|
|
285
|
+
services:
|
|
286
|
+
specialists:
|
|
287
|
+
image: ghcr.io/<org>/specialists-service:v1.2.3
|
|
288
|
+
restart: unless-stopped
|
|
289
|
+
command:
|
|
290
|
+
- sp
|
|
291
|
+
- serve
|
|
292
|
+
- --port=8000
|
|
293
|
+
- --user-dir=/work/.specialists/user
|
|
294
|
+
- --db-path=/state/observability.db
|
|
295
|
+
- --request-concurrency=4
|
|
296
|
+
environment:
|
|
297
|
+
# Pi resolves models and credentials from $HOME/.pi by default.
|
|
298
|
+
# Override here only if the host pi config lives elsewhere.
|
|
299
|
+
HOME: /pi-home
|
|
300
|
+
volumes:
|
|
301
|
+
- ./.specialists/user:/work/.specialists/user:ro
|
|
302
|
+
- ./.specialists-service:/state
|
|
303
|
+
# Pi credentials, models config, and skills — read-only mount.
|
|
304
|
+
# Container UID must be able to read the user's ~/.pi.
|
|
305
|
+
- ${HOME}/.pi:/pi-home/.pi:ro
|
|
306
|
+
healthcheck:
|
|
307
|
+
test: ["CMD", "sp", "health", "--exit-code"]
|
|
308
|
+
interval: 15s
|
|
309
|
+
timeout: 3s
|
|
310
|
+
retries: 5
|
|
311
|
+
start_period: 10s
|
|
312
|
+
deploy:
|
|
313
|
+
resources:
|
|
314
|
+
limits:
|
|
315
|
+
memory: 1g
|
|
316
|
+
cpus: "1.0"
|
|
317
|
+
read_only: true
|
|
318
|
+
tmpfs:
|
|
319
|
+
- /tmp
|
|
320
|
+
cap_drop:
|
|
321
|
+
- ALL
|
|
322
|
+
security_opt:
|
|
323
|
+
- no-new-privileges:true
|
|
324
|
+
networks:
|
|
325
|
+
- app
|
|
326
|
+
|
|
327
|
+
app:
|
|
328
|
+
build: ./src
|
|
329
|
+
environment:
|
|
330
|
+
SPECIALISTS_URL: http://specialists:8000
|
|
331
|
+
depends_on:
|
|
332
|
+
specialists:
|
|
333
|
+
condition: service_healthy
|
|
334
|
+
networks:
|
|
335
|
+
- app
|
|
336
|
+
|
|
337
|
+
networks:
|
|
338
|
+
app:
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
This is the **sidecar-per-service** topology — the supported v1 default.
|
|
342
|
+
|
|
343
|
+
**Why no `secrets:` block:** pi already owns credential management via `~/.pi/agent/auth.json` (multi-provider oauth + api_key, rotation, refresh). Bind-mounting that directory read-only is the entire credential story. The service binary never reads, copies, or logs keys. This eliminates an entire class of failure modes (key leak in audit rows, env-var inheritance, file-watch races) that the original spec spent paragraphs trying to mitigate.
|
|
344
|
+
|
|
345
|
+
### 9.5 Mount contract (what the image expects)
|
|
346
|
+
|
|
347
|
+
| Path inside container | Mode | Required? | Purpose |
|
|
348
|
+
|---|---|---|---|
|
|
349
|
+
| `/work/.specialists/user` | `ro` | yes | Specialist JSON files |
|
|
350
|
+
| `/state` | `rw` | yes | SQLite DB, file watcher state, debug sinks if enabled |
|
|
351
|
+
| `/pi-home/.pi` | `ro` | yes | Pi credentials, models config, skills — owned by pi, read by pi |
|
|
352
|
+
| `/work` | `ro` | optional | Project root for tooling that needs context |
|
|
353
|
+
|
|
354
|
+
The image fails fast at boot if `/work/.specialists/user` is missing or empty (`specialist_load_error: empty user dir`) or if `/pi-home/.pi/agent/auth.json` cannot be read (`pi_config_unreadable`). The image runs as a non-root UID; both the user-dir and the `~/.pi` mount must be readable by that UID — document the UID in the image label, and provide a `--user <uid>` recipe for compose users whose host UID differs.
|
|
355
|
+
|
|
356
|
+
**Two supported oauth modes (B5):**
|
|
357
|
+
|
|
358
|
+
- **Mode A — host-side refresh (default, recommended):** mount `~/.pi` read-only. Pi's in-container refresh-on-401 path is **disabled** by setting `PI_REFRESH_DISABLED=1` (or equivalent — exact env name is one of the implementation tasks in §12). Tokens are refreshed by the user's host-side pi tooling on a schedule or on demand; the container picks up new tokens on next file-mtime check. Acceptable when host has an active user or a scheduled refresh job. Failure mode: token expires mid-request → pi returns auth error → service maps to `auth` and returns. Caller retries; by then host has refreshed.
|
|
359
|
+
- **Mode B — container-writable token cache:** mount `~/.pi` read-only *for everything except* a writable token cache at `/state/pi-tokens/auth.json`. Service launches with `PI_AUTH_PATH=/state/pi-tokens/auth.json`, seeded once from `/pi-home/.pi/agent/auth.json` at boot. Pi refresh writes go to the writable copy only; the host file is never modified from inside the container. Required when the host has no active refresh agent (servers, CI runners, air-gapped hosts). Failure mode: concurrent refresh races between pi subprocesses → mitigated by file-lock around `auth.json` writes (pi already does this; verify on integration).
|
|
360
|
+
|
|
361
|
+
The image label `org.specialists.oauth-mode` documents which mode is active; mixing modes across replicas is unsupported. Choose at deploy time.
|
|
362
|
+
|
|
363
|
+
### 9.6 Configuration sources (precedence)
|
|
364
|
+
|
|
365
|
+
Highest to lowest:
|
|
366
|
+
|
|
367
|
+
1. CLI flags (`--port`, `--user-dir`, etc.) — explicit, audit-friendly.
|
|
368
|
+
2. Environment variables (`SP_PORT`, `SP_USER_DIR`) — for compose/k8s.
|
|
369
|
+
3. `/etc/specialists-service/config.yaml` — image default (mostly empty).
|
|
370
|
+
4. Built-in defaults.
|
|
371
|
+
|
|
372
|
+
**API keys are not a configuration concern of this service.** Pi reads `$HOME/.pi/agent/auth.json` for all provider credentials. The service inherits `HOME` (or `PI_HOME` if set), spawns pi as a subprocess, and that's the entire credential surface. There is no `--api-key-*` flag, no env-var fallback, no key file path. The image fails fast at boot if pi cannot read its own config.
|
|
373
|
+
|
|
374
|
+
### 9.7 Authoring without the specialists repo
|
|
375
|
+
|
|
376
|
+
The image ships with a `validate` subcommand that runs locally and never makes network calls:
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
docker run --rm \
|
|
380
|
+
-v "$PWD/.specialists/user:/work:ro" \
|
|
381
|
+
ghcr.io/<org>/specialists-service:v1.2.3 \
|
|
382
|
+
sp validate --target script /work/echo-summarizer.specialist.json
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`--target` selects the compatibility validator (`agent` or `script`). Exit code is 0 on pass, non-zero on schema or compatibility failure. CI hooks this into PR checks for any change under `.specialists/user/`.
|
|
386
|
+
|
|
387
|
+
### 9.8 Hot reload across bind mounts
|
|
388
|
+
|
|
389
|
+
The service watches `--user-dir` with a debounced inotify watcher. Constraints:
|
|
390
|
+
|
|
391
|
+
- **Linux bind mounts:** inotify works.
|
|
392
|
+
- **macOS Docker Desktop:** inotify across the VirtioFS bridge is best-effort; document `--reload-poll-ms 1000` as a fallback for dev.
|
|
393
|
+
- **Read-only mount:** watching is fine; only file mtimes are read.
|
|
394
|
+
|
|
395
|
+
**Atomic per-request snapshot (B7):** at request entry the handler resolves the spec by name and captures the **immutable parsed spec object** for the rest of the request. Reloads cannot mutate an in-flight request. Audit row records `meta.spec_sha256` so a later reload that drops or replaces the spec doesn't poison forensic replay.
|
|
396
|
+
|
|
397
|
+
**File event handling:** the watcher subscribes to `create`, `modify`, `delete`, and `move` events. The cache is name-keyed; every event updates the cache deterministically:
|
|
398
|
+
|
|
399
|
+
- `create` / `modify` / atomic-save (`tmp` → `rename`): re-parse, validate, atomic swap on success; previous version retained on parse/validation failure with an error event.
|
|
400
|
+
- `delete`: purge the cache entry. Subsequent requests for that name return `specialist_not_found` immediately. No ghost serving. Today's loader (`loader.ts:166-200`) only invalidates manually — the service must implement deletion-aware invalidation as part of B7.
|
|
401
|
+
- `move` / `rename` (in-dir): treated as delete-old-name + create-new-name. If the move overwrites another name, the destination cache entry is rebuilt.
|
|
402
|
+
|
|
403
|
+
Every cache mutation writes one `specialist_events` row (`spec_reloaded`, `spec_deleted`, `spec_load_failed`) with old/new sha256. If a reload produces an invalid spec, the previous version is kept and an error event is written; the request endpoint never serves a half-loaded spec.
|
|
404
|
+
|
|
405
|
+
**Editor atomic-save behavior:** vim/IntelliJ/VS Code typically write a temp file then rename. On Linux bind mounts inotify emits `CREATE` then `MOVED_TO` (rename target) — handler must collapse a `delete`+`create` pair within the debounce window into a single `modify` to avoid a brief window where `specialist_not_found` is returned for the same name.
|
|
406
|
+
|
|
407
|
+
### 9.9 Health, readiness, and observability scraping
|
|
408
|
+
|
|
409
|
+
| Endpoint | Returns | Use |
|
|
410
|
+
|---|---|---|
|
|
411
|
+
| `GET /healthz` | `200` if process alive | Liveness probe |
|
|
412
|
+
| `GET /readyz` | `200` if user-dir loaded **and all specs valid** (fail-closed), DB writable, pi config readable. `503` with reason if degraded | Readiness probe |
|
|
413
|
+
| `GET /metrics` | Prometheus text | Optional scrape target |
|
|
414
|
+
| `GET /v1/specialists` | List of loaded specs with version, class, sha256 | Caller introspection |
|
|
415
|
+
|
|
416
|
+
`sp health --exit-code` is the Docker healthcheck shim that hits `/readyz`.
|
|
417
|
+
|
|
418
|
+
**DB-write failure handling (B8):** the audit row is best-effort, never request-blocking. Sequence:
|
|
419
|
+
|
|
420
|
+
1. Model call returns successfully.
|
|
421
|
+
2. Service attempts `INSERT INTO specialist_jobs ...`.
|
|
422
|
+
3. On insert failure (disk full, lock timeout, schema mismatch): log structured error to stderr with `trace_id`, increment `db_write_failures_total` Prometheus counter, return the response to the caller with `meta.trace_persisted: false`.
|
|
423
|
+
4. If `db_write_failures_total` exceeds a configurable rate (default: 5 failures in 60s), `/readyz` flips to `503 degraded:audit`. Upstream LBs drain traffic until the rate drops; the service does not crash.
|
|
424
|
+
5. Configuration `--audit-failure-mode {best_effort|fail_request}` lets operators with strict audit requirements opt into hard-fail (returns `503 audit_unavailable` to the caller). Default is `best_effort`.
|
|
425
|
+
|
|
426
|
+
This contract is identical across HTTP, `sp script`, and programmatic transports. Caller-visible behavior is fully described by `meta.trace_persisted`.
|
|
427
|
+
|
|
428
|
+
### 9.10 Logging
|
|
429
|
+
|
|
430
|
+
- Structured JSON to stdout. One line per request with `trace_id`, `specialist`, `resolved_model`, `duration_ms`, `error_type`, `prompt_sha`, `output_sha`.
|
|
431
|
+
- Stderr only for service-level errors and reload diagnostics.
|
|
432
|
+
- No prompt or rendered output is logged by default. A `--log-prompts=trace_only` flag enables redacted prompt logging keyed by `trace_id` for forensic replay against `observability.db`.
|
|
433
|
+
- API keys are explicitly redacted; the redaction list is documented and tested.
|
|
434
|
+
|
|
435
|
+
### 9.11 Resource limits and concurrency
|
|
436
|
+
|
|
437
|
+
The image declares conservative defaults; consumers tune in compose:
|
|
438
|
+
|
|
439
|
+
| Knob | Default | Notes |
|
|
440
|
+
|---|---|---|
|
|
441
|
+
| `--request-concurrency` | 4 | Global semaphore. Excess requests wait `--queue-timeout-ms` then return `429 concurrency_limit`. |
|
|
442
|
+
| `--queue-timeout-ms` | 5000 | — |
|
|
443
|
+
| `--max-prompt-bytes` | 256 KB | Pre-flight cap → `prompt_too_large`. |
|
|
444
|
+
| `--max-output-bytes` | 4 MB | Post-call cap → `output_too_large`. |
|
|
445
|
+
| Memory limit | 1 GB | Tune for largest expected output × concurrency. |
|
|
446
|
+
| CPU limit | 1.0 | Pi process is the dominant CPU consumer; one in-flight request ≈ one core. |
|
|
447
|
+
|
|
448
|
+
### 9.12 Air-gapped and private registry
|
|
449
|
+
|
|
450
|
+
- Image must build deterministically from a public Dockerfile (no fetch-from-internet at runtime).
|
|
451
|
+
- `pi` and Node modules are bundled into the image — not downloaded on first run.
|
|
452
|
+
- For air-gapped sites: `docker save` / `docker load` works because the image is self-contained.
|
|
453
|
+
- For private registries: re-tag and push; nothing else changes.
|
|
454
|
+
|
|
455
|
+
### 9.13 Upgrade story and drain contract
|
|
456
|
+
|
|
457
|
+
1. Consumer reads release notes and `compat.json` for breaking changes.
|
|
458
|
+
2. Run `sp validate --target script` against current `.specialists/user/` using the new image — catches schema regressions before deployment.
|
|
459
|
+
3. Bump tag in `docker-compose.yml`.
|
|
460
|
+
4. `docker compose up -d` triggers rolling restart; `/readyz` gates traffic until the new container is healthy.
|
|
461
|
+
5. If something fails: revert tag, restart. Observability DB is forward-compatible across minor versions (additive columns only); a major upgrade may require an explicit migration step documented per release.
|
|
462
|
+
|
|
463
|
+
**Shutdown drain contract (B9):** orchestrators send `SIGTERM` then `SIGKILL` after a grace period. The service must handle both cleanly:
|
|
464
|
+
|
|
465
|
+
| Event | Behavior |
|
|
466
|
+
|---|---|
|
|
467
|
+
| `SIGTERM` received | (a) stop accepting new requests — listener returns `503 shutting_down` for any new socket; (b) drain in-flight requests up to `--shutdown-grace-ms` (default 30000); (c) flush pending audit writes; (d) close DB; (e) exit `0`. |
|
|
468
|
+
| In-flight pi subprocess at SIGTERM | Service forwards `SIGTERM` to each pi child; waits up to `min(shutdown-grace-ms, request-timeout-ms)` for graceful exit; then `SIGKILL` to children before parent exit. Process group via `setsid` ensures grandchildren are reaped. |
|
|
469
|
+
| `SIGKILL` (grace exceeded) | Kernel kills parent and process group. Audit rows for in-flight requests are lost (they were buffered in memory). Acceptable trade-off; clients must treat shutdown errors as retryable. |
|
|
470
|
+
| Existing connections during drain | Allowed to complete. New requests on existing keep-alive connections also get `503 shutting_down`. |
|
|
471
|
+
| Health endpoints during drain | `/healthz` continues to return `200` until the listener closes; `/readyz` returns `503 draining` immediately on SIGTERM so LBs steer away. |
|
|
472
|
+
|
|
473
|
+
This makes rolling restart safe under any orchestrator (compose, k8s, systemd) without orphan pi processes or broken-socket noise in client logs.
|
|
474
|
+
|
|
475
|
+
### 9.14 Multi-tenant deployments
|
|
476
|
+
|
|
477
|
+
Out of scope for v1 as a supported topology. Document explicitly:
|
|
478
|
+
|
|
479
|
+
- v1 is **single-tenant per container** (sidecar pattern).
|
|
480
|
+
- Running multiple consumers against one container is permitted but unsupported: there is no per-tenant authn, no per-tenant rate limit, no per-tenant secret scope.
|
|
481
|
+
- A v2 multi-tenant story (tenant header, namespaced user-dirs, per-tenant secret scopes, per-tenant quotas) is a separate design.
|
|
482
|
+
|
|
483
|
+
### 9.15 What this section guarantees
|
|
484
|
+
|
|
485
|
+
A consumer repo with **only** a `docker-compose.yml`, a `.specialists/user/` directory, and a bind-mount of `~/.pi` can: *(B10)*
|
|
486
|
+
|
|
487
|
+
- pull the published image
|
|
488
|
+
- validate every spec in CI without source code or npm
|
|
489
|
+
- run the service and call `/v1/generate` from its own code
|
|
490
|
+
- upgrade by bumping a tag and re-running validate
|
|
491
|
+
- audit every call via the SQLite DB it owns
|
|
492
|
+
- operate fully air-gapped after the initial image load
|
|
493
|
+
|
|
494
|
+
If any of those is not true after implementation, the distribution story is incomplete and ship is blocked.
|
|
495
|
+
|
|
496
|
+
## 10. Non-HTTP usage — `sp script`, programmatic, cron
|
|
497
|
+
|
|
498
|
+
The HTTP service is one transport. The same script-class spec must run identically without a daemon, without a container, and without any new credential plumbing — because pi already owns credentials on the host.
|
|
499
|
+
|
|
500
|
+
### 10.1 Architectural rule
|
|
501
|
+
|
|
502
|
+
**One spec format, one compatibility validator, three transports.**
|
|
503
|
+
|
|
504
|
+
| Transport | Use when | Surface |
|
|
505
|
+
|---|---|---|
|
|
506
|
+
| HTTP (`sp serve`) | Multi-process consumers, language-agnostic clients, container deployments | `POST /v1/generate` |
|
|
507
|
+
| CLI (`sp script`) | Shell scripts, cron, single-host automation | `sp script <name> --vars k=v --json` |
|
|
508
|
+
| Programmatic | In-process Node services, custom embedding | `import { runScript } from '@org/specialists'` |
|
|
509
|
+
|
|
510
|
+
All three load specs through the same `SpecialistLoader` (in service-isolation mode for the service surface, see §11.1), run the same `validateForScriptService` compatibility check, spawn pi with the same one-shot flags, and — **when tracing is enabled** — write the same SQLite audit row schema. A spec that works in one works in all three. The CLI's `--no-trace` and the HTTP `trace: false` request flag are the only opt-outs from audit writes; neither changes the spec contract or the spawn behavior. Anything that adds a runtime knob to one transport but not the others reintroduces the schema-fork problem at a different layer.
|
|
511
|
+
|
|
512
|
+
### 10.2 `sp script` CLI
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
sp script <name>
|
|
516
|
+
--vars key=value [--vars key=value ...]
|
|
517
|
+
[--template task_template|normalize_template]
|
|
518
|
+
[--model <override>]
|
|
519
|
+
[--thinking <level>]
|
|
520
|
+
[--user-dir <path>]
|
|
521
|
+
[--db-path <path>]
|
|
522
|
+
[--timeout-ms <n>]
|
|
523
|
+
[--json] # emit response as JSON to stdout (default: just the output text)
|
|
524
|
+
[--single-instance <lockpath>] # cron-safe: flock guard, skip if locked
|
|
525
|
+
[--log-file <path>]
|
|
526
|
+
[--no-trace] # skip the SQLite write
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Exit codes (cron-friendly):
|
|
530
|
+
|
|
531
|
+
| Code | Meaning |
|
|
532
|
+
|---|---|
|
|
533
|
+
| 0 | success |
|
|
534
|
+
| 1 | internal error |
|
|
535
|
+
| 2 | spec/load error (`specialist_not_found`, `specialist_load_error`) |
|
|
536
|
+
| 3 | template error (`template_variable_missing`) |
|
|
537
|
+
| 4 | auth/quota (`auth`, `quota`) |
|
|
538
|
+
| 5 | timeout/network (`timeout`, `network`, `circuit_open`) |
|
|
539
|
+
| 6 | validation (`invalid_json`, `output_validation_failed`) |
|
|
540
|
+
| 7 | output too large / prompt too large |
|
|
541
|
+
|
|
542
|
+
Stdout is the model output (or full JSON with `--json`). Stderr is structured diagnostic logging. This makes shell composition trivial:
|
|
543
|
+
|
|
544
|
+
```sh
|
|
545
|
+
summary=$(sp script summarize-log --vars content="$(cat /var/log/app.log)") || exit $?
|
|
546
|
+
echo "$summary" | mail -s "Daily summary" ops@example.com
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### 10.3 Programmatic usage
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
import { runScript } from '@org/specialists';
|
|
553
|
+
|
|
554
|
+
const result = await runScript({
|
|
555
|
+
name: 'classify-intent',
|
|
556
|
+
variables: { text: incoming.body },
|
|
557
|
+
userDir: '.specialists/user',
|
|
558
|
+
dbPath: '.specialists-service/observability.db',
|
|
559
|
+
timeoutMs: 60_000,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
if (!result.success) throw new Error(`${result.error_type}: ${result.error}`);
|
|
563
|
+
return result.parsed_json;
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
`runScript` is the same function the HTTP handler calls. No HTTP round-trip, no JSON serialization overhead, same audit row in SQLite.
|
|
567
|
+
|
|
568
|
+
### 10.4 Cron caveats — what changes when there is no container
|
|
569
|
+
|
|
570
|
+
Pi's credential ownership eliminates most of the original cron concerns. What's left:
|
|
571
|
+
|
|
572
|
+
| Caveat | Why it still matters | Resolution |
|
|
573
|
+
|---|---|---|
|
|
574
|
+
| State ownership | Where does `observability.db` live? | Default to `~/.specialists/observability.db`. Override with `--db-path` or `SP_DB_PATH`. Document the precedence: flag → env → home default. |
|
|
575
|
+
| Concurrency | Cron can overlap if a run exceeds the interval; two runs racing on the same DB row writes is unsafe. | `--single-instance <lockpath>` uses `flock`. If locked, exit code 75 (`EX_TEMPFAIL`) so cron logs the skip without alerting. |
|
|
576
|
+
| cwd / project resolution | The loader scans `cwd/.specialists/user`. Cron's cwd is minimal. | `cd` into the project root in the cron command, or pass `--user-dir` explicitly. |
|
|
577
|
+
| Log rotation | Host scripts don't have a log scraper. | `--log-file` plus document a logrotate snippet. Default: stderr only. |
|
|
578
|
+
| HOME not set | Cron sometimes runs without `HOME`. Pi can't find auth.json. | The CLI fails fast with `pi_config_unreadable` and a message naming `HOME`/`PI_HOME`. Cron user must export `HOME` in the crontab. |
|
|
579
|
+
|
|
580
|
+
What is **not** a caveat: API keys, secret rotation, env-var leakage, key file permissions. None of those exist as service concerns because pi owns them.
|
|
581
|
+
|
|
582
|
+
### 10.5 Example: cron job using a script-class specialist
|
|
583
|
+
|
|
584
|
+
`crontab -e`:
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
HOME=/home/dawid
|
|
588
|
+
PATH=/usr/local/bin:/usr/bin:/bin
|
|
589
|
+
|
|
590
|
+
# Every 15 minutes, classify recent error logs and post severe ones to Slack
|
|
591
|
+
*/15 * * * * cd /home/dawid/dev/myproject && \
|
|
592
|
+
sp script classify-error-batch \
|
|
593
|
+
--vars window_minutes=15 \
|
|
594
|
+
--single-instance /tmp/sp-classify.lock \
|
|
595
|
+
--json \
|
|
596
|
+
| jq -r '.parsed_json.severe_events[]?' \
|
|
597
|
+
| xargs -I{} curl -s -X POST $SLACK_WEBHOOK -d "text={}" \
|
|
598
|
+
>> /var/log/sp-classify.log 2>&1
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
No secret config. No HTTP. The same `classify-error-batch.specialist.json` could run identically in the HTTP service and in `runScript()` from a Node app — because pi handles the model and the spec handles the prompt.
|
|
602
|
+
|
|
603
|
+
### 10.6 When NOT to use `sp script`
|
|
604
|
+
|
|
605
|
+
If you need full agent runtime — tools, worktrees, beads, multi-turn keep-alive, mandatory rules, GitNexus injection — that is `sp run`, not `sp script`. The two commands deliberately have different shapes:
|
|
606
|
+
|
|
607
|
+
| `sp script` | `sp run` |
|
|
608
|
+
|---|---|
|
|
609
|
+
| Script-class specs only | Agent-class specs (default) |
|
|
610
|
+
| One-shot, no tools, no worktree | Full Supervisor lifecycle |
|
|
611
|
+
| Synchronous, returns when done | Background-capable, steer/resume |
|
|
612
|
+
| No mandatory rules / memory / GitNexus injection | Full project context injection |
|
|
613
|
+
| SQLite row + optional file log | Full job dir, events, status, result |
|
|
614
|
+
|
|
615
|
+
Picking the wrong one is the same mistake the original HTTP spec made by trying to host both shapes on one transport. Two surfaces, two compatibility validators, one schema.
|
|
616
|
+
|
|
617
|
+
## 11. Unsupported topologies, isolation rules, and supply-chain requirements
|
|
618
|
+
|
|
619
|
+
### 11.1 Service-isolation loader mode (B1)
|
|
620
|
+
|
|
621
|
+
The current `SpecialistLoader.getScanDirs()` (`loader.ts:73-82`) walks seven candidate paths: user dir, nested-user, default dir, nested-default, `config/specialists`, and two legacy locations. For interactive `sp run` this fallback chain is correct and useful. **For the service surface it is dangerous**: a spec name typo or a deleted user-dir file silently falls through to a package-bundled spec, and the operator believes they have an isolated sidecar while the container serves something else.
|
|
622
|
+
|
|
623
|
+
**Required behavior (must implement before image ships):**
|
|
624
|
+
|
|
625
|
+
- New loader option `mode: 'service' | 'agent'`. Service mode scans **exactly one root** (the configured `--user-dir`). No `default/`, no `config/specialists/`, no legacy dirs.
|
|
626
|
+
- Boot scan validates every file under the root with `validateForScriptService`. Any failure is fatal: `/readyz` stays `503` until fixed. No silent skip-and-stderr.
|
|
627
|
+
- Cache is name-keyed and **fail-closed**: a delete event purges the entry; subsequent requests for that name return `specialist_not_found`, never a fallback from another dir.
|
|
628
|
+
- The service logs the resolved root path on every startup and includes it in `/v1/specialists` introspection.
|
|
629
|
+
|
|
630
|
+
This is what makes the §9.3 "authoring surface" claim true. Without it, the claim is fiction.
|
|
631
|
+
|
|
632
|
+
### 11.2 OAuth refresh modes — see §9.5 (B5)
|
|
633
|
+
|
|
634
|
+
Mode A (read-only mount + host refresh) and Mode B (writable token cache at `/state/pi-tokens`) are the two supported configurations. Mixing modes across replicas of the same logical service is **unsupported** and will cause refresh races.
|
|
635
|
+
|
|
636
|
+
### 11.3 Skill-injection trust model (B2)
|
|
637
|
+
|
|
638
|
+
`skills.paths` and `prompt.skill_inherit` reach pi via `--skill <path>` and modify the agent's instruction context. In service mode they are **denied by default**. Two opt-in paths:
|
|
639
|
+
|
|
640
|
+
- `--allow-skills` — single-tenant trusted deployments only. Every resolved skill path is logged in `meta.skill_sources` with sha256.
|
|
641
|
+
- `--allow-skills-roots <path>[:<path>...]` — restrict permitted skill paths to a list of trusted roots. The loader rejects skills resolving outside those roots at validation time.
|
|
642
|
+
|
|
643
|
+
Multi-tenant deployments must NEVER enable `--allow-skills` without the path-roots restriction; without it, any spec author can prompt-inject from any path the service UID can read.
|
|
644
|
+
|
|
645
|
+
### 11.4 Unsupported topologies
|
|
646
|
+
|
|
647
|
+
| Topology | Status | Why |
|
|
648
|
+
|---|---|---|
|
|
649
|
+
| Multi-replica sharing one `/state` (SQLite + token cache) | **Unsupported** | SQLite write contention, watcher fan-out duplication, oauth refresh races on shared `auth.json`. Run one replica per `/state` mount. |
|
|
650
|
+
| Multi-tenant on a single shared container | **Unsupported in v1** | No tenant authn, no per-tenant rate limit, no per-tenant secret scope. Sidecar-per-service is the supported pattern. |
|
|
651
|
+
| Mixed oauth modes across replicas | **Unsupported** | Refresh races. Pick Mode A or B per logical service. |
|
|
652
|
+
| `~/.pi` mounted from NFS / remote FS | **Unsupported** | inotify semantics, lock semantics, and atomic-write guarantees vary across remote filesystems. Local bind mount only. |
|
|
653
|
+
| Public exposure of the HTTP endpoint | **Unsupported** | No authn, no authz, no rate-limit-per-caller. Container-network access only. |
|
|
654
|
+
| Container running as root | **Unsupported** | Image declares non-root UID; running as root defeats the bind-mount permission model. |
|
|
655
|
+
|
|
656
|
+
These are documented to fail predictably, not to be supported.
|
|
657
|
+
|
|
658
|
+
### 11.5 Supply-chain requirements
|
|
659
|
+
|
|
660
|
+
Image must ship with verifiable provenance:
|
|
661
|
+
|
|
662
|
+
- **SBOM** generated at build (CycloneDX or SPDX); attached to the GitHub release and to the image as an OCI annotation.
|
|
663
|
+
- **Image signing** with cosign (sigstore); verification recipe in release notes. Consumers `cosign verify ghcr.io/<org>/specialists-service:v1.2.3 --certificate-identity ...`.
|
|
664
|
+
- **Base image refresh policy:** monthly rebuild on the slim/distroless base to pick up CVE patches. Patch releases for CVE rebuilds keep the same code, bump the patch version, document under "security only" in `compat.json`.
|
|
665
|
+
- **Pi binary provenance:** if `pi` is bundled, it must come from a pinned npm version recorded in the SBOM. A separate cosign signature on the bundled pi binary is preferred.
|
|
666
|
+
- **Dependency scan in CI:** `npm audit --omit=dev` and Trivy/Grype against the built image must pass before publish.
|
|
667
|
+
- **Reproducible build:** a documented build recipe that produces byte-identical images from the same inputs (or, at minimum, the same SBOM hash). Required for air-gapped consumers who need to verify what they deployed.
|
|
668
|
+
|
|
669
|
+
Without these, "production-ready" is a marketing claim, not an operational one.
|
|
670
|
+
|
|
671
|
+
## 12. Required implementation surface (B4)
|
|
672
|
+
|
|
673
|
+
The memo references several commands and exports that **do not exist in the codebase today**. They are blockers for the image build, not aspirations. Documented here so the implementation phase has an explicit checklist.
|
|
674
|
+
|
|
675
|
+
| Surface | Today | Required |
|
|
676
|
+
|---|---|---|
|
|
677
|
+
| `sp serve` | absent (`src/index.ts:958-967`) | HTTP server entry point: `--port`, `--user-dir`, `--db-path`, `--request-concurrency`, `--queue-timeout-ms`, `--max-prompt-bytes`, `--max-output-bytes`, `--shutdown-grace-ms`, `--audit-failure-mode`, `--allow-skills`, `--allow-skills-roots`, `--reload-poll-ms`. |
|
|
678
|
+
| `sp script` | absent | One-shot CLI: `--vars`, `--template`, `--model`, `--thinking`, `--user-dir`, `--db-path`, `--timeout-ms`, `--json`, `--single-instance`, `--log-file`, `--no-trace`. Exit codes per §10.2. |
|
|
679
|
+
| `sp validate --target script\|agent` | partial (`src/cli/validate.ts` validates schema, no `--target`) | Add `--target` flag that runs `validateForScriptService` or `validateForAgent` on top of base parse. Image-callable for CI. |
|
|
680
|
+
| `sp health --exit-code` | absent | Healthcheck shim hitting `/readyz`, exit 0/1. |
|
|
681
|
+
| `runScript()` library export | absent | New entry in `package.json#exports` returning `Promise<GenerateResponse>`. Same code path as HTTP handler. |
|
|
682
|
+
| `runScriptStream()` (optional v1) | absent | Streaming variant if needed; defer until consumer demand is real. |
|
|
683
|
+
| `class` field in `SpecialistSchema` | absent (`schema.ts`) | Add `specialist.class: 'agent' | 'script'` (default `'agent'`). |
|
|
684
|
+
| `.passthrough()` on nested objects | absent — verified bug | Required for `sp edit` round-trip and class-discriminator forward compat. |
|
|
685
|
+
| `validateForAgent` / `validateForScriptService` | absent | Two compatibility validators on top of base parse. |
|
|
686
|
+
| Service-isolation loader mode | absent | `SpecialistLoader({ mode: 'service', root: <path> })`. |
|
|
687
|
+
| Deletion-aware cache invalidation | absent (`loader.ts:166-200` is name-keyed manual-only) | Watcher → cache purge on delete/rename. |
|
|
688
|
+
| `template_variable_missing` enforcement | absent (`templateEngine.ts`) | Pre-scan rendered template for un-substituted `$ident`; raise typed error. |
|
|
689
|
+
| Output validation `fail_request` mode | partial (engine exists at `runner.ts:689-789`, only warns) | New spec field `output_validation: 'warn' | 'fail_request'`; map failures to `output_validation_failed`. |
|
|
690
|
+
| Audit row extensions | partial | Add `resolved_model`, `fallback_used`, `breaker_state`, `validation_mode`, `prompt_sha`, `output_sha`, `attempts`, `skill_sources`, `trace_persisted`, `caller_id`. |
|
|
691
|
+
| Image build pipeline | absent | Multi-arch buildx, SBOM gen, cosign sign, Trivy/Grype scan, npm audit, GHCR push on tag. |
|
|
692
|
+
| `compat.json` per release | absent | Document supported `class` values, schema fields, and migration notes per release. |
|
|
693
|
+
|
|
694
|
+
This list is the implementation epic. Until each row is `done`, the memo's promises are forward-looking, not present-tense.
|
|
695
|
+
|
|
696
|
+
## Final answer to the question that started this
|
|
697
|
+
|
|
698
|
+
> "Why are we dropping functionalities entirely?"
|
|
699
|
+
|
|
700
|
+
We are not. Three of the doc's "non-goals" (strict validation, backend-only swap, future classes) **must not be dropped**. Five (multi-stage orchestration, keep-alive, worktree ownership, file-based job lifecycle, beads ownership) **are correctly bounded out of the service**. All eight "forbidden fields" are **the wrong mechanism** — they should become per-surface compatibility rules on real schema field names. The schema must remain a single SSOT with a `class` discriminator and passthrough objects, or every claim of cross-project reusability is false.
|
|
701
|
+
|
|
702
|
+
Anything that ships under the current draft will fork the schema, break `sp edit` round-trips, hide model swaps from callers, lose forensic data, and silently accept malformed output. None of those are acceptable for production.
|
|
703
|
+
|
|
704
|
+
## Ship verdict
|
|
705
|
+
|
|
706
|
+
**BLOCK** until every row in §12 ("Required implementation surface") is implemented and every blocker B1–B11 is resolved by the patches in this memo. Once those land:
|
|
707
|
+
|
|
708
|
+
1. Re-run the second-pass critique against the patched memo to confirm no contradictions remain.
|
|
709
|
+
2. Build the first multi-arch image with full supply-chain attestations (§11.5).
|
|
710
|
+
3. Validate end-to-end with one real consumer (HTTP), one real cron user (`sp script`), and one real Node embedder (`runScript()`).
|
|
711
|
+
4. Only after all three pass, mark the memo as the canonical spec and rewrite `docs/specialists-service.md` from it.
|
|
712
|
+
|
|
713
|
+
Until then this is a design contract, not a release plan.
|