car-runtime 0.46.1 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.d.ts +397 -19
  2. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -59,6 +59,14 @@ export interface CoderStartOptions {
59
59
  * hypothesis, the other buys a retry.
60
60
  */
61
61
  transientRetries?: number | undefined | null;
62
+ /**
63
+ * A `coder.discuss` conversation this run was distilled from. Its agreed
64
+ * constraints ride into contract derivation, so a rule stated once in the
65
+ * discussion does not have to be restated in the intent, and the session
66
+ * records the provenance. An unknown id is a clear error — never a silently
67
+ * ungrounded run.
68
+ */
69
+ discussionId?: string | undefined | null;
62
70
  }
63
71
 
64
72
  export class CarRuntime {
@@ -143,6 +151,28 @@ export class CarRuntime {
143
151
  /** Register a tool by name. */
144
152
  registerTool(name: string): Promise<void>;
145
153
 
154
+ /**
155
+ * The tools currently registered on this runtime, as a JSON array of full
156
+ * `ToolSchema` objects sorted by name.
157
+ *
158
+ * Counterpart to `registerTool` / `registerToolSchema`, which had none: a
159
+ * caller could add tools but never ask what was actually in effect, so a
160
+ * governed or read-only deployment could not prove "only these tools are
161
+ * callable". Sorted, so two calls with no registration in between are
162
+ * byte-identical and can be diffed.
163
+ */
164
+ listTools(): Promise<string>;
165
+
166
+ /**
167
+ * Remove a tool by name. Resolves to how many were removed — `0` means
168
+ * nothing matched, which is not an error, so cleanup can call this
169
+ * unconditionally.
170
+ *
171
+ * Drops the tool from both the runtime's registry and its schema map, so
172
+ * the model stops seeing it and the validator stops accepting it.
173
+ */
174
+ unregisterTool(name: string): Promise<number>;
175
+
146
176
  /** Register CAR's built-in agent utility tools. */
147
177
  registerAgentBasics(): Promise<void>;
148
178
 
@@ -391,6 +421,26 @@ export class CarRuntime {
391
421
  * dispatches the chosen components.
392
422
  */
393
423
  planEvolutionLive(requestJson: string): Promise<string>;
424
+ /**
425
+ * `supervision.subscribe` — register this connection as a supervisor of the
426
+ * admission gate: `{ filter?: { tools?, sessions?, min_reversibility? } }`.
427
+ *
428
+ * Intents arrive as `supervision.intent` NOTIFICATIONS on the same socket.
429
+ * A caller that cannot read notifications should poll `supervisionPending`
430
+ * instead — subscribing without consuming them blocks every supervised
431
+ * proposal until it fails closed.
432
+ */
433
+ supervisionSubscribe(requestJson: string): Promise<string>;
434
+ /**
435
+ * `supervision.unsubscribe` — stop supervising: `{}`. Intents already parked
436
+ * run out their timeout and fail closed rather than being released, so a
437
+ * supervisor cannot turn a pending deny into an allow by disconnecting.
438
+ */
439
+ supervisionUnsubscribe(requestJson: string): Promise<string>;
440
+ /** `supervision.pending` — every intent currently parked on a verdict: `{}`. */
441
+ supervisionPending(requestJson: string): Promise<string>;
442
+ /** `supervision.decide` — `{ intent_id, decision: { kind: "allow" | "deny" | "escalate", reason? } }`. */
443
+ supervisionDecide(requestJson: string): Promise<string>;
394
444
  /** `sync.status` — roster, journal frontier, stable frontier, state hash (B6). */
395
445
  syncStatus(requestJson: string): Promise<string>;
396
446
  /** `sync.append` — record an op on any surface: `{ surface, payload, scope? }` (B6). */
@@ -464,6 +514,27 @@ export class CarRuntime {
464
514
  */
465
515
  enforceSkillDeploymentLive(requestJson: string): Promise<string>;
466
516
 
517
+ /**
518
+ * Read the standing permission tier granted to this connection's daemon
519
+ * session (`read_only` | `sandbox_edit` | `full_access`) — the tier every
520
+ * {@link submitProposal} on this connection is judged against
521
+ * (Parslee-ai/car#890).
522
+ */
523
+ permissionGetTier(): Promise<string>;
524
+
525
+ /**
526
+ * Set this connection's standing permission tier and return the tier as the
527
+ * daemon now holds it. `tier` is `read_only` | `sandbox_edit` |
528
+ * `full_access`.
529
+ *
530
+ * Lets a binding client govern its own session — most usefully by tightening
531
+ * it: dropping to `read_only` makes the runtime escalate any write this
532
+ * client proposes to a human instead of running it. Raising the tier is
533
+ * host-gated whenever the daemon runs under a host token, so an agent
534
+ * connection cannot self-elevate.
535
+ */
536
+ permissionSetTier(tier: string): Promise<string>;
537
+
467
538
  /**
468
539
  * Ingest a skill through the deployment gate on the daemon (arXiv 2602.12430
469
540
  * "Agent Skills" — the loader integration). `requestJson` carries the skill
@@ -477,6 +548,21 @@ export class CarRuntime {
477
548
  */
478
549
  ingestSkillGoverned(requestJson: string): Promise<string>;
479
550
 
551
+ /**
552
+ * Adopt an installed skill pack on the daemon through the skill-trust
553
+ * deployment gate (arXiv 2602.12430 "Agent Skills" — the pack-adoption
554
+ * call-site). `requestJson` carries `pack` (an `ApprovedSkillPack`),
555
+ * `requested_tier?` (default `read_only`), and either `manifest?` — the signed
556
+ * bundle, whose signature trust is derived against the operator's
557
+ * `.car/config.toml` `trusted_skill_signers` keyring — or `provenance?`
558
+ * (caller-assembled), plus optional `scanned?`/`vulnerabilities?`/`source?`.
559
+ * Governance is unconditional: a denied skill never enters the graph. Returns
560
+ * `{ loaded, pending, refused, requested_tier, provenance, trusted_signers }`;
561
+ * a pending deny is resolved via `permission.approve`/`permission.reject` by
562
+ * the returned `fingerprint`, then re-adopted.
563
+ */
564
+ adoptSkillPack(requestJson: string): Promise<string>;
565
+
480
566
  /**
481
567
  * Save a learned skill with trigger context. Returns the node
482
568
  * index.
@@ -589,6 +675,13 @@ export class CarRuntime {
589
675
  * with reasoning suppressed to produce a direct answer, or
590
676
  * `"thinking_truncated"` when even that retry was empty (car-releases#60).
591
677
  *
678
+ * `auth_fallback_from` is present ONLY when a candidate earlier in the
679
+ * fallback chain was skipped because its credential was **rejected**
680
+ * (not merely absent) and a later model then answered. It names that
681
+ * dead lane, so a caller can tell the user their sign-in lapsed instead
682
+ * of silently serving a different model (Parslee-ai/car#888). Absent on
683
+ * the common path.
684
+ *
592
685
  * **Note:** intent is not exposed on the tracked path until the
593
686
  * positional argument list is converted to an options object —
594
687
  * this method already takes 9 positional parameters and adding
@@ -774,8 +867,24 @@ export class CarRuntime {
774
867
  /**
775
868
  * Unified registry (local + remote). Returns JSON array of
776
869
  * `{ id, name, provider, capabilities, param_count, size_mb,
777
- * context_length, available, is_local, max_output_tokens,
778
- * public_benchmarks, cost }`. `max_output_tokens` is the registry-declared
870
+ * context_length, available, is_local, weights_ready, downloads_weights,
871
+ * max_output_tokens, public_benchmarks, cost }`. `available` means CAR
872
+ * can use the model
873
+ * here — for a local MLX entry with a declared `hf_repo` it is `true`
874
+ * before a byte is fetched, because it lazy-downloads on first use —
875
+ * whereas `weights_ready` means the weights are already on disk (remote
876
+ * models, having none to install, report `true`). Older daemons omit
877
+ * `weights_ready`; it defaults to `false` rather than failing.
878
+ * `downloads_weights` is `true` only for entries whose weights CAR fetches
879
+ * before use (GGUF, MLX, whisper.cpp); when it is `false` — OS-provided
880
+ * models such as `windows/speech-synthesis:os` and
881
+ * `apple/foundation:default`, server-backed local models such as
882
+ * `vllm-mlx/*` and Ollama, and every remote entry — there is nothing to
883
+ * install, so `weights_ready` is meaningless and the CLI renders
884
+ * `INSTALLED` as `-`. Do not substitute `is_local`: those first four are
885
+ * all local and all download nothing. Older daemons omit
886
+ * `downloads_weights`; it defaults to `false` rather than failing.
887
+ * `max_output_tokens` is the registry-declared
779
888
  * per-model output ceiling (`null` when the entry omits it; callers
780
889
  * then fall back to a fraction of `context_length`).
781
890
  * `public_benchmarks` is `[{ name, score, harness?, source_url?,
@@ -937,9 +1046,93 @@ export class CarRuntime {
937
1046
  */
938
1047
  coderApproveMerge(sessionId: string, approve: boolean): Promise<string>;
939
1048
 
940
- /** Cancel a session: stop the loop, abandon, remove the worktree. */
1049
+ /**
1050
+ * Cancel a session: stop the loop, abandon, remove the worktree. Returns
1051
+ * `{state, already_terminal, message}`.
1052
+ *
1053
+ * An already-finished session **succeeds** rather than rejecting: `state`
1054
+ * keeps its pre-existing name and type, `already_terminal` is `true`, and
1055
+ * `message` names what already happened. Callers that cancel unconditionally
1056
+ * on shutdown depend on that — rejecting would turn a quiet exit into a
1057
+ * protocol error whenever the session raced to terminal first.
1058
+ */
941
1059
  coderCancel(sessionId: string): Promise<string>;
942
1060
 
1061
+ /**
1062
+ * The current session list AND registration for `coder.session_changed` on
1063
+ * this connection, atomically (registered under the same lock the list is
1064
+ * snapshotted under, so nothing slips through the gap). Notifications are
1065
+ * WebSocket-only, same contract as `coder.subscribe`.
1066
+ *
1067
+ * Each row carries the full summary: the pre-existing
1068
+ * `{session_id, state, intent, repo, engine, iterations, updated_at, live,
1069
+ * error}` plus `needs_you` (`"contract" | "question" | "approval" | "auth" |
1070
+ * null`), `needs_you_label` (the daemon-owned wording, so every client says
1071
+ * the same thing), `question_prompt`, `auth_message`, `auth_wait_secs`,
1072
+ * `failure_kind` (`"budget_exhausted" | "auth_required" | "infrastructure" |
1073
+ * "error"` when failed), `worktree` (only when it still exists on disk),
1074
+ * `project`, `result_branch`, `model`, `discussion_id`, and `next_seq` (live
1075
+ * only — the `coder.subscribe` cursor).
1076
+ *
1077
+ * Pass `renew: true` for the lease-renewal form: it re-registers and answers
1078
+ * `{ was_registered }` — `false` means this connection had been shed and
1079
+ * should take a full snapshot — and builds NO summaries, so it is cheap
1080
+ * enough to call on a timer. The default form is unchanged.
1081
+ */
1082
+ coderWatch(renew?: boolean | undefined | null): Promise<string>;
1083
+
1084
+ /** Stop receiving `coder.session_changed` on this connection. */
1085
+ coderUnwatch(): Promise<string>;
1086
+
1087
+ /**
1088
+ * Redraft a PROPOSED outcome contract from a plain-English request (e.g.
1089
+ * "also verify the Windows path"). Legal only in `contract_proposed`;
1090
+ * nothing executes and the session stays at the gate either way. Unlimited
1091
+ * rounds.
1092
+ *
1093
+ * Returns `{state, revised, contract, baseline, baseline_gates_nothing,
1094
+ * message}`. **Check `revised` before trusting `contract`**: on a redraft
1095
+ * that does not validate, the previous contract comes back byte-identical
1096
+ * with `revised: false` and a `message` explaining why, and the daemon emits
1097
+ * a `contract_revision_rejected` event.
1098
+ */
1099
+ coderReviseContract(sessionId: string, request: string): Promise<string>;
1100
+
1101
+ /**
1102
+ * Open a repo-grounded, strictly **read-only** discussion — a thinking
1103
+ * surface for working out what a change should be, before a run exists.
1104
+ * Bound at `PermissionTier::ReadOnly` with every write/shell escalation
1105
+ * auto-denied, so it can never touch the repo. Returns
1106
+ * `{discussion_id, repo, repo_summary}`; a non-git path is a clear error.
1107
+ */
1108
+ coderDiscussStart(repo: string): Promise<string>;
1109
+
1110
+ /**
1111
+ * Send one operator message. Returns `{ok, seq}` where `seq` is the first
1112
+ * event this turn emits; the reply streams as `coder.discuss.event`
1113
+ * (WebSocket-only, same contract as `coder.event`).
1114
+ */
1115
+ coderDiscussSend(discussionId: string, text: string): Promise<string>;
1116
+
1117
+ /**
1118
+ * Distill the discussion into `{discussion_id, proposed_intent,
1119
+ * constraints}`. **Starts nothing** — no worktree, no branch, no session.
1120
+ * The caller shows `proposed_intent` (never the transcript) for the operator
1121
+ * to edit, then passes it to `coderStart` with `discussion_id` so the agreed
1122
+ * constraints reach contract derivation. Callable repeatedly.
1123
+ */
1124
+ coderDiscussPromote(discussionId: string): Promise<string>;
1125
+
1126
+ /** Free an in-memory discussion. Discussions do not survive a daemon restart. */
1127
+ coderDiscussClose(discussionId: string): Promise<string>;
1128
+
1129
+ /**
1130
+ * Open discussions: `{discussions: [{discussion_id, repo, created_at,
1131
+ * turns}]}`. Also the capability probe — a daemon predating this surface
1132
+ * answers JSON-RPC `-32601`.
1133
+ */
1134
+ coderDiscussList(): Promise<string>;
1135
+
943
1136
  /**
944
1137
  * Managed projects + in-daemon declarative agents (the non-developer path).
945
1138
  *
@@ -1436,10 +1629,25 @@ export class CarRuntime {
1436
1629
  /**
1437
1630
  * Execute a proposal through a CarRuntime with a JS tool callback.
1438
1631
  * The callback receives
1439
- * `{"tool":"name","params":{...},"action_id":"<id>"}` as a JSON
1440
- * string and must return a JSON string. `action_id` is the
1632
+ * `{"tool":"name","params":{...},"action_id":"<id>","request_id":"<id>","timeout_ms":<ms|null>,"session_id":"<id>|null","attempt":<n>}`
1633
+ *
1634
+ * `attempt` is the engine's retry counter, 1-based — which retry you are
1635
+ * serving. (Correlate a specific in-flight call by `request_id` instead.) It
1636
+ * was hardcoded to 1 on the wire and dropped here before car#928.
1637
+ *
1638
+ * `session_id` is the daemon-stamped execution session (car#904) — the
1639
+ * attribution key for which mission a callback belongs to. Null when the
1640
+ * caller has no session. Prefer it over reconstructing attribution from
1641
+ * `action_id`, which is client-authored and not unique across concurrent or
1642
+ * retried attempts.
1643
+ * as a JSON string and must return a JSON string. `action_id` is the
1441
1644
  * originating `Action.id` from the proposal — useful for routing
1442
1645
  * when the same callback closes over multiple in-flight calls.
1646
+ * `request_id` is the daemon's callback-routing id, which a
1647
+ * `tools.cancel` notification repeats so the host can abort the right
1648
+ * in-flight call. `timeout_ms` is the action's declared budget in
1649
+ * milliseconds when the action declared one (`null` otherwise); the
1650
+ * host's tool runner may use it to bound its own work.
1443
1651
  *
1444
1652
  * `sessionId`, when provided, scopes per-action policy validation to
1445
1653
  * the named session opened via `CarRuntime.openSession()`. Global
@@ -1657,6 +1865,13 @@ export function registerVoiceEventHandler(
1657
1865
  * Promise is logged, not surfaced to the host. Process-wide setter,
1658
1866
  * symmetric to `registerVoiceEventHandler`; pair with
1659
1867
  * `unregisterChatHandler` to clear.
1868
+ *
1869
+ * The handler may call any runtime method and should run the turn inline.
1870
+ * NAPI dispatches through a non-blocking `ThreadsafeFunction` and `chatEvent`
1871
+ * is async, so this side never had the reentrancy hazard that made the same
1872
+ * surface unusable from Python before Parslee-ai/car#905 — noted here because
1873
+ * the two bindings' handlers now carry the same contract for the same reason,
1874
+ * arrived at differently.
1660
1875
  */
1661
1876
  export function registerChatHandler(
1662
1877
  handlerFn: (paramsJson: string) => void,
@@ -1675,11 +1890,27 @@ export function unregisterChatHandler(): void;
1675
1890
  * this handler.
1676
1891
  *
1677
1892
  * `handlerFn(callJson)` receives
1678
- * `{"tool":"name","params":{...},"action_id":"<id>"}` as a JSON
1679
- * string and MUST return a Promise resolving to the tool's
1893
+ * `{"tool":"name","params":{...},"action_id":"<id>","request_id":"<id>","timeout_ms":<ms|null>,"session_id":"<id>|null","attempt":<n>}`
1894
+ *
1895
+ * `attempt` is the engine's retry counter, 1-based — which retry you are
1896
+ * serving. (Correlate a specific in-flight call by `request_id` instead.) It
1897
+ * was hardcoded to 1 on the wire and dropped here before car#928.
1898
+ *
1899
+ * `session_id` is the daemon-stamped execution session (car#904) — the
1900
+ * attribution key for which mission a callback belongs to. Null when the
1901
+ * caller has no session. Prefer it over reconstructing attribution from
1902
+ * `action_id`, which is client-authored and not unique across concurrent or
1903
+ * retried attempts.
1904
+ * as a JSON string and MUST return a Promise resolving to the tool's
1680
1905
  * JSON-encoded result. Throwing rejects the daemon-side action
1681
1906
  * with a -32000 JSON-RPC error.
1682
1907
  *
1908
+ * `request_id` is the daemon's callback-routing id, repeated by the
1909
+ * `tools.cancel` notification so the host can abort the right
1910
+ * in-flight call. `timeout_ms` is the action's declared budget in
1911
+ * milliseconds when the action declared one (`null` otherwise); the
1912
+ * host's tool runner may use it to bound its own work.
1913
+ *
1683
1914
  * `action_id` carries the originating `Action.id` from the
1684
1915
  * proposal so process-wide handlers can route concurrent
1685
1916
  * callbacks back to the right per-call closure. Empty string when
@@ -1942,6 +2173,30 @@ export function resumeWorkflow(pausedJson: string, inputJson: string): Promise<s
1942
2173
  * resumable runs after a restart. */
1943
2174
  export function listPausedWorkflows(runsDir: string): string;
1944
2175
 
2176
+ /** Bind (or clear) the memory namespace for subsequent daemon connections
2177
+ * (car-releases#81). Returns the namespace now in effect, or `null` for the
2178
+ * daemon's shared graph.
2179
+ *
2180
+ * Prefer this over setting `CAR_MEMORY_NAMESPACE` when the namespace is
2181
+ * per-project. An env var cannot carry a per-project value: the host learns
2182
+ * which project it is *after* the process starts, and on **bun** a JS-side
2183
+ * `process.env` write never reaches the C `environ` this library reads — the
2184
+ * write is silently ignored and the session falls back to the shared graph.
2185
+ *
2186
+ * Blank or `null` clears the override, falling back to `CAR_MEMORY_NAMESPACE`
2187
+ * and then the shared graph.
2188
+ *
2189
+ * **Takes effect on the next connection.** The namespace is negotiated during
2190
+ * `session.auth`, so an already-established connection keeps the graph it
2191
+ * bound. Call this before your first CAR call, or disconnect afterwards to
2192
+ * force a rebind — otherwise you keep writing to the previous project's
2193
+ * graph. */
2194
+ export function setMemoryNamespace(namespace?: string | null): string | null;
2195
+
2196
+ /** The memory namespace currently in effect — the explicit override if set,
2197
+ * otherwise `CAR_MEMORY_NAMESPACE`, otherwise `null` (shared graph). */
2198
+ export function getMemoryNamespace(): string | null;
2199
+
1945
2200
  /** NLP (F4): identify the dominant language of `text`. Returns
1946
2201
  * `{language, backend}` JSON (Apple NaturalLanguage on macOS, pure-Rust
1947
2202
  * fallback elsewhere). */
@@ -2145,10 +2400,25 @@ export function sendA2AMessage(rt: CarRuntime, paramsJson: string): Promise<stri
2145
2400
  * `{ valid, issues, simulated_state, execution_levels, conflicts,
2146
2401
  * evidence }`. `evidence` is the verifier's declared scope (survey
2147
2402
  * "Code as Agent Harness" §5.2.2): `{ checks: [{ name, ran, verifies,
2148
- * cannot_verify, findings }], assumptions, untested_regions,
2403
+ * cannot_verify, findings, tier }], assumptions, untested_regions,
2149
2404
  * residual_risks, confidence }` — so a `valid: true` verdict can be read
2150
2405
  * with its scope (what was checked, what was not, coverage confidence)
2151
2406
  * rather than as a blanket guarantee.
2407
+ *
2408
+ * Each issue is `{ action_id, severity, message, tier }`. `tier` is the
2409
+ * **evidence tier** — `"decision_procedure" | "heuristic" | "sampled"` —
2410
+ * naming which kind of check produced the finding, so you don't have to
2411
+ * pattern-match the message to tell them apart. All of `verify`'s
2412
+ * findings are `decision_procedure` (set membership, the STRIPS-style
2413
+ * forward walk, write-conflict detection) except the
2414
+ * repeated-identical-call rule, which is `heuristic`: the count is
2415
+ * exact, "runaway loop" is a proxy, and a legitimate 3× poll trips it.
2416
+ * The tier is orthogonal to `severity` (how bad, not how derived), and
2417
+ * `decision_procedure` is not a proof or a soundness claim — it means
2418
+ * the check decides the property it reports over the inputs it was
2419
+ * given, which for the state-dependent checks is a forward model built
2420
+ * only from *declared* effects. Each `CheckRecord` carries the same
2421
+ * `tier` as the findings it contributed.
2152
2422
  */
2153
2423
  export function verify(
2154
2424
  proposalJson: string,
@@ -2652,8 +2922,13 @@ export function cascadeDecide(
2652
2922
  *
2653
2923
  * `statsJson` is aggregate counters (parsed by serde — keys are snake_case):
2654
2924
  * `{ total_facts, structured_facts, total_edges, total_retrievals,
2655
- * helpful_retrievals, conflicts_resolved, outstanding_outdated, facts_created,
2656
- * facts_superseded }` (omitted fields default to 0). Returns the report JSON:
2925
+ * total_proactive_injections, helpful_retrievals, conflicts_resolved,
2926
+ * outstanding_outdated, facts_created, facts_superseded }` (omitted fields
2927
+ * default to 0). `total_retrievals` counts DELIBERATE recalls only;
2928
+ * harness-initiated proactive injections are reported separately as
2929
+ * `total_proactive_injections` and never feed ranking (car#816).
2930
+ * `helpful_retrievals` is always 0 today — `record_fact_helpful` has no
2931
+ * production caller — so read a 0 there as "not wired", not "nothing helped". Returns the report JSON:
2657
2932
  * `{ representation_fidelity, retrieval_precision, update_correctness,
2658
2933
  * long_horizon_stability, overall }` (each 0..1), `bottleneck` (one of
2659
2934
  * `"representation" | "retrieval" | "update_correctness" |
@@ -2742,6 +3017,16 @@ export function simulateWithPredictions(
2742
3017
  * `state_consistency` (changes, snapshots, rollbacks), `safety`
2743
3018
  * (permission escalations/denials/approvals), and `replayability` — to
2744
3019
  * complement task-success accuracy when comparing harness variants.
3020
+ *
3021
+ * The optional `task_pass_rate` field — and its two companions,
3022
+ * `task_pass_denominator` (how many tasks that rate is over) and
3023
+ * `tasks_unrunnable` (how many the runner could not measure) — are **always
3024
+ * absent** from this result: end-task success is not in the event stream (the
3025
+ * log records what ran, not whether the task was satisfied), and inventing any
3026
+ * of the three here would hand the regression gate a fabricated number. Only a
3027
+ * runner holding the task suite and its grading criteria can supply them —
3028
+ * `car-bench-harness --metrics-out` does. Absent means *not measured*, never
3029
+ * zero.
2745
3030
  */
2746
3031
  export function harnessMetrics(eventsJsonl: string): string;
2747
3032
 
@@ -2817,9 +3102,30 @@ export function evolutionDiagnose(
2817
3102
  * `HarnessMutation`; `baselineJson`/`candidateJson` are `HarnessMetrics`
2818
3103
  * measured before/after applying it on held-out telemetry. Returns the
2819
3104
  * `PromotionDecision` JSON `{ decision: "promote" | "needs_approval" |
2820
- * "reject", reason }` — a mutation is promoted only if its target improved
2821
- * without regressing guarded metrics; safety-affecting mutations route to
2822
- * `needs_approval` even when they pass.
3105
+ * "reject" | "incomparable", reason }` — a mutation is promoted only if its
3106
+ * target improved without regressing guarded metrics; safety-affecting
3107
+ * mutations route to `needs_approval` even when they pass.
3108
+ *
3109
+ * Reliability is guarded twice, because the two available measures are
3110
+ * different quantities. `task_pass_rate` is end-task success and is checked
3111
+ * first, but only when BOTH documents carry it (absent on either side = not
3112
+ * measured, so the guard does not fire rather than defaulting to 0.0 or 1.0).
3113
+ * `trajectory_efficiency.success_rate` is tool-attempt success and is always
3114
+ * checked. A candidate that cuts tokens by abandoning hard tasks earlier holds
3115
+ * a perfect attempt-level rate while solving fewer tasks — only the first guard
3116
+ * sees that.
3117
+ *
3118
+ * Before either guard, the two pass rates must be over the SAME task set.
3119
+ * `HarnessMetrics` carries two optional companions to `task_pass_rate`:
3120
+ * `task_pass_denominator` (how many tasks the rate is over) and
3121
+ * `tasks_unrunnable` (how many the runner could not measure). When both
3122
+ * documents carry a denominator and they differ, the result is
3123
+ * `incomparable` — no verdict, nothing applied. That is not pedantry: a
3124
+ * harness that loses a capability also loses the ability to *measure* the
3125
+ * tasks needing it, so those tasks leave the denominator and the surviving
3126
+ * rate goes up. Both fields are optional and absent from documents written
3127
+ * before they existed, in which case the check is skipped rather than
3128
+ * failing.
2823
3129
  */
2824
3130
  export function evolutionEvaluate(
2825
3131
  mutationJson: string,
@@ -2849,11 +3155,76 @@ export function evolutionApply(
2849
3155
  // (read_only | sandbox_edit | full_access), gate it against the session's
2850
3156
  // granted standing tier, and record human-in-the-loop approvals as durable,
2851
3157
  // auditable state (a JSONL ledger keyed by a stable action fingerprint).
2852
-
2853
- /**
2854
- * Classify each action in a proposal into its minimum required permission
2855
- * tier. Returns a JSON array of `{ action_id, tool, required_tier }` where
2856
- * `required_tier` is `"read_only" | "sandbox_edit" | "full_access"`.
3158
+ //
3159
+ // Two axes, not one. The tier answers "who may authorize this?" and says
3160
+ // nothing about whether the effect can be undone a `git push`, a production
3161
+ // INSERT, and a charged card are all `full_access` with three different
3162
+ // rollback contracts. Every row below therefore carries a `reversibility`
3163
+ // alongside its `required_tier`.
3164
+ //
3165
+ // The matching Action IR fields (any `proposalJson` this package accepts, and
3166
+ // the full spec in docs/agent-ir-spec.md):
3167
+ //
3168
+ // "reversibility": "reversible" | "compensable" | "irreversible"
3169
+ // Optional. The rollback contract for this action's effects.
3170
+ // `reversible` is undone by restoring the scope it ran in;
3171
+ // `compensable` needs a compensating action run against it;
3172
+ // `irreversible` cannot be undone once it reaches the world.
3173
+ // **Defaults to "irreversible"** when omitted — deliberately, because
3174
+ // the default decides what the runtime believes about an unclassified
3175
+ // action and the two directions fail asymmetrically. Guessing
3176
+ // "reversible" wrongly means silently believing a sent email can be
3177
+ // unsent; guessing "irreversible" wrongly means over-asking for an
3178
+ // approval on something recoverable, which is annoying, visible, and
3179
+ // fixed locally by annotating the action.
3180
+ //
3181
+ // "compensation": { "type": "tool", "tool": string, "parameters"?: object }
3182
+ // | { "type": "action_ref", "action_id": string }
3183
+ // Optional. How to undo the action once it has run — the action-level
3184
+ // analogue of car-workflow's saga CompensationHandler. Meaningful only
3185
+ // with `"reversibility": "compensable"`; omitted from the serialized
3186
+ // form when absent, so older consumers see the payload they saw before.
3187
+ // Declaring one is a *claim*: nothing checks that the named tool is a
3188
+ // true inverse, exactly as nothing checks `expected_effects`.
3189
+ //
3190
+ // Nothing in the runtime enforces on either field yet — they are typed,
3191
+ // classified, and audited. Do not read `"reversible"` as a promise that the
3192
+ // runtime will undo anything for you: rollback restores the state map and
3193
+ // leaves whatever a tool wrote to disk where it is. See
3194
+ // docs/proposals/shepherd-substrate-adoption.md.
3195
+
3196
+ /**
3197
+ * Classify each action in a proposal on both authorization-adjacent axes.
3198
+ * Returns a JSON array of `{ action_id, tool, required_tier, reversibility,
3199
+ * missing_compensation }`. The keys live inside a JSON string, so they stay
3200
+ * snake_case — napi-rs camelCases function and parameter names, not payload
3201
+ * contents.
3202
+ *
3203
+ * - `required_tier` — **who may authorize this**: `"read_only" |
3204
+ * "sandbox_edit" | "full_access"`.
3205
+ * - `reversibility` — **can this be undone**: `"reversible" | "compensable" |
3206
+ * "irreversible"`. Independent of the tier and not derived from it:
3207
+ * `read_secret` is `full_access` and perfectly reversible (a read leaves
3208
+ * nothing to undo), while `send_email` and `deploy_service` are both
3209
+ * `full_access` and differ completely. This is the classifier's answer from
3210
+ * the tool name and flattened parameters — **not** an echo of the action's
3211
+ * declared `reversibility` field, which defaults to `"irreversible"` and so
3212
+ * would tell you only what you sent. It is a keyword heuristic: an
3213
+ * unrecognized tool comes back `"irreversible"`, deliberately.
3214
+ * - `missing_compensation` — the action **declared** `"compensable"` and
3215
+ * supplied no `compensation`, the one incoherent combination the IR cannot
3216
+ * exclude by construction. Keyed off the declared field, so it stays `false`
3217
+ * for proposals that never opted into the axis.
3218
+ *
3219
+ * Severity ascends `"reversible" < "compensable" < "irreversible"`, so the
3220
+ * rollback contract of a whole batch is the worst row — a plan is only as
3221
+ * recoverable as its least recoverable step. (The daemon's `permission.classify`
3222
+ * returns that roll-up precomputed as `declared_rollback_contract`, over the
3223
+ * *declared* fields; this function returns a bare array with nowhere to hang
3224
+ * it, so compute it from the column you care about.)
3225
+ *
3226
+ * Nothing in the runtime gates on the second axis yet — it is typed,
3227
+ * classified, and audited, not enforced.
2857
3228
  */
2858
3229
  export function permissionClassify(proposalJson: string): string;
2859
3230
 
@@ -2861,10 +3232,17 @@ export function permissionClassify(proposalJson: string): string;
2861
3232
  * Evaluate each action against a granted standing tier, consulting the
2862
3233
  * durable approval ledger JSONL at `ledgerPath` when supplied. Returns a
2863
3234
  * JSON array of per-action decisions, each `{ decision, required, granted,
2864
- * action_id, fingerprint, ... }` where `decision` is `"allow" |
3235
+ * action_id, fingerprint, reversibility, ... }` where `decision` is `"allow" |
2865
3236
  * "needs_approval" | "deny"`. A `needs_approval` decision means autonomy is
2866
3237
  * suspended pending a human decision; resolve it with
2867
3238
  * {@link permissionRecordForFingerprint}.
3239
+ *
3240
+ * `reversibility` is orthogonal to `decision` and rides on **every** row, the
3241
+ * `allow`s included: the gate's verdict says whether the action may run, not
3242
+ * whether it could be taken back afterwards, and a caller that only learns the
3243
+ * rollback contract of the actions it was stopped on is missing exactly the
3244
+ * rows an incident review reads first. Same field, labels, and classifier as
3245
+ * the `PermissionDecision` event the engine writes to its audit log.
2868
3246
  */
2869
3247
  export function permissionEvaluate(
2870
3248
  proposalJson: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "car-runtime",
3
- "version": "0.46.1",
3
+ "version": "0.48.0",
4
4
  "description": "Common Agent Runtime — a deterministic execution layer for AI agents",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",