car-runtime 0.51.0 → 0.52.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.
package/README.md CHANGED
@@ -74,8 +74,15 @@ const result = await executeProposal(rt, proposal, async (callJson) => {
74
74
  });
75
75
  ```
76
76
 
77
- Full API reference lives in [`index.d.ts`](./index.d.ts). High-level docs and
78
- examples: https://github.com/Parslee-ai/car-releases
77
+ Full API reference lives in [`index.d.ts`](./index.d.ts). The package also
78
+ ships a `docs/` directory (`node_modules/car-runtime/docs/`) with prose
79
+ reference docs — `SPEC.md`, `GUIDE.md`, `CLI.md`, `ASSISTANT.md`, `MCP.md`,
80
+ `agent-ir-spec.md` — so an agent with this package installed and no network
81
+ access can still read how the `car` CLI, the flagship assistant's tools, and
82
+ `car-mcp` actually work. `docs/websocket-protocol.md` is a short pointer, not
83
+ the full reference (~510KB, deliberately not bundled) — read the real thing at
84
+ https://car.parslee.ai/websocket-protocol.md. High-level docs and examples
85
+ online: https://github.com/Parslee-ai/car-releases
79
86
 
80
87
  ## Environment variables
81
88
 
@@ -0,0 +1,491 @@
1
+ # Parslee Core — the `car do` assistant
2
+
3
+ > Describes CAR **{{VERSION}}**. Check yours with `car --version`; if it
4
+ > differs, prefer `car help <command>` on your own binary over this page.
5
+
6
+ CAR ships with a general-purpose agent that works out of the box: no tools to
7
+ register, no proposal schema to write. Point it at a task and it uses files, a
8
+ real shell, the web, image/speech/music/video generation, browser control,
9
+ Microsoft 365, and a durable memory graph to get it done.
10
+
11
+ ```
12
+ car do "generate a 20-second commercial for a cold-brew coffee brand called Wanderlust Roasters, with a voiceover and music"
13
+ ```
14
+
15
+ This is a different thing from the `CarRuntime` / `register_tool` / proposal
16
+ pattern in [SPEC.md](./SPEC.md) and [GUIDE.md](./GUIDE.md). Those docs are for
17
+ *building an agent on CAR* — you supply the tools, CAR supplies the DAG
18
+ executor, validator, and policy engine. `car do` is a finished agent CAR
19
+ already built that way: its tools are `car-server-core`'s own
20
+ `GeneralExecutor` plus a set of host-side delegates, driven through the same
21
+ `Runtime` (validator → policy → permission tiers → event log) any embedder
22
+ gets. If you want to write your own tools, read SPEC/GUIDE. If you want an
23
+ agent that already has real ones, this is it.
24
+
25
+ ## Three ways to run it
26
+
27
+ ```
28
+ car do "<goal>" # one-shot: run to a terminal outcome, print it, exit
29
+ car do # interactive REPL — multi-turn, in one process
30
+ car do --serve # supervised, conversational agent for CarHost / agents.chat
31
+ ```
32
+
33
+ `car do --serve` is what CarHost auto-starts: on daemon boot it registers
34
+ itself as agent id `parslee-core` (`car-assistant` is kept as a compatibility
35
+ alias for older clients) with `auto_start: true`, so a fresh CAR install has a
36
+ working conversational agent with no separate install step. Nothing else in
37
+ this document changes between the three modes — they share one system prompt,
38
+ one tool set, and one loop; they differ only in how a turn starts and how
39
+ output is delivered.
40
+
41
+ Full flag reference (`--local`, `--full-access`, `--until`, `--json`, …) is in
42
+ [CLI.md](./CLI.md#car-do).
43
+
44
+ ## Execution posture: sandboxed by default
45
+
46
+ Unless you pass `--local`, `car do` binds a hardened Docker sandbox before
47
+ running anything:
48
+
49
+ - Container image `python:3.11` by default (override with `--image`) — the
50
+ full (non-slim) image, so git/gcc/make/curl are present without network
51
+ access to fetch them.
52
+ - `--network none`: file writes and shell execute **inside** the container;
53
+ network tools (`http_request`, `web_search`, browser control) run from the
54
+ **host**, because the container can't reach the network at all.
55
+ - If Docker isn't available, `car do` does not silently drop the sandbox — it
56
+ falls back to the local host with every write and shell call gated behind
57
+ human approval (see below), and tells the model why in the environment
58
+ description.
59
+ - `--local` skips the sandbox and runs directly against your filesystem.
60
+ `-y` / `--full-access` lifts the local-host approval gate.
61
+
62
+ Either way, execution goes through the runtime's admission gates before
63
+ anything runs: a **static verification gate** (tool exists, parameters are
64
+ well-formed) and, by default, an **information-flow gate** that blocks
65
+ confidential local data (files, recalled memories) from reaching an outbound
66
+ tool like `web_search` in the same proposal — CAR calls this out explicitly
67
+ because assistants that let persistent, high-privilege context flow straight
68
+ into outbound tools are a known failure class.
69
+
70
+ ## What it can actually do
71
+
72
+ Every capability below is a real tool the model can call — read the parameter
73
+ names straight out of the source, not paraphrased. Several groups are
74
+ **conditionally advertised**: a tool never appears in the model's tool list
75
+ (and so never appears in the system prompt, since the prompt doesn't
76
+ re-enumerate tools — it points the model at the defs) on a host that can't run
77
+ it. That's deliberate: a tool that would only ever return an error is worse
78
+ than no tool.
79
+
80
+ ### Files, shell, and planning — always available
81
+
82
+ | Tool | Parameters |
83
+ |---|---|
84
+ | `read_file` | `path`, `offset?`, `limit?` |
85
+ | `list_dir` | `path` |
86
+ | `find_files` | `pattern` (glob, `**` spans directories), `path?` (default `.`), `max_results?` (default 1000) |
87
+ | `grep_files` | `pattern` (regex), `path?` (default `.`), `max_results?` (default 50) |
88
+ | `write_file` | `path`, `content`, `append?` — overwriting or appending to an existing file requires you to have read its full current content earlier in the session (read-before-write guard); creating a new file needs no prior read |
89
+ | `edit_file` | `path`, `old_text`, `new_text`, `replace_all?` — same read-before-edit guard; `old_text` must match exactly one place unless `replace_all: true` |
90
+ | `calculate` | `expression` (`^` for exponentiation, plus `+ - * / %`, parentheses, `sqrt`/`sin`/`ln`/…) — pure, no substrate access |
91
+ | `shell` | `command`, `timeout_secs?` (default 120, max 600) |
92
+ | `todo_write` | `items: [{text, status?: "open"\|"done"\|"dropped"}]` — replaces the whole checklist each call; it's live state that survives history compaction, not a transcript entry |
93
+ | `events_query` | `kinds?`, `action_id?`, `limit?` — queries this run's own append-only event log: what you already tried, what failed. Advertised whenever a run has a bound event log (every `car do` invocation) |
94
+
95
+ `write_file`, `edit_file`, and `shell` auto-run inside the sandbox or under
96
+ `--full-access`; on the local host without `--full-access` they need approval
97
+ (see [Approvals](#approvals-what-it-may-do-without-asking)).
98
+
99
+ ### Web and delegated work — approval-gated by default
100
+
101
+ | Tool | Parameters |
102
+ |---|---|
103
+ | `http_request` | `url`, `method?`, `headers?`, `body?`, `timeout_secs?` (default 30, max 120) |
104
+ | `web_search` | `query`, `max_results?` |
105
+ | `m365_task` | `task`, `conversation_id?` — delegates to the user's Microsoft 365 Parslee AI Employee (email, calendar, contacts, HubSpot CRM, meetings) over one chat call. Mutating actions (send mail, create an event) come back **drafted for approval**, never executed silently. Advertised only with a Parslee session (`car auth login`) |
106
+
107
+ These self-declare `"tier": "full_access"`, so — unlike file/shell — they need
108
+ approval even *inside* the sandbox unless the session was started with
109
+ `--full-access`. Network egress crosses the sandbox boundary; the container
110
+ walls it off from the filesystem, but not from the approval gate.
111
+
112
+ ### Memory
113
+
114
+ | Tool | Parameters |
115
+ |---|---|
116
+ | `remember` | `subject`, `body`, `kind?: "fact"\|"preference"\|"procedure"` (full_access, gated) |
117
+ | `recall` | `query` (no declared tier — never gated) |
118
+
119
+ See [Memory](#memory-what-actually-persists) below for what this backs and
120
+ what it explicitly does not.
121
+
122
+ ### Creative and media generation
123
+
124
+ The capability a text-only coding agent structurally does not have. Two tiers:
125
+ local models (fast, always-available-if-installed) and Parslee Studio
126
+ (slower, needs `car auth login`, materially higher quality on some axes).
127
+
128
+ **Local (`sandbox_edit` tier — no approval needed once the run has edit
129
+ permission), advertised only when the matching local model is installed:**
130
+
131
+ | Tool | Parameters |
132
+ |---|---|
133
+ | `generate_image` | `prompt`, `output_path?`, `width?` (≤1024, default 768), `height?` (≤1024, default 512), `seed?` |
134
+ | `generate_speech` | `text`, `output_path?`, `voice?` |
135
+
136
+ **Parslee Studio (`full_access` tier), advertised only with a Parslee
137
+ session:**
138
+
139
+ | Tool | Parameters |
140
+ |---|---|
141
+ | `generate_music` | `prompt`, `duration_seconds?` (5–300, default 30), `output_path?` — ElevenLabs Music, instrumental/ambience |
142
+ | `generate_jingle` | `brand_name`, `style?`, `tagline?`, `output_path?` — short sonic branding |
143
+ | `generate_studio_image` | `prompt`, `aspect_ratio?` (default "16:9"), `quality?: "low"\|"medium"\|"high"`, `output_path?` — gpt-image-2; reliably renders legible in-image text where the local generator doesn't |
144
+ | `generate_song` | `prompt`, `duration_seconds?` (30–480, default 60), `style?`, `lyrics?`, `instrumental?`, `title?`, `output_path?` — Suno, full song with vocals, up to 8 minutes |
145
+ | `list_voices` | — lists Studio's stock voices plus any this org has cloned from real people; call before `generate_voiceover` when a specific voice matters |
146
+ | `generate_voiceover` | `text`, `voice?` (name or id — matched against `list_voices`), `rate?` (e.g. `"-10%"`; **omitting it is not neutral** — the tool description documents measured wpm at each setting and recommends `"-10%"` for narration), `output_path?` |
147
+ | `generate_video` | `prompt`, `image_path?` (omit for text-to-video, supply to animate a still), `duration_seconds?` (default 5), `provider?: "veo"\|"kling"\|"ltx"`, `output_path?` — image-to-video is a diffusion repaint of every frame, so it warps fine text/UI in the source image; the tool description warns against animating text-heavy slides with it |
148
+ | `produce_commercial` | `brief`, `duration_seconds?` (8–60, default 20), `voiceover_script?`, `voiceover_voice?` (default `"brian"`), `music?` (default true), `output_path?` — plans shots, generates keyframes/video, adds voiceover and a music bed, and assembles a finished MP4. Slow (real productions run up to ~25 minutes) |
149
+
150
+ Every generator returns a **file path under the working directory**, never
151
+ inline bytes — the artifact contract exists so a producer's output survives
152
+ the loop's observation-size cap; embed or read the path like any other file.
153
+
154
+ ### Vision — reading images back
155
+
156
+ The consumer counterpart to the generators. `read_image_text` (OCR) runs via
157
+ the Apple Vision shim on macOS with a Tesseract fallback elsewhere;
158
+ `classify_image` runs via Apple Vision on macOS and a bundled MobileNetV2
159
+ model elsewhere (auto-downloaded and cached on first use off-macOS). Both are
160
+ read-only; each is advertised whenever its own backend is present.
161
+
162
+ | Tool | Parameters |
163
+ |---|---|
164
+ | `read_image_text` | `image_path` — OCR: screenshots, scans, photographed documents, or a generated image containing words |
165
+ | `classify_image` | `image_path`, `top_k?` (default 5, max 20) — ranked content labels with confidence |
166
+
167
+ ### Browser control and recording — `full_access` tier
168
+
169
+ Chromium launches lazily on first use, so a session that never browses pays
170
+ nothing for it.
171
+
172
+ | Tool | Parameters |
173
+ |---|---|
174
+ | `browse_navigate` | `url` |
175
+ | `browse_click` | `element_id` (accessibility node id, e.g. `"el_5"`) |
176
+ | `browse_type` | `element_id`, `text` |
177
+ | `browse_scroll` | `delta_y` |
178
+ | `browse_keypress` | `key`, `modifiers?: ["shift"\|"control"\|"alt"\|"meta"]` |
179
+ | `browse_wait` | `condition` (`"page_loaded"` or `"url_changed"`), `timeout_ms?` (default 5000) |
180
+ | `browse_observe` | `include_screenshot?`, `ocr?` — screenshot + accessibility tree + a fused `ui_map`; OCR recovers labels a polished SPA's accessibility tree misses |
181
+ | `browser_await_answer` | `timeout_seconds?` (default 45) — blocks until a page stops changing after you submit something, so you don't screenshot mid-load |
182
+ | `browser_await_signin` | `url?`, `success_url_contains?`, `timeout_seconds?` (default 300, max 1800) — asks the human to complete a sign-in the agent can't (SSO, MFA); the session persists afterward |
183
+ | `browser_record_start` | `quality?` (JPEG 1–100, default 80) |
184
+ | `browser_record_stop` | `output_path?` — writes an MP4 (requires ffmpeg), captures only frames where the page actually changed |
185
+
186
+ ### Desktop automation — `full_access` tier, one platform-native tool
187
+
188
+ Cannot be sandboxed by construction — it drives the real host GUI — so it
189
+ self-declares `full_access` and is approval-gated by the same tier rule as
190
+ `web_search`/browser control/`m365_task` (see
191
+ [Approvals](#approvals-what-it-may-do-without-asking)): gated unless the
192
+ session was started with `--full-access`.
193
+
194
+ | Platform | Tool | Parameters |
195
+ |---|---|---|
196
+ | macOS | `run_applescript` | `script`, `language?: "applescript"\|"javascript"` (JXA preferred — models generate it more cleanly) |
197
+ | Windows | `run_powershell` | `script` |
198
+
199
+ ### Linked devices
200
+
201
+ | Tool | Parameters |
202
+ |---|---|
203
+ | `linked_devices` | — (read-only, never gated) lists the user's linked CAR host devices and what they advertise (chat, approvals, notifications) |
204
+ | `notify_linked_device` | `device_id?`, `title`, `body` (full_access, gated) — a title/body push, nothing more; no contacts/location/photos/microphone access |
205
+
206
+ ### Identity
207
+
208
+ | Tool | Parameters |
209
+ |---|---|
210
+ | `set_assistant_name` | `name`, `spellings?` (alternate speech-to-text spellings), `user_name?` |
211
+
212
+ `set_assistant_name` is gated on **every** session regardless of tier —
213
+ `--full-access` does not exempt it. Every other gate in this document is about
214
+ what the *session* is allowed to do; this one is about where the instruction
215
+ could have come from. A rename request can arrive inside a fetched web page,
216
+ a file, or a recalled memory, and an assistant that silently starts answering
217
+ to a name it read somewhere is an identity-spoof surface — one approval tap is
218
+ the cheaper mistake. See [Identity](#identity-1) below.
219
+
220
+ ## Approvals: what it may do without asking
221
+
222
+ Two independent axes decide whether a tool call runs immediately or stops for
223
+ a human:
224
+
225
+ **1. The environment tier**, set once per run:
226
+
227
+ | Environment | Standing tier | Meaning |
228
+ |---|---|---|
229
+ | Sandbox (default) | `SandboxEdit` | Writes/shell inside the container auto-run; nothing crosses the container boundary without approval |
230
+ | Sandbox, `--full-access` | `FullAccess` | Everything auto-runs, including network/browser/automation |
231
+ | Local host (`--local`) | `ReadOnly` | Even `write_file`/`edit_file`/`shell` need approval |
232
+ | Local host, `--full-access` | `FullAccess` | Everything auto-runs |
233
+ | Docker unavailable (auto fallback) | `ReadOnly` | Same as `--local` without it — never a silent unsandboxed run |
234
+
235
+ **2. Each tool's self-declared `tier`** in its schema (`sandbox_edit` or
236
+ `full_access`; a tool with no `tier` field is never gated by this rule). A
237
+ tool is routed to approval whenever its declared tier **exceeds** the run's
238
+ standing tier. This is why `web_search`, the browser tools, `run_applescript`
239
+ / `run_powershell`, `m365_task`, the Studio media tools, `remember`, and
240
+ `notify_linked_device` all stop for approval inside the *default sandboxed
241
+ run* — they declare `full_access`, and the sandbox's standing tier is only
242
+ `SandboxEdit`. The local generators (`generate_image`, `generate_speech`)
243
+ declare `sandbox_edit`, so they run without asking inside the sandbox but are
244
+ gated on an unelevated local run.
245
+
246
+ **How the human is asked** depends on the surface:
247
+
248
+ - One-shot / REPL (`car do "<goal>"` or `car do`): a terminal prompt —
249
+ `⚠ Approve <tool>(<brief>)? [y/N]` — reading a single line from stdin.
250
+ Anything but `y` is a denial with reason `"declined by user"`.
251
+ - `car do --serve` / `agents.chat`: routed through the chat surface's
252
+ `approval_pending` → park → resolve flow. A connected host can present that
253
+ same pending approval as a reviewable control rather than a plain
254
+ yes/no modal — CarHost and the mobile apps do this — but the wire contract
255
+ `car do` itself guarantees is the `approval_pending` event; how a given host
256
+ renders it is that host's decision, not the assistant's.
257
+
258
+ **Changing the default posture** — the CLI counterpart to CarHost's
259
+ **Approvals** settings screen:
260
+
261
+ ```
262
+ car approvals get # show the current default posture
263
+ car approvals default cautious # ask before any edit or full-access action
264
+ car approvals default balanced # allow sandboxed edits without asking
265
+ car approvals default trusting # allow everything without asking
266
+ ```
267
+
268
+ This proxies the daemon's `agent_permissions.*` JSON-RPC surface: it moves the
269
+ **default** posture applied to every agent that has no explicit per-agent
270
+ override, so setting it once changes the fallback for every agent the daemon
271
+ supervises, not just one `car do` invocation. The three presets are the only
272
+ accepted values; anything else is rejected before it reaches the daemon.
273
+
274
+ ## Memory: what actually persists
275
+
276
+ `recall` and `remember` are backed by CAR's graph memory engine
277
+ (`car-memgine`) — not a bespoke store built for the assistant. It's the same
278
+ durable note store `car-mcp` reads and writes (with a two-writer caveat if you
279
+ run both at once — see [MCP.md's memory-durability
280
+ section](./MCP.md#memory-durability--read-this-before-relying-on-it)).
281
+
282
+ - **What persists**: durable *facts*, written one at a time by `remember`
283
+ (`subject`, `body`, `kind`), stored as flat JSON notes at
284
+ `<CAR_HOME>/memory/assistant.json` (`~/.car/memory/assistant.json` by
285
+ default) and re-ingested into a fresh in-memory graph every time the
286
+ assistant opens. A `remember` call with a subject that already exists
287
+ **replaces** that note (case-insensitive match) rather than accumulating
288
+ duplicates. `recall` queries that graph and returns a relevance-scored
289
+ context string, not a raw list.
290
+ - **What does not persist**: the *conversation itself*. There is no
291
+ disk-backed transcript store behind `car do` — a prior, unrelated
292
+ `ConversationStore` mechanism was removed from CAR entirely, and nothing
293
+ replaced it as a session-replay feature. What "the assistant remembers the
294
+ conversation" cashes out to in practice is: whatever the model chose to
295
+ write with `remember` during that conversation, as a fact — not a replay of
296
+ what was said. Don't tell a user their chat history itself survives a
297
+ restart; only the facts it explicitly saved do.
298
+ - **Cross-device**: only on `car do --serve` (the daemon-attached path). There,
299
+ every `remember` is additionally mirrored into the daemon's synced knowledge
300
+ oplog, and `recall` pulls in facts a *different* device wrote for subjects
301
+ the local store doesn't already hold (local notes win on a subject both
302
+ devices have — this is deliberately conservative, not last-write-wins). A
303
+ one-shot `car do "<goal>"` run has no sync sink attached, so its `remember`
304
+ calls stay local to that machine.
305
+ - **`kind` changes retrieval behavior, not just categorization**: a `"fact"`
306
+ is retrieved when relevant to a query; a `"preference"` is surfaced in
307
+ *every* future session (a standing instruction, not a recall hit) — reserve
308
+ it for rules that should never be violated; a `"procedure"` records how a
309
+ task was done and whether it worked, for reuse.
310
+
311
+ ## Identity
312
+
313
+ The product is **Parslee Core** — that name is fixed (it's what the product
314
+ is called in store copy) and never changes. What the user calls the agent day
315
+ to day is a nickname layered on top, defaulting to **Parslee** (so an install
316
+ that never sets a name still answers to something short and won't confuse
317
+ itself with the brand string in casual conversation). The agent is told in
318
+ its system prompt to answer to either without correcting the user.
319
+
320
+ ```
321
+ car identity show # current name, spoken forms, where the record lives
322
+ car identity set Jarvis --also-hear jervis # rename it; --also-hear registers STT mis-hearings
323
+ car identity set-user Dana # tell it what to call YOU
324
+ ```
325
+
326
+ The record lives at `<CAR_HOME>/identity.json` — one file feeding the system
327
+ prompt, the voice wake-word matcher, and every host's addressing copy, so
328
+ `car identity show` reports the same name CarHost displays and the voice
329
+ pipeline wakes on. Saying "sure, I'll go by Friday" in conversation changes
330
+ nothing by itself: the model is instructed to call the gated
331
+ `set_assistant_name` tool (approval required — see above) to make a rename
332
+ actually take effect for the next session and the wake word; agreeing in
333
+ prose alone is explicitly called out in the prompt as *not* persisting
334
+ anything.
335
+
336
+ ## Worked examples
337
+
338
+ These are **illustrative** — derived directly from the tool schemas and loop
339
+ behavior above, not captured output from an actual run.
340
+
341
+ ### 1. A capability a text-only coding agent doesn't have
342
+
343
+ ```
344
+ car do "Make a 20-second commercial for a cold-brew coffee brand called \
345
+ Wanderlust Roasters — warm, adventurous tone, with a voiceover and \
346
+ background music."
347
+ ```
348
+
349
+ Given a Parslee session, the model has `produce_commercial` in its tool list
350
+ and (per its schema) would call something like:
351
+
352
+ ```json
353
+ {"tool": "produce_commercial", "parameters": {
354
+ "brief": "Wanderlust Roasters cold brew — warm, adventurous, morning-ritual tone",
355
+ "duration_seconds": 20,
356
+ "music": true
357
+ }}
358
+ ```
359
+
360
+ `produce_commercial` self-declares `full_access`, so on a sandboxed run
361
+ without `--full-access` this call pauses for approval first. Once approved,
362
+ Studio plans shots, generates keyframes and video, adds a voiceover and a
363
+ music bed, and the tool result is an MP4 path under the working directory —
364
+ which the agent then references in its final summary rather than describing
365
+ in prose.
366
+
367
+ ### 2. Approval prompt on the local host
368
+
369
+ ```
370
+ car do --local "delete every file in this directory older than 30 days"
371
+ ```
372
+
373
+ `shell` (or `write_file`) is not `--full-access`, so before running the
374
+ deleting command the terminal shows:
375
+
376
+ ```
377
+ ⚠ Approve shell(find . -mtime +30 -delete)? [y/N]
378
+ ```
379
+
380
+ Typing anything but `y` denies the call with `"declined by user"`, and the
381
+ model is told the boundary — the prompt instructs it not to retry the exact
382
+ same call, but to explain the limit and offer an alternative.
383
+
384
+ ### 3. Machine-readable output (`--json`)
385
+
386
+ ```
387
+ car do --json "summarize the CSV files in this directory"
388
+ ```
389
+
390
+ emits exactly one JSON document on stdout (`schema: "car.do/1"`) — progress
391
+ goes to stderr as JSONL instead of human-readable text. The shape, per the
392
+ emitter that builds it:
393
+
394
+ ```json
395
+ {
396
+ "schema": "car.do/1",
397
+ "status": "success",
398
+ "summary": "…",
399
+ "turns": 5,
400
+ "delegations": 0,
401
+ "model_used": "anthropic/claude-haiku-4-5:latest",
402
+ "receipts": {
403
+ "total": 11,
404
+ "failed": 0,
405
+ "by_tool": {"list_dir": 1, "read_file": 6, "grep_files": 4},
406
+ "sample": [
407
+ {"tool": "list_dir", "ok": true, "brief": "."},
408
+ {"tool": "read_file", "ok": true, "brief": "prices.csv"},
409
+ {"tool": "read_file", "ok": true, "brief": "orders.csv"},
410
+ {"tool": "read_file", "ok": true, "brief": "notes.csv"},
411
+ {"tool": "read_file", "ok": true, "brief": "invoices.csv"},
412
+ {"tool": "read_file", "ok": true, "brief": "returns.csv"},
413
+ {"tool": "read_file", "ok": true, "brief": "summary.csv"},
414
+ {"tool": "grep_files", "ok": true, "brief": "prices.csv"}
415
+ ],
416
+ "sample_omitted": 3
417
+ },
418
+ "ungrounded_claims": [],
419
+ "sandbox": {
420
+ "mode": "docker",
421
+ "image": "python:3.11",
422
+ "network": "none",
423
+ "tier": "sandbox_edit",
424
+ "root": "/work",
425
+ "fallback_notice": null
426
+ },
427
+ "elapsed_seconds": 12.4
428
+ }
429
+ ```
430
+
431
+ `sample` is capped at 8 receipts (failures first, then successes) regardless of
432
+ how many tool calls the run actually made; `sample_omitted` says how many were
433
+ left out of that cap so the array never silently reads as the complete list.
434
+
435
+ `ungrounded_claims` is the mechanism behind "receipts decide completion, not
436
+ the prose": before the run is reported as finished, operational claims in the
437
+ model's own summary ("I ran the tests", "the file is updated") are
438
+ cross-checked against the actual tool receipts from *this* run. An unmatched
439
+ claim is not silently trusted — a deterministic pass (e.g. `--until` goal
440
+ mode) only annotates the reply with a `[claim check]` note, while a
441
+ model-judged completion is failed closed on an unmatched claim. This array is
442
+ empty here because nothing in the summary asserted anything the receipts
443
+ didn't back up.
444
+
445
+ ### The failure shape
446
+
447
+ The block above is a **successful** run. A failed one keeps the same
448
+ `schema` and swaps in an error triple — parse `status` first, not the presence
449
+ of `summary`, which a failed run does not carry:
450
+
451
+ ```json
452
+ {
453
+ "schema": "car.do/1",
454
+ "status": "error",
455
+ "error": "AssistantLoopFailed",
456
+ "message": "inference failed: Not enough memory is free to start this model while preserving CAR's 6553 MB emergency reserve. Close memory-heavy apps or choose a smaller model.",
457
+ "turns": 1,
458
+ "model_used": "",
459
+ "elapsed_seconds": 0.007832208,
460
+ "suggestions": [
461
+ "Re-run the goal; the run failed mid-loop rather than completing with an answer.",
462
+ "Check `receipts` for what had already executed before the failure."
463
+ ],
464
+ "receipts": {"total": 0, "failed": 0, "by_tool": {}, "sample": [], "sample_omitted": 0}
465
+ }
466
+ ```
467
+
468
+ - `status` is `"success"` or `"error"`. Branch on it.
469
+ - `error` is a stable machine-readable kind (e.g. `AssistantLoopFailed`);
470
+ `message` is the human-readable detail and is NOT stable enough to match on.
471
+ - `receipts` is always present, including on failure, and is how you see what
472
+ had already run before the failure — a partially-completed run reports the
473
+ tools it did execute.
474
+ - `model_used` is empty when the run failed before a model was selected.
475
+ - The process exits non-zero on `"error"`, so an exit-code check and a
476
+ `status` check agree; the JSON is still emitted.
477
+
478
+ The exit status and the JSON both being authoritative matters for scripting:
479
+ `car do --json` writes exactly one document to stdout either way, with progress
480
+ as JSONL on stderr, so redirecting stdout to a file gives a parseable result
481
+ even on failure.
482
+
483
+ ## Where this fits with the rest of CAR
484
+
485
+ - Building your *own* agent with your *own* tools — [SPEC.md](./SPEC.md) /
486
+ [GUIDE.md](./GUIDE.md).
487
+ - Every CLI subcommand, including the rest of `car do`'s flags and every other
488
+ top-level command — [CLI.md](./CLI.md).
489
+ - Wiring CAR (including a poll-based handle onto this same assistant,
490
+ `assistant_start`/`assistant_poll`) into Claude Code, Cursor, or another
491
+ MCP host — [MCP.md](./MCP.md).