@azure-id/orc 1.2.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) ─────────────────────────────────────
@@ -262,22 +552,50 @@ process.stdin.on("end", () => {
262
552
  for (const u of usageBad) reasons.push(u);
263
553
  }
264
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
+
265
576
  let line;
266
577
  if (verdict === "ready") {
267
- line = `✅ ORC-ready ${tier}${pct ? " · " + pct : ""}`;
578
+ line = `✅ ${brand} - ${tier}${pct ? " · " + pct : ""}`;
268
579
  } else if (verdict === "boosted") {
269
- line = `🚀 ORC-boosted ${tier}${pct ? " · " + pct : ""}`;
580
+ line = `🚀 ${brand} - ${tier}${pct ? " · " + pct : ""}`;
270
581
  } else {
271
- line = `⛔ ORC WILL DEGRADE (${reasons.join(", ")}) — now: ${tier}${pct ? " · " + pct : ""}`;
582
+ line = `⛔ ${brand} - ${tier} (${reasons.join(", ")})${pct ? " · " + pct : ""}`;
272
583
  }
273
584
 
274
585
  // Subscription-usage segment (rendered after ctx, before wiki). Empty on
275
586
  // older Claude Code that doesn't surface `rate_limits`.
276
587
  if (rlSeg) line += " · " + rlSeg;
277
588
 
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.
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.
281
599
  try {
282
600
  const fs = require("fs");
283
601
  const path = require("path");
@@ -289,7 +607,7 @@ process.stdin.on("end", () => {
289
607
  const w = led && led.five_hour;
290
608
  if (w && typeof w.last === "number" && typeof w.baseline === "number") {
291
609
  const used = Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline));
292
- if (used > 0) line += " · sess +" + used + "%";
610
+ line += " · ucs " + used + "%";
293
611
  }
294
612
  } catch (_) {}
295
613
 
@@ -456,15 +774,32 @@ process.stdin.on("end", () => {
456
774
  let spawns = 0;
457
775
  let running = 0;
458
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;
459
788
  try {
460
789
  for (const f of fs.readdirSync(logDir)) {
461
790
  if (!f.startsWith("run-") || !f.endsWith(".txt")) continue;
462
791
  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
792
  let st;
466
793
  try { st = fs.statSync(full); } catch (_) { continue; }
467
- if (st.mtimeMs < (led.started_at || 0)) 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;
468
803
  const text = fs.readFileSync(full, "utf8");
469
804
  // Count by the trace's OWN line timestamps, not the file's mtime. A
470
805
  // run that was already going when this session started shares its
@@ -486,6 +821,12 @@ process.stdin.on("end", () => {
486
821
  if (at >= sessionFloor) mine += 1;
487
822
  }
488
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
+ }
489
830
  let openHere = 0;
490
831
  try {
491
832
  const pend = JSON.parse(fs.readFileSync(full + ".pending.json", "utf8"));
@@ -500,7 +841,10 @@ process.stdin.on("end", () => {
500
841
  }
501
842
  }
502
843
  } catch (_) {}
503
- led.dispatch = { spawns, running, lanes, scanned_at: now };
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);
504
848
  }
505
849
 
506
850
  led.updated_at = now;
@@ -509,19 +853,31 @@ process.stdin.on("end", () => {
509
853
  fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
510
854
  } catch (_) {}
511
855
 
512
- const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [] };
856
+ const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [], phase: null };
513
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
+ );
514
868
  // `running` is never hidden, because an agent still in flight is the thing
515
869
  // a user most needs to see (v1.2.0). Zero is simply not printed.
516
870
  parts.push(
517
871
  "agents " + dsp.spawns + (dsp.running ? " (" + dsp.running + " running)" : "")
518
872
  );
519
873
  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
874
  if (led.started_at)
524
- parts.push(Math.max(0, Math.round((now - led.started_at) / 60000)) + "m");
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);
525
881
  line2 = " " + parts.join(" · ");
526
882
  } catch (_) {
527
883
  line2 = "";