@azure-id/orc 1.2.1 → 1.4.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 +2907 -2663
- package/README-id.md +44 -0
- package/README.md +631 -665
- package/bin/cli.js +36866 -34355
- package/bin/verify-contracts.js +35 -1
- package/bin/verify-package.js +15 -0
- package/bin/webui/api.js +1283 -1201
- package/bin/webui/app.html +213 -210
- package/bin/webui/css/04-motion.css +43 -0
- package/bin/webui/css/06-responsive.css +42 -0
- package/bin/webui/css/panels/hookui.css +595 -0
- package/bin/webui/fixtures/hookui.js +431 -0
- package/bin/webui/fixtures/index.js +17 -0
- package/bin/webui/i18n/en/hookui.json +180 -0
- package/bin/webui/i18n/en/nav.json +22 -21
- package/bin/webui/i18n/en/overview.json +4 -1
- package/bin/webui/i18n/en/tour.json +3 -1
- package/bin/webui/i18n/id/hookui.json +180 -0
- package/bin/webui/i18n/id/nav.json +22 -21
- package/bin/webui/i18n/id/overview.json +4 -1
- package/bin/webui/i18n/id/tour.json +3 -1
- package/bin/webui/js/01-i18n.js +152 -151
- package/bin/webui/js/90-tour.js +6 -0
- package/bin/webui/js/panels/hookui.js +1313 -0
- package/bin/webui/js/panels/overview.js +7 -0
- package/package.json +1 -1
- package/templates/hooks/README.md +80 -2
- package/templates/hooks/orc-statusline-render.js +921 -0
- package/templates/hooks/orc-statusline.js +715 -68
- package/templates/hooks/orc-subagent-line.js +219 -0
|
@@ -339,6 +339,123 @@ function fmtTokens(tok) {
|
|
|
339
339
|
return String(n);
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// ── The per-session ledger: ONE read, ONE write, ONE throttle (v1.3.0 W0) ───
|
|
343
|
+
// `.claude/orc/usage-session.json` is the per-session ledger, and three
|
|
344
|
+
// separate blocks below want it: the rate-limit tracker, `ucs`, and the
|
|
345
|
+
// throttled line-2 scan. Each used to open the file itself and two of them
|
|
346
|
+
// wrote it, so one render cost three reads and two writes — on a surface that
|
|
347
|
+
// re-renders on every keystroke. It is memoised here instead: loaded at most
|
|
348
|
+
// once per process, mutated in place by whoever needs it, and flushed exactly
|
|
349
|
+
// once at the end. Same rules as before — RAW numbers only, never a computed
|
|
350
|
+
// word, fail-silent, and the reader decides what it means.
|
|
351
|
+
//
|
|
352
|
+
// The scan interval is the ONE seam over this budget, on the ORC_TEST_PROBE_MS
|
|
353
|
+
// precedent: a test that proves the throttle by SLEEPING past it is a test that
|
|
354
|
+
// fails on a loaded machine, and a flake is recorded and removed, never retried
|
|
355
|
+
// away. Unset, this is byte-identical to a hardcoded 5000, and nothing in ORC
|
|
356
|
+
// ever sets it.
|
|
357
|
+
let LED = null;
|
|
358
|
+
let LED_FILE = null;
|
|
359
|
+
|
|
360
|
+
// The scan's own answers, in ONE shape, so a composed layout (v1.3.0) reads
|
|
361
|
+
// exactly what the shipped lines read. It is populated as the blocks below
|
|
362
|
+
// compute their segments — never by a second pass over the disk. A provider
|
|
363
|
+
// nothing binds is simply never filled in, and every binding over it answers
|
|
364
|
+
// null, which renders an em dash. UNKNOWN IS NOT ZERO.
|
|
365
|
+
const SCAN = {
|
|
366
|
+
spawns: 0, running: 0, lanes: [], phase: null, slug: null,
|
|
367
|
+
branch: null, head: null, wiki: null, diy: null,
|
|
368
|
+
extra_enabled: false, update_version: null, inflight: null,
|
|
369
|
+
trace_age_min: null, trace_state: null, last_agent: null, retries: null,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
function ledger(projectDir, sid) {
|
|
373
|
+
if (LED) return LED;
|
|
374
|
+
const fs = require("fs");
|
|
375
|
+
const path = require("path");
|
|
376
|
+
LED_FILE = path.join(projectDir, ".claude", "orc", "usage-session.json");
|
|
377
|
+
let led = null;
|
|
378
|
+
try {
|
|
379
|
+
led = JSON.parse(fs.readFileSync(LED_FILE, "utf8"));
|
|
380
|
+
} catch (_) {}
|
|
381
|
+
if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
|
|
382
|
+
LED = led;
|
|
383
|
+
return LED;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function ledgerFlush() {
|
|
387
|
+
if (!LED || !LED_FILE) return;
|
|
388
|
+
try {
|
|
389
|
+
const fs = require("fs");
|
|
390
|
+
const path = require("path");
|
|
391
|
+
LED.updated_at = Date.now();
|
|
392
|
+
fs.mkdirSync(path.dirname(LED_FILE), { recursive: true });
|
|
393
|
+
fs.writeFileSync(LED_FILE, JSON.stringify(LED) + "\n");
|
|
394
|
+
} catch (_) {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function scanEveryMs() {
|
|
398
|
+
const n = Number(process.env.ORC_STATUSLINE_SCAN_MS);
|
|
399
|
+
return Number.isFinite(n) && n >= 0 ? n : 5000;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Per-provider TTL (v1.3.0 W2), replacing the single global throttle for
|
|
403
|
+
// everything but the trace scan. A wiki tier does not move in five seconds and
|
|
404
|
+
// a flow lock moves when somebody runs a command, so paying the 5-second rate
|
|
405
|
+
// for either is paying for a change that cannot have happened.
|
|
406
|
+
//
|
|
407
|
+
// `ORC_STATUSLINE_SCAN_MS` still overrides ALL of them — it stays the ONE seam
|
|
408
|
+
// over this budget, and a seam that only covered one provider would be a seam
|
|
409
|
+
// tests could not use.
|
|
410
|
+
const TTL = {
|
|
411
|
+
trace: 5000, // a run moves; this is the one thing that really is that fast
|
|
412
|
+
git: 5000, // .git/HEAD, no subprocess
|
|
413
|
+
wiki: 60000, // a wiki tier does not move in five seconds
|
|
414
|
+
config: 30000, // a config file is edited by hand
|
|
415
|
+
diy: 30000, // a flow lock moves when somebody runs a command
|
|
416
|
+
knowledge: 60000, // a pattern cache, a peer list, a gotcha count
|
|
417
|
+
extra: 15000, // a spend log DOES move during a wave
|
|
418
|
+
run: 10000, // RESUME.md is rewritten at every stop
|
|
419
|
+
gates: 60000, // a pact does not drift between keystrokes
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// `at` is a ledger `scanned_at` stamp. Absent is stale — unknown is not fresh.
|
|
423
|
+
function scanStale(at, now, ttl) {
|
|
424
|
+
const budget = process.env.ORC_STATUSLINE_SCAN_MS !== undefined ? scanEveryMs() : ttl == null ? scanEveryMs() : ttl;
|
|
425
|
+
return typeof at !== "number" || now - at >= budget;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// The read plan, from the compiled layout's lock. Returns null in EVERY state
|
|
429
|
+
// but one — the feature off, no lock, an unparseable lock — and null means
|
|
430
|
+
// "read everything", which is what makes `off` byte-identical.
|
|
431
|
+
//
|
|
432
|
+
// It deliberately does NOT re-run the gate ladder. This is a question about
|
|
433
|
+
// what to READ, and the ladder is a question about what to RENDER; a layout
|
|
434
|
+
// that later fails the ladder falls back to the shipped lines and simply has a
|
|
435
|
+
// segment or two missing for one render. That is a far better failure than
|
|
436
|
+
// reading the disk twice on a surface that re-renders on every keystroke.
|
|
437
|
+
function readPlan(d) {
|
|
438
|
+
try {
|
|
439
|
+
const fs = require("fs");
|
|
440
|
+
const path = require("path");
|
|
441
|
+
const projectDir =
|
|
442
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
443
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
444
|
+
if (!/^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw)) return null;
|
|
445
|
+
const lock = JSON.parse(
|
|
446
|
+
fs.readFileSync(path.join(projectDir, ".claude", "orc", "statusline.lock.json"), "utf8")
|
|
447
|
+
);
|
|
448
|
+
if (!lock || !Array.isArray(lock.bindings)) return null;
|
|
449
|
+
return {
|
|
450
|
+
bindings: new Set(lock.bindings),
|
|
451
|
+
providers: new Set(lock.providers || []),
|
|
452
|
+
series: new Set(lock.series || []),
|
|
453
|
+
};
|
|
454
|
+
} catch (_) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
342
459
|
let raw = "";
|
|
343
460
|
process.stdin.on("data", (c) => (raw += c));
|
|
344
461
|
process.stdin.on("end", () => {
|
|
@@ -357,6 +474,23 @@ process.stdin.on("end", () => {
|
|
|
357
474
|
? `context (${d.context_window.used_percentage}%)`
|
|
358
475
|
: "";
|
|
359
476
|
|
|
477
|
+
// ── THE READ PLAN (v1.3.0 W2) ──────────────────────────────────────────────
|
|
478
|
+
// The compiler knows exactly which bindings a layout uses, therefore exactly
|
|
479
|
+
// which providers it needs, and it records both in the lock. So: A PROVIDER
|
|
480
|
+
// NOTHING BINDS IS NOT READ.
|
|
481
|
+
//
|
|
482
|
+
// That is the claim this feature has to earn — the shipped status line pays
|
|
483
|
+
// for the wiki read, the diy read and the trace scan unconditionally, and a
|
|
484
|
+
// user whose layout names none of them should pay for none of them. Composing
|
|
485
|
+
// your own line is allowed to make the bar FASTER than the hardcoded one it
|
|
486
|
+
// replaces, and here is where that happens.
|
|
487
|
+
//
|
|
488
|
+
// `null` means the feature is off, and then everything below runs exactly as
|
|
489
|
+
// it always has. That is what keeps `off` byte-identical.
|
|
490
|
+
const PLAN = readPlan(d);
|
|
491
|
+
const wants = (b) => !PLAN || PLAN.bindings.has(b);
|
|
492
|
+
const wantsProvider = (p) => !PLAN || PLAN.providers.has(p);
|
|
493
|
+
|
|
360
494
|
// ── Session-model bridge (fail-silent) ─────────────────────────────────────
|
|
361
495
|
// The PreToolUse effort guard cannot see the model id; it can only read
|
|
362
496
|
// effort. Persist {model_id, effort, written_at} here so the guard can grant
|
|
@@ -431,9 +565,7 @@ process.stdin.on("end", () => {
|
|
|
431
565
|
// bank what was consumed before the reset into `accumulated` and
|
|
432
566
|
// re-baseline, so the running total keeps counting across the boundary.
|
|
433
567
|
const sid = String(d.session_id || d.sessionId || "");
|
|
434
|
-
const
|
|
435
|
-
let led = null;
|
|
436
|
-
try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
|
|
568
|
+
const led = ledger(projectDir, sid);
|
|
437
569
|
const pctOf = (o) => (o && typeof o.used_percentage === "number" ? o.used_percentage : null);
|
|
438
570
|
const track = (prev, cur) => {
|
|
439
571
|
if (cur == null) return prev || null;
|
|
@@ -447,13 +579,10 @@ process.stdin.on("end", () => {
|
|
|
447
579
|
};
|
|
448
580
|
return { baseline: prev.baseline, last: cur, accumulated: prev.accumulated, resets: prev.resets };
|
|
449
581
|
};
|
|
450
|
-
if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
|
|
451
582
|
led.five_hour = track(led.five_hour, pctOf(rl0 && rl0.five_hour));
|
|
452
583
|
led.seven_day = track(led.seven_day, pctOf(rl0 && rl0.seven_day));
|
|
453
584
|
led.context_used_percentage =
|
|
454
585
|
cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null;
|
|
455
|
-
led.updated_at = Date.now();
|
|
456
|
-
fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
|
|
457
586
|
}
|
|
458
587
|
|
|
459
588
|
} catch (_) {}
|
|
@@ -597,13 +726,9 @@ process.stdin.on("end", () => {
|
|
|
597
726
|
// And it is still a delta of an ACCOUNT-WIDE window, not a private meter: a
|
|
598
727
|
// second terminal moves it too.
|
|
599
728
|
try {
|
|
600
|
-
const fs = require("fs");
|
|
601
|
-
const path = require("path");
|
|
602
729
|
const projectDir =
|
|
603
730
|
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
604
|
-
const led =
|
|
605
|
-
fs.readFileSync(path.join(projectDir, ".claude", "orc", "usage-session.json"), "utf8")
|
|
606
|
-
);
|
|
731
|
+
const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
607
732
|
const w = led && led.five_hour;
|
|
608
733
|
if (w && typeof w.last === "number" && typeof w.baseline === "number") {
|
|
609
734
|
const used = Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline));
|
|
@@ -616,43 +741,85 @@ process.stdin.on("end", () => {
|
|
|
616
741
|
// wiki / no git / any error → no segment. Thresholds mirror the config
|
|
617
742
|
// defaults (wiki_fresh_max 10 / wiki_aging_max 30); the hook can't read the
|
|
618
743
|
// resolved config, so a user override shifts skill behavior, not this label.
|
|
744
|
+
//
|
|
745
|
+
// The git distance rides in the SAME throttled scan as everything on line 2
|
|
746
|
+
// (v1.3.0 W0). It used to be an `execSync` on every render — one child
|
|
747
|
+
// process PER KEYSTROKE in any repo with a wiki, which is the exact hazard
|
|
748
|
+
// the throttle exists to prevent. The ledger caches the RAW facts (a commit
|
|
749
|
+
// count, a boolean) and never the word: `fresh` / `AGING` / `STALE` is
|
|
750
|
+
// computed here, on read, every time.
|
|
619
751
|
try {
|
|
620
752
|
const fs = require("fs");
|
|
621
753
|
const path = require("path");
|
|
622
|
-
const { execSync } = require("child_process");
|
|
623
754
|
const projectDir =
|
|
624
755
|
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
)
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
756
|
+
// A layout that names no wiki component performs ZERO wiki reads — not a
|
|
757
|
+
// cheaper one, none. This is the read planner's whole point.
|
|
758
|
+
if (!wants("wiki.tier") && !wants("wiki.distance")) throw new Error("not needed");
|
|
759
|
+
const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
760
|
+
const now = Date.now();
|
|
761
|
+
// A wiki tier does not move in five seconds. Per-provider TTL replaces the
|
|
762
|
+
// single global throttle for everything but the trace scan, which is the
|
|
763
|
+
// one thing that really does move that fast.
|
|
764
|
+
if (scanStale(led.wiki && led.wiki.scanned_at, now, TTL.wiki)) {
|
|
765
|
+
const w = { unregistered: false, distance: null, scanned_at: now };
|
|
766
|
+
const metaPath = path.join(projectDir, ".claude", "orc", "wiki-meta.json");
|
|
767
|
+
if (!fs.existsSync(metaPath)) {
|
|
768
|
+
// Docs but no manifest = UNREGISTERED: a real wiki nothing has indexed
|
|
769
|
+
// (usually a scan stopped at a 5-area pause). It is otherwise invisible
|
|
770
|
+
// — consumers and `orc crosslink` read the manifest — so surface it
|
|
771
|
+
// here, with the free fix. Never say "no wiki": these docs are already
|
|
772
|
+
// paid for.
|
|
773
|
+
const wikiDir = path.join(projectDir, "wiki");
|
|
774
|
+
w.unregistered =
|
|
775
|
+
fs.existsSync(wikiDir) &&
|
|
776
|
+
fs.readdirSync(wikiDir).some((f) => f.startsWith("orc-") && f.endsWith(".md"));
|
|
777
|
+
} else {
|
|
778
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
779
|
+
if (meta && meta.scan_commit) {
|
|
780
|
+
// A FAILED probe is a fact, and it is cached like any other. No git,
|
|
781
|
+
// a detached commit, a timeout — without this inner catch the whole
|
|
782
|
+
// block aborts before the ledger is stamped, and the subprocess runs
|
|
783
|
+
// again on the very next keystroke. The segment stays absent either
|
|
784
|
+
// way; what changes is that it costs nothing to stay absent.
|
|
785
|
+
try {
|
|
786
|
+
const { execSync } = require("child_process");
|
|
787
|
+
const n = parseInt(
|
|
788
|
+
execSync(`git rev-list --count ${meta.scan_commit}..HEAD`, {
|
|
789
|
+
cwd: projectDir,
|
|
790
|
+
timeout: 3000,
|
|
791
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
792
|
+
})
|
|
793
|
+
.toString()
|
|
794
|
+
.trim(),
|
|
795
|
+
10
|
|
796
|
+
);
|
|
797
|
+
if (Number.isFinite(n)) w.distance = n;
|
|
798
|
+
} catch (_) {}
|
|
654
799
|
}
|
|
655
800
|
}
|
|
801
|
+
led.wiki = w;
|
|
802
|
+
}
|
|
803
|
+
const w = led.wiki;
|
|
804
|
+
if (w) {
|
|
805
|
+
SCAN.wiki = {
|
|
806
|
+
distance: typeof w.distance === "number" ? w.distance : null,
|
|
807
|
+
tier: w.unregistered
|
|
808
|
+
? "unregistered"
|
|
809
|
+
: typeof w.distance !== "number"
|
|
810
|
+
? null
|
|
811
|
+
: w.distance > 30
|
|
812
|
+
? "stale"
|
|
813
|
+
: w.distance >= 10
|
|
814
|
+
? "aging"
|
|
815
|
+
: "fresh",
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
if (w && w.unregistered) line += " · wiki: UNREGISTERED (run `orc wiki sync`)";
|
|
819
|
+
else if (w && typeof w.distance === "number") {
|
|
820
|
+
if (w.distance >= 10 && w.distance <= 30) line += ` · wiki: AGING (${w.distance}c)`;
|
|
821
|
+
else if (w.distance > 30) line += ` · wiki: STALE (${w.distance}c)`;
|
|
822
|
+
else line += " · wiki: fresh";
|
|
656
823
|
}
|
|
657
824
|
} catch (_) {}
|
|
658
825
|
|
|
@@ -665,6 +832,8 @@ process.stdin.on("end", () => {
|
|
|
665
832
|
const crypto = require("crypto");
|
|
666
833
|
const projectDir =
|
|
667
834
|
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
835
|
+
if (!wants("diy.state") && !wants("diy.name") && !wants("diy.tier_state"))
|
|
836
|
+
throw new Error("not needed");
|
|
668
837
|
const lockPath = path.join(projectDir, ".claude", "orc", "diy", "flow.lock.json");
|
|
669
838
|
if (fs.existsSync(lockPath)) {
|
|
670
839
|
const lock = JSON.parse(
|
|
@@ -715,6 +884,23 @@ process.stdin.on("end", () => {
|
|
|
715
884
|
} catch (_) {}
|
|
716
885
|
}
|
|
717
886
|
|
|
887
|
+
// The branch is its OWN provider: `.git/HEAD` with no subprocess, on a
|
|
888
|
+
// different clock from the trace scan, and a layout that shows a branch and
|
|
889
|
+
// nothing else must not pay for a trace scan to get it.
|
|
890
|
+
if (wantsProvider("scan.git")) {
|
|
891
|
+
try {
|
|
892
|
+
const projectDir =
|
|
893
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
894
|
+
SCAN.branch = gitBranch(projectDir) || null;
|
|
895
|
+
} catch (_) {}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// The extended scan (v1.3.0 W3). Only a composed layout can reach it, and
|
|
899
|
+
// only for the providers its own lock names.
|
|
900
|
+
extendedScan(d, wants, wantsProvider);
|
|
901
|
+
// The sparkline series, sampled during the scan that already ran.
|
|
902
|
+
sampleSeries(d, PLAN);
|
|
903
|
+
|
|
718
904
|
// ── Session line (v1.2.0) ──────────────────────────────────────────────────
|
|
719
905
|
// Line 1 answers "what tier am I on and how full is the window". This second
|
|
720
906
|
// line answers "what has this session actually been DOING" — how many agents
|
|
@@ -734,13 +920,7 @@ process.stdin.on("end", () => {
|
|
|
734
920
|
const path = require("path");
|
|
735
921
|
const projectDir =
|
|
736
922
|
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
737
|
-
const
|
|
738
|
-
const sfile = path.join(orcDir, "usage-session.json");
|
|
739
|
-
const sid = String(d.session_id || d.sessionId || "");
|
|
740
|
-
|
|
741
|
-
let led = null;
|
|
742
|
-
try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
|
|
743
|
-
if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
|
|
923
|
+
const led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
744
924
|
|
|
745
925
|
// The hook cannot read the RESOLVED config — that is the lane resolver's
|
|
746
926
|
// job, and a hook has no lane — so this reads the two raw keys it needs
|
|
@@ -757,18 +937,8 @@ process.stdin.on("end", () => {
|
|
|
757
937
|
} catch (_) {}
|
|
758
938
|
|
|
759
939
|
const now = Date.now();
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// past it is a test that fails on a loaded machine, and a flake is recorded
|
|
763
|
-
// and removed, never retried away. Unset, this is byte-identical to a
|
|
764
|
-
// hardcoded 5000, and nothing in ORC ever sets it.
|
|
765
|
-
const scanEvery = (() => {
|
|
766
|
-
const n = Number(process.env.ORC_STATUSLINE_SCAN_MS);
|
|
767
|
-
return Number.isFinite(n) && n >= 0 ? n : 5000;
|
|
768
|
-
})();
|
|
769
|
-
const stale = !led.dispatch || typeof led.dispatch.scanned_at !== "number" ||
|
|
770
|
-
now - led.dispatch.scanned_at >= scanEvery;
|
|
771
|
-
if (stale) {
|
|
940
|
+
if (!wantsProvider("scan.trace")) throw new Error("not needed");
|
|
941
|
+
if (scanStale(led.dispatch && led.dispatch.scanned_at, now, TTL.trace)) {
|
|
772
942
|
const logDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, logRel);
|
|
773
943
|
const sessionFloor = Math.floor((led.started_at || 0) / 1000) * 1000;
|
|
774
944
|
let spawns = 0;
|
|
@@ -847,13 +1017,14 @@ process.stdin.on("end", () => {
|
|
|
847
1017
|
led.tok = scanTokens(led, d.transcript_path || null);
|
|
848
1018
|
}
|
|
849
1019
|
|
|
850
|
-
led.updated_at = now;
|
|
851
|
-
try {
|
|
852
|
-
fs.mkdirSync(orcDir, { recursive: true });
|
|
853
|
-
fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
|
|
854
|
-
} catch (_) {}
|
|
855
|
-
|
|
856
1020
|
const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [], phase: null };
|
|
1021
|
+
// The composed layout reads THESE — the same numbers the shipped line
|
|
1022
|
+
// below prints, from the same scan.
|
|
1023
|
+
SCAN.spawns = dsp.spawns || 0;
|
|
1024
|
+
SCAN.running = dsp.running || 0;
|
|
1025
|
+
SCAN.lanes = dsp.lanes || [];
|
|
1026
|
+
SCAN.phase = dsp.phase || null;
|
|
1027
|
+
SCAN.extra_enabled = !!extraOn;
|
|
857
1028
|
const parts = [];
|
|
858
1029
|
// `status:` leads, because what ORC is doing right now is the one thing on
|
|
859
1030
|
// this line that changes minute to minute. It is also the ONE segment
|
|
@@ -876,12 +1047,488 @@ process.stdin.on("end", () => {
|
|
|
876
1047
|
// MTok keeps its slot in every state. An em dash says "not measured"; a `0`
|
|
877
1048
|
// would say the session was free, and that is a different claim.
|
|
878
1049
|
parts.push("MTok " + (fmtTokens(led.tok) || "—"));
|
|
879
|
-
|
|
880
|
-
if (branch) parts.push(branch);
|
|
1050
|
+
if (SCAN.branch) parts.push(SCAN.branch);
|
|
881
1051
|
line2 = " " + parts.join(" · ");
|
|
882
1052
|
} catch (_) {
|
|
883
1053
|
line2 = "";
|
|
884
1054
|
}
|
|
885
1055
|
|
|
1056
|
+
// ONE write, after every block that touches the ledger has had its say. It is
|
|
1057
|
+
// last on purpose: a render that throws half way through still prints, and a
|
|
1058
|
+
// ledger that could not be written never takes the status line down with it.
|
|
1059
|
+
ledgerFlush();
|
|
1060
|
+
|
|
1061
|
+
// ── THE CUSTOM LAYOUT (v1.3.0) ──────────────────────────────────────────
|
|
1062
|
+
// Everything above is the SHIPPED status line, and it is what renders unless
|
|
1063
|
+
// the user composed their own. `custom()` returns null in every state but
|
|
1064
|
+
// one, and each of its gates is a FALLBACK rather than a throw: a hook cannot
|
|
1065
|
+
// refuse, so a bad layout must degrade to something correct rather than paint
|
|
1066
|
+
// garbage. The ladder is documented on the function itself.
|
|
1067
|
+
const composed = custom(d, {
|
|
1068
|
+
payload: d,
|
|
1069
|
+
ledger: LED || {},
|
|
1070
|
+
scan: SCAN,
|
|
1071
|
+
derived: { verdict, reasons, version: ver },
|
|
1072
|
+
now: Date.now(),
|
|
1073
|
+
});
|
|
1074
|
+
if (composed != null) {
|
|
1075
|
+
process.stdout.write(composed);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
886
1079
|
process.stdout.write(line2 ? line + "\n" + line2 : line);
|
|
887
1080
|
});
|
|
1081
|
+
|
|
1082
|
+
// ── The six-rung gate ladder ────────────────────────────────────────────────
|
|
1083
|
+
// In order, and EVERY RUNG IS A FALLBACK, never a throw:
|
|
1084
|
+
//
|
|
1085
|
+
// 1 statusline_custom is not `on` the shipped lines. BYTE-IDENTICAL.
|
|
1086
|
+
// 2 statusline-compiled.json missing / default + statusline-layout-unreadable
|
|
1087
|
+
// unparseable / schema mismatch
|
|
1088
|
+
// 3 orc_version or catalog_hash moved default + statusline-layout-stale
|
|
1089
|
+
// 4 an unknown op or an unknown binding default + statusline-layout-skew
|
|
1090
|
+
// 5 the cheap shape guard fails default + statusline-layout-invalid
|
|
1091
|
+
// 6 otherwise run the program
|
|
1092
|
+
//
|
|
1093
|
+
// Rungs 3 and 4 are why the lock file exists. Rung 5 is this hook RE-CHECKING
|
|
1094
|
+
// rather than trusting the file, because a hand-edited compiled file is a file
|
|
1095
|
+
// nobody validated. Every fallback RECORDS ITSELF in the ledger so `orc doctor`
|
|
1096
|
+
// can name it: a status line that quietly went back to the default and never
|
|
1097
|
+
// said why is a bug the user cannot report.
|
|
1098
|
+
function custom(d, ctx) {
|
|
1099
|
+
try {
|
|
1100
|
+
const fs = require("fs");
|
|
1101
|
+
const path = require("path");
|
|
1102
|
+
const projectDir =
|
|
1103
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
1104
|
+
const orcDir = path.join(projectDir, ".claude", "orc");
|
|
1105
|
+
|
|
1106
|
+
// Rung 1. The hook cannot resolve config — that is the lane resolver's job,
|
|
1107
|
+
// and a hook has no lane — so it reads the raw key off the file and takes
|
|
1108
|
+
// the documented default otherwise, exactly as it already does for
|
|
1109
|
+
// `log_dir` and `extra_enabled`.
|
|
1110
|
+
let on = false;
|
|
1111
|
+
try {
|
|
1112
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
1113
|
+
// The trailing \r is tolerated on purpose: a config file written on
|
|
1114
|
+
// Windows carries CRLF, and a $-anchored match silently never fires
|
|
1115
|
+
// there — which is a feature that is ON in the file and OFF on the bar.
|
|
1116
|
+
on = /^[ \t]*statusline_custom:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw);
|
|
1117
|
+
} catch (_) {}
|
|
1118
|
+
if (!on) return null;
|
|
1119
|
+
|
|
1120
|
+
// Rung 2. THE HOOK NEVER READS THE AUTHORED LAYOUT — not as a fallback, not
|
|
1121
|
+
// on a cache miss, not ever. One consumer per file.
|
|
1122
|
+
let prog = null;
|
|
1123
|
+
try {
|
|
1124
|
+
prog = JSON.parse(fs.readFileSync(path.join(orcDir, "statusline-compiled.json"), "utf8"));
|
|
1125
|
+
} catch (_) {}
|
|
1126
|
+
if (!prog || prog.schema !== 1) return slFallback(orcDir, "statusline-layout-unreadable");
|
|
1127
|
+
|
|
1128
|
+
// Rung 3. An ORC upgrade that adds, removes or changes a component
|
|
1129
|
+
// invalidates every compiled layout on the machine. Better the shipped
|
|
1130
|
+
// lines than a program compiled against a catalogue that no longer exists.
|
|
1131
|
+
let lock = null;
|
|
1132
|
+
try {
|
|
1133
|
+
lock = JSON.parse(fs.readFileSync(path.join(orcDir, "statusline.lock.json"), "utf8"));
|
|
1134
|
+
} catch (_) {}
|
|
1135
|
+
let installed = null;
|
|
1136
|
+
try {
|
|
1137
|
+
installed = JSON.parse(fs.readFileSync(path.join(__dirname, "orc-version.json"), "utf8")).version;
|
|
1138
|
+
} catch (_) {}
|
|
1139
|
+
if (!lock || (installed && lock.orc_version !== installed))
|
|
1140
|
+
return slFallback(orcDir, "statusline-layout-stale");
|
|
1141
|
+
|
|
1142
|
+
const engine = require("./orc-statusline-render.js");
|
|
1143
|
+
|
|
1144
|
+
// Rung 4. A binding this build does not have is an INSTALL SKEW, not a
|
|
1145
|
+
// rendering problem, and it must not be papered over one item at a time.
|
|
1146
|
+
for (const b of lock.bindings || []) {
|
|
1147
|
+
if (!engine.BINDINGS[b]) return slFallback(orcDir, "statusline-layout-skew");
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Rung 5. The cheap shape guard: at most three lines, at most five
|
|
1151
|
+
// components on any of them, and the dense-prefix rule —
|
|
1152
|
+
// A line may hold a component only if every line above it holds at least one.
|
|
1153
|
+
if (!Array.isArray(prog.lines) || prog.lines.length > 3)
|
|
1154
|
+
return slFallback(orcDir, "statusline-layout-invalid");
|
|
1155
|
+
let seenEmpty = false;
|
|
1156
|
+
for (const l of prog.lines) {
|
|
1157
|
+
const n = (l.ops || []).filter((o) => o.op === "item").length;
|
|
1158
|
+
if (n > 5) return slFallback(orcDir, "statusline-layout-invalid");
|
|
1159
|
+
if (n === 0) seenEmpty = true;
|
|
1160
|
+
else if (seenEmpty) return slFallback(orcDir, "statusline-layout-invalid");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Rung 6. Run the program. Failure isolation lives inside the engine: one
|
|
1164
|
+
// throwing item emits its unknown form and the rest of the line survives.
|
|
1165
|
+
SCAN.preset = prog.preset || null;
|
|
1166
|
+
const out = engine.render(prog, {
|
|
1167
|
+
payload: ctx.payload,
|
|
1168
|
+
ledger: ctx.ledger,
|
|
1169
|
+
scan: ctx.scan,
|
|
1170
|
+
derived: ctx.derived,
|
|
1171
|
+
now: ctx.now,
|
|
1172
|
+
cols: Number(process.env.COLUMNS) || 0,
|
|
1173
|
+
env: process.env,
|
|
1174
|
+
});
|
|
1175
|
+
if (out.errors && out.errors.length) slNote(orcDir, "statusline-item-failed", out.errors[0]);
|
|
1176
|
+
return out.text;
|
|
1177
|
+
} catch (_) {
|
|
1178
|
+
// Even the ladder must not throw. A status line that crashes is a status
|
|
1179
|
+
// line that is simply absent, with no way to find out why.
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// A fallback RECORDS ITSELF. `orc doctor` reads this file and names the finding
|
|
1185
|
+
// with the exact command that clears it.
|
|
1186
|
+
function slFallback(orcDir, finding) {
|
|
1187
|
+
slNote(orcDir, finding, null);
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
function slNote(orcDir, finding, detail) {
|
|
1191
|
+
try {
|
|
1192
|
+
const fs = require("fs");
|
|
1193
|
+
const path = require("path");
|
|
1194
|
+
fs.mkdirSync(orcDir, { recursive: true });
|
|
1195
|
+
fs.writeFileSync(
|
|
1196
|
+
path.join(orcDir, "statusline-state.json"),
|
|
1197
|
+
JSON.stringify({ finding, detail: detail || null, at: Date.now() }) + "\n"
|
|
1198
|
+
);
|
|
1199
|
+
} catch (_) {}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// ── THE EXTENDED SCAN (v1.3.0 W3) ──────────────────────────────────────────
|
|
1203
|
+
// Groups D, E, F and G — knowledge, extra, flow and the health gates. Every one
|
|
1204
|
+
// of them is a `new read`: a read that does not exist in the shipped status
|
|
1205
|
+
// line and only happens when a layout asks for it.
|
|
1206
|
+
//
|
|
1207
|
+
// THE BAR IS 15 MILLISECONDS, NOT 300. W0 measured node startup at ~285 ms of a
|
|
1208
|
+
// 300 ms budget, so a component here has to be answerable from a small JSON
|
|
1209
|
+
// file, on its own clock, or it does not ship. Two are refused for that reason
|
|
1210
|
+
// and say so in the catalogue.
|
|
1211
|
+
//
|
|
1212
|
+
// Every read below is:
|
|
1213
|
+
// - gated on a BINDING the compiled lock names (the read planner);
|
|
1214
|
+
// - cached in the per-session ledger under its own key;
|
|
1215
|
+
// - given its OWN TTL, because a pact ledger and a spend log do not move at
|
|
1216
|
+
// the same rate.
|
|
1217
|
+
//
|
|
1218
|
+
// A read that throws leaves its slot null, and a null renders an em dash.
|
|
1219
|
+
// UNKNOWN IS NOT ZERO — a `0` would say the thing was measured and found empty.
|
|
1220
|
+
function extendedScan(d, wants, wantsProvider) {
|
|
1221
|
+
if (!wantsProvider("scan.extended")) return;
|
|
1222
|
+
let fs, path, projectDir, led, now;
|
|
1223
|
+
try {
|
|
1224
|
+
fs = require("fs");
|
|
1225
|
+
path = require("path");
|
|
1226
|
+
projectDir = (d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
1227
|
+
led = ledger(projectDir, String(d.session_id || d.sessionId || ""));
|
|
1228
|
+
now = Date.now();
|
|
1229
|
+
} catch (_) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const orc = path.join(projectDir, ".claude", "orc");
|
|
1233
|
+
const readJson = (p) => {
|
|
1234
|
+
try {
|
|
1235
|
+
return JSON.parse(fs.readFileSync(p, "utf8").replace(/^/, ""));
|
|
1236
|
+
} catch (_) {
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
// One cached sub-scan. `key` is its ledger slot, `ttl` its own clock.
|
|
1241
|
+
const cached = (key, ttl, fn) => {
|
|
1242
|
+
const slot = led.ext && led.ext[key];
|
|
1243
|
+
if (slot && !scanStale(slot.at, now, ttl)) return slot.v;
|
|
1244
|
+
let v = null;
|
|
1245
|
+
try {
|
|
1246
|
+
v = fn();
|
|
1247
|
+
} catch (_) {
|
|
1248
|
+
v = null;
|
|
1249
|
+
}
|
|
1250
|
+
led.ext = led.ext || {};
|
|
1251
|
+
led.ext[key] = { v, at: now };
|
|
1252
|
+
return v;
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
// ── Group D — knowledge. All of it comes out of wiki-meta.json, which is
|
|
1256
|
+
// written ONLY by `orc wiki sync` and is 100% doc-header-derived. A wiki
|
|
1257
|
+
// tier does not move in a minute, so the TTL is generous.
|
|
1258
|
+
if (wants("wiki.docs")) {
|
|
1259
|
+
const w = cached("wiki_meta", TTL.wiki, () => {
|
|
1260
|
+
const meta = readJson(path.join(orc, "wiki-meta.json"));
|
|
1261
|
+
if (!meta) return null;
|
|
1262
|
+
const docs = Array.isArray(meta.docs) ? meta.docs : [];
|
|
1263
|
+
return {
|
|
1264
|
+
docs: docs.length,
|
|
1265
|
+
};
|
|
1266
|
+
});
|
|
1267
|
+
SCAN.wiki_meta = w;
|
|
1268
|
+
}
|
|
1269
|
+
if (wants("pattern.state")) {
|
|
1270
|
+
// EXISTENCE only, and by the deterministic probe's own rule: the cache
|
|
1271
|
+
// lives under the hidden .claude/ dir, so a raw filesystem search
|
|
1272
|
+
// false-negatives from the wrong cwd.
|
|
1273
|
+
SCAN.pattern = cached("pattern", TTL.knowledge, () => {
|
|
1274
|
+
const dir = path.join(orc, "patterns");
|
|
1275
|
+
try {
|
|
1276
|
+
return fs.readdirSync(dir).some((f) => f.endsWith("-pattern.md")) ? "cached" : "none";
|
|
1277
|
+
} catch (_) {
|
|
1278
|
+
return "none";
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
if (wants("crosslink.state") || wants("crosslink.peers")) {
|
|
1283
|
+
const c = cached("crosslink", TTL.knowledge, () => {
|
|
1284
|
+
const raw = (() => {
|
|
1285
|
+
try {
|
|
1286
|
+
return fs.readFileSync(path.join(projectDir, ".claude", "orc-crosslink.config.yaml"), "utf8");
|
|
1287
|
+
} catch (_) {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
})();
|
|
1291
|
+
if (!raw) return null;
|
|
1292
|
+
const n = (raw.match(/^[ \t]*-[ \t]*name:/gm) || []).length;
|
|
1293
|
+
return { peers: n, state: n ? "linked" : "none" };
|
|
1294
|
+
});
|
|
1295
|
+
SCAN.crosslink = c;
|
|
1296
|
+
}
|
|
1297
|
+
if (wants("gotchas.count")) {
|
|
1298
|
+
SCAN.gotchas = cached("gotchas", TTL.knowledge, () => {
|
|
1299
|
+
try {
|
|
1300
|
+
return fs.readdirSync(path.join(orc, "gotchas")).filter((f) => f.endsWith(".md")).length;
|
|
1301
|
+
} catch (_) {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// ── Group E — extra. `extra.json` is small; the SPEND LOG is not, so it is
|
|
1308
|
+
// read by TAIL and on its own faster clock, because a spend log is the one
|
|
1309
|
+
// thing here that moves during a wave.
|
|
1310
|
+
if (wants("extra.profile") || wants("extra.provider") || wants("extra.inflight") || wants("extra.passphrase") || wants("extra.demoted")) {
|
|
1311
|
+
SCAN.extra = cached("extra", TTL.extra, () => {
|
|
1312
|
+
const j = readJson(path.join(orc, "extra.json"));
|
|
1313
|
+
if (!j) return null;
|
|
1314
|
+
const profiles = j.profiles || {};
|
|
1315
|
+
const names = Object.keys(profiles);
|
|
1316
|
+
const first = names.length ? profiles[names[0]] : null;
|
|
1317
|
+
return {
|
|
1318
|
+
profile: names.length ? names[0] : null,
|
|
1319
|
+
provider: first && first.provider ? first.provider : null,
|
|
1320
|
+
profiles: names.length,
|
|
1321
|
+
};
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
if (wants("extra.spend") || wants("extra.tasks")) {
|
|
1325
|
+
SCAN.extra_spend = cached("extra_spend", TTL.extra, () => {
|
|
1326
|
+
// The tail only. A spend log grows for the life of a project and reading
|
|
1327
|
+
// all of it on a per-keystroke surface is the hazard this whole subsystem
|
|
1328
|
+
// is shaped around.
|
|
1329
|
+
const p = path.join(orc, "extra-spend.jsonl");
|
|
1330
|
+
let st;
|
|
1331
|
+
try {
|
|
1332
|
+
st = fs.statSync(p);
|
|
1333
|
+
} catch (_) {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
const want = Math.min(st.size, 64 * 1024);
|
|
1337
|
+
const buf = Buffer.alloc(want);
|
|
1338
|
+
const fd = fs.openSync(p, "r");
|
|
1339
|
+
try {
|
|
1340
|
+
fs.readSync(fd, buf, 0, want, st.size - want);
|
|
1341
|
+
} finally {
|
|
1342
|
+
fs.closeSync(fd);
|
|
1343
|
+
}
|
|
1344
|
+
const lines = buf.toString("utf8").split("\n").slice(1).filter(Boolean);
|
|
1345
|
+
let tasks = 0;
|
|
1346
|
+
let usd = 0;
|
|
1347
|
+
let priced = false;
|
|
1348
|
+
for (const l of lines) {
|
|
1349
|
+
try {
|
|
1350
|
+
const r = JSON.parse(l);
|
|
1351
|
+
tasks++;
|
|
1352
|
+
if (typeof r.usd === "number") {
|
|
1353
|
+
usd += r.usd;
|
|
1354
|
+
priced = true;
|
|
1355
|
+
}
|
|
1356
|
+
} catch (_) {}
|
|
1357
|
+
}
|
|
1358
|
+
// A cost figure ORC did not price itself is never printed: `usd` stays
|
|
1359
|
+
// null rather than becoming a confident 0.
|
|
1360
|
+
return { tasks, usd: priced ? usd : null, partial: st.size > want };
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// ── Group F — flow and lanes. The diy lock is already read for the shipped
|
|
1365
|
+
// segment; `wait` is a small run-state file.
|
|
1366
|
+
if (wants("wait.state")) {
|
|
1367
|
+
SCAN.wait = cached("wait", TTL.run, () => {
|
|
1368
|
+
const j = readJson(path.join(orc, "wait.json"));
|
|
1369
|
+
if (!j) return "none";
|
|
1370
|
+
if (j.block_reason) return "blocked";
|
|
1371
|
+
return j.until || j.hops ? "waiting" : "none";
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
// `preset.name` is NOT read here: it rides in the compiled program, which is
|
|
1375
|
+
// the only file that crosses the wall. The hook never opens the authored
|
|
1376
|
+
// layout, and a field being convenient is not an exception to that.
|
|
1377
|
+
|
|
1378
|
+
// ── Group C remainder — the run's own progress, from RESUME.md, which is the
|
|
1379
|
+
// ONE line `orc resume` and `orc run list` parse. Reading the same line is
|
|
1380
|
+
// how a listing never has to open a checkpoint.
|
|
1381
|
+
if (wants("run.wave") || wants("run.wave_total") || wants("run.resume") || wants("run.open")) {
|
|
1382
|
+
SCAN.runs = cached("runs", TTL.run, () => {
|
|
1383
|
+
let logRel = ".claude/orc/logs";
|
|
1384
|
+
try {
|
|
1385
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
1386
|
+
const m = /^[ \t]*run_dir:[ \t]*["']?([^"'#\r\n]+)/m.exec(raw);
|
|
1387
|
+
if (m) logRel = m[1].trim();
|
|
1388
|
+
} catch (_) {}
|
|
1389
|
+
const runDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, ".claude", "orc", "run");
|
|
1390
|
+
let open = 0;
|
|
1391
|
+
let wave = null;
|
|
1392
|
+
let waves = null;
|
|
1393
|
+
try {
|
|
1394
|
+
for (const slug of fs.readdirSync(runDir)) {
|
|
1395
|
+
const rp = path.join(runDir, slug, "RESUME.md");
|
|
1396
|
+
if (!fs.existsSync(rp)) continue;
|
|
1397
|
+
if (fs.existsSync(path.join(runDir, slug, "closed.json"))) continue;
|
|
1398
|
+
open++;
|
|
1399
|
+
// The byte-stable `Where it stands:` line, at column 0. It is the one
|
|
1400
|
+
// line two other commands already parse, which is exactly why this
|
|
1401
|
+
// does not open a checkpoint.
|
|
1402
|
+
const m = /^Where it stands:.*wave (\d+) of (\d+)/m.exec(fs.readFileSync(rp, "utf8"));
|
|
1403
|
+
if (m && wave == null) {
|
|
1404
|
+
wave = Number(m[1]);
|
|
1405
|
+
waves = Number(m[2]);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
} catch (_) {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
return { open, wave, waves, resume: open > 0 ? "waiting" : "none" };
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// ── Group G — the health gates. Each is one small ledger, each on the slow
|
|
1416
|
+
// clock: a pact does not drift between keystrokes.
|
|
1417
|
+
const gate = (key, file, fn) => {
|
|
1418
|
+
if (!wants(key)) return null;
|
|
1419
|
+
return cached(key.replace(/\./g, "_"), TTL.gates, () => fn(readJson(path.join(orc, file))));
|
|
1420
|
+
};
|
|
1421
|
+
SCAN.pact = gate("pact.state", "pact.json", (j) => {
|
|
1422
|
+
if (!j || !Array.isArray(j.entries)) return null;
|
|
1423
|
+
const live = j.entries.filter((e) => e.state !== "retired");
|
|
1424
|
+
const broken = live.filter((e) => e.state === "broken").length;
|
|
1425
|
+
const drifted = live.filter((e) => e.state === "drifted").length;
|
|
1426
|
+
// UNCHECKABLE is the honest state and it never reads as a failure.
|
|
1427
|
+
return { state: broken ? "broken" : drifted ? "drifted" : "holding", drifted, broken, total: live.length };
|
|
1428
|
+
});
|
|
1429
|
+
SCAN.boundary = gate("boundary.state", "boundary.json", (j) => {
|
|
1430
|
+
if (!j || !Array.isArray(j.cards)) return null;
|
|
1431
|
+
const refused = j.cards.filter((c) => c.verdict === "REFUSE").length;
|
|
1432
|
+
// An area with no card is UNKNOWN, never assumed safe.
|
|
1433
|
+
return { state: refused ? "refused" : "clear", refused, cards: j.cards.length };
|
|
1434
|
+
});
|
|
1435
|
+
SCAN.challenge = gate("challenge.state", "challenge.json", (j) => {
|
|
1436
|
+
if (!j) return null;
|
|
1437
|
+
const open = Array.isArray(j.findings) ? j.findings.filter((f) => !f.resolved).length : 0;
|
|
1438
|
+
return { state: j.state || (open ? "open" : "pass"), open, iteration: j.iteration || null };
|
|
1439
|
+
});
|
|
1440
|
+
SCAN.doc = gate("doc.state", "doc.json", (j) => {
|
|
1441
|
+
if (!j) return null;
|
|
1442
|
+
const outline = Array.isArray(j.outline) ? j.outline : [];
|
|
1443
|
+
const done = outline.filter((s) => s.hash).length;
|
|
1444
|
+
return { state: j.shipped ? "shipped" : "draft", done, total: outline.length };
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
// `usage-gate` reads the usage bridge this hook already writes — so it is the
|
|
1448
|
+
// cheapest `new read` in the set, and it is the only one that can say
|
|
1449
|
+
// `unknown`, which is a real answer and never a stop.
|
|
1450
|
+
if (wants("usage.state")) {
|
|
1451
|
+
SCAN.usage = cached("usage", TTL.trace, () => {
|
|
1452
|
+
const j = readJson(path.join(orc, "usage.json"));
|
|
1453
|
+
if (!j || !j.written_at) return "unknown";
|
|
1454
|
+
if (now - j.written_at > 30 * 60 * 1000) return "unknown";
|
|
1455
|
+
const w = [j.five_hour, j.seven_day].filter(Boolean).map((x) => x.used_percentage);
|
|
1456
|
+
if (!w.length) return "unknown";
|
|
1457
|
+
return Math.max(...w) >= 90 ? "low" : "ok";
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// A config key as a component. The config file is edited by hand, so 30s.
|
|
1462
|
+
if (wants("config.value")) {
|
|
1463
|
+
SCAN.config_raw = cached("config", TTL.config, () => {
|
|
1464
|
+
try {
|
|
1465
|
+
return fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
1466
|
+
} catch (_) {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// ── THE SERIES LEDGER (v1.3.0 W3) ──────────────────────────────────────────
|
|
1474
|
+
// `spark`, `spark-braille`, `trend` and `delta` need history, and a status line
|
|
1475
|
+
// has none: it is a fresh process every render. So the LAST 16 SAMPLES per
|
|
1476
|
+
// series live in the per-session ledger, appended during the throttled scan
|
|
1477
|
+
// that already ran.
|
|
1478
|
+
//
|
|
1479
|
+
// NO NEW READ, NO NEW TIMER. Every value sampled here was already computed for
|
|
1480
|
+
// something else this render; the series is a side effect of the scan, never a
|
|
1481
|
+
// reason for one.
|
|
1482
|
+
//
|
|
1483
|
+
// SIXTEEN, and only for the series a compiled layout actually names — the read
|
|
1484
|
+
// planner again. A series nothing binds is not kept, so the ledger does not
|
|
1485
|
+
// grow for a user whose layout has no sparkline on it.
|
|
1486
|
+
const SERIES_MAX = 16;
|
|
1487
|
+
const SERIES_MIN_GAP_MS = 20000;
|
|
1488
|
+
|
|
1489
|
+
function sampleSeries(d, plan) {
|
|
1490
|
+
try {
|
|
1491
|
+
if (!plan || !plan.series || !plan.series.size) return;
|
|
1492
|
+
const led = LED;
|
|
1493
|
+
if (!led) return;
|
|
1494
|
+
const now = Date.now();
|
|
1495
|
+
// A sample every 20 seconds, not every render. Sixteen samples at the
|
|
1496
|
+
// render rate would be five seconds of history, which is not history — it
|
|
1497
|
+
// is the same number sixteen times.
|
|
1498
|
+
if (led.series_at && now - led.series_at < SERIES_MIN_GAP_MS) return;
|
|
1499
|
+
led.series_at = now;
|
|
1500
|
+
led.series = led.series || {};
|
|
1501
|
+
const push = (key, v) => {
|
|
1502
|
+
if (!plan.series.has(key)) return;
|
|
1503
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return;
|
|
1504
|
+
const arr = led.series[key] || [];
|
|
1505
|
+
arr.push(Math.round(v));
|
|
1506
|
+
while (arr.length > SERIES_MAX) arr.shift();
|
|
1507
|
+
led.series[key] = arr;
|
|
1508
|
+
};
|
|
1509
|
+
const rl = d.rate_limits || {};
|
|
1510
|
+
const w = led.five_hour;
|
|
1511
|
+
push("quota5h", rl.five_hour && rl.five_hour.used_percentage);
|
|
1512
|
+
push("quotawk", rl.seven_day && rl.seven_day.used_percentage);
|
|
1513
|
+
push("ucs", w && typeof w.last === "number" && typeof w.baseline === "number"
|
|
1514
|
+
? Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline))
|
|
1515
|
+
: null);
|
|
1516
|
+
const tok = led.tok;
|
|
1517
|
+
if (tok) {
|
|
1518
|
+
push("mtok", (tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0));
|
|
1519
|
+
push("mtokkind", tok.cache_read || 0);
|
|
1520
|
+
}
|
|
1521
|
+
push("agents", SCAN.spawns);
|
|
1522
|
+
push("cost", d.cost && d.cost.total_cost_usd != null ? d.cost.total_cost_usd * 100 : null);
|
|
1523
|
+
push("cachehit", d.prompt_cache && d.prompt_cache.hit_ratio != null
|
|
1524
|
+
? (d.prompt_cache.hit_ratio <= 1 ? d.prompt_cache.hit_ratio * 100 : d.prompt_cache.hit_ratio)
|
|
1525
|
+
: null);
|
|
1526
|
+
push("cachewrite", d.prompt_cache && d.prompt_cache.cache_write_tokens);
|
|
1527
|
+
push("lines", d.cost && d.cost.total_lines_added != null
|
|
1528
|
+
? (d.cost.total_lines_added || 0) - (d.cost.total_lines_removed || 0)
|
|
1529
|
+
: null);
|
|
1530
|
+
push("extraspend", SCAN.extra_spend && SCAN.extra_spend.usd != null ? SCAN.extra_spend.usd * 100 : null);
|
|
1531
|
+
} catch (_) {
|
|
1532
|
+
// A series is a nicety. It never takes the status line down with it.
|
|
1533
|
+
}
|
|
1534
|
+
}
|