@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.
- package/CHANGELOG.md +2663 -2437
- package/README.md +665 -694
- package/bin/cli.js +658 -2
- package/bin/verify-contracts.js +63 -1
- package/bin/verify-package.js +573 -568
- package/package.json +1 -1
- package/templates/hooks/README.md +202 -0
- package/templates/hooks/orc-statusline.js +552 -9
- package/templates/skills/_shared/phases/execution.md +2 -0
- package/templates/skills/_shared/return-validation.md +222 -145
- package/templates/skills/orc/SKILL.md +241 -237
- package/templates/skills/orc-doc/SKILL.md +484 -480
- package/templates/skills/orc-fast/SKILL.md +216 -214
- package/templates/skills/orc-mini/SKILL.md +246 -243
- package/templates/skills/orc-quick/SKILL.md +347 -345
- package/templates/skills/orc-wiki/SKILL.md +174 -170
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
?
|
|
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 = `✅
|
|
578
|
+
line = `✅ ${brand} - ${tier}${pct ? " · " + pct : ""}`;
|
|
229
579
|
} else if (verdict === "boosted") {
|
|
230
|
-
line = `🚀
|
|
580
|
+
line = `🚀 ${brand} - ${tier}${pct ? " · " + pct : ""}`;
|
|
231
581
|
} else {
|
|
232
|
-
line = `⛔
|
|
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
|
-
|
|
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 -->
|