@azure-id/orc 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,266 @@ 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
+
52
342
  let raw = "";
53
343
  process.stdin.on("data", (c) => (raw += c));
54
344
  process.stdin.on("end", () => {
@@ -64,7 +354,7 @@ process.stdin.on("end", () => {
64
354
  const effort = String((d.effort && d.effort.level) || "").toLowerCase();
65
355
  const pct =
66
356
  d.context_window && typeof d.context_window.used_percentage === "number"
67
- ? `${d.context_window.used_percentage}% ctx`
357
+ ? `context (${d.context_window.used_percentage}%)`
68
358
  : "";
69
359
 
70
360
  // ── Session-model bridge (fail-silent) ─────────────────────────────────────
@@ -126,7 +416,46 @@ process.stdin.on("end", () => {
126
416
  written_at: Date.now(),
127
417
  }) + "\n"
128
418
  );
419
+ // -- Session consumption (v1.2.0) -------------------------------------
420
+ // `usage.json` is a SNAPSHOT of the window. It cannot answer "how much
421
+ // has THIS session eaten", which is the question a user actually asks
422
+ // mid-run -- and the one they could otherwise only answer by remembering
423
+ // what the number was an hour ago.
424
+ //
425
+ // So keep a per-session ledger beside it: the reading when this session
426
+ // first rendered, and the reading now. Same rules as every other bridge
427
+ // here -- RAW numbers only, never a computed word, fail-silent, and the
428
+ // reader decides what it means.
429
+ //
430
+ // A window RESET mid-session (used_percentage drops) is not a refund:
431
+ // bank what was consumed before the reset into `accumulated` and
432
+ // re-baseline, so the running total keeps counting across the boundary.
433
+ const sid = String(d.session_id || d.sessionId || "");
434
+ const sfile = path.join(orcDir, "usage-session.json");
435
+ let led = null;
436
+ try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
437
+ const pctOf = (o) => (o && typeof o.used_percentage === "number" ? o.used_percentage : null);
438
+ const track = (prev, cur) => {
439
+ if (cur == null) return prev || null;
440
+ if (!prev) return { baseline: cur, last: cur, accumulated: 0, resets: 0 };
441
+ if (cur < prev.baseline)
442
+ return {
443
+ baseline: cur,
444
+ last: cur,
445
+ accumulated: prev.accumulated + Math.max(0, prev.last - prev.baseline),
446
+ resets: prev.resets + 1,
447
+ };
448
+ return { baseline: prev.baseline, last: cur, accumulated: prev.accumulated, resets: prev.resets };
449
+ };
450
+ if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
451
+ led.five_hour = track(led.five_hour, pctOf(rl0 && rl0.five_hour));
452
+ led.seven_day = track(led.seven_day, pctOf(rl0 && rl0.seven_day));
453
+ led.context_used_percentage =
454
+ cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null;
455
+ led.updated_at = Date.now();
456
+ fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
129
457
  }
458
+
130
459
  } catch (_) {}
131
460
 
132
461
  // ── Subscription usage (Claude Code v2.1.80+) ──────────────────────────────
@@ -223,19 +552,65 @@ process.stdin.on("end", () => {
223
552
  for (const u of usageBad) reasons.push(u);
224
553
  }
225
554
 
555
+ // The installed ORC version (v1.2.1). It replaces the verdict WORD, not the
556
+ // verdict: the icon still carries that, and the degrade branch still names
557
+ // every reason — dropping those would turn the loudest safety segment in ORC
558
+ // into an emoji. A version we cannot read renders as plain `ORC`, never
559
+ // `ORC vnull`: the statusline never prints a word for a thing it does not know.
560
+ let ver = null;
561
+ try {
562
+ ver = updater ? updater.installedVersion(__dirname) : null;
563
+ } catch (_) {}
564
+ if (!ver) {
565
+ try {
566
+ ver = JSON.parse(
567
+ require("fs").readFileSync(
568
+ require("path").join(__dirname, "orc-version.json"),
569
+ "utf8"
570
+ )
571
+ ).version || null;
572
+ } catch (_) {}
573
+ }
574
+ const brand = "ORC" + (ver ? " v" + ver : "");
575
+
226
576
  let line;
227
577
  if (verdict === "ready") {
228
- line = `✅ ORC-ready ${tier}${pct ? " · " + pct : ""}`;
578
+ line = `✅ ${brand} - ${tier}${pct ? " · " + pct : ""}`;
229
579
  } else if (verdict === "boosted") {
230
- line = `🚀 ORC-boosted ${tier}${pct ? " · " + pct : ""}`;
580
+ line = `🚀 ${brand} - ${tier}${pct ? " · " + pct : ""}`;
231
581
  } else {
232
- line = `⛔ ORC WILL DEGRADE (${reasons.join(", ")}) — now: ${tier}${pct ? " · " + pct : ""}`;
582
+ line = `⛔ ${brand} - ${tier} (${reasons.join(", ")})${pct ? " · " + pct : ""}`;
233
583
  }
234
584
 
235
585
  // Subscription-usage segment (rendered after ctx, before wiki). Empty on
236
586
  // older Claude Code that doesn't surface `rate_limits`.
237
587
  if (rlSeg) line += " · " + rlSeg;
238
588
 
589
+ // ucs — USAGE, CURRENT SESSION. How far the 5-hour window moved while THIS
590
+ // session ran (v1.2.0; renamed v1.2.1). The ledger below keeps the raw
591
+ // numbers; this renders the delta.
592
+ //
593
+ // It KEEPS ITS SLOT at zero. "This session has consumed nothing measurable
594
+ // yet" and "this build has no ucs segment" are different facts and must not
595
+ // look the same — the same rule as `lanes: none yet` and `used 0/20`.
596
+ //
597
+ // And it is still a delta of an ACCOUNT-WIDE window, not a private meter: a
598
+ // second terminal moves it too.
599
+ try {
600
+ const fs = require("fs");
601
+ const path = require("path");
602
+ const projectDir =
603
+ (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
604
+ const led = JSON.parse(
605
+ fs.readFileSync(path.join(projectDir, ".claude", "orc", "usage-session.json"), "utf8")
606
+ );
607
+ const w = led && led.five_hour;
608
+ if (w && typeof w.last === "number" && typeof w.baseline === "number") {
609
+ const used = Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline));
610
+ line += " · ucs " + used + "%";
611
+ }
612
+ } catch (_) {}
613
+
239
614
  // Wiki freshness tier (computed on read from wiki-meta.json — zero model
240
615
  // tokens; the manifest is written only by `orc wiki sync`). Fail-silent: no
241
616
  // wiki / no git / any error → no segment. Thresholds mirror the config
@@ -340,5 +715,173 @@ process.stdin.on("end", () => {
340
715
  } catch (_) {}
341
716
  }
342
717
 
343
- process.stdout.write(line);
718
+ // ── Session line (v1.2.0) ──────────────────────────────────────────────────
719
+ // Line 1 answers "what tier am I on and how full is the window". This second
720
+ // line answers "what has this session actually been DOING" — how many agents
721
+ // it spawned, which lanes ran, whether work can leave Claude, and how long it
722
+ // has been going. All of it is read from disk; none of it costs a model call.
723
+ //
724
+ // The dispatch count is the one that earns its place. v1.2.0 exists because a
725
+ // retry cloned a live agent three times over and nothing surfaced it. A count
726
+ // that says `7 (2 running)` makes that visible from the status bar.
727
+ //
728
+ // Fail-silent and THROTTLED: the statusline re-renders on every keystroke, so
729
+ // the trace scan runs at most every 5s and its answer is cached in the same
730
+ // per-session ledger. Any error → no second line, never a broken one.
731
+ let line2 = "";
732
+ try {
733
+ const fs = require("fs");
734
+ const path = require("path");
735
+ const projectDir =
736
+ (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
737
+ const orcDir = path.join(projectDir, ".claude", "orc");
738
+ const sfile = path.join(orcDir, "usage-session.json");
739
+ const sid = String(d.session_id || d.sessionId || "");
740
+
741
+ let led = null;
742
+ try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
743
+ if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
744
+
745
+ // The hook cannot read the RESOLVED config — that is the lane resolver's
746
+ // job, and a hook has no lane — so this reads the two raw keys it needs
747
+ // straight from the file and takes the documented default
748
+ // otherwise — the same caveat the wiki segment above already carries. A
749
+ // user override shifts skill behaviour; this label follows the file.
750
+ let logRel = ".claude/orc/logs";
751
+ let extraOn = false;
752
+ try {
753
+ const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
754
+ const ld = /^[ \t]*log_dir:[ \t]*["']?([^"'#\r\n]+)/m.exec(raw);
755
+ if (ld) logRel = ld[1].trim();
756
+ extraOn = /^[ \t]*extra_enabled:[ \t]*true[ \t]*$/m.test(raw);
757
+ } catch (_) {}
758
+
759
+ const now = Date.now();
760
+ // The scan interval is the ONE seam over this budget, on the
761
+ // ORC_TEST_PROBE_MS precedent: a test that proves the throttle by SLEEPING
762
+ // past it is a test that fails on a loaded machine, and a flake is recorded
763
+ // and removed, never retried away. Unset, this is byte-identical to a
764
+ // hardcoded 5000, and nothing in ORC ever sets it.
765
+ const scanEvery = (() => {
766
+ const n = Number(process.env.ORC_STATUSLINE_SCAN_MS);
767
+ return Number.isFinite(n) && n >= 0 ? n : 5000;
768
+ })();
769
+ const stale = !led.dispatch || typeof led.dispatch.scanned_at !== "number" ||
770
+ now - led.dispatch.scanned_at >= scanEvery;
771
+ if (stale) {
772
+ const logDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, logRel);
773
+ const sessionFloor = Math.floor((led.started_at || 0) / 1000) * 1000;
774
+ let spawns = 0;
775
+ let running = 0;
776
+ const lanes = [];
777
+ // The ACTIVE run, for `status:` (v1.2.1). `.current` is the pointer the
778
+ // lanes and the trace hook both write, so it — not "the newest file" — is
779
+ // what names the run in progress. A pointer naming a file that is not
780
+ // there answers nothing, and a pointer nobody deleted is not a run: the
781
+ // resolver's own staleness gate settles that from the trace's last line.
782
+ let activeFile = null;
783
+ try {
784
+ const cur = fs.readFileSync(path.join(logDir, ".current"), "utf8").trim();
785
+ if (cur) activeFile = cur;
786
+ } catch (_) {}
787
+ let phase = null;
788
+ try {
789
+ for (const f of fs.readdirSync(logDir)) {
790
+ if (!f.startsWith("run-") || !f.endsWith(".txt")) continue;
791
+ const full = path.join(logDir, f);
792
+ let st;
793
+ try { st = fs.statSync(full); } catch (_) { continue; }
794
+ const active = f === activeFile;
795
+ // Only traces touched since this session began count towards the
796
+ // session's SPEND — a trace from last week is not this session's.
797
+ // The ACTIVE run is exempt from that filter: `status:` answers "what
798
+ // is ORC doing", which has nothing to do with who paid for it, and a
799
+ // run already going when this session opened would otherwise be
800
+ // invisible until it happened to write its next line. Its own
801
+ // staleness gate decides whether it is still running.
802
+ if (!active && st.mtimeMs < (led.started_at || 0)) continue;
803
+ const text = fs.readFileSync(full, "utf8");
804
+ // Count by the trace's OWN line timestamps, not the file's mtime. A
805
+ // run that was already going when this session started shares its
806
+ // file with the session before it, and mtime cannot tell the two
807
+ // apart — it would attribute the whole file to whoever looked last.
808
+ let mine = 0;
809
+ for (const raw of text.split("\n")) {
810
+ const t = /^\[(\d{2})(\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(raw);
811
+ if (!t) continue;
812
+ if (raw.indexOf("] hook") === -1 || raw.indexOf(" SPAWN ") === -1) continue;
813
+ const at = new Date(
814
+ 2000 + Number(t[3]), Number(t[2]) - 1, Number(t[1]),
815
+ Number(t[4]), Number(t[5]), Number(t[6])
816
+ ).getTime();
817
+ // Trace stamps have SECOND resolution and started_at has
818
+ // milliseconds, so a dispatch in the same second as the
819
+ // session start compares as earlier than it. Floor the
820
+ // boundary to the second the trace could actually express.
821
+ if (at >= sessionFloor) mine += 1;
822
+ }
823
+ spawns += mine;
824
+ // The phase comes off the text this loop already read — one more
825
+ // regex pass, no second file read.
826
+ if (active) {
827
+ const lm = /^run-([a-z0-9-]+?)-.+-\d{6}-\d{6}\.txt$/.exec(f);
828
+ if (lm) phase = resolvePhase(lm[1], text);
829
+ }
830
+ let openHere = 0;
831
+ try {
832
+ const pend = JSON.parse(fs.readFileSync(full + ".pending.json", "utf8"));
833
+ if (Array.isArray(pend)) openHere = pend.length;
834
+ } catch (_) {}
835
+ running += openHere;
836
+ // A lane earns its name by having actually dispatched in this
837
+ // session — listing a lane that contributed nothing is noise.
838
+ if (mine > 0 || openHere > 0) {
839
+ const m = /^run-([a-z0-9-]+?)-.+-\d{6}-\d{6}\.txt$/.exec(f);
840
+ if (m && lanes.indexOf(m[1]) === -1) lanes.push(m[1]);
841
+ }
842
+ }
843
+ } catch (_) {}
844
+ led.dispatch = { spawns, running, lanes, phase, scanned_at: now };
845
+ // MTok rides in the same throttled pass, and reads only the bytes the
846
+ // transcript has grown by since the last one.
847
+ led.tok = scanTokens(led, d.transcript_path || null);
848
+ }
849
+
850
+ led.updated_at = now;
851
+ try {
852
+ fs.mkdirSync(orcDir, { recursive: true });
853
+ fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
854
+ } catch (_) {}
855
+
856
+ const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [], phase: null };
857
+ const parts = [];
858
+ // `status:` leads, because what ORC is doing right now is the one thing on
859
+ // this line that changes minute to minute. It is also the ONE segment
860
+ // allowed to vanish: a phase the disk cannot prove is hidden rather than
861
+ // guessed, and the glyph goes with it. It replaces v1.2.0's `lanes:` list —
862
+ // the running lane is its first word, and `orc stats` / `orc run list`
863
+ // still hold the whole session's history.
864
+ if (dsp.phase && dsp.phase.label)
865
+ parts.push(
866
+ motifFrame(dsp.phase.kind) + " status: " + dsp.phase.lane + " · " + dsp.phase.label
867
+ );
868
+ // `running` is never hidden, because an agent still in flight is the thing
869
+ // a user most needs to see (v1.2.0). Zero is simply not printed.
870
+ parts.push(
871
+ "agents " + dsp.spawns + (dsp.running ? " (" + dsp.running + " running)" : "")
872
+ );
873
+ parts.push("orc-extra: " + (extraOn ? "on" : "off"));
874
+ if (led.started_at)
875
+ parts.push("Dur " + Math.max(0, Math.round((now - led.started_at) / 60000)) + "m");
876
+ // MTok keeps its slot in every state. An em dash says "not measured"; a `0`
877
+ // would say the session was free, and that is a different claim.
878
+ parts.push("MTok " + (fmtTokens(led.tok) || "—"));
879
+ const branch = gitBranch(projectDir);
880
+ if (branch) parts.push(branch);
881
+ line2 = " " + parts.join(" · ");
882
+ } catch (_) {
883
+ line2 = "";
884
+ }
885
+
886
+ process.stdout.write(line2 ? line + "\n" + line2 : line);
344
887
  });
@@ -91,6 +91,8 @@ the codifier); hold resolved patterns in run state.
91
91
  invalidates a DONE task → re-run once, then set every reverse-`depends_on`
92
92
  consumer to `stale_review`. **Worker failure/garbage/timeout:** flag +
93
93
  continue the wave; audit and re-dispatch at the next batch checkpoint
94
+
95
+ **Before any re-dispatch, run `orc run inflight`** (0 clear · 1 in-flight · 2 unknown). A Task error does not kill the agent behind it, and exit 2 REFUSES by default — `a lane that re-dispatches over a live attempt` has broken the contract. Canonical: `../return-validation.md`.
94
96
  (`requeued`, retry_count++). Hard retry cap 2 → STOP and surface.
95
97
 
96
98
  <!-- /orc:layer -->