pi-supernova 0.10.0 → 0.10.2

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
@@ -12,161 +12,38 @@ Ordinary JavaScript control flow remains available; the guest command bindings
12
12
  are only `read`, `edit`, `write`, and `bash`. Supernova supplies retrieval,
13
13
  transactional file operations, batching, bounded results and the grouped nova UI.
14
14
 
15
- ## What is new in 0.10.0
16
-
17
- - Background bash sessions support launch, polling, input and stop with bounded
18
- transcripts, deadlines, ownership and process-tree cleanup. macOS/Linux offer
19
- interactive PTYs; Windows uses pipes and explicitly rejects PTYs.
20
- - Literal argv stays native across platforms. Session changes, file commits,
21
- read provenance and error diagnostics retain their safety boundaries during
22
- asynchronous work. See [verification](#verification) for the cross-platform
23
- Node/Bun matrix and its limits.
24
-
25
- ## What is new in 0.9.0
26
-
27
- - **Text clipping does not stop batches:** sequential and parallel programs keep
28
- running when the combined display text exceeds its allowance. Results disclose
29
- truncation; execution, deadline, host-call, log and image limits remain enforced.
30
- - **Decoded image validation:** PNG/JPEG/GIF/WebP reads and returned attachments
31
- require matching formats, canonical base64 and decodable pixels, including all
32
- GIF/WebP frames. PNG preflight checks chunk boundaries and CRCs too. Failures
33
- occur before shell boundaries or final commit/delivery, with no image attached.
34
- - **Isolated image work:** Sharp 0.35.4 decodes at most 32 million pixels across
35
- all frames, one image at a time, in a Node/Bun subprocess with a 5-second kill
36
- deadline. Text-only work never loads the decoder. A bounded 16-entry digest cache
37
- avoids decoding unchanged bytes again; neither image data nor file paths are cached.
38
- Encoded limits remain 16 attachments / 20 MiB. Sharp's platform-specific optional
39
- dependencies must be installed; decoder failure never falls back to unchecked data.
40
- Local decoding does not guarantee acceptance under every provider's image policy.
41
-
42
- - **Data is not a display preview:** ordinary text reads return complete data up to
43
- 64 MiB; JSON selections remain actual values and directories remain arrays.
44
- The former 160-line / 8192-character and 31,744-character restrictions no longer
45
- constrain computation inside a program. Only returned/logged text is displayed.
46
- - **Large batches stay bounded:** small reads share one reply; larger items are
47
- delivered with acknowledgements within eight I/O slots, not retained as one
48
- giant host-side batch. Explicit arrays and coalesced reads use the same path.
49
- - **Output work stays in the worker:** bounded formatting avoids expanding large
50
- values before clipping. Model-visible source previews retain exact ranges and
51
- continuation; a clipped preview cannot be passed back as a complete edit view.
52
-
53
- - **Cancellation no longer crashes the host:** bounded reads and CAS signing use
54
- abort-checked file handles instead of aborting streams. A failed read can cancel
55
- sibling reads without an uncaught `AbortError` terminating OMP.
56
- - **Memory is charged to the guest:** worker-local heap and external buffers
57
- replace process-wide RSS accounting. Bun enforcement remains best-effort.
58
- - **Focused internals:** read adapters, file I/O, transactions, worker lifecycle,
59
- tool ownership, source ranking and rendering have separate modules. All read
60
- modes, batching, checkpoints and rollback behavior remain supported.
61
-
62
- **0.9.0 release-candidate baseline:** 315/315 package tests passed on Node and
63
- Bun, plus actual Pi/OMP and clean tarball installation checks. See the newer
64
- [cross-platform verification results and coverage limits](#verification) below.
65
-
66
- ### Concurrent file operations
67
-
68
- Batching and explicit parallel programs remain supported. Within one program,
69
- `Promise.all` now overlaps native edits and writes to different files, up to eight
70
- operations at once:
71
-
72
- ```js
73
- await Promise.all([
74
- edit("src/a.js", "oldA", "newA"),
75
- edit("src/b.js", "oldB", "newB"),
76
- ]);
77
- return await bash("npm test");
78
- ```
79
-
80
- Same-file operations retain submission order. Reads before/after mutations,
81
- `bash`, edit checkpoints, and overridden mutating tools remain ordering barriers.
82
- Shell calls inside one program stay sequential; use `programs` with
83
- `parallel:true` for explicitly independent shell workflows or separate JS workers.
84
- Edits still stage until program success (or a shell boundary); transactional disk
85
- commits retain their conflict checks. `await edit(...)` one after another is still
86
- sequential, and multiple replacements in one file remain one edit operation.
87
-
88
- ## What is new in 0.8.2
89
-
90
- This patch release fixes shell failure handling, cancellation and parallel-batch
91
- limits, and makes one-call batching guidance explicit.
92
-
93
- - **Shell quoting stays intact:** quoted executable paths are no longer unwrapped.
94
- Shell syntax errors suggest literal `bash({command,args})` with `data` for
95
- embedded scripts, or a quoted heredoc. Commands are not rewritten or retried.
96
- - **Validation before commit:** invalid timeouts and null-byte
97
- arguments are rejected before the shell boundary flushes staged files.
98
- - **Useful failure output:** long command labels are bounded so the original
99
- stderr is not crowded out by a repeated script.
100
- - **Timeouts retain diagnostics:** the outer program deadline stops the worker
101
- and gives pending host calls a bounded drain to retain shell output. Explicit
102
- cancellation is reported separately from timeout. The outer `timeoutMs` covers
103
- every wait and command, including `sleep`.
104
- - **Parallel budgets fail honestly:** exceeding the shared log or image
105
- allowance marks the batch failed and stops queued entries. Already-running
106
- entries settle; their results and completed commits remain. Aggregate logs stay
107
- capped rather than multiplying the allowance per guest. In 0.8.2 this also
108
- applied to output text; 0.9.0 makes display-text clipping nonfatal.
109
- - **Batch known work in one call:** combine independent reads/checks with
110
- `Promise.all`, then sequence edits and verification in the same program. Use
111
- another invocation when returned evidence is needed for the next decision.
112
-
113
- Verified on **macOS / Node 26.7**: 267 package tests (384 repository tests),
114
- 2,328 stress invocations, actual Pi/OMP host checks, lint and both token-budget
115
- checks. This is not a claim of exhaustive platform or formal mutation testing.
116
-
117
- ## 0.8.0 features and measurements
118
-
119
- - **Shared program source:** top-level `code` or `file` supplies a batch default;
120
- entries may override it. A shared program is sent once instead of in every entry,
121
- and defaults count once against the 48,000-character admission cap.
122
- - **Explicit object defaults:** `mergeData:true` shallowly overlays per-entry data
123
- onto common data (entry keys win; nested objects are replaced). Whole-input
124
- replacement remains the default.
125
- - **Checkpoint failures throw:** a failed `edit(async () => {...})` rolls back and
126
- rethrows its original cause; catch explicitly when rejecting a candidate is
127
- intentional. Ignored failures no longer report success.
128
- - **Accurate failure cards:** the nova card reads the host's error flag, shows the
129
- original cause and `committed`/`rolledBack` totals, marks writes whose
130
- persistence cannot be attributed as attempted, and labels pure JavaScript runs
131
- instead of "complete".
132
- - **Historical read limits (superseded in 0.9.0):** errors stated both limits (`160 lines / 8192
133
- characters`) with copyable recovery (`offset`, `about`, `complete:true`, and
134
- `Promise.allSettled` for optional siblings). Markdown edits skip code-reference
135
- searches; exact-symbol evidence excludes generic matches.
136
- - **Fail-closed images:** unsupported formats (for example BMP) fail before model
137
- delivery with PNG-conversion guidance, and sets over 16 images / 20 MiB report
138
- aggregate sizes instead of silently omitting attachments. Pending changes roll back.
139
- - **Shell follows the program clock:** `bash()` inherits the program's `timeoutMs`;
140
- explicit per-command limits still win.
141
-
142
- ### Tokens: 0.7.1 to 0.8.0 (`js-tiktoken`, `o200k_base` / `cl100k_base`)
143
-
144
- | Metric | 0.7.1 | 0.8.0 | Change |
145
- |---|---:|---:|---:|
146
- | Standing definition per request | 596 / 588 | 631 / 626 | +35 / +38 |
147
- | Frozen 6-call mixed workload, total traffic | 9,596 / 9,458 | 9,841 / 9,724 | +2.6% / +2.8% |
148
- | 16-program job with shared source + data (32 files) | 17,771 / 17,595 | 3,587 / 3,533 | -79.8% / -79.9% |
149
- | 8 programs sharing a 48-path input | 16,309 / 14,649 | 5,499 / 5,197 | -66.3% / -64.5% |
150
-
151
- Rows 3-4 deliver identical complete outputs and files; only argument placement
152
- changes. Row 2 repeats no inputs, so it pays the +35-token guidance and nothing
153
- else. Method, gates and limits: [TOKEN_COSTS.md](https://github.com/AdityaVG13/pi-stack/blob/main/packages/pi-supernova/docs/TOKEN_COSTS.md).
154
-
155
- ### Speed: engine micro-benchmarks (Apple M5 Max, Node 26.7)
156
-
157
- | Benchmark | Before | After | Change |
158
- |---|---:|---:|---:|
159
- | Package 200-row report (median, 10k iterations) | 0.1665 ms | 0.1314 ms | -21% |
160
- | Package nested source object (median) | 0.0366 ms | 0.0240 ms | -35% |
161
- | Idle worker exit | 267 ms | 17 ms | -94% |
162
- | 8-file read wave p50 / p95 (300 samples) | 1.90 / 2.74 ms | 1.77 / 2.53 ms | -7% / -8% |
163
- | Cold unbatched p95 vs coalesced warm p95 (8 reads) | 13.85 ms | 2.39 ms | -83% |
164
-
165
- Identical output hashes before and after. Local engine benchmarks, not end-to-end
166
- agent latency or provider time.
167
-
168
- See the [changelog](https://github.com/AdityaVG13/pi-stack/blob/main/packages/pi-supernova/docs/CHANGELOG.md)
169
- and [token measurements](https://github.com/AdityaVG13/pi-stack/blob/main/packages/pi-supernova/docs/TOKEN_COSTS.md).
15
+ ## What is new in 0.10.2
16
+
17
+ - Results print the `mutations:` line only when it informs: files committed or
18
+ rolled back, an uncertain filesystem outcome, or shell side effects left behind
19
+ by a failed program. It had appeared on most results, usually all zeros.
20
+ - Clearer guest errors: naming a variable after a command (`const read = await
21
+ read(...)`) says so, `supernova(...)` inside a program says to call the
22
+ commands directly, and an empty `oldText` explains how to insert.
23
+
24
+ Earlier releases: [changelog](https://github.com/AdityaVG13/pi-stack/blob/main/packages/pi-supernova/docs/CHANGELOG.md).
25
+
26
+ ## Tokens: native tool calls vs Supernova
27
+
28
+ Fixed 13-program workload (inspect, reproduce, repair, verify; JSON report update;
29
+ five-term audit), full tool-history replay, `js-tiktoken`. Regenerate with
30
+ `npm run test:tokens --prefix packages/pi-supernova`.
31
+
32
+ | Workload | Tokenizer | Without Supernova batching | Through Supernova 0.10.2 | Reduction |
33
+ |---|---|---:|---:|---:|
34
+ | Mixed 13-program workload | o200k_base | 28,130 | 9,631 | **65.8%** |
35
+ | | cl100k_base | 27,841 | 9,476 | **66.0%** |
36
+ | 16 programs, shared source + data (32 files) | o200k_base | 17,771 | 3,559 | **80.0%** |
37
+ | | cl100k_base | 17,595 | 3,493 | **80.1%** |
38
+ | 8 programs sharing a 48-path input | o200k_base | 16,309 | 5,471 | **66.5%** |
39
+ | | cl100k_base | 14,649 | 5,157 | **64.8%** |
40
+ | Standing tool definition per request | o200k_base / cl100k_base | 908 / 901 | 617 / 606 | 32.0% / 32.7% |
41
+
42
+ All rows deliver identical complete outputs and files; only call and argument
43
+ placement change. The baseline is a frozen non-batched snapshot, not a separate
44
+ product. Excludes provider envelopes, unrelated conversation, reasoning tokens and
45
+ caching; no model-quality claim. Method and speed measurements:
46
+ [TOKEN_COSTS.md](https://github.com/AdityaVG13/pi-stack/blob/main/packages/pi-supernova/docs/TOKEN_COSTS.md).
170
47
 
171
48
  ## Install and update
172
49
 
@@ -183,8 +60,10 @@ Local checkout installs are for development, not distribution:
183
60
  pi install /path/to/pi-stack/packages/pi-supernova
184
61
  ```
185
62
 
186
- 0.9.0 adds Sharp for image validation. Keep its platform-specific optional
187
- dependencies enabled. For a local checkout, refresh dependencies before starting
63
+ Image reads and returned images are validated with Sharp: at most 32 million
64
+ decoded pixels across all frames, one image at a time, in a subprocess with a
65
+ 5-second kill deadline; text-only work never loads it. Keep Sharp's
66
+ platform-specific optional dependencies enabled. For a local checkout, refresh dependencies before starting
188
67
  the host:
189
68
 
190
69
  ```bash
@@ -640,7 +519,9 @@ return {verdict, values};
640
519
  ```
641
520
 
642
521
  Selectors support "." (root), .field, .nested[0], .items[0:10], and .["quoted.key"].
643
- Use json:true for the complete parsed value. Selectors are not full jq: pipes,
522
+ Use json:true for the complete parsed value. Put the selector in `json`
523
+ (`json:".field"`), not a second `selector` key; a leftover `selector` folds
524
+ when `json` is absent, `true`, or `"."`. Selectors are not full jq: pipes,
644
525
  filters, wildcards and negative indices fail explicitly. Missing keys and indices
645
526
  fail; false, zero and null remain values. Slices use an exclusive end and clamp to
646
527
  array length. Only own JSON properties are traversed; nothing is evaluated.
@@ -683,6 +564,14 @@ continuation handles.
683
564
 
684
565
  ## Execution and automatic batching
685
566
 
567
+ ```js
568
+ await Promise.all([
569
+ edit("src/a.js", "oldA", "newA"),
570
+ edit("src/b.js", "oldB", "newB"),
571
+ ]);
572
+ return await bash("npm test");
573
+ ```
574
+
686
575
  Put already-known independent reads and checks in **one** Supernova program using
687
576
  `Promise.all` (or `Promise.allSettled` when failures should remain independent).
688
577
  Sequence edits and their known verification in that same program. Start another
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.2] - 2026-09-24
4
+
5
+ ### Changed
6
+
7
+ - Results print the `mutations:` line only when it informs: files committed or
8
+ rolled back, an uncertain filesystem outcome, or (failures only) shell side
9
+ effects that survive the rollback. Five days of sessions carried it on 2,207
10
+ of 2,467 results, 1,928 of them all-zero (~240k characters).
11
+
12
+ ### Fixed
13
+
14
+ - Redeclaring or shadowing `read`/`edit`/`write`/`bash` (e.g.
15
+ `const read = await read(...)`) now says the name is a supernova command,
16
+ instead of a misleading `data` hint or a bare TDZ error. The `data` hint
17
+ appears only when `data` itself was redeclared.
18
+ - `supernova(...)` called inside a program says to call the commands directly.
19
+ - Empty `oldText` says how to insert: anchor on adjacent existing text.
20
+
21
+ ## [0.10.1] - 2026-09-23
22
+
23
+ ### Fixed
24
+
25
+ - JSON `read` folds a leftover `selector` key into `json`. The standing schema
26
+ wrote `json:true|selector` as a union on `json`'s value; models sent a second
27
+ option and hit `read does not accept option "selector"`. `json:true` plus
28
+ `selector:".field"` or `selector:"field"` now projects `.field`. Two real
29
+ projections still fail.
30
+
3
31
  ## [0.9.0] - 2026-09-20
4
32
 
5
33
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-supernova",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "CodeMode for Pi and OMP: read, edit, write and bash, with transactional files, source views and shared-input program batches.",
5
5
  "type": "module",
6
6
  "author": "AdityaVG13",
@@ -82,7 +82,7 @@ function classifyReplacements(args) {
82
82
 
83
83
  if (!Array.isArray(edits) || !edits.length) throw new Error(EDIT_USAGE);
84
84
 
85
- for (const e of edits) if (!isString(e?.oldText) || !e.oldText.length || !isString(e?.newText)) throw new Error(EDIT_USAGE + "; replacements require non-empty oldText and string newText");
85
+ for (const e of edits) if (!isString(e?.oldText) || !e.oldText.length || !isString(e?.newText)) throw new Error(EDIT_USAGE + "; replacements require non-empty oldText and string newText" + (isString(e?.oldText) && !e.oldText.length ? "; to insert, include adjacent existing text in oldText and repeat it in newText" : ""));
86
86
 
87
87
  return { kind: "edits", command: "edit", args };
88
88
  }
@@ -1,5 +1,5 @@
1
1
  import { errorMessage, isString, isObject, isNumber, looksLikePath } from "../shared/decode.js";
2
- import { sessionJsonArgs, validateJsonRead } from "../fs/json-read.js";
2
+ import { foldJsonSelectorAlias, sessionJsonArgs, validateJsonRead } from "../fs/json-read.js";
3
3
 
4
4
  export const SESSION_URI = /^(?:agent|artifact):\/\//i;
5
5
 
@@ -79,11 +79,13 @@ function autoResolve(args) {
79
79
  export function normalizeRead(params) {
80
80
  if (!isObject(params)) throw new Error("read requires an options object");
81
81
 
82
- if (params.path !== undefined && params.target !== undefined && params.path !== params.target) {
82
+ const folded = foldJsonSelectorAlias(params);
83
+
84
+ if (folded.path !== undefined && folded.target !== undefined && folded.path !== folded.target) {
83
85
  throw new Error("read accepts either path or target, not both");
84
86
  }
85
87
 
86
- const args = sessionJsonArgs({ ...params, path: params.path ?? params.target });
88
+ const args = sessionJsonArgs({ ...folded, path: folded.path ?? folded.target });
87
89
  validateJsonRead(args);
88
90
  assertReadOptions(args);
89
91
  assertReadFlags(args);
@@ -96,6 +96,48 @@ export function jsonProjector(json) {
96
96
  };
97
97
  }
98
98
 
99
+ function isWholeJson(json) {
100
+ return json === true || json === ".";
101
+ }
102
+
103
+ function coerceOneSelector(value) {
104
+ if (!isString(value) || value.length === 0 || value.length > 2048) throw new Error(SELECTOR_HELP);
105
+ const selector = value.startsWith(".") ? value : "." + value;
106
+
107
+ parseSelector(selector);
108
+
109
+ return selector;
110
+ }
111
+
112
+ function coerceSelector(value) {
113
+ if (Array.isArray(value)) {
114
+ if (!value.length || value.length > 64) throw new Error("JSON selector list requires 1 to 64 selectors");
115
+
116
+ return value.map(coerceOneSelector);
117
+ }
118
+
119
+ return coerceOneSelector(value);
120
+ }
121
+
122
+ /**
123
+ * Standing copy writes json:true|selector as a union on json's value.
124
+ * Models still send a leftover selector key (often with json:true).
125
+ * Fold it when json is absent or the whole document; reject two real projections.
126
+ */
127
+ export function foldJsonSelectorAlias(args) {
128
+ if (!isObject(args) || args.selector === undefined) return args;
129
+
130
+ if (args.json !== undefined && !isWholeJson(args.json)) {
131
+ throw new Error("read accepts json or selector, not both; put the selector in json (json:\".field\")");
132
+ }
133
+
134
+ const next = { ...args, json: coerceSelector(args.selector) };
135
+
136
+ delete next.selector;
137
+
138
+ return next;
139
+ }
140
+
99
141
  export function sessionJsonArgs(args) {
100
142
  if (!isString(args.path) || !/^(agent|artifact):\/\//i.test(args.path) || !args.path.includes("?")) return args;
101
143
  const [uri, query] = args.path.split("?");
@@ -92,7 +92,7 @@ function nearMissPreview(content, oldText) {
92
92
  }
93
93
  function matchReplacement(target, content, replacement) {
94
94
  if (!isString(replacement?.oldText) || replacement.oldText.length === 0) {
95
- throw new Error("edit requires non-empty oldText");
95
+ throw new Error("edit requires non-empty oldText; to insert, include adjacent existing text in oldText and repeat it in newText");
96
96
  }
97
97
 
98
98
  if (!isString(replacement?.newText)) throw new Error("edit requires newText");
@@ -11,14 +11,20 @@ function logsBlock(outcome, tail = "") {
11
11
  return `\n--- logs${outcome.logTruncated ? " [logs truncated]" : ""}\n${outcome.logs?.join("\n") ?? ""}${tail}`;
12
12
  }
13
13
 
14
- function mutationText(outcome) {
14
+ // Printed only when it informs: files committed or rolled back, an uncertain
15
+ // filesystem outcome, or (failures only) shell side effects that survive the
16
+ // rollback. A successful program rolls nothing back, so its shell calls need
17
+ // no warning.
18
+ function mutationText(outcome, failed) {
15
19
  const m = outcome.mutations;
16
20
 
17
21
  if (!m) return "";
18
- const external = m.external ? "; external calls attempted=" + m.external + ", their side effects cannot be rolled back" : "";
22
+ const external = failed && m.external ? "; external calls attempted=" + m.external + ", their side effects cannot be rolled back" : "";
19
23
  const uncertain = m.pendingCommits || m.recoveryFailed ? "; filesystem outcome uncertain: inspect disk and any recovery backups before retrying" : "";
20
24
 
21
- return "\nmutations: committed=" + m.committed + " rolledBack=" + m.rolledBack + " (file versions)" + external + uncertain;
25
+ if (!m.committed && !m.rolledBack && !external && !uncertain) return "";
26
+
27
+ return "\nmutations: committed=" + (m.committed || 0) + " rolledBack=" + (m.rolledBack || 0) + " (file versions)" + external + uncertain;
22
28
  }
23
29
 
24
30
  function mutationReceipts(trace) {
@@ -39,17 +45,15 @@ function splitTurnHint(outcome) {
39
45
  }
40
46
 
41
47
  function errorText(outcome, call) {
42
- return `error #${call} ${outcome.wallMs}ms${outcome.returnTruncated ? " [output truncated]" : ""}${mutationText(outcome)}${splitTurnHint(outcome)}
48
+ return `error #${call} ${outcome.wallMs}ms${outcome.returnTruncated ? " [output truncated]" : ""}${mutationText(outcome, true)}${splitTurnHint(outcome)}
43
49
  error: ${outcome.error}${logsBlock(outcome)}`;
44
50
  }
45
51
 
46
52
  function successText(outcome, call) {
47
53
  const truncated = outcome.returnTruncated ? " [return truncated]" : "";
48
54
  const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
49
- const m = outcome.mutations;
50
- const showMutations = m && (m.committed || m.rolledBack || m.external || m.pendingCommits || m.recoveryFailed) ? mutationText(outcome) : "";
51
55
 
52
- return `ok #${call} ${outcome.wallMs}ms${truncated}${showMutations}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
56
+ return `ok #${call} ${outcome.wallMs}ms${truncated}${mutationText(outcome, false)}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
53
57
  }
54
58
 
55
59
  function fitOutput(outcome, call, limit, format) {
@@ -227,7 +227,7 @@ async function handleRun(msg) {
227
227
  // Existing programs may declare their own data variable; bind it only when supplied.
228
228
  compiled = compileGuest(prepared, msg.data);
229
229
  } catch (err) {
230
- postFailure(runId, new Error("JavaScript syntax error: " + err.message + "; no commands ran. When passing data, do not redeclare its binding."));
230
+ postFailure(runId, new Error("JavaScript syntax error: " + err.message + "; no commands ran."));
231
231
 
232
232
  return;
233
233
  }
@@ -2,7 +2,7 @@
2
2
  export const REFERENCE = `JS body/async arrow; read/write/edit/bash, no fs/import/require. file: workspace JS; data: literals ≤48000 JSON chars.
3
3
  read(path|paths,offset=1,limit?) → text/text[]; dirs→entries; PNG/JPEG/GIF/WebP ≤16/20MiB.
4
4
  Text ≤64 MiB internally; complete:true requires whole file. Display capped: summarize or read(path,{offset:1,limit:80}); larger files via bash.
5
- read({path,json:true|selector}) → JSON, no jq (16MiB input/64MiB selection). Values retain their types.
5
+ read({path,json:true}) or json:".field" or json:[".a"] → JSON, no jq (16MiB input/64MiB selection). Values retain their types. selector is json's value; a leftover selector key folds.
6
6
  read("symbol or question") = read({query,resolve:true}) → view; check status; view.text is a span. read(path,{about}) windows; read({query,evidence:true}) evidence; read({path,outline:true}) declarations.
7
7
  write/edit workspace-only; external changes need separately authorized command. write(path,text) or {path,content,append:true}; after read edit or replace:true.
8
8
  edit(path,oldText,newText) or {path,edits:[{oldText,newText}]}; unique exact match; numbered windows. edit(view,text) or edit(view,old,new). edit(async()=>{...}) checkpoint: no bash; merge/rollback+rethrow.
@@ -12,6 +12,24 @@ import { errorMessage, isFunction, isObject, isString } from "../shared/decode.j
12
12
  import { errorContext } from "../shared/syntax-context.js";
13
13
  import {validateReturnedImages} from "../shared/image.js";
14
14
 
15
+ const GUEST_COMMANDS = new Set(["read", "edit", "write", "bash"]);
16
+
17
+ // Hints for mistakes seen in real sessions. Bindings named after a command
18
+ // shadow it (redeclaration or TDZ); `supernova` is the tool, not a guest
19
+ // function; `data` can only collide when the call supplied it. Anything
20
+ // else gets no hint.
21
+ function guestMistakeHint(message) {
22
+ const name = /Identifier '([^']+)' has already been declared/.exec(message)?.[1] ?? /Cannot access '([^']+)' before initialization/.exec(message)?.[1];
23
+
24
+ if (GUEST_COMMANDS.has(name)) return "; `" + name + "` is a supernova command: give your variable another name (e.g. const text = await " + name + "(...))";
25
+
26
+ if (name === "data") return "; the data parameter is already bound as `data`: do not redeclare its binding";
27
+
28
+ if (/\bsupernova is not defined\b/.test(message)) return "; inside a program, call read, edit, write or bash directly (supernova is the tool you are already in)";
29
+
30
+ return "";
31
+ }
32
+
15
33
  const ABORT_MESSAGE = "supernova aborted";
16
34
  const TIMEOUT_MESSAGE = "supernova timed out: increase the outer timeoutMs (and any shorter bash timeoutMs), or split the program; sleeps count toward the deadline";
17
35
 
@@ -241,7 +259,7 @@ class GuestRun {
241
259
  guestError(msg) {
242
260
  const location = normalizeGuestLocation(this.prepared.body, msg.location);
243
261
  const where = location ? " (line " + location.line + ":" + location.col + ")" : "";
244
- void this.complete(this.fail(msg.message + where));
262
+ void this.complete(this.fail(msg.message + where + guestMistakeHint(msg.message)));
245
263
  }
246
264
 
247
265
  onMemory(msg) {