@azure-id/orc 1.2.0 → 1.4.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.
@@ -20,10 +20,34 @@
20
20
  * `rate_limits.{five_hour,seven_day}` (Anthropic API headers, not estimated).
21
21
  * A window ≥90% folds into the DEGRADE verdict; fail-silent when absent.
22
22
  *
23
- * Three-tier verdict (the "ORC-ready" acceptance matrix):
24
- * ORC-ready Opus 4.8 high (the baseline)
25
- * 🚀 ORC-boosted Opus 4.8 xhigh/max, or Opus 5 / Fable 5 medium…max
26
- * ORC WILL DEGRADE everything below (wrong model, sub-baseline effort, quota)
23
+ * Three-tier verdict (the "ORC-ready" acceptance matrix). Since v1.2.1 the
24
+ * ICON carries the verdict and the words carry the installed ORC version — but
25
+ * the degrade branch still names every reason, because a warning with no reason
26
+ * is an emoji:
27
+ * ✅ ORC v1.2.1 - Opus 5/high Opus 4.8 high (the baseline)
28
+ * 🚀 ORC v1.2.1 - Opus 5/high Opus 4.8 xhigh/max, or Opus 5 / Fable 5 medium…max
29
+ * ⛔ ORC v1.2.1 - Sonnet 5/high (why) everything below (wrong model, sub-baseline effort, quota)
30
+ *
31
+ * The two lines, in full (v1.2.1):
32
+ *
33
+ * {icon} ORC v{version} - {model}/{effort} · context (N%) · 5h N% (reset) ↔ wk N%
34
+ * · ucs N% · wiki: … · diy:… · orc N.N.N available
35
+ * {glyph} status: {lane} · {phase} · agents N (M running) · orc-extra: on|off
36
+ * · Dur Nm · MTok NNNK · {branch}
37
+ *
38
+ * Line 1 answers "what tier am I on, and how full is the window". Line 2
39
+ * answers "what is this session DOING". Every segment on both is read from
40
+ * disk or from the payload; none of it costs a model call.
41
+ *
42
+ * Three segments on line 2 carry a rule worth stating here, because each is a
43
+ * place where the easy version would lie:
44
+ * - `status:` is the ONLY segment allowed to vanish. A phase the disk cannot
45
+ * prove is HIDDEN, never guessed — see the phase-rail block below for what
46
+ * that costs and why it is still the right trade.
47
+ * - `MTok` is MAIN TOKEN: this session's own turns. Claude Code records no
48
+ * token usage for a dispatched subagent, so an hour of Opus executors adds
49
+ * almost nothing. An em dash means not measured; `0` would mean free.
50
+ * - `ucs` is a delta of an ACCOUNT-WIDE window, not a private meter.
27
51
  *
28
52
  * This is the ONLY place Claude Code exposes the live model id, so it also
29
53
  * writes a fail-silent session-model bridge (.claude/orc/session-model.json)
@@ -34,6 +58,12 @@
34
58
  * Also appends a "newer orc version available" hint from the 24h update cache
35
59
  * (cache-only here — never a network call in the statusline hot path; the
36
60
  * PreToolUse guard refreshes the cache when /orc is invoked).
61
+ *
62
+ * Everything on line 2 rides in ONE scan, throttled to 5s and cached in the
63
+ * per-session ledger, because a statusline re-renders on every keystroke and
64
+ * anything unthrottled here is a per-keystroke disk scan. `MTok` additionally
65
+ * reads only the bytes the transcript has GROWN by. There is exactly one seam
66
+ * over that budget — ORC_STATUSLINE_SCAN_MS — and nothing in ORC ever sets it.
37
67
  */
38
68
 
39
69
  // Opus 4.8 / Opus 5 / Fable 5 are matched by tolerant regexes below (accept
@@ -49,6 +79,383 @@ try {
49
79
  updater = null;
50
80
  }
51
81
 
82
+ // ── The phase rail, the motifs, and the two seams (v1.2.1) ──────────────────
83
+ // `status:` says which phase an ORC run is in. Three rules hold it up.
84
+ //
85
+ // 1. THE CLI COMPUTES, THIS FILE RENDERS. The phase ids, their order, their
86
+ // labels and their motif kind all come from `orc-lane-rails.json`, which
87
+ // `orc init` / `orc update` generates from the CLI's own registries. This
88
+ // hook holds no idea of what ORC's phases are. A second phase table here
89
+ // would be the Flow-stepper failure on a second surface, and no lint could
90
+ // see it. Frames are the exception and belong here: they are presentation,
91
+ // and a motif change must not need a reinstall.
92
+ //
93
+ // 2. A PHASE THE DISK CANNOT PROVE IS HIDDEN. Never guessed, never carried
94
+ // over from a minute ago. A stale phase word gets believed — the same
95
+ // reasoning as `unknown is not low` and `unknown is not zero`.
96
+ //
97
+ // 3. THE FLOOR IS HOOK-WRITTEN. `PHASE-EDGE <family>` and `SPAWN <agent>` are
98
+ // written by orc-trace.js with zero model cooperation. A trace verb the
99
+ // orchestrator narrated is allowed to REFINE that (it is more specific:
100
+ // `Q3 DO` rather than `execution`) but only when it is later in the file,
101
+ // and only when that lane's own rail publishes the verb — so it can sharpen
102
+ // the answer and can never invent one.
103
+ //
104
+ // What this cannot see, stated so nobody reads a blank as a bug:
105
+ // - a phase that dispatches nothing AND narrates nothing is INVISIBLE
106
+ // (/orc-quick Q1 LOOK and Q2 ASK, and every ask-the-user gate);
107
+ // - a CONTINUED agent emits no PreToolUse/SubagentStop pair, so the skeleton
108
+ // is a floor, never a census (orc-trace.js documents this);
109
+ // - `orc extra` runs a worker through Bash, so a foreign wave writes no
110
+ // SPAWN and resolves only through its narrated `EXTRA` verb, or hides.
111
+ //
112
+ // The animation is a LIVENESS TELL, not a driven animation. A statusline is a
113
+ // pull surface — Claude Code re-renders it, ORC cannot — so the frame is picked
114
+ // off the wall clock. It advances while you type and while turns land, and it
115
+ // FREEZES when the session is idle, which is true and is the point.
116
+ //
117
+ // Two env seams, for tests and for terminals, and nothing in ORC ever sets
118
+ // either: ORC_STATUSLINE_ASCII=1 swaps the glyph set, ORC_STATUSLINE_MOTION=0
119
+ // REMOVES motion rather than slowing it (a frozen frame of a cycling animation
120
+ // is a bug that looks like a hang — the web panel learned this at v0.44.0, and
121
+ // so the still frame is designed as a still frame: frame 0 of each set).
122
+
123
+ const PHASE_STALE_MS = 10 * 60 * 1000;
124
+
125
+ const MOTIFS = {
126
+ look: { u: ["◔", "◑", "◕", "●"], a: [".", "o", "O", "0"], ms: 260 },
127
+ ask: { u: ["?", "¿", "?", "·"], a: ["?", "?", "?", "."], ms: 500 },
128
+ plan: { u: ["▁", "▃", "▅", "▇"], a: ["_", "-", "=", "#"], ms: 220 },
129
+ do: { u: ["▰", "▱", "▰", "▱"], a: ["=", "-", "=", "-"], ms: 180 },
130
+ check: { u: ["◇", "◈", "◆", "◈"], a: ["<", "=", ">", "="], ms: 240 },
131
+ ship: { u: ["›", "»", "≫", "»"], a: [">", ">", "=", ">"], ms: 200 },
132
+ wait: { u: ["·", "˙", "·", "˙"], a: [".", "'", ".", "'"], ms: 700 },
133
+ generic: {
134
+ u: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
135
+ a: ["-", "\\", "|", "/"],
136
+ ms: 90,
137
+ },
138
+ };
139
+
140
+ function motifFrame(kind) {
141
+ const m = MOTIFS[kind] || MOTIFS.generic;
142
+ const frames = process.env.ORC_STATUSLINE_ASCII === "1" ? m.a : m.u;
143
+ if (process.env.ORC_STATUSLINE_MOTION === "0") return frames[0];
144
+ return frames[Math.floor(Date.now() / m.ms) % frames.length];
145
+ }
146
+
147
+ // The rail manifest, read once per process. Absent (a pre-1.2.1 install, or a
148
+ // write that failed) → null → `status:` hides. Never throws.
149
+ let RAILS = undefined;
150
+ function rails() {
151
+ if (RAILS !== undefined) return RAILS;
152
+ RAILS = null;
153
+ try {
154
+ const j = JSON.parse(
155
+ require("fs").readFileSync(
156
+ require("path").join(__dirname, "orc-lane-rails.json"),
157
+ "utf8"
158
+ )
159
+ );
160
+ if (j && j.lanes) RAILS = j;
161
+ } catch (_) {}
162
+ return RAILS;
163
+ }
164
+
165
+ // Which phase is the run in? Called once per throttled scan, over text the scan
166
+ // has already read, so it costs one regex pass and no extra I/O.
167
+ //
168
+ // `text` is the active trace, `laneToken` its filename's lane. Returns
169
+ // {lane, label, kind} or null — and null is a real answer.
170
+ function resolvePhase(laneToken, text) {
171
+ const r = rails();
172
+ if (!r || !laneToken) return null;
173
+ const row = r.lanes[laneToken];
174
+ if (!row) return null;
175
+
176
+ // Every line carries its own timestamp; the newest one dates the run. A trace
177
+ // whose last line is old is not a run in progress, whatever it says.
178
+ const at = (l) => {
179
+ const t = /^\[(\d{2})(\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(l);
180
+ if (!t) return 0;
181
+ return new Date(
182
+ 2000 + Number(t[3]), Number(t[2]) - 1, Number(t[1]),
183
+ Number(t[4]), Number(t[5]), Number(t[6])
184
+ ).getTime();
185
+ };
186
+
187
+ const lines = text.split("\n");
188
+ let newest = 0;
189
+ let edgeIdx = -1;
190
+ let edgeFam = null;
191
+ let verbIdx = -1;
192
+ let verbPhase = null;
193
+ for (let i = 0; i < lines.length; i++) {
194
+ const l = lines[i];
195
+ if (!l) continue;
196
+ const ts = at(l);
197
+ if (ts > newest) newest = ts;
198
+ const e = /PHASE-EDGE ([a-z-]+)/.exec(l);
199
+ if (e && r.families[e[1]]) {
200
+ edgeIdx = i;
201
+ edgeFam = e[1];
202
+ }
203
+ // A verb line is the orchestrator's. It counts only when this lane's own
204
+ // rail published that verb — the rail is what stops a narrated word from
205
+ // inventing a phase the lane does not have.
206
+ const body = l.replace(/^\[[^\]]*\]\s*\S+\s*/, "");
207
+ for (const p of row.phases) {
208
+ for (const v of p.verbs) {
209
+ if (body.indexOf(v) === 0) {
210
+ verbIdx = i;
211
+ verbPhase = p;
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ if (!newest || Date.now() - newest > PHASE_STALE_MS) return null;
218
+
219
+ if (verbPhase && verbIdx > edgeIdx)
220
+ return { lane: laneToken, label: verbPhase.label, kind: verbPhase.kind };
221
+ if (edgeFam) {
222
+ const fam = r.families[edgeFam];
223
+ return { lane: laneToken, label: fam.label, kind: fam.kind };
224
+ }
225
+ if (verbPhase) return { lane: laneToken, label: verbPhase.label, kind: verbPhase.kind };
226
+ return null;
227
+ }
228
+
229
+ // The current branch, without a subprocess. A statusline re-renders on every
230
+ // keystroke, so `git rev-parse` here would be one process per keystroke.
231
+ // Anything unrecognised returns null and the segment is simply absent.
232
+ function gitBranch(projectDir) {
233
+ const fs = require("fs");
234
+ const path = require("path");
235
+ try {
236
+ const dot = path.join(projectDir, ".git");
237
+ let gitDir = dot;
238
+ if (fs.statSync(dot).isFile()) {
239
+ // A worktree or a submodule: `.git` is a pointer file.
240
+ const m = /gitdir:\s*(.+)/.exec(fs.readFileSync(dot, "utf8"));
241
+ if (!m) return null;
242
+ const g = m[1].trim();
243
+ gitDir = path.isAbsolute(g) ? g : path.join(projectDir, g);
244
+ }
245
+ const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
246
+ const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
247
+ const name = ref
248
+ ? ref[1]
249
+ : /^[0-9a-f]{40}$/.test(head)
250
+ ? "@" + head.slice(0, 7)
251
+ : null;
252
+ if (!name) return null;
253
+ return name.length > 24 ? name.slice(0, 23) + "…" : name;
254
+ } catch (_) {
255
+ return null;
256
+ }
257
+ }
258
+
259
+ // MTok — MAIN TOKEN. The tokens THIS session's own turns consumed, summed from
260
+ // the session transcript's `usage` blocks.
261
+ //
262
+ // Two honesty rules ship with it.
263
+ //
264
+ // It is the MAIN session only. Claude Code records NO token usage for a
265
+ // dispatched subagent (v1.2.0 verified that across every transcript on two
266
+ // machines), so a wave of three Opus executors adds almost nothing here. That
267
+ // is why an unreadable transcript renders an em dash and NEVER `0` — a zero
268
+ // would say the session was free.
269
+ //
270
+ // It is all four kinds summed. `/orc-budget`'s rule is four kinds never
271
+ // blended, and that rule is about REPORTS; this is one cell on a status bar.
272
+ // Any subset ORC picked would be a weighting ORC invented, which is worse. The
273
+ // vector stays authoritative in `orc usage report`, and the ledger below keeps
274
+ // all four kinds so it always can be.
275
+ //
276
+ // The read is INCREMENTAL. A transcript is append-only and reaches tens of
277
+ // megabytes; re-reading it inside a 5-second loop is the per-keystroke disk
278
+ // hazard the throttle exists to prevent. Only bytes past the stored offset are
279
+ // read, and the offset resets when the file shrinks or the path changes.
280
+ function scanTokens(led, transcriptPath) {
281
+ const fs = require("fs");
282
+ const fresh = (p) => ({
283
+ path: p, offset: 0, size: 0, input: 0, cache_write: 0, cache_read: 0, output: 0,
284
+ });
285
+ if (!transcriptPath) return led.tok || null;
286
+ let prev = led.tok && led.tok.path === transcriptPath ? led.tok : fresh(transcriptPath);
287
+ let st;
288
+ try {
289
+ st = fs.statSync(transcriptPath);
290
+ } catch (_) {
291
+ return prev.offset ? prev : null; // never read it → em dash, not 0
292
+ }
293
+ // Truncated or rotated under us: everything counted so far is unprovable.
294
+ if (st.size < prev.size) prev = fresh(transcriptPath);
295
+ if (st.size > prev.offset) {
296
+ let chunk = "";
297
+ try {
298
+ const fd = fs.openSync(transcriptPath, "r");
299
+ const len = st.size - prev.offset;
300
+ const buf = Buffer.alloc(len);
301
+ fs.readSync(fd, buf, 0, len, prev.offset);
302
+ fs.closeSync(fd);
303
+ chunk = buf.toString("utf8");
304
+ } catch (_) {
305
+ return prev.offset ? prev : null;
306
+ }
307
+ // The final line may be half-written. Stop at the last newline and leave
308
+ // the remainder for the next scan.
309
+ const cut = chunk.lastIndexOf("\n");
310
+ if (cut >= 0) {
311
+ const whole = chunk.slice(0, cut);
312
+ prev.offset += Buffer.byteLength(whole, "utf8") + 1;
313
+ for (const l of whole.split("\n")) {
314
+ if (!l || l.indexOf('"usage"') === -1) continue;
315
+ let u = null;
316
+ try {
317
+ const j = JSON.parse(l);
318
+ u = (j && j.message && j.message.usage) || (j && j.usage) || null;
319
+ } catch (_) {}
320
+ if (!u) continue;
321
+ prev.input += Number(u.input_tokens) || 0;
322
+ prev.cache_write += Number(u.cache_creation_input_tokens) || 0;
323
+ prev.cache_read += Number(u.cache_read_input_tokens) || 0;
324
+ prev.output += Number(u.output_tokens) || 0;
325
+ }
326
+ }
327
+ }
328
+ prev.size = st.size;
329
+ return prev;
330
+ }
331
+
332
+ function fmtTokens(tok) {
333
+ if (!tok) return null;
334
+ const n =
335
+ (tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0);
336
+ if (n <= 0) return null;
337
+ if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
338
+ if (n >= 1000) return Math.round(n / 1000) + "K";
339
+ return String(n);
340
+ }
341
+
342
+ // ── The per-session ledger: ONE read, ONE write, ONE throttle (v1.3.0 W0) ───
343
+ // `.claude/orc/usage-session.json` is the per-session ledger, and three
344
+ // separate blocks below want it: the rate-limit tracker, `ucs`, and the
345
+ // throttled line-2 scan. Each used to open the file itself and two of them
346
+ // wrote it, so one render cost three reads and two writes — on a surface that
347
+ // re-renders on every keystroke. It is memoised here instead: loaded at most
348
+ // once per process, mutated in place by whoever needs it, and flushed exactly
349
+ // once at the end. Same rules as before — RAW numbers only, never a computed
350
+ // word, fail-silent, and the reader decides what it means.
351
+ //
352
+ // The scan interval is the ONE seam over this budget, on the ORC_TEST_PROBE_MS
353
+ // precedent: a test that proves the throttle by SLEEPING past it is a test that
354
+ // fails on a loaded machine, and a flake is recorded and removed, never retried
355
+ // away. Unset, this is byte-identical to a hardcoded 5000, and nothing in ORC
356
+ // ever sets it.
357
+ let LED = null;
358
+ let LED_FILE = null;
359
+
360
+ // The scan's own answers, in ONE shape, so a composed layout (v1.3.0) reads
361
+ // exactly what the shipped lines read. It is populated as the blocks below
362
+ // compute their segments — never by a second pass over the disk. A provider
363
+ // nothing binds is simply never filled in, and every binding over it answers
364
+ // null, which renders an em dash. UNKNOWN IS NOT ZERO.
365
+ const SCAN = {
366
+ spawns: 0, running: 0, lanes: [], phase: null, slug: null,
367
+ branch: null, head: null, wiki: null, diy: null,
368
+ extra_enabled: false, update_version: null, inflight: null,
369
+ trace_age_min: null, trace_state: null, last_agent: null, retries: null,
370
+ };
371
+
372
+ function ledger(projectDir, sid) {
373
+ if (LED) return LED;
374
+ const fs = require("fs");
375
+ const path = require("path");
376
+ LED_FILE = path.join(projectDir, ".claude", "orc", "usage-session.json");
377
+ let led = null;
378
+ try {
379
+ led = JSON.parse(fs.readFileSync(LED_FILE, "utf8"));
380
+ } catch (_) {}
381
+ if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
382
+ LED = led;
383
+ return LED;
384
+ }
385
+
386
+ function ledgerFlush() {
387
+ if (!LED || !LED_FILE) return;
388
+ try {
389
+ const fs = require("fs");
390
+ const path = require("path");
391
+ LED.updated_at = Date.now();
392
+ fs.mkdirSync(path.dirname(LED_FILE), { recursive: true });
393
+ fs.writeFileSync(LED_FILE, JSON.stringify(LED) + "\n");
394
+ } catch (_) {}
395
+ }
396
+
397
+ function scanEveryMs() {
398
+ const n = Number(process.env.ORC_STATUSLINE_SCAN_MS);
399
+ return Number.isFinite(n) && n >= 0 ? n : 5000;
400
+ }
401
+
402
+ // Per-provider TTL (v1.3.0 W2), replacing the single global throttle for
403
+ // everything but the trace scan. A wiki tier does not move in five seconds and
404
+ // a flow lock moves when somebody runs a command, so paying the 5-second rate
405
+ // for either is paying for a change that cannot have happened.
406
+ //
407
+ // `ORC_STATUSLINE_SCAN_MS` still overrides ALL of them — it stays the ONE seam
408
+ // over this budget, and a seam that only covered one provider would be a seam
409
+ // tests could not use.
410
+ const TTL = {
411
+ trace: 5000, // a run moves; this is the one thing that really is that fast
412
+ git: 5000, // .git/HEAD, no subprocess
413
+ wiki: 60000, // a wiki tier does not move in five seconds
414
+ config: 30000, // a config file is edited by hand
415
+ diy: 30000, // a flow lock moves when somebody runs a command
416
+ knowledge: 60000, // a pattern cache, a peer list, a gotcha count
417
+ extra: 15000, // a spend log DOES move during a wave
418
+ run: 10000, // RESUME.md is rewritten at every stop
419
+ gates: 60000, // a pact does not drift between keystrokes
420
+ };
421
+
422
+ // `at` is a ledger `scanned_at` stamp. Absent is stale — unknown is not fresh.
423
+ function scanStale(at, now, ttl) {
424
+ const budget = process.env.ORC_STATUSLINE_SCAN_MS !== undefined ? scanEveryMs() : ttl == null ? scanEveryMs() : ttl;
425
+ return typeof at !== "number" || now - at >= budget;
426
+ }
427
+
428
+ // The read plan, from the compiled layout's lock. Returns null in EVERY state
429
+ // but one — the feature off, no lock, an unparseable lock — and null means
430
+ // "read everything", which is what makes `off` byte-identical.
431
+ //
432
+ // It deliberately does NOT re-run the gate ladder. This is a question about
433
+ // what to READ, and the ladder is a question about what to RENDER; a layout
434
+ // that later fails the ladder falls back to the shipped lines and simply has a
435
+ // segment or two missing for one render. That is a far better failure than
436
+ // reading the disk twice on a surface that re-renders on every keystroke.
437
+ function readPlan(d) {
438
+ try {
439
+ const fs = require("fs");
440
+ const path = require("path");
441
+ const projectDir =
442
+ (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
443
+ const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
444
+ if (!/^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw)) return null;
445
+ const lock = JSON.parse(
446
+ fs.readFileSync(path.join(projectDir, ".claude", "orc", "statusline.lock.json"), "utf8")
447
+ );
448
+ if (!lock || !Array.isArray(lock.bindings)) return null;
449
+ return {
450
+ bindings: new Set(lock.bindings),
451
+ providers: new Set(lock.providers || []),
452
+ series: new Set(lock.series || []),
453
+ };
454
+ } catch (_) {
455
+ return null;
456
+ }
457
+ }
458
+
52
459
  let raw = "";
53
460
  process.stdin.on("data", (c) => (raw += c));
54
461
  process.stdin.on("end", () => {
@@ -64,9 +471,26 @@ process.stdin.on("end", () => {
64
471
  const effort = String((d.effort && d.effort.level) || "").toLowerCase();
65
472
  const pct =
66
473
  d.context_window && typeof d.context_window.used_percentage === "number"
67
- ? `${d.context_window.used_percentage}% ctx`
474
+ ? `context (${d.context_window.used_percentage}%)`
68
475
  : "";
69
476
 
477
+ // ── THE READ PLAN (v1.3.0 W2) ──────────────────────────────────────────────
478
+ // The compiler knows exactly which bindings a layout uses, therefore exactly
479
+ // which providers it needs, and it records both in the lock. So: A PROVIDER
480
+ // NOTHING BINDS IS NOT READ.
481
+ //
482
+ // That is the claim this feature has to earn — the shipped status line pays
483
+ // for the wiki read, the diy read and the trace scan unconditionally, and a
484
+ // user whose layout names none of them should pay for none of them. Composing
485
+ // your own line is allowed to make the bar FASTER than the hardcoded one it
486
+ // replaces, and here is where that happens.
487
+ //
488
+ // `null` means the feature is off, and then everything below runs exactly as
489
+ // it always has. That is what keeps `off` byte-identical.
490
+ const PLAN = readPlan(d);
491
+ const wants = (b) => !PLAN || PLAN.bindings.has(b);
492
+ const wantsProvider = (p) => !PLAN || PLAN.providers.has(p);
493
+
70
494
  // ── Session-model bridge (fail-silent) ─────────────────────────────────────
71
495
  // The PreToolUse effort guard cannot see the model id; it can only read
72
496
  // effort. Persist {model_id, effort, written_at} here so the guard can grant
@@ -141,9 +565,7 @@ process.stdin.on("end", () => {
141
565
  // bank what was consumed before the reset into `accumulated` and
142
566
  // re-baseline, so the running total keeps counting across the boundary.
143
567
  const sid = String(d.session_id || d.sessionId || "");
144
- const sfile = path.join(orcDir, "usage-session.json");
145
- let led = null;
146
- try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
568
+ const led = ledger(projectDir, sid);
147
569
  const pctOf = (o) => (o && typeof o.used_percentage === "number" ? o.used_percentage : null);
148
570
  const track = (prev, cur) => {
149
571
  if (cur == null) return prev || null;
@@ -157,13 +579,10 @@ process.stdin.on("end", () => {
157
579
  };
158
580
  return { baseline: prev.baseline, last: cur, accumulated: prev.accumulated, resets: prev.resets };
159
581
  };
160
- if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
161
582
  led.five_hour = track(led.five_hour, pctOf(rl0 && rl0.five_hour));
162
583
  led.seven_day = track(led.seven_day, pctOf(rl0 && rl0.seven_day));
163
584
  led.context_used_percentage =
164
585
  cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null;
165
- led.updated_at = Date.now();
166
- fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
167
586
  }
168
587
 
169
588
  } catch (_) {}
@@ -262,34 +681,58 @@ process.stdin.on("end", () => {
262
681
  for (const u of usageBad) reasons.push(u);
263
682
  }
264
683
 
684
+ // The installed ORC version (v1.2.1). It replaces the verdict WORD, not the
685
+ // verdict: the icon still carries that, and the degrade branch still names
686
+ // every reason — dropping those would turn the loudest safety segment in ORC
687
+ // into an emoji. A version we cannot read renders as plain `ORC`, never
688
+ // `ORC vnull`: the statusline never prints a word for a thing it does not know.
689
+ let ver = null;
690
+ try {
691
+ ver = updater ? updater.installedVersion(__dirname) : null;
692
+ } catch (_) {}
693
+ if (!ver) {
694
+ try {
695
+ ver = JSON.parse(
696
+ require("fs").readFileSync(
697
+ require("path").join(__dirname, "orc-version.json"),
698
+ "utf8"
699
+ )
700
+ ).version || null;
701
+ } catch (_) {}
702
+ }
703
+ const brand = "ORC" + (ver ? " v" + ver : "");
704
+
265
705
  let line;
266
706
  if (verdict === "ready") {
267
- line = `✅ ORC-ready ${tier}${pct ? " · " + pct : ""}`;
707
+ line = `✅ ${brand} - ${tier}${pct ? " · " + pct : ""}`;
268
708
  } else if (verdict === "boosted") {
269
- line = `🚀 ORC-boosted ${tier}${pct ? " · " + pct : ""}`;
709
+ line = `🚀 ${brand} - ${tier}${pct ? " · " + pct : ""}`;
270
710
  } else {
271
- line = `⛔ ORC WILL DEGRADE (${reasons.join(", ")}) — now: ${tier}${pct ? " · " + pct : ""}`;
711
+ line = `⛔ ${brand} - ${tier} (${reasons.join(", ")})${pct ? " · " + pct : ""}`;
272
712
  }
273
713
 
274
714
  // Subscription-usage segment (rendered after ctx, before wiki). Empty on
275
715
  // older Claude Code that doesn't surface `rate_limits`.
276
716
  if (rlSeg) line += " · " + rlSeg;
277
717
 
278
- // How far the window moved while THIS session ran (v1.2.0). The ledger below
279
- // keeps the raw numbers; this renders the delta. Never shown as "this session
280
- // used X%" the window is per ACCOUNT, and a second terminal moves it too.
718
+ // ucs — USAGE, CURRENT SESSION. How far the 5-hour window moved while THIS
719
+ // session ran (v1.2.0; renamed v1.2.1). The ledger below keeps the raw
720
+ // numbers; this renders the delta.
721
+ //
722
+ // It KEEPS ITS SLOT at zero. "This session has consumed nothing measurable
723
+ // yet" and "this build has no ucs segment" are different facts and must not
724
+ // look the same — the same rule as `lanes: none yet` and `used 0/20`.
725
+ //
726
+ // And it is still a delta of an ACCOUNT-WIDE window, not a private meter: a
727
+ // second terminal moves it too.
281
728
  try {
282
- const fs = require("fs");
283
- const path = require("path");
284
729
  const projectDir =
285
730
  (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
286
- const led = JSON.parse(
287
- fs.readFileSync(path.join(projectDir, ".claude", "orc", "usage-session.json"), "utf8")
288
- );
731
+ const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
289
732
  const w = led && led.five_hour;
290
733
  if (w && typeof w.last === "number" && typeof w.baseline === "number") {
291
734
  const used = Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline));
292
- if (used > 0) line += " · sess +" + used + "%";
735
+ line += " · ucs " + used + "%";
293
736
  }
294
737
  } catch (_) {}
295
738
 
@@ -298,43 +741,85 @@ process.stdin.on("end", () => {
298
741
  // wiki / no git / any error → no segment. Thresholds mirror the config
299
742
  // defaults (wiki_fresh_max 10 / wiki_aging_max 30); the hook can't read the
300
743
  // resolved config, so a user override shifts skill behavior, not this label.
744
+ //
745
+ // The git distance rides in the SAME throttled scan as everything on line 2
746
+ // (v1.3.0 W0). It used to be an `execSync` on every render — one child
747
+ // process PER KEYSTROKE in any repo with a wiki, which is the exact hazard
748
+ // the throttle exists to prevent. The ledger caches the RAW facts (a commit
749
+ // count, a boolean) and never the word: `fresh` / `AGING` / `STALE` is
750
+ // computed here, on read, every time.
301
751
  try {
302
752
  const fs = require("fs");
303
753
  const path = require("path");
304
- const { execSync } = require("child_process");
305
754
  const projectDir =
306
755
  (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
307
- const metaPath = path.join(projectDir, ".claude", "orc", "wiki-meta.json");
308
- if (!fs.existsSync(metaPath)) {
309
- // Docs but no manifest = UNREGISTERED: a real wiki nothing has indexed
310
- // (usually a scan stopped at a 5-area pause). It is otherwise invisible —
311
- // consumers and `orc crosslink` read the manifest — so surface it here,
312
- // with the free fix. Never say "no wiki": these docs are already paid for.
313
- const wikiDir = path.join(projectDir, "wiki");
314
- const docs =
315
- fs.existsSync(wikiDir) &&
316
- fs.readdirSync(wikiDir).some((f) => f.startsWith("orc-") && f.endsWith(".md"));
317
- if (docs) line += " · wiki: UNREGISTERED (run `orc wiki sync`)";
318
- } else {
319
- const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
320
- if (meta && meta.scan_commit) {
321
- const distance = parseInt(
322
- execSync(`git rev-list --count ${meta.scan_commit}..HEAD`, {
323
- cwd: projectDir,
324
- timeout: 3000,
325
- stdio: ["ignore", "pipe", "ignore"],
326
- })
327
- .toString()
328
- .trim(),
329
- 10
330
- );
331
- if (Number.isFinite(distance)) {
332
- if (distance >= 10 && distance <= 30)
333
- line += ` · wiki: AGING (${distance}c)`;
334
- else if (distance > 30) line += ` · wiki: STALE (${distance}c)`;
335
- else line += " · wiki: fresh";
756
+ // A layout that names no wiki component performs ZERO wiki reads — not a
757
+ // cheaper one, none. This is the read planner's whole point.
758
+ if (!wants("wiki.tier") && !wants("wiki.distance")) throw new Error("not needed");
759
+ const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
760
+ const now = Date.now();
761
+ // A wiki tier does not move in five seconds. Per-provider TTL replaces the
762
+ // single global throttle for everything but the trace scan, which is the
763
+ // one thing that really does move that fast.
764
+ if (scanStale(led.wiki && led.wiki.scanned_at, now, TTL.wiki)) {
765
+ const w = { unregistered: false, distance: null, scanned_at: now };
766
+ const metaPath = path.join(projectDir, ".claude", "orc", "wiki-meta.json");
767
+ if (!fs.existsSync(metaPath)) {
768
+ // Docs but no manifest = UNREGISTERED: a real wiki nothing has indexed
769
+ // (usually a scan stopped at a 5-area pause). It is otherwise invisible
770
+ // consumers and `orc crosslink` read the manifest — so surface it
771
+ // here, with the free fix. Never say "no wiki": these docs are already
772
+ // paid for.
773
+ const wikiDir = path.join(projectDir, "wiki");
774
+ w.unregistered =
775
+ fs.existsSync(wikiDir) &&
776
+ fs.readdirSync(wikiDir).some((f) => f.startsWith("orc-") && f.endsWith(".md"));
777
+ } else {
778
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
779
+ if (meta && meta.scan_commit) {
780
+ // A FAILED probe is a fact, and it is cached like any other. No git,
781
+ // a detached commit, a timeout without this inner catch the whole
782
+ // block aborts before the ledger is stamped, and the subprocess runs
783
+ // again on the very next keystroke. The segment stays absent either
784
+ // way; what changes is that it costs nothing to stay absent.
785
+ try {
786
+ const { execSync } = require("child_process");
787
+ const n = parseInt(
788
+ execSync(`git rev-list --count ${meta.scan_commit}..HEAD`, {
789
+ cwd: projectDir,
790
+ timeout: 3000,
791
+ stdio: ["ignore", "pipe", "ignore"],
792
+ })
793
+ .toString()
794
+ .trim(),
795
+ 10
796
+ );
797
+ if (Number.isFinite(n)) w.distance = n;
798
+ } catch (_) {}
336
799
  }
337
800
  }
801
+ led.wiki = w;
802
+ }
803
+ const w = led.wiki;
804
+ if (w) {
805
+ SCAN.wiki = {
806
+ distance: typeof w.distance === "number" ? w.distance : null,
807
+ tier: w.unregistered
808
+ ? "unregistered"
809
+ : typeof w.distance !== "number"
810
+ ? null
811
+ : w.distance > 30
812
+ ? "stale"
813
+ : w.distance >= 10
814
+ ? "aging"
815
+ : "fresh",
816
+ };
817
+ }
818
+ if (w && w.unregistered) line += " · wiki: UNREGISTERED (run `orc wiki sync`)";
819
+ else if (w && typeof w.distance === "number") {
820
+ if (w.distance >= 10 && w.distance <= 30) line += ` · wiki: AGING (${w.distance}c)`;
821
+ else if (w.distance > 30) line += ` · wiki: STALE (${w.distance}c)`;
822
+ else line += " · wiki: fresh";
338
823
  }
339
824
  } catch (_) {}
340
825
 
@@ -347,6 +832,8 @@ process.stdin.on("end", () => {
347
832
  const crypto = require("crypto");
348
833
  const projectDir =
349
834
  (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
835
+ if (!wants("diy.state") && !wants("diy.name") && !wants("diy.tier_state"))
836
+ throw new Error("not needed");
350
837
  const lockPath = path.join(projectDir, ".claude", "orc", "diy", "flow.lock.json");
351
838
  if (fs.existsSync(lockPath)) {
352
839
  const lock = JSON.parse(
@@ -397,6 +884,23 @@ process.stdin.on("end", () => {
397
884
  } catch (_) {}
398
885
  }
399
886
 
887
+ // The branch is its OWN provider: `.git/HEAD` with no subprocess, on a
888
+ // different clock from the trace scan, and a layout that shows a branch and
889
+ // nothing else must not pay for a trace scan to get it.
890
+ if (wantsProvider("scan.git")) {
891
+ try {
892
+ const projectDir =
893
+ (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
894
+ SCAN.branch = gitBranch(projectDir) || null;
895
+ } catch (_) {}
896
+ }
897
+
898
+ // The extended scan (v1.3.0 W3). Only a composed layout can reach it, and
899
+ // only for the providers its own lock names.
900
+ extendedScan(d, wants, wantsProvider);
901
+ // The sparkline series, sampled during the scan that already ran.
902
+ sampleSeries(d, PLAN);
903
+
400
904
  // ── Session line (v1.2.0) ──────────────────────────────────────────────────
401
905
  // Line 1 answers "what tier am I on and how full is the window". This second
402
906
  // line answers "what has this session actually been DOING" — how many agents
@@ -416,13 +920,7 @@ process.stdin.on("end", () => {
416
920
  const path = require("path");
417
921
  const projectDir =
418
922
  (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
419
- const orcDir = path.join(projectDir, ".claude", "orc");
420
- const sfile = path.join(orcDir, "usage-session.json");
421
- const sid = String(d.session_id || d.sessionId || "");
422
-
423
- let led = null;
424
- try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
425
- if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
923
+ const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
426
924
 
427
925
  // The hook cannot read the RESOLVED config — that is the lane resolver's
428
926
  // job, and a hook has no lane — so this reads the two raw keys it needs
@@ -439,32 +937,39 @@ process.stdin.on("end", () => {
439
937
  } catch (_) {}
440
938
 
441
939
  const now = Date.now();
442
- // The scan interval is the ONE seam over this budget, on the
443
- // ORC_TEST_PROBE_MS precedent: a test that proves the throttle by SLEEPING
444
- // past it is a test that fails on a loaded machine, and a flake is recorded
445
- // and removed, never retried away. Unset, this is byte-identical to a
446
- // hardcoded 5000, and nothing in ORC ever sets it.
447
- const scanEvery = (() => {
448
- const n = Number(process.env.ORC_STATUSLINE_SCAN_MS);
449
- return Number.isFinite(n) && n >= 0 ? n : 5000;
450
- })();
451
- const stale = !led.dispatch || typeof led.dispatch.scanned_at !== "number" ||
452
- now - led.dispatch.scanned_at >= scanEvery;
453
- if (stale) {
940
+ if (!wantsProvider("scan.trace")) throw new Error("not needed");
941
+ if (scanStale(led.dispatch && led.dispatch.scanned_at, now, TTL.trace)) {
454
942
  const logDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, logRel);
455
943
  const sessionFloor = Math.floor((led.started_at || 0) / 1000) * 1000;
456
944
  let spawns = 0;
457
945
  let running = 0;
458
946
  const lanes = [];
947
+ // The ACTIVE run, for `status:` (v1.2.1). `.current` is the pointer the
948
+ // lanes and the trace hook both write, so it — not "the newest file" — is
949
+ // what names the run in progress. A pointer naming a file that is not
950
+ // there answers nothing, and a pointer nobody deleted is not a run: the
951
+ // resolver's own staleness gate settles that from the trace's last line.
952
+ let activeFile = null;
953
+ try {
954
+ const cur = fs.readFileSync(path.join(logDir, ".current"), "utf8").trim();
955
+ if (cur) activeFile = cur;
956
+ } catch (_) {}
957
+ let phase = null;
459
958
  try {
460
959
  for (const f of fs.readdirSync(logDir)) {
461
960
  if (!f.startsWith("run-") || !f.endsWith(".txt")) continue;
462
961
  const full = path.join(logDir, f);
463
- // Only traces touched since this session began. A trace from last
464
- // week is not this session's spend.
465
962
  let st;
466
963
  try { st = fs.statSync(full); } catch (_) { continue; }
467
- if (st.mtimeMs < (led.started_at || 0)) continue;
964
+ const active = f === activeFile;
965
+ // Only traces touched since this session began count towards the
966
+ // session's SPEND — a trace from last week is not this session's.
967
+ // The ACTIVE run is exempt from that filter: `status:` answers "what
968
+ // is ORC doing", which has nothing to do with who paid for it, and a
969
+ // run already going when this session opened would otherwise be
970
+ // invisible until it happened to write its next line. Its own
971
+ // staleness gate decides whether it is still running.
972
+ if (!active && st.mtimeMs < (led.started_at || 0)) continue;
468
973
  const text = fs.readFileSync(full, "utf8");
469
974
  // Count by the trace's OWN line timestamps, not the file's mtime. A
470
975
  // run that was already going when this session started shares its
@@ -486,6 +991,12 @@ process.stdin.on("end", () => {
486
991
  if (at >= sessionFloor) mine += 1;
487
992
  }
488
993
  spawns += mine;
994
+ // The phase comes off the text this loop already read — one more
995
+ // regex pass, no second file read.
996
+ if (active) {
997
+ const lm = /^run-([a-z0-9-]+?)-.+-\d{6}-\d{6}\.txt$/.exec(f);
998
+ if (lm) phase = resolvePhase(lm[1], text);
999
+ }
489
1000
  let openHere = 0;
490
1001
  try {
491
1002
  const pend = JSON.parse(fs.readFileSync(full + ".pending.json", "utf8"));
@@ -500,32 +1011,524 @@ process.stdin.on("end", () => {
500
1011
  }
501
1012
  }
502
1013
  } catch (_) {}
503
- led.dispatch = { spawns, running, lanes, scanned_at: now };
1014
+ led.dispatch = { spawns, running, lanes, phase, scanned_at: now };
1015
+ // MTok rides in the same throttled pass, and reads only the bytes the
1016
+ // transcript has grown by since the last one.
1017
+ led.tok = scanTokens(led, d.transcript_path || null);
504
1018
  }
505
1019
 
506
- led.updated_at = now;
507
- try {
508
- fs.mkdirSync(orcDir, { recursive: true });
509
- fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
510
- } catch (_) {}
511
-
512
- const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [] };
1020
+ const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [], phase: null };
1021
+ // The composed layout reads THESE — the same numbers the shipped line
1022
+ // below prints, from the same scan.
1023
+ SCAN.spawns = dsp.spawns || 0;
1024
+ SCAN.running = dsp.running || 0;
1025
+ SCAN.lanes = dsp.lanes || [];
1026
+ SCAN.phase = dsp.phase || null;
1027
+ SCAN.extra_enabled = !!extraOn;
513
1028
  const parts = [];
1029
+ // `status:` leads, because what ORC is doing right now is the one thing on
1030
+ // this line that changes minute to minute. It is also the ONE segment
1031
+ // allowed to vanish: a phase the disk cannot prove is hidden rather than
1032
+ // guessed, and the glyph goes with it. It replaces v1.2.0's `lanes:` list —
1033
+ // the running lane is its first word, and `orc stats` / `orc run list`
1034
+ // still hold the whole session's history.
1035
+ if (dsp.phase && dsp.phase.label)
1036
+ parts.push(
1037
+ motifFrame(dsp.phase.kind) + " status: " + dsp.phase.lane + " · " + dsp.phase.label
1038
+ );
514
1039
  // `running` is never hidden, because an agent still in flight is the thing
515
1040
  // a user most needs to see (v1.2.0). Zero is simply not printed.
516
1041
  parts.push(
517
1042
  "agents " + dsp.spawns + (dsp.running ? " (" + dsp.running + " running)" : "")
518
1043
  );
519
1044
  parts.push("orc-extra: " + (extraOn ? "on" : "off"));
520
- // An empty lane list means no ORC lane has dispatched yet this session —
521
- // an ANSWER, not a gap, so it keeps its slot and says so.
522
- parts.push("lanes: " + (dsp.lanes.length ? dsp.lanes.join(", ") : "none yet"));
523
1045
  if (led.started_at)
524
- parts.push(Math.max(0, Math.round((now - led.started_at) / 60000)) + "m");
1046
+ parts.push("Dur " + Math.max(0, Math.round((now - led.started_at) / 60000)) + "m");
1047
+ // MTok keeps its slot in every state. An em dash says "not measured"; a `0`
1048
+ // would say the session was free, and that is a different claim.
1049
+ parts.push("MTok " + (fmtTokens(led.tok) || "—"));
1050
+ if (SCAN.branch) parts.push(SCAN.branch);
525
1051
  line2 = " " + parts.join(" · ");
526
1052
  } catch (_) {
527
1053
  line2 = "";
528
1054
  }
529
1055
 
1056
+ // ONE write, after every block that touches the ledger has had its say. It is
1057
+ // last on purpose: a render that throws half way through still prints, and a
1058
+ // ledger that could not be written never takes the status line down with it.
1059
+ ledgerFlush();
1060
+
1061
+ // ── THE CUSTOM LAYOUT (v1.3.0) ──────────────────────────────────────────
1062
+ // Everything above is the SHIPPED status line, and it is what renders unless
1063
+ // the user composed their own. `custom()` returns null in every state but
1064
+ // one, and each of its gates is a FALLBACK rather than a throw: a hook cannot
1065
+ // refuse, so a bad layout must degrade to something correct rather than paint
1066
+ // garbage. The ladder is documented on the function itself.
1067
+ const composed = custom(d, {
1068
+ payload: d,
1069
+ ledger: LED || {},
1070
+ scan: SCAN,
1071
+ derived: { verdict, reasons, version: ver },
1072
+ now: Date.now(),
1073
+ });
1074
+ if (composed != null) {
1075
+ process.stdout.write(composed);
1076
+ return;
1077
+ }
1078
+
530
1079
  process.stdout.write(line2 ? line + "\n" + line2 : line);
531
1080
  });
1081
+
1082
+ // ── The six-rung gate ladder ────────────────────────────────────────────────
1083
+ // In order, and EVERY RUNG IS A FALLBACK, never a throw:
1084
+ //
1085
+ // 1 statusline_custom is not `on` the shipped lines. BYTE-IDENTICAL.
1086
+ // 2 statusline-compiled.json missing / default + statusline-layout-unreadable
1087
+ // unparseable / schema mismatch
1088
+ // 3 orc_version or catalog_hash moved default + statusline-layout-stale
1089
+ // 4 an unknown op or an unknown binding default + statusline-layout-skew
1090
+ // 5 the cheap shape guard fails default + statusline-layout-invalid
1091
+ // 6 otherwise run the program
1092
+ //
1093
+ // Rungs 3 and 4 are why the lock file exists. Rung 5 is this hook RE-CHECKING
1094
+ // rather than trusting the file, because a hand-edited compiled file is a file
1095
+ // nobody validated. Every fallback RECORDS ITSELF in the ledger so `orc doctor`
1096
+ // can name it: a status line that quietly went back to the default and never
1097
+ // said why is a bug the user cannot report.
1098
+ function custom(d, ctx) {
1099
+ try {
1100
+ const fs = require("fs");
1101
+ const path = require("path");
1102
+ const projectDir =
1103
+ (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
1104
+ const orcDir = path.join(projectDir, ".claude", "orc");
1105
+
1106
+ // Rung 1. The hook cannot resolve config — that is the lane resolver's job,
1107
+ // and a hook has no lane — so it reads the raw key off the file and takes
1108
+ // the documented default otherwise, exactly as it already does for
1109
+ // `log_dir` and `extra_enabled`.
1110
+ let on = false;
1111
+ try {
1112
+ const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
1113
+ // The trailing \r is tolerated on purpose: a config file written on
1114
+ // Windows carries CRLF, and a $-anchored match silently never fires
1115
+ // there — which is a feature that is ON in the file and OFF on the bar.
1116
+ on = /^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw);
1117
+ } catch (_) {}
1118
+ if (!on) return null;
1119
+
1120
+ // Rung 2. THE HOOK NEVER READS THE AUTHORED LAYOUT — not as a fallback, not
1121
+ // on a cache miss, not ever. One consumer per file.
1122
+ let prog = null;
1123
+ try {
1124
+ prog = JSON.parse(fs.readFileSync(path.join(orcDir, "statusline-compiled.json"), "utf8"));
1125
+ } catch (_) {}
1126
+ if (!prog || prog.schema !== 1) return slFallback(orcDir, "statusline-layout-unreadable");
1127
+
1128
+ // Rung 3. An ORC upgrade that adds, removes or changes a component
1129
+ // invalidates every compiled layout on the machine. Better the shipped
1130
+ // lines than a program compiled against a catalogue that no longer exists.
1131
+ let lock = null;
1132
+ try {
1133
+ lock = JSON.parse(fs.readFileSync(path.join(orcDir, "statusline.lock.json"), "utf8"));
1134
+ } catch (_) {}
1135
+ let installed = null;
1136
+ try {
1137
+ installed = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")).version;
1138
+ } catch (_) {}
1139
+ if (!lock || (installed && lock.orc_version !== installed))
1140
+ return slFallback(orcDir, "statusline-layout-stale");
1141
+
1142
+ const engine = require("./orc-statusline-render.js");
1143
+
1144
+ // Rung 4. A binding this build does not have is an INSTALL SKEW, not a
1145
+ // rendering problem, and it must not be papered over one item at a time.
1146
+ for (const b of lock.bindings || []) {
1147
+ if (!engine.BINDINGS[b]) return slFallback(orcDir, "statusline-layout-skew");
1148
+ }
1149
+
1150
+ // Rung 5. The cheap shape guard: at most three lines, at most five
1151
+ // components on any of them, and the dense-prefix rule —
1152
+ // A line may hold a component only if every line above it holds at least one.
1153
+ if (!Array.isArray(prog.lines) || prog.lines.length > 3)
1154
+ return slFallback(orcDir, "statusline-layout-invalid");
1155
+ let seenEmpty = false;
1156
+ for (const l of prog.lines) {
1157
+ const n = (l.ops || []).filter((o) => o.op === "item").length;
1158
+ if (n > 5) return slFallback(orcDir, "statusline-layout-invalid");
1159
+ if (n === 0) seenEmpty = true;
1160
+ else if (seenEmpty) return slFallback(orcDir, "statusline-layout-invalid");
1161
+ }
1162
+
1163
+ // Rung 6. Run the program. Failure isolation lives inside the engine: one
1164
+ // throwing item emits its unknown form and the rest of the line survives.
1165
+ SCAN.preset = prog.preset || null;
1166
+ const out = engine.render(prog, {
1167
+ payload: ctx.payload,
1168
+ ledger: ctx.ledger,
1169
+ scan: ctx.scan,
1170
+ derived: ctx.derived,
1171
+ now: ctx.now,
1172
+ cols: Number(process.env.COLUMNS) || 0,
1173
+ env: process.env,
1174
+ });
1175
+ if (out.errors && out.errors.length) slNote(orcDir, "statusline-item-failed", out.errors[0]);
1176
+ return out.text;
1177
+ } catch (_) {
1178
+ // Even the ladder must not throw. A status line that crashes is a status
1179
+ // line that is simply absent, with no way to find out why.
1180
+ return null;
1181
+ }
1182
+ }
1183
+
1184
+ // A fallback RECORDS ITSELF. `orc doctor` reads this file and names the finding
1185
+ // with the exact command that clears it.
1186
+ function slFallback(orcDir, finding) {
1187
+ slNote(orcDir, finding, null);
1188
+ return null;
1189
+ }
1190
+ function slNote(orcDir, finding, detail) {
1191
+ try {
1192
+ const fs = require("fs");
1193
+ const path = require("path");
1194
+ fs.mkdirSync(orcDir, { recursive: true });
1195
+ fs.writeFileSync(
1196
+ path.join(orcDir, "statusline-state.json"),
1197
+ JSON.stringify({ finding, detail: detail || null, at: Date.now() }) + "\n"
1198
+ );
1199
+ } catch (_) {}
1200
+ }
1201
+
1202
+ // ── THE EXTENDED SCAN (v1.3.0 W3) ──────────────────────────────────────────
1203
+ // Groups D, E, F and G — knowledge, extra, flow and the health gates. Every one
1204
+ // of them is a `new read`: a read that does not exist in the shipped status
1205
+ // line and only happens when a layout asks for it.
1206
+ //
1207
+ // THE BAR IS 15 MILLISECONDS, NOT 300. W0 measured node startup at ~285 ms of a
1208
+ // 300 ms budget, so a component here has to be answerable from a small JSON
1209
+ // file, on its own clock, or it does not ship. Two are refused for that reason
1210
+ // and say so in the catalogue.
1211
+ //
1212
+ // Every read below is:
1213
+ // - gated on a BINDING the compiled lock names (the read planner);
1214
+ // - cached in the per-session ledger under its own key;
1215
+ // - given its OWN TTL, because a pact ledger and a spend log do not move at
1216
+ // the same rate.
1217
+ //
1218
+ // A read that throws leaves its slot null, and a null renders an em dash.
1219
+ // UNKNOWN IS NOT ZERO — a `0` would say the thing was measured and found empty.
1220
+ function extendedScan(d, wants, wantsProvider) {
1221
+ if (!wantsProvider("scan.extended")) return;
1222
+ let fs, path, projectDir, led, now;
1223
+ try {
1224
+ fs = require("fs");
1225
+ path = require("path");
1226
+ projectDir = (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
1227
+ led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
1228
+ now = Date.now();
1229
+ } catch (_) {
1230
+ return;
1231
+ }
1232
+ const orc = path.join(projectDir, ".claude", "orc");
1233
+ const readJson = (p) => {
1234
+ try {
1235
+ return JSON.parse(fs.readFileSync(p, "utf8").replace(/^/, ""));
1236
+ } catch (_) {
1237
+ return null;
1238
+ }
1239
+ };
1240
+ // One cached sub-scan. `key` is its ledger slot, `ttl` its own clock.
1241
+ const cached = (key, ttl, fn) => {
1242
+ const slot = led.ext && led.ext[key];
1243
+ if (slot && !scanStale(slot.at, now, ttl)) return slot.v;
1244
+ let v = null;
1245
+ try {
1246
+ v = fn();
1247
+ } catch (_) {
1248
+ v = null;
1249
+ }
1250
+ led.ext = led.ext || {};
1251
+ led.ext[key] = { v, at: now };
1252
+ return v;
1253
+ };
1254
+
1255
+ // ── Group D — knowledge. All of it comes out of wiki-meta.json, which is
1256
+ // written ONLY by `orc wiki sync` and is 100% doc-header-derived. A wiki
1257
+ // tier does not move in a minute, so the TTL is generous.
1258
+ if (wants("wiki.docs")) {
1259
+ const w = cached("wiki_meta", TTL.wiki, () => {
1260
+ const meta = readJson(path.join(orc, "wiki-meta.json"));
1261
+ if (!meta) return null;
1262
+ const docs = Array.isArray(meta.docs) ? meta.docs : [];
1263
+ return {
1264
+ docs: docs.length,
1265
+ };
1266
+ });
1267
+ SCAN.wiki_meta = w;
1268
+ }
1269
+ if (wants("pattern.state")) {
1270
+ // EXISTENCE only, and by the deterministic probe's own rule: the cache
1271
+ // lives under the hidden .claude/ dir, so a raw filesystem search
1272
+ // false-negatives from the wrong cwd.
1273
+ SCAN.pattern = cached("pattern", TTL.knowledge, () => {
1274
+ const dir = path.join(orc, "patterns");
1275
+ try {
1276
+ return fs.readdirSync(dir).some((f) => f.endsWith("-pattern.md")) ? "cached" : "none";
1277
+ } catch (_) {
1278
+ return "none";
1279
+ }
1280
+ });
1281
+ }
1282
+ if (wants("crosslink.state") || wants("crosslink.peers")) {
1283
+ const c = cached("crosslink", TTL.knowledge, () => {
1284
+ const raw = (() => {
1285
+ try {
1286
+ return fs.readFileSync(path.join(projectDir, ".claude", "orc-crosslink.config.yaml"), "utf8");
1287
+ } catch (_) {
1288
+ return null;
1289
+ }
1290
+ })();
1291
+ if (!raw) return null;
1292
+ const n = (raw.match(/^[ \t]*-[ \t]*name:/gm) || []).length;
1293
+ return { peers: n, state: n ? "linked" : "none" };
1294
+ });
1295
+ SCAN.crosslink = c;
1296
+ }
1297
+ if (wants("gotchas.count")) {
1298
+ SCAN.gotchas = cached("gotchas", TTL.knowledge, () => {
1299
+ try {
1300
+ return fs.readdirSync(path.join(orc, "gotchas")).filter((f) => f.endsWith(".md")).length;
1301
+ } catch (_) {
1302
+ return null;
1303
+ }
1304
+ });
1305
+ }
1306
+
1307
+ // ── Group E — extra. `extra.json` is small; the SPEND LOG is not, so it is
1308
+ // read by TAIL and on its own faster clock, because a spend log is the one
1309
+ // thing here that moves during a wave.
1310
+ if (wants("extra.profile") || wants("extra.provider") || wants("extra.inflight") || wants("extra.passphrase") || wants("extra.demoted")) {
1311
+ SCAN.extra = cached("extra", TTL.extra, () => {
1312
+ const j = readJson(path.join(orc, "extra.json"));
1313
+ if (!j) return null;
1314
+ const profiles = j.profiles || {};
1315
+ const names = Object.keys(profiles);
1316
+ const first = names.length ? profiles[names[0]] : null;
1317
+ return {
1318
+ profile: names.length ? names[0] : null,
1319
+ provider: first && first.provider ? first.provider : null,
1320
+ profiles: names.length,
1321
+ };
1322
+ });
1323
+ }
1324
+ if (wants("extra.spend") || wants("extra.tasks")) {
1325
+ SCAN.extra_spend = cached("extra_spend", TTL.extra, () => {
1326
+ // The tail only. A spend log grows for the life of a project and reading
1327
+ // all of it on a per-keystroke surface is the hazard this whole subsystem
1328
+ // is shaped around.
1329
+ const p = path.join(orc, "extra-spend.jsonl");
1330
+ let st;
1331
+ try {
1332
+ st = fs.statSync(p);
1333
+ } catch (_) {
1334
+ return null;
1335
+ }
1336
+ const want = Math.min(st.size, 64 * 1024);
1337
+ const buf = Buffer.alloc(want);
1338
+ const fd = fs.openSync(p, "r");
1339
+ try {
1340
+ fs.readSync(fd, buf, 0, want, st.size - want);
1341
+ } finally {
1342
+ fs.closeSync(fd);
1343
+ }
1344
+ const lines = buf.toString("utf8").split("\n").slice(1).filter(Boolean);
1345
+ let tasks = 0;
1346
+ let usd = 0;
1347
+ let priced = false;
1348
+ for (const l of lines) {
1349
+ try {
1350
+ const r = JSON.parse(l);
1351
+ tasks++;
1352
+ if (typeof r.usd === "number") {
1353
+ usd += r.usd;
1354
+ priced = true;
1355
+ }
1356
+ } catch (_) {}
1357
+ }
1358
+ // A cost figure ORC did not price itself is never printed: `usd` stays
1359
+ // null rather than becoming a confident 0.
1360
+ return { tasks, usd: priced ? usd : null, partial: st.size > want };
1361
+ });
1362
+ }
1363
+
1364
+ // ── Group F — flow and lanes. The diy lock is already read for the shipped
1365
+ // segment; `wait` is a small run-state file.
1366
+ if (wants("wait.state")) {
1367
+ SCAN.wait = cached("wait", TTL.run, () => {
1368
+ const j = readJson(path.join(orc, "wait.json"));
1369
+ if (!j) return "none";
1370
+ if (j.block_reason) return "blocked";
1371
+ return j.until || j.hops ? "waiting" : "none";
1372
+ });
1373
+ }
1374
+ // `preset.name` is NOT read here: it rides in the compiled program, which is
1375
+ // the only file that crosses the wall. The hook never opens the authored
1376
+ // layout, and a field being convenient is not an exception to that.
1377
+
1378
+ // ── Group C remainder — the run's own progress, from RESUME.md, which is the
1379
+ // ONE line `orc resume` and `orc run list` parse. Reading the same line is
1380
+ // how a listing never has to open a checkpoint.
1381
+ if (wants("run.wave") || wants("run.wave_total") || wants("run.resume") || wants("run.open")) {
1382
+ SCAN.runs = cached("runs", TTL.run, () => {
1383
+ let logRel = ".claude/orc/logs";
1384
+ try {
1385
+ const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
1386
+ const m = /^[ \t]*run_dir:[ \t]*["']?([^"'#\r\n]+)/m.exec(raw);
1387
+ if (m) logRel = m[1].trim();
1388
+ } catch (_) {}
1389
+ const runDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, ".claude", "orc", "run");
1390
+ let open = 0;
1391
+ let wave = null;
1392
+ let waves = null;
1393
+ try {
1394
+ for (const slug of fs.readdirSync(runDir)) {
1395
+ const rp = path.join(runDir, slug, "RESUME.md");
1396
+ if (!fs.existsSync(rp)) continue;
1397
+ if (fs.existsSync(path.join(runDir, slug, "closed.json"))) continue;
1398
+ open++;
1399
+ // The byte-stable `Where it stands:` line, at column 0. It is the one
1400
+ // line two other commands already parse, which is exactly why this
1401
+ // does not open a checkpoint.
1402
+ const m = /^Where it stands:.*wave (\d+) of (\d+)/m.exec(fs.readFileSync(rp, "utf8"));
1403
+ if (m && wave == null) {
1404
+ wave = Number(m[1]);
1405
+ waves = Number(m[2]);
1406
+ }
1407
+ }
1408
+ } catch (_) {
1409
+ return null;
1410
+ }
1411
+ return { open, wave, waves, resume: open > 0 ? "waiting" : "none" };
1412
+ });
1413
+ }
1414
+
1415
+ // ── Group G — the health gates. Each is one small ledger, each on the slow
1416
+ // clock: a pact does not drift between keystrokes.
1417
+ const gate = (key, file, fn) => {
1418
+ if (!wants(key)) return null;
1419
+ return cached(key.replace(/\./g, "_"), TTL.gates, () => fn(readJson(path.join(orc, file))));
1420
+ };
1421
+ SCAN.pact = gate("pact.state", "pact.json", (j) => {
1422
+ if (!j || !Array.isArray(j.entries)) return null;
1423
+ const live = j.entries.filter((e) => e.state !== "retired");
1424
+ const broken = live.filter((e) => e.state === "broken").length;
1425
+ const drifted = live.filter((e) => e.state === "drifted").length;
1426
+ // UNCHECKABLE is the honest state and it never reads as a failure.
1427
+ return { state: broken ? "broken" : drifted ? "drifted" : "holding", drifted, broken, total: live.length };
1428
+ });
1429
+ SCAN.boundary = gate("boundary.state", "boundary.json", (j) => {
1430
+ if (!j || !Array.isArray(j.cards)) return null;
1431
+ const refused = j.cards.filter((c) => c.verdict === "REFUSE").length;
1432
+ // An area with no card is UNKNOWN, never assumed safe.
1433
+ return { state: refused ? "refused" : "clear", refused, cards: j.cards.length };
1434
+ });
1435
+ SCAN.challenge = gate("challenge.state", "challenge.json", (j) => {
1436
+ if (!j) return null;
1437
+ const open = Array.isArray(j.findings) ? j.findings.filter((f) => !f.resolved).length : 0;
1438
+ return { state: j.state || (open ? "open" : "pass"), open, iteration: j.iteration || null };
1439
+ });
1440
+ SCAN.doc = gate("doc.state", "doc.json", (j) => {
1441
+ if (!j) return null;
1442
+ const outline = Array.isArray(j.outline) ? j.outline : [];
1443
+ const done = outline.filter((s) => s.hash).length;
1444
+ return { state: j.shipped ? "shipped" : "draft", done, total: outline.length };
1445
+ });
1446
+
1447
+ // `usage-gate` reads the usage bridge this hook already writes — so it is the
1448
+ // cheapest `new read` in the set, and it is the only one that can say
1449
+ // `unknown`, which is a real answer and never a stop.
1450
+ if (wants("usage.state")) {
1451
+ SCAN.usage = cached("usage", TTL.trace, () => {
1452
+ const j = readJson(path.join(orc, "usage.json"));
1453
+ if (!j || !j.written_at) return "unknown";
1454
+ if (now - j.written_at > 30 * 60 * 1000) return "unknown";
1455
+ const w = [j.five_hour, j.seven_day].filter(Boolean).map((x) => x.used_percentage);
1456
+ if (!w.length) return "unknown";
1457
+ return Math.max(...w) >= 90 ? "low" : "ok";
1458
+ });
1459
+ }
1460
+
1461
+ // A config key as a component. The config file is edited by hand, so 30s.
1462
+ if (wants("config.value")) {
1463
+ SCAN.config_raw = cached("config", TTL.config, () => {
1464
+ try {
1465
+ return fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
1466
+ } catch (_) {
1467
+ return null;
1468
+ }
1469
+ });
1470
+ }
1471
+ }
1472
+
1473
+ // ── THE SERIES LEDGER (v1.3.0 W3) ──────────────────────────────────────────
1474
+ // `spark`, `spark-braille`, `trend` and `delta` need history, and a status line
1475
+ // has none: it is a fresh process every render. So the LAST 16 SAMPLES per
1476
+ // series live in the per-session ledger, appended during the throttled scan
1477
+ // that already ran.
1478
+ //
1479
+ // NO NEW READ, NO NEW TIMER. Every value sampled here was already computed for
1480
+ // something else this render; the series is a side effect of the scan, never a
1481
+ // reason for one.
1482
+ //
1483
+ // SIXTEEN, and only for the series a compiled layout actually names — the read
1484
+ // planner again. A series nothing binds is not kept, so the ledger does not
1485
+ // grow for a user whose layout has no sparkline on it.
1486
+ const SERIES_MAX = 16;
1487
+ const SERIES_MIN_GAP_MS = 20000;
1488
+
1489
+ function sampleSeries(d, plan) {
1490
+ try {
1491
+ if (!plan || !plan.series || !plan.series.size) return;
1492
+ const led = LED;
1493
+ if (!led) return;
1494
+ const now = Date.now();
1495
+ // A sample every 20 seconds, not every render. Sixteen samples at the
1496
+ // render rate would be five seconds of history, which is not history — it
1497
+ // is the same number sixteen times.
1498
+ if (led.series_at && now - led.series_at < SERIES_MIN_GAP_MS) return;
1499
+ led.series_at = now;
1500
+ led.series = led.series || {};
1501
+ const push = (key, v) => {
1502
+ if (!plan.series.has(key)) return;
1503
+ if (typeof v !== "number" || !Number.isFinite(v)) return;
1504
+ const arr = led.series[key] || [];
1505
+ arr.push(Math.round(v));
1506
+ while (arr.length > SERIES_MAX) arr.shift();
1507
+ led.series[key] = arr;
1508
+ };
1509
+ const rl = d.rate_limits || {};
1510
+ const w = led.five_hour;
1511
+ push("quota5h", rl.five_hour && rl.five_hour.used_percentage);
1512
+ push("quotawk", rl.seven_day && rl.seven_day.used_percentage);
1513
+ push("ucs", w && typeof w.last === "number" && typeof w.baseline === "number"
1514
+ ? Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline))
1515
+ : null);
1516
+ const tok = led.tok;
1517
+ if (tok) {
1518
+ push("mtok", (tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0));
1519
+ push("mtokkind", tok.cache_read || 0);
1520
+ }
1521
+ push("agents", SCAN.spawns);
1522
+ push("cost", d.cost && d.cost.total_cost_usd != null ? d.cost.total_cost_usd * 100 : null);
1523
+ push("cachehit", d.prompt_cache && d.prompt_cache.hit_ratio != null
1524
+ ? (d.prompt_cache.hit_ratio <= 1 ? d.prompt_cache.hit_ratio * 100 : d.prompt_cache.hit_ratio)
1525
+ : null);
1526
+ push("cachewrite", d.prompt_cache && d.prompt_cache.cache_write_tokens);
1527
+ push("lines", d.cost && d.cost.total_lines_added != null
1528
+ ? (d.cost.total_lines_added || 0) - (d.cost.total_lines_removed || 0)
1529
+ : null);
1530
+ push("extraspend", SCAN.extra_spend && SCAN.extra_spend.usd != null ? SCAN.extra_spend.usd * 100 : null);
1531
+ } catch (_) {
1532
+ // A series is a nicety. It never takes the status line down with it.
1533
+ }
1534
+ }