@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.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,151 @@ Format: `### v<version> — <title> _(<date>)_`.
10
10
 
11
11
  ---
12
12
 
13
+ ### v1.2.1 - the status line says what ORC is doing _(2026-09-04)_
14
+
15
+ **Still on the unscoped `orc` package?** Do this once first — your `orc upgrade`
16
+ is the pre-v0.56.0 one and cannot install itself. Full detail in the CAUTION at
17
+ the top of this file.
18
+
19
+ - **Step 1 — release the command from the old package:** `npm uninstall -g orc`
20
+ - **Step 2 — install the current package:** `npm i -g @azure-id/orc`
21
+ - **Step 3 — re-apply it to your project:** `orc update`
22
+
23
+ **Do not use `npm i -g -f`.** Full detail in v0.56.0 below.
24
+
25
+ v1.2.0 gave the status line a second row and it said which lanes had run. It
26
+ could not say what any of them was doing *now* — which is the one thing you look
27
+ down at a status bar to find out. This release puts the running phase in front
28
+ of that row, gives each kind of phase its own small animated mark (the `orc-cli`
29
+ lane-menu badge, keyed to the phase instead of the lane), and closes the row with
30
+ the two facts a terminal should always carry: what the session has cost and which
31
+ branch it is on.
32
+
33
+ ```
34
+ 🚀 ORC v1.2.1 - Opus 5/high · context (34%) · 5h 41% (2h13m) ↔ wk 12% · ucs 6% · wiki: fresh
35
+ ▰ status: quick · Q3 DO · agents 7 (2 running) · orc-extra: on · Dur 48m · MTok 412K · main
36
+ ```
37
+
38
+ #### The phase is computed by the CLI and rendered by the hook
39
+
40
+ `orc init` / `orc update` now stamps **`hooks/orc-lane-rails.json`** beside
41
+ `hooks/orc-version.json`, built from the same `LANE_PHASES` / `LANE_OWN_PHASES`
42
+ / `LANE_TRACE` registries `orc lane phases` reads. The hook renders it and
43
+ derives nothing — the Flow-stepper rule (`orc ui` v0.43.7) on a second surface.
44
+
45
+ The hook cannot call the CLI: a status line re-renders on every keystroke, so a
46
+ subprocess per render is not on the table. The alternative was a phase table
47
+ hardcoded in the hook, which is a **second source of truth no lint could see**.
48
+ The manifest is a registered contract token instead, plus three goldens: every
49
+ phase has a label and a kind from the closed set, the rail names every traced
50
+ lane **and no lane it does not**, and its role families are asserted against the
51
+ trace hook's own `roleFamily()` **by source text** (the `OPUS5_BANDS` technique
52
+ — the two live in different files, so no import can tie them).
53
+
54
+ New read: **`orc lane rails [--json]`**, which prints the whole object.
55
+
56
+ #### A phase the disk cannot prove is HIDDEN
57
+
58
+ Four rungs, most recent line wins, and the floor is deterministic:
59
+
60
+ 1. the active run comes from `.current` — not "the newest file", which is a
61
+ different and wrong answer during a lane suspend, when two traces are live;
62
+ 2. a narrated trace verb **this lane's rail publishes** resolves the own-phase
63
+ (`Q3 DO`) — but only when it is later in the file than the edge below it, so
64
+ it can sharpen the answer and can never invent one;
65
+ 3. the trace hook's `PHASE-EDGE <family>` resolves the shared phase
66
+ (`execution`) with **zero model cooperation**;
67
+ 4. nothing, or a trace whose newest line is over 10 minutes old → **hidden**.
68
+
69
+ **The cost is stated rather than papered over.** A phase that dispatches nothing
70
+ and narrates nothing is invisible: `/orc-quick` `Q1 LOOK` and `Q2 ASK`, and every
71
+ ask-the-user gate in every lane. Covering those would mean asking 24 spines to
72
+ write a phase marker at phase open — the **remembered-not-dispatched** bet this
73
+ repo has already lost five times (v0.32.0 narration, v0.49.5 hand-back, v0.53.2
74
+ spend log, v0.54.0 journal, v1.0.0 W5 demotion). A `status:` that is right when
75
+ the model is diligent and silently wrong when it is not is worse than one that is
76
+ sometimes blank, because **a stale phase word gets believed** — the same
77
+ reasoning as *unknown is not low* and *unknown is not zero*.
78
+
79
+ Two more limits inherited and restated: a **continued** agent emits no
80
+ PreToolUse/SubagentStop pair, so the skeleton is a floor and never a census; and
81
+ `orc extra` runs its worker through Bash, so a foreign wave writes no `SPAWN` and
82
+ resolves only through its narrated `EXTRA` verb, or hides.
83
+
84
+ #### The animation is a liveness tell, not a progress bar
85
+
86
+ Eight motif kinds — `look` · `ask` · `plan` · `do` · `check` · `ship` · `wait` ·
87
+ `generic` — each one cell wide, each with an ASCII twin. A phase the kind table
88
+ does not name gets `generic`, the honest sweep, **not a guess**.
89
+
90
+ A status line is a **pull** surface: Claude Code re-renders it, ORC cannot. So
91
+ the frame is picked off the wall clock. It advances while you type and while
92
+ turns land, and it **freezes when the session is idle** — which is true, and is
93
+ why frame 0 is designed as a still frame.
94
+
95
+ - `ORC_STATUSLINE_MOTION=0` **removes** the motion rather than slowing it. A
96
+ frozen frame of a cycling animation is a bug that looks like a hang; the web
97
+ panel learned that at v0.44.0 and we are not learning it twice.
98
+ - `ORC_STATUSLINE_ASCII=1` swaps the glyph set.
99
+
100
+ #### `MTok` — MAIN token, and what it cannot see
101
+
102
+ The tokens **this session's own turns** consumed, summed from the session
103
+ transcript's `usage` blocks and read **incrementally** (only the bytes the file
104
+ has grown by — a transcript reaches tens of megabytes and this loop runs every
105
+ five seconds).
106
+
107
+ **Claude Code records no token usage for a dispatched subagent** — v1.2.0
108
+ verified that across every transcript on two machines — so an hour of Opus
109
+ executors adds almost nothing here. `MTok` is what your conversation costs, not
110
+ what a run costs; `orc usage report` and `/orc-budget` remain the truth for that.
111
+ An unreadable transcript renders **`—`, never `0`**, because a zero would say the
112
+ session was free.
113
+
114
+ All four kinds are summed. That is a deliberate exception to `/orc-budget`'s
115
+ *four kinds, never blended*, and it holds only because this is one cell on a
116
+ status bar and not a report: any subset ORC picked would be a weighting **ORC
117
+ invented**, which is worse. The ledger keeps all four apart regardless.
118
+
119
+ #### The rest of the line
120
+
121
+ - **The verdict word becomes the version, the ICON keeps the verdict.** The ⛔
122
+ branch still names every reason — a warning with no reason is an emoji. A
123
+ version ORC cannot read renders as plain `ORC`, never `ORC vnull`.
124
+ - `34% ctx` → **`context (34%)`**.
125
+ - `sess +6%` → **`ucs 6%`** (usage, current session), and it now **keeps its
126
+ slot at zero**: "nothing consumed yet" and "this build has no ucs segment" are
127
+ different facts. It is still a delta of an **account-wide** window.
128
+ - **`Dur 48m`** labels the duration.
129
+ - **The branch** comes from `.git/HEAD` with **no subprocess** — including the
130
+ `gitdir:` pointer file a worktree or submodule uses. Detached HEAD reads as
131
+ `@a1b2c3d`. Not a repository → the segment is absent, not empty.
132
+ - **`lanes:` is replaced by `status:`.** The running lane is its first word, and
133
+ `orc stats` / `orc run list` still hold the session's whole history.
134
+
135
+ #### The status line explains itself
136
+
137
+ **`.claude/hooks/README.md`** ships next to the hook and documents every segment
138
+ in Simplified Technical English (`bin/webui/i18n/TERMS.md` is the term list),
139
+ including what each **absent** segment means — because a user who cannot find a
140
+ segment needs to know whether it is broken or answering.
141
+
142
+ #### Deliberately absent
143
+
144
+ - **A config key.** A status line is display, and a hook cannot resolve a lane's
145
+ config anyway (it has no lane) — the caveat the `log_dir` and wiki segments
146
+ already carry. The two env seams exist for tests and terminals, and nothing in
147
+ ORC ever sets them.
148
+ - **A second throttle.** Everything new rides in v1.2.0's one 5-second scan;
149
+ `ORC_STATUSLINE_SCAN_MS` stays the only seam over it.
150
+ - **A `git` subprocess**, on a per-keystroke surface.
151
+ - **A token estimate for dispatched subagents.** Claude Code records none, and a
152
+ fake measurement would be worse than none.
153
+ - **An `orc ui` panel.** The statusline is not a panel surface; nothing in
154
+ `bin/webui/` moved.
155
+
156
+ ---
157
+
13
158
  ### v1.2.0 - a retry that cloned the agent, and a window you can watch empty _(2026-09-04)_
14
159
 
15
160
  **Still on the unscoped `orc` package?** Do this once first — your `orc upgrade`
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  ![Dependencies](https://img.shields.io/badge/dependencies-zero-lightgrey.svg?style=for-the-badge)
15
15
  ![GitHub stars](https://img.shields.io/github/stars/azure-id/orc?style=for-the-badge&color=yellow)
16
16
 
17
- **Latest: v1.2.0** · updated 2026-09-04 · [full changelog](CHANGELOG.md)
17
+ **Latest: v1.2.1** · updated 2026-09-04 · [full changelog](CHANGELOG.md)
18
18
 
19
19
  **On npm: [`@azure-id/orc`](https://www.npmjs.com/package/@azure-id/orc)** — `npm i -g @azure-id/orc`
20
20
 
@@ -575,7 +575,7 @@ a current audit: [EVAL-REPORT.md](EVAL-REPORT.md).
575
575
  **Full history: [CHANGELOG.md](CHANGELOG.md)** — or `orc changelog`, which prints
576
576
  only what is newer than the version you have.
577
577
 
578
- ### v1.2.0 - a retry that cloned the agent, and a window you can watch empty _(2026-09-04)_
578
+ ### v1.2.1 - the status line says what ORC is doing _(2026-09-04)_
579
579
 
580
580
  **Still on the unscoped `orc` package?** Do this once first — your `orc upgrade`
581
581
  is the pre-v0.56.0 one and cannot install itself. Full detail in the CAUTION at
@@ -587,80 +587,64 @@ the top of this file.
587
587
 
588
588
  **Do not use `npm i -g -f`.** Full detail in v0.56.0 below.
589
589
 
590
- A Task error does not kill the subagent behind it. Every lane's retry rule
591
- assumed it did. One graded `/orc-quick` entry put **three** `orc-executor-opus-5-low`
592
- agents on the same task 50m19s, 115m22s and 100m53s, **266 minutes of Opus 5
593
- for one authorised dispatch**, all editing the same files inside a 2h04m window.
594
- The trace hook had recorded every one of them. Nothing had ever read it.
595
-
596
- - **`orc run inflight`** — the one reader of the pending sidecar the trace hook
597
- has written on every `SPAWN` since v0.34. `0` clear · `1` in-flight ·
598
- `2` unknown. Every lane that re-dispatches now asks it first.
599
- - **A re-dispatch is refused over a live attempt.** New registered contract
600
- token, `_shared/return-validation.md` **§0**, placed above every existing rule
601
- because every one of them ends in "re-dispatch". The refusal names the agent,
602
- the task and its age, and always offers "dispatch anyway" — never as the
603
- default.
604
- - **Exit 2 refuses, and it is the one place in ORC where an absent reading
605
- blocks.** `orc usage check` exit 2 never stops a run; an UNCHECKABLE pact
606
- never raises an exit code. It inverts here because the two errors are not the
607
- same size: a wrongly-refused dispatch costs one question, a wrongly-issued one
608
- costs a second Opus agent for an hour.
609
- - **An interrupted turn is UNKNOWN, never FAILED.** A usage limit, an API error
610
- or a `Ctrl+C` between a dispatch and its return says nothing about the agent.
611
- Classifying that as a failure is what made the incident compound: it paid
612
- twice, hit the limit sooner, and retried again.
613
- - **Unknown is not zero.** A missing sidecar, an unreadable one, records older
614
- than six hours, or a sidecar that disagrees with the trace's own SPAWN/RETURN
615
- balance all read `unknown` — never `clear`.
616
- - **The honest limit is stated, not papered over.** It cannot see an *ad-hoc*
617
- dispatch (`/orc-quick` recon, dispatched by model+effort rather than a pinned
618
- `orc-*` agent): the hook writes no `SPAWN` for one, so no record exists. Those
619
- are read-only and short, and a lane must never read `clear` as proof one
620
- finished.
621
-
622
- The other half — **you can now watch the window empty**:
623
-
624
- - **`orc usage report`** — 5-hour, 7-day and context in one place, plus the line
625
- the snapshot could never give you: **"This session has consumed 59% of the
626
- 5-hour window and is still counting"**, with the caveat that other sessions on
627
- the same account share that window.
628
- - **A window reset mid-session is not a refund.** The statusline keeps a
629
- per-session ledger beside `usage.json` — raw numbers only, never a computed
630
- word — and banks what was spent before a reset so the running total keeps
631
- counting across the boundary. A new session re-baselines.
632
- - **The statusline grew a second line, and a `sess +X%` segment on the first.**
590
+ The second line arrived in v1.2.0 saying which lanes had run. It could not say
591
+ what any of them was doing *now*. This release puts the running phase in front —
592
+ with a small animated mark per kind of phase, borrowed from `orc-cli`'s lane
593
+ menu and finishes the line with the two facts a terminal should always carry:
594
+ what the session has cost, and which branch it is on.
633
595
 
634
596
  ```
635
- 🚀 ORC-boosted Opus 5/high · 22% ctx · 5h 69% (1h30m) ↔ wk 30% · sess +59%
636
- agents 4 (1 running) · orc-extra: on · lanes: mini, quick · 41m
597
+ 🚀 ORC v1.2.1 - Opus 5/high · context (34%) · 5h 41% (2h13m) ↔ wk 12% · ucs 6% · wiki: fresh
598
+ ▰ status: quick · Q3 DO · agents 7 (2 running) · orc-extra: on · Dur 48m · MTok 412K · main
637
599
  ```
638
600
 
639
- `agents` counts what this session spawned and **never hides what is still
640
- running** the thing that was invisible while three agents worked on one
641
- task. Dispatches are attributed by the trace's **own line timestamps**, not
642
- the file's mtime, so a run that was already going when the session started is
643
- not counted twice. A lane is named only if it actually dispatched, and
644
- `lanes: none yet` keeps its slot rather than vanishing. The scan is throttled
645
- to once every 5s, because a statusline re-renders on every keystroke.
646
-
647
- - **Top 5 by measured wall time, and it says that is what it is.** Claude Code
648
- records **no token usage for a dispatched subagent** `isSidechain` is never
649
- set and no sidechain message carries a usage blockso a per-agent token
650
- figure cannot be measured. Every Claude row reports `tokens: null` and the
651
- reason, **never `0`**, which would tell you the work was free. Only
652
- `orc extra` foreign workers report real four-kind vectors, and those rows say
653
- so. Inventing the rest would be the same class of bug as the one above.
654
-
655
- **Nothing here is on by default that was not already.** `usage_gate` still ships
656
- `off`. The in-flight guard adds one free CLI read before a re-dispatch — it
657
- changes no dispatch that was already correct.
601
+ - **The phase is CLI-computed and rendered, never derived.** `orc init` /
602
+ `orc update` stamps `hooks/orc-lane-rails.json` from the same registries
603
+ `orc lane phases` reads; the hook holds no idea of its own about what ORC's
604
+ phases are. Inspect it with the new `orc lane rails [--json]`. A second phase
605
+ table in a hook would be the Flow-stepper failure on a second surface, and no
606
+ lint could see it so it is a registered contract token instead.
607
+ - **A phase the disk cannot prove is HIDDEN.** The floor is the trace hook's
608
+ own `PHASE-EDGE`, written with zero model cooperation; a narrated verb the
609
+ lane's rail publishes may only *refine* it, never invent one. The cost is
610
+ stated rather than papered over: a phase that dispatches nothing and narrates
611
+ nothing `/orc-quick` `Q1 LOOK`, `Q2 ASK`, every ask-the-user gate shows
612
+ no segment at all. **Asking 24 spines to write a phase marker would have
613
+ covered those, and it is the remembered-not-dispatched bet this repo has
614
+ already lost five times.** A stale phase word gets believed.
615
+ - **The animation is a liveness tell, not a progress bar.** A statusline is a
616
+ pull surface, so the frame comes off the wall clock: it moves while you type
617
+ and while ORC works, and freezes when the session is idle. `ORC_STATUSLINE_MOTION=0`
618
+ **removes** the motion rather than slowing it (a frozen frame of a cycling
619
+ animation is a bug that looks like a hang), and `ORC_STATUSLINE_ASCII=1`
620
+ swaps in the ASCII twin every motif ships with.
621
+ - **`MTok` — MAIN token — is honest about what it cannot see.** All four token
622
+ kinds summed, read incrementally from the session transcript. **Claude Code
623
+ records no token usage for a dispatched subagent**, so an hour of Opus
624
+ executors adds almost nothing: this is your conversation's cost, not the
625
+ run's. Unreadable renders `—`, **never `0`**, which would say the session was
626
+ free. `orc usage report` and `/orc-budget` remain the four-kind truth.
627
+ - **The rest of the line.** The verdict WORD becomes the installed version and
628
+ the ICON keeps the verdict — the ⛔ branch still names every reason, because a
629
+ warning with no reason is an emoji. `ctx` → `context (N%)`. `sess +X%` → `ucs
630
+ X%`, which now keeps its slot at zero. `Dur` labels the duration, and the
631
+ branch comes off `.git/HEAD` with **no subprocess** on a per-keystroke
632
+ surface.
633
+ - **`lanes:` is replaced by `status:`.** The running lane is its first word;
634
+ `orc stats` and `orc run list` still hold the session's whole history.
635
+ - **The status line now explains itself.** `.claude/hooks/README.md` documents
636
+ every segment in Simplified Technical English, including what each absent
637
+ segment means.
638
+
639
+ Everything rides in the one 5-second scan v1.2.0 already added. No new config
640
+ key — a status line is display, and a hook cannot resolve a lane's config anyway.
658
641
 
659
642
  **Full entry: [CHANGELOG.md](CHANGELOG.md).**
660
643
 
661
644
  <details>
662
645
  <summary>Earlier releases</summary>
663
646
 
647
+ - **v1.2.0 - a retry that cloned the agent, and a window you can watch empty — _(2026-09-04)_**
664
648
  - **v1.1.0 - the wait, and a window ORC can finally see — _(2026-08-31)_**
665
649
  - **v1.0.0 - config, phases and calls stop being prose — _(2026-08-30)_**
666
650
 
package/bin/cli.js CHANGED
@@ -220,6 +220,16 @@ function installGuards(claudeDir) {
220
220
  JSON.stringify({ version: currentVersion() }) + "\n"
221
221
  );
222
222
  } catch (_) {}
223
+ // The statusline's phase rail (v1.2.1). Regenerated every install from the
224
+ // CLI's own registries, so the hook renders a phase list it never derives.
225
+ // Same fail-silent contract: a missing manifest hides the `status:` segment,
226
+ // it never breaks a render.
227
+ try {
228
+ fs.writeFileSync(
229
+ path.join(hooksDest, "orc-lane-rails.json"),
230
+ JSON.stringify(laneRailsManifest()) + "\n"
231
+ );
232
+ } catch (_) {}
223
233
 
224
234
  const guardCmd = nodeCmd(path.join(hooksDest, "orc-effort-guard.js"));
225
235
  const statusCmd = nodeCmd(path.join(hooksDest, "orc-statusline.js"));
@@ -346,6 +356,7 @@ function shippedFootprint() {
346
356
  for (const rel of listSrcRel(SRC_AGENTS)) files.push("agents/" + rel);
347
357
  for (const rel of listSrcRel(SRC_HOOKS)) files.push("hooks/" + rel);
348
358
  files.push("hooks/orc-version.json"); // generated by installGuards, ORC-owned
359
+ files.push("hooks/orc-lane-rails.json"); // generated by installGuards, ORC-owned
349
360
  return files;
350
361
  }
351
362
 
@@ -4089,6 +4100,150 @@ function lanePhaseRows(lane) {
4089
4100
  return rows;
4090
4101
  }
4091
4102
 
4103
+ // ---------------------------------------------------------------------------
4104
+ // The STATUSLINE RAIL MANIFEST (v1.2.1)
4105
+ //
4106
+ // `templates/hooks/orc-statusline.js` shows which phase an ORC run is in. It
4107
+ // cannot call this CLI: a statusline re-renders on every keystroke, so a
4108
+ // subprocess per render is not on the table. It also must never hold its own
4109
+ // idea of what the phases are — that is the Flow-stepper failure (`orc ui`
4110
+ // v0.43.7) applied to a second surface, and a second phase table is drift no
4111
+ // lint could see.
4112
+ //
4113
+ // So the CLI COMPUTES and the hook RENDERS: `orc init` / `orc update` writes
4114
+ // `hooks/orc-lane-rails.json` from the registries below, exactly the way
4115
+ // `hooks/orc-version.json` is stamped at install time.
4116
+ //
4117
+ // FRAMES ARE NOT IN HERE. The glyphs live in the hook, because they are
4118
+ // presentation: a motif change must not need a reinstall. What ships is the
4119
+ // KIND — a closed word the hook maps to a frame set.
4120
+ // ---------------------------------------------------------------------------
4121
+
4122
+ // A phase's motif kind. The set is CLOSED, and a phase this table does not name
4123
+ // gets `generic` — the honest sweep, not a guess. Ported in spirit from
4124
+ // orc-cli's motifs.js, keyed on the PHASE rather than the lane: the lane name is
4125
+ // already the first word of the statusline's `status:` segment, so the glyph is
4126
+ // free to say what ORC is DOING.
4127
+ const PHASE_KINDS = ["look", "ask", "plan", "do", "check", "ship", "wait", "generic"];
4128
+ const PHASE_KIND = {
4129
+ // reading, scanning, gathering evidence
4130
+ preflight: "look",
4131
+ trace: "look",
4132
+ "wiki-consult": "look",
4133
+ "analyst-gates": "look",
4134
+ "house-rules": "look",
4135
+ analysis: "look",
4136
+ combine: "look",
4137
+ // waiting on the user
4138
+ intake: "ask",
4139
+ // deciding what to do and in what order
4140
+ planning: "plan",
4141
+ "plan-handoff": "plan",
4142
+ "wave-grouping": "plan",
4143
+ scoring: "plan",
4144
+ // executors running
4145
+ execution: "do",
4146
+ // grading what came back
4147
+ review: "check",
4148
+ security: "check",
4149
+ "security-checklist": "check",
4150
+ verify: "check",
4151
+ testgen: "check",
4152
+ "mock-example": "check",
4153
+ "ultra-gate": "check",
4154
+ // handing it over
4155
+ ship: "ship",
4156
+ summary: "ship",
4157
+ // stopped on purpose
4158
+ "stop-resume": "wait",
4159
+ };
4160
+
4161
+ // A word in an own-phase heading that decides its kind. Checked in order, so
4162
+ // the first match wins; no match falls through to `generic`. This is how a
4163
+ // lane's own pipeline (`Q1 LOOK`, `D4 DRAFT`, `C6 ADVISE`) gets a motif without
4164
+ // a hand-maintained row per phase per lane.
4165
+ const OWN_PHASE_KIND_WORDS = [
4166
+ [/\b(ask|question|intake|interview|frame|scope|choose|pick|gate)\b/i, "ask"],
4167
+ [/\b(look|preflight|read|scan|ingest|evidence|load|context|recon|harvest)\b/i, "look"],
4168
+ [/\b(plan|outline|cluster|shape|diverge|converge|order|batch|wave)\b/i, "plan"],
4169
+ [/\b(do|write|build|execute|implement|draft|generate|dispatch|compile)\b/i, "do"],
4170
+ [/\b(check|review|verify|judge|challenge|stress|grade|lint|audit|conserv)\b/i, "check"],
4171
+ [/\b(ship|finish|exit|deliver|hand|summar|report|card|record)\b/i, "ship"],
4172
+ [/\b(wait|stop|resume|pause|suspend)\b/i, "wait"],
4173
+ ];
4174
+
4175
+ // The display label for one phase. 18 visible chars, because line 2 of the
4176
+ // statusline already carries five other segments.
4177
+ //
4178
+ // A shared phase is its own id — those ids ARE the words ORC uses everywhere
4179
+ // else, and renaming them for a status bar would invent a second vocabulary.
4180
+ // An own phase is its heading up to the first bracket or dash, which is what
4181
+ // turns `## Q1 — LOOK (silent — no questions here)` into `Q1 LOOK`.
4182
+ function railLabel(row) {
4183
+ if (!row.heading) return String(row.id).slice(0, 18);
4184
+ let s = String(row.heading).replace(/^#+\s*/, "");
4185
+ s = s.split(/\s+[—–-]\s+|\s*\(/)[0].trim();
4186
+ // A heading whose whole first clause is just the code (`## C6 — Advise`)
4187
+ // loses the only interesting word, so keep the second clause too when the
4188
+ // first is a bare code.
4189
+ if (/^[A-Za-z]{1,2}\d+[a-z]?$/.test(s)) {
4190
+ const rest = String(row.heading)
4191
+ .replace(/^#+\s*/, "")
4192
+ .slice(s.length)
4193
+ .replace(/^\s*[—–-]\s*/, "")
4194
+ .split(/\s*\(/)[0]
4195
+ .trim();
4196
+ if (rest) s = s + " " + rest;
4197
+ }
4198
+ s = s.replace(/\s+/g, " ").trim();
4199
+ if (s.length <= 18) return s;
4200
+ // Cut at a word boundary. `D2 Supporting docu` is a typo the reader has to
4201
+ // decode; `D2 Supporting` is a label.
4202
+ const cut = s.slice(0, 18);
4203
+ const sp = cut.lastIndexOf(" ");
4204
+ return (sp > 3 ? cut.slice(0, sp) : cut).trim();
4205
+ }
4206
+
4207
+ function railKind(row) {
4208
+ if (PHASE_KIND[row.id]) return PHASE_KIND[row.id];
4209
+ const hay = `${row.id} ${row.heading || ""}`;
4210
+ for (const [re, kind] of OWN_PHASE_KIND_WORDS) if (re.test(hay)) return kind;
4211
+ return "generic";
4212
+ }
4213
+
4214
+ // The role families `templates/hooks/orc-trace.js` writes as `PHASE-EDGE <fam>`.
4215
+ // This is a MIRROR of that hook's `roleFamily()`, and it is the one new drift
4216
+ // surface this feature adds — a golden test compares the two by source text,
4217
+ // the way the `OPUS5_BANDS` alias is asserted.
4218
+ const TRACE_FAMILIES = ["combine", "analysis", "planning", "execution", "review", "verify", "testgen", "ultra-gate"];
4219
+
4220
+ // The whole object, deterministic from the registries above. `--json is not a
4221
+ // summary`: this is what the file holds and what `orc lane rails` prints.
4222
+ function laneRailsManifest() {
4223
+ const lanes = {};
4224
+ for (const lane of LANE_NAMES) {
4225
+ const t = LANE_TRACE[lane];
4226
+ // A lane that opens no run writes no trace file, so no `run-<lane>-…` name
4227
+ // can ever name it and the resolver could never reach the row. Omitting it
4228
+ // is an answer, not a gap.
4229
+ if (!t || !t.token) continue;
4230
+ const rows = [];
4231
+ for (const p of lanePhaseRows(lane))
4232
+ rows.push({ id: p.id, label: railLabel(p), kind: railKind(p), verbs: [] });
4233
+ for (const p of LANE_OWN_PHASES[lane] || [])
4234
+ rows.push({
4235
+ id: p.id,
4236
+ label: railLabel(p),
4237
+ kind: railKind(p),
4238
+ verbs: Array.isArray(p.trace_verbs) ? p.trace_verbs.slice() : [],
4239
+ });
4240
+ lanes[t.token] = { lane, tier: t.tier, phases: rows };
4241
+ }
4242
+ const families = {};
4243
+ for (const f of TRACE_FAMILIES) families[f] = { label: f, kind: PHASE_KIND[f] || "generic" };
4244
+ return { version: currentVersion(), kinds: PHASE_KINDS, families, lanes };
4245
+ }
4246
+
4092
4247
  // \`orc lane phases <lane>\` / \`--all\`. Exit 0 in every state; 2 for an unknown
4093
4248
  // lane or no argument — the \`orc lane calls\` contract, unchanged.
4094
4249
  //
@@ -4284,6 +4439,33 @@ function laneConfigCmd(lane, claudeDir) {
4284
4439
  }
4285
4440
 
4286
4441
 
4442
+ // `orc lane rails [--json]` — the statusline's phase rail, exactly as it is
4443
+ // written to `hooks/orc-lane-rails.json` at install. It exists so the manifest
4444
+ // is inspectable without opening a hook directory, and so a golden test reads
4445
+ // the same object the hook does. Exit 0 in every state.
4446
+ //
4447
+ // `--json is not a summary`: this prints the WHOLE object, not a count of it.
4448
+ function laneRailsCmd() {
4449
+ const m = laneRailsManifest();
4450
+ if (wantsJson()) {
4451
+ emitJson(Object.assign({ ok: true }, m));
4452
+ return 0;
4453
+ }
4454
+ console.log(`\nORC statusline rails ${ui.color.gray("v" + m.version)}`);
4455
+ console.log(ui.color.gray(` kinds ${m.kinds.join(", ")}`));
4456
+ console.log(ui.color.gray(` families ${Object.keys(m.families).join(", ")}`));
4457
+ for (const [token, row] of Object.entries(m.lanes)) {
4458
+ console.log(`\n ${ui.color.bold(token)} ${ui.color.gray(row.lane + " · " + row.tier)}`);
4459
+ for (const p of row.phases)
4460
+ console.log(
4461
+ " " + String(p.kind).padEnd(8) + " " + String(p.label).padEnd(20) +
4462
+ ui.color.gray(p.verbs.length ? "verbs: " + p.verbs.join(", ") : "")
4463
+ );
4464
+ }
4465
+ console.log("");
4466
+ return 0;
4467
+ }
4468
+
4287
4469
  // `orc lane` — the noun. `list` and `config` today; `phases` lands in W12 and
4288
4470
  // `calls` in W10.
4289
4471
  function lane() {
@@ -4299,13 +4481,16 @@ function lane() {
4299
4481
  process.exit(laneCallsCmd(pos[2], claudeDir));
4300
4482
  case "phases":
4301
4483
  process.exit(lanePhasesCmd(pos[2], claudeDir));
4484
+ case "rails":
4485
+ process.exit(laneRailsCmd());
4302
4486
  default:
4303
4487
  console.error(
4304
4488
  `Unknown: orc lane ${pos[1]}\n` +
4305
4489
  "Usage: orc lane list [--json]\n" +
4306
4490
  " orc lane config <lane> [--json]\n" +
4307
4491
  " orc lane calls <lane> [--json] | orc lane calls --all [--json]\n" +
4308
- " orc lane phases <lane> [--json] | orc lane phases --all [--json]"
4492
+ " orc lane phases <lane> [--json] | orc lane phases --all [--json]" +
4493
+ "\n orc lane rails [--json]"
4309
4494
  );
4310
4495
  process.exit(2);
4311
4496
  }
@@ -411,6 +411,7 @@ const CONTRACTS = [
411
411
  token: "orc run inflight",
412
412
  binFiles: ["bin/cli.js"],
413
413
  files: [
414
+ "hooks/README.md",
414
415
  "skills/_shared/return-validation.md",
415
416
  "skills/_shared/phases/execution.md",
416
417
  "skills/orc/SKILL.md",
@@ -842,6 +843,10 @@ const CONTRACTS = [
842
843
  files: [
843
844
  "agents/orc-trace-writer-haiku-4-5.md",
844
845
  "hooks/orc-trace.js",
846
+ // v1.2.1: the statusline READS the pointer to decide which run `status:`
847
+ // is about. "The newest file" would be a different, wrong answer during
848
+ // a lane suspend, when two traces are live and only one is the run.
849
+ "hooks/orc-statusline.js",
845
850
  "skills/_shared/lane-suspend.md",
846
851
  "skills/context-combiner/SKILL.md",
847
852
  "skills/orc-aftermath/SKILL.md",
@@ -905,6 +910,18 @@ const CONTRACTS = [
905
910
  "skills/_shared/phases/trace.md",
906
911
  ],
907
912
  },
913
+ {
914
+ // v1.2.1 — the statusline's phase rail. The CLI COMPUTES the phase list
915
+ // (labels, order, motif kind) and stamps it at install; the hook RENDERS it
916
+ // and derives nothing, the same way `orc ui` renders the Flow stepper. The
917
+ // two halves live in two files, so a rename of the manifest silently blanks
918
+ // `status:` unless it fails here.
919
+ name: "statusline phase rail (v1.2.1 — orc-lane-rails.json)",
920
+ token: "orc-lane-rails.json",
921
+ binFiles: ["bin/cli.js"],
922
+ files: [
923
+ "hooks/README.md","hooks/orc-statusline.js"],
924
+ },
908
925
  {
909
926
  // v0.32.0: the hook's zero-model-dependence phase segmentation. Producer
910
927
  // (the hook) and both consumers (retro skill + its miner) must agree on the
@@ -922,6 +939,11 @@ const CONTRACTS = [
922
939
  // v0.42.0: the combiner is segmented as its own PHASE-EDGE family INSIDE the
923
940
  // analyze trace — the reason it needs no lane of its own.
924
941
  "skills/context-combiner/SKILL.md",
942
+ // v1.2.1: the statusline READS these edges to render `status:`. It is the
943
+ // deterministic floor under that segment — the model-narrated verb can
944
+ // only refine it — so a rename of this verb would silently blank the one
945
+ // segment on line 2 that changes minute to minute.
946
+ "hooks/orc-statusline.js",
925
947
  ],
926
948
  },
927
949
  {
@@ -946,6 +968,7 @@ const CONTRACTS = [
946
968
  token: "orc wiki sync",
947
969
  binFiles: ["bin/cli.js"],
948
970
  files: [
971
+ "hooks/README.md",
949
972
  "agents/orc-wiki-scanner-opus-4-8-high.md",
950
973
  "agents/orc-wiki-scanner-opus-5-med.md",
951
974
  "agents/orc-wiki-scanner-sonnet-5-high.md",
@@ -985,6 +1008,7 @@ const CONTRACTS = [
985
1008
  name: "wiki UNREGISTERED state (v0.18.0 — docs without a manifest ≠ no wiki; sync, never re-scan)",
986
1009
  token: "UNREGISTERED",
987
1010
  files: [
1011
+ "hooks/README.md",
988
1012
  "hooks/orc-statusline.js",
989
1013
  "skills/orc-wiki/README.md",
990
1014
  "skills/orc-wiki/references/phases/phase-0.md",
@@ -1489,6 +1513,7 @@ const CONTRACTS = [
1489
1513
  name: "wiki freshness tier enum (v0.11.0 — FRESH/AGING/STALE, computed on read)",
1490
1514
  token: "AGING",
1491
1515
  files: [
1516
+ "hooks/README.md",
1492
1517
  "hooks/orc-statusline.js",
1493
1518
  "skills/_shared/gotchas.md",
1494
1519
  "skills/_shared/interview.md",