@arnilo/prism 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/README.md +18 -16
- package/dist/agent-run-lifecycle.d.ts +2 -1
- package/dist/agent-run-lifecycle.js +1 -1
- package/dist/agent-session/session/assemble.js +9 -7
- package/dist/agent-session/session/tool-round.js +30 -20
- package/dist/agent-session/session/types.d.ts +1 -0
- package/dist/agent-session/session.d.ts +1 -0
- package/dist/agent-session/session.js +3 -2
- package/dist/checkpoint-restore.d.ts +50 -14
- package/dist/checkpoint-restore.js +104 -28
- package/dist/contracts-core/session.d.ts +2 -1
- package/dist/contracts-run-state.d.ts +12 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/leases.js +32 -6
- package/dist/node/contribution-discovery.d.ts +16 -1
- package/dist/node/contribution-discovery.js +47 -0
- package/dist/node/session-store-jsonl.js +67 -17
- package/dist/run-limits.d.ts +11 -5
- package/dist/session-stores.js +61 -12
- package/dist/testing/prefix-stability-conformance.d.ts +44 -1
- package/dist/testing/prefix-stability-conformance.js +92 -29
- package/dist/usage-estimation.d.ts +7 -1
- package/dist/usage-estimation.js +16 -10
- package/docs/acp.md +2 -2
- package/docs/agent-events.md +7 -6
- package/docs/agent-session-runtime.md +1 -1
- package/docs/coding-agent-tools.md +1 -1
- package/docs/coding-tools.md +7 -11
- package/docs/context-and-skills.md +6 -7
- package/docs/contribution-discovery.md +13 -0
- package/docs/durable-runs.md +10 -3
- package/docs/embeddings.md +3 -1
- package/docs/execution-timeline.md +6 -0
- package/docs/extensions.md +1 -2
- package/docs/impeccable.md +1 -2
- package/docs/index.md +24 -20
- package/docs/live-testing.md +1 -2
- package/docs/memory-fabric.md +3 -2
- package/docs/migrate-to-0.11.md +65 -0
- package/docs/migration.md +12 -1
- package/docs/node-jsonl-session-store.md +4 -3
- package/docs/operations.md +1 -1
- package/docs/peer-dependencies.md +3 -5
- package/docs/policy-and-audit.md +1 -1
- package/docs/prefix-stability-conformance.md +30 -7
- package/docs/provider-packages.md +20 -20
- package/docs/public-contracts.md +1 -1
- package/docs/rag.md +2 -2
- package/docs/release-and-install.md +57 -57
- package/docs/runs-and-usage.md +6 -4
- package/docs/session-stores.md +2 -2
- package/docs/supervisors.md +14 -6
- package/docs/testing.md +17 -9
- package/docs/workflows.md +2 -2
- package/package.json +5 -4
- package/docs/caveman.md +0 -130
- package/docs/graft.md +0 -149
- package/docs/ponytail.md +0 -129
|
@@ -5,29 +5,29 @@
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
Prism's current **0.
|
|
8
|
+
Prism's current **0.11.0** line has **12 publishable manifests**: the root `@arnilo/prism` core package plus **11 workspace packages** — **20 provider adapters** (20 provider adapter subpaths inside the `@arnilo/prism-providers` family), 4 `prism-*` family packages, and 7 capability packages. (Generated by `node scripts/package-truth.mjs` → `scripts/package-truth.json` — the manifest-derived single source for counts, provider membership, umbrella closures, and profile closures.) The last lockstep cut was 0.3.0; Decision B now publishes changed packages independently inside `^0.3.0` — the plan 039 changed-package cut moved root `@arnilo/prism` and every plan-035+ changed package to **0.3.1**, and the plan 050 changed-package cut moved root plus four changed packages to **0.3.2**; the plan 041-044 changed-package cut moves root to **0.3.3** with `@arnilo/prism-memory@0.3.2` (composite recall scoring), `@arnilo/prism-evals@0.3.1` (trace-to-dataset curation), the three session-store packages at **0.3.1** (run-ledger `promptVersion` provenance), and the initial `@arnilo/prism-prompts@0.0.1` (independent opt-in, outside `prism-all`); plan 054 consolidation then folded `@arnilo/prism-browser` and `@arnilo/prism-obscura` into the `@arnilo/prism-web-tools` family as `/browser` and `/obscura` subpaths, folded `@arnilo/prism-rag`, both compaction strategies, `@arnilo/prism-graft`, and `@arnilo/prism-wiki` into the `@arnilo/prism-memory` family as `/rag`, `/compaction/llm`, `/compaction/observational-memory`, `/graft`, and `/wiki` subpaths (deleting the `@arnilo/prism-compaction` profile), and folded all 17 `@arnilo/prism-provider-*` packages into the `@arnilo/prism-providers` family as `/<adapter>` subpaths (Azure/Bedrock/Vertex stop being special all-only manifests); independent publication continues inside `^0.3.0` ranges (which satisfy 0.3.1, 0.3.2, and 0.3.3). This page describes how they are packed, what each tarball contains, how to install them, the required non-optional **caret** `@arnilo/prism@^0.11.0` peer range, the release workflow, and the offline test budget. The measurable 1.0 readiness gates (command-per-gate) live in [`0.1.0-readiness.md`](history/./0.1.0-readiness.md).
|
|
9
9
|
|
|
10
10
|
Core `@arnilo/prism` ships runtime, CLI, templates, and docs. Every code package has a required `@arnilo/prism` peer inside the Decision B window — the caret current spec is `@arnilo/prism@^0.3.3` and every declared window peer satisfies it: packages republishing in the plan 050 cut carry `^0.3.2`; the plan 039 set keeps `^0.3.1`; unchanged packages keep their `^0.3.0` peer; profiles are pure manifests. The plan 050 republished set declares the required `@arnilo/prism@^0.3.2` peer; the plan 041-044 republished set keeps its existing `^0.3.0` window peer; unchanged packages keep their prior window. Installation activates no provider, listener, database, browser, credential, or tool capability.
|
|
11
11
|
|
|
12
|
-
The **0.
|
|
12
|
+
The **0.11.0 lockstep cut** moved all **twelve** manifests together. The current declared peer is `@arnilo/prism@^0.11.0` on every package, and `release.mjs` lockstep mode fails closed on any internal range that merely satisfies the cut version instead of matching it. The **0.6.0–0.10.0 lockstep cuts** each moved their then-current manifest set together (`@arnilo/prism-hooks` arrived in the 0.10.0 cut). The independent-publication history above (0.3.x, 0.4.x, 0.5.x) describes how the line grew when packages moved separately.
|
|
13
13
|
|
|
14
14
|
<!-- generated:package-truth:inventory begin -->
|
|
15
15
|
**12 publishable manifests** — root `@arnilo/prism` plus 11 workspace packages (4 `prism-*` family packages, 7 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
|
|
16
16
|
|
|
17
17
|
| package | version | notes |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
| `@arnilo/prism` | 0.
|
|
20
|
-
| `@arnilo/prism-channels` | 0.
|
|
21
|
-
| `@arnilo/prism-coding-tools` | 0.
|
|
22
|
-
| `@arnilo/prism-core` | 0.
|
|
23
|
-
| `@arnilo/prism-providers` | 0.
|
|
24
|
-
| `@arnilo/prism-acp-agent` | 0.
|
|
25
|
-
| `@arnilo/prism-ag-ui` | 0.
|
|
26
|
-
| `@arnilo/prism-hooks` | 0.
|
|
27
|
-
| `@arnilo/prism-mcp` | 0.
|
|
28
|
-
| `@arnilo/prism-memory` | 0.
|
|
29
|
-
| `@arnilo/prism-web-tools` | 0.
|
|
30
|
-
| `@arnilo/prism-work` | 0.
|
|
19
|
+
| `@arnilo/prism` | 0.11.0 | core — runtime, CLI/RPC, templates, docs |
|
|
20
|
+
| `@arnilo/prism-channels` | 0.11.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
|
|
21
|
+
| `@arnilo/prism-coding-tools` | 0.11.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /impeccable subpaths |
|
|
22
|
+
| `@arnilo/prism-core` | 0.11.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
|
|
23
|
+
| `@arnilo/prism-providers` | 0.11.0 | family — all provider adapters as `/<adapter>` subpaths |
|
|
24
|
+
| `@arnilo/prism-acp-agent` | 0.11.0 | capability — ACP adapter |
|
|
25
|
+
| `@arnilo/prism-ag-ui` | 0.11.0 | capability — AG-UI/A2A/A2UI adapter |
|
|
26
|
+
| `@arnilo/prism-hooks` | 0.11.0 | capability — Claude/Codex-compatible hooks.json adapter compiled onto middleware, guardrail, injector, and stop-hook seams |
|
|
27
|
+
| `@arnilo/prism-mcp` | 0.11.0 | capability — MCP client/server/OAuth interop |
|
|
28
|
+
| `@arnilo/prism-memory` | 0.11.0 | capability — memory plus /rag, /compaction/*, /fabric, /wiki subpaths |
|
|
29
|
+
| `@arnilo/prism-web-tools` | 0.11.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
|
|
30
|
+
| `@arnilo/prism-work` | 0.11.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
|
|
31
31
|
<!-- generated:package-truth:inventory end -->
|
|
32
32
|
|
|
33
33
|
|
|
@@ -36,30 +36,30 @@ The **0.10.0 lockstep cut** moved all **twelve** manifests together — `@arnilo
|
|
|
36
36
|
|
|
37
37
|
| adapter package | version |
|
|
38
38
|
| --- | --- |
|
|
39
|
-
| `@arnilo/prism-providers/ai-sdk` | 0.
|
|
40
|
-
| `@arnilo/prism-providers/alibaba` | 0.
|
|
41
|
-
| `@arnilo/prism-providers/anthropic` | 0.
|
|
42
|
-
| `@arnilo/prism-providers/azure` | 0.
|
|
43
|
-
| `@arnilo/prism-providers/bedrock` | 0.
|
|
44
|
-
| `@arnilo/prism-providers/clinepass` | 0.
|
|
45
|
-
| `@arnilo/prism-providers/commandcode` | 0.
|
|
46
|
-
| `@arnilo/prism-providers/deepseek` | 0.
|
|
47
|
-
| `@arnilo/prism-providers/google` | 0.
|
|
48
|
-
| `@arnilo/prism-providers/hyper` | 0.
|
|
49
|
-
| `@arnilo/prism-providers/kimi` | 0.
|
|
50
|
-
| `@arnilo/prism-providers/model-discovery` | 0.
|
|
51
|
-
| `@arnilo/prism-providers/neuralwatt` | 0.
|
|
52
|
-
| `@arnilo/prism-providers/ollama` | 0.
|
|
53
|
-
| `@arnilo/prism-providers/openai` | 0.
|
|
54
|
-
| `@arnilo/prism-providers/opencode-go` | 0.
|
|
55
|
-
| `@arnilo/prism-providers/openrouter` | 0.
|
|
56
|
-
| `@arnilo/prism-providers/vertex` | 0.
|
|
57
|
-
| `@arnilo/prism-providers/xai` | 0.
|
|
58
|
-
| `@arnilo/prism-providers/zai` | 0.
|
|
39
|
+
| `@arnilo/prism-providers/ai-sdk` | 0.11.0 |
|
|
40
|
+
| `@arnilo/prism-providers/alibaba` | 0.11.0 |
|
|
41
|
+
| `@arnilo/prism-providers/anthropic` | 0.11.0 |
|
|
42
|
+
| `@arnilo/prism-providers/azure` | 0.11.0 |
|
|
43
|
+
| `@arnilo/prism-providers/bedrock` | 0.11.0 |
|
|
44
|
+
| `@arnilo/prism-providers/clinepass` | 0.11.0 |
|
|
45
|
+
| `@arnilo/prism-providers/commandcode` | 0.11.0 |
|
|
46
|
+
| `@arnilo/prism-providers/deepseek` | 0.11.0 |
|
|
47
|
+
| `@arnilo/prism-providers/google` | 0.11.0 |
|
|
48
|
+
| `@arnilo/prism-providers/hyper` | 0.11.0 |
|
|
49
|
+
| `@arnilo/prism-providers/kimi` | 0.11.0 |
|
|
50
|
+
| `@arnilo/prism-providers/model-discovery` | 0.11.0 |
|
|
51
|
+
| `@arnilo/prism-providers/neuralwatt` | 0.11.0 |
|
|
52
|
+
| `@arnilo/prism-providers/ollama` | 0.11.0 |
|
|
53
|
+
| `@arnilo/prism-providers/openai` | 0.11.0 |
|
|
54
|
+
| `@arnilo/prism-providers/opencode-go` | 0.11.0 |
|
|
55
|
+
| `@arnilo/prism-providers/openrouter` | 0.11.0 |
|
|
56
|
+
| `@arnilo/prism-providers/vertex` | 0.11.0 |
|
|
57
|
+
| `@arnilo/prism-providers/xai` | 0.11.0 |
|
|
58
|
+
| `@arnilo/prism-providers/zai` | 0.11.0 |
|
|
59
59
|
<!-- generated:package-truth:providers end -->
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
Core ships `dist`, docs, templates, and `CHANGELOG.md`; code packages ship compiled output, README, license, and changelog. Family/profile packages ship manifest, README, and changelog. `@arnilo/prism-providers` is the unified provider family: all provider adapters ship as `dist/<adapter>` subpaths in one tarball (Azure/Bedrock/Vertex included), with the required `@arnilo/prism` peer as the only dependency and `@ai-sdk/provider` an optional peer of `/ai-sdk`. `@arnilo/prism-core` provides the unified runtime, sessions, governance, credentials, and enterprise persistence family package. `@arnilo/prism-channels` provides the transport-neutral messaging runtime, durable journal and pairing surface; it has only the required `@arnilo/prism` peer, while its SQLite/PostgreSQL conformance drivers are development-only. `@arnilo/prism-web-tools` provides the unified web tools family: root Brave/Exa/Firecrawl research tools plus `/browser` (Playwright-peer gated) and `/obscura` (host-binary + MCP gated) subpaths. `@arnilo/prism-memory` provides the unified memory and context family: root working/vector memory plus `/rag` (with `/rag/loaders` and `/rag/parsers`), `/compaction/llm`, `/compaction/observational-memory`,
|
|
62
|
+
Core ships `dist`, docs, templates, and `CHANGELOG.md`; code packages ship compiled output, README, license, and changelog. Family/profile packages ship manifest, README, and changelog. `@arnilo/prism-providers` is the unified provider family: all provider adapters ship as `dist/<adapter>` subpaths in one tarball (Azure/Bedrock/Vertex included), with the required `@arnilo/prism` peer as the only dependency and `@ai-sdk/provider` an optional peer of `/ai-sdk`. `@arnilo/prism-core` provides the unified runtime, sessions, governance, credentials, and enterprise persistence family package. `@arnilo/prism-channels` provides the transport-neutral messaging runtime, durable journal and pairing surface; it has only the required `@arnilo/prism` peer, while its SQLite/PostgreSQL conformance drivers are development-only. `@arnilo/prism-web-tools` provides the unified web tools family: root Brave/Exa/Firecrawl research tools plus `/browser` (Playwright-peer gated) and `/obscura` (host-binary + MCP gated) subpaths. `@arnilo/prism-memory` provides the unified memory and context family: root working/vector memory plus `/rag` (with `/rag/loaders` and `/rag/parsers`), `/compaction/llm`, `/compaction/observational-memory`, and `/wiki` subpaths, including the `prism-wiki` bin and bundled skills. `@arnilo/prism-coding-tools/dev` ships the loopback dev inspector — the `prism-dev` bin, the `prism dev` CLI composition, and the `/dev/cli` export the core CLI delegates to for `prism dev` (plan 040 Tasks 4–5); dev tooling is developer-time only and must never be the production API boundary. `@arnilo/prism-core/governance/prompts` (plan 042) is the versioned prompt registry: an explicit host opt-in with no first-party package depending on it — unlike `@arnilo/prism-memory` (a family member) and `@arnilo/prism-core/governance/evals` (used by the promotion helper as an optional peer). `@arnilo/prism-work` is the unified work family: `/connectors`, `/documents`, `/sheets`, `/diagrams`, `/document-reader`, `/sandbox`, `/skills`, and `/tools` subpaths in one tarball. Office dependencies are exact-pinned; `/document-reader` keeps `mammoth` and `pdf-parse` optional and fail-closed, while `playwright-core` remains a devDependency for gated draw.io testing. Importing `/connectors` never evaluates document dependencies.
|
|
63
63
|
|
|
64
64
|
## When to use it
|
|
65
65
|
|
|
@@ -91,7 +91,6 @@ Consumers install the core package for the runtime and add first-party packages
|
|
|
91
91
|
| Install Obscura browser-engine tools (host supplies the binary; `/obscura`) | `npm install @arnilo/prism @arnilo/prism-web-tools @arnilo/prism-mcp` |
|
|
92
92
|
| Install RAG retrieval (memory family `/rag`) | `npm install @arnilo/prism @arnilo/prism-memory` |
|
|
93
93
|
| Install the Wiki CLI and skills (memory family `/wiki`) | `npm install @arnilo/prism @arnilo/prism-memory` (`npx prism-wiki --help`) |
|
|
94
|
-
| Install the Graft context-graph bridge (`/graft`, host supplies the CLI) | `npm install @arnilo/prism @arnilo/prism-memory` (+ host-installed `@nanonets/graft`) |
|
|
95
94
|
| Install work connectors | `npm install @arnilo/prism @arnilo/prism-work` (import `@arnilo/prism-work/connectors`) |
|
|
96
95
|
| Install document/spreadsheet/presentation engine | `npm install @arnilo/prism @arnilo/prism-work` (import `@arnilo/prism-work/documents`) |
|
|
97
96
|
| Install spreadsheet and CSV data engine | `npm install @arnilo/prism @arnilo/prism-work` (import `@arnilo/prism-work/sheets`) |
|
|
@@ -153,13 +152,13 @@ A packed tarball contains only public compiled output and release files:
|
|
|
153
152
|
- Code packages ship `README.md`, `LICENSE`, and `CHANGELOG.md`; family/profile packages ship `README.md` and `CHANGELOG.md`.
|
|
154
153
|
- The core tarball additionally ships the full `docs/` directory (the docs hub), `templates/init/`, and the `templates/` gallery (e.g. `deep-research`) used by `prism init`.
|
|
155
154
|
- `dist/cli.js` and the `bin` link in core.
|
|
156
|
-
- **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.
|
|
155
|
+
- **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.11.0.tgz`; family packages produce `arnilo-prism-core-0.11.0.tgz`, `arnilo-prism-coding-tools-0.11.0.tgz`, `arnilo-prism-providers-0.11.0.tgz` (all 20 adapters inside), `arnilo-prism-channels-0.11.0.tgz`, `arnilo-prism-memory-0.11.0.tgz`, and `arnilo-prism-web-tools-0.11.0.tgz`; capability packages like `arnilo-prism-hooks-0.11.0.tgz`, `arnilo-prism-mcp-0.11.0.tgz`, and `arnilo-prism-work-0.11.0.tgz` carry their own package version. Independent-package tags carry their own version. The CLI bin name `prism` is unaffected by the package name (`npx prism` still works; npm allows the bin field to differ from the package name).
|
|
157
156
|
|
|
158
157
|
Excluded from every tarball by `files` negation:
|
|
159
158
|
|
|
160
159
|
- `dist/__tests__/` — compiled tests and the meta-tests (`packaging.test.js`, `install-smoke.test.js`, `docs.test.js`, `network-free-guard.test.js`, and the phase boundary tests).
|
|
161
160
|
- `dist/**/*.map` — source maps. Source maps are still emitted locally (`tsconfig` `sourceMap: true`) for debugging; the `!dist/**/*.map` line is the **map-retention knob**: remove that negation to ship source maps in releases.
|
|
162
|
-
- `src/`, `plans/`, `.agents/`, `.github/`, `tsconfig*.json`, `roadmap.md`, and `
|
|
161
|
+
- `src/`, `plans/`, `.agents/`, `.github/`, `tsconfig*.json`, `roadmap.md`, and `bun.lock` are never packed (outside the `files` whitelist and/or explicitly ignored).
|
|
163
162
|
|
|
164
163
|
`sideEffects` is `false` for every first-party package (their entrypoints export only types and declarations). Core sets `sideEffects: ["dist/cli.js"]` because `src/cli.ts` runs the CLI and sets `process.exitCode` at import time; every other core entrypoint is side-effect-free.
|
|
165
164
|
|
|
@@ -254,13 +253,13 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
|
|
|
254
253
|
|
|
255
254
|
| Runtime | Supported | Measured in CI |
|
|
256
255
|
| --- | --- | --- |
|
|
257
|
-
| Node | 22, 24 (`engines.node >=22`) | `verify` runs the full `sdk:ready` gate on Node 24; `node22-compat` builds and imports every public root `exports` target on Node 22. Node 20 support was dropped in 0.6.0 (`dev-006`; Node 20 reached upstream end-of-life 2026-04-30); 0.
|
|
256
|
+
| Node | 22, 24 (`engines.node >=22`) | `verify` runs the full `sdk:ready` gate on Node 24; `node22-compat` builds and imports every public root `exports` target on Node 22. Node 20 support was dropped in 0.6.0 (`dev-006`; Node 20 reached upstream end-of-life 2026-04-30); 0.11.0 keeps the same floor. |
|
|
258
257
|
| PostgreSQL | 16 (`pgvector/pgvector:pg16`) | `postgres-integration` service container |
|
|
259
258
|
|
|
260
259
|
## Extension and configuration notes
|
|
261
260
|
|
|
262
261
|
|
|
263
|
-
- **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional **caret** `@arnilo/prism@^0.
|
|
262
|
+
- **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional **caret** `@arnilo/prism@^0.11.0` peer (the lockstep 0.11.0 cut rewrote every internal range; the version-literal gate rejects a declared range that only satisfies the cut version) (`peerDependenciesMeta` must not mark `@arnilo/prism` optional; other peers such as `playwright-core` may be optional). **Peer-version policy (plan 030, Decision B — independent packages):** internal ranges stay inside the caret window of the cut they shipped in, so a package may patch independently while consumers remain on a compatible 0.x line. A package outside that window is refused by the release gate until the next coordinated peer bump. Inside the workspace each package also declares `"@arnilo/prism": "file:../.."` in `devDependencies` so `npm install` resolves the peer locally; that devDependency is stripped from consumer installs and is not a runtime dependency.
|
|
264
263
|
- **Public access.** All 56 manifests (root + 55 workspace packages: 49 code packages + 6 pure-manifest family/profile packages — the 10 `prism-*` family/profile set is the 6 pure-manifest profiles plus the 4 code packages `prism-caveman`, `prism-impeccable`, `prism-openapi-tools`, `prism-ponytail`) declare `"publishConfig": { "access": "public" }`; the publisher also passes `--access public` explicitly because scoped packages otherwise default to restricted on first publish.
|
|
265
264
|
- **Shipped vs repository docs.** The npm tarball ships `docs/` pages linked from `docs/index.md` (public API, security, migration, providers, install). It excludes `docs/_evidence/` (per-phase evidence freezes, including `release-0.2.7-evidence.md`), `docs/release-*-evidence.md`, and `docs/api-page-template.md`. Those files remain in git for audit. `dist/__tests__` and `*.map` stay excluded.
|
|
266
265
|
- **Map retention knob.** Source maps are emitted locally but stripped from tarballs by `!dist/**/*.map`. Removing that `files` negation ships maps in releases (larger tarballs, better consumer stack traces).
|
|
@@ -286,7 +285,7 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
|
|
|
286
285
|
|
|
287
286
|
|
|
288
287
|
- **Export-count budget.** `scripts/budget-gate.test.mjs` counts each publishable package's public exports (same name classes as `scripts/dead-exports.mjs`) and fails CI when any exceed the `exportCounts` ceilings in `scripts/budgets.json`; the failure names the package and the exact delta. The 0.10.0 cut carries `@arnilo/prism` 1456 and `@arnilo/prism-memory` 934 (the 0.9.0 pre-release baselines were 1445 and 892, plan 099 Task 0 measured 2026-09-19; the +11 root raise is logged for plans 103, 104, and 106 and the +42 memory raise for plans 102 and 105, per-entry in `scripts/budgets.json#exportCounts`), and every other ceiling is unchanged since its recorded rebaseline. Each raise carries its measured value and the plans that caused it, and `docs/_evidence/phase54-package-map.md` records the same per-package count in its Budget-Gated Exports column. Growth requires removing exports or rebaselining with a recorded reason.
|
|
289
|
-
- **Artifact diet.** The same gate re-packs the root tarball and fails if packed bytes, unpacked bytes, or file count exceed `scripts/budgets.json#root` + 5%; the 0.9.0 pre-release baselines
|
|
288
|
+
- **Artifact diet.** The same gate re-packs the root tarball and fails if packed bytes, unpacked bytes, or file count exceed `scripts/budgets.json#root` + 5%; the 0.9.0 pre-release baselines were 1414295 packed / 4647338 unpacked / 533 files (measured 2026-09-19, plan 099 Task 0). The 0.11.0 cut raised packed bytes to 1486492 (measured 2026-09-24); unpacked stayed inside that band and file count measured 534. Tests, fixtures, plans, scripts, `src/`, and `docs/_evidence/**` stay out of the pack (plan 026 rule), and every page linked from shipped `docs/index.md` must be present.
|
|
290
289
|
- **No secrets or fixtures in tarballs.** Tests, fixtures, `src/`, `plans/`, `.agents/`, `roadmap.md`, and `tsconfig` files are excluded. The `docs avoid real-looking secret examples` docs check and the packaging guard's deny list prevent secret-bearing fixtures from shipping.
|
|
291
290
|
- **Live tests stay opt-in.** The default `npm test` is network-free by construction and never sets these vars. Provider/compaction live gates stay credential-gated and are not set by default or during `sdk:ready`. The PostgreSQL adapter live matrix is the exception that runs in CI via the dedicated `postgres-integration` job (still skipped in the default suite).
|
|
292
291
|
- `PRISM_LIVE_PROVIDER_TESTS=1` — gates the eight provider packages' `src/__tests__/live.test.ts` (`@arnilo/prism-providers/anthropic`, `provider-google`, `provider-openai`, `provider-opencode-go`, `provider-openrouter`, `provider-zai`, `provider-kimi`, `provider-neuralwatt`). Each provider live test also requires its own API key env var and skips safely when it is missing:
|
|
@@ -309,13 +308,13 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
|
|
|
309
308
|
- `PRISM_TEST_KEYCHAIN=1` — gates `@arnilo/prism-core/credentials/node` system-keychain round-trips (requires a working OS keychain backend; skipped by default).
|
|
310
309
|
- Provider live tests read the API key from the env only when both gates are set; the key is used as a bearer token and never logged. `assertNoSecretLeak` verifies the key value does not appear in any streamed event. The compaction placeholders still carry no real credentials.
|
|
311
310
|
- Enforced by `network-free-guard.test.ts` (default suite stays network-free) and by source-scanning meta-tests that assert each `live.test.ts` keeps its `skip:` guard.
|
|
312
|
-
- **Supply-chain workflows.** `.github/workflows/security.yml` runs CodeQL JavaScript/TypeScript SAST, PR-only dependency review, `
|
|
311
|
+
- **Supply-chain workflows.** `.github/workflows/security.yml` runs CodeQL JavaScript/TypeScript SAST, PR-only dependency review, `bun audit --audit-level=moderate`, SPDX 2.3 generation, exact license allow/deny policy, tracked-source plus unpacked-tarball credential-pattern scans, and seven-day SBOM retention. Dependabot opens bounded weekly npm and GitHub Actions updates. Every third-party action uses a full immutable revision; workflows never use `pull_request_target`. GitHub repository secret scanning/push protection and required-check branch rules remain repository settings because GitHub provides no equivalent checked-in workflow toggle; enable `security / codeql`, `security / supply-chain`, PR dependency review, and release checks on protected branches.
|
|
313
312
|
- **Sandbox/browser protected workflow.** `.github/workflows/sandbox-browser.yml` is scheduled/manual only in protected `sandbox-browser` environment. It runs network-free adversarial eval fixtures by default, optionally enables digest-pinned Docker and Playwright gates via repository variables (`PRISM_TEST_DOCKER_IMAGE`, `PRISM_ENABLE_PLAYWRIGHT_GATE`), plus Obscura (`PRISM_ENABLE_OBSCURA_GATE`, `PRISM_OBSCURA_BIN`) and draw.io (`PRISM_ENABLE_DRAWIO_GATE`, `PRISM_TEST_DRAWIO_URL`) legs whose suites live in `@arnilo/prism-web-tools` and `@arnilo/prism-work` — the draw.io leg runs through `scripts/live-matrix.mjs` with `PRISM_LIVE_FILTER=work/drawio-live`; it receives no provider/npm/OIDC secrets, and uploads only a redacted aggregate status artifact (7-day retention).
|
|
314
313
|
- **Release attestations.** Tag publication uses GitHub OIDC with only `contents: read`, `id-token: write`, and `attestations: write` at the publish job. `actions/attest-build-provenance` attests every `.tgz` and `sbom.spdx.json` before npm publication; npm still receives `--provenance`. Verify downloaded attestations with GitHub CLI and npm signatures on the release host.
|
|
315
|
-
- **Install smoke is offline.** The install-smoke test packs core + every package into a temp dir and installs tarballs with `--offline --no-audit --no-fund` into a fresh project. External dependencies are satisfied from the lockfile-backed npm cache prepared by `
|
|
314
|
+
- **Install smoke is offline.** The install-smoke test packs core + every package into a temp dir and installs tarballs with `--offline --no-audit --no-fund` into a fresh project. External dependencies are satisfied from the lockfile-backed npm cache prepared by `bun ci`; any attempted uncached registry fetch fails the gate.
|
|
316
315
|
- **Packed-install e2e journeys (plan 012 Task 3).** `scripts/e2e-enterprise-journey.test.mjs` and `scripts/e2e-coding-journey.test.mjs` pack the first-party packages for their journey, install the exact tarballs into a fresh consumer project, and run the journey script inside that consumer — public exports only, no workspace-relative resolution (asserted per run). The **enterprise journey** composes OIDC identity → OPA policy decision (durable ledger) → agent run with durable events (memory, or real PostgreSQL when `PRISM_TEST_POSTGRES_URL` is set **and the `pg` peer is installed in that consumer**) → batched approval → OpenAPI side effect with idempotency → artifact upload + signed delivery, with policy-deny and hash-mismatch fail-closed injections. The durable leg is peer-gated and never assumed from the env alone: with `PRISM_TEST_POSTGRES_URL` ambient but `pg` unresolvable (the default, since `pg` is a peer of `@arnilo/prism-core` and the consumer installs only Prism tarballs), the fixture prints `SKIP durable postgres leg: …` and runs the memory event source instead of dying with `ERR_MODULE_NOT_FOUND`; the test reports that line as a TAP diagnostic and asserts it, so the skip can never be silent. Scope the env to the phase that needs it (`PRISM_TEST_POSTGRES_URL=… npm run test:postgres`, or `phase release:gate` in `.github/workflows/release.yml`) rather than exporting it globally. The **coding journey** composes an ACP editor session (init capability negotiation, session new + load/resume) → bounded coding tools (git-aware list/search, glob, read-before-write write, delete, move) → sandboxed process session → forge handoff with idempotent PR creation, with execution-policy and read-before-write denial paths. Each fixture asserts the installed version matches the packed manifest graph and stays within the frozen `e2eJourneyFixtureMsCeiling` (120 s in `scripts/phase12-freeze-manifest.json`).
|
|
317
316
|
- **Protected restart-recovery leg (plan 012 Task 4).** `scripts/phase12-restart-recovery.test.mjs` (run by `npm run test:postgres` after the Phase 7 suite) spawns two real processes against one PostgreSQL schema: replica A runs a durable agent, suspends on a batched tool approval, appends durable events and is then SIGKILLed by the driver; replica B reconnects and resumes. Operators re-run the leg with `PRISM_TEST_POSTGRES_URL="postgresql://…" npm run test:postgres` against a disposable PostgreSQL 16 (e.g. `pgvector/pgvector:pg16`). Without the URL the gate records a named `BLOCKED GATE` failure instead of skipping. Reconnect p95 and 16-worker append contention p95 are asserted against the frozen `reconnectP95Ms` / `pointOpP95Ms` ceilings; set `PRISM_PHASE12_RECORD_EVIDENCE=1` to refresh the checked-in evidence file `scripts/phase12-restart-recovery.json`.
|
|
318
|
-
- **Offline test budget.** The default `npm test` (no `PRISM_LIVE_PROVIDER_TESTS`) is pinned at **<
|
|
317
|
+
- **Offline test budget.** The default `npm test` (no `PRISM_LIVE_PROVIDER_TESTS`) is pinned at **< 110s** with a measured local baseline of **~92s** (one chain sum ~92s on this host, including the Node branch-coverage audit at ~20s; the pre-audit baseline was ~72s in `docs/_evidence/phase115-suite-budget.md`). The gate stage's former 33 s critical path is split across `scripts/phase54-legacy-registry-{dry-run,apply,fail-closed}.test.mjs` so worker files overlap, and the workspace stage runs one `npm` process per package two at a time, each taking `scripts/with-build-lock.mjs --shared` (readers overlap; `tsc` keeps the exclusive lock). Every stage except the SQLite suites runs on `node --test`; the SQLite suites use `bun test --timeout=0` (the only file set the Bun 1.4.2 inventory measured `bun-ok` and faster than Node), while `npm run test:coverage` measures with `bun test --coverage` (plan 114 moved the instrument to Bun) and the PostgreSQL TAP leg stays on Node. Plan 057 retired the historical `phase11-freeze` … `phase34-freeze`/`phase30-release` gate files from the default suite (17 files, 247 tests) — they stay in the repo as immutable release evidence and remain audit-runnable standalone via `node --test scripts/<file>.test.mjs`, with their self-wiring assertions flipped to assert non-wiring so the retirement cannot silently regress. Their criteria are content-verified, not filesystem-verified (plan 071 Tasks 5 and 10): capture ordering compares the baselines' recorded `captured` dates instead of file mtimes, phase markers resolve against the live file plus the frozen lineage (the phase plan of record and `docs/history/`) so a living doc that is rewritten between releases cannot fail an audit gate (a marker set that is checked this way is declared in the gate's own manifest — `phase26-freeze-manifest.json` `lineageCheckedFiles` — so the fallback stays bounded to the files that need it), a path retired since the freeze resolves to its recorded archive instead of being hash-compared (`status: "retired"` + `retiredTo` in the baseline, with the Task 0 `sha256` kept as history), and a version literal is asserted as a *transition* (`!`-prefixed marker: the pre-cut literal must be gone) rather than an equality that the next release sweep would invalidate. Release/security gates (`release-gate`, `tooling-gate`, `budget-gate`, `phase23-quality-gates`, `phase8–11` conformance) stay in the run. The full CI `sdk:ready` gate runs on Node 24 because docs tests execute 38 `examples/*.ts` demos via native TypeScript stripping; `scripts/examples-execution.test.mjs` spawns the rest or records a manifest skip. `npm run sdk:ready` also runs typecheck, pack dry-run, and the coverage summary, so it is allowed to exceed the `npm test` budget while remaining network-free. `npm run test:coverage` additionally runs the combined coverage summary (`npm run coverage:summary`, measured ~66s local on 16 cores: core + each workspace suite once under `bun test --coverage`; the whole `test:coverage` stage is **~137s**, measured 136.8/137.1 s after plan 115 Task 6, up from ~128s under the Node instrument because the core suite is ~1.8× slower under Bun). The stage runs the core suite once and hands that run to the summary: `test:coverage` captures the first `bun test --coverage` run's output and exit code, and `coverage-summary.mjs` parses that capture for the core row instead of spawning a second ~35 s run (the standalone `coverage-summary` and a non-zero exit code still measure, so the row is never stale), which recovered ~35 s of the ~170s pre-task stage; only the `phase23-coverage` fail-closed gate rerun measures the suite again. The core gate and the per-package lines floors are the hard thresholds. The CI `sdk:ready` step has `timeout-minutes: 30` as a hang backstop; the separate Node 22 compatibility job has `timeout-minutes: 10`. The budget was raised from 30s after the default suite grew to include every first-party package, offline install smoke, packaging guards, docs examples, and workspace tests, from 60s to 80s after the plan 115 Task 2 trims (the critical-path gate file split three ways and the shared-lock concurrent workspace stage) landed the suite at 71.7/73.9 s, then to 110s after plan 120 Task 6 added the Node branch-coverage audit.
|
|
319
318
|
|
|
320
319
|
### GitHub Actions pipeline (0.0.27+)
|
|
321
320
|
|
|
@@ -325,27 +324,27 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
|
|
|
325
324
|
## Formatting, linting, and coverage
|
|
326
325
|
|
|
327
326
|
|
|
328
|
-
Prism uses one tool for formatting and linting — [Biome](https://biomejs.dev) — configured once at the repo root (`biome.json`) and inherited by every workspace. Coverage uses
|
|
327
|
+
Prism uses one tool for formatting and linting — [Biome](https://biomejs.dev) — configured once at the repo root (`biome.json`) and inherited by every workspace. Coverage uses Bun's built-in test coverage (`bun test --coverage`); there is no third-party coverage service. `npm test` also runs a Node branch-coverage audit (`scripts/branch-coverage-audit.mjs`, `--test-coverage-include=dist/**`, floor **83.49** in `scripts/branch-coverage.test.mjs`); it does not replace the Bun gate, which still records `branches: null`.
|
|
329
328
|
|
|
330
329
|
| Command | What it does |
|
|
331
330
|
| --- | --- |
|
|
332
331
|
| `npm run lint` | `biome lint .` — fails on any lint error (warnings are non-fatal). |
|
|
333
332
|
| `npm run format:check` | `biome format .` — fails if any file is unformatted. |
|
|
334
333
|
| `npm run format` | `biome format --write .` — normalizes formatting in place. |
|
|
335
|
-
| `npm run test:coverage` | `
|
|
334
|
+
| `npm run test:coverage` | `bun test --coverage --timeout=0` over the core suite with the Bun-measured core floors **lines 91.48%**, **functions 92.21%** (measured 94.48 / 95.21 − 3pp; the branch floor is dropped because Bun 1.4.2 emits no branch data), then `scripts/coverage-summary.mjs` + the `phase23-coverage` gate. The first run's output and exit code are captured and handed to the summary (`PRISM_COVERAGE_CORE_*`), so the core row is parsed from the run the contributor already saw — one measurement per stage; the standalone entry point and any non-zero captured exit still measure for themselves. Scoping is `bunfig.toml` `coveragePathIgnorePatterns`: the root file for the core row, one package-local file per gated workspace. |
|
|
336
335
|
|
|
337
|
-
All four gates run inside `npm run sdk:ready` (after `typecheck`, before `pack:dry-run`). A few rules are disabled in `biome.json` because they are false positives for this codebase: `noControlCharactersInRegex` and `noAssignInExpressions` (security/redaction code intentionally matches control characters and uses `while ((m = re.exec(…)))` loops), `noShadowRestrictedNames`, `noThenProperty` (the workflow DSL has a legitimate `then` branch field), `noExplicitAny`, `noVoidTypeReturn`, and `useYield`. Raise the
|
|
336
|
+
All four gates run inside `npm run sdk:ready` (after `typecheck`, before `pack:dry-run`). A few rules are disabled in `biome.json` because they are false positives for this codebase: `noControlCharactersInRegex` and `noAssignInExpressions` (security/redaction code intentionally matches control characters and uses `while ((m = re.exec(…)))` loops), `noShadowRestrictedNames`, `noThenProperty` (the workflow DSL has a legitimate `then` branch field), `noExplicitAny`, `noVoidTypeReturn`, and `useYield`. Raise coverage floors only by re-measuring: the core floors live in `scripts/coverage-thresholds.json` `core` and the per-package rows in the same file; `package.json` carries no numeric threshold.
|
|
338
337
|
|
|
339
338
|
### Coverage denominators and per-package thresholds
|
|
340
339
|
|
|
341
340
|
|
|
342
|
-
Workspace coverage rows used to include the symlinked root core `dist/` (workspace tests `import … from "@arnilo/prism"`, which resolves via `node_modules/@arnilo/prism -> ../..`), diluting every package denominator. Each workspace
|
|
341
|
+
Workspace coverage rows used to include the symlinked root core `dist/` (workspace tests `import … from "@arnilo/prism"`, which resolves via `node_modules/@arnilo/prism -> ../..`), diluting every package denominator. Each workspace row now runs `bun test --coverage` from its own package directory with a package-local `bunfig.toml` (`[test] coveragePathIgnorePatterns = ["../**"]`), so only `packages/<name>/dist/**` counts — Bun 1.4.2 reads bunfig only from the current directory, so the root file cannot scope a workspace row (evidence: `docs/_evidence/phase114-bun-coverage.md` §1.6).
|
|
343
342
|
|
|
344
343
|
| Fact | Value |
|
|
345
344
|
| --- | --- |
|
|
346
|
-
| Workspace
|
|
345
|
+
| Workspace filter | package-local `bunfig.toml` `[test] coveragePathIgnorePatterns = ["../**"]` per package (the equivalent of the old `--test-coverage-include=dist/**`) |
|
|
347
346
|
| Workspace discovery | any `*.test.js` under `packages/<name>/dist/**`, nested layouts included (`@arnilo/prism-acp-agent` builds to `dist/src/__tests__`, `@arnilo/prism-work` to `dist/<area>/__tests__`) — all 11 workspace packages are measured and artifact keys must match their live manifest names |
|
|
348
|
-
| Per-package gate | `lines >= threshold` from `scripts/coverage-thresholds.json` (recaptured 2026-09-
|
|
347
|
+
| Per-package gate | `lines >= threshold` from `scripts/coverage-thresholds.json` (recaptured 2026-09-23 under `bun test --coverage` = min of two back-to-back runs − 3pp; the two runs differed by ≤ 0.05pp); functions recorded, not gated, and branches recorded as `null` because Bun 1.4.2 emits no branch data. `phase23-coverage` also fails when a row names a package that is not in the live workspace graph — retired rows must be pruned |
|
|
349
348
|
| Protected exceptions | `@arnilo/prism-core` only (durable postgres/NATS legs need `PRISM_TEST_POSTGRES_URL` / `PRISM_TEST_NATS_URL`); exempt from the gate and reported separately with the reason. Env- or capability-gated legs elsewhere (memory postgres, coding-tools native sandbox, provider live legs) skip as protected skips but their packages stay gated on the measured run |
|
|
350
349
|
| Artifact | `scripts/coverage-summary.json` (gitignored, CI-retained): its package-key set must exactly equal live workspace `package.json` names; each row carries `lines`/`branches`/`functions`/`denominatorFiles`/`threshold`/`pass`/`protectedException` + `belowThreshold`; a row whose child failed additionally carries `status`/`exitCode`/`tail` (a redacted tail of the child's output) |
|
|
351
350
|
| Fail-closed | a non-protected package below its threshold, a suite failure, or a run producing no coverage data exits non-zero; a missing threshold entry is a config error |
|
|
@@ -397,7 +396,7 @@ Timing assertions in tests follow a deterministic-barrier policy: racy wall-cloc
|
|
|
397
396
|
| Timeout | 120s default; override with `PRISM_BUILD_LOCK_TIMEOUT_MS` |
|
|
398
397
|
| Retry | 100ms backoff; stale-PID reclaim via `process.kill(pid, 0)` |
|
|
399
398
|
| Fail-closed | acquisition error or timeout exits non-zero, nothing runs |
|
|
400
|
-
| Wrapped | `build:core`, every workspace `build`, the `node --test` runs in `test
|
|
399
|
+
| Wrapped | `build:core`, every workspace `build`, the `node --test` runs in `test`/workspace tests, the `bun test --coverage` run in `test:coverage`, `coverage-summary.mjs`, the script-gate `node --test` run (the `phase*-conformance`/`phase*-security` gates import `@arnilo/prism` from `dist`) |
|
|
401
400
|
| Not wrapped | `npm run clean` (standalone), `tsc -p examples --noEmit` and workspace `typecheck` (read `dist` `.d.ts`; within any single script the build completes before reads, so only a concurrent external emitter can cause a spurious typecheck error), `scripts/phase23-build-race.test.mjs` (the lock's own regression — it runs unwrapped so its children acquire the real lock) |
|
|
402
401
|
|
|
403
402
|
Directly invoking `tsc` instead of `npm run build` bypasses the lock — use the npm scripts when another build/test could be running in the same tree (CI runs them sequentially).
|
|
@@ -436,14 +435,14 @@ Development type packages track the **declared** engines floor, not the machine
|
|
|
436
435
|
| Public surface import smoke (all 21 root `exports` default targets) | 20.20.2 | ✅ all import cleanly. |
|
|
437
436
|
| Full core test suite | 20.20.2 | 1311/1312 — the single failure is `examples_demos_run_to_completion_and_emit_no_secret`, which executes `examples/*.ts` via Node's native TypeScript stripping (Node 22.6+). This is a test-harness capability, not an SDK runtime incompatibility, and is exactly why CI scopes Node 20 to build + import smoke. |
|
|
438
437
|
|
|
439
|
-
**CI enforcement** (`.github/workflows/release.yml`): the `verify` job runs `npm run sdk:ready` on Node 24; `node22-compat` runs `
|
|
438
|
+
**CI enforcement** (`.github/workflows/release.yml`): the `verify` job runs `npm run sdk:ready` on Node 24; `node22-compat` runs `bun ci`, `npm run build`, and the public-import smoke on Node 22; `supply-chain` runs `bun audit --audit-level=moderate`, SPDX/license checks, SBOM, and source-secret scans; `publish` `needs:` all of `verify`, `node22-compat`, `postgres-integration`, `codeql-release`, and `supply-chain`, so nothing publishes unless every leg — including the audit/SBOM gates — passes.
|
|
440
439
|
|
|
441
440
|
**Process for a major-upgrade PR:** (1) bump exactly one dependency major in its own branch; (2) `npm run sdk:ready` green; (3) packed-install evidence (`npm run pack:dry-run`, or a scratch `npm install <tarball>` import smoke for native deps like `better-sqlite3`); (4) review lockfile churn line-by-line; (5) the `supply-chain` job supplies audit/SBOM; (6) confirm no build-time regression beyond measured noise on the matrix above; (7) merge separately from any feature work.
|
|
442
441
|
|
|
443
442
|
## Release checklist
|
|
444
443
|
|
|
445
444
|
|
|
446
|
-
Every release gate maps to an exact enforcement test or command, so the checklist is executable rather than manual. Run `npm run sdk:ready` for the full local SDK readiness gate: `npm run typecheck`, `npm run lint`, `npm run format:check`, network-free `npm test`, `npm run test:coverage`, `npm run pack:dry-run`, and `npm run release:gate`. `npm run release:dry-run` is an alias for the same gate. The GitHub Actions `verify` job runs `
|
|
445
|
+
Every release gate maps to an exact enforcement test or command, so the checklist is executable rather than manual. Run `npm run sdk:ready` for the full local SDK readiness gate: `npm run typecheck`, `npm run lint`, `npm run format:check`, network-free `npm test`, `npm run test:coverage`, `npm run pack:dry-run`, and `npm run release:gate`. `npm run release:dry-run` is an alias for the same gate. The GitHub Actions `verify` job runs `bun ci` and `npm run sdk:ready` on Node 24; `node22-compat` runs `bun ci`, `npm run build`, and public export imports on Node 22; `postgres-integration` runs the opt-in PostgreSQL adapter suite against a CI Postgres service. Contributor installs use Bun: the committed lockfile is `bun.lock` (JSONC-shaped, read by `scripts/bun-lock.mjs`), `bun ci` is the frozen CI install, and `package-lock.json` is retired; `npm pack` and `npm publish` remain the publish path.
|
|
447
446
|
|
|
448
447
|
| Gate | Enforcement |
|
|
449
448
|
| --- | --- |
|
|
@@ -457,14 +456,15 @@ Every release gate maps to an exact enforcement test or command, so the checklis
|
|
|
457
456
|
| NeuralWatt package/docs/examples release gate | `packaging.test.ts` pins `@arnilo/prism-providers/neuralwatt` package exports/type declarations and `@arnilo/prism-providers` family membership; `docs.test.ts` asserts `docs/index.md` links `providers/neuralwatt.md` and `provider-caching.md`, and that `examples/cache-aware-prompt-assembly.ts` plus `examples/neuralwatt-agent-run.ts` exist and are listed. |
|
|
458
457
|
| Enterprise PostgreSQL package/docs/example gate | Packaging/install/public-contract tests include `@arnilo/prism-core/enterprise/postgres`; `docs.test.ts` pins its API page, four-store migration/ownership/unknown-outcome/async-router guidance, and `examples/enterprise-postgres-state.ts`; `npm run test:postgres` exercises migration, restart, contention, and cleanup with an explicit database URL. |
|
|
459
458
|
| Version graph and resumable publication | `release.test.ts` covers exact package/lock/range validation, topological order, registry collisions, dry-run, interrupted reports/resume, clean tagged git state, provenance/public/tag arguments, and token-safe errors. `release:check` and `release:publish` derive the workspace graph without a manual package list. |
|
|
460
|
-
|
|
|
459
|
+
| Freeze-test current-side package presence | `workspacePackageCounts()` (`scripts/package-truth.mjs`) counts every `packages/*` directory with a manifest. A new workspace package is counted, and the frozen expected side fails until one regen. |
|
|
460
|
+
| Release version claims have one source | `currentVersion()` (`scripts/package-truth.mjs`) reads the root manifest, and `scripts/version-literal-gate.test.mjs` fails when any surface that claims the release version disagrees with it: all 12 manifests, every internal `@arnilo/*` caret range, `bun.lock`, the `src/index.ts` version constant, the `docs/index.md` current-line banner, the `release.yml` tag list, and the generated `scripts/package-truth.json`. Each stale surface is named in one pre-flight (with a positive control for a half-finished cut), and the release-line suites (`packaging.test.ts`, `docs.test.ts`, `release.test.ts`, `phase24-truth`, `phase34-freeze`) derive their expectations from the manifest instead of pinning the cut. |
|
|
461
461
|
| Pre-publish compatibility gates | `release:gate` (in `sdk:ready`) fails on removed/changed `.d.ts` exports vs `scripts/compat-baseline/` (unless `--allow-break` + migration note), version-range/lockfile drift, and tarball deny-list violations (`plans/`, `code-reviews/`, `docs/review-coverage-*`, `*.map`, `__tests__/`); unit-tested in `scripts/release-gate.test.mjs`. |
|
|
462
|
-
| Legacy registry markers (plan 054 Task 7) | `scripts/phase54-legacy-registry.mjs --dry-run` verifies every retired name's final published version exists and `latest` is unchanged, and that each deprecation URL anchor exists in `docs/migrate-to-0.4.md`, without mutating the registry; `--apply --confirm` pre-flights all 54 entries and fails closed (zero mutations) on any mismatch, then idempotently adds the `legacy` dist-tag and `<0.4.0` deprecation warning (already-correct entries skipped; per-entry status in `release-artifacts/legacy-registry-plan.json` for safe resume). `packaging.test.ts` asserts the generated plan covers all 54 retired names with uniform messages and valid guide anchors; the offline fixture suite `scripts/phase54-legacy-registry.test.mjs` proves the dry-run/apply/resume behavior without network or tokens. |
|
|
463
|
-
| Formatting, linting, and coverage thresholds | `npm run lint` and `npm run format:check` run Biome (single root `biome.json`, workspaces inherit) and fail on any lint error or unformatted file; `npm run test:coverage` uses
|
|
462
|
+
| Legacy registry markers (plan 054 Task 7) | `scripts/phase54-legacy-registry.mjs --dry-run` verifies every retired name's final published version exists and `latest` is unchanged, and that each deprecation URL anchor exists in `docs/migrate-to-0.4.md`, without mutating the registry; `--apply --confirm` pre-flights all 54 entries and fails closed (zero mutations) on any mismatch, then idempotently adds the `legacy` dist-tag and `<0.4.0` deprecation warning (already-correct entries skipped; per-entry status in `release-artifacts/legacy-registry-plan.json` for safe resume). `packaging.test.ts` asserts the generated plan covers all 54 retired names with uniform messages and valid guide anchors; the offline fixture suite `scripts/phase54-legacy-registry-{dry-run,apply,fail-closed}.test.mjs` proves the dry-run/apply/resume behavior without network or tokens. |
|
|
463
|
+
| Formatting, linting, and coverage thresholds | `npm run lint` and `npm run format:check` run Biome (single root `biome.json`, workspaces inherit) and fail on any lint error or unformatted file; `npm run test:coverage` uses Bun's built-in `bun test --coverage` with Bun-measured floors (core lines 91.48 / functions 92.21; per-package lines rows in `scripts/coverage-thresholds.json`; no branch floor — Bun 1.4.2 emits no branch data) and no third-party service. All three run inside `sdk:ready`. |
|
|
464
464
|
| Supply-chain and live-canary policy | `supply-chain-security.test.ts` verifies SPDX allow/deny behavior, bounded source/artifact secret detection, credential-free canary reports, timeout/redacted failures, immutable action revisions, no `pull_request_target`, protected live environment, attestation paths, and publish dependency on `supply-chain`; CI adds CodeQL and PR dependency review. |
|
|
465
465
|
| Release secret scan covers the tracked release set | `scripts/phase27-release.test.mjs` enumerates `git ls-files` (plus built `packages/prism-core/dist` when present) and passes that explicit list to the unchanged `scanSecrets` — the same tracked set CI scans (`git ls-files -z \| xargs -0 node scripts/scan-secrets.mjs` in `release.yml`/`security.yml`), so no untracked or gitignored working-tree file can fail the gate or mask a tracked finding; a bare `node scripts/scan-secrets.mjs <path>` still walks everything for pre-commit use except the two local-only credential names `.gitignore` already excludes (`scripts/live.env`, `*.local.env`), and the gate reports the mode, file count, and untracked-but-unignored findings as a note without failing on them. |
|
|
466
466
|
| Workflow and script references resolve | `scripts/workflow-liveness.test.mjs` resolves every `-w <pkg>` / `--workspace <pkg>` target in `.github/workflows/*.yml` against the live workspace inventory and every named npm script against that package's manifest (root scripts included for a bare `npm run x`), resolves every `@arnilo/*` specifier in `scripts/**/*.mjs` against the live package and its `exports` subpaths, and rejects any `uses:` reference that is not a full 40-hex commit SHA — the drift class that left `sandbox-browser.yml` building four retired packages, `scripts/fixtures/phase26-coding-journey.mjs` packing `packages/coding-agent`/`-security` and `scripts/benchmark-scenarios/phase11-auth.mjs` importing `@arnilo/prism-openapi-tools`/`-server` after plan 054 folded them, with a positive control for each. |
|
|
467
|
-
| Network-free + offline test budget | `network-free-guard.test.ts` keeps the default suite network-free; budget pinned `<
|
|
467
|
+
| Network-free + offline test budget | `network-free-guard.test.ts` keeps the default suite network-free; budget pinned `< 110s` (measured baseline above). Install-smoke is offline (`--offline --no-audit --no-fund`, zero registry fetches). |
|
|
468
468
|
| Core security invariants reaffirmed | Runtime/docs tests hold the trust boundary: **no built-in app tools** (hosts register tools; the core ships only the mock provider and contract helpers), **no hidden provider/credential globals** (providers/credentials are host-owned `AgentConfig` fields, resolved via explicit `providerSource`/`CredentialResolver`), **no auto package discovery** (provider/tool/skill packages are opt-in and individually installed; contribution discovery is realpath-contained and emits inert envelopes the host registers), and **no secret persistence in core** (redaction applies before any `RunLedger`/`SessionStore` append; the ledger gate asserts each message event is written exactly once and redacted). |
|
|
469
469
|
|
|
470
470
|
A change that adds a public persistence/runtime surface, a new package, or a new example must extend the matching row's enforcement (add the page to `apiPages`, the package to the `packages` array, or the example to the demos list) so the checklist stays self-maintaining.
|
package/docs/runs-and-usage.md
CHANGED
|
@@ -63,7 +63,7 @@ Defaults are the unconfigured fence (OWASP LLM10): turns 16, provider attempts 2
|
|
|
63
63
|
labeled estimate (or zero with `usageEstimation: "off"`) to the token counters and never a
|
|
64
64
|
price, so a configured `maxCost` stays the fail-closed envelope for usage-less vendors.
|
|
65
65
|
|
|
66
|
-
Prism charges turns before assembly, provider attempts before generation, request bytes per request payload, response bytes per provider event (each frame must fit the byte cap on its own), tool rounds before a batch, tool calls before dispatch, and usage before another turn. A breach stops new work, aborts active work through the run signal, emits exactly one redacted `run_limit_exceeded` event/ledger row, and throws `AgentRunError` with `result.limit` (`limit`, `maximum`, `observed`, optional `currency`). Just before the terminal `error`, the run also emits one `budget_exhausted` attribution event — the axis that fired, run counters at exhaustion, the three closest other axes, and hashes of the last ten dispatched tool calls ([Agent events § Run limit events](agent-events.md#run-limit-events)). Provider-reported token/cost totals arrive after generation, so that completed provider turn can be the unavoidable overshoot boundary.
|
|
66
|
+
Prism charges turns before assembly, provider attempts before generation, request bytes per request payload, response bytes per provider event (each frame must fit the byte cap on its own), tool rounds before a batch, tool calls before dispatch, and usage before another turn. A breach stops new work, aborts active work through the run signal, emits exactly one redacted `run_limit_exceeded` event/ledger row, and throws `AgentRunError` with `result.limit` (`limit`, `maximum`, `observed`, optional `currency`) plus `result.attribution` (`consumed`, `closestOtherAxes`, `recentToolCalls`). Just before the terminal `error`, the run also emits one `budget_exhausted` attribution event carrying that same payload — the axis that fired, run counters at exhaustion, the three closest other axes, and hashes of the last ten dispatched tool calls ([Agent events § Run limit events](agent-events.md#run-limit-events)). Provider-reported token/cost totals arrive after generation, so that completed provider turn can be the unavoidable overshoot boundary.
|
|
67
67
|
|
|
68
68
|
`createRunLimitTracker()` and `resolveRunLimits()` are public for adapters that need the same validation and accounting semantics. Workflow agent nodes forward `RunWorkflowOptions.limits`; supervisor delegation narrows its step/tool/token/timeout budget into core limits; MCP tool calls use a per-call tracker.
|
|
69
69
|
|
|
@@ -78,7 +78,7 @@ const estimate = estimateMessageTokens(messages, "claude-sonnet-4.5"); // model
|
|
|
78
78
|
// { tokens: 41_200, confidence: "medium", lowConfidence: false }
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
`MODEL_FAMILY_TOKENS` holds the chars/token ratio, per-message overhead, and confidence label per family (`anthropic`, `openai`, `google`, `deepseek`, `openrouter-generic`, `mistral`, `unknown`). `resolveModelFamily(modelId)` maps a model id or provider id to a table key; unmatched input resolves to `unknown`, whose row is the most conservative (highest estimated token count) and carries `confidence: "low"` / `lowConfidence: true`. Estimates are heuristics, not tokenizers: prose, fenced code, and CJK content are weighted separately, and every calibrated family is `confidence: "medium"` because Prism ships no real tokenizer. The estimator is pure — no network, no I/O, and no content retention.
|
|
81
|
+
`MODEL_FAMILY_TOKENS` holds the chars/token ratio, per-message overhead, and confidence label per family (`anthropic`, `openai`, `google`, `deepseek`, `openrouter-generic`, `mistral`, `unknown`). `resolveModelFamily(modelId)` maps a model id or provider id to a table key; unmatched input resolves to `unknown`, whose row is the most conservative (highest estimated token count) and carries `confidence: "low"` / `lowConfidence: true`. Estimates are heuristics, not tokenizers: prose, fenced code, and CJK content are weighted separately, and every calibrated family is `confidence: "medium"` because Prism ships no real tokenizer. The estimator is pure — no network, no I/O, and no content retention. `MODEL_FAMILY_TOKENS` is frozen at runtime (every row and the table), so ratios cannot be overridden in place — recalibration stays a source change plus the live leg below, never a runtime override.
|
|
82
82
|
|
|
83
83
|
Row provenance and recalibration: reference counts per family are frozen in `src/__tests__/fixtures/usage-calibration.json` — `openai` measured against `o200k_base` (dev-time oracle; no tokenizer ships), `anthropic`/`google`/`mistral` as their published chars/token guidance, and `deepseek`/`openrouter-generic` as row-basis values (no public count endpoint). `src/__tests__/usage-calibration.test.ts` fails when a shipped row drifts outside the recorded bands (prose ±12%, CJK ±20%, per-message overhead ±1 token). To re-measure, run `PRISM_LIVE_PROVIDER_TESTS=1` with `ANTHROPIC_API_KEY` and/or `GEMINI_API_KEY`/`GOOGLE_API_KEY` and `node --test scripts/usage-calibration-live.test.mjs` (matrix suite `calibration/vendor-count-tokens`): it posts the fixed corpus to `POST /v1/messages/count_tokens` and `POST /v1beta/models/{model}:countTokens`, asserts each shipped row against the measured count, and refreshes `docs/_evidence/phase103-family-token-calibration.md`. A measurement outside its band means updating the row in `src/usage-estimation.ts` and the fixture counts in one change.
|
|
84
84
|
|
|
@@ -86,7 +86,7 @@ Row provenance and recalibration: reference counts per family are frozen in `src
|
|
|
86
86
|
|
|
87
87
|
`usageEstimation` is `"fallback"` (default), `"off"`, or `"strict"`. With the default, a provider turn that reports no usage records one labeled estimate at the existing usage seam — no adapter changes:
|
|
88
88
|
|
|
89
|
-
- the `provider_turn_finished.usage` carries `{ inputTokens, estimated: true, confidence }`, and its `budgets.inputTokens`/`runInputUsed` use that estimate, with `budgets.inputTokensSource
|
|
89
|
+
- the `provider_turn_finished.usage` carries `{ inputTokens, estimated: true, confidence }`, and its `budgets.inputTokens`/`runInputUsed` use that estimate, with `budgets.inputTokensSource` labeling the figure's provenance (below), so the attention axes and run limits from plans 086/087 work on non-reporting models;
|
|
90
90
|
- ledger `appendUsage` rows (`scope: "provider_turn"` and the `run_total` aggregate) and `AgentRunResult.usage` keep `estimated: true` (plus `confidence`) — a billing surface can always tell an estimate from a report;
|
|
91
91
|
- estimates are **never priced**: the cost catalog is not consulted, and estimated usage carries no `cost`/`currency`, so a `maxCost` limit still fails closed instead of blocking on invented numbers;
|
|
92
92
|
- `"off"` leaves absent usage absent — no ledger row, no run total, never a zero.
|
|
@@ -94,12 +94,14 @@ Row provenance and recalibration: reference counts per family are frozen in `src
|
|
|
94
94
|
|
|
95
95
|
The estimate covers the turn's own request — messages plus tool declarations and context blocks — and prefers the most exact measurement that already exists (plan 103 T6), in this order:
|
|
96
96
|
|
|
97
|
-
1. **The budget pass's own measurement.** When `AgentConfig.contextBudget` sets `reportOmissions: true`, the request carries a `ContextBudgetReport`, and the fallback reuses its `keptTokens` verbatim — the same whole-request figure (post-eviction messages, context, skills, tool declarations) that decided evictions, so usage accounting and budget decisions can never show two different numbers for the same request. It is measured at budget time, so content added afterwards (tail segments, middleware edits) is not included
|
|
97
|
+
1. **The budget pass's own measurement.** When `AgentConfig.contextBudget` sets `reportOmissions: true`, the request carries a `ContextBudgetReport`, and the fallback reuses its `keptTokens` verbatim — the same whole-request figure (post-eviction messages, context, skills, tool declarations) that decided evictions, so usage accounting and budget decisions can never show two different numbers for the same request. It is measured at budget time, so content added afterwards (tail segments, middleware edits) is not included — a measured 19.2–19.4% shortfall with an ≈8k-character post-budget append and 0.0% without one (`docs/_evidence/phase112-primitive-review.md` §4.4) — and `confidence` names the basis: `"high"` when a host `tokenEstimator` made the measurement, `"low"` for the built-in ÷4 basis (uncalibrated).
|
|
98
98
|
2. **The host tokenizer.** With `contextBudget.tokenEstimator` and no report, the fallback projects the request through that tokenizer — per message plus tool/context portions — using the assembler's own text shapes (the `measureAll` tool-list line and context-block text, never `JSON.stringify` of the schemas), validated exactly like the budget pass validates it. `confidence: "high"`: a host tokenizer's count is still an estimate (`estimated: true`), never `"reported"`.
|
|
99
99
|
3. **The family heuristic** (plan 091): the model id's family table for messages (per-message overhead included), and those same assembler text shapes for the tool/context portions.
|
|
100
100
|
|
|
101
101
|
All three paths keep `estimated: true` and are never priced. `usageEstimation: "off"` and `"strict"` never consult the report or the tokenizer — their behavior is decided before any measurement.
|
|
102
102
|
|
|
103
|
+
`budgets.inputTokensSource` labels the figure's provenance: `"reported"` when the provider reported that turn's own usage, `"estimated"` for any of the three fallback paths above, and absent together with `inputTokens`. The payload shape is in [Agent events § Run limit events](agent-events.md#run-limit-events).
|
|
104
|
+
|
|
103
105
|
### `session.contextMeter()`
|
|
104
106
|
|
|
105
107
|
One state read for host UIs (Clay's token meter, Synapta's model-router budgets):
|
package/docs/session-stores.md
CHANGED
|
@@ -108,7 +108,7 @@ Recognize it with `isSessionAppendConflict(error)`, not message text. Built-in s
|
|
|
108
108
|
- Store adapters own id generation policy, ordering, duplicate detection, idempotency storage, and error handling.
|
|
109
109
|
- `AgentSession` uses `AgentSessionConfig.store` before `AgentConfig.store`; otherwise it falls back to a private memory store.
|
|
110
110
|
- Branch semantics are parent links plus a leaf id. External UIs should keep branch handles as `(sessionId, leafId)`; RPC exposes an additional `handleId` for active handles.
|
|
111
|
-
-
|
|
111
|
+
- The memory store implements `readBranchPath` (ancestor chain root→leaf, numeric offset cursor, one clone on the way out). The JSONL store still omits it; the runtime falls back to `list(sessionId)`. Database-backed stores should implement `readBranchPath` so `entries()`, `clone()`, and context rebuild read only the selected ancestor chain.
|
|
112
112
|
|
|
113
113
|
## Session search
|
|
114
114
|
|
|
@@ -152,7 +152,7 @@ Sizing (plan 095): SQLite FTS5 and the Postgres `tsvector` column are maintained
|
|
|
152
152
|
|
|
153
153
|
- Do not store provider credentials, credential resolvers, provider instances, or unredacted secrets in session entries, append options, idempotency keys, or branch records.
|
|
154
154
|
- Use `AgentConfig.redactor` or `RunOptions.redactor` to redact secrets before entries reach durable stores. Stores receive already-redacted `SessionEntry` values.
|
|
155
|
-
- `createMemorySessionStore()` keeps O(1) duplicate/idempotency/parent checks in process-local maps; it is not durable.
|
|
155
|
+
- `createMemorySessionStore()` keeps O(1) duplicate/idempotency/parent checks in process-local maps; it is not durable. Idempotency dedup remembers the latest 4,096 keys; an older replay appends as a new entry.
|
|
156
156
|
- The JSONL adapter serializes appends per store instance, has no cross-process lock, and is not suitable for production multi-writer storage.
|
|
157
157
|
- Database-backed stores should follow the indexes and retention guidance in [Database persistence](database-persistence.md). Implement `readBranchPath` as a single branch-path query (for example a recursive CTE) and avoid loading entire large sessions into memory when only one branch is needed.
|
|
158
158
|
|
package/docs/supervisors.md
CHANGED
|
@@ -28,7 +28,7 @@ Use a supervisor when a host or agent must choose a child dynamically. Use `@arn
|
|
|
28
28
|
|
|
29
29
|
## Outputs / response / events
|
|
30
30
|
|
|
31
|
-
`delegate()` returns the child's `AgentRunResult` or throws its `AgentRunError`/a supervisor denial or limit error. A failure (an error or a run-limit death) publishes `child_failed` before the terminal `delegation_error`: the redacted `reason`, the terminal `status` and `stopReason`, the plan-086/087 `RunLimitBreach` (`limit`, `maximum`, `observed`) in `limit` when a configured ceiling fired,
|
|
31
|
+
`delegate()` returns the child's `AgentRunResult` or throws its `AgentRunError`/a supervisor denial or limit error. A child that died on its own run limit throws `SupervisorLimitError` with that result in `result` — the same breach and `attribution` the `child_failed` event carries, so a host that catches it needs no event join; a supervisor-level ceiling (depth, active children, delegation or wait timeout, cycle, input bytes) has no child run behind it and carries none. A failure (an error or a run-limit death) publishes `child_failed` before the terminal `delegation_error`: the redacted `reason`, the terminal `status` and `stopReason`, the plan-086/087 `RunLimitBreach` (`limit`, `maximum`, `observed`) in `limit` when a configured ceiling fired, terminal `usage`, and — on that ceiling death — the child's own plan-087 attribution (`consumed`, `closestOtherAxes`, `recentToolCalls`: counters, axis ratios, and `sha256:` tool-call hashes, the same payload as the child result's `attribution` and the child's `budget_exhausted` event). Host cancels, policy denials, and hook rejections are not failures and never emit it. `delegateAsync()` returns a local running handle; `wait()` returns its result (or `{ status: "cancelled" }` after `cancel()`), and stays idempotent while its terminal record is retained (bounded by `limits.maxQueuedEvents`; an evicted or foreign id returns the same non-enumerating error). `subscribe()` emits bounded `delegation_started`, `delegation_finished`, `delegation_rejected`, and `delegation_error` metadata events, plus the opt-in child-event family below. Aborting `CreateSupervisorOptions.signal` aborts every running child (session- and task-lifetime) and closes the stream. Hosts routing child events onto a parent session stream pass `childEventSink`; it receives the identical payload the supervisor stream carries — a redacted, capped, rate-coalesced `AgentEvent` tagged with `child: { childId, delegationId, depth }` (contract type `ChildEventOrigin`) — so a parent subscriber can route it with `event.child` and no per-type handling. Hosts may project the lifecycle events through observability `handleDelegation()` using the parent Prism run ID; no OpenTelemetry dependency enters this package.
|
|
32
32
|
|
|
33
33
|
### Child lifetime, reporting, and budget share
|
|
34
34
|
|
|
@@ -45,12 +45,20 @@ Every `SupervisorChild` may carry a `policy` of host ceilings/defaults; a `Deleg
|
|
|
45
45
|
|
|
46
46
|
### Recovery telemetry
|
|
47
47
|
|
|
48
|
-
`summary()` returns one frozen row per allow-listed child — `{ childId, attempts, retries, failures, failureRadius, outcome }` — maintained incrementally (O(1) per delegation, O(children) to read)
|
|
48
|
+
`summary()` returns one frozen row per allow-listed child — `{ childId, attempts, retries, failures, failureRadius, outcome }` — maintained incrementally (O(1) per delegation, O(children) to read). Counters are cumulative for the supervisor's lifetime, so a host can diff snapshots per root run or watch a long-lived supervisor without host-side aggregation; `summary({ reset: true })` instead zeroes every counter and starts a new window, which is the direct per-root-run read:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
supervisor.summary({ reset: true }); // root run start: new window (a no-op before the first delegation)
|
|
52
|
+
const run = await agent.run(input);
|
|
53
|
+
const perChild = supervisor.summary().children; // this root run's counters
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A reset restates `outcome` for the window — `running` when that child has a live delegation, else `idle` — and leaves live delegations untouched, so a run that settles after the reset still records its failures and failure radius. Because the pre-window outcome is gone, a dispatch after a `failed`/`aborted` outcome from *before* the window counts `attempts: 1` with no `retries` bump; `retries` counts only failures inside the window. `reset: false` or no argument is the cumulative read. There is no `supervisor_run_summary` event: the supervisor has no root-run boundary — a turn boundary or a momentary `activeChildren === 0` between two delegations is not one — so the window is host-declared.
|
|
49
57
|
|
|
50
58
|
- `outcome` is `idle` before the first delegation, `running` while any is live, otherwise the `delegation_finished.status` vocabulary (`succeeded`/`failed`/`aborted`/`suspended`/`denied`) or `rejected` for a hook denial. Resuming a suspended run updates the outcome but is not a new attempt.
|
|
51
59
|
- `attempts` counts started delegations, hook rejections included. `retries` counts attempts started after a `failed`/`aborted` outcome — the recovery re-dispatch metric.
|
|
52
|
-
- `failures` counts delegations that died on an error or a limit; host cancels, denials, and hook rejections are excluded.
|
|
53
|
-
- `failureRadius` is the blast radius of the child's most recent failure:
|
|
60
|
+
- `failures` counts delegations that died on an error or a limit, in their live run or on a resume of it; host cancels, denials, and hook rejections are excluded.
|
|
61
|
+
- `failureRadius` is the blast radius of the child's most recent failure: descendant delegations (any depth) still live at that moment. Ancestry is tracked per delegation, so two concurrent delegations of the same child id stay distinct; an unrelated or already-finished child is not counted, and a session-lifetime child is detached from the failed subtree by design. Liveness is per supervisor instance: a delegation resumed after a restart is live again from the moment its rebuilt run starts, so it counts as a live descendant and holds its child's `outcome` at `running` — and a delegation started after that resume is numbered past the resumed id, so the two never share one. A suspended child that is never resumed is not counted: nothing runs for it in this process.
|
|
54
62
|
|
|
55
63
|
Failure attribution is the same object the `child_failed` event carries, so a host that only keeps the summary and one that only keeps events read the same taxonomy.
|
|
56
64
|
|
|
@@ -102,7 +110,7 @@ await parent.createSession().run("Research auth and billing", {
|
|
|
102
110
|
|
|
103
111
|
## Durable child approvals
|
|
104
112
|
|
|
105
|
-
With `checkpoints` + `definitionRevision`, every child run is durable with `interruptBeforeTool: true`. A child that suspends on pending decisions throws `AgentDelegationSuspendedError` out of `delegate()`; when the delegation runs inside a root agent's tool, core converts it into a root suspension whose `interruption.pendingDecisions` carry hashed root-visible approval ids (`sub_<sha256(runId:childApprovalId)>`) and `attribution.path` (redacted child ids, root first, at most 8 deep). Root decisions route back through the same CAS rules: pass `supervisor.resumeNestedRun` as `resumeNestedRun` in the root run's `runState` and in every `resumeAgentRun` options object. The supervisor rebuilds the child from a bounded delegation mapping stored in the same checkpoint store (child id, delegation/thread ids, redacted input, version), re-runs the `before` hook so its narrowing applies to the resumed run (hooks must be idempotent), and re-attributes re-suspensions recursively, so grandchild decisions surface with the full path. A delegating child's own `interruptBeforeTool` also gates its delegate tool, so hosts approve delegation and the child's own side effects as separate stages. Root `*_for_run` stickies record the attribution path and only match the same delegation path; child stickies live on the child run and expire with it. A root approval never widens the child: the child's narrowed permission re-runs at dispatch. Unknown or foreign nested run ids fail closed with one non-enumerating error. A resumed attempt is terminal-symmetric with live `delegate()`: it publishes `delegation_finished` (`delegation_rejected` when the re-run `before` hook denies) and runs `hooks.after` once with the original `childId`/`delegationId`, which is what lets an isolated child's worktree be cleaned up. A suspended child stays non-terminal — no finish event, no `after` — and a rebuild that throws before the run starts (stale version, fingerprint drift) publishes nothing and runs no terminal hook, so a duplicate resume attempt can never clean up a live suspended child. Child factories must return stable configs and a durable (or rebuild-stable) session store for resume to work.
|
|
113
|
+
With `checkpoints` + `definitionRevision`, every child run is durable with `interruptBeforeTool: true`. A child that suspends on pending decisions throws `AgentDelegationSuspendedError` out of `delegate()`; when the delegation runs inside a root agent's tool, core converts it into a root suspension whose `interruption.pendingDecisions` carry hashed root-visible approval ids (`sub_<sha256(runId:childApprovalId)>`) and `attribution.path` (redacted child ids, root first, at most 8 deep). Root decisions route back through the same CAS rules: pass `supervisor.resumeNestedRun` as `resumeNestedRun` in the root run's `runState` and in every `resumeAgentRun` options object. The supervisor rebuilds the child from a bounded delegation mapping stored in the same checkpoint store (child id, delegation/thread ids, redacted input, version), re-runs the `before` hook so its narrowing applies to the resumed run (hooks must be idempotent), and re-attributes re-suspensions recursively, so grandchild decisions surface with the full path. A delegating child's own `interruptBeforeTool` also gates its delegate tool, so hosts approve delegation and the child's own side effects as separate stages. Root `*_for_run` stickies record the attribution path and only match the same delegation path; child stickies live on the child run and expire with it. A root approval never widens the child: the child's narrowed permission re-runs at dispatch. Unknown or foreign nested run ids fail closed with one non-enumerating error. A resumed attempt is terminal-symmetric with live `delegate()`: it publishes `delegation_finished` (`delegation_rejected` when the re-run `before` hook denies; `child_failed` plus `delegation_error` when the resumed run dies) and runs `hooks.after` once with the original `childId`/`delegationId`, which is what lets an isolated child's worktree be cleaned up. A suspended child stays non-terminal — no finish event, no `after` — and a rebuild that throws before the run starts (stale version, fingerprint drift) publishes nothing and runs no terminal hook, so a duplicate resume attempt can never clean up a live suspended child. Child factories must return stable configs and a durable (or rebuild-stable) session store for resume to work.
|
|
106
114
|
|
|
107
115
|
## Extension and configuration notes
|
|
108
116
|
|
|
@@ -129,7 +137,7 @@ Supervisors propagate parent `identity` and `effectStore` to every child agent/r
|
|
|
129
137
|
- [A2A interoperability](a2a.md): separate remote protocol boundary. `A2ATaskLifecycle` adapts host durable agent/workflow state directly; it does not route A2A execution through local supervisor child planning.
|
|
130
138
|
- [Workflows](workflows.md): preferred deterministic orchestration.
|
|
131
139
|
- [Coding workspaces](coding-workspaces.md): opt-in per-child worktree isolation via `createWorktreeChildFactory`.
|
|
132
|
-
- [Coding agent tools](coding-agent-tools.md): opt-in `observeSupervisorLifecycle` bridges supervisor `delegation_*` events to coding `subagent_started` / `subagent_stopped` for host timelines; `supervisor.summary()` covers recovery counters (`retries`, `failures`, `failureRadius`) that the
|
|
140
|
+
- [Coding agent tools](coding-agent-tools.md): opt-in `observeSupervisorLifecycle` bridges supervisor `delegation_*` events to coding `subagent_started` / `subagent_stopped` for host timelines; `supervisor.summary()` covers recovery counters (`retries`, `failures`, `failureRadius`), and the bridge carries that row plus the redacted failure attribution only when the host opts into `includeRecovery` / `includeFailure`.
|
|
133
141
|
- Examples: [`examples/autonomous-coding-loop.ts`](../examples/autonomous-coding-loop.ts) — per-child models, factory returns `Agent`; [`examples/spawn-agent-tool.ts`](../examples/spawn-agent-tool.ts) — two model-requested explore children in one tool turn.
|
|
134
142
|
- [Working and semantic memory](working-and-semantic-memory.md): child scope construction.
|
|
135
143
|
- [Host security](host-security.md): permission and credential boundaries.
|
package/docs/testing.md
CHANGED
|
@@ -11,15 +11,23 @@ Documents how the hermetic suite runs, which stage a new suite belongs to, and t
|
|
|
11
11
|
|
|
12
12
|
## Running the suite
|
|
13
13
|
|
|
14
|
-
`npm test` delegates to `scripts/run-all-tests.mjs`, which runs
|
|
14
|
+
`npm test` delegates to `scripts/run-all-tests.mjs`, which runs every stage in `STAGES` and reports each one even when an earlier stage fails:
|
|
15
15
|
|
|
16
|
-
| stage | contents |
|
|
17
|
-
| :--- | :--- |
|
|
18
|
-
| build | `npm run build`
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
16
|
+
| stage | runner | contents |
|
|
17
|
+
| :--- | :--- | :--- |
|
|
18
|
+
| build | `npm run build` | TypeScript emit for the root and every workspace |
|
|
19
|
+
| performance budget | `node --test` | `scripts/budget-gate.test.mjs`, alone because its ceiling measures host contention |
|
|
20
|
+
| root suites | `node --test` | `dist/__tests__/*.test.js` |
|
|
21
|
+
| sqlite suites | `bun test --timeout=0` | `packages/prism-core/dist/sessions/sqlite/__tests__/*.test.js` — the one file set the Bun 1.4.2 inventory measured `bun-ok` and faster than Node; prism-core's own Node run excludes this glob |
|
|
22
|
+
| gate suites | `node --test` | `scripts/*.test.mjs` — the protection, truth, benchmark, journey, and conformance gates listed in `GATE_FILES` (`scripts/run-all-tests.mjs`), including the three split `scripts/phase54-legacy-registry-{dry-run,apply,fail-closed}.test.mjs` scenario files |
|
|
23
|
+
| build race | `node --test` | `scripts/phase23-build-race.test.mjs` |
|
|
24
|
+
| workspace suites | `npm run test --workspace <dir> --if-present` × 11 | each package's `node --test` file list, two packages in flight at a time; every leaf takes `scripts/with-build-lock.mjs --shared` |
|
|
25
|
+
| examples execution | `node --test` | `scripts/examples-execution.test.mjs` — spawns `examples/*.ts` not already run by docs demos or a dedicated spawn; manifest skips use a fixed vocabulary |
|
|
26
|
+
| branch coverage | `node` | `scripts/branch-coverage-audit.mjs` — Node instrument, core `dist/**` only; floor 83.49; Bun's gate still records `branches: null` |
|
|
27
|
+
|
|
28
|
+
Only the SQLite stage and the coverage instrument run on Bun. The root glob is ~2.3× slower under `bun test` and the prism-core workspace glob ~2.4× slower, so the split follows that measured classification instead of a wholesale switch; `npm run test:coverage` measures with `bun test --coverage` (Bun-measured floors, per-package `bunfig.toml` scoping) and runs the core suite once: the stage captures that run's output and exit code and `coverage-summary.mjs` parses the capture for the core row instead of spawning a second run (no seam set — a standalone summary, or a captured non-zero exit — still measures), while the PostgreSQL TAP leg stays on `node --test`.
|
|
29
|
+
|
|
30
|
+
The build lock has two modes: `tsc`/emit leaves keep the exclusive default, and dist-consuming test leaves pass `--shared`. Readers overlap each other (the workspace stage depends on it), while a writer still excludes every reader and a reader excludes writers — `scripts/phase23-build-race.test.mjs` proves both directions with concurrent children, plus stale-reader reclaim. The workspace stage's pool is bounded at two in-flight packages because some package suites carry soft real-time ceilings that a busier host starves; the bound and its upgrade path are documented at the stage in `scripts/run-all-tests.mjs`.
|
|
23
31
|
|
|
24
32
|
Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part of `npm test`; they fail closed with one canonical `BLOCKED GATE <id> requires=<names> evidence=<surface> hint=<how to unblock>` record and a non-zero exit when their infrastructure is absent (registry and audit: `node scripts/blocked-gate.mjs`). A successful `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` first removes stale evidence, then writes gitignored `scripts/postgres-evidence.json` with only current `gitHead`, capture time, and TAP counts; release evidence accepts it only at the same `HEAD`. Retired phase freeze/release gates live in `scripts/` for audit but are deliberately kept out of the chain. 0.7.0 host-completeness packed proof is `scripts/fixtures/e2e-070-host-completeness-journey.mjs` (same packed consumer as the full-surface journey) plus `scripts/host-completeness-evidence.test.mjs`; live legs stay skip-not-fail. R16/R17 stay blocked until plans 077/074 ship.
|
|
25
33
|
|
|
@@ -28,7 +36,7 @@ Protected-environment legs (Postgres, PTY, NATS, live credentials) are not part
|
|
|
28
36
|
- **Scratch roots come from the OS.** A suite that writes anything creates its root with `mkdtempSync(join(tmpdir(), "prism-…"))` and removes it in `after()`. Never rely on `process.cwd()` for write targets: the same suite runs with different working directories (workspace stage vs. root stage), so a cwd-relative root silently writes into the repository.
|
|
29
37
|
- **Pass explicit roots.** Wiki, memory, and store helpers default `workspaceRoot` to `process.cwd()`; suites pass their scratch root (and a `wikiRoot` relative to it) instead of accepting the default.
|
|
30
38
|
- **Tracked fixtures stay byte-identical.** `packages/memory/.wiki/` is a tracked wiki fixture and `docs/` is a tracked corpus. `scripts/wiki-scratch-isolation.test.mjs` runs the wiki suites from the package and from the repository root and fails if the tracked fixture hashes change, if a new file appears inside the fixture, if `<repo>/.wiki/` is scaffolded, or if the old cwd-relative scratch directories reappear.
|
|
31
|
-
- **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count. The wiki gate additionally uses `--test-isolation=none`: all nested files run in its one runner process, avoiding process-worker IPC deserialization without retrying failures.
|
|
39
|
+
- **Gates never write inside the repository.** A gate asserts against tracked content and spawns suites in temporary directories only. A gate that spawns `node --test` must strip `NODE_TEST_CONTEXT`/`NODE_TEST_WORKER_ID` from the child environment (an inherited value makes the nested runner skip every file and still exit 0) and assert the child reported a non-zero pass count. `bun test` sets neither `BUN_*` nor `NODE_TEST_*` (measured on 1.4.2), so no `BUN_TEST_*` strip is added. The wiki gate additionally uses `--test-isolation=none`: all nested files run in its one runner process, avoiding process-worker IPC deserialization without retrying failures. A child that runs a Node-only test flag (such as `--test` or `--test-isolation`) spawns `node` by name instead of `process.execPath`: under a Bun parent `process.execPath` is a Bun child, and `bun --test` is a script run, not a test runner. Runner-agnostic spawns (`-e` snippets, CLI invocations) keep `process.execPath` on purpose — Bun's `-e` exists, which is why the root `bun test` run works. `scripts/tooling-gate.test.mjs` scans the repository for the violation.
|
|
32
40
|
- **Wait by polling, not by sleeping.** Async browser state (download quarantine, idle reaping) is not awaitable from the outside — `manager.ts` settles it on a fire-and-forget listener promise — so a fixed sleep is a race that loses under CPU load and fails the assertion for a reason unrelated to the behavior under test. Suites poll observable state through `waitFor(read, ok, label, { timeoutMs, intervalMs })` in `packages/web-tools/src/browser/__tests__/wait-for.ts`, which returns as soon as the state appears and otherwise throws naming the label and the last observed value. Fixed sleeps remain only where real elapsed time is the subject of the test (idle TTLs).
|
|
33
41
|
|
|
34
42
|
## Related APIs
|
package/docs/workflows.md
CHANGED
|
@@ -89,14 +89,14 @@ All workflow limits and runtime `concurrency` reject non-safe integers, zero, ne
|
|
|
89
89
|
| `resume` | For suspended runs: `{ decision: "approve" | "deny", input?, expectedVersion }`; version is mandatory for an exact-once CAS claim |
|
|
90
90
|
| `validateResume` | Host validator for resume input; required when `suspend()` declares `resumeSchema` |
|
|
91
91
|
| `metadata` | Sidecar map (`Record<string, unknown>`) persisted on the checkpoint value; a resume that does not re-state it keeps the recorded map |
|
|
92
|
-
| `restoreHooks` | External-state restore hooks (`CheckpointRestoreHook`) run sequentially on every resume before the scheduler writes; the first failure/timeout throws `CheckpointRestoreError` (`ERR_PRISM_CHECKPOINT_RESTORE`) and leaves the checkpoint untouched |
|
|
92
|
+
| `restoreHooks` | External-state restore hooks (`CheckpointRestoreHook` bare functions, or `{ id?, restore, compensate? }` objects shared with agent resumes) run sequentially on every resume before the scheduler writes; the first failure/timeout throws `CheckpointRestoreError` (`ERR_PRISM_CHECKPOINT_RESTORE`) after compensating the applied layers in reverse and leaves the checkpoint untouched |
|
|
93
93
|
| `restoreHookTimeoutMs` | Per-hook restore ceiling in ms; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS` (10 s) |
|
|
94
94
|
| `validateState` | Host validator for every initial/restored/updated state; required when workflow declares `state.schema` |
|
|
95
95
|
| `initialState` | Optional host initial state override; nested workflows receive parent state automatically |
|
|
96
96
|
|
|
97
97
|
A function node returns `suspend({ reason, data?, resumeSchema? })` to persist `status: "suspended"`. Its next invocation receives `ctx.resume` only after an approved resume. `resumeWorkflow(workflow, { runId }, options)` validates schema/version/ownership/`definitionHash`, claims the checkpoint before node execution, and continues the suspended node. Denial persists terminal `denied` status without invoking it. Existing failed/aborted checkpoint resume remains available without a human decision.
|
|
98
98
|
|
|
99
|
-
Restore hooks make the resume all-or-nothing across layers: workflow checkpoints carry the host's `metadata` (git commit, document version, workspace fingerprint), `restoreHooks` put each recorded layer back, and only when every hook succeeds does the scheduler claim the checkpoint and continue. Each hook receives `{ workflowId, runId, version, status, metadata, checkpoint }` and an `AbortSignal`; the successful run's `workflow_resumed` event carries `restore: { hooks: [{ hook, durationMs }], durationMs }`. No hooks ⇒ no hook call and no `restore` field.
|
|
99
|
+
Restore hooks make the resume all-or-nothing across layers: workflow checkpoints carry the host's `metadata` (git commit, document version, workspace fingerprint), `restoreHooks` put each recorded layer back, and only when every hook succeeds does the scheduler claim the checkpoint and continue. Each hook receives `{ workflowId, runId, version, status, metadata, checkpoint }` and an `AbortSignal`; the successful run's `workflow_resumed` event carries `restore: { hooks: [{ hook, durationMs }], durationMs }`. No hooks ⇒ no hook call and no `restore` field. Compensation is the same rule as agent resumes: an object-form handler may declare `compensate`, a failed restore undoes the applied layers in reverse (failing hook first) under the same per-hook timeout, and `CheckpointRestoreError.compensation` reports `{ ran, failed? }` best-effort while the checkpoint stays unclaimed and resumable. A successful resume's audit also reaches the review surface: `projectWorkflowTimeline` carries the same `ExecutionTimeline.restore` as the agent timeline.
|
|
100
100
|
|
|
101
101
|
> **Contract — resume-aware nodes.** After an approved resume, the **same** node's `execute` is re-invoked with `ctx.resume`. Returning `suspend(...)` unconditionally re-suspends silently; downstream nodes never run. Branch on `ctx.resume`:
|
|
102
102
|
>
|