@azure-id/orc 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2663 -2437
- package/README.md +665 -694
- package/bin/cli.js +658 -2
- package/bin/verify-contracts.js +63 -1
- package/bin/verify-package.js +573 -568
- package/package.json +1 -1
- package/templates/hooks/README.md +202 -0
- package/templates/hooks/orc-statusline.js +552 -9
- package/templates/skills/_shared/phases/execution.md +2 -0
- package/templates/skills/_shared/return-validation.md +222 -145
- package/templates/skills/orc/SKILL.md +241 -237
- package/templates/skills/orc-doc/SKILL.md +484 -480
- package/templates/skills/orc-fast/SKILL.md +216 -214
- package/templates/skills/orc-mini/SKILL.md +246 -243
- package/templates/skills/orc-quick/SKILL.md +347 -345
- package/templates/skills/orc-wiki/SKILL.md +174 -170
package/bin/cli.js
CHANGED
|
@@ -220,6 +220,16 @@ function installGuards(claudeDir) {
|
|
|
220
220
|
JSON.stringify({ version: currentVersion() }) + "\n"
|
|
221
221
|
);
|
|
222
222
|
} catch (_) {}
|
|
223
|
+
// The statusline's phase rail (v1.2.1). Regenerated every install from the
|
|
224
|
+
// CLI's own registries, so the hook renders a phase list it never derives.
|
|
225
|
+
// Same fail-silent contract: a missing manifest hides the `status:` segment,
|
|
226
|
+
// it never breaks a render.
|
|
227
|
+
try {
|
|
228
|
+
fs.writeFileSync(
|
|
229
|
+
path.join(hooksDest, "orc-lane-rails.json"),
|
|
230
|
+
JSON.stringify(laneRailsManifest()) + "\n"
|
|
231
|
+
);
|
|
232
|
+
} catch (_) {}
|
|
223
233
|
|
|
224
234
|
const guardCmd = nodeCmd(path.join(hooksDest, "orc-effort-guard.js"));
|
|
225
235
|
const statusCmd = nodeCmd(path.join(hooksDest, "orc-statusline.js"));
|
|
@@ -346,6 +356,7 @@ function shippedFootprint() {
|
|
|
346
356
|
for (const rel of listSrcRel(SRC_AGENTS)) files.push("agents/" + rel);
|
|
347
357
|
for (const rel of listSrcRel(SRC_HOOKS)) files.push("hooks/" + rel);
|
|
348
358
|
files.push("hooks/orc-version.json"); // generated by installGuards, ORC-owned
|
|
359
|
+
files.push("hooks/orc-lane-rails.json"); // generated by installGuards, ORC-owned
|
|
349
360
|
return files;
|
|
350
361
|
}
|
|
351
362
|
|
|
@@ -3091,6 +3102,18 @@ const LANE_CALLS = {
|
|
|
3091
3102
|
never: "never merge `.claude/orc.config.yaml` yourself, and never re-derive a precedence — the answer already carries it",
|
|
3092
3103
|
lanes: ["context-combiner", "orc", "orc-aftermath", "orc-analyze", "orc-analyze-mini", "orc-boundary", "orc-brainstorm", "orc-budget", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-explain", "orc-export", "orc-fast", "orc-grill", "orc-handoff", "orc-learn", "orc-mini", "orc-pact", "orc-pattern", "orc-poly", "orc-pr-driver", "orc-pr-setup", "orc-quick", "orc-retro", "orc-route", "orc-verify", "orc-wiki"],
|
|
3093
3104
|
},
|
|
3105
|
+
"run-inflight": {
|
|
3106
|
+
cmd: "orc run inflight [--json]",
|
|
3107
|
+
what: "is a dispatch from this run still alive — the ONE reader of the trace's pending sidecar",
|
|
3108
|
+
exits: { 0: "clear — provably nothing in flight", 1: "in-flight — at least one dispatch has not returned", 2: "unknown — cannot prove either way" },
|
|
3109
|
+
states: ["clear", "in-flight", "unknown"],
|
|
3110
|
+
cost: "free",
|
|
3111
|
+
when: "before ANY re-dispatch, requeue or repair round, and before the first dispatch of a resumed run",
|
|
3112
|
+
on_absent: "exit 2 REFUSES by default — the one place an absent reading blocks, because a wrongly-refused dispatch costs a question and a wrongly-issued one costs a second Opus agent for an hour",
|
|
3113
|
+
canonical: "_shared/return-validation.md",
|
|
3114
|
+
never: "never read `clear` as proof that an AD-HOC dispatch finished — the hook writes no SPAWN for one, so no record exists",
|
|
3115
|
+
lanes: ["orc", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"],
|
|
3116
|
+
},
|
|
3094
3117
|
"lane-phases": {
|
|
3095
3118
|
cmd: "orc lane phases <lane> [--json]",
|
|
3096
3119
|
what: "which SHARED phases this lane runs, in order — the file, the layers to read, and when",
|
|
@@ -4077,6 +4100,150 @@ function lanePhaseRows(lane) {
|
|
|
4077
4100
|
return rows;
|
|
4078
4101
|
}
|
|
4079
4102
|
|
|
4103
|
+
// ---------------------------------------------------------------------------
|
|
4104
|
+
// The STATUSLINE RAIL MANIFEST (v1.2.1)
|
|
4105
|
+
//
|
|
4106
|
+
// `templates/hooks/orc-statusline.js` shows which phase an ORC run is in. It
|
|
4107
|
+
// cannot call this CLI: a statusline re-renders on every keystroke, so a
|
|
4108
|
+
// subprocess per render is not on the table. It also must never hold its own
|
|
4109
|
+
// idea of what the phases are — that is the Flow-stepper failure (`orc ui`
|
|
4110
|
+
// v0.43.7) applied to a second surface, and a second phase table is drift no
|
|
4111
|
+
// lint could see.
|
|
4112
|
+
//
|
|
4113
|
+
// So the CLI COMPUTES and the hook RENDERS: `orc init` / `orc update` writes
|
|
4114
|
+
// `hooks/orc-lane-rails.json` from the registries below, exactly the way
|
|
4115
|
+
// `hooks/orc-version.json` is stamped at install time.
|
|
4116
|
+
//
|
|
4117
|
+
// FRAMES ARE NOT IN HERE. The glyphs live in the hook, because they are
|
|
4118
|
+
// presentation: a motif change must not need a reinstall. What ships is the
|
|
4119
|
+
// KIND — a closed word the hook maps to a frame set.
|
|
4120
|
+
// ---------------------------------------------------------------------------
|
|
4121
|
+
|
|
4122
|
+
// A phase's motif kind. The set is CLOSED, and a phase this table does not name
|
|
4123
|
+
// gets `generic` — the honest sweep, not a guess. Ported in spirit from
|
|
4124
|
+
// orc-cli's motifs.js, keyed on the PHASE rather than the lane: the lane name is
|
|
4125
|
+
// already the first word of the statusline's `status:` segment, so the glyph is
|
|
4126
|
+
// free to say what ORC is DOING.
|
|
4127
|
+
const PHASE_KINDS = ["look", "ask", "plan", "do", "check", "ship", "wait", "generic"];
|
|
4128
|
+
const PHASE_KIND = {
|
|
4129
|
+
// reading, scanning, gathering evidence
|
|
4130
|
+
preflight: "look",
|
|
4131
|
+
trace: "look",
|
|
4132
|
+
"wiki-consult": "look",
|
|
4133
|
+
"analyst-gates": "look",
|
|
4134
|
+
"house-rules": "look",
|
|
4135
|
+
analysis: "look",
|
|
4136
|
+
combine: "look",
|
|
4137
|
+
// waiting on the user
|
|
4138
|
+
intake: "ask",
|
|
4139
|
+
// deciding what to do and in what order
|
|
4140
|
+
planning: "plan",
|
|
4141
|
+
"plan-handoff": "plan",
|
|
4142
|
+
"wave-grouping": "plan",
|
|
4143
|
+
scoring: "plan",
|
|
4144
|
+
// executors running
|
|
4145
|
+
execution: "do",
|
|
4146
|
+
// grading what came back
|
|
4147
|
+
review: "check",
|
|
4148
|
+
security: "check",
|
|
4149
|
+
"security-checklist": "check",
|
|
4150
|
+
verify: "check",
|
|
4151
|
+
testgen: "check",
|
|
4152
|
+
"mock-example": "check",
|
|
4153
|
+
"ultra-gate": "check",
|
|
4154
|
+
// handing it over
|
|
4155
|
+
ship: "ship",
|
|
4156
|
+
summary: "ship",
|
|
4157
|
+
// stopped on purpose
|
|
4158
|
+
"stop-resume": "wait",
|
|
4159
|
+
};
|
|
4160
|
+
|
|
4161
|
+
// A word in an own-phase heading that decides its kind. Checked in order, so
|
|
4162
|
+
// the first match wins; no match falls through to `generic`. This is how a
|
|
4163
|
+
// lane's own pipeline (`Q1 LOOK`, `D4 DRAFT`, `C6 ADVISE`) gets a motif without
|
|
4164
|
+
// a hand-maintained row per phase per lane.
|
|
4165
|
+
const OWN_PHASE_KIND_WORDS = [
|
|
4166
|
+
[/\b(ask|question|intake|interview|frame|scope|choose|pick|gate)\b/i, "ask"],
|
|
4167
|
+
[/\b(look|preflight|read|scan|ingest|evidence|load|context|recon|harvest)\b/i, "look"],
|
|
4168
|
+
[/\b(plan|outline|cluster|shape|diverge|converge|order|batch|wave)\b/i, "plan"],
|
|
4169
|
+
[/\b(do|write|build|execute|implement|draft|generate|dispatch|compile)\b/i, "do"],
|
|
4170
|
+
[/\b(check|review|verify|judge|challenge|stress|grade|lint|audit|conserv)\b/i, "check"],
|
|
4171
|
+
[/\b(ship|finish|exit|deliver|hand|summar|report|card|record)\b/i, "ship"],
|
|
4172
|
+
[/\b(wait|stop|resume|pause|suspend)\b/i, "wait"],
|
|
4173
|
+
];
|
|
4174
|
+
|
|
4175
|
+
// The display label for one phase. 18 visible chars, because line 2 of the
|
|
4176
|
+
// statusline already carries five other segments.
|
|
4177
|
+
//
|
|
4178
|
+
// A shared phase is its own id — those ids ARE the words ORC uses everywhere
|
|
4179
|
+
// else, and renaming them for a status bar would invent a second vocabulary.
|
|
4180
|
+
// An own phase is its heading up to the first bracket or dash, which is what
|
|
4181
|
+
// turns `## Q1 — LOOK (silent — no questions here)` into `Q1 LOOK`.
|
|
4182
|
+
function railLabel(row) {
|
|
4183
|
+
if (!row.heading) return String(row.id).slice(0, 18);
|
|
4184
|
+
let s = String(row.heading).replace(/^#+\s*/, "");
|
|
4185
|
+
s = s.split(/\s+[—–-]\s+|\s*\(/)[0].trim();
|
|
4186
|
+
// A heading whose whole first clause is just the code (`## C6 — Advise`)
|
|
4187
|
+
// loses the only interesting word, so keep the second clause too when the
|
|
4188
|
+
// first is a bare code.
|
|
4189
|
+
if (/^[A-Za-z]{1,2}\d+[a-z]?$/.test(s)) {
|
|
4190
|
+
const rest = String(row.heading)
|
|
4191
|
+
.replace(/^#+\s*/, "")
|
|
4192
|
+
.slice(s.length)
|
|
4193
|
+
.replace(/^\s*[—–-]\s*/, "")
|
|
4194
|
+
.split(/\s*\(/)[0]
|
|
4195
|
+
.trim();
|
|
4196
|
+
if (rest) s = s + " " + rest;
|
|
4197
|
+
}
|
|
4198
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
4199
|
+
if (s.length <= 18) return s;
|
|
4200
|
+
// Cut at a word boundary. `D2 Supporting docu` is a typo the reader has to
|
|
4201
|
+
// decode; `D2 Supporting` is a label.
|
|
4202
|
+
const cut = s.slice(0, 18);
|
|
4203
|
+
const sp = cut.lastIndexOf(" ");
|
|
4204
|
+
return (sp > 3 ? cut.slice(0, sp) : cut).trim();
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
function railKind(row) {
|
|
4208
|
+
if (PHASE_KIND[row.id]) return PHASE_KIND[row.id];
|
|
4209
|
+
const hay = `${row.id} ${row.heading || ""}`;
|
|
4210
|
+
for (const [re, kind] of OWN_PHASE_KIND_WORDS) if (re.test(hay)) return kind;
|
|
4211
|
+
return "generic";
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
// The role families `templates/hooks/orc-trace.js` writes as `PHASE-EDGE <fam>`.
|
|
4215
|
+
// This is a MIRROR of that hook's `roleFamily()`, and it is the one new drift
|
|
4216
|
+
// surface this feature adds — a golden test compares the two by source text,
|
|
4217
|
+
// the way the `OPUS5_BANDS` alias is asserted.
|
|
4218
|
+
const TRACE_FAMILIES = ["combine", "analysis", "planning", "execution", "review", "verify", "testgen", "ultra-gate"];
|
|
4219
|
+
|
|
4220
|
+
// The whole object, deterministic from the registries above. `--json is not a
|
|
4221
|
+
// summary`: this is what the file holds and what `orc lane rails` prints.
|
|
4222
|
+
function laneRailsManifest() {
|
|
4223
|
+
const lanes = {};
|
|
4224
|
+
for (const lane of LANE_NAMES) {
|
|
4225
|
+
const t = LANE_TRACE[lane];
|
|
4226
|
+
// A lane that opens no run writes no trace file, so no `run-<lane>-…` name
|
|
4227
|
+
// can ever name it and the resolver could never reach the row. Omitting it
|
|
4228
|
+
// is an answer, not a gap.
|
|
4229
|
+
if (!t || !t.token) continue;
|
|
4230
|
+
const rows = [];
|
|
4231
|
+
for (const p of lanePhaseRows(lane))
|
|
4232
|
+
rows.push({ id: p.id, label: railLabel(p), kind: railKind(p), verbs: [] });
|
|
4233
|
+
for (const p of LANE_OWN_PHASES[lane] || [])
|
|
4234
|
+
rows.push({
|
|
4235
|
+
id: p.id,
|
|
4236
|
+
label: railLabel(p),
|
|
4237
|
+
kind: railKind(p),
|
|
4238
|
+
verbs: Array.isArray(p.trace_verbs) ? p.trace_verbs.slice() : [],
|
|
4239
|
+
});
|
|
4240
|
+
lanes[t.token] = { lane, tier: t.tier, phases: rows };
|
|
4241
|
+
}
|
|
4242
|
+
const families = {};
|
|
4243
|
+
for (const f of TRACE_FAMILIES) families[f] = { label: f, kind: PHASE_KIND[f] || "generic" };
|
|
4244
|
+
return { version: currentVersion(), kinds: PHASE_KINDS, families, lanes };
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4080
4247
|
// \`orc lane phases <lane>\` / \`--all\`. Exit 0 in every state; 2 for an unknown
|
|
4081
4248
|
// lane or no argument — the \`orc lane calls\` contract, unchanged.
|
|
4082
4249
|
//
|
|
@@ -4272,6 +4439,33 @@ function laneConfigCmd(lane, claudeDir) {
|
|
|
4272
4439
|
}
|
|
4273
4440
|
|
|
4274
4441
|
|
|
4442
|
+
// `orc lane rails [--json]` — the statusline's phase rail, exactly as it is
|
|
4443
|
+
// written to `hooks/orc-lane-rails.json` at install. It exists so the manifest
|
|
4444
|
+
// is inspectable without opening a hook directory, and so a golden test reads
|
|
4445
|
+
// the same object the hook does. Exit 0 in every state.
|
|
4446
|
+
//
|
|
4447
|
+
// `--json is not a summary`: this prints the WHOLE object, not a count of it.
|
|
4448
|
+
function laneRailsCmd() {
|
|
4449
|
+
const m = laneRailsManifest();
|
|
4450
|
+
if (wantsJson()) {
|
|
4451
|
+
emitJson(Object.assign({ ok: true }, m));
|
|
4452
|
+
return 0;
|
|
4453
|
+
}
|
|
4454
|
+
console.log(`\nORC statusline rails ${ui.color.gray("v" + m.version)}`);
|
|
4455
|
+
console.log(ui.color.gray(` kinds ${m.kinds.join(", ")}`));
|
|
4456
|
+
console.log(ui.color.gray(` families ${Object.keys(m.families).join(", ")}`));
|
|
4457
|
+
for (const [token, row] of Object.entries(m.lanes)) {
|
|
4458
|
+
console.log(`\n ${ui.color.bold(token)} ${ui.color.gray(row.lane + " · " + row.tier)}`);
|
|
4459
|
+
for (const p of row.phases)
|
|
4460
|
+
console.log(
|
|
4461
|
+
" " + String(p.kind).padEnd(8) + " " + String(p.label).padEnd(20) +
|
|
4462
|
+
ui.color.gray(p.verbs.length ? "verbs: " + p.verbs.join(", ") : "")
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
console.log("");
|
|
4466
|
+
return 0;
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4275
4469
|
// `orc lane` — the noun. `list` and `config` today; `phases` lands in W12 and
|
|
4276
4470
|
// `calls` in W10.
|
|
4277
4471
|
function lane() {
|
|
@@ -4287,13 +4481,16 @@ function lane() {
|
|
|
4287
4481
|
process.exit(laneCallsCmd(pos[2], claudeDir));
|
|
4288
4482
|
case "phases":
|
|
4289
4483
|
process.exit(lanePhasesCmd(pos[2], claudeDir));
|
|
4484
|
+
case "rails":
|
|
4485
|
+
process.exit(laneRailsCmd());
|
|
4290
4486
|
default:
|
|
4291
4487
|
console.error(
|
|
4292
4488
|
`Unknown: orc lane ${pos[1]}\n` +
|
|
4293
4489
|
"Usage: orc lane list [--json]\n" +
|
|
4294
4490
|
" orc lane config <lane> [--json]\n" +
|
|
4295
4491
|
" orc lane calls <lane> [--json] | orc lane calls --all [--json]\n" +
|
|
4296
|
-
" orc lane phases <lane> [--json] | orc lane phases --all [--json]"
|
|
4492
|
+
" orc lane phases <lane> [--json] | orc lane phases --all [--json]" +
|
|
4493
|
+
"\n orc lane rails [--json]"
|
|
4297
4494
|
);
|
|
4298
4495
|
process.exit(2);
|
|
4299
4496
|
}
|
|
@@ -9006,6 +9203,149 @@ function resume() {
|
|
|
9006
9203
|
}
|
|
9007
9204
|
|
|
9008
9205
|
// `orc run list` / `orc run show <slug|n>`
|
|
9206
|
+
// ── `orc run inflight` — is a previous dispatch still alive? (v1.2.0) ───────
|
|
9207
|
+
//
|
|
9208
|
+
// WHY THIS EXISTS. Claude Code's Task tool returning an error does NOT kill the
|
|
9209
|
+
// subagent behind it. The agent keeps running and keeps writing files. Every
|
|
9210
|
+
// lane's retry rule ("a broken return = a failure, re-dispatch") silently
|
|
9211
|
+
// assumed the opposite, so an interrupted turn produced a SECOND agent on the
|
|
9212
|
+
// same task while the first was still working. A graded run put THREE
|
|
9213
|
+
// `orc-executor-opus-5-low` agents on one task for 50m19s + 115m22s + 100m53s
|
|
9214
|
+
// — 266 minutes of Opus 5 for one authorised dispatch, all editing the same
|
|
9215
|
+
// files. The hook already recorded every one of them; nothing ever READ it.
|
|
9216
|
+
//
|
|
9217
|
+
// The pending sidecar (`<trace>.pending.json`, written by orc-trace.js on every
|
|
9218
|
+
// SPAWN) is the evidence. This command makes it authoritative:
|
|
9219
|
+
// `a lane that re-dispatches over a live attempt` has broken the contract in
|
|
9220
|
+
// `_shared/return-validation.md` §0.
|
|
9221
|
+
//
|
|
9222
|
+
// UNKNOWN IS NOT ZERO. A missing pointer, an unreadable sidecar or a record too
|
|
9223
|
+
// old to trust all exit 2 — never 0. "I cannot prove anything is running" and
|
|
9224
|
+
// "I proved nothing is running" are different facts, and re-dispatching on the
|
|
9225
|
+
// first one is exactly the bug. Only a readable sidecar that is EMPTY *and* a
|
|
9226
|
+
// trace whose SPAWN/RETURN counts agree earns exit 0.
|
|
9227
|
+
//
|
|
9228
|
+
// exit 0 clear — provably nothing in flight
|
|
9229
|
+
// exit 1 in-flight — >=1 dispatch has not returned
|
|
9230
|
+
// exit 2 unknown — cannot prove either way
|
|
9231
|
+
const INFLIGHT_STALE_MS = 6 * 60 * 60 * 1000;
|
|
9232
|
+
|
|
9233
|
+
function runInflightCmd(claudeDir) {
|
|
9234
|
+
const dir = resolveLogDir(claudeDir);
|
|
9235
|
+
const out = {
|
|
9236
|
+
ok: true,
|
|
9237
|
+
state: "unknown",
|
|
9238
|
+
reason: null,
|
|
9239
|
+
count: 0,
|
|
9240
|
+
entries: [],
|
|
9241
|
+
trace: null,
|
|
9242
|
+
lane: null,
|
|
9243
|
+
slug: null,
|
|
9244
|
+
log_dir: dir,
|
|
9245
|
+
sidecar: null,
|
|
9246
|
+
sidecar_readable: false,
|
|
9247
|
+
spawns: null,
|
|
9248
|
+
returns: null,
|
|
9249
|
+
balance_agrees: null,
|
|
9250
|
+
stale_entries: 0,
|
|
9251
|
+
};
|
|
9252
|
+
|
|
9253
|
+
const finish = (state, reason, code) => {
|
|
9254
|
+
out.state = state;
|
|
9255
|
+
out.reason = reason;
|
|
9256
|
+
if (wantsJson()) emitJson(out);
|
|
9257
|
+
else renderInflight(out);
|
|
9258
|
+
process.exit(code);
|
|
9259
|
+
};
|
|
9260
|
+
|
|
9261
|
+
// 1. The run pointer. No pointer = no open run we can reason about.
|
|
9262
|
+
let cur = null;
|
|
9263
|
+
try {
|
|
9264
|
+
cur = fs.readFileSync(path.join(dir, ".current"), "utf8").trim();
|
|
9265
|
+
} catch (_) {}
|
|
9266
|
+
if (!cur) return finish("unknown", "no trace pointer — no run is open, or the pointer was never written", 2);
|
|
9267
|
+
out.trace = cur;
|
|
9268
|
+
const m = TRACE_NAME.exec(cur);
|
|
9269
|
+
if (m) { out.lane = m[1]; out.slug = m[2]; }
|
|
9270
|
+
|
|
9271
|
+
// 2. The trace's own SPAWN/RETURN balance — an independent second opinion.
|
|
9272
|
+
// Counted from the hook's own skeleton lines only.
|
|
9273
|
+
const tracePath = path.join(dir, cur);
|
|
9274
|
+
try {
|
|
9275
|
+
const text = fs.readFileSync(tracePath, "utf8");
|
|
9276
|
+
out.spawns = (text.match(/\] hook\s+SPAWN /g) || []).length;
|
|
9277
|
+
const all = (text.match(/\] hook\s+RETURN /g) || []).length;
|
|
9278
|
+
const loose = (text.match(/\] hook\s+RETURN ~agent :: unattributed/g) || []).length;
|
|
9279
|
+
out.returns = all - loose;
|
|
9280
|
+
} catch (_) {
|
|
9281
|
+
return finish("unknown", `trace pointer names "${cur}" but it cannot be read`, 2);
|
|
9282
|
+
}
|
|
9283
|
+
|
|
9284
|
+
// 3. The pending sidecar — the record of what was dispatched and never closed.
|
|
9285
|
+
const side = path.join(dir, cur + ".pending.json");
|
|
9286
|
+
out.sidecar = side;
|
|
9287
|
+
let pend = null;
|
|
9288
|
+
try {
|
|
9289
|
+
const raw = JSON.parse(fs.readFileSync(side, "utf8"));
|
|
9290
|
+
if (Array.isArray(raw)) { pend = raw; out.sidecar_readable = true; }
|
|
9291
|
+
} catch (_) {}
|
|
9292
|
+
|
|
9293
|
+
const now = Date.now();
|
|
9294
|
+
if (pend) {
|
|
9295
|
+
out.entries = pend.map((r) => {
|
|
9296
|
+
const ts = typeof r.ts === "number" ? r.ts : null;
|
|
9297
|
+
const age = ts == null ? null : Math.round((now - ts) / 1000);
|
|
9298
|
+
return {
|
|
9299
|
+
agent: r.agent == null ? null : String(r.agent),
|
|
9300
|
+
desc: r.desc == null ? null : String(r.desc),
|
|
9301
|
+
started_ms: ts,
|
|
9302
|
+
age_s: age,
|
|
9303
|
+
stale: ts != null && now - ts > INFLIGHT_STALE_MS,
|
|
9304
|
+
};
|
|
9305
|
+
});
|
|
9306
|
+
out.count = out.entries.length;
|
|
9307
|
+
out.stale_entries = out.entries.filter((e) => e.stale).length;
|
|
9308
|
+
}
|
|
9309
|
+
|
|
9310
|
+
const balance = out.spawns != null && out.returns != null ? out.spawns - out.returns : null;
|
|
9311
|
+
out.balance_agrees = balance == null || pend == null ? null : balance === out.count;
|
|
9312
|
+
|
|
9313
|
+
// 4. Verdict. Conservative in every direction.
|
|
9314
|
+
if (!out.sidecar_readable) {
|
|
9315
|
+
if (balance != null && balance > 0)
|
|
9316
|
+
return finish("in-flight", `sidecar unreadable, but the trace shows ${balance} SPAWN(s) with no RETURN`, 1);
|
|
9317
|
+
return finish("unknown", "the pending sidecar is missing or unreadable — cannot prove a dispatch finished", 2);
|
|
9318
|
+
}
|
|
9319
|
+
if (out.count > 0) {
|
|
9320
|
+
if (out.stale_entries === out.count)
|
|
9321
|
+
return finish(
|
|
9322
|
+
"unknown",
|
|
9323
|
+
`${out.count} record(s), all older than 6h — the run probably died without closing them`,
|
|
9324
|
+
2
|
|
9325
|
+
);
|
|
9326
|
+
return finish("in-flight", `${out.count} dispatch(es) have not returned`, 1);
|
|
9327
|
+
}
|
|
9328
|
+
// Sidecar empty. Only trust it when the trace agrees.
|
|
9329
|
+
if (balance != null && balance > 0)
|
|
9330
|
+
return finish("unknown", `sidecar is empty but the trace shows ${balance} unmatched SPAWN(s) — they disagree`, 2);
|
|
9331
|
+
return finish("clear", "no dispatch is in flight", 0);
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
function renderInflight(o) {
|
|
9335
|
+
const head =
|
|
9336
|
+
o.state === "clear" ? "clear" : o.state === "in-flight" ? `IN FLIGHT (${o.count})` : "unknown";
|
|
9337
|
+
console.log(`${ui.color.bold("dispatch:")} ${head}`);
|
|
9338
|
+
console.log(` ${o.reason}`);
|
|
9339
|
+
if (o.trace) console.log(` trace ${o.trace}`);
|
|
9340
|
+
for (const e of o.entries) {
|
|
9341
|
+
const age = e.age_s == null ? "age unknown" : `${Math.floor(e.age_s / 60)}m${e.age_s % 60}s ago`;
|
|
9342
|
+
console.log(` - ${e.agent || "(unnamed)"} ${age}${e.stale ? " [stale]" : ""}`);
|
|
9343
|
+
if (e.desc) console.log(` ${e.desc}`);
|
|
9344
|
+
}
|
|
9345
|
+
if (o.state === "in-flight")
|
|
9346
|
+
console.log("\n Do NOT re-dispatch these tasks. A Task error does not kill the agent behind it.");
|
|
9347
|
+
}
|
|
9348
|
+
|
|
9009
9349
|
function runCmd() {
|
|
9010
9350
|
const claudeDir = resolveClaudeDir();
|
|
9011
9351
|
const pos = positionals(); // ["run", <sub?>, <arg?>]
|
|
@@ -9016,6 +9356,9 @@ function runCmd() {
|
|
|
9016
9356
|
// on disk. Neither deletes anything.
|
|
9017
9357
|
if (sub === "close" || sub === "reopen") return runCloseCmd(claudeDir, runs, sub, pos[2]);
|
|
9018
9358
|
|
|
9359
|
+
// Read-only, and the ONE reader of the pending sidecar. 0 clear / 1 in-flight / 2 unknown.
|
|
9360
|
+
if (sub === "inflight") return runInflightCmd(claudeDir);
|
|
9361
|
+
|
|
9019
9362
|
if (sub === "show") {
|
|
9020
9363
|
const arg = pos[2];
|
|
9021
9364
|
const pick = /^\d+$/.test(String(arg)) ? runs[Number(arg) - 1] : runs.find((r) => r.slug === arg);
|
|
@@ -33210,6 +33553,318 @@ function usageResetMs(v) {
|
|
|
33210
33553
|
return Number.isFinite(p) ? p : NaN;
|
|
33211
33554
|
}
|
|
33212
33555
|
|
|
33556
|
+
// ── `orc usage report` — where the window went (v1.2.0) ─────────────────────
|
|
33557
|
+
//
|
|
33558
|
+
// `orc usage check` answers "is there room". This answers the question a user
|
|
33559
|
+
// actually asks mid-run: "how much has THIS session eaten, and what ate it".
|
|
33560
|
+
//
|
|
33561
|
+
// THE HONEST PART, and it decides the whole shape of this command. Claude Code
|
|
33562
|
+
// records NO token usage for a dispatched subagent — `isSidechain` is never set
|
|
33563
|
+
// and no sidechain message carries a usage block, in any transcript on disk. So
|
|
33564
|
+
// a per-executor TOKEN figure cannot be measured, and inventing one would be
|
|
33565
|
+
// the same class of bug as a fake validator. What IS measured, exactly, is WALL
|
|
33566
|
+
// TIME, from the trace hook's own SPAWN/RETURN lines. Foreign workers are the
|
|
33567
|
+
// one exception: `orc extra` records real four-kind vectors, so those rows
|
|
33568
|
+
// carry tokens and say so. Every other row reports `tokens: null` plus the
|
|
33569
|
+
// reason — never 0. Unknown is not zero.
|
|
33570
|
+
const USAGE_TOP_N = 5;
|
|
33571
|
+
|
|
33572
|
+
// "12m43s" | "1m7s" | "45s" → seconds. Anything else → null, never 0.
|
|
33573
|
+
function usageDurSeconds(s) {
|
|
33574
|
+
if (!s) return null;
|
|
33575
|
+
const m = /^(?:(\d+)m)?(\d+)s$/.exec(String(s).trim());
|
|
33576
|
+
if (!m) return null;
|
|
33577
|
+
return (m[1] ? Number(m[1]) * 60 : 0) + Number(m[2]);
|
|
33578
|
+
}
|
|
33579
|
+
|
|
33580
|
+
// Per-agent wall time for the run the trace pointer names. Returns null when
|
|
33581
|
+
// there is no open run — an absent trace is an absent measurement, not zero.
|
|
33582
|
+
function usageRunConsumers(claudeDir) {
|
|
33583
|
+
const dir = resolveLogDir(claudeDir);
|
|
33584
|
+
let cur = null;
|
|
33585
|
+
try {
|
|
33586
|
+
cur = fs.readFileSync(path.join(dir, ".current"), "utf8").trim();
|
|
33587
|
+
} catch (_) {}
|
|
33588
|
+
if (!cur) return null;
|
|
33589
|
+
let text = "";
|
|
33590
|
+
try {
|
|
33591
|
+
text = fs.readFileSync(path.join(dir, cur), "utf8");
|
|
33592
|
+
} catch (_) {
|
|
33593
|
+
return null;
|
|
33594
|
+
}
|
|
33595
|
+
const nameMatch = TRACE_NAME.exec(cur);
|
|
33596
|
+
const by = new Map();
|
|
33597
|
+
const bump = (agent, secs, running) => {
|
|
33598
|
+
const k = agent || "(unnamed)";
|
|
33599
|
+
const r = by.get(k) || { agent: k, dispatches: 0, wall_seconds: 0, running: 0, unmeasured: 0 };
|
|
33600
|
+
r.dispatches += 1;
|
|
33601
|
+
if (running) r.running += 1;
|
|
33602
|
+
if (secs == null) r.unmeasured += 1;
|
|
33603
|
+
else r.wall_seconds += secs;
|
|
33604
|
+
by.set(k, r);
|
|
33605
|
+
};
|
|
33606
|
+
for (const line of text.split("\n")) {
|
|
33607
|
+
// Only the hook's own skeleton lines. `~agent :: unattributed` is a
|
|
33608
|
+
// bookkeeping artefact of >=2 in flight and is never a dispatch.
|
|
33609
|
+
const r = /\] hook\s+RETURN ~?([^\s:]+) :: /.exec(line);
|
|
33610
|
+
if (!r) continue;
|
|
33611
|
+
if (r[1] === "agent") continue;
|
|
33612
|
+
const d = /\bdur=(\S+)/.exec(line);
|
|
33613
|
+
bump(r[1], usageDurSeconds(d && d[1]), false);
|
|
33614
|
+
}
|
|
33615
|
+
// Anything still open is real spend happening RIGHT NOW — the exact case the
|
|
33616
|
+
// v1.2.0 in-flight guard exists for, and the one a user most wants to see.
|
|
33617
|
+
let pending = [];
|
|
33618
|
+
try {
|
|
33619
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, cur + ".pending.json"), "utf8"));
|
|
33620
|
+
if (Array.isArray(raw)) pending = raw;
|
|
33621
|
+
} catch (_) {}
|
|
33622
|
+
const now = Date.now();
|
|
33623
|
+
for (const p of pending) {
|
|
33624
|
+
const secs = typeof p.ts === "number" ? Math.round((now - p.ts) / 1000) : null;
|
|
33625
|
+
bump(p.agent, secs, true);
|
|
33626
|
+
}
|
|
33627
|
+
return {
|
|
33628
|
+
trace: cur,
|
|
33629
|
+
lane: nameMatch ? nameMatch[1] : null,
|
|
33630
|
+
slug: nameMatch ? nameMatch[2] : null,
|
|
33631
|
+
agents: [...by.values()],
|
|
33632
|
+
in_flight: pending.length,
|
|
33633
|
+
};
|
|
33634
|
+
}
|
|
33635
|
+
|
|
33636
|
+
// Foreign dispatches DO carry measured tokens. Same file the spend report reads.
|
|
33637
|
+
function usageForeignSpend(claudeDir) {
|
|
33638
|
+
const f = path.join(claudeDir, "orc", "extra-spend.jsonl");
|
|
33639
|
+
let lines = [];
|
|
33640
|
+
try {
|
|
33641
|
+
lines = fs.readFileSync(f, "utf8").split("\n").filter(Boolean);
|
|
33642
|
+
} catch (_) {
|
|
33643
|
+
return { rows: [], unreadable: 0 };
|
|
33644
|
+
}
|
|
33645
|
+
const rows = [];
|
|
33646
|
+
let unreadable = 0;
|
|
33647
|
+
for (const l of lines) {
|
|
33648
|
+
try {
|
|
33649
|
+
rows.push(JSON.parse(l));
|
|
33650
|
+
} catch (_) {
|
|
33651
|
+
unreadable += 1;
|
|
33652
|
+
}
|
|
33653
|
+
}
|
|
33654
|
+
return { rows, unreadable };
|
|
33655
|
+
}
|
|
33656
|
+
|
|
33657
|
+
function usageReportCmd(claudeDir) {
|
|
33658
|
+
const asJson = wantsJson();
|
|
33659
|
+
const now = Date.now();
|
|
33660
|
+
const cfg = resolvedConfig(claudeDir);
|
|
33661
|
+
const stopPct = Math.min(50, Math.max(1, Number(cfg.usage_stop_pct) || 10));
|
|
33662
|
+
|
|
33663
|
+
// Read the bridge RAW here rather than through readUsageBridge: a stale
|
|
33664
|
+
// reading is still worth SHOWING, with its age, where a gate would rightly
|
|
33665
|
+
// discard it. The state word still comes from freshness — age is displayed,
|
|
33666
|
+
// never ignored.
|
|
33667
|
+
let raw = null;
|
|
33668
|
+
try {
|
|
33669
|
+
raw = JSON.parse(fs.readFileSync(path.join(claudeDir, "orc", "usage.json"), "utf8"));
|
|
33670
|
+
} catch (_) {}
|
|
33671
|
+
const ageMin =
|
|
33672
|
+
raw && typeof raw.written_at === "number" ? Math.round((now - raw.written_at) / 60000) : null;
|
|
33673
|
+
const stale = ageMin == null ? true : now - raw.written_at > WAIT_BRIDGE_MAX_AGE_MS;
|
|
33674
|
+
|
|
33675
|
+
const view = (o, label) => {
|
|
33676
|
+
if (!o || typeof o.used_percentage !== "number") return null;
|
|
33677
|
+
const used = Math.round(o.used_percentage);
|
|
33678
|
+
const at = usageResetMs(o.resets_at);
|
|
33679
|
+
return {
|
|
33680
|
+
window: label,
|
|
33681
|
+
used_percentage: used,
|
|
33682
|
+
remaining_percentage: Math.max(0, 100 - used),
|
|
33683
|
+
resets_at: Number.isFinite(at) ? new Date(at).toISOString() : null,
|
|
33684
|
+
resets_in_minutes: Number.isFinite(at) && at > now ? Math.round((at - now) / 60000) : null,
|
|
33685
|
+
low: 100 - used <= stopPct,
|
|
33686
|
+
};
|
|
33687
|
+
};
|
|
33688
|
+
const fh = raw ? view(raw.five_hour, "5h") : null;
|
|
33689
|
+
const sd = raw ? view(raw.seven_day, "wk") : null;
|
|
33690
|
+
|
|
33691
|
+
// Session consumption — the ledger the statusline keeps.
|
|
33692
|
+
let led = null;
|
|
33693
|
+
try {
|
|
33694
|
+
led = JSON.parse(fs.readFileSync(path.join(claudeDir, "orc", "usage-session.json"), "utf8"));
|
|
33695
|
+
} catch (_) {}
|
|
33696
|
+
const consumed = (w) =>
|
|
33697
|
+
!w
|
|
33698
|
+
? null
|
|
33699
|
+
: {
|
|
33700
|
+
baseline_percentage: w.baseline,
|
|
33701
|
+
now_percentage: w.last,
|
|
33702
|
+
consumed_percentage: Math.max(0, w.accumulated + Math.max(0, w.last - w.baseline)),
|
|
33703
|
+
window_resets: w.resets,
|
|
33704
|
+
};
|
|
33705
|
+
const session = !led
|
|
33706
|
+
? null
|
|
33707
|
+
: {
|
|
33708
|
+
session_id: led.session_id || null,
|
|
33709
|
+
started_at: led.started_at ? new Date(led.started_at).toISOString() : null,
|
|
33710
|
+
running_minutes: led.started_at ? Math.round((now - led.started_at) / 60000) : null,
|
|
33711
|
+
five_hour: consumed(led.five_hour),
|
|
33712
|
+
seven_day: consumed(led.seven_day),
|
|
33713
|
+
still_counting: true,
|
|
33714
|
+
// Never overclaim. The window is per ACCOUNT: a second Claude Code
|
|
33715
|
+
// window, a cloud session, or anyone else on the same key moves it too.
|
|
33716
|
+
caveat:
|
|
33717
|
+
"this is how far the window moved while this session ran — other sessions on the same account share it",
|
|
33718
|
+
};
|
|
33719
|
+
|
|
33720
|
+
const run = usageRunConsumers(claudeDir);
|
|
33721
|
+
const foreign = usageForeignSpend(claudeDir);
|
|
33722
|
+
|
|
33723
|
+
// Rank by measured wall time. A row with nothing measured keeps its slot.
|
|
33724
|
+
const top = [];
|
|
33725
|
+
if (run) {
|
|
33726
|
+
const sorted = run.agents.slice().sort((a, b) => b.wall_seconds - a.wall_seconds);
|
|
33727
|
+
for (const a of sorted.slice(0, USAGE_TOP_N))
|
|
33728
|
+
top.push({
|
|
33729
|
+
agent: a.agent,
|
|
33730
|
+
dispatches: a.dispatches,
|
|
33731
|
+
running: a.running,
|
|
33732
|
+
wall_seconds: a.wall_seconds,
|
|
33733
|
+
unmeasured_dispatches: a.unmeasured,
|
|
33734
|
+
tokens: null,
|
|
33735
|
+
tokens_source: "unavailable",
|
|
33736
|
+
});
|
|
33737
|
+
}
|
|
33738
|
+
for (const r of foreign.rows.slice(-USAGE_TOP_N)) {
|
|
33739
|
+
top.push({
|
|
33740
|
+
agent: "extra:" + (r.profile || "?") + "/" + (r.model || "?"),
|
|
33741
|
+
dispatches: 1,
|
|
33742
|
+
running: 0,
|
|
33743
|
+
wall_seconds: usageDurSeconds(r.dur),
|
|
33744
|
+
unmeasured_dispatches: 0,
|
|
33745
|
+
tokens: r.usage || null,
|
|
33746
|
+
tokens_source: r.usage ? "measured" : "not reported by the worker",
|
|
33747
|
+
});
|
|
33748
|
+
}
|
|
33749
|
+
|
|
33750
|
+
const windows = [fh, sd].filter(Boolean);
|
|
33751
|
+
const state = !windows.length || stale ? "unknown" : windows.some((w) => w.low) ? "low" : "ok";
|
|
33752
|
+
const code = state === "low" ? 1 : state === "unknown" ? 2 : 0;
|
|
33753
|
+
const out = {
|
|
33754
|
+
ok: true,
|
|
33755
|
+
state,
|
|
33756
|
+
five_hour: fh,
|
|
33757
|
+
seven_day: sd,
|
|
33758
|
+
context_used_percentage:
|
|
33759
|
+
raw && typeof raw.context_used_percentage === "number" ? raw.context_used_percentage : null,
|
|
33760
|
+
reading_age_minutes: ageMin,
|
|
33761
|
+
reading_stale: stale,
|
|
33762
|
+
stop_pct: stopPct,
|
|
33763
|
+
gate: String(cfg.usage_gate || "off"),
|
|
33764
|
+
session,
|
|
33765
|
+
run: run
|
|
33766
|
+
? {
|
|
33767
|
+
trace: run.trace,
|
|
33768
|
+
lane: run.lane,
|
|
33769
|
+
slug: run.slug,
|
|
33770
|
+
dispatches: run.agents.reduce((n, a) => n + a.dispatches, 0),
|
|
33771
|
+
in_flight: run.in_flight,
|
|
33772
|
+
wall_seconds: run.agents.reduce((n, a) => n + a.wall_seconds, 0),
|
|
33773
|
+
}
|
|
33774
|
+
: null,
|
|
33775
|
+
top,
|
|
33776
|
+
foreign_spend_rows: foreign.rows.length,
|
|
33777
|
+
unreadable_spend_lines: foreign.unreadable,
|
|
33778
|
+
// Say it on every emission. A reader who does not know this reads a
|
|
33779
|
+
// wall-time ranking as a token ranking.
|
|
33780
|
+
tokens_note:
|
|
33781
|
+
"Claude Code records no token usage for a dispatched subagent, so a Claude agent's tokens are null and never 0. Rows are ranked by MEASURED WALL TIME. Only `orc extra` foreign workers report real token vectors.",
|
|
33782
|
+
};
|
|
33783
|
+
if (asJson) emitJson(out, code);
|
|
33784
|
+
|
|
33785
|
+
console.log(ui.header("ORC · usage"));
|
|
33786
|
+
console.log("");
|
|
33787
|
+
const pctLine = (w) =>
|
|
33788
|
+
!w
|
|
33789
|
+
? ui.color.gray(" — (no reading)")
|
|
33790
|
+
: " " +
|
|
33791
|
+
String(w.used_percentage).padStart(3) +
|
|
33792
|
+
"% used · " +
|
|
33793
|
+
w.remaining_percentage +
|
|
33794
|
+
"% left" +
|
|
33795
|
+
(w.resets_in_minutes != null ? " · resets in " + w.resets_in_minutes + "m" : "") +
|
|
33796
|
+
(w.low ? " " + ui.color.yellow("LOW") : "");
|
|
33797
|
+
console.log(" 5 hours");
|
|
33798
|
+
console.log(pctLine(fh));
|
|
33799
|
+
console.log(" 7 days");
|
|
33800
|
+
console.log(pctLine(sd));
|
|
33801
|
+
if (out.context_used_percentage != null)
|
|
33802
|
+
console.log(" context " + out.context_used_percentage + "% of the window");
|
|
33803
|
+
if (stale)
|
|
33804
|
+
console.log(
|
|
33805
|
+
ui.color.gray(
|
|
33806
|
+
" reading " +
|
|
33807
|
+
(ageMin == null ? "none" : ageMin + "m old") +
|
|
33808
|
+
" — treated as unknown, and a run is never stopped on it"
|
|
33809
|
+
)
|
|
33810
|
+
);
|
|
33811
|
+
console.log("");
|
|
33812
|
+
if (session && session.five_hour) {
|
|
33813
|
+
const c = session.five_hour;
|
|
33814
|
+
console.log(
|
|
33815
|
+
" " +
|
|
33816
|
+
ui.color.bold(
|
|
33817
|
+
"This session has consumed " +
|
|
33818
|
+
c.consumed_percentage +
|
|
33819
|
+
"% of the 5-hour window and is still counting"
|
|
33820
|
+
)
|
|
33821
|
+
);
|
|
33822
|
+
console.log(
|
|
33823
|
+
ui.color.gray(
|
|
33824
|
+
" " +
|
|
33825
|
+
c.baseline_percentage +
|
|
33826
|
+
"% → " +
|
|
33827
|
+
c.now_percentage +
|
|
33828
|
+
"%" +
|
|
33829
|
+
(c.window_resets ? " across " + c.window_resets + " window reset(s)" : "") +
|
|
33830
|
+
(session.running_minutes != null ? " · " + session.running_minutes + "m in session" : "")
|
|
33831
|
+
)
|
|
33832
|
+
);
|
|
33833
|
+
console.log(ui.color.gray(" " + session.caveat));
|
|
33834
|
+
} else {
|
|
33835
|
+
console.log(ui.color.gray(" session consumption: no reading yet (the statusline writes it)"));
|
|
33836
|
+
}
|
|
33837
|
+
console.log("");
|
|
33838
|
+
if (top.length) {
|
|
33839
|
+
console.log(" " + ui.color.bold("Top " + USAGE_TOP_N + " by measured wall time"));
|
|
33840
|
+
for (const t of top.slice(0, USAGE_TOP_N)) {
|
|
33841
|
+
const secs = t.wall_seconds;
|
|
33842
|
+
const w =
|
|
33843
|
+
secs == null
|
|
33844
|
+
? "—"
|
|
33845
|
+
: Math.floor(secs / 60) + "m" + String(secs % 60).padStart(2, "0") + "s";
|
|
33846
|
+
const tok = t.tokens ? "tokens measured" : ui.color.gray("tokens —");
|
|
33847
|
+
console.log(
|
|
33848
|
+
" " +
|
|
33849
|
+
w.padStart(8) +
|
|
33850
|
+
" " +
|
|
33851
|
+
t.agent +
|
|
33852
|
+
" " +
|
|
33853
|
+
ui.color.gray("x" + t.dispatches) +
|
|
33854
|
+
(t.running ? " " + ui.color.yellow(t.running + " RUNNING") : "") +
|
|
33855
|
+
" " +
|
|
33856
|
+
tok
|
|
33857
|
+
);
|
|
33858
|
+
}
|
|
33859
|
+
console.log(
|
|
33860
|
+
ui.color.gray(" wall time, not tokens — Claude Code does not record a subagent's tokens")
|
|
33861
|
+
);
|
|
33862
|
+
} else {
|
|
33863
|
+
console.log(ui.color.gray(" no dispatches in the open run (or no run is open)"));
|
|
33864
|
+
}
|
|
33865
|
+
process.exit(code);
|
|
33866
|
+
}
|
|
33867
|
+
|
|
33213
33868
|
function usageCheckCmd(claudeDir) {
|
|
33214
33869
|
const asJson = wantsJson();
|
|
33215
33870
|
const now = Date.now();
|
|
@@ -33641,8 +34296,9 @@ function jsonCrash(err) {
|
|
|
33641
34296
|
// v1.1.0 W4 — the ONE reader of the statusline's usage bridge.
|
|
33642
34297
|
case "usage":
|
|
33643
34298
|
if (positionals()[1] === "check") usageCheckCmd(resolveClaudeDir());
|
|
34299
|
+
else if (positionals()[1] === "report") usageReportCmd(resolveClaudeDir());
|
|
33644
34300
|
else {
|
|
33645
|
-
console.error("usage: orc usage check [--json]");
|
|
34301
|
+
console.error("usage: orc usage check|report [--json]");
|
|
33646
34302
|
process.exit(1);
|
|
33647
34303
|
}
|
|
33648
34304
|
break;
|