@azure-id/orc 1.2.1 → 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.
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * ORC subagent line — a Claude Code `subagentStatusLine` command. (v1.4.0.)
6
+ *
7
+ * THE SECOND BOARD. Claude Code renders a custom row body for every subagent in
8
+ * the agent panel, and hands this script the whole task list. That surface is
9
+ * ORC's exact domain: one row per dispatched agent, live, while it runs.
10
+ *
11
+ * Wiring (installed by `orc init` ONLY if no subagentStatusLine already exists):
12
+ * settings.subagentStatusLine { type:"command", command:'node "<.claude>/hooks/orc-subagent-line.js"' }
13
+ *
14
+ * Output is ONE JSON LINE PER ROW: {"id": "<task id>", "content": "…"}.
15
+ * Omitting an id keeps Claude Code's default row; an empty `content` hides it.
16
+ * So a row this build cannot render is simply left alone — the default is a
17
+ * better answer than a blank line.
18
+ *
19
+ * ── WHY THIS MATTERS MORE TO ORC THAN TO ANYONE ELSE ────────────────────────
20
+ *
21
+ * v1.2.0 established that Claude Code records NO token usage for a dispatched
22
+ * subagent: `isSidechain` is never set, no sidechain message carries a usage
23
+ * block, verified across every transcript on two machines. `orc usage report`
24
+ * therefore reports `tokens: null` for every Claude row and says why, because
25
+ * a fake measurement is worse than none.
26
+ *
27
+ * That remains true of the TRANSCRIPT. It is not true of this payload: each
28
+ * task here carries `tokenCount`, and the resolved `model` and `effort`.
29
+ *
30
+ * So this hook does one thing beyond drawing: it WRITES WHAT IT SAW into
31
+ * `.claude/orc/subagent-usage.json`, and `orc usage report` reads it. That is
32
+ * the measurement v1.2.0 concluded was unavailable, and it arrives without a
33
+ * single new read — Claude Code hands it to us.
34
+ *
35
+ * THE HONEST LIMIT, and it ships with the number rather than being discovered
36
+ * later: this hook only sees a task WHILE IT IS IN THE AGENT PANEL. An agent
37
+ * that started and finished between two renders is never seen at all, and a
38
+ * count read at the last render before an agent finished is short by whatever
39
+ * came after. So the record is a FLOOR, it says so in its own field
40
+ * (`floor: true`), and `orc usage report` renders it as one. A floor reported
41
+ * as a total is the same class of lie as a zero reported for an unknown.
42
+ *
43
+ * ── THE SAME WALL ───────────────────────────────────────────────────────────
44
+ *
45
+ * The CLI compiles, this hook renders. Same compiler, same IR, same renderers,
46
+ * same glyph sets, same colour model, same gate ladder — a DIFFERENT BINDING
47
+ * TABLE and a different config key, and that is the whole difference. There is
48
+ * no second compiler and there will not be one.
49
+ */
50
+
51
+ const SUB_SCHEMA = 1;
52
+
53
+ let raw = "";
54
+ process.stdin.on("data", (c) => (raw += c));
55
+ process.stdin.on("end", () => {
56
+ let d = {};
57
+ try {
58
+ d = JSON.parse(raw || "{}");
59
+ } catch (_) {
60
+ d = {};
61
+ }
62
+ const tasks = Array.isArray(d.tasks) ? d.tasks : [];
63
+
64
+ // The usage record runs even when the custom board is OFF. It is not part of
65
+ // the feature — it is a measurement Claude Code is handing us either way, and
66
+ // throwing it away because a display setting is off would be the wrong trade
67
+ // by a wide margin.
68
+ recordUsage(d, tasks);
69
+
70
+ const out = render(d, tasks);
71
+ if (out) process.stdout.write(out);
72
+ });
73
+
74
+ // ── the record ─────────────────────────────────────────────────────────────
75
+ // Raw numbers only, never a computed word — the rule every other bridge in
76
+ // these hooks already follows. Keyed by task id so a re-render UPDATES rather
77
+ // than appends, and the highest count seen for a task wins: a count can only go
78
+ // up, so a lower reading is a stale one.
79
+ function recordUsage(d, tasks) {
80
+ if (!tasks.length) return;
81
+ try {
82
+ const fs = require("fs");
83
+ const path = require("path");
84
+ const projectDir =
85
+ (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
86
+ const orcDir = path.join(projectDir, ".claude", "orc");
87
+ const file = path.join(orcDir, "subagent-usage.json");
88
+ let led = null;
89
+ try {
90
+ led = JSON.parse(fs.readFileSync(file, "utf8"));
91
+ } catch (_) {}
92
+ const sid = String(d.session_id || d.sessionId || "");
93
+ if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now(), tasks: {} };
94
+ led.tasks = led.tasks || {};
95
+
96
+ for (const task of tasks) {
97
+ const id = String(task.id || "");
98
+ if (!id) continue;
99
+ const prev = led.tasks[id] || {};
100
+ const tok = num(task.tokenCount);
101
+ led.tasks[id] = {
102
+ // The agent NAME is what `orc usage report` groups by, and it is the
103
+ // one field that ties this record to ORC's own traces.
104
+ name: task.name || task.type || prev.name || null,
105
+ type: task.type || prev.type || null,
106
+ // OBSERVED, not derived from the agent's name and not quoted back by
107
+ // the agent itself. ORC's downgrade check has two readings — one from
108
+ // the agent's NAME and one the agent REPORTS — and this is the third,
109
+ // the only one nobody had to be trusted for.
110
+ model: task.model || prev.model || null,
111
+ effort: task.effort || prev.effort || null,
112
+ context_window_size: num(task.contextWindowSize) != null ? num(task.contextWindowSize) : prev.context_window_size || null,
113
+ // A count can only go up, so a lower reading is a stale one.
114
+ tokens: tok == null ? (prev.tokens == null ? null : prev.tokens) : Math.max(tok, prev.tokens || 0),
115
+ status: task.status || prev.status || null,
116
+ started_at: task.startTime != null ? task.startTime : prev.started_at || null,
117
+ seen_at: Date.now(),
118
+ // THE FLOOR FLAG, stored rather than inferred. This hook only sees a
119
+ // task while it is in the agent panel: an agent that started and
120
+ // finished between two renders is never seen, and a count read at the
121
+ // last render before it finished is short by whatever came after.
122
+ floor: true,
123
+ };
124
+ }
125
+ led.updated_at = Date.now();
126
+ fs.mkdirSync(orcDir, { recursive: true });
127
+ fs.writeFileSync(file, JSON.stringify(led) + "\n");
128
+ } catch (_) {
129
+ // A record is a nicety. It never takes a row down with it.
130
+ }
131
+ }
132
+
133
+ function num(v) {
134
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
135
+ }
136
+
137
+ // ── the board ──────────────────────────────────────────────────────────────
138
+ // The SAME six-rung gate ladder as the main status line, for the same reason:
139
+ // this hook cannot refuse either. Every rung falls back to Claude Code's own
140
+ // default row, which is a real answer and a better one than a blank.
141
+ function render(d, tasks) {
142
+ try {
143
+ const fs = require("fs");
144
+ const path = require("path");
145
+ const projectDir =
146
+ (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
147
+ const orcDir = path.join(projectDir, ".claude", "orc");
148
+
149
+ // Rung 1. A hook cannot resolve config — it has no lane — so it reads the
150
+ // raw key off the file, exactly as the status line already does.
151
+ let on = false;
152
+ try {
153
+ const cfg = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
154
+ on = /^[ \t]*subagent_line_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(cfg);
155
+ } catch (_) {}
156
+ if (!on) return null;
157
+
158
+ // Rung 2. The AUTHORED layout is never read here either.
159
+ let prog = null;
160
+ try {
161
+ prog = JSON.parse(fs.readFileSync(path.join(orcDir, "subagent-compiled.json"), "utf8"));
162
+ } catch (_) {}
163
+ if (!prog || prog.schema !== SUB_SCHEMA) return null;
164
+
165
+ // Rung 3. A layout compiled against a catalogue this build no longer ships.
166
+ let lock = null;
167
+ try {
168
+ lock = JSON.parse(fs.readFileSync(path.join(orcDir, "subagent.lock.json"), "utf8"));
169
+ } catch (_) {}
170
+ let installed = null;
171
+ try {
172
+ installed = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")).version;
173
+ } catch (_) {}
174
+ if (!lock || (installed && lock.orc_version !== installed)) return null;
175
+
176
+ const engine = require("./orc-statusline-render.js");
177
+
178
+ // Rung 4. A binding this build does not have is an install skew.
179
+ for (const b of lock.bindings || []) {
180
+ if (!engine.BINDINGS[b]) return null;
181
+ }
182
+
183
+ // Rung 5. A subagent row is ONE line by construction — Claude Code renders
184
+ // one row per task — so the board's three-line shape does not apply and the
185
+ // cheap guard is simply "one line, at most five things on it".
186
+ if (!Array.isArray(prog.lines) || !prog.lines.length) return null;
187
+ const line = prog.lines[0];
188
+ if ((line.ops || []).filter((o) => o.op === "item").length > 5) return null;
189
+
190
+ // Rung 6. One row per task, each rendered with THAT TASK bound.
191
+ const now = Date.now();
192
+ const rows = [];
193
+ for (const task of tasks) {
194
+ if (!task || !task.id) continue;
195
+ try {
196
+ const out = engine.render(
197
+ { schema: 1, ansi: prog.ansi, formats: prog.formats, glyphsets: prog.glyphsets, statemaps: prog.statemaps, ramps: prog.ramps, lines: [line], plans: prog.plans },
198
+ {
199
+ payload: d,
200
+ ledger: {},
201
+ scan: {},
202
+ derived: { verdict: null, reasons: [], version: installed },
203
+ task,
204
+ now,
205
+ cols: Number(process.env.COLUMNS) || 0,
206
+ env: process.env,
207
+ }
208
+ );
209
+ // An EMPTY row hides the task's row entirely, which is almost never
210
+ // what a user meant — so an empty render falls back to the default row
211
+ // by omitting the id, rather than rendering a task as nothing.
212
+ if (out.text && out.text.trim()) rows.push(JSON.stringify({ id: String(task.id), content: out.text.split("\n")[0] }));
213
+ } catch (_) {}
214
+ }
215
+ return rows.length ? rows.join("\n") + "\n" : null;
216
+ } catch (_) {
217
+ return null;
218
+ }
219
+ }