@azure-id/orc 1.7.0 → 1.8.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 +3649 -3339
- package/README-id.md +923 -844
- package/README.md +836 -860
- package/bin/build-agents.js +43 -27
- package/bin/cli.js +701 -3
- package/bin/graph-extract.js +927 -0
- package/bin/graph-notes.js +188 -0
- package/bin/graph-query.js +808 -0
- package/bin/graph-resolve.js +178 -0
- package/bin/graph-signals.js +277 -0
- package/bin/graph.js +605 -0
- package/bin/verify-contracts.js +4669 -4527
- package/bin/verify-package.js +626 -616
- package/bin/webui/api.js +1419 -1414
- package/bin/webui/fixtures/index.js +579 -576
- package/bin/webui/fixtures/knowledge.js +316 -291
- package/bin/webui/i18n/en/knowledge.json +167 -151
- package/bin/webui/i18n/en/overview.json +101 -100
- package/bin/webui/i18n/id/knowledge.json +167 -151
- package/bin/webui/i18n/id/overview.json +101 -100
- package/bin/webui/js/panels/knowledge.js +1065 -1006
- package/bin/webui/js/panels/overview.js +492 -488
- package/package.json +39 -39
- package/templates/agents/MODEL-MAPPING.md +163 -158
- package/templates/agents/orc-executor-haiku-4-5.md +133 -113
- package/templates/agents/orc-executor-opus-4-7-high.md +134 -114
- package/templates/agents/orc-executor-opus-4-7-med.md +134 -114
- package/templates/agents/orc-executor-opus-4-8-high.md +134 -114
- package/templates/agents/orc-executor-opus-5-high.md +134 -114
- package/templates/agents/orc-executor-opus-5-low.md +134 -114
- package/templates/agents/orc-executor-opus-5-med.md +134 -114
- package/templates/agents/orc-executor-sonnet-4-6-high.md +134 -114
- package/templates/agents/orc-executor-sonnet-4-6-med.md +134 -114
- package/templates/agents/orc-executor-sonnet-5-high.md +134 -114
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +86 -0
- package/templates/hooks/README.md +444 -396
- package/templates/hooks/orc-graph-hook.js +336 -0
- package/templates/hooks/orc-statusline-render.js +922 -921
- package/templates/hooks/orc-statusline.js +1596 -1545
- package/templates/skills/_shared/README.md +4 -0
- package/templates/skills/_shared/code-graph.md +220 -0
- package/templates/skills/_shared/extra-dispatch.md +1 -1
- package/templates/skills/_shared/opus5-only.md +4 -0
- package/templates/skills/_shared/phases/execution.md +166 -145
- package/templates/skills/_shared/phases/planning.md +142 -135
- package/templates/skills/_shared/phases/preflight.md +132 -118
- package/templates/skills/_shared/phases/review.md +63 -53
- package/templates/skills/_shared/phases/rules.md +5 -0
- package/templates/skills/_shared/phases/ship.md +96 -88
- package/templates/skills/_shared/phases/trace.md +6 -0
- package/templates/skills/_shared/phases/wiki-consult.md +194 -189
- package/templates/skills/_shared/read-ladder.md +124 -102
- package/templates/skills/_shared/return-validation.md +259 -250
- package/templates/skills/orc/SKILL.md +255 -254
- package/templates/skills/orc/references/preflight-report.md +6 -0
- package/templates/skills/orc/subskills/orc-execution/SKILL.md +2 -1
- package/templates/skills/orc/subskills/orc-execution/core.md +9 -1
- package/templates/skills/orc-diy/references/flow-schema.md +101 -100
- package/templates/skills/orc-fast/SKILL.md +236 -229
- package/templates/skills/orc-mini/SKILL.md +267 -259
- package/templates/skills/orc-quick/SKILL.md +378 -360
- package/templates/skills/orc-quick/references/dispatch-gate.md +6 -0
- package/templates/skills/orc-wiki/references/staleness.md +294 -288
|
@@ -1,1545 +1,1596 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* ORC statusline — a Claude Code statusLine command.
|
|
6
|
-
*
|
|
7
|
-
* The statusline is the ONLY place Claude Code exposes the live model id
|
|
8
|
-
* (`model.id`) together with the reasoning effort (`effort.level`). It is
|
|
9
|
-
* display-only — it cannot block — so this is the "model warn" half of the
|
|
10
|
-
* "run ORC on Opus 4.8 high" rule. When the session is NOT Opus 4.8 at high
|
|
11
|
-
* effort, it renders a loud warning so you never launch /orc on the wrong tier.
|
|
12
|
-
*
|
|
13
|
-
* The deterministic hard-stop for effort lives in orc-effort-guard.js.
|
|
14
|
-
*
|
|
15
|
-
* Wiring (installed by `orc init` ONLY if no statusLine already exists):
|
|
16
|
-
* settings.statusLine { type:"command", command:'node "<.claude>/hooks/orc-statusline.js"' }
|
|
17
|
-
*
|
|
18
|
-
* On Claude Code v2.1.80+ it also renders the official subscription-usage
|
|
19
|
-
* segment `5h N% (reset) ↔ wk N%`, read straight from the payload's
|
|
20
|
-
* `rate_limits.{five_hour,seven_day}` (Anthropic API headers, not estimated).
|
|
21
|
-
* A window ≥90% folds into the DEGRADE verdict; fail-silent when absent.
|
|
22
|
-
*
|
|
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.
|
|
51
|
-
*
|
|
52
|
-
* This is the ONLY place Claude Code exposes the live model id, so it also
|
|
53
|
-
* writes a fail-silent session-model bridge (.claude/orc/session-model.json)
|
|
54
|
-
* that the PreToolUse effort guard reads — the guard can't see the model id
|
|
55
|
-
* on its own, so the bridge is how the Opus 5 / Fable 5 medium-effort allowance
|
|
56
|
-
* reaches it.
|
|
57
|
-
*
|
|
58
|
-
* Also appends a "newer orc version available" hint from the 24h update cache
|
|
59
|
-
* (cache-only here — never a network call in the statusline hot path; the
|
|
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.
|
|
67
|
-
*/
|
|
68
|
-
|
|
69
|
-
// Opus 4.8 / Opus 5 / Fable 5 are matched by tolerant regexes below (accept
|
|
70
|
-
// dated/suffixed ids and the display name), not strict strings. Effort ranks give
|
|
71
|
-
// the acceptance-matrix tiers (0 = unknown, never treated as a positive downgrade).
|
|
72
|
-
const EFFORT_RANK = { low: 1, medium: 2, high: 3, xhigh: 4, max: 5 };
|
|
73
|
-
|
|
74
|
-
// Shared update-check helper (sibling file). Degrade gracefully if absent.
|
|
75
|
-
let updater = null;
|
|
76
|
-
try {
|
|
77
|
-
updater = require("./orc-update-lib.js");
|
|
78
|
-
} catch (_) {
|
|
79
|
-
updater = null;
|
|
80
|
-
}
|
|
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
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
fs.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
//
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
//
|
|
689
|
-
let
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
} else
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
//
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
try {
|
|
1235
|
-
fs = require("fs");
|
|
1236
|
-
path = require("path");
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
try {
|
|
1343
|
-
|
|
1344
|
-
} catch (_) {
|
|
1345
|
-
return null;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
let
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
if (
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
return {
|
|
1423
|
-
});
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// ── Group
|
|
1427
|
-
//
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
//
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
//
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ORC statusline — a Claude Code statusLine command.
|
|
6
|
+
*
|
|
7
|
+
* The statusline is the ONLY place Claude Code exposes the live model id
|
|
8
|
+
* (`model.id`) together with the reasoning effort (`effort.level`). It is
|
|
9
|
+
* display-only — it cannot block — so this is the "model warn" half of the
|
|
10
|
+
* "run ORC on Opus 4.8 high" rule. When the session is NOT Opus 4.8 at high
|
|
11
|
+
* effort, it renders a loud warning so you never launch /orc on the wrong tier.
|
|
12
|
+
*
|
|
13
|
+
* The deterministic hard-stop for effort lives in orc-effort-guard.js.
|
|
14
|
+
*
|
|
15
|
+
* Wiring (installed by `orc init` ONLY if no statusLine already exists):
|
|
16
|
+
* settings.statusLine { type:"command", command:'node "<.claude>/hooks/orc-statusline.js"' }
|
|
17
|
+
*
|
|
18
|
+
* On Claude Code v2.1.80+ it also renders the official subscription-usage
|
|
19
|
+
* segment `5h N% (reset) ↔ wk N%`, read straight from the payload's
|
|
20
|
+
* `rate_limits.{five_hour,seven_day}` (Anthropic API headers, not estimated).
|
|
21
|
+
* A window ≥90% folds into the DEGRADE verdict; fail-silent when absent.
|
|
22
|
+
*
|
|
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.
|
|
51
|
+
*
|
|
52
|
+
* This is the ONLY place Claude Code exposes the live model id, so it also
|
|
53
|
+
* writes a fail-silent session-model bridge (.claude/orc/session-model.json)
|
|
54
|
+
* that the PreToolUse effort guard reads — the guard can't see the model id
|
|
55
|
+
* on its own, so the bridge is how the Opus 5 / Fable 5 medium-effort allowance
|
|
56
|
+
* reaches it.
|
|
57
|
+
*
|
|
58
|
+
* Also appends a "newer orc version available" hint from the 24h update cache
|
|
59
|
+
* (cache-only here — never a network call in the statusline hot path; the
|
|
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.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
// Opus 4.8 / Opus 5 / Fable 5 are matched by tolerant regexes below (accept
|
|
70
|
+
// dated/suffixed ids and the display name), not strict strings. Effort ranks give
|
|
71
|
+
// the acceptance-matrix tiers (0 = unknown, never treated as a positive downgrade).
|
|
72
|
+
const EFFORT_RANK = { low: 1, medium: 2, high: 3, xhigh: 4, max: 5 };
|
|
73
|
+
|
|
74
|
+
// Shared update-check helper (sibling file). Degrade gracefully if absent.
|
|
75
|
+
let updater = null;
|
|
76
|
+
try {
|
|
77
|
+
updater = require("./orc-update-lib.js");
|
|
78
|
+
} catch (_) {
|
|
79
|
+
updater = null;
|
|
80
|
+
}
|
|
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
|
+
// The commit HEAD points at, from .git alone — never a subprocess, because this
|
|
260
|
+
// runs on a surface that re-renders on every keystroke. A loose ref first, then
|
|
261
|
+
// packed-refs, then a detached HEAD. Anything unreadable is null, and null never
|
|
262
|
+
// claims a state.
|
|
263
|
+
function gitHeadCommit(projectDir) {
|
|
264
|
+
const fs = require("fs");
|
|
265
|
+
const path = require("path");
|
|
266
|
+
try {
|
|
267
|
+
const dot = path.join(projectDir, ".git");
|
|
268
|
+
let gitDir = dot;
|
|
269
|
+
if (fs.statSync(dot).isFile()) {
|
|
270
|
+
const m = /gitdir:\s*(.+)/.exec(fs.readFileSync(dot, "utf8"));
|
|
271
|
+
if (!m) return null;
|
|
272
|
+
const g = m[1].trim();
|
|
273
|
+
gitDir = path.isAbsolute(g) ? g : path.join(projectDir, g);
|
|
274
|
+
}
|
|
275
|
+
const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
|
|
276
|
+
if (/^[0-9a-f]{40}$/.test(head)) return head;
|
|
277
|
+
const ref = /^ref:\s*(refs\/.+)$/.exec(head);
|
|
278
|
+
if (!ref) return null;
|
|
279
|
+
try {
|
|
280
|
+
const loose = fs.readFileSync(path.join(gitDir, ...ref[1].split("/")), "utf8").trim();
|
|
281
|
+
if (/^[0-9a-f]{40}$/.test(loose)) return loose;
|
|
282
|
+
} catch (_) {}
|
|
283
|
+
const packed = fs.readFileSync(path.join(gitDir, "packed-refs"), "utf8");
|
|
284
|
+
const line = packed.split(/\r?\n/).find((l) => l.endsWith(" " + ref[1]));
|
|
285
|
+
return line ? line.slice(0, 40) : null;
|
|
286
|
+
} catch (_) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// MTok — MAIN TOKEN. The tokens THIS session's own turns consumed, summed from
|
|
292
|
+
// the session transcript's `usage` blocks.
|
|
293
|
+
//
|
|
294
|
+
// Two honesty rules ship with it.
|
|
295
|
+
//
|
|
296
|
+
// It is the MAIN session only. Claude Code records NO token usage for a
|
|
297
|
+
// dispatched subagent (v1.2.0 verified that across every transcript on two
|
|
298
|
+
// machines), so a wave of three Opus executors adds almost nothing here. That
|
|
299
|
+
// is why an unreadable transcript renders an em dash and NEVER `0` — a zero
|
|
300
|
+
// would say the session was free.
|
|
301
|
+
//
|
|
302
|
+
// It is all four kinds summed. `/orc-budget`'s rule is four kinds never
|
|
303
|
+
// blended, and that rule is about REPORTS; this is one cell on a status bar.
|
|
304
|
+
// Any subset ORC picked would be a weighting ORC invented, which is worse. The
|
|
305
|
+
// vector stays authoritative in `orc usage report`, and the ledger below keeps
|
|
306
|
+
// all four kinds so it always can be.
|
|
307
|
+
//
|
|
308
|
+
// The read is INCREMENTAL. A transcript is append-only and reaches tens of
|
|
309
|
+
// megabytes; re-reading it inside a 5-second loop is the per-keystroke disk
|
|
310
|
+
// hazard the throttle exists to prevent. Only bytes past the stored offset are
|
|
311
|
+
// read, and the offset resets when the file shrinks or the path changes.
|
|
312
|
+
function scanTokens(led, transcriptPath) {
|
|
313
|
+
const fs = require("fs");
|
|
314
|
+
const fresh = (p) => ({
|
|
315
|
+
path: p, offset: 0, size: 0, input: 0, cache_write: 0, cache_read: 0, output: 0,
|
|
316
|
+
});
|
|
317
|
+
if (!transcriptPath) return led.tok || null;
|
|
318
|
+
let prev = led.tok && led.tok.path === transcriptPath ? led.tok : fresh(transcriptPath);
|
|
319
|
+
let st;
|
|
320
|
+
try {
|
|
321
|
+
st = fs.statSync(transcriptPath);
|
|
322
|
+
} catch (_) {
|
|
323
|
+
return prev.offset ? prev : null; // never read it → em dash, not 0
|
|
324
|
+
}
|
|
325
|
+
// Truncated or rotated under us: everything counted so far is unprovable.
|
|
326
|
+
if (st.size < prev.size) prev = fresh(transcriptPath);
|
|
327
|
+
if (st.size > prev.offset) {
|
|
328
|
+
let chunk = "";
|
|
329
|
+
try {
|
|
330
|
+
const fd = fs.openSync(transcriptPath, "r");
|
|
331
|
+
const len = st.size - prev.offset;
|
|
332
|
+
const buf = Buffer.alloc(len);
|
|
333
|
+
fs.readSync(fd, buf, 0, len, prev.offset);
|
|
334
|
+
fs.closeSync(fd);
|
|
335
|
+
chunk = buf.toString("utf8");
|
|
336
|
+
} catch (_) {
|
|
337
|
+
return prev.offset ? prev : null;
|
|
338
|
+
}
|
|
339
|
+
// The final line may be half-written. Stop at the last newline and leave
|
|
340
|
+
// the remainder for the next scan.
|
|
341
|
+
const cut = chunk.lastIndexOf("\n");
|
|
342
|
+
if (cut >= 0) {
|
|
343
|
+
const whole = chunk.slice(0, cut);
|
|
344
|
+
prev.offset += Buffer.byteLength(whole, "utf8") + 1;
|
|
345
|
+
for (const l of whole.split("\n")) {
|
|
346
|
+
if (!l || l.indexOf('"usage"') === -1) continue;
|
|
347
|
+
let u = null;
|
|
348
|
+
try {
|
|
349
|
+
const j = JSON.parse(l);
|
|
350
|
+
u = (j && j.message && j.message.usage) || (j && j.usage) || null;
|
|
351
|
+
} catch (_) {}
|
|
352
|
+
if (!u) continue;
|
|
353
|
+
prev.input += Number(u.input_tokens) || 0;
|
|
354
|
+
prev.cache_write += Number(u.cache_creation_input_tokens) || 0;
|
|
355
|
+
prev.cache_read += Number(u.cache_read_input_tokens) || 0;
|
|
356
|
+
prev.output += Number(u.output_tokens) || 0;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
prev.size = st.size;
|
|
361
|
+
return prev;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function fmtTokens(tok) {
|
|
365
|
+
if (!tok) return null;
|
|
366
|
+
const n =
|
|
367
|
+
(tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0);
|
|
368
|
+
if (n <= 0) return null;
|
|
369
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
|
|
370
|
+
if (n >= 1000) return Math.round(n / 1000) + "K";
|
|
371
|
+
return String(n);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── The per-session ledger: ONE read, ONE write, ONE throttle (v1.3.0 W0) ───
|
|
375
|
+
// `.claude/orc/usage-session.json` is the per-session ledger, and three
|
|
376
|
+
// separate blocks below want it: the rate-limit tracker, `ucs`, and the
|
|
377
|
+
// throttled line-2 scan. Each used to open the file itself and two of them
|
|
378
|
+
// wrote it, so one render cost three reads and two writes — on a surface that
|
|
379
|
+
// re-renders on every keystroke. It is memoised here instead: loaded at most
|
|
380
|
+
// once per process, mutated in place by whoever needs it, and flushed exactly
|
|
381
|
+
// once at the end. Same rules as before — RAW numbers only, never a computed
|
|
382
|
+
// word, fail-silent, and the reader decides what it means.
|
|
383
|
+
//
|
|
384
|
+
// The scan interval is the ONE seam over this budget, on the ORC_TEST_PROBE_MS
|
|
385
|
+
// precedent: a test that proves the throttle by SLEEPING past it is a test that
|
|
386
|
+
// fails on a loaded machine, and a flake is recorded and removed, never retried
|
|
387
|
+
// away. Unset, this is byte-identical to a hardcoded 5000, and nothing in ORC
|
|
388
|
+
// ever sets it.
|
|
389
|
+
let LED = null;
|
|
390
|
+
let LED_FILE = null;
|
|
391
|
+
|
|
392
|
+
// The scan's own answers, in ONE shape, so a composed layout (v1.3.0) reads
|
|
393
|
+
// exactly what the shipped lines read. It is populated as the blocks below
|
|
394
|
+
// compute their segments — never by a second pass over the disk. A provider
|
|
395
|
+
// nothing binds is simply never filled in, and every binding over it answers
|
|
396
|
+
// null, which renders an em dash. UNKNOWN IS NOT ZERO.
|
|
397
|
+
const SCAN = {
|
|
398
|
+
spawns: 0, running: 0, lanes: [], phase: null, slug: null,
|
|
399
|
+
branch: null, head: null, wiki: null, diy: null,
|
|
400
|
+
extra_enabled: false, update_version: null, inflight: null,
|
|
401
|
+
trace_age_min: null, trace_state: null, last_agent: null, retries: null,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
function ledger(projectDir, sid) {
|
|
405
|
+
if (LED) return LED;
|
|
406
|
+
const fs = require("fs");
|
|
407
|
+
const path = require("path");
|
|
408
|
+
LED_FILE = path.join(projectDir, ".claude", "orc", "usage-session.json");
|
|
409
|
+
let led = null;
|
|
410
|
+
try {
|
|
411
|
+
led = JSON.parse(fs.readFileSync(LED_FILE, "utf8"));
|
|
412
|
+
} catch (_) {}
|
|
413
|
+
if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
|
|
414
|
+
LED = led;
|
|
415
|
+
return LED;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function ledgerFlush() {
|
|
419
|
+
if (!LED || !LED_FILE) return;
|
|
420
|
+
try {
|
|
421
|
+
const fs = require("fs");
|
|
422
|
+
const path = require("path");
|
|
423
|
+
LED.updated_at = Date.now();
|
|
424
|
+
fs.mkdirSync(path.dirname(LED_FILE), { recursive: true });
|
|
425
|
+
fs.writeFileSync(LED_FILE, JSON.stringify(LED) + "\n");
|
|
426
|
+
} catch (_) {}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function scanEveryMs() {
|
|
430
|
+
const n = Number(process.env.ORC_STATUSLINE_SCAN_MS);
|
|
431
|
+
return Number.isFinite(n) && n >= 0 ? n : 5000;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Per-provider TTL (v1.3.0 W2), replacing the single global throttle for
|
|
435
|
+
// everything but the trace scan. A wiki tier does not move in five seconds and
|
|
436
|
+
// a flow lock moves when somebody runs a command, so paying the 5-second rate
|
|
437
|
+
// for either is paying for a change that cannot have happened.
|
|
438
|
+
//
|
|
439
|
+
// `ORC_STATUSLINE_SCAN_MS` still overrides ALL of them — it stays the ONE seam
|
|
440
|
+
// over this budget, and a seam that only covered one provider would be a seam
|
|
441
|
+
// tests could not use.
|
|
442
|
+
const TTL = {
|
|
443
|
+
trace: 5000, // a run moves; this is the one thing that really is that fast
|
|
444
|
+
git: 5000, // .git/HEAD, no subprocess
|
|
445
|
+
wiki: 60000, // a wiki tier does not move in five seconds
|
|
446
|
+
config: 30000, // a config file is edited by hand
|
|
447
|
+
diy: 30000, // a flow lock moves when somebody runs a command
|
|
448
|
+
knowledge: 60000, // a pattern cache, a peer list, a gotcha count
|
|
449
|
+
extra: 15000, // a spend log DOES move during a wave
|
|
450
|
+
run: 10000, // RESUME.md is rewritten at every stop
|
|
451
|
+
gates: 60000, // a pact does not drift between keystrokes
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// `at` is a ledger `scanned_at` stamp. Absent is stale — unknown is not fresh.
|
|
455
|
+
function scanStale(at, now, ttl) {
|
|
456
|
+
const budget = process.env.ORC_STATUSLINE_SCAN_MS !== undefined ? scanEveryMs() : ttl == null ? scanEveryMs() : ttl;
|
|
457
|
+
return typeof at !== "number" || now - at >= budget;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// The read plan, from the compiled layout's lock. Returns null in EVERY state
|
|
461
|
+
// but one — the feature off, no lock, an unparseable lock — and null means
|
|
462
|
+
// "read everything", which is what makes `off` byte-identical.
|
|
463
|
+
//
|
|
464
|
+
// It deliberately does NOT re-run the gate ladder. This is a question about
|
|
465
|
+
// what to READ, and the ladder is a question about what to RENDER; a layout
|
|
466
|
+
// that later fails the ladder falls back to the shipped lines and simply has a
|
|
467
|
+
// segment or two missing for one render. That is a far better failure than
|
|
468
|
+
// reading the disk twice on a surface that re-renders on every keystroke.
|
|
469
|
+
function readPlan(d) {
|
|
470
|
+
try {
|
|
471
|
+
const fs = require("fs");
|
|
472
|
+
const path = require("path");
|
|
473
|
+
const projectDir =
|
|
474
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
475
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
476
|
+
if (!/^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw)) return null;
|
|
477
|
+
const lock = JSON.parse(
|
|
478
|
+
fs.readFileSync(path.join(projectDir, ".claude", "orc", "statusline.lock.json"), "utf8")
|
|
479
|
+
);
|
|
480
|
+
if (!lock || !Array.isArray(lock.bindings)) return null;
|
|
481
|
+
return {
|
|
482
|
+
bindings: new Set(lock.bindings),
|
|
483
|
+
providers: new Set(lock.providers || []),
|
|
484
|
+
series: new Set(lock.series || []),
|
|
485
|
+
};
|
|
486
|
+
} catch (_) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let raw = "";
|
|
492
|
+
process.stdin.on("data", (c) => (raw += c));
|
|
493
|
+
process.stdin.on("end", () => {
|
|
494
|
+
let d = {};
|
|
495
|
+
try {
|
|
496
|
+
d = JSON.parse(raw || "{}");
|
|
497
|
+
} catch (_) {
|
|
498
|
+
d = {};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const model = (d.model && d.model.id) || "";
|
|
502
|
+
const display = (d.model && d.model.display_name) || model || "unknown";
|
|
503
|
+
const effort = String((d.effort && d.effort.level) || "").toLowerCase();
|
|
504
|
+
const pct =
|
|
505
|
+
d.context_window && typeof d.context_window.used_percentage === "number"
|
|
506
|
+
? `context (${d.context_window.used_percentage}%)`
|
|
507
|
+
: "";
|
|
508
|
+
|
|
509
|
+
// ── THE READ PLAN (v1.3.0 W2) ──────────────────────────────────────────────
|
|
510
|
+
// The compiler knows exactly which bindings a layout uses, therefore exactly
|
|
511
|
+
// which providers it needs, and it records both in the lock. So: A PROVIDER
|
|
512
|
+
// NOTHING BINDS IS NOT READ.
|
|
513
|
+
//
|
|
514
|
+
// That is the claim this feature has to earn — the shipped status line pays
|
|
515
|
+
// for the wiki read, the diy read and the trace scan unconditionally, and a
|
|
516
|
+
// user whose layout names none of them should pay for none of them. Composing
|
|
517
|
+
// your own line is allowed to make the bar FASTER than the hardcoded one it
|
|
518
|
+
// replaces, and here is where that happens.
|
|
519
|
+
//
|
|
520
|
+
// `null` means the feature is off, and then everything below runs exactly as
|
|
521
|
+
// it always has. That is what keeps `off` byte-identical.
|
|
522
|
+
const PLAN = readPlan(d);
|
|
523
|
+
const wants = (b) => !PLAN || PLAN.bindings.has(b);
|
|
524
|
+
const wantsProvider = (p) => !PLAN || PLAN.providers.has(p);
|
|
525
|
+
|
|
526
|
+
// ── Session-model bridge (fail-silent) ─────────────────────────────────────
|
|
527
|
+
// The PreToolUse effort guard cannot see the model id; it can only read
|
|
528
|
+
// effort. Persist {model_id, effort, written_at} here so the guard can grant
|
|
529
|
+
// the Opus 5 / Fable 5 medium-effort allowance. The statusline re-renders constantly
|
|
530
|
+
// while a session is active, so written_at stays fresh; the guard treats a
|
|
531
|
+
// stale file (older than its freshness window) as absent and never blocks on
|
|
532
|
+
// it. Any error (no dir, read-only fs) is swallowed — this is a nicety, not a
|
|
533
|
+
// guarantee, and must never break the statusline render.
|
|
534
|
+
try {
|
|
535
|
+
if (model || effort) {
|
|
536
|
+
const fs = require("fs");
|
|
537
|
+
const path = require("path");
|
|
538
|
+
const projectDir =
|
|
539
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
540
|
+
const orcDir = path.join(projectDir, ".claude", "orc");
|
|
541
|
+
fs.mkdirSync(orcDir, { recursive: true });
|
|
542
|
+
fs.writeFileSync(
|
|
543
|
+
path.join(orcDir, "session-model.json"),
|
|
544
|
+
JSON.stringify({ model_id: model, effort, written_at: Date.now() }) + "\n"
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
} catch (_) {}
|
|
548
|
+
|
|
549
|
+
// ── Usage bridge (v1.1.0 W4, fail-silent) ─────────────────────────────────
|
|
550
|
+
// The `rate_limits` block below reaches ONLY this process: the statusline
|
|
551
|
+
// renders a string and exits, so nothing else in ORC has ever been able to
|
|
552
|
+
// see how full the window is. A lane therefore started a wave with no idea it
|
|
553
|
+
// was about to run out, and the wave stopped in the middle.
|
|
554
|
+
//
|
|
555
|
+
// Persist the RAW numbers (never a computed word like `LOW` — a stored state
|
|
556
|
+
// is wrong one minute later; `orc usage check` computes it on read) plus
|
|
557
|
+
// `context_window`, which the wait needs to decide whether continuing
|
|
558
|
+
// in-session is cheaper than a fresh one. Same fail-silent contract as the
|
|
559
|
+
// session-model bridge above: any error is swallowed, and a reading older
|
|
560
|
+
// than its freshness window reads as `unknown`, never as `low`.
|
|
561
|
+
try {
|
|
562
|
+
const rl0 = d.rate_limits;
|
|
563
|
+
const cw0 = d.context_window;
|
|
564
|
+
if (rl0 || cw0) {
|
|
565
|
+
const fs = require("fs");
|
|
566
|
+
const path = require("path");
|
|
567
|
+
const projectDir =
|
|
568
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
569
|
+
const orcDir = path.join(projectDir, ".claude", "orc");
|
|
570
|
+
const win = (o) =>
|
|
571
|
+
o && typeof o.used_percentage === "number"
|
|
572
|
+
? { used_percentage: o.used_percentage, resets_at: o.resets_at == null ? null : o.resets_at }
|
|
573
|
+
: null;
|
|
574
|
+
fs.mkdirSync(orcDir, { recursive: true });
|
|
575
|
+
fs.writeFileSync(
|
|
576
|
+
path.join(orcDir, "usage.json"),
|
|
577
|
+
JSON.stringify({
|
|
578
|
+
five_hour: win(rl0 && rl0.five_hour),
|
|
579
|
+
seven_day: win(rl0 && rl0.seven_day),
|
|
580
|
+
context_used_percentage:
|
|
581
|
+
cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null,
|
|
582
|
+
written_at: Date.now(),
|
|
583
|
+
}) + "\n"
|
|
584
|
+
);
|
|
585
|
+
// -- Session consumption (v1.2.0) -------------------------------------
|
|
586
|
+
// `usage.json` is a SNAPSHOT of the window. It cannot answer "how much
|
|
587
|
+
// has THIS session eaten", which is the question a user actually asks
|
|
588
|
+
// mid-run -- and the one they could otherwise only answer by remembering
|
|
589
|
+
// what the number was an hour ago.
|
|
590
|
+
//
|
|
591
|
+
// So keep a per-session ledger beside it: the reading when this session
|
|
592
|
+
// first rendered, and the reading now. Same rules as every other bridge
|
|
593
|
+
// here -- RAW numbers only, never a computed word, fail-silent, and the
|
|
594
|
+
// reader decides what it means.
|
|
595
|
+
//
|
|
596
|
+
// A window RESET mid-session (used_percentage drops) is not a refund:
|
|
597
|
+
// bank what was consumed before the reset into `accumulated` and
|
|
598
|
+
// re-baseline, so the running total keeps counting across the boundary.
|
|
599
|
+
const sid = String(d.session_id || d.sessionId || "");
|
|
600
|
+
const led = ledger(projectDir, sid);
|
|
601
|
+
const pctOf = (o) => (o && typeof o.used_percentage === "number" ? o.used_percentage : null);
|
|
602
|
+
const track = (prev, cur) => {
|
|
603
|
+
if (cur == null) return prev || null;
|
|
604
|
+
if (!prev) return { baseline: cur, last: cur, accumulated: 0, resets: 0 };
|
|
605
|
+
if (cur < prev.baseline)
|
|
606
|
+
return {
|
|
607
|
+
baseline: cur,
|
|
608
|
+
last: cur,
|
|
609
|
+
accumulated: prev.accumulated + Math.max(0, prev.last - prev.baseline),
|
|
610
|
+
resets: prev.resets + 1,
|
|
611
|
+
};
|
|
612
|
+
return { baseline: prev.baseline, last: cur, accumulated: prev.accumulated, resets: prev.resets };
|
|
613
|
+
};
|
|
614
|
+
led.five_hour = track(led.five_hour, pctOf(rl0 && rl0.five_hour));
|
|
615
|
+
led.seven_day = track(led.seven_day, pctOf(rl0 && rl0.seven_day));
|
|
616
|
+
led.context_used_percentage =
|
|
617
|
+
cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
} catch (_) {}
|
|
621
|
+
|
|
622
|
+
// ── Subscription usage (Claude Code v2.1.80+) ──────────────────────────────
|
|
623
|
+
// Official 5-hour + 7-day usage, surfaced by Claude Code straight from
|
|
624
|
+
// Anthropic's API headers into this payload's `rate_limits`. Display-only,
|
|
625
|
+
// fail-silent: an absent block (older Claude Code / no headers) → no segment,
|
|
626
|
+
// never `undefined`. A window at/above USAGE_CRIT contributes to the DEGRADE
|
|
627
|
+
// verdict — running low on quota is a real mid-run degradation risk.
|
|
628
|
+
const USAGE_WARN = 75;
|
|
629
|
+
const USAGE_CRIT = 90;
|
|
630
|
+
const fmtReset = (v) => {
|
|
631
|
+
// resets_at may be an ISO string or an epoch (seconds or ms). "" on anything odd.
|
|
632
|
+
let t = NaN;
|
|
633
|
+
if (typeof v === "number") t = v < 1e12 ? v * 1000 : v;
|
|
634
|
+
else if (typeof v === "string") {
|
|
635
|
+
const n = Number(v);
|
|
636
|
+
t = Number.isFinite(n) ? (n < 1e12 ? n * 1000 : n) : Date.parse(v);
|
|
637
|
+
}
|
|
638
|
+
if (!Number.isFinite(t)) return "";
|
|
639
|
+
const ms = t - Date.now();
|
|
640
|
+
if (ms <= 0) return "";
|
|
641
|
+
const mins = Math.round(ms / 60000);
|
|
642
|
+
if (mins < 60) return `${mins}m`;
|
|
643
|
+
const h = Math.floor(mins / 60);
|
|
644
|
+
const m = mins % 60;
|
|
645
|
+
return m ? `${h}h${m}m` : `${h}h`;
|
|
646
|
+
};
|
|
647
|
+
let rlSeg = "";
|
|
648
|
+
const usageBad = [];
|
|
649
|
+
try {
|
|
650
|
+
const rl = d.rate_limits || {};
|
|
651
|
+
const win = (obj, label) => {
|
|
652
|
+
if (!obj || typeof obj.used_percentage !== "number") return "";
|
|
653
|
+
const p = Math.round(obj.used_percentage);
|
|
654
|
+
const reset = fmtReset(obj.resets_at);
|
|
655
|
+
const mark = p >= USAGE_CRIT ? "⛔" : p >= USAGE_WARN ? "⚠" : "";
|
|
656
|
+
if (p >= USAGE_CRIT) usageBad.push(`${label}≥${USAGE_CRIT}%`);
|
|
657
|
+
// Reset shown for the short (5h) window always; weekly only when elevated.
|
|
658
|
+
const showReset = reset && (label === "5h" || p >= USAGE_WARN);
|
|
659
|
+
return `${mark}${label} ${p}%${showReset ? ` (${reset})` : ""}`;
|
|
660
|
+
};
|
|
661
|
+
const fh = win(rl.five_hour, "5h");
|
|
662
|
+
const sd = win(rl.seven_day, "wk");
|
|
663
|
+
if (fh && sd) rlSeg = `${fh} ↔ ${sd}`;
|
|
664
|
+
else rlSeg = fh || sd || "";
|
|
665
|
+
} catch (_) {
|
|
666
|
+
rlSeg = "";
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Tolerant tier detection. A strict `model === "claude-opus-4-8"` false-fired
|
|
670
|
+
// whenever Claude Code reported a dated/suffixed id (e.g. claude-opus-4-8-
|
|
671
|
+
// YYYYMMDD) or only a display name — even on the correct tier. Match Opus 4.8
|
|
672
|
+
// and Fable 5 by normalized id OR display name, accepting any variant; the
|
|
673
|
+
// trailing \b keeps 4.7 / 4.85 / Sonnet etc. correctly warning.
|
|
674
|
+
const hay = `${model} ${display}`.toLowerCase();
|
|
675
|
+
const isOpus48 = /opus[\s._-]?4[\s._-]?8\b/.test(hay);
|
|
676
|
+
const isFable5 = /fable[\s._-]?5\b/.test(hay);
|
|
677
|
+
// Opus 5 (v0.34.0) — strictly above the baseline, so it boosts from medium up,
|
|
678
|
+
// exactly like Fable 5. The `\b` keeps opus 4.8 / 4.7 out of this branch.
|
|
679
|
+
const isOpus5 = /opus[\s._-]?5\b/.test(hay);
|
|
680
|
+
const modelKnown = model !== "" || (display !== "" && display !== "unknown");
|
|
681
|
+
// Effort rank; 0 = unknown. A missing/empty effort field is NOT proof of a
|
|
682
|
+
// downgrade — the PreToolUse guard already hard-blocks a real low-effort /orc
|
|
683
|
+
// — so an unknown effort never forces DEGRADE on effort grounds here.
|
|
684
|
+
const er = EFFORT_RANK[effort] || 0;
|
|
685
|
+
|
|
686
|
+
const tier = `${display}${effort ? "/" + effort : ""}`;
|
|
687
|
+
|
|
688
|
+
// Verdict per the acceptance matrix. Only a POSITIVELY-known bad tier degrades.
|
|
689
|
+
let verdict = "ready";
|
|
690
|
+
const reasons = [];
|
|
691
|
+
if (isOpus48) {
|
|
692
|
+
if (er >= 4) verdict = "boosted"; // xhigh / max
|
|
693
|
+
else if (er === 3 || er === 0) verdict = "ready"; // high (or unknown → lenient)
|
|
694
|
+
else {
|
|
695
|
+
verdict = "degrade"; // opus 4.8 below high
|
|
696
|
+
reasons.push("effort≠high");
|
|
697
|
+
}
|
|
698
|
+
} else if (isOpus5 || isFable5) {
|
|
699
|
+
const label = isOpus5 ? "Opus-5" : "Fable-5";
|
|
700
|
+
if (er >= 2 || er === 0) verdict = "boosted"; // medium…max (or unknown → lenient)
|
|
701
|
+
else {
|
|
702
|
+
verdict = "degrade"; // opus 5 / fable 5 below medium
|
|
703
|
+
reasons.push(`${label} effort<medium`);
|
|
704
|
+
}
|
|
705
|
+
} else if (modelKnown) {
|
|
706
|
+
verdict = "degrade";
|
|
707
|
+
reasons.push("model≠Opus5/Opus4.8/Fable5");
|
|
708
|
+
} // else model unknown → stay lenient (the guard enforces effort)
|
|
709
|
+
|
|
710
|
+
// A quota window at/above the crit threshold folds into DEGRADE regardless.
|
|
711
|
+
if (usageBad.length) {
|
|
712
|
+
verdict = "degrade";
|
|
713
|
+
for (const u of usageBad) reasons.push(u);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// The installed ORC version (v1.2.1). It replaces the verdict WORD, not the
|
|
717
|
+
// verdict: the icon still carries that, and the degrade branch still names
|
|
718
|
+
// every reason — dropping those would turn the loudest safety segment in ORC
|
|
719
|
+
// into an emoji. A version we cannot read renders as plain `ORC`, never
|
|
720
|
+
// `ORC vnull`: the statusline never prints a word for a thing it does not know.
|
|
721
|
+
let ver = null;
|
|
722
|
+
try {
|
|
723
|
+
ver = updater ? updater.installedVersion(__dirname) : null;
|
|
724
|
+
} catch (_) {}
|
|
725
|
+
if (!ver) {
|
|
726
|
+
try {
|
|
727
|
+
ver = JSON.parse(
|
|
728
|
+
require("fs").readFileSync(
|
|
729
|
+
require("path").join(__dirname, "orc-version.json"),
|
|
730
|
+
"utf8"
|
|
731
|
+
)
|
|
732
|
+
).version || null;
|
|
733
|
+
} catch (_) {}
|
|
734
|
+
}
|
|
735
|
+
const brand = "ORC" + (ver ? " v" + ver : "");
|
|
736
|
+
|
|
737
|
+
let line;
|
|
738
|
+
if (verdict === "ready") {
|
|
739
|
+
line = `✅ ${brand} - ${tier}${pct ? " · " + pct : ""}`;
|
|
740
|
+
} else if (verdict === "boosted") {
|
|
741
|
+
line = `🚀 ${brand} - ${tier}${pct ? " · " + pct : ""}`;
|
|
742
|
+
} else {
|
|
743
|
+
line = `⛔ ${brand} - ${tier} (${reasons.join(", ")})${pct ? " · " + pct : ""}`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Subscription-usage segment (rendered after ctx, before wiki). Empty on
|
|
747
|
+
// older Claude Code that doesn't surface `rate_limits`.
|
|
748
|
+
if (rlSeg) line += " · " + rlSeg;
|
|
749
|
+
|
|
750
|
+
// ucs — USAGE, CURRENT SESSION. How far the 5-hour window moved while THIS
|
|
751
|
+
// session ran (v1.2.0; renamed v1.2.1). The ledger below keeps the raw
|
|
752
|
+
// numbers; this renders the delta.
|
|
753
|
+
//
|
|
754
|
+
// It KEEPS ITS SLOT at zero. "This session has consumed nothing measurable
|
|
755
|
+
// yet" and "this build has no ucs segment" are different facts and must not
|
|
756
|
+
// look the same — the same rule as `lanes: none yet` and `used 0/20`.
|
|
757
|
+
//
|
|
758
|
+
// And it is still a delta of an ACCOUNT-WIDE window, not a private meter: a
|
|
759
|
+
// second terminal moves it too.
|
|
760
|
+
try {
|
|
761
|
+
const projectDir =
|
|
762
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
763
|
+
const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
764
|
+
const w = led && led.five_hour;
|
|
765
|
+
if (w && typeof w.last === "number" && typeof w.baseline === "number") {
|
|
766
|
+
const used = Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline));
|
|
767
|
+
line += " · ucs " + used + "%";
|
|
768
|
+
}
|
|
769
|
+
} catch (_) {}
|
|
770
|
+
|
|
771
|
+
// Wiki freshness tier (computed on read from wiki-meta.json — zero model
|
|
772
|
+
// tokens; the manifest is written only by `orc wiki sync`). Fail-silent: no
|
|
773
|
+
// wiki / no git / any error → no segment. Thresholds mirror the config
|
|
774
|
+
// defaults (wiki_fresh_max 10 / wiki_aging_max 30); the hook can't read the
|
|
775
|
+
// resolved config, so a user override shifts skill behavior, not this label.
|
|
776
|
+
//
|
|
777
|
+
// The git distance rides in the SAME throttled scan as everything on line 2
|
|
778
|
+
// (v1.3.0 W0). It used to be an `execSync` on every render — one child
|
|
779
|
+
// process PER KEYSTROKE in any repo with a wiki, which is the exact hazard
|
|
780
|
+
// the throttle exists to prevent. The ledger caches the RAW facts (a commit
|
|
781
|
+
// count, a boolean) and never the word: `fresh` / `AGING` / `STALE` is
|
|
782
|
+
// computed here, on read, every time.
|
|
783
|
+
try {
|
|
784
|
+
const fs = require("fs");
|
|
785
|
+
const path = require("path");
|
|
786
|
+
const projectDir =
|
|
787
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
788
|
+
// A layout that names no wiki component performs ZERO wiki reads — not a
|
|
789
|
+
// cheaper one, none. This is the read planner's whole point.
|
|
790
|
+
if (!wants("wiki.tier") && !wants("wiki.distance")) throw new Error("not needed");
|
|
791
|
+
const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
792
|
+
const now = Date.now();
|
|
793
|
+
// A wiki tier does not move in five seconds. Per-provider TTL replaces the
|
|
794
|
+
// single global throttle for everything but the trace scan, which is the
|
|
795
|
+
// one thing that really does move that fast.
|
|
796
|
+
if (scanStale(led.wiki && led.wiki.scanned_at, now, TTL.wiki)) {
|
|
797
|
+
const w = { unregistered: false, distance: null, scanned_at: now };
|
|
798
|
+
const metaPath = path.join(projectDir, ".claude", "orc", "wiki-meta.json");
|
|
799
|
+
if (!fs.existsSync(metaPath)) {
|
|
800
|
+
// Docs but no manifest = UNREGISTERED: a real wiki nothing has indexed
|
|
801
|
+
// (usually a scan stopped at a 5-area pause). It is otherwise invisible
|
|
802
|
+
// — consumers and `orc crosslink` read the manifest — so surface it
|
|
803
|
+
// here, with the free fix. Never say "no wiki": these docs are already
|
|
804
|
+
// paid for.
|
|
805
|
+
const wikiDir = path.join(projectDir, "wiki");
|
|
806
|
+
w.unregistered =
|
|
807
|
+
fs.existsSync(wikiDir) &&
|
|
808
|
+
fs.readdirSync(wikiDir).some((f) => f.startsWith("orc-") && f.endsWith(".md"));
|
|
809
|
+
} else {
|
|
810
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
811
|
+
if (meta && meta.scan_commit) {
|
|
812
|
+
// A FAILED probe is a fact, and it is cached like any other. No git,
|
|
813
|
+
// a detached commit, a timeout — without this inner catch the whole
|
|
814
|
+
// block aborts before the ledger is stamped, and the subprocess runs
|
|
815
|
+
// again on the very next keystroke. The segment stays absent either
|
|
816
|
+
// way; what changes is that it costs nothing to stay absent.
|
|
817
|
+
try {
|
|
818
|
+
const { execSync } = require("child_process");
|
|
819
|
+
const n = parseInt(
|
|
820
|
+
execSync(`git rev-list --count ${meta.scan_commit}..HEAD`, {
|
|
821
|
+
cwd: projectDir,
|
|
822
|
+
timeout: 3000,
|
|
823
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
824
|
+
})
|
|
825
|
+
.toString()
|
|
826
|
+
.trim(),
|
|
827
|
+
10
|
|
828
|
+
);
|
|
829
|
+
if (Number.isFinite(n)) w.distance = n;
|
|
830
|
+
} catch (_) {}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
led.wiki = w;
|
|
834
|
+
}
|
|
835
|
+
const w = led.wiki;
|
|
836
|
+
if (w) {
|
|
837
|
+
SCAN.wiki = {
|
|
838
|
+
distance: typeof w.distance === "number" ? w.distance : null,
|
|
839
|
+
tier: w.unregistered
|
|
840
|
+
? "unregistered"
|
|
841
|
+
: typeof w.distance !== "number"
|
|
842
|
+
? null
|
|
843
|
+
: w.distance > 30
|
|
844
|
+
? "stale"
|
|
845
|
+
: w.distance >= 10
|
|
846
|
+
? "aging"
|
|
847
|
+
: "fresh",
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
if (w && w.unregistered) line += " · wiki: UNREGISTERED (run `orc wiki sync`)";
|
|
851
|
+
else if (w && typeof w.distance === "number") {
|
|
852
|
+
if (w.distance >= 10 && w.distance <= 30) line += ` · wiki: AGING (${w.distance}c)`;
|
|
853
|
+
else if (w.distance > 30) line += ` · wiki: STALE (${w.distance}c)`;
|
|
854
|
+
else line += " · wiki: fresh";
|
|
855
|
+
}
|
|
856
|
+
} catch (_) {}
|
|
857
|
+
|
|
858
|
+
// orc-diy gate segment (only when a flow exists). Recomputes the same
|
|
859
|
+
// staleness checks as `orc diy status` from flow.lock.json — written only
|
|
860
|
+
// by the `orc diy` CLI. Fail-silent: any error → no segment.
|
|
861
|
+
try {
|
|
862
|
+
const fs = require("fs");
|
|
863
|
+
const path = require("path");
|
|
864
|
+
const crypto = require("crypto");
|
|
865
|
+
const projectDir =
|
|
866
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
867
|
+
if (!wants("diy.state") && !wants("diy.name") && !wants("diy.tier_state"))
|
|
868
|
+
throw new Error("not needed");
|
|
869
|
+
const lockPath = path.join(projectDir, ".claude", "orc", "diy", "flow.lock.json");
|
|
870
|
+
if (fs.existsSync(lockPath)) {
|
|
871
|
+
const lock = JSON.parse(
|
|
872
|
+
fs.readFileSync(lockPath, "utf8").replace(/^\uFEFF/, "")
|
|
873
|
+
);
|
|
874
|
+
const sha = (p) =>
|
|
875
|
+
crypto.createHash("sha256").update(fs.readFileSync(p, "utf8")).digest("hex");
|
|
876
|
+
const cfgPath = path.join(projectDir, ".claude", "orc-diy.config.yaml");
|
|
877
|
+
const compiledPath = path.join(projectDir, ".claude", "orc", "diy", "FLOW-COMPILED.md");
|
|
878
|
+
let installedV = null;
|
|
879
|
+
try {
|
|
880
|
+
installedV = JSON.parse(
|
|
881
|
+
fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")
|
|
882
|
+
).version;
|
|
883
|
+
} catch (_) {}
|
|
884
|
+
const ready =
|
|
885
|
+
lock.compiled_hash &&
|
|
886
|
+
fs.existsSync(cfgPath) &&
|
|
887
|
+
lock.config_hash === sha(cfgPath) &&
|
|
888
|
+
fs.existsSync(compiledPath) &&
|
|
889
|
+
lock.compiled_hash === sha(compiledPath) &&
|
|
890
|
+
(!installedV || lock.orc_version === installedV);
|
|
891
|
+
let seg = `diy:${lock.flow_name || "flow"} ${ready ? "READY" : "STALE→recompile"}`;
|
|
892
|
+
// Model half of the compiled session_tier — warn-only (hooks can't block on
|
|
893
|
+
// model). Keyed by the slug's MODEL part, so the full tier grid (sonnet-4-6,
|
|
894
|
+
// opus-4-7, opus-4-8, fable-5 at any effort) is covered without enumerating
|
|
895
|
+
// every effort slug.
|
|
896
|
+
if (ready && modelKnown && lock.session_tier) {
|
|
897
|
+
const modelPart = String(lock.session_tier).replace(/-(med|high|xhigh|max)$/, "");
|
|
898
|
+
const want = {
|
|
899
|
+
"sonnet-4-6": /sonnet[\s._-]?4[\s._-]?6\b/,
|
|
900
|
+
"opus-4-7": /opus[\s._-]?4[\s._-]?7\b/,
|
|
901
|
+
"opus-4-8": /opus[\s._-]?4[\s._-]?8\b/,
|
|
902
|
+
"opus-5": /opus[\s._-]?5\b/,
|
|
903
|
+
"fable-5": /fable[\s._-]?5\b/,
|
|
904
|
+
}[modelPart];
|
|
905
|
+
if (want && !want.test(hay)) seg += ` ⛔model≠${lock.session_tier}`;
|
|
906
|
+
}
|
|
907
|
+
line += " · " + seg;
|
|
908
|
+
}
|
|
909
|
+
} catch (_) {}
|
|
910
|
+
|
|
911
|
+
// Append an update hint from the cache (instant, no network here).
|
|
912
|
+
if (updater) {
|
|
913
|
+
try {
|
|
914
|
+
const nudge = updater.readCachedNudge(updater.installedVersion(__dirname));
|
|
915
|
+
if (nudge) line += " · " + nudge;
|
|
916
|
+
} catch (_) {}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// The branch is its OWN provider: `.git/HEAD` with no subprocess, on a
|
|
920
|
+
// different clock from the trace scan, and a layout that shows a branch and
|
|
921
|
+
// nothing else must not pay for a trace scan to get it.
|
|
922
|
+
if (wantsProvider("scan.git")) {
|
|
923
|
+
try {
|
|
924
|
+
const projectDir =
|
|
925
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
926
|
+
SCAN.branch = gitBranch(projectDir) || null;
|
|
927
|
+
} catch (_) {}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// The extended scan (v1.3.0 W3). Only a composed layout can reach it, and
|
|
931
|
+
// only for the providers its own lock names.
|
|
932
|
+
extendedScan(d, wants, wantsProvider);
|
|
933
|
+
// The sparkline series, sampled during the scan that already ran.
|
|
934
|
+
sampleSeries(d, PLAN);
|
|
935
|
+
|
|
936
|
+
// ── Session line (v1.2.0) ──────────────────────────────────────────────────
|
|
937
|
+
// Line 1 answers "what tier am I on and how full is the window". This second
|
|
938
|
+
// line answers "what has this session actually been DOING" — how many agents
|
|
939
|
+
// it spawned, which lanes ran, whether work can leave Claude, and how long it
|
|
940
|
+
// has been going. All of it is read from disk; none of it costs a model call.
|
|
941
|
+
//
|
|
942
|
+
// The dispatch count is the one that earns its place. v1.2.0 exists because a
|
|
943
|
+
// retry cloned a live agent three times over and nothing surfaced it. A count
|
|
944
|
+
// that says `7 (2 running)` makes that visible from the status bar.
|
|
945
|
+
//
|
|
946
|
+
// Fail-silent and THROTTLED: the statusline re-renders on every keystroke, so
|
|
947
|
+
// the trace scan runs at most every 5s and its answer is cached in the same
|
|
948
|
+
// per-session ledger. Any error → no second line, never a broken one.
|
|
949
|
+
let line2 = "";
|
|
950
|
+
try {
|
|
951
|
+
const fs = require("fs");
|
|
952
|
+
const path = require("path");
|
|
953
|
+
const projectDir =
|
|
954
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
955
|
+
const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
956
|
+
|
|
957
|
+
// The hook cannot read the RESOLVED config — that is the lane resolver's
|
|
958
|
+
// job, and a hook has no lane — so this reads the two raw keys it needs
|
|
959
|
+
// straight from the file and takes the documented default
|
|
960
|
+
// otherwise — the same caveat the wiki segment above already carries. A
|
|
961
|
+
// user override shifts skill behaviour; this label follows the file.
|
|
962
|
+
let logRel = ".claude/orc/logs";
|
|
963
|
+
let extraOn = false;
|
|
964
|
+
try {
|
|
965
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
966
|
+
const ld = /^[ \t]*log_dir:[ \t]*["']?([^"'#\r\n]+)/m.exec(raw);
|
|
967
|
+
if (ld) logRel = ld[1].trim();
|
|
968
|
+
extraOn = /^[ \t]*extra_enabled:[ \t]*true[ \t]*$/m.test(raw);
|
|
969
|
+
} catch (_) {}
|
|
970
|
+
|
|
971
|
+
const now = Date.now();
|
|
972
|
+
if (!wantsProvider("scan.trace")) throw new Error("not needed");
|
|
973
|
+
if (scanStale(led.dispatch && led.dispatch.scanned_at, now, TTL.trace)) {
|
|
974
|
+
const logDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, logRel);
|
|
975
|
+
const sessionFloor = Math.floor((led.started_at || 0) / 1000) * 1000;
|
|
976
|
+
let spawns = 0;
|
|
977
|
+
let running = 0;
|
|
978
|
+
const lanes = [];
|
|
979
|
+
// The ACTIVE run, for `status:` (v1.2.1). `.current` is the pointer the
|
|
980
|
+
// lanes and the trace hook both write, so it — not "the newest file" — is
|
|
981
|
+
// what names the run in progress. A pointer naming a file that is not
|
|
982
|
+
// there answers nothing, and a pointer nobody deleted is not a run: the
|
|
983
|
+
// resolver's own staleness gate settles that from the trace's last line.
|
|
984
|
+
let activeFile = null;
|
|
985
|
+
try {
|
|
986
|
+
const cur = fs.readFileSync(path.join(logDir, ".current"), "utf8").trim();
|
|
987
|
+
if (cur) activeFile = cur;
|
|
988
|
+
} catch (_) {}
|
|
989
|
+
let phase = null;
|
|
990
|
+
try {
|
|
991
|
+
for (const f of fs.readdirSync(logDir)) {
|
|
992
|
+
if (!f.startsWith("run-") || !f.endsWith(".txt")) continue;
|
|
993
|
+
const full = path.join(logDir, f);
|
|
994
|
+
let st;
|
|
995
|
+
try { st = fs.statSync(full); } catch (_) { continue; }
|
|
996
|
+
const active = f === activeFile;
|
|
997
|
+
// Only traces touched since this session began count towards the
|
|
998
|
+
// session's SPEND — a trace from last week is not this session's.
|
|
999
|
+
// The ACTIVE run is exempt from that filter: `status:` answers "what
|
|
1000
|
+
// is ORC doing", which has nothing to do with who paid for it, and a
|
|
1001
|
+
// run already going when this session opened would otherwise be
|
|
1002
|
+
// invisible until it happened to write its next line. Its own
|
|
1003
|
+
// staleness gate decides whether it is still running.
|
|
1004
|
+
if (!active && st.mtimeMs < (led.started_at || 0)) continue;
|
|
1005
|
+
const text = fs.readFileSync(full, "utf8");
|
|
1006
|
+
// Count by the trace's OWN line timestamps, not the file's mtime. A
|
|
1007
|
+
// run that was already going when this session started shares its
|
|
1008
|
+
// file with the session before it, and mtime cannot tell the two
|
|
1009
|
+
// apart — it would attribute the whole file to whoever looked last.
|
|
1010
|
+
let mine = 0;
|
|
1011
|
+
for (const raw of text.split("\n")) {
|
|
1012
|
+
const t = /^\[(\d{2})(\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(raw);
|
|
1013
|
+
if (!t) continue;
|
|
1014
|
+
if (raw.indexOf("] hook") === -1 || raw.indexOf(" SPAWN ") === -1) continue;
|
|
1015
|
+
const at = new Date(
|
|
1016
|
+
2000 + Number(t[3]), Number(t[2]) - 1, Number(t[1]),
|
|
1017
|
+
Number(t[4]), Number(t[5]), Number(t[6])
|
|
1018
|
+
).getTime();
|
|
1019
|
+
// Trace stamps have SECOND resolution and started_at has
|
|
1020
|
+
// milliseconds, so a dispatch in the same second as the
|
|
1021
|
+
// session start compares as earlier than it. Floor the
|
|
1022
|
+
// boundary to the second the trace could actually express.
|
|
1023
|
+
if (at >= sessionFloor) mine += 1;
|
|
1024
|
+
}
|
|
1025
|
+
spawns += mine;
|
|
1026
|
+
// The phase comes off the text this loop already read — one more
|
|
1027
|
+
// regex pass, no second file read.
|
|
1028
|
+
if (active) {
|
|
1029
|
+
const lm = /^run-([a-z0-9-]+?)-.+-\d{6}-\d{6}\.txt$/.exec(f);
|
|
1030
|
+
if (lm) phase = resolvePhase(lm[1], text);
|
|
1031
|
+
}
|
|
1032
|
+
let openHere = 0;
|
|
1033
|
+
try {
|
|
1034
|
+
const pend = JSON.parse(fs.readFileSync(full + ".pending.json", "utf8"));
|
|
1035
|
+
if (Array.isArray(pend)) openHere = pend.length;
|
|
1036
|
+
} catch (_) {}
|
|
1037
|
+
running += openHere;
|
|
1038
|
+
// A lane earns its name by having actually dispatched in this
|
|
1039
|
+
// session — listing a lane that contributed nothing is noise.
|
|
1040
|
+
if (mine > 0 || openHere > 0) {
|
|
1041
|
+
const m = /^run-([a-z0-9-]+?)-.+-\d{6}-\d{6}\.txt$/.exec(f);
|
|
1042
|
+
if (m && lanes.indexOf(m[1]) === -1) lanes.push(m[1]);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
} catch (_) {}
|
|
1046
|
+
led.dispatch = { spawns, running, lanes, phase, scanned_at: now };
|
|
1047
|
+
// MTok rides in the same throttled pass, and reads only the bytes the
|
|
1048
|
+
// transcript has grown by since the last one.
|
|
1049
|
+
led.tok = scanTokens(led, d.transcript_path || null);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [], phase: null };
|
|
1053
|
+
// The composed layout reads THESE — the same numbers the shipped line
|
|
1054
|
+
// below prints, from the same scan.
|
|
1055
|
+
SCAN.spawns = dsp.spawns || 0;
|
|
1056
|
+
SCAN.running = dsp.running || 0;
|
|
1057
|
+
SCAN.lanes = dsp.lanes || [];
|
|
1058
|
+
SCAN.phase = dsp.phase || null;
|
|
1059
|
+
SCAN.extra_enabled = !!extraOn;
|
|
1060
|
+
const parts = [];
|
|
1061
|
+
// `status:` leads, because what ORC is doing right now is the one thing on
|
|
1062
|
+
// this line that changes minute to minute. It is also the ONE segment
|
|
1063
|
+
// allowed to vanish: a phase the disk cannot prove is hidden rather than
|
|
1064
|
+
// guessed, and the glyph goes with it. It replaces v1.2.0's `lanes:` list —
|
|
1065
|
+
// the running lane is its first word, and `orc stats` / `orc run list`
|
|
1066
|
+
// still hold the whole session's history.
|
|
1067
|
+
if (dsp.phase && dsp.phase.label)
|
|
1068
|
+
parts.push(
|
|
1069
|
+
motifFrame(dsp.phase.kind) + " status: " + dsp.phase.lane + " · " + dsp.phase.label
|
|
1070
|
+
);
|
|
1071
|
+
// `running` is never hidden, because an agent still in flight is the thing
|
|
1072
|
+
// a user most needs to see (v1.2.0). Zero is simply not printed.
|
|
1073
|
+
parts.push(
|
|
1074
|
+
"agents " + dsp.spawns + (dsp.running ? " (" + dsp.running + " running)" : "")
|
|
1075
|
+
);
|
|
1076
|
+
parts.push("orc-extra: " + (extraOn ? "on" : "off"));
|
|
1077
|
+
if (led.started_at)
|
|
1078
|
+
parts.push("Dur " + Math.max(0, Math.round((now - led.started_at) / 60000)) + "m");
|
|
1079
|
+
// MTok keeps its slot in every state. An em dash says "not measured"; a `0`
|
|
1080
|
+
// would say the session was free, and that is a different claim.
|
|
1081
|
+
parts.push("MTok " + (fmtTokens(led.tok) || "—"));
|
|
1082
|
+
if (SCAN.branch) parts.push(SCAN.branch);
|
|
1083
|
+
line2 = " " + parts.join(" · ");
|
|
1084
|
+
} catch (_) {
|
|
1085
|
+
line2 = "";
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ONE write, after every block that touches the ledger has had its say. It is
|
|
1089
|
+
// last on purpose: a render that throws half way through still prints, and a
|
|
1090
|
+
// ledger that could not be written never takes the status line down with it.
|
|
1091
|
+
ledgerFlush();
|
|
1092
|
+
|
|
1093
|
+
// ── THE CUSTOM LAYOUT (v1.3.0) ──────────────────────────────────────────
|
|
1094
|
+
// Everything above is the SHIPPED status line, and it is what renders unless
|
|
1095
|
+
// the user composed their own. `custom()` returns null in every state but
|
|
1096
|
+
// one, and each of its gates is a FALLBACK rather than a throw: a hook cannot
|
|
1097
|
+
// refuse, so a bad layout must degrade to something correct rather than paint
|
|
1098
|
+
// garbage. The ladder is documented on the function itself.
|
|
1099
|
+
const composed = custom(d, {
|
|
1100
|
+
payload: d,
|
|
1101
|
+
ledger: LED || {},
|
|
1102
|
+
scan: SCAN,
|
|
1103
|
+
derived: { verdict, reasons, version: ver },
|
|
1104
|
+
now: Date.now(),
|
|
1105
|
+
});
|
|
1106
|
+
if (composed != null) {
|
|
1107
|
+
process.stdout.write(composed);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
process.stdout.write(line2 ? line + "\n" + line2 : line);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// ── The six-rung gate ladder ────────────────────────────────────────────────
|
|
1115
|
+
// In order, and EVERY RUNG IS A FALLBACK, never a throw:
|
|
1116
|
+
//
|
|
1117
|
+
// 1 statusline_custom is not `on` the shipped lines. BYTE-IDENTICAL.
|
|
1118
|
+
// 2 statusline-compiled.json missing / default + statusline-layout-unreadable
|
|
1119
|
+
// unparseable / schema mismatch
|
|
1120
|
+
// 3 orc_version or catalog_hash moved default + statusline-layout-stale
|
|
1121
|
+
// 4 an unknown op or an unknown binding default + statusline-layout-skew
|
|
1122
|
+
// 5 the cheap shape guard fails default + statusline-layout-invalid
|
|
1123
|
+
// 6 otherwise run the program
|
|
1124
|
+
//
|
|
1125
|
+
// Rungs 3 and 4 are why the lock file exists. Rung 5 is this hook RE-CHECKING
|
|
1126
|
+
// rather than trusting the file, because a hand-edited compiled file is a file
|
|
1127
|
+
// nobody validated. Every fallback RECORDS ITSELF in the ledger so `orc doctor`
|
|
1128
|
+
// can name it: a status line that quietly went back to the default and never
|
|
1129
|
+
// said why is a bug the user cannot report.
|
|
1130
|
+
function custom(d, ctx) {
|
|
1131
|
+
try {
|
|
1132
|
+
const fs = require("fs");
|
|
1133
|
+
const path = require("path");
|
|
1134
|
+
const projectDir =
|
|
1135
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
1136
|
+
const orcDir = path.join(projectDir, ".claude", "orc");
|
|
1137
|
+
|
|
1138
|
+
// Rung 1. The hook cannot resolve config — that is the lane resolver's job,
|
|
1139
|
+
// and a hook has no lane — so it reads the raw key off the file and takes
|
|
1140
|
+
// the documented default otherwise, exactly as it already does for
|
|
1141
|
+
// `log_dir` and `extra_enabled`.
|
|
1142
|
+
let on = false;
|
|
1143
|
+
try {
|
|
1144
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
1145
|
+
// The trailing \r is tolerated on purpose: a config file written on
|
|
1146
|
+
// Windows carries CRLF, and a $-anchored match silently never fires
|
|
1147
|
+
// there — which is a feature that is ON in the file and OFF on the bar.
|
|
1148
|
+
on = /^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw);
|
|
1149
|
+
} catch (_) {}
|
|
1150
|
+
if (!on) return null;
|
|
1151
|
+
|
|
1152
|
+
// Rung 2. THE HOOK NEVER READS THE AUTHORED LAYOUT — not as a fallback, not
|
|
1153
|
+
// on a cache miss, not ever. One consumer per file.
|
|
1154
|
+
let prog = null;
|
|
1155
|
+
try {
|
|
1156
|
+
prog = JSON.parse(fs.readFileSync(path.join(orcDir, "statusline-compiled.json"), "utf8"));
|
|
1157
|
+
} catch (_) {}
|
|
1158
|
+
if (!prog || prog.schema !== 1) return slFallback(orcDir, "statusline-layout-unreadable");
|
|
1159
|
+
|
|
1160
|
+
// Rung 3. An ORC upgrade that adds, removes or changes a component
|
|
1161
|
+
// invalidates every compiled layout on the machine. Better the shipped
|
|
1162
|
+
// lines than a program compiled against a catalogue that no longer exists.
|
|
1163
|
+
let lock = null;
|
|
1164
|
+
try {
|
|
1165
|
+
lock = JSON.parse(fs.readFileSync(path.join(orcDir, "statusline.lock.json"), "utf8"));
|
|
1166
|
+
} catch (_) {}
|
|
1167
|
+
let installed = null;
|
|
1168
|
+
try {
|
|
1169
|
+
installed = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")).version;
|
|
1170
|
+
} catch (_) {}
|
|
1171
|
+
if (!lock || (installed && lock.orc_version !== installed))
|
|
1172
|
+
return slFallback(orcDir, "statusline-layout-stale");
|
|
1173
|
+
|
|
1174
|
+
const engine = require("./orc-statusline-render.js");
|
|
1175
|
+
|
|
1176
|
+
// Rung 4. A binding this build does not have is an INSTALL SKEW, not a
|
|
1177
|
+
// rendering problem, and it must not be papered over one item at a time.
|
|
1178
|
+
for (const b of lock.bindings || []) {
|
|
1179
|
+
if (!engine.BINDINGS[b]) return slFallback(orcDir, "statusline-layout-skew");
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Rung 5. The cheap shape guard: at most three lines, at most `max_per_line`
|
|
1183
|
+
// components on any of them, and the dense-prefix rule —
|
|
1184
|
+
// A line may hold a component only if every line above it holds at least one.
|
|
1185
|
+
//
|
|
1186
|
+
// THE CAP AND THE COUNTING RULE ARE BOTH THE COMPILER'S (v1.4.2). This held
|
|
1187
|
+
// its own `5` and counted EVERY item op, so it disagreed with the validator
|
|
1188
|
+
// twice: it bounced a legal six-part line, and it bounced a legal five plus
|
|
1189
|
+
// a spacer. Both fell silently back to the shipped lines with the reason
|
|
1190
|
+
// only in a ledger file, which reads exactly like "my sixth part reset the
|
|
1191
|
+
// bar to the default". The cap rides on the lock (already gated on
|
|
1192
|
+
// `orc_version` at rung 3, so a stale copy cannot outlive its build) and
|
|
1193
|
+
// the compiler marks a structural op `s` — this cannot know that on its
|
|
1194
|
+
// own, because a compiled op carries an INSTANCE id and never a type.
|
|
1195
|
+
if (!Array.isArray(prog.lines) || prog.lines.length > 3)
|
|
1196
|
+
return slFallback(orcDir, "statusline-layout-invalid");
|
|
1197
|
+
const cap = Number(lock.max_per_line) > 0 ? Number(lock.max_per_line) : 6;
|
|
1198
|
+
let seenEmpty = false;
|
|
1199
|
+
for (const l of prog.lines) {
|
|
1200
|
+
const ops = (l.ops || []).filter((o) => o.op === "item");
|
|
1201
|
+
if (ops.filter((o) => !o.s).length > cap) return slFallback(orcDir, "statusline-layout-invalid");
|
|
1202
|
+
if (ops.length === 0) seenEmpty = true;
|
|
1203
|
+
else if (seenEmpty) return slFallback(orcDir, "statusline-layout-invalid");
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Rung 6. Run the program. Failure isolation lives inside the engine: one
|
|
1207
|
+
// throwing item emits its unknown form and the rest of the line survives.
|
|
1208
|
+
SCAN.preset = prog.preset || null;
|
|
1209
|
+
const out = engine.render(prog, {
|
|
1210
|
+
payload: ctx.payload,
|
|
1211
|
+
ledger: ctx.ledger,
|
|
1212
|
+
scan: ctx.scan,
|
|
1213
|
+
derived: ctx.derived,
|
|
1214
|
+
now: ctx.now,
|
|
1215
|
+
cols: Number(process.env.COLUMNS) || 0,
|
|
1216
|
+
env: process.env,
|
|
1217
|
+
});
|
|
1218
|
+
if (out.errors && out.errors.length) slNote(orcDir, "statusline-item-failed", out.errors[0]);
|
|
1219
|
+
return out.text;
|
|
1220
|
+
} catch (_) {
|
|
1221
|
+
// Even the ladder must not throw. A status line that crashes is a status
|
|
1222
|
+
// line that is simply absent, with no way to find out why.
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// A fallback RECORDS ITSELF. `orc doctor` reads this file and names the finding
|
|
1228
|
+
// with the exact command that clears it.
|
|
1229
|
+
function slFallback(orcDir, finding) {
|
|
1230
|
+
slNote(orcDir, finding, null);
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
function slNote(orcDir, finding, detail) {
|
|
1234
|
+
try {
|
|
1235
|
+
const fs = require("fs");
|
|
1236
|
+
const path = require("path");
|
|
1237
|
+
fs.mkdirSync(orcDir, { recursive: true });
|
|
1238
|
+
fs.writeFileSync(
|
|
1239
|
+
path.join(orcDir, "statusline-state.json"),
|
|
1240
|
+
JSON.stringify({ finding, detail: detail || null, at: Date.now() }) + "\n"
|
|
1241
|
+
);
|
|
1242
|
+
} catch (_) {}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// ── THE EXTENDED SCAN (v1.3.0 W3) ──────────────────────────────────────────
|
|
1246
|
+
// Groups D, E, F and G — knowledge, extra, flow and the health gates. Every one
|
|
1247
|
+
// of them is a `new read`: a read that does not exist in the shipped status
|
|
1248
|
+
// line and only happens when a layout asks for it.
|
|
1249
|
+
//
|
|
1250
|
+
// THE BAR IS 15 MILLISECONDS, NOT 300. W0 measured node startup at ~285 ms of a
|
|
1251
|
+
// 300 ms budget, so a component here has to be answerable from a small JSON
|
|
1252
|
+
// file, on its own clock, or it does not ship. Two are refused for that reason
|
|
1253
|
+
// and say so in the catalogue.
|
|
1254
|
+
//
|
|
1255
|
+
// Every read below is:
|
|
1256
|
+
// - gated on a BINDING the compiled lock names (the read planner);
|
|
1257
|
+
// - cached in the per-session ledger under its own key;
|
|
1258
|
+
// - given its OWN TTL, because a pact ledger and a spend log do not move at
|
|
1259
|
+
// the same rate.
|
|
1260
|
+
//
|
|
1261
|
+
// A read that throws leaves its slot null, and a null renders an em dash.
|
|
1262
|
+
// UNKNOWN IS NOT ZERO — a `0` would say the thing was measured and found empty.
|
|
1263
|
+
function extendedScan(d, wants, wantsProvider) {
|
|
1264
|
+
if (!wantsProvider("scan.extended")) return;
|
|
1265
|
+
let fs, path, projectDir, led, now;
|
|
1266
|
+
try {
|
|
1267
|
+
fs = require("fs");
|
|
1268
|
+
path = require("path");
|
|
1269
|
+
projectDir = (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
1270
|
+
led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
1271
|
+
now = Date.now();
|
|
1272
|
+
} catch (_) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const orc = path.join(projectDir, ".claude", "orc");
|
|
1276
|
+
const readJson = (p) => {
|
|
1277
|
+
try {
|
|
1278
|
+
return JSON.parse(fs.readFileSync(p, "utf8").replace(/^/, ""));
|
|
1279
|
+
} catch (_) {
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
// One cached sub-scan. `key` is its ledger slot, `ttl` its own clock.
|
|
1284
|
+
const cached = (key, ttl, fn) => {
|
|
1285
|
+
const slot = led.ext && led.ext[key];
|
|
1286
|
+
if (slot && !scanStale(slot.at, now, ttl)) return slot.v;
|
|
1287
|
+
let v = null;
|
|
1288
|
+
try {
|
|
1289
|
+
v = fn();
|
|
1290
|
+
} catch (_) {
|
|
1291
|
+
v = null;
|
|
1292
|
+
}
|
|
1293
|
+
led.ext = led.ext || {};
|
|
1294
|
+
led.ext[key] = { v, at: now };
|
|
1295
|
+
return v;
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
// ── Group D — knowledge. All of it comes out of wiki-meta.json, which is
|
|
1299
|
+
// written ONLY by `orc wiki sync` and is 100% doc-header-derived. A wiki
|
|
1300
|
+
// tier does not move in a minute, so the TTL is generous.
|
|
1301
|
+
if (wants("wiki.docs")) {
|
|
1302
|
+
const w = cached("wiki_meta", TTL.wiki, () => {
|
|
1303
|
+
const meta = readJson(path.join(orc, "wiki-meta.json"));
|
|
1304
|
+
if (!meta) return null;
|
|
1305
|
+
const docs = Array.isArray(meta.docs) ? meta.docs : [];
|
|
1306
|
+
return {
|
|
1307
|
+
docs: docs.length,
|
|
1308
|
+
};
|
|
1309
|
+
});
|
|
1310
|
+
SCAN.wiki_meta = w;
|
|
1311
|
+
}
|
|
1312
|
+
if (wants("pattern.state")) {
|
|
1313
|
+
// EXISTENCE only, and by the deterministic probe's own rule: the cache
|
|
1314
|
+
// lives under the hidden .claude/ dir, so a raw filesystem search
|
|
1315
|
+
// false-negatives from the wrong cwd.
|
|
1316
|
+
SCAN.pattern = cached("pattern", TTL.knowledge, () => {
|
|
1317
|
+
const dir = path.join(orc, "patterns");
|
|
1318
|
+
try {
|
|
1319
|
+
return fs.readdirSync(dir).some((f) => f.endsWith("-pattern.md")) ? "cached" : "none";
|
|
1320
|
+
} catch (_) {
|
|
1321
|
+
return "none";
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
if (wants("crosslink.state") || wants("crosslink.peers")) {
|
|
1326
|
+
const c = cached("crosslink", TTL.knowledge, () => {
|
|
1327
|
+
const raw = (() => {
|
|
1328
|
+
try {
|
|
1329
|
+
return fs.readFileSync(path.join(projectDir, ".claude", "orc-crosslink.config.yaml"), "utf8");
|
|
1330
|
+
} catch (_) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
})();
|
|
1334
|
+
if (!raw) return null;
|
|
1335
|
+
const n = (raw.match(/^[ \t]*-[ \t]*name:/gm) || []).length;
|
|
1336
|
+
return { peers: n, state: n ? "linked" : "none" };
|
|
1337
|
+
});
|
|
1338
|
+
SCAN.crosslink = c;
|
|
1339
|
+
}
|
|
1340
|
+
if (wants("gotchas.count")) {
|
|
1341
|
+
SCAN.gotchas = cached("gotchas", TTL.knowledge, () => {
|
|
1342
|
+
try {
|
|
1343
|
+
return fs.readdirSync(path.join(orc, "gotchas")).filter((f) => f.endsWith(".md")).length;
|
|
1344
|
+
} catch (_) {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
// The code graph (v1.8.0). A FLOOR, exactly like the wiki row: the commit the
|
|
1350
|
+
// index was built at against HEAD, both read from disk with no subprocess.
|
|
1351
|
+
// `behind` means HEAD moved — never that files changed, which only
|
|
1352
|
+
// `orc graph status` can see. The key is read off the raw file, because a hook
|
|
1353
|
+
// has no lane and cannot resolve config (the `statusline_custom` answer).
|
|
1354
|
+
if (wants("graph.state")) {
|
|
1355
|
+
SCAN.graph = cached("graph", TTL.knowledge, () => {
|
|
1356
|
+
let raw = "";
|
|
1357
|
+
try {
|
|
1358
|
+
raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
1359
|
+
} catch (_) {}
|
|
1360
|
+
if (!/^[ \t]*code_graph:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw)) return { state: "off" };
|
|
1361
|
+
const meta = readJson(path.join(orc, "graph", "meta.json"));
|
|
1362
|
+
if (!meta) return { state: "none" };
|
|
1363
|
+
const head = gitHeadCommit(projectDir);
|
|
1364
|
+
if (!head || !meta.head_commit) return null;
|
|
1365
|
+
return { state: head === meta.head_commit ? "fresh" : "behind" };
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// ── Group E — extra. `extra.json` is small; the SPEND LOG is not, so it is
|
|
1370
|
+
// read by TAIL and on its own faster clock, because a spend log is the one
|
|
1371
|
+
// thing here that moves during a wave.
|
|
1372
|
+
if (wants("extra.profile") || wants("extra.provider") || wants("extra.inflight") || wants("extra.passphrase") || wants("extra.demoted")) {
|
|
1373
|
+
SCAN.extra = cached("extra", TTL.extra, () => {
|
|
1374
|
+
const j = readJson(path.join(orc, "extra.json"));
|
|
1375
|
+
if (!j) return null;
|
|
1376
|
+
const profiles = j.profiles || {};
|
|
1377
|
+
const names = Object.keys(profiles);
|
|
1378
|
+
const first = names.length ? profiles[names[0]] : null;
|
|
1379
|
+
return {
|
|
1380
|
+
profile: names.length ? names[0] : null,
|
|
1381
|
+
provider: first && first.provider ? first.provider : null,
|
|
1382
|
+
profiles: names.length,
|
|
1383
|
+
};
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
if (wants("extra.spend") || wants("extra.tasks")) {
|
|
1387
|
+
SCAN.extra_spend = cached("extra_spend", TTL.extra, () => {
|
|
1388
|
+
// The tail only. A spend log grows for the life of a project and reading
|
|
1389
|
+
// all of it on a per-keystroke surface is the hazard this whole subsystem
|
|
1390
|
+
// is shaped around.
|
|
1391
|
+
const p = path.join(orc, "extra-spend.jsonl");
|
|
1392
|
+
let st;
|
|
1393
|
+
try {
|
|
1394
|
+
st = fs.statSync(p);
|
|
1395
|
+
} catch (_) {
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
const want = Math.min(st.size, 64 * 1024);
|
|
1399
|
+
const buf = Buffer.alloc(want);
|
|
1400
|
+
const fd = fs.openSync(p, "r");
|
|
1401
|
+
try {
|
|
1402
|
+
fs.readSync(fd, buf, 0, want, st.size - want);
|
|
1403
|
+
} finally {
|
|
1404
|
+
fs.closeSync(fd);
|
|
1405
|
+
}
|
|
1406
|
+
const lines = buf.toString("utf8").split("\n").slice(1).filter(Boolean);
|
|
1407
|
+
let tasks = 0;
|
|
1408
|
+
let usd = 0;
|
|
1409
|
+
let priced = false;
|
|
1410
|
+
for (const l of lines) {
|
|
1411
|
+
try {
|
|
1412
|
+
const r = JSON.parse(l);
|
|
1413
|
+
tasks++;
|
|
1414
|
+
if (typeof r.usd === "number") {
|
|
1415
|
+
usd += r.usd;
|
|
1416
|
+
priced = true;
|
|
1417
|
+
}
|
|
1418
|
+
} catch (_) {}
|
|
1419
|
+
}
|
|
1420
|
+
// A cost figure ORC did not price itself is never printed: `usd` stays
|
|
1421
|
+
// null rather than becoming a confident 0.
|
|
1422
|
+
return { tasks, usd: priced ? usd : null, partial: st.size > want };
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// ── Group F — flow and lanes. The diy lock is already read for the shipped
|
|
1427
|
+
// segment; `wait` is a small run-state file.
|
|
1428
|
+
if (wants("wait.state")) {
|
|
1429
|
+
SCAN.wait = cached("wait", TTL.run, () => {
|
|
1430
|
+
const j = readJson(path.join(orc, "wait.json"));
|
|
1431
|
+
if (!j) return "none";
|
|
1432
|
+
if (j.block_reason) return "blocked";
|
|
1433
|
+
return j.until || j.hops ? "waiting" : "none";
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
// `preset.name` is NOT read here: it rides in the compiled program, which is
|
|
1437
|
+
// the only file that crosses the wall. The hook never opens the authored
|
|
1438
|
+
// layout, and a field being convenient is not an exception to that.
|
|
1439
|
+
|
|
1440
|
+
// ── Group C remainder — the run's own progress, from RESUME.md, which is the
|
|
1441
|
+
// ONE line `orc resume` and `orc run list` parse. Reading the same line is
|
|
1442
|
+
// how a listing never has to open a checkpoint.
|
|
1443
|
+
if (wants("run.wave") || wants("run.wave_total") || wants("run.resume") || wants("run.open")) {
|
|
1444
|
+
SCAN.runs = cached("runs", TTL.run, () => {
|
|
1445
|
+
let logRel = ".claude/orc/logs";
|
|
1446
|
+
try {
|
|
1447
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
1448
|
+
const m = /^[ \t]*run_dir:[ \t]*["']?([^"'#\r\n]+)/m.exec(raw);
|
|
1449
|
+
if (m) logRel = m[1].trim();
|
|
1450
|
+
} catch (_) {}
|
|
1451
|
+
const runDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, ".claude", "orc", "run");
|
|
1452
|
+
let open = 0;
|
|
1453
|
+
let wave = null;
|
|
1454
|
+
let waves = null;
|
|
1455
|
+
try {
|
|
1456
|
+
for (const slug of fs.readdirSync(runDir)) {
|
|
1457
|
+
const rp = path.join(runDir, slug, "RESUME.md");
|
|
1458
|
+
if (!fs.existsSync(rp)) continue;
|
|
1459
|
+
if (fs.existsSync(path.join(runDir, slug, "closed.json"))) continue;
|
|
1460
|
+
open++;
|
|
1461
|
+
// The byte-stable `Where it stands:` line, at column 0. It is the one
|
|
1462
|
+
// line two other commands already parse, which is exactly why this
|
|
1463
|
+
// does not open a checkpoint.
|
|
1464
|
+
const m = /^Where it stands:.*wave (\d+) of (\d+)/m.exec(fs.readFileSync(rp, "utf8"));
|
|
1465
|
+
if (m && wave == null) {
|
|
1466
|
+
wave = Number(m[1]);
|
|
1467
|
+
waves = Number(m[2]);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
} catch (_) {
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
return { open, wave, waves, resume: open > 0 ? "waiting" : "none" };
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// ── Group G — the health gates. Each is one small ledger, each on the slow
|
|
1478
|
+
// clock: a pact does not drift between keystrokes.
|
|
1479
|
+
const gate = (key, file, fn) => {
|
|
1480
|
+
if (!wants(key)) return null;
|
|
1481
|
+
return cached(key.replace(/\./g, "_"), TTL.gates, () => fn(readJson(path.join(orc, file))));
|
|
1482
|
+
};
|
|
1483
|
+
SCAN.pact = gate("pact.state", "pact.json", (j) => {
|
|
1484
|
+
if (!j || !Array.isArray(j.entries)) return null;
|
|
1485
|
+
const live = j.entries.filter((e) => e.state !== "retired");
|
|
1486
|
+
const broken = live.filter((e) => e.state === "broken").length;
|
|
1487
|
+
const drifted = live.filter((e) => e.state === "drifted").length;
|
|
1488
|
+
// UNCHECKABLE is the honest state and it never reads as a failure.
|
|
1489
|
+
return { state: broken ? "broken" : drifted ? "drifted" : "holding", drifted, broken, total: live.length };
|
|
1490
|
+
});
|
|
1491
|
+
SCAN.boundary = gate("boundary.state", "boundary.json", (j) => {
|
|
1492
|
+
if (!j || !Array.isArray(j.cards)) return null;
|
|
1493
|
+
const refused = j.cards.filter((c) => c.verdict === "REFUSE").length;
|
|
1494
|
+
// An area with no card is UNKNOWN, never assumed safe.
|
|
1495
|
+
return { state: refused ? "refused" : "clear", refused, cards: j.cards.length };
|
|
1496
|
+
});
|
|
1497
|
+
SCAN.challenge = gate("challenge.state", "challenge.json", (j) => {
|
|
1498
|
+
if (!j) return null;
|
|
1499
|
+
const open = Array.isArray(j.findings) ? j.findings.filter((f) => !f.resolved).length : 0;
|
|
1500
|
+
return { state: j.state || (open ? "open" : "pass"), open, iteration: j.iteration || null };
|
|
1501
|
+
});
|
|
1502
|
+
SCAN.doc = gate("doc.state", "doc.json", (j) => {
|
|
1503
|
+
if (!j) return null;
|
|
1504
|
+
const outline = Array.isArray(j.outline) ? j.outline : [];
|
|
1505
|
+
const done = outline.filter((s) => s.hash).length;
|
|
1506
|
+
return { state: j.shipped ? "shipped" : "draft", done, total: outline.length };
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
// `usage-gate` reads the usage bridge this hook already writes — so it is the
|
|
1510
|
+
// cheapest `new read` in the set, and it is the only one that can say
|
|
1511
|
+
// `unknown`, which is a real answer and never a stop.
|
|
1512
|
+
if (wants("usage.state")) {
|
|
1513
|
+
SCAN.usage = cached("usage", TTL.trace, () => {
|
|
1514
|
+
const j = readJson(path.join(orc, "usage.json"));
|
|
1515
|
+
if (!j || !j.written_at) return "unknown";
|
|
1516
|
+
if (now - j.written_at > 30 * 60 * 1000) return "unknown";
|
|
1517
|
+
const w = [j.five_hour, j.seven_day].filter(Boolean).map((x) => x.used_percentage);
|
|
1518
|
+
if (!w.length) return "unknown";
|
|
1519
|
+
return Math.max(...w) >= 90 ? "low" : "ok";
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// A config key as a component. The config file is edited by hand, so 30s.
|
|
1524
|
+
if (wants("config.value")) {
|
|
1525
|
+
SCAN.config_raw = cached("config", TTL.config, () => {
|
|
1526
|
+
try {
|
|
1527
|
+
return fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
1528
|
+
} catch (_) {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// ── THE SERIES LEDGER (v1.3.0 W3) ──────────────────────────────────────────
|
|
1536
|
+
// `spark`, `spark-braille`, `trend` and `delta` need history, and a status line
|
|
1537
|
+
// has none: it is a fresh process every render. So the LAST 16 SAMPLES per
|
|
1538
|
+
// series live in the per-session ledger, appended during the throttled scan
|
|
1539
|
+
// that already ran.
|
|
1540
|
+
//
|
|
1541
|
+
// NO NEW READ, NO NEW TIMER. Every value sampled here was already computed for
|
|
1542
|
+
// something else this render; the series is a side effect of the scan, never a
|
|
1543
|
+
// reason for one.
|
|
1544
|
+
//
|
|
1545
|
+
// SIXTEEN, and only for the series a compiled layout actually names — the read
|
|
1546
|
+
// planner again. A series nothing binds is not kept, so the ledger does not
|
|
1547
|
+
// grow for a user whose layout has no sparkline on it.
|
|
1548
|
+
const SERIES_MAX = 16;
|
|
1549
|
+
const SERIES_MIN_GAP_MS = 20000;
|
|
1550
|
+
|
|
1551
|
+
function sampleSeries(d, plan) {
|
|
1552
|
+
try {
|
|
1553
|
+
if (!plan || !plan.series || !plan.series.size) return;
|
|
1554
|
+
const led = LED;
|
|
1555
|
+
if (!led) return;
|
|
1556
|
+
const now = Date.now();
|
|
1557
|
+
// A sample every 20 seconds, not every render. Sixteen samples at the
|
|
1558
|
+
// render rate would be five seconds of history, which is not history — it
|
|
1559
|
+
// is the same number sixteen times.
|
|
1560
|
+
if (led.series_at && now - led.series_at < SERIES_MIN_GAP_MS) return;
|
|
1561
|
+
led.series_at = now;
|
|
1562
|
+
led.series = led.series || {};
|
|
1563
|
+
const push = (key, v) => {
|
|
1564
|
+
if (!plan.series.has(key)) return;
|
|
1565
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return;
|
|
1566
|
+
const arr = led.series[key] || [];
|
|
1567
|
+
arr.push(Math.round(v));
|
|
1568
|
+
while (arr.length > SERIES_MAX) arr.shift();
|
|
1569
|
+
led.series[key] = arr;
|
|
1570
|
+
};
|
|
1571
|
+
const rl = d.rate_limits || {};
|
|
1572
|
+
const w = led.five_hour;
|
|
1573
|
+
push("quota5h", rl.five_hour && rl.five_hour.used_percentage);
|
|
1574
|
+
push("quotawk", rl.seven_day && rl.seven_day.used_percentage);
|
|
1575
|
+
push("ucs", w && typeof w.last === "number" && typeof w.baseline === "number"
|
|
1576
|
+
? Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline))
|
|
1577
|
+
: null);
|
|
1578
|
+
const tok = led.tok;
|
|
1579
|
+
if (tok) {
|
|
1580
|
+
push("mtok", (tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0));
|
|
1581
|
+
push("mtokkind", tok.cache_read || 0);
|
|
1582
|
+
}
|
|
1583
|
+
push("agents", SCAN.spawns);
|
|
1584
|
+
push("cost", d.cost && d.cost.total_cost_usd != null ? d.cost.total_cost_usd * 100 : null);
|
|
1585
|
+
push("cachehit", d.prompt_cache && d.prompt_cache.hit_ratio != null
|
|
1586
|
+
? (d.prompt_cache.hit_ratio <= 1 ? d.prompt_cache.hit_ratio * 100 : d.prompt_cache.hit_ratio)
|
|
1587
|
+
: null);
|
|
1588
|
+
push("cachewrite", d.prompt_cache && d.prompt_cache.cache_write_tokens);
|
|
1589
|
+
push("lines", d.cost && d.cost.total_lines_added != null
|
|
1590
|
+
? (d.cost.total_lines_added || 0) - (d.cost.total_lines_removed || 0)
|
|
1591
|
+
: null);
|
|
1592
|
+
push("extraspend", SCAN.extra_spend && SCAN.extra_spend.usd != null ? SCAN.extra_spend.usd * 100 : null);
|
|
1593
|
+
} catch (_) {
|
|
1594
|
+
// A series is a nicety. It never takes the status line down with it.
|
|
1595
|
+
}
|
|
1596
|
+
}
|