@arnilo/prism 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +18 -16
  3. package/dist/agent-run-lifecycle.d.ts +2 -1
  4. package/dist/agent-run-lifecycle.js +1 -1
  5. package/dist/agent-session/session/assemble.js +9 -7
  6. package/dist/agent-session/session/tool-round.js +30 -20
  7. package/dist/agent-session/session/types.d.ts +1 -0
  8. package/dist/agent-session/session.d.ts +1 -0
  9. package/dist/agent-session/session.js +3 -2
  10. package/dist/checkpoint-restore.d.ts +50 -14
  11. package/dist/checkpoint-restore.js +104 -28
  12. package/dist/contracts-core/session.d.ts +2 -1
  13. package/dist/contracts-run-state.d.ts +12 -4
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js +1 -1
  16. package/dist/leases.js +32 -6
  17. package/dist/node/contribution-discovery.d.ts +16 -1
  18. package/dist/node/contribution-discovery.js +47 -0
  19. package/dist/node/session-store-jsonl.js +67 -17
  20. package/dist/run-limits.d.ts +11 -5
  21. package/dist/session-stores.js +61 -12
  22. package/dist/testing/prefix-stability-conformance.d.ts +44 -1
  23. package/dist/testing/prefix-stability-conformance.js +92 -29
  24. package/dist/usage-estimation.d.ts +7 -1
  25. package/dist/usage-estimation.js +16 -10
  26. package/docs/acp.md +2 -2
  27. package/docs/agent-events.md +7 -6
  28. package/docs/agent-session-runtime.md +1 -1
  29. package/docs/coding-agent-tools.md +1 -1
  30. package/docs/coding-tools.md +7 -11
  31. package/docs/context-and-skills.md +6 -7
  32. package/docs/contribution-discovery.md +13 -0
  33. package/docs/durable-runs.md +10 -3
  34. package/docs/embeddings.md +3 -1
  35. package/docs/execution-timeline.md +6 -0
  36. package/docs/extensions.md +1 -2
  37. package/docs/impeccable.md +1 -2
  38. package/docs/index.md +26 -21
  39. package/docs/live-testing.md +1 -2
  40. package/docs/memory-fabric.md +3 -2
  41. package/docs/migrate-to-0.11.md +65 -0
  42. package/docs/migration.md +12 -1
  43. package/docs/node-jsonl-session-store.md +4 -3
  44. package/docs/operations.md +1 -1
  45. package/docs/peer-dependencies.md +3 -5
  46. package/docs/policy-and-audit.md +1 -1
  47. package/docs/prefix-stability-conformance.md +30 -7
  48. package/docs/provider-packages.md +23 -21
  49. package/docs/providers/laya.md +113 -0
  50. package/docs/providers/typesafe.md +145 -0
  51. package/docs/public-contracts.md +1 -1
  52. package/docs/rag.md +2 -2
  53. package/docs/release-and-install.md +60 -58
  54. package/docs/runs-and-usage.md +6 -4
  55. package/docs/session-stores.md +2 -2
  56. package/docs/supervisors.md +14 -6
  57. package/docs/testing.md +17 -9
  58. package/docs/workflows.md +2 -2
  59. package/package.json +5 -4
  60. package/docs/caveman.md +0 -130
  61. package/docs/graft.md +0 -149
  62. package/docs/ponytail.md +0 -129
@@ -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: "estimated"` labeling the figure (`"reported"` when the provider did report it), so the attention axes and run limits from plans 086/087 work on non-reporting models;
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, and `confidence` names the basis: `"high"` when a host `tokenEstimator` made the measurement, `"low"` for the built-in ÷4 basis (uncalibrated).
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):
@@ -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
- - Development stores can omit `readBranchPath`; the runtime falls back to `list(sessionId)` and the pure in-memory branch walk. Database-backed stores should implement `readBranchPath` so `entries()`, `clone()`, and context rebuild read only the selected ancestor chain.
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
 
@@ -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, and terminal `usage`. 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.
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) and 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.
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: task-lifetime descendant delegations still live at that moment. An unrelated or already-finished child is not counted, and a session-lifetime child is detached from the failed subtree by design.
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 lifecycle bridge does not carry.
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 five stages and reports every stage even when an earlier one fails:
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` (all workspaces) |
19
- | root suites | `dist/__tests__/*.test.js` |
20
- | gate suites | `scripts/*.test.mjs` — the protection, truth, benchmark, journey, and conformance gates listed in `GATE_FILES` (`scripts/run-all-tests.mjs`) |
21
- | build race | `scripts/phase23-build-race.test.mjs` |
22
- | workspace suites | `npm run test --workspaces --if-present` |
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
  >
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
+ "packageManager": "bun@1.4.2",
6
7
  "main": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
8
9
  "exports": {
@@ -141,12 +142,12 @@
141
142
  "scripts": {
142
143
  "build:core": "node scripts/with-build-lock.mjs tsc",
143
144
  "clean": "rm -rf dist packages/*/dist",
144
- "build": "npm run build:core && npm run build --workspaces --if-present",
145
- "typecheck": "npm run build && npm run typecheck --workspaces --if-present && tsc -p examples --noEmit",
145
+ "build": "npm run build:core && for p in @arnilo/prism-mcp @arnilo/prism-providers @arnilo/prism-memory @arnilo/prism-work @arnilo/prism-core @arnilo/prism-channels @arnilo/prism-coding-tools @arnilo/prism-ag-ui @arnilo/prism-web-tools @arnilo/prism-acp-agent @arnilo/prism-hooks; do bun run --filter \"$p\" build || exit 1; done",
146
+ "typecheck": "bun run build && bun run --workspaces --if-present typecheck && tsc -p examples --noEmit",
146
147
  "sweep:unused": "node scripts/sweep-unused.mjs --json",
147
148
  "test:live": "node scripts/live-matrix.mjs",
148
149
  "test": "node scripts/run-all-tests.mjs",
149
- "test:coverage": "node scripts/with-build-lock.mjs node --test --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=70 --test-coverage-branches=75 --test-coverage-exclude='**/__tests__/**' --test-coverage-exclude='**/node_modules/**' --test-coverage-exclude='**/scripts/**' --test-coverage-exclude='**/packages/**' --test-coverage-exclude='**/examples/**' dist/__tests__/*.test.js && node scripts/with-build-lock.mjs node scripts/coverage-summary.mjs && node --test scripts/phase23-coverage.test.mjs && node --test scripts/phase23-skip-manifest.test.mjs",
150
+ "test:coverage": "node scripts/with-build-lock.mjs bun test --coverage --timeout=0 dist/__tests__/*.test.js > node_modules/.prism-core-coverage.out 2>&1; core_exit=$?; cat node_modules/.prism-core-coverage.out; PRISM_COVERAGE_CORE_OUTPUT=node_modules/.prism-core-coverage.out PRISM_COVERAGE_CORE_EXIT=$core_exit node scripts/with-build-lock.mjs node scripts/coverage-summary.mjs && node --test scripts/phase23-coverage.test.mjs && node --test scripts/phase23-skip-manifest.test.mjs; stage_exit=$?; rm -f node_modules/.prism-core-coverage.out; exit $stage_exit",
150
151
  "coverage:summary": "node scripts/with-build-lock.mjs node scripts/coverage-summary.mjs",
151
152
  "lint": "biome lint . --reporter=sarif --reporter-file=scripts/lint-report.sarif",
152
153
  "format": "biome format --write .",
package/docs/caveman.md DELETED
@@ -1,130 +0,0 @@
1
- # Caveman behavior integration
2
-
3
- ## What it does
4
-
5
- `@arnilo/prism-coding-tools/caveman` is an optional package that wires [juliusbrussee/caveman](https://github.com/juliusbrussee/caveman) into Prism contribution contracts.
6
-
7
- It registers upstream skills and commands, injects active level prompt slices via `InstructionInjector`, and persists level as session custom `caveman-level` entries. Import and extension `setup` without a resolvable upstream path fail closed with a bounded redacted error and register zero contributions.
8
-
9
- Upstream prompt fragments, skill bodies, and rules load from the host-supplied upstream checkout — Prism does not reimplement or vendor Caveman content.
10
-
11
- ## When to use it
12
-
13
- Use it when a host wants terse token-efficient communication modes (`lite`, `full`, `ultra`, wenyan variants, `micro`) with upstream Caveman skills (`caveman-commit`, `caveman-review`, `caveman-stats`, `caveman-compress`, `caveman-help`, `cavecrew`) in a Prism extension kernel.
14
-
15
- Skip it when you do not have a local Caveman checkout (Caveman is not published on npm) or when you only need progressive skill catalog without mode injection.
16
-
17
- Pair with Phase 3 progressive disclosure: register `createLoadSkillTool` and keep `skillsDisclosure: "progressive"` so full `SKILL.md` bodies stay catalog-only; mode slices come from the `caveman-mode` injector, not eager skill bodies.
18
-
19
- ## Inputs / request
20
-
21
- `createCavemanExtension(options)`:
22
-
23
- | Field | Type | Required | Purpose |
24
- | --- | --- | --- | --- |
25
- | `upstreamPath` | `string` | yes | Absolute path to a Caveman checkout containing `skills/`. |
26
- | `defaultLevel` | `CavemanLevel` | no | Initial level when no session entry exists (default upstream: `full`). |
27
- | `showStatus` | `boolean` | no | Emit `caveman:status` extension events on level changes. |
28
- | `appendEntry` | `(entry, opts?) => Promise<void>` | yes | Host session append (OM `attach` pattern). |
29
- | `getEntries` | `() => readonly SessionEntry[] \| Promise<...>` | yes | Current branch entries for level restore. |
30
- | `configPath` | `string` | no | Bounded local config file for `defaultLevel` / `showStatus`. |
31
-
32
- `CavemanLevel`: `off` \| `lite` \| `full` \| `ultra` \| `wenyan-lite` \| `wenyan` \| `wenyan-ultra` \| `micro`.
33
-
34
- Session custom entry shape:
35
-
36
- ```json
37
- { "kind": "custom", "data": { "type": "caveman-level", "level": "full" } }
38
- ```
39
-
40
- Required skills (fail closed if missing): `caveman`, `caveman-commit`, `caveman-review`, `caveman-stats`, `caveman-compress`, `caveman-help`, `cavecrew`. Extra `skills/*/SKILL.md` (v2.1 extras like `caveman-explore`) register as optional skills and `load_skill` commands. Dirs without `SKILL.md` (`*.mjs`, `registry.json`, `generated/`) are skipped.
41
-
42
- Registered commands: `caveman` (level), `caveman-init`, plus one `load_skill` dispatch per registered skill except `caveman`.
43
-
44
- ## Outputs / response / events
45
-
46
- | Export | Purpose |
47
- | --- | --- |
48
- | `createCavemanExtension(options)` | Returns an inert `Extension` until `kernel.load([...])`. |
49
- | `caveman-mode` injector | `InstructionInjector` — upstream filtered `skills/caveman/SKILL.md` slice when level ≠ `off`. |
50
- | `caveman` command | Set level (`/caveman lite\|full\|ultra\|wenyan\|micro\|off`) or toggle `off`↔`full`. |
51
- | Alias commands | Dispatch `{ skill, dispatch: "load_skill" }` metadata for companion skills. |
52
- | `caveman:status` event | Optional metadata when `showStatus: true`. |
53
-
54
- Deactivation phrases `stop caveman` and `normal mode` clear active injection without erasing session history.
55
-
56
- ## Request/response example
57
-
58
- ```json
59
- { "command": "caveman", "args": { "level": "ultra" }, "sessionId": "s1" }
60
- ```
61
-
62
- ```json
63
- { "kind": "custom", "data": { "type": "caveman-level", "level": "ultra" } }
64
- ```
65
-
66
- ## Implementation example
67
-
68
- ```ts
69
- import { createCavemanExtension } from "@arnilo/prism-coding-tools/caveman";
70
- import {
71
- createExtensionKernel,
72
- createLoadSkillTool,
73
- createLoadedSkillSet,
74
- createMemorySessionStore,
75
- createSkillRegistry,
76
- createSessionEntry,
77
- } from "@arnilo/prism";
78
-
79
- const store = createMemorySessionStore();
80
- const callbacks = {
81
- appendEntry: async (entry, options) => store.append(entry, options),
82
- getEntries: async () => store.list("s1"),
83
- };
84
-
85
- const kernel = createExtensionKernel({ errorPolicy: "throw" });
86
- await kernel.load([
87
- createCavemanExtension({
88
- upstreamPath: "/path/to/juliusbrussee-caveman",
89
- defaultLevel: "full",
90
- ...callbacks,
91
- }),
92
- ]);
93
-
94
- const registry = createSkillRegistry(kernel.registries.skills.list());
95
- const loaded = createLoadedSkillSet();
96
- const loadSkill = createLoadSkillTool({ registry, loaded });
97
-
98
- await kernel.registries.commands.get("caveman")!.execute({ level: "lite" }, { sessionId: "s1" });
99
- // Select instructionInjectors: ["caveman-mode"] on runs that should receive level slices.
100
- ```
101
-
102
- See `examples/caveman-ponytail.ts` for progressive catalog + `load_skill` wiring with fixture upstream trees (network-free).
103
-
104
- ## Extension and configuration notes
105
-
106
- - Import alone registers nothing and starts no timers, watchers, or network I/O (`sideEffects: false`).
107
- - `kernel.load` calls `setup`, which resolves upstream first; failure throws before any `register*`.
108
- - Level restore scans `getEntries()` for the latest `data.type === "caveman-level"` — same OM attach pattern; it is entry-scan based rather than `session_start` middleware so a reloaded or resumed session restores its level too.
109
- - Host must register `createLoadSkillTool` and pass `skillsDisclosure: "progressive"` for catalog-only skill bodies.
110
- - `caveman-stats` dispatches skill metadata only; full stats need host session-log integration.
111
- - `caveman-init` returns upstream guidance text; it does not write files in the host repo.
112
- - No TUI status bar; optional `caveman:status` events for host UI.
113
- - Caveman 2 compression proxy/engine is **not** a Prism runtime. Only `SKILL.md` files under `skills/` load.
114
-
115
- ## Security and performance notes
116
-
117
- - Upstream `SKILL.md` and injected text are untrusted host-supplied content; reads are size-bounded (`MAX_SKILL_FILE_BYTES` 256 KiB, `MAX_INJECTED_INSTRUCTION_BYTES` 32 KiB).
118
- - Config read/write is bounded (`MAX_CONFIG_FILE_BYTES` 16 KiB) at host-owned `configPath` only.
119
- - Errors redact home directories and absolute paths.
120
- - Setup is O(skills) directory scan; mode read/write is O(1) per change; injection is O(1) upstream lookup per turn.
121
- - Session custom entries respect host session ownership and redaction policies.
122
-
123
- ## Related APIs
124
-
125
- - [Ponytail behavior integration](ponytail.md): complementary lazy-minimalism mode package.
126
- - [Extension kernel and event bus](extensions.md): `kernel.load` and contribution registration.
127
- - [Context and skills](context-and-skills.md): progressive disclosure + `createLoadSkillTool`.
128
- - [Instruction injection](instruction-injection.md): `caveman-mode` injector selection.
129
- - [Observational memory compaction package](compaction-observational-memory.md): `appendEntry` / `getEntries` attach precedent.
130
- - [Migration guide](migration.md): `0.0.21 → 0.0.22` install and opt-in notes.
package/docs/graft.md DELETED
@@ -1,149 +0,0 @@
1
- # Graft context-graph integration
2
-
3
- > **Optional peer install:** `@nanonets/graft` — see [Optional peer dependencies](peer-dependencies.md).
4
-
5
- ## What it does
6
-
7
- `@arnilo/prism-memory/graft` is an optional subpath that wires [nanonets/graft](https://github.com/nanonets/graft) — a repository context-graph CLI (`graft/` directory, INDEX.md orientation, symbol-level wiring graph) — into Prism contribution contracts.
8
-
9
- It registers six pull tools backed by the graft CLI (`--json`, argv-safe), a push-mode retrieval-pack context provider plus first-turn orientation injector carried on the `graft` skill, commands (`graft`, `graft-build`, `graft-build-deep`, `graft-check`, `graft-viz`, `graft-init`), and an edit-watch middleware that computes blast radius after mutating tool calls. Import is inert; a missing graft CLI fails closed at `setup` with a bounded redacted error.
10
-
11
- ## When to use it
12
-
13
- Use it when a host wants agents to locate code by architecture, callers, and coupling before grep-spelunking. Three modes:
14
-
15
- - `"pull"` (default) — register the tools; the agent decides when to query.
16
- - `"push"` — per-turn retrieval pack (pointers only) + first-turn orientation, injected automatically.
17
- - `"both"` — everything.
18
-
19
- Install optional peer `@nanonets/graft@^0.16.0 || ^0.18.0` **or** pass `packageRoot`/`cliPath` explicitly. Both floors are smoke-tested by the offline peer-contract suite (`resolveGraftCli` bin discovery + packaged manifest); the range lists exactly the two released lines Prism validates, and `0.17` is absent because upstream never published one. Pair with progressive disclosure: the `graft` skill body stays small; tool schemas carry the details. Graft complements indexed code search (`repository_search`): graph/semantic locators vs literal search — neither replaces the other.
20
-
21
- Zero-code alternative (L0): hosts can skip this package entirely and let agents call `graft <command> --json` through their shell tool, optionally seeding context with graft's own generated instruction files. This package exists for native-tool ergonomics, budgeted subprocesses, session persistence, and push mode.
22
-
23
- ## Inputs / request
24
-
25
- `createGraftExtension(options)`:
26
-
27
- | Field | Type | Required | Purpose |
28
- | --- | --- | --- | --- |
29
- | `cliPath` / `packageRoot` | `string` | no | Explicit stub/binary or checkout root with a manifest-declared bin; default resolves optional peer `@nanonets/graft`. Relative paths rejected; explicit paths existence-checked at resolve time. |
30
- | `mode` | `"pull" \| "push" \| "both"` | no | Surface selection. Default `pull`. |
31
- | `projectDir` | `string` | no | Directory graft operates on. Default `process.cwd()` at setup. |
32
- | `retrievalBudgetMs` | `number` | no | Wall-clock budget per CLI child call (default 8000). |
33
- | `maxResultBytes` | `number` | no | Stdout cap before parsing (default 512 KiB). |
34
- | `maxPromptChars` | `number` | no | Prompts longer than this never become ask argv (default 4096). |
35
- | `allowUpstreamTelemetry` | `boolean` | no | Default false → children run with `DO_NOT_TRACK=1`. |
36
- | `providerEnv` | `Record<string, string>` | no | Explicit graft provider settings (`GRAFT_API_KEY`, …). Never inherited from host env; only `GRAFT_*` keys reach the child. |
37
- | `deepModel` | `{ provider: "openai" \| "anthropic" \| "litellm" \| "orcarouter", model: string, apiKey: string, baseUrl?: string }` | no | Model for `graft build --deep` (Graft's own LLM client — **Prism's `Provider` is not Graft's LLM**; they have different protocols). Merged over `providerEnv` as `GRAFT_PROVIDER`/`GRAFT_MODEL`/`GRAFT_API_KEY`/`GRAFT_BASE_URL`; wins on conflict. |
38
- | `initAgents` / `initYes` / `initWireMcp` | `readonly string[]` / `boolean` / `boolean` | no | `graft init` configuration: agent ids for `--agents`, `--yes`, and whether to wire graft MCP servers (default off — Prism provides its own graft surfaces). `graft-init` refuses to spawn without `initAgents` or `initYes` (the child has no TTY). |
39
- | `buildBudgetMs` / `deepBuildBudgetMs` / `buildMaxResultBytes` | `number` | no | Budgets for graph builds: structural `build`/`init` default 120000 ms, `--deep` default 600000 ms (the LLM pass over the graph), stdout cap 2 MiB. Ask/grep stay on `retrievalBudgetMs`. |
40
- | `editToolNames` | `readonly string[]` | no | Tools triggering blast-radius lookup. Default `write`, `edit`, `move`. |
41
- | `quietStartup`, `hideStatus` | `boolean` | no | Suppress startup status events / status reporting. |
42
- | `appendEntry` | `(entry, opts?) => Promise<void>` | yes | Host session append (OM attach pattern). |
43
- | `getEntries` | `() => readonly SessionEntry[] \| Promise<...>` | yes | Current branch entries for state restore. |
44
-
45
- Pull tools (mode includes `pull`): `graft_ask`, `graft_grep`, `graft_callers`, `graft_skeleton`, `graft_map`, `graft_blast`.
46
-
47
- Push surfaces (mode includes `push`): skill `graft` carrying context provider `graft-context` (per-turn pointers-only pack, gated: ≥12-char prompt, dedup by seen node ids, 32 KiB block ceiling) and instruction injector `graft-orient` (`first_turn`, byte-capped INDEX.md cut + staleness banner).
48
-
49
- Registered commands: `graft` (`status` \| `build` [deep:true] \| `check` \| `viz` \| `init` dispatch), plus `graft-build`, `graft-build-deep`, `graft-check`, `graft-viz`, `graft-init` aliases.
50
-
51
- ### Graph builds and init
52
-
53
- - `/graft-build` — structural rebuild via `graft build` (tree-sitter pass, no API key, plain-text progress — no `JSON.parse` on this surface).
54
- - `/graft-build-deep` — `graft build --deep --provider <> --model <> [--base-url <>]` using the host's `deepModel`. Without a configured model it errors before spawning. `GRAFT_API_KEY` rides in the child env, never on argv.
55
- - `/graft-init` — `graft init --no-global` (never writes user-level state), default `--no-mcp --no-hooks --no-statusline` (opt in via `initWireMcp`), plus `--agents <id>` per `initAgents` and `--yes` when `initYes`. Requires one of the two; non-interactive by design.
56
-
57
- ```bash
58
- # structural, no key
59
- /graft-build
60
- # deep — host-configured model
61
- e.g. /graft-build-deep
62
- ```
63
-
64
- ## Outputs / response / events
65
-
66
- | Export | Purpose |
67
- | --- | --- |
68
- | `createGraftExtension(options)` | Returns an inert `Extension` until `kernel.load([...])`; emits `graft:loaded` on setup. |
69
- | `resolveGraftCli(options)` | Fail-closed CLI resolution (`explicit` → command+argv, `peer-bin` → node + manifest bin). |
70
- | `runGraftJson(cli, argv, options)` / `runGraftExit(cli, argv, options)` / `childEnv(options)` / `childTimeoutMs` / `DEFAULT_MAX_RESULT_BYTES` | Budgeted runners for hosts building custom surfaces — JSON surfaces (`check`/`ask`) vs exit-code surfaces (`build`/`init`). |
71
- | `deepProviderEnv(deepModel, providerEnv)` | `deepModel` merged over `providerEnv`, filtered to `GRAFT_*`. |
72
- | `readBoundedFile` / `redactPaths` / `GraftResolveError` | Bounded-read and redaction helpers. |
73
-
74
- Events: `graft:status` (check/build outcomes), `graft:dirty` (post-edit, repo-relative path + optional `staleCountEstimate`), `graft:loaded` (mode + cliKind metadata).
75
-
76
- Session custom entry shape (`data.type === "graft-state"`, CAS via `expectedParentId`):
77
-
78
- ```json
79
- { "kind": "custom", "data": { "type": "graft-state", "freshness": { "checkedAt": "...", "fresh": true }, "seen": ["node-a"], "savedTokensApprox": 120 } }
80
- ```
81
-
82
- The graph never rebuilds itself mid-session (no auto-rebuild): after edits, ask/grep results may lag one turn; graft self-refreshes on the next indexed query, or run `/graft build` for an immediate refresh. The skill text states this contract to the agent.
83
-
84
- ## Request/response example
85
-
86
- Tool call (pull):
87
-
88
- ```json
89
- { "name": "graft_ask", "arguments": { "query": "where is auth handled?", "count": 3 } }
90
- → { "nodes": [{ "id": "auth-guard", "title": "requireAuth", "path": "src/auth.ts", "line": 41 }] }
91
- ```
92
-
93
- Status event:
94
-
95
- ```json
96
- { "type": "graft:status", "extension": "@arnilo/prism-memory/graft", "metadata": { "fresh": true, "missing": 0, "stale": 2 } }
97
- ```
98
-
99
- ## Implementation example
100
-
101
- See [`examples/graft-extension.ts`](../examples/graft-extension.ts) — network-free demo against the package fixture stub: one pull-tool call, one push turn with pack injection + dedup, one simulated edit producing blast radius, and the `DO_NOT_TRACK` child-env guard.
102
-
103
- ```ts
104
- import { createExtensionKernel, createMemorySessionStore } from "@arnilo/prism";
105
- import { createGraftExtension } from "@arnilo/prism-memory/graft";
106
-
107
- const store = createMemorySessionStore();
108
- const kernel = createExtensionKernel({ errorPolicy: "throw" });
109
- await kernel.load([
110
- createGraftExtension({
111
- packageRoot: "./vendor/graft-checkout",
112
- mode: "both",
113
- quietStartup: true,
114
- deepModel: { provider: "anthropic", model: "claude-sonnet-4-5", apiKey: process.env.ANTHROPIC_API_KEY! },
115
- initAgents: ["codex"],
116
- appendEntry: async (entry, options) => store.append(entry, options),
117
- getEntries: async () => store.list("s1"),
118
- }),
119
- ]);
120
- // Pull: dispatch graft_ask/… tools. Push: runs assemble the skill-carried
121
- // provider + graft-orient injector. Edits: middleware emits graft:dirty.
122
- // /graft-build-deep runs graft's own LLM pass; /graft-init wires codex, --no-global.
123
- ```
124
-
125
- ## Extension and configuration notes
126
-
127
- - Import alone registers nothing (`sideEffects: false`); no timers, watchers, or network. The only child processes are budgeted graft CLI calls.
128
- - Retrieval happens in-process via Prism primitives (context provider, injector, tool_result middleware) — no external hook shims.
129
- - Ask result shape is parsed tolerantly (`nodes|results|matches|hits`) because graft is pre-1.0; formatters emit pointers (`title` + `file:line` + `[[wikilink]]`), never source bodies.
130
- - Deliberate opt-out from family/umbrella packaging (like Caveman/Ponytail) — opt-in install only.
131
- - Multi-repo layouts work as upstream graft defines them (workspaces, submodules with `--follow-submodules`, sibling repos); point `projectDir` at the graft root that owns the target repo.
132
-
133
- ## Security and performance notes
134
-
135
- - Telemetry default-off: children always get `DO_NOT_TRACK=1` unless `allowUpstreamTelemetry` is true; child env is fixed-base — host env vars are never inherited, and only explicit `GRAFT_*` keys from `providerEnv`/`deepModel` pass through. Route secrets like `GRAFT_API_KEY` through the host's credential resolution when populating `deepModel`/`providerEnv`. `deepModel`'s API key reaches the child via env only — never on argv (no `--api-key` flag exists in the surface), so it cannot leak through `ps` or logs.
136
- - Build/init commands are budgeted separately from retrieval (`buildBudgetMs`, `deepBuildBudgetMs`, `buildMaxResultBytes`); deep builds fail closed without a configured model instead of spawning unconfigured.
137
- - `graft-init` always passes `--no-global` — it never writes user-level agent state; MCP/hook/statusline wiring stays off unless the host opts in via `initWireMcp`.
138
- - Upstream output is untrusted: stdout capped (`maxResultBytes`), prompts capped (`maxPromptChars`), injected packs bounded (32 KiB), orientation cut byte-capped (8 KiB); error paths are logged redacted (absolute paths/home dirs).
139
- - Every CLI call is wall-clock-budgeted (`retrievalBudgetMs`, minus fixed overhead for the timeout math) and every failure degrades silently: pull tools return structured errors, the push pack contributes nothing, edit-watch passes the tool result through untouched.
140
- - No background workers; state persists through two CAS appends per turn at most (freshness patch, seen-set/saved-tokens update).
141
-
142
- ## Related APIs
143
-
144
- - [Ponytail behavior integration](ponytail.md): same adapter pattern (optional peer/upstream path, fail-closed setup, session custom entries).
145
- - [Caveman behavior integration](caveman.md): complementary terse-communication mode package.
146
- - [Indexed code search](indexed-code-search.md): literal `repository_search` seam — complement, not overlap.
147
- - [Context and skills](context-and-skills.md): progressive catalog + `load_skill`; skill-carried context providers.
148
- - [Instruction injection](instruction-injection.md): injector seams (`graft-orient` rides `first_turn`).
149
- - [Extension kernel and event bus](extensions.md): explicit `kernel.load`, extension events.