@azure-id/orc 1.4.2 → 1.6.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 (38) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +63 -50
  3. package/bin/cli.js +5732 -133
  4. package/bin/mockrun-catalog.js +4 -0
  5. package/bin/verify-contracts.js +4509 -4426
  6. package/bin/verify-package.js +24 -2
  7. package/bin/webui/api.js +108 -8
  8. package/bin/webui/app.html +9 -0
  9. package/bin/webui/css/06-responsive.css +14 -0
  10. package/bin/webui/css/panels/test.css +132 -0
  11. package/bin/webui/fixtures/index.js +23 -1
  12. package/bin/webui/fixtures/test.js +547 -0
  13. package/bin/webui/i18n/en/common.json +1 -0
  14. package/bin/webui/i18n/en/nav.json +1 -0
  15. package/bin/webui/i18n/en/overview.json +5 -0
  16. package/bin/webui/i18n/en/test.json +141 -0
  17. package/bin/webui/i18n/id/common.json +1 -0
  18. package/bin/webui/i18n/id/nav.json +1 -0
  19. package/bin/webui/i18n/id/overview.json +5 -0
  20. package/bin/webui/i18n/id/test.json +141 -0
  21. package/bin/webui/js/01-i18n.js +1 -0
  22. package/bin/webui/js/panels/challenge.js +8 -3
  23. package/bin/webui/js/panels/overview.js +18 -0
  24. package/bin/webui/js/panels/test.js +733 -0
  25. package/bin/webui/serve.js +572 -557
  26. package/mock-run/orc-test.md +327 -0
  27. package/package.json +1 -1
  28. package/templates/agents/MODEL-MAPPING.md +2 -0
  29. package/templates/agents/orc-test-designer-opus-5-high.md +158 -0
  30. package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -0
  31. package/templates/commands/orc-test.md +66 -0
  32. package/templates/hooks/README.md +110 -1
  33. package/templates/hooks/orc-read-gate.js +279 -0
  34. package/templates/skills/_shared/extra-dispatch.md +1346 -1337
  35. package/templates/skills/_shared/live-target.md +188 -0
  36. package/templates/skills/_shared/phases/trace.md +2 -2
  37. package/templates/skills/_shared/wait.md +241 -240
  38. package/templates/skills/orc-test/SKILL.md +233 -0
@@ -0,0 +1,66 @@
1
+ ---
2
+ description: Run the tests against the real running system — happy path to security — and report only what was observed
3
+ ---
4
+
5
+ Use the **orc-test** skill. Standalone — no plan, no build, no code written into
6
+ your project.
7
+
8
+ Every other ORC lane reasons about your system. This one **runs it**:
9
+
10
+ > **A test that ran is a FACT; a test that was written is an OPINION.**
11
+
12
+ ORC already has a lane that WRITES tests and never runs them (Phase 6.5,
13
+ `test-generator/`). This is the mirror image, and the two never mix: `/orc-test`
14
+ writes nothing into your test tree and runs everything.
15
+
16
+ **It never edits the system it is testing.** If the server will not come up, it
17
+ prints exactly what it ran and the tail of the output, and hands back. You fix
18
+ it, and re-run for free — the same shape as `/orc-challenge`, and for the same
19
+ reason: a session that just fixed the code cannot be trusted to measure it.
20
+
21
+ **It never reports a result it did not observe.** Three verdicts and no fourth:
22
+ `pass` (observed, matched), `fail` (observed, did not match), and `unknown` —
23
+ not observed. `unknown` keeps its slot in the report and never becomes a pass.
24
+ A flake is recorded, never retried away: there is no retry count, because if the
25
+ same case answers differently twice, *that instability is the finding*.
26
+
27
+ One pass:
28
+
29
+ 1. **Target** — `orc test init <slug>` asks what it must not guess: back end or
30
+ front end, local or remote, the base URL. On a remote target it also requires
31
+ `--authorized "<who authorized this, and where it is recorded>"`, stored
32
+ verbatim and reprinted at the head of every report. ORC cannot verify
33
+ authorization and does not pretend to — it makes the assertion impossible to
34
+ skip. Frozen, and never asked again.
35
+ 2. **Surface** — free, deterministic: an OpenAPI or GraphQL spec on disk, the
36
+ same spec at the target, route extraction by framework fingerprint, FE router
37
+ config. Anything ambiguous comes back as `unresolved[]` and is never guessed.
38
+ The **code-vs-live diff** is a first-class output: routes that answer live and
39
+ exist in no source file are zombie APIs.
40
+ 3. **Flow** — it digs the repo for how a request reaches your endpoint: the
41
+ route registration, the middleware in order, the handler, what it writes, what
42
+ must be true first, which fields name an object, and the login flow itself.
43
+ 4. **Environment** (local only) — it brings the system up and waits for health.
44
+ Five states, one next action each.
45
+ 5. **Cases** — the CLI derives the bulk for free: equivalence partitions,
46
+ boundary values, method and content-type negatives, stateful create → read →
47
+ update → delete sequences. An agent fills only what a schema cannot know.
48
+ 6. **Run** — paced, capped, and fenced to the frozen origin. A 429 is a RESULT,
49
+ not an error: it means rate limiting works. Every case leaves its exact
50
+ request, its exact response and a reproducible `curl`, with every credential
51
+ redacted **before the bytes reach disk**.
52
+ 7. **Report** — `orc/orc-test/<slug>/REPORT.md`, written for someone who does
53
+ not read code.
54
+
55
+ The security tier is a **closed set** mapped to the OWASP API Security Top 10
56
+ (2023), and it demonstrates a CONDITION rather than an extraction — confirming
57
+ an exploit is your decision on your authority. With one identity, BOLA and BFLA
58
+ report `UNCHECKABLE`, which keeps its slot and never becomes a pass.
59
+
60
+ **The run folder is never staged.** Evidence contains real response bodies from
61
+ a real system. The lane offers you the one `.gitignore` line; it does not edit
62
+ `.gitignore` itself.
63
+
64
+ Read the state back any time without this lane: `orc test status <slug>`.
65
+
66
+ The slug to open or reopen (or nothing, and it will ask): $ARGUMENTS
@@ -1,8 +1,11 @@
1
- # The ORC status line
1
+ # The ORC hooks
2
2
 
3
3
  > This page is written in Simplified Technical English. Short sentences, one
4
4
  > idea each, plain words. See `bin/webui/i18n/TERMS.md` for the term list.
5
5
 
6
+ This page covers the two hooks you can see. The STATUS LINE, which shows what
7
+ is happening, and the READ GATE, which can stop an oversized read.
8
+
6
9
  ORC shows two lines at the bottom of your terminal. Claude Code draws them.
7
10
  ORC writes them.
8
11
 
@@ -258,6 +261,91 @@ it did not see.
258
261
 
259
262
  ---
260
263
 
264
+ ## The read gate
265
+
266
+ This is a different hook. It is off when you install ORC.
267
+
268
+ ORC has a rule about reading. The main session reads a file to FIND things.
269
+ To UNDERSTAND a file, ORC sends an agent to read it and report back. The agent
270
+ uses its own context, not yours. The rule was written in three guide files and
271
+ nothing checked it. The read gate is the part that can say no.
272
+
273
+ Turn it on like this:
274
+
275
+ ```
276
+ orc config set read_gate warn # it tells you, and still reads the file
277
+ orc config set read_gate block # it stops the read
278
+ orc config set read_gate off # the default
279
+ ```
280
+
281
+ ### What it does
282
+
283
+ It looks at one thing: a `Read` of a whole file, by the main session, during an
284
+ ORC run, when the file is 1000 lines or more.
285
+
286
+ In `warn` it shows you a note and reads the file anyway. In `block` it stops the
287
+ read and tells you three other ways to get what you need:
288
+
289
+ - Read the file with `offset` and `limit`. This is never stopped.
290
+ - Search the file first, then read that part.
291
+ - Send an agent to read it. An agent's reads are never stopped.
292
+
293
+ **A block always names another way.** A gate that only says no is a gate people
294
+ turn off.
295
+
296
+ ### When it says nothing
297
+
298
+ This list is the important part. The gate is quiet in all of these states, and
299
+ each one is on purpose:
300
+
301
+ | State | Why |
302
+ |---|---|
303
+ | An agent is reading | An agent must read a file in full before it edits it. If it could not, it would guess the old text and damage the file. |
304
+ | `read_gate` is `off` | This is the default. With `off`, the hook does nothing at all. |
305
+ | No ORC run is open | The gate is about ORC's own reading. It is not a rule for your session. Outside a run it never stops anything. |
306
+ | You used `offset` or `limit` | This is the behaviour the gate wants. It can never stop it. |
307
+ | The file is under 1000 lines | See the next part. |
308
+ | The file is a build log, a test result, or `.jsonl` | ORC reads these to decide pass or fail. A cut-short failing build looks like a passing build. That is worse than any saving. |
309
+ | The gate hit an error | It always lets the read through. Then it writes down what went wrong. |
310
+
311
+ **The gate cannot see a file you read with a shell command** such as `cat` or
312
+ `head`. It only sees the `Read` tool.
313
+
314
+ ### Why 1000 lines
315
+
316
+ Sending an agent is not free. One real agent read cost about 13,000 tokens and
317
+ about 76 seconds. A line in this project is about 55 characters. So a file must
318
+ be near 1000 lines before sending an agent costs less than reading it yourself.
319
+
320
+ Below that number, sending an agent costs more than it saves.
321
+
322
+ You can change it:
323
+
324
+ ```
325
+ orc config set read_gate_max_lines 500
326
+ ```
327
+
328
+ ### What it never does
329
+
330
+ - It never stops an agent's read.
331
+ - It never stops a read outside an ORC run.
332
+ - It never stops a `Bash` command.
333
+ - It never reads the file to you. It only counts the lines.
334
+ - It never fails closed. If the hook breaks, your read still happens.
335
+
336
+ ### Where to look when it acts
337
+
338
+ Every `warn` and every `block` writes one line in the run trace:
339
+
340
+ ```
341
+ [070926 14:22:01.220] hook READ-GATE block :: lines=2400 max=1000 file=big-plan.md
342
+ ```
343
+
344
+ An allowed read writes nothing. `orc doctor` tells you if the gate is on but not
345
+ wired, and if it ever had to let a read through because it could not judge it.
346
+
347
+ ---
348
+
261
349
  ## For maintainers
262
350
 
263
351
  - The hook is `orc-statusline.js`. `orc init` installs it and wires it into
@@ -285,3 +373,24 @@ it did not see.
285
373
  - The cache file is `.claude/orc/usage-session.json`. The hook reads it once and
286
374
  writes it once, after the text is ready. It stores raw numbers only — never a
287
375
  word like `fresh` or `STALE`, which is computed each time it is shown.
376
+
377
+ ### The read gate
378
+
379
+ - The hook is `orc-read-gate.js`, on `PreToolUse` with matcher `Read`. `orc
380
+ init` wires it even though `read_gate` defaults to `off`, so arming it is a
381
+ config edit and never an install step somebody has to find.
382
+ - **`off` is byte-identical to not having the hook**, and a test asserts it.
383
+ - **`agent_id` is the only way to tell a subagent's read from the main
384
+ session's, and this was MEASURED, not assumed.** `PreToolUse` does fire
385
+ inside a dispatched subagent, and `session_id` and `transcript_path` are
386
+ identical in both. A gate written against either would block the full read an
387
+ executor must do before it edits, and a reconstructed `old_string` corrupts
388
+ files. Test for the PRESENCE of `agent_id`. Never test for the absence of
389
+ another key — that is not a positive statement about anything.
390
+ - The threshold is measured, not borrowed. See `read_gate_max_lines`.
391
+ - It fails open on every path, and each failure it can name writes
392
+ `.claude/orc/read-gate-fallback.json` for `orc doctor` to turn into a
393
+ sentence — only while the feature is armed.
394
+ - It writes one trace line per `warn` and per `block`, never on an allow. That
395
+ is affordable here for a structural reason: the gate only acts while a run is
396
+ open, so a trace always exists.
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * ORC read gate — a Claude Code PreToolUse hook on `Read`.
6
+ *
7
+ * ORC's read discipline (`skills/_shared/read-ladder.md`, orc-doc hard rule 0,
8
+ * orc-quick line 21) says the ORCHESTRATOR reads to LOCATE and dispatches to
9
+ * UNDERSTAND. That rule was advisory prose in three places and enforced in
10
+ * none. This hook is the one layer that can refuse.
11
+ *
12
+ * It is DELIBERATELY the narrowest useful gate. Everything below is a state in
13
+ * which it says nothing at all, and every one of them is intentional:
14
+ *
15
+ * - a SUBAGENT is reading (measured: W1 — see `agent_id` below)
16
+ * - `read_gate` is `off` (the default)
17
+ * - no ORC run is open
18
+ * - the read is already targeted (`offset` / `limit` — ladder step 3)
19
+ * - the file is under the threshold
20
+ * - the file is output a gate parses (ladder exception 2)
21
+ * - `read_gate` is `warn` (it says so, and still allows)
22
+ *
23
+ * THE SUBAGENT RULE IS LOAD-BEARING, AND IT IS MEASURED, NOT ASSUMED.
24
+ * `PreToolUse` DOES fire inside a dispatched subagent. `session_id` and
25
+ * `transcript_path` are IDENTICAL in both contexts — a gate written against
26
+ * either would treat an executor's read as the orchestrator's and block the
27
+ * full read that must precede an `Edit`, whose `old_string` cannot be
28
+ * reconstructed from an outline. That is a file-corruption path. The ONLY
29
+ * discriminator is `agent_id`, present in a subagent payload and absent in a
30
+ * main-session one. Test for its PRESENCE — never for the absence of some
31
+ * other key, which is not a positive assertion.
32
+ *
33
+ * Wiring (installed by `orc init` into .claude/settings.json):
34
+ * hooks.PreToolUse[] { matcher: "Read", hooks:[{ type:"command",
35
+ * command: 'node "<.claude>/hooks/orc-read-gate.js"' }] }
36
+ *
37
+ * Contract: read PreToolUse JSON from stdin. Exit 0 = allow. Exit 2 = block
38
+ * the tool call and show stderr to Claude, which relays it to the user.
39
+ *
40
+ * FAIL-OPEN, ALWAYS. A read gate that throws and blocks a read has broken the
41
+ * tool. Every failure path here exits 0, and the ones we can name RECORD
42
+ * themselves so `orc doctor` can turn them into a sentence — but only while
43
+ * the feature is armed. Precedent: orc-statusline.js is fail-silent, and the
44
+ * v1.3.0 statusline gate ladder is all fallbacks, each of which records itself.
45
+ */
46
+
47
+ const fs = require("fs");
48
+ const path = require("path");
49
+
50
+ // .claude/hooks/orc-read-gate.js → CLAUDE_DIR = .. → PROJECT_ROOT = ../..
51
+ const CLAUDE_DIR = path.join(__dirname, "..");
52
+ const PROJECT_ROOT = path.join(CLAUDE_DIR, "..");
53
+
54
+ // A run whose trace has been idle this long has ENDED. Same window as
55
+ // orc-trace.js — one idea of "a run is open", not two.
56
+ const STALE_MS = 6 * 60 * 60 * 1000;
57
+
58
+ // Default line threshold. MEASURED, not copied.
59
+ //
60
+ // The number in the source material this pattern came from is 350, derived
61
+ // from a 10-30s delegation round trip. ORC's is nothing like that: a Task
62
+ // dispatch measured p50 76s / p90 188s (n=125), and one real read-only
63
+ // dispatch cost 13,276 tokens to read a four-line file and return three words.
64
+ // At the measured 55.2 chars/line across 316 main-session full reads, that
65
+ // floor is ~962 lines; at the median file's 51.9 chars/line it is ~1024.
66
+ // Break-even is therefore ~1000 lines, and BELOW it delegating a read costs
67
+ // more than the read does. Copying 350 would delegate work whose overhead
68
+ // exceeds its saving.
69
+ const DEFAULT_THRESHOLD = 1000;
70
+
71
+ // Output a gate PARSES is read whole — read-ladder exception 2. The smoke
72
+ // gate, the TDD gate, the verifier and /orc-quick's build loop all decide red
73
+ // vs green from these exact bytes, and a truncated red build reads GREEN.
74
+ // That is worse than any token saving, so this list is deliberately generous:
75
+ // a false ALLOW costs tokens, a false BLOCK costs correctness.
76
+ const GATE_PARSED =
77
+ /(\.(log|tap|xml|lcov|junit)$|(^|[\\/])(coverage|test-results|npm-debug|yarn-error)([\\/]|$)|\.jsonl$)/i;
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Config — tolerant top-level YAML scalar read, matching orc-trace.js exactly.
81
+ // The repo is zero-dep; there is no YAML parser.
82
+ // ---------------------------------------------------------------------------
83
+ function readConfigScalar(key) {
84
+ try {
85
+ const text = fs.readFileSync(path.join(CLAUDE_DIR, "orc.config.yaml"), "utf8");
86
+ const re = new RegExp("^" + key + "\\s*:\\s*(.+?)\\s*(?:#.*)?$", "m");
87
+ const m = text.match(re);
88
+ return m ? m[1].replace(/^['"]|['"]$/g, "").trim() : null;
89
+ } catch (_) {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function logDir() {
95
+ const rel = readConfigScalar("log_dir") || ".claude/orc/logs";
96
+ return path.isAbsolute(rel) ? rel : path.join(PROJECT_ROOT, rel);
97
+ }
98
+
99
+ // Is an ORC run open? `.current` names the run; the trace file it names must
100
+ // exist and be fresh. A pointer naming a file that does not exist is
101
+ // indistinguishable from a dangling one (v0.34.2), so honour a pointer whose
102
+ // OWN mtime is fresh too — neither half depends on the other.
103
+ function runIsOpen() {
104
+ const dir = logDir();
105
+ let name;
106
+ try {
107
+ name = fs.readFileSync(path.join(dir, ".current"), "utf8").trim();
108
+ } catch (_) {
109
+ return false;
110
+ }
111
+ if (!name) return false;
112
+ const now = Date.now();
113
+ try {
114
+ if (now - fs.statSync(path.join(dir, name)).mtimeMs < STALE_MS) return true;
115
+ } catch (_) {
116
+ /* trace file not created yet — fall through to the pointer's own mtime */
117
+ }
118
+ try {
119
+ return now - fs.statSync(path.join(dir, ".current")).mtimeMs < STALE_MS;
120
+ } catch (_) {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ // D10 — A GATE DECISION THAT LEAVES NO LINE CANNOT BE COUNTED. Same rule as a
126
+ // resume and a demotion, and it is CHEAP HERE FOR A STRUCTURAL REASON: the gate
127
+ // only ever acts while a run is open (rung 2), so a trace to write into is
128
+ // guaranteed to exist by the time we get here. There is no "a read happened
129
+ // with no run" case to handle, because that read was already allowed.
130
+ //
131
+ // Hook-composed, like every other line orc-trace.js writes. Best effort: a line
132
+ // that cannot be written must never take the gate down with it.
133
+ function traceLine(verb, tail) {
134
+ try {
135
+ const dir = logDir();
136
+ const name = fs.readFileSync(path.join(dir, ".current"), "utf8").trim();
137
+ if (!name) return;
138
+ const d = new Date();
139
+ const p = (n, w) => String(n).padStart(w || 2, "0");
140
+ const stamp =
141
+ p(d.getDate()) + p(d.getMonth() + 1) + p(d.getFullYear() % 100) + " " +
142
+ p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds()) +
143
+ "." + p(d.getMilliseconds(), 3);
144
+ fs.appendFileSync(
145
+ path.join(dir, name),
146
+ `[${stamp}] ${"hook".padEnd(8)} ${verb}` + (tail ? ` :: ${tail}` : "") + "\n"
147
+ );
148
+ } catch (_) {}
149
+ }
150
+
151
+ // A fallback RECORDS ITSELF (v1.3.0's rule). Best effort by construction: a
152
+ // record that cannot be written must never take the gate down with it.
153
+ function recordFallback(rung, detail) {
154
+ try {
155
+ const dir = path.join(CLAUDE_DIR, "orc");
156
+ fs.mkdirSync(dir, { recursive: true });
157
+ fs.writeFileSync(
158
+ path.join(dir, "read-gate-fallback.json"),
159
+ JSON.stringify({ rung, detail: String(detail || "").slice(0, 300), at: Date.now() })
160
+ );
161
+ } catch (_) {}
162
+ }
163
+
164
+ // Count lines without holding the whole file in memory twice. Returns null when
165
+ // the file cannot be measured — and null NEVER blocks (unknown is not "big").
166
+ function countLines(file) {
167
+ try {
168
+ const st = fs.statSync(file);
169
+ if (!st.isFile()) return null;
170
+ // A file that cannot possibly reach the threshold is not worth reading.
171
+ // 1 byte/line is the floor, so bytes < threshold ⇒ lines < threshold.
172
+ const buf = fs.readFileSync(file);
173
+ let n = 1;
174
+ for (let i = 0; i < buf.length; i++) if (buf[i] === 10) n++;
175
+ return n;
176
+ } catch (_) {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ let raw = "";
182
+ process.stdin.on("data", (c) => (raw += c));
183
+ process.stdin.on("end", () => {
184
+ // RUNG 0 — anything at all goes wrong: ALLOW. This wrapper is the whole
185
+ // fail-open guarantee; every `return` below it is an allow.
186
+ try {
187
+ gate();
188
+ } catch (e) {
189
+ recordFallback("threw", e && e.message);
190
+ process.exit(0);
191
+ }
192
+ process.exit(0);
193
+ });
194
+
195
+ function gate() {
196
+ let data;
197
+ try {
198
+ data = JSON.parse(raw || "{}");
199
+ } catch (e) {
200
+ recordFallback("unparseable-payload", e && e.message);
201
+ return; // allow
202
+ }
203
+
204
+ if (data.tool_name !== "Read") return;
205
+
206
+ // RUNG 0.5 — a SUBAGENT is reading. ALLOW, always, and this check comes
207
+ // before every other one. See the header: this is what keeps the gate off
208
+ // the pre-`Edit` full read that an executor must perform, and it is why the
209
+ // gate never needs to know a slice's declared_files.
210
+ if (data.agent_id != null) return;
211
+
212
+ const mode = String(readConfigScalar("read_gate") || "off").toLowerCase();
213
+ if (mode !== "warn" && mode !== "block") return; // `off` (the default), or garbage
214
+
215
+ // RUNG 2 — no ORC run is open. The gate constrains ORC's own reading; it is
216
+ // not a policy on the user's session. A read outside a run is not ORC's to
217
+ // refuse, and the gate says nothing. This is an HONEST LIMIT, not an
218
+ // oversight: it is stated in templates/hooks/README.md and in `orc doctor`.
219
+ if (!runIsOpen()) return;
220
+
221
+ const input = data.tool_input || {};
222
+ const file = String(input.file_path || "");
223
+ if (!file) return;
224
+
225
+ // RUNG 3 — already a targeted read. This IS ladder step 3; it is the
226
+ // behaviour the gate exists to encourage, so it can never be refused.
227
+ if (input.offset != null || input.limit != null) return;
228
+
229
+ // RUNG 5 — output a gate parses. Exception 2 is not a preference.
230
+ if (GATE_PARSED.test(file)) return;
231
+
232
+ let threshold = parseInt(readConfigScalar("read_gate_max_lines"), 10);
233
+ if (!Number.isFinite(threshold) || threshold < 1) threshold = DEFAULT_THRESHOLD;
234
+
235
+ const lines = countLines(file);
236
+ if (lines == null) {
237
+ // Unmeasurable — a directory, a missing file, a permissions error, a binary
238
+ // the Read tool will handle its own way. UNKNOWN IS NOT BIG.
239
+ recordFallback("unmeasurable", file);
240
+ return;
241
+ }
242
+ if (lines < threshold) return; // RUNG 4 — under the threshold
243
+
244
+ // RUNG 7 — `warn`: allow, and say so. `systemMessage` is shown to the user
245
+ // and is NOT added to model context, so the warning itself costs no tokens.
246
+ if (mode === "warn") {
247
+ traceLine("READ-GATE warn", `lines=${lines} max=${threshold} file=${path.basename(file)}`);
248
+ try {
249
+ process.stdout.write(
250
+ JSON.stringify({
251
+ systemMessage:
252
+ `📖 ORC read gate — ${path.basename(file)} is ${lines} lines ` +
253
+ `(threshold ${threshold}). Allowed: read_gate is 'warn'.\n` +
254
+ ` Cheaper: Read with offset/limit, or dispatch an agent to read it and report back.`,
255
+ })
256
+ );
257
+ } catch (_) {}
258
+ return;
259
+ }
260
+
261
+ // RUNG 8 — block, and NAME THE ALTERNATIVE. A block that only refuses
262
+ // teaches people to switch it off.
263
+ traceLine("READ-GATE block", `lines=${lines} max=${threshold} file=${path.basename(file)}`);
264
+ process.stderr.write(
265
+ `\n⛔ ORC read gate — ${path.basename(file)} is ${lines} lines, over the ` +
266
+ `${threshold}-line threshold.\n` +
267
+ ` Reading it whole spends the orchestrator's context on material a ` +
268
+ `worker could summarise.\n\n` +
269
+ ` Do one of these instead:\n` +
270
+ ` • Read with offset/limit — the ladder's targeted read, and always allowed.\n` +
271
+ ` • Grep for what you actually need, then read that range.\n` +
272
+ ` • Dispatch an agent to read it and report back (its reads are never gated).\n\n` +
273
+ ` If you genuinely need the whole file here: set read_gate to warn or off\n` +
274
+ ` orc config set read_gate warn\n` +
275
+ ` The gate is silent outside an ORC run, on targeted reads, on files a\n` +
276
+ ` gate parses, and on every read a subagent makes.\n`
277
+ );
278
+ process.exit(2);
279
+ }