@azure-id/orc 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +296 -0
- package/README-id.md +44 -0
- package/README.md +54 -78
- package/bin/cli.js +2615 -5
- package/bin/verify-contracts.js +60 -1
- package/bin/verify-package.js +588 -568
- package/bin/webui/api.js +71 -0
- package/bin/webui/app.html +213 -210
- package/bin/webui/css/06-responsive.css +28 -0
- package/bin/webui/css/panels/hookui.css +379 -0
- package/bin/webui/fixtures/hookui.js +409 -0
- package/bin/webui/fixtures/index.js +17 -0
- package/bin/webui/i18n/en/hookui.json +119 -0
- package/bin/webui/i18n/en/nav.json +22 -21
- package/bin/webui/i18n/en/overview.json +4 -1
- package/bin/webui/i18n/en/tour.json +3 -1
- package/bin/webui/i18n/id/hookui.json +119 -0
- package/bin/webui/i18n/id/nav.json +22 -21
- package/bin/webui/i18n/id/overview.json +4 -1
- package/bin/webui/i18n/id/tour.json +3 -1
- package/bin/webui/js/01-i18n.js +152 -151
- package/bin/webui/js/90-tour.js +6 -0
- package/bin/webui/js/panels/hookui.js +891 -0
- package/bin/webui/js/panels/overview.js +7 -0
- package/package.json +1 -1
- package/templates/hooks/README.md +280 -0
- package/templates/hooks/orc-statusline-render.js +921 -0
- package/templates/hooks/orc-statusline.js +1090 -87
- package/templates/hooks/orc-subagent-line.js +219 -0
|
@@ -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
|
+
}
|