@azure-id/orc 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +84 -34
  3. package/bin/cli.js +1110 -0
  4. package/bin/verify-contracts.js +112 -1
  5. package/bin/verify-package.js +568 -563
  6. package/bin/webui/api.js +15 -0
  7. package/bin/webui/app.html +210 -207
  8. package/bin/webui/css/panels/wait.css +123 -0
  9. package/bin/webui/fixtures/index.js +7 -0
  10. package/bin/webui/fixtures/wait.js +97 -0
  11. package/bin/webui/i18n/en/nav.json +21 -20
  12. package/bin/webui/i18n/en/wait.json +41 -0
  13. package/bin/webui/i18n/id/nav.json +21 -20
  14. package/bin/webui/i18n/id/wait.json +41 -0
  15. package/bin/webui/js/01-i18n.js +151 -150
  16. package/bin/webui/js/panels/wait.js +253 -0
  17. package/package.json +1 -1
  18. package/templates/commands/orc-wait.md +19 -0
  19. package/templates/hooks/orc-statusline.js +227 -1
  20. package/templates/skills/_shared/phases/execution.md +2 -0
  21. package/templates/skills/_shared/phases/preflight.md +22 -0
  22. package/templates/skills/_shared/return-validation.md +222 -145
  23. package/templates/skills/_shared/wait.md +240 -0
  24. package/templates/skills/orc/SKILL.md +247 -238
  25. package/templates/skills/orc-aftermath/SKILL.md +6 -1
  26. package/templates/skills/orc-analyze/SKILL.md +6 -1
  27. package/templates/skills/orc-boundary/SKILL.md +6 -1
  28. package/templates/skills/orc-brainstorm/SKILL.md +6 -1
  29. package/templates/skills/orc-budget/SKILL.md +6 -1
  30. package/templates/skills/orc-challenge/SKILL.md +6 -1
  31. package/templates/skills/orc-claude/SKILL.md +6 -1
  32. package/templates/skills/orc-diy/SKILL.md +6 -1
  33. package/templates/skills/orc-doc/SKILL.md +490 -481
  34. package/templates/skills/orc-explain/SKILL.md +5 -0
  35. package/templates/skills/orc-export/SKILL.md +5 -0
  36. package/templates/skills/orc-fast/SKILL.md +222 -215
  37. package/templates/skills/orc-grill/SKILL.md +6 -1
  38. package/templates/skills/orc-learn/SKILL.md +6 -1
  39. package/templates/skills/orc-mini/SKILL.md +252 -244
  40. package/templates/skills/orc-pact/SKILL.md +6 -1
  41. package/templates/skills/orc-pattern/SKILL.md +6 -1
  42. package/templates/skills/orc-poly/SKILL.md +6 -1
  43. package/templates/skills/orc-quick/SKILL.md +353 -346
  44. package/templates/skills/orc-retro/SKILL.md +6 -1
  45. package/templates/skills/orc-route/SKILL.md +6 -1
  46. package/templates/skills/orc-verify/SKILL.md +6 -1
  47. package/templates/skills/orc-wait/SKILL.md +163 -0
  48. package/templates/skills/orc-wiki/SKILL.md +180 -171
package/bin/cli.js CHANGED
@@ -105,6 +105,12 @@ function positionals() {
105
105
  i++; // skip the flag's value
106
106
  continue;
107
107
  }
108
+ // v1.1.0 W1 — `orc wait plan 5h --max-hops 3` must not read "3" as part of
109
+ // the spec: "5h 3" parses as 5h3m, so the wait silently gains three minutes.
110
+ if (a === "--hop" || a === "--max-hops") {
111
+ i++;
112
+ continue;
113
+ }
108
114
  // `orc extra` takes value-flags that can legally precede a positional
109
115
  // (`orc extra add --provider deepseek deepseek`), and a value swallowed
110
116
  // as a positional is a profile named after a provider id.
@@ -1161,6 +1167,10 @@ const CONFIG_FAMILIES = {
1161
1167
  retro: { contested: false, question: "where a retro is delivered" },
1162
1168
  paths: { contested: false, question: "where ORC writes on disk" },
1163
1169
  session: { contested: false, question: "what the main session itself runs as" },
1170
+ // v1.1.0 — when ORC stops for wall-clock time, and how much it finishes
1171
+ // first. EVERY default here is off or ask: nothing in this family may stop a
1172
+ // run the user did not ask it to stop.
1173
+ wait: { contested: false, question: "when a run stops to wait for quota, and how much it finishes first" },
1164
1174
  };
1165
1175
 
1166
1176
  // Ordered, tiered metadata. Common first, then advanced.
@@ -1198,6 +1208,17 @@ const CONFIG_META = [
1198
1208
  { key: "stacked_pr_loc", def: 1000, tier: "common", answers: [{ family: "pr", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pr-setup"], validate: vInt(1), options: [500, 800, 1000, 1500, 2000], desc: "Change LoC (additions+deletions, exclusions applied) >= this trips the stacked-PR gate — and is ALSO the per-layer LoC ceiling: a change that cannot fit in one layer's budget is what is worth stacking." },
1199
1209
  { key: "stacked_pr_files", def: 20, tier: "common", answers: [{ family: "pr", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pr-setup"], validate: vInt(1), options: [10, 15, 20, 30, 40], desc: "Changed-file count >= this trips the stacked-PR gate; also the per-layer hard max (soft target = half of it)." },
1200
1210
  { key: "stacked_pr_max_layers", def: 6, tier: "common", answers: [{ family: "pr", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pr-setup"], validate: vInt(2), options: [4, 5, 6, 8, 10], desc: "Soft cap on layers per stack: <= cap proceed, cap+1..cap+2 warn + explicit override, beyond → STOP (multiple stacks or a phased release). N layers = N full CI runs." },
1211
+ // --- v1.1.0 — the usage gate and the wait ---------------------------------
1212
+ // EVERY DEFAULT IS OFF. A fresh install behaves exactly as it did before this
1213
+ // release: `usage_gate: off` means the automatic half does nothing at all,
1214
+ // and `wait_default_mode: ask` means there is no stop behaviour the user did
1215
+ // not choose. `/orc-wait` needs none of these keys to work — a typed wait is
1216
+ // the user's decision and no config may pre-empt it.
1217
+ { key: "usage_gate", def: "off", tier: "common", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vEnum("off", "warn", "stop", "wait"), options: ["off", "warn", "stop", "wait"], desc: "What ORC does when the 5-hour or 7-day window is nearly full, checked before a wave: off = nothing (the default — ORC never stops you until you ask it to), warn = print the reading and continue, stop = write the hand-back and stop, wait = hand back, then wait out the reset in detached hops that cost zero tokens. A missing or stale reading is `unknown` and NEVER stops a run. `/orc-wait block <reason>` suppresses this for one run; a typed `/orc-wait` is never suppressed." },
1218
+ { key: "usage_stop_pct", def: 10, tier: "common", answers: [{ family: "wait", prio: "P2", mode: "replace" }], gated_by: "usage_gate", lanes: ["orc", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vInt(1), options: [5, 10, 15, 20, 30], desc: "How much of a window must REMAIN for a wave to start. The WORST of the two windows decides — a 7-day window at 96% is not a green light because the 5-hour one is at 20%. Inert while usage_gate is off." },
1219
+ { key: "wait_default_mode", def: "ask", tier: "common", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vEnum("ask", "safe", "soft", "hard"), options: ["ask", "safe", "soft", "hard"], desc: "The mode a wait uses when you do not name one: ask = ONE question with the cost of each spelled out (the default — there is no stop behaviour you did not choose), safe = finish the current wave first and lose nothing, soft = stop at the next model turn but FORCE the checkpoint (and do not stop if that write fails), hard = stop at the next model turn with the hand-back only, which can lose an in-flight return. A mode named on the command line always wins." },
1220
+ { key: "wait_hop_minutes", def: 30, tier: "advanced", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [5, 10, 15, 30], desc: "How long ONE detached hop waits before ORC re-reads the window. Short on purpose: each wake-up is session activity, and session activity is the only thing that makes the statusline write a fresh reading. One long sleep wakes into a reading as stale as the sleep was long." },
1221
+ { key: "wait_max_hops", def: 5, tier: "advanced", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [1, 2, 3, 5, 8, 12], desc: "How many hops before ORC gives up and stops with the hand-back. A wrong reset time must cost you a bounded wait, never a session that never comes back." },
1201
1222
  // --- v0.50.0 — orc extra: dispatch ORC's WORKERS to non-Claude agents -----
1202
1223
  // Nine keys, and the count is the feature: the combinatorial part —
1203
1224
  // providers x models x bands — is a LEDGER with a CLI and a panel
@@ -2603,6 +2624,9 @@ const LANES = [
2603
2624
  { lane: "orc-retro", command: "orc-retro" },
2604
2625
  { lane: "orc-route", command: "orc-route" },
2605
2626
  { lane: "orc-verify", command: "orc-verify" },
2627
+ // v1.1.0 W2 — command-entry only, and it opens no run of its own: it waits
2628
+ // INSIDE whatever run is already in flight, or with none at all.
2629
+ { lane: "orc-wait", command: "orc-wait" },
2606
2630
  { lane: "orc-wiki", command: "orc-wiki" },
2607
2631
  ];
2608
2632
  const LANE_NAMES = LANES.map((l) => l.lane);
@@ -2647,6 +2671,16 @@ const LANE_INERT = {
2647
2671
  "orc-doc": [
2648
2672
  { key: "opus5_only", reason: "both agents in this lane are already claude-opus-5, so this is a no-op — the lane is unaffected, not exempt" },
2649
2673
  ],
2674
+ // v1.1.0 W2 — this lane DISPATCHES NOTHING. A detached command does the
2675
+ // waiting, so every family that answers "which model runs this" has no work
2676
+ // to name here. Reporting an executor band as live for a lane that never
2677
+ // executes is the mirror of a shadowed setting being silent: a setting that
2678
+ // does nothing must never be reported as live.
2679
+ "orc-wait": [
2680
+ { family: "executor-band", reason: "this lane dispatches nothing — a detached command does the waiting, and no model runs during it" },
2681
+ { family: "fixed-role-model", reason: "this lane dispatches nothing — there is no role here to pin a model to" },
2682
+ { key: "opus5_only", reason: "this lane dispatches nothing, so there is no agent for a forcing mode to replace" },
2683
+ ],
2650
2684
  };
2651
2685
 
2652
2686
  // A value that can HARD-STOP a run, so a lane prints the gate without deciding
@@ -3057,6 +3091,18 @@ const LANE_CALLS = {
3057
3091
  never: "never merge `.claude/orc.config.yaml` yourself, and never re-derive a precedence — the answer already carries it",
3058
3092
  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"],
3059
3093
  },
3094
+ "run-inflight": {
3095
+ cmd: "orc run inflight [--json]",
3096
+ what: "is a dispatch from this run still alive — the ONE reader of the trace's pending sidecar",
3097
+ exits: { 0: "clear — provably nothing in flight", 1: "in-flight — at least one dispatch has not returned", 2: "unknown — cannot prove either way" },
3098
+ states: ["clear", "in-flight", "unknown"],
3099
+ cost: "free",
3100
+ when: "before ANY re-dispatch, requeue or repair round, and before the first dispatch of a resumed run",
3101
+ 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",
3102
+ canonical: "_shared/return-validation.md",
3103
+ never: "never read `clear` as proof that an AD-HOC dispatch finished — the hook writes no SPAWN for one, so no record exists",
3104
+ lanes: ["orc", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"],
3105
+ },
3060
3106
  "lane-phases": {
3061
3107
  cmd: "orc lane phases <lane> [--json]",
3062
3108
  what: "which SHARED phases this lane runs, in order — the file, the layers to read, and when",
@@ -8972,6 +9018,149 @@ function resume() {
8972
9018
  }
8973
9019
 
8974
9020
  // `orc run list` / `orc run show <slug|n>`
9021
+ // ── `orc run inflight` — is a previous dispatch still alive? (v1.2.0) ───────
9022
+ //
9023
+ // WHY THIS EXISTS. Claude Code's Task tool returning an error does NOT kill the
9024
+ // subagent behind it. The agent keeps running and keeps writing files. Every
9025
+ // lane's retry rule ("a broken return = a failure, re-dispatch") silently
9026
+ // assumed the opposite, so an interrupted turn produced a SECOND agent on the
9027
+ // same task while the first was still working. A graded run put THREE
9028
+ // `orc-executor-opus-5-low` agents on one task for 50m19s + 115m22s + 100m53s
9029
+ // — 266 minutes of Opus 5 for one authorised dispatch, all editing the same
9030
+ // files. The hook already recorded every one of them; nothing ever READ it.
9031
+ //
9032
+ // The pending sidecar (`<trace>.pending.json`, written by orc-trace.js on every
9033
+ // SPAWN) is the evidence. This command makes it authoritative:
9034
+ // `a lane that re-dispatches over a live attempt` has broken the contract in
9035
+ // `_shared/return-validation.md` §0.
9036
+ //
9037
+ // UNKNOWN IS NOT ZERO. A missing pointer, an unreadable sidecar or a record too
9038
+ // old to trust all exit 2 — never 0. "I cannot prove anything is running" and
9039
+ // "I proved nothing is running" are different facts, and re-dispatching on the
9040
+ // first one is exactly the bug. Only a readable sidecar that is EMPTY *and* a
9041
+ // trace whose SPAWN/RETURN counts agree earns exit 0.
9042
+ //
9043
+ // exit 0 clear — provably nothing in flight
9044
+ // exit 1 in-flight — >=1 dispatch has not returned
9045
+ // exit 2 unknown — cannot prove either way
9046
+ const INFLIGHT_STALE_MS = 6 * 60 * 60 * 1000;
9047
+
9048
+ function runInflightCmd(claudeDir) {
9049
+ const dir = resolveLogDir(claudeDir);
9050
+ const out = {
9051
+ ok: true,
9052
+ state: "unknown",
9053
+ reason: null,
9054
+ count: 0,
9055
+ entries: [],
9056
+ trace: null,
9057
+ lane: null,
9058
+ slug: null,
9059
+ log_dir: dir,
9060
+ sidecar: null,
9061
+ sidecar_readable: false,
9062
+ spawns: null,
9063
+ returns: null,
9064
+ balance_agrees: null,
9065
+ stale_entries: 0,
9066
+ };
9067
+
9068
+ const finish = (state, reason, code) => {
9069
+ out.state = state;
9070
+ out.reason = reason;
9071
+ if (wantsJson()) emitJson(out);
9072
+ else renderInflight(out);
9073
+ process.exit(code);
9074
+ };
9075
+
9076
+ // 1. The run pointer. No pointer = no open run we can reason about.
9077
+ let cur = null;
9078
+ try {
9079
+ cur = fs.readFileSync(path.join(dir, ".current"), "utf8").trim();
9080
+ } catch (_) {}
9081
+ if (!cur) return finish("unknown", "no trace pointer — no run is open, or the pointer was never written", 2);
9082
+ out.trace = cur;
9083
+ const m = TRACE_NAME.exec(cur);
9084
+ if (m) { out.lane = m[1]; out.slug = m[2]; }
9085
+
9086
+ // 2. The trace's own SPAWN/RETURN balance — an independent second opinion.
9087
+ // Counted from the hook's own skeleton lines only.
9088
+ const tracePath = path.join(dir, cur);
9089
+ try {
9090
+ const text = fs.readFileSync(tracePath, "utf8");
9091
+ out.spawns = (text.match(/\] hook\s+SPAWN /g) || []).length;
9092
+ const all = (text.match(/\] hook\s+RETURN /g) || []).length;
9093
+ const loose = (text.match(/\] hook\s+RETURN ~agent :: unattributed/g) || []).length;
9094
+ out.returns = all - loose;
9095
+ } catch (_) {
9096
+ return finish("unknown", `trace pointer names "${cur}" but it cannot be read`, 2);
9097
+ }
9098
+
9099
+ // 3. The pending sidecar — the record of what was dispatched and never closed.
9100
+ const side = path.join(dir, cur + ".pending.json");
9101
+ out.sidecar = side;
9102
+ let pend = null;
9103
+ try {
9104
+ const raw = JSON.parse(fs.readFileSync(side, "utf8"));
9105
+ if (Array.isArray(raw)) { pend = raw; out.sidecar_readable = true; }
9106
+ } catch (_) {}
9107
+
9108
+ const now = Date.now();
9109
+ if (pend) {
9110
+ out.entries = pend.map((r) => {
9111
+ const ts = typeof r.ts === "number" ? r.ts : null;
9112
+ const age = ts == null ? null : Math.round((now - ts) / 1000);
9113
+ return {
9114
+ agent: r.agent == null ? null : String(r.agent),
9115
+ desc: r.desc == null ? null : String(r.desc),
9116
+ started_ms: ts,
9117
+ age_s: age,
9118
+ stale: ts != null && now - ts > INFLIGHT_STALE_MS,
9119
+ };
9120
+ });
9121
+ out.count = out.entries.length;
9122
+ out.stale_entries = out.entries.filter((e) => e.stale).length;
9123
+ }
9124
+
9125
+ const balance = out.spawns != null && out.returns != null ? out.spawns - out.returns : null;
9126
+ out.balance_agrees = balance == null || pend == null ? null : balance === out.count;
9127
+
9128
+ // 4. Verdict. Conservative in every direction.
9129
+ if (!out.sidecar_readable) {
9130
+ if (balance != null && balance > 0)
9131
+ return finish("in-flight", `sidecar unreadable, but the trace shows ${balance} SPAWN(s) with no RETURN`, 1);
9132
+ return finish("unknown", "the pending sidecar is missing or unreadable — cannot prove a dispatch finished", 2);
9133
+ }
9134
+ if (out.count > 0) {
9135
+ if (out.stale_entries === out.count)
9136
+ return finish(
9137
+ "unknown",
9138
+ `${out.count} record(s), all older than 6h — the run probably died without closing them`,
9139
+ 2
9140
+ );
9141
+ return finish("in-flight", `${out.count} dispatch(es) have not returned`, 1);
9142
+ }
9143
+ // Sidecar empty. Only trust it when the trace agrees.
9144
+ if (balance != null && balance > 0)
9145
+ return finish("unknown", `sidecar is empty but the trace shows ${balance} unmatched SPAWN(s) — they disagree`, 2);
9146
+ return finish("clear", "no dispatch is in flight", 0);
9147
+ }
9148
+
9149
+ function renderInflight(o) {
9150
+ const head =
9151
+ o.state === "clear" ? "clear" : o.state === "in-flight" ? `IN FLIGHT (${o.count})` : "unknown";
9152
+ console.log(`${ui.color.bold("dispatch:")} ${head}`);
9153
+ console.log(` ${o.reason}`);
9154
+ if (o.trace) console.log(` trace ${o.trace}`);
9155
+ for (const e of o.entries) {
9156
+ const age = e.age_s == null ? "age unknown" : `${Math.floor(e.age_s / 60)}m${e.age_s % 60}s ago`;
9157
+ console.log(` - ${e.agent || "(unnamed)"} ${age}${e.stale ? " [stale]" : ""}`);
9158
+ if (e.desc) console.log(` ${e.desc}`);
9159
+ }
9160
+ if (o.state === "in-flight")
9161
+ console.log("\n Do NOT re-dispatch these tasks. A Task error does not kill the agent behind it.");
9162
+ }
9163
+
8975
9164
  function runCmd() {
8976
9165
  const claudeDir = resolveClaudeDir();
8977
9166
  const pos = positionals(); // ["run", <sub?>, <arg?>]
@@ -8982,6 +9171,9 @@ function runCmd() {
8982
9171
  // on disk. Neither deletes anything.
8983
9172
  if (sub === "close" || sub === "reopen") return runCloseCmd(claudeDir, runs, sub, pos[2]);
8984
9173
 
9174
+ // Read-only, and the ONE reader of the pending sidecar. 0 clear / 1 in-flight / 2 unknown.
9175
+ if (sub === "inflight") return runInflightCmd(claudeDir);
9176
+
8985
9177
  if (sub === "show") {
8986
9178
  const arg = pos[2];
8987
9179
  const pick = /^\d+$/.test(String(arg)) ? runs[Number(arg) - 1] : runs.find((r) => r.slug === arg);
@@ -32847,6 +33039,12 @@ Usage:
32847
33039
  orc run reopen <slug|n> put it back — it is waiting again [--json]
32848
33040
  orc stats [--since YYYY-MM-DD] [--json] how much you actually use each lane and agent, counted
32849
33041
  from the trace filenames — no model, instant, free
33042
+ orc wait the deterministic half of /orc-wait — a wait costs zero
33043
+ tokens, because a detached command does it, not a model
33044
+ orc wait lanes [--json] which lanes support a wait, what each one checkpoints,
33045
+ and where its safe point is
33046
+ orc wait plan <spec> [--json] turn 30 · 90m · 2h · until 18:41 · reset into hops
33047
+ [--hop <min>] [--max-hops <n>] (exit 0 planned / 1 unparsable / 2 no usage reading)
32850
33048
  orc onboarding [<topic>] guided walkthrough (menu on a TTY; prints all when piped)
32851
33049
  topics: overview, install, first-run, lanes,
32852
33050
  config, knowledge, upgrade, troubleshooting
@@ -32901,6 +33099,904 @@ update vs upgrade:
32901
33099
  Skills installed: ${listSkillNames().join(", ")}`);
32902
33100
  }
32903
33101
 
33102
+ // ── `orc wait` (v1.1.0 W1) ──────────────────────────────────────────────────
33103
+ // The deterministic half of `/orc-wait`. The skill never does this arithmetic
33104
+ // itself: a pipeline the model recomputes is a pipeline that drifts (the
33105
+ // `orc doc next` / Flow-stepper rule). Canonical prose is
33106
+ // `templates/skills/_shared/wait.md`, and WAIT_LANE_SHAPES below is the
33107
+ // machine-readable copy of its `## Which lanes support a wait` table — a golden
33108
+ // test compares the two IN BOTH DIRECTIONS, the EXTRA_LANE_SHAPES precedent.
33109
+ //
33110
+ // `checkpoint: "none"` is an ANSWER, not a gap: a single-dispatch lane has
33111
+ // nothing to checkpoint, so all three modes collapse to the same plain wait
33112
+ // there and this table says so rather than omitting the row.
33113
+ const WAIT_LANE_SHAPES = [
33114
+ { lane: "/orc", checkpoint: "full", safe_point: "wave or phase edge" },
33115
+ { lane: "/orc-ultra", checkpoint: "full", safe_point: "wave or judge gate" },
33116
+ { lane: "/orc-mini", checkpoint: "full", safe_point: "after the executor returns" },
33117
+ { lane: "/orc-fast", checkpoint: "full", safe_point: "after the executor returns" },
33118
+ { lane: "/orc-diy", checkpoint: "full", safe_point: "compiled phase edge" },
33119
+ { lane: "/orc-doc", checkpoint: "full", safe_point: "wave edge" },
33120
+ { lane: "/orc-wiki", checkpoint: "full", safe_point: "scan-task boundary" },
33121
+ { lane: "/orc-analyze", checkpoint: "full", safe_point: "after the analyst returns" },
33122
+ { lane: "/orc-poly", checkpoint: "docset", safe_point: "after a per-repo plan is written" },
33123
+ { lane: "/orc-quick", checkpoint: "entry", safe_point: "after an entry closes" },
33124
+ { lane: "/orc-challenge", checkpoint: "cycle", safe_point: "after a cycle records" },
33125
+ { lane: "/orc-brainstorm", checkpoint: "snapshot", safe_point: "phase edge" },
33126
+ { lane: "/orc-grill", checkpoint: "snapshot", safe_point: "round edge" },
33127
+ { lane: "/orc-learn", checkpoint: "none", safe_point: "single dispatch" },
33128
+ { lane: "/orc-plan", checkpoint: "none", safe_point: "single dispatch" },
33129
+ { lane: "/orc-verify", checkpoint: "none", safe_point: "single dispatch" },
33130
+ { lane: "/orc-pattern", checkpoint: "none", safe_point: "single dispatch" },
33131
+ { lane: "/orc-claude", checkpoint: "none", safe_point: "single dispatch" },
33132
+ { lane: "/orc-explain", checkpoint: "none", safe_point: "read-only, seconds long" },
33133
+ { lane: "/orc-route", checkpoint: "none", safe_point: "read-only, seconds long" },
33134
+ { lane: "/orc-boundary", checkpoint: "none", safe_point: "read-only, seconds long" },
33135
+ { lane: "/orc-budget", checkpoint: "none", safe_point: "read-only, seconds long" },
33136
+ { lane: "/orc-aftermath", checkpoint: "none", safe_point: "read-only, seconds long" },
33137
+ { lane: "/orc-export", checkpoint: "none", safe_point: "read-only, seconds long" },
33138
+ { lane: "/orc-retro", checkpoint: "none", safe_point: "read-only, seconds long" },
33139
+ { lane: "/orc-pact", checkpoint: "none", safe_point: "read-only, seconds long" },
33140
+ ];
33141
+ const WAIT_MODES = ["safe", "soft", "hard"];
33142
+ const WAIT_CHECKPOINTS = ["full", "docset", "entry", "cycle", "snapshot", "none"];
33143
+ const WAIT_HOP_DEFAULT_MIN = 30;
33144
+ const WAIT_MAX_HOPS_DEFAULT = 5;
33145
+ // The same freshness window the effort guard already uses for the session-model
33146
+ // bridge. A reading older than this is `unknown`, and unknown never blocks.
33147
+ const WAIT_BRIDGE_MAX_AGE_MS = 30 * 60 * 1000;
33148
+
33149
+ // The reader half of the statusline usage bridge. The WRITER lands in W4; until
33150
+ // then every caller correctly reports `unknown`, which is the honest answer and
33151
+ // never a stop. Unknown is not zero.
33152
+ function readUsageBridge(claudeDir, now) {
33153
+ const t = typeof now === "number" ? now : Date.now();
33154
+ const file = path.join(claudeDir, "orc", "usage.json");
33155
+ try {
33156
+ const j = JSON.parse(fs.readFileSync(file, "utf8"));
33157
+ if (!j || typeof j.written_at !== "number") return null;
33158
+ if (t - j.written_at > WAIT_BRIDGE_MAX_AGE_MS) return null;
33159
+ return j;
33160
+ } catch (_) {
33161
+ return null;
33162
+ }
33163
+ }
33164
+
33165
+ // `30` `90m` `2h` `2h30m` `until 18:41` `reset` → minutes.
33166
+ // Returns { ok, kind, minutes, ... } or { ok:false, reason }.
33167
+ function waitParseSpec(spec, now) {
33168
+ const t = typeof now === "number" ? now : Date.now();
33169
+ const s = String(spec == null ? "" : spec).trim().toLowerCase().replace(/\s+/g, " ");
33170
+ if (!s) return { ok: false, reason: "empty", hint: "say how long. Example: orc wait plan 30" };
33171
+ if (s === "reset") return { ok: true, kind: "reset", minutes: null };
33172
+ let m = /^until (\d{1,2}):(\d{2})$/.exec(s);
33173
+ if (m) {
33174
+ const hh = Number(m[1]);
33175
+ const mm = Number(m[2]);
33176
+ if (hh > 23 || mm > 59)
33177
+ return { ok: false, reason: "bad-time", hint: `${m[1]}:${m[2]} is not a time of day` };
33178
+ const d = new Date(t);
33179
+ const target = new Date(d.getFullYear(), d.getMonth(), d.getDate(), hh, mm, 0, 0);
33180
+ // A time already past today means the next one — never a negative wait.
33181
+ if (target.getTime() <= t) target.setDate(target.getDate() + 1);
33182
+ return {
33183
+ ok: true,
33184
+ kind: "until",
33185
+ minutes: Math.max(1, Math.round((target.getTime() - t) / 60000)),
33186
+ until: target.toISOString(),
33187
+ };
33188
+ }
33189
+ m = /^(\d+) ?h(?: ?(\d+) ?m?)?$/.exec(s);
33190
+ if (m) {
33191
+ const mins = Number(m[1]) * 60 + Number(m[2] || 0);
33192
+ return mins > 0
33193
+ ? { ok: true, kind: "duration", minutes: mins }
33194
+ : { ok: false, reason: "zero", hint: "a wait of zero is not a wait" };
33195
+ }
33196
+ m = /^(\d+) ?m$/.exec(s) || /^(\d+)$/.exec(s);
33197
+ if (m) {
33198
+ const mins = Number(m[1]);
33199
+ return mins > 0
33200
+ ? { ok: true, kind: "duration", minutes: mins }
33201
+ : { ok: false, reason: "zero", hint: "a wait of zero is not a wait" };
33202
+ }
33203
+ return {
33204
+ ok: false,
33205
+ reason: "unparsable",
33206
+ hint: "use 30 · 90m · 2h · 2h30m · until 18:41 · reset",
33207
+ };
33208
+ }
33209
+
33210
+ // The LAST hop is the remainder, never a full hop. A wait that overshoots its
33211
+ // own reset time has waited for nothing.
33212
+ function waitHops(totalMinutes, hopMinutes, maxHops) {
33213
+ const hop = Math.max(1, Number(hopMinutes) || WAIT_HOP_DEFAULT_MIN);
33214
+ const cap = Math.max(1, Number(maxHops) || WAIT_MAX_HOPS_DEFAULT);
33215
+ const hops = [];
33216
+ let left = Math.max(0, Math.round(Number(totalMinutes) || 0));
33217
+ while (left > 0 && hops.length < cap) {
33218
+ const h = Math.min(hop, left);
33219
+ hops.push(h);
33220
+ left -= h;
33221
+ }
33222
+ return { hops, uncovered_minutes: left, truncated: left > 0 };
33223
+ }
33224
+
33225
+ function waitPlanCmd(claudeDir) {
33226
+ const asJson = wantsJson();
33227
+ const pos = positionals().slice(2);
33228
+ const spec = pos.join(" ");
33229
+ const now = Date.now();
33230
+ const cfg = resolvedConfig(claudeDir);
33231
+ const hopMin = Number(flag("--hop")) || Number(cfg.wait_hop_minutes) || WAIT_HOP_DEFAULT_MIN;
33232
+ const maxHops = Number(flag("--max-hops")) || Number(cfg.wait_max_hops) || WAIT_MAX_HOPS_DEFAULT;
33233
+
33234
+ const parsed = waitParseSpec(spec, now);
33235
+ if (!parsed.ok) {
33236
+ if (asJson) emitJson({ ok: false, reason: parsed.reason, spec, hint: parsed.hint }, 1);
33237
+ console.error(ui.mark.err(`I cannot read "${spec}" as a wait.`));
33238
+ console.error(" " + parsed.hint);
33239
+ process.exit(1);
33240
+ }
33241
+
33242
+ let minutes = parsed.minutes;
33243
+ let source = parsed.kind;
33244
+ let reading = null;
33245
+ if (parsed.kind === "reset") {
33246
+ reading = readUsageBridge(claudeDir, now);
33247
+ const resets = reading && reading.five_hour && reading.five_hour.resets_at;
33248
+ const at = resets == null ? NaN : Number(resets) < 1e12 ? Number(resets) * 1000 : Number(resets);
33249
+ if (!Number.isFinite(at) || at <= now) {
33250
+ const why = reading
33251
+ ? "the reading has no usable reset time"
33252
+ : "there is no reading in the last 30 minutes";
33253
+ if (asJson)
33254
+ emitJson(
33255
+ { ok: false, reason: "no-reading", spec, hint: `${why}. Type a time instead. Example: orc wait plan 45` },
33256
+ 2
33257
+ );
33258
+ console.error(ui.mark.err("I cannot read the reset time."));
33259
+ console.error(" " + why + ".");
33260
+ console.error(" Type a time instead. Example: " + ui.color.cyan("orc wait plan 45"));
33261
+ process.exit(2);
33262
+ }
33263
+ minutes = Math.max(1, Math.round((at - now) / 60000));
33264
+ source = "reset";
33265
+ }
33266
+
33267
+ const h = waitHops(minutes, hopMin, maxHops);
33268
+ const endsAt = new Date(now + minutes * 60000);
33269
+ const out = {
33270
+ ok: true,
33271
+ spec,
33272
+ source,
33273
+ minutes,
33274
+ hop_minutes: hopMin,
33275
+ max_hops: maxHops,
33276
+ hops: h.hops,
33277
+ hop_count: h.hops.length,
33278
+ uncovered_minutes: h.uncovered_minutes,
33279
+ truncated: h.truncated,
33280
+ ends_at: endsAt.toISOString(),
33281
+ // A wait longer than the prompt cache TTL re-reads the whole context on the
33282
+ // next turn, at full input price. The caller must be able to say so.
33283
+ crosses_cache_ttl: minutes > 60,
33284
+ modes: WAIT_MODES,
33285
+ note: "A detached command does the waiting. No model runs, and no tokens are spent.",
33286
+ };
33287
+ if (asJson) emitJson(out, 0);
33288
+
33289
+ console.log(ui.header("ORC · wait — the plan"));
33290
+ console.log("");
33291
+ console.log(` ${ui.color.cyan("length")} ${minutes} min (from ${source})`);
33292
+ console.log(` ${ui.color.cyan("ends at")} ${endsAt.toTimeString().slice(0, 5)}`);
33293
+ console.log(` ${ui.color.cyan("hops")} ${h.hops.join(" + ")} (${h.hops.length} of ${maxHops} max)`);
33294
+ if (h.truncated)
33295
+ console.log(
33296
+ ui.mark.warn(` ${h.uncovered_minutes} min are NOT covered — wait_max_hops is ${maxHops}.`)
33297
+ );
33298
+ if (out.crosses_cache_ttl)
33299
+ console.log(
33300
+ ui.color.gray(
33301
+ " Longer than one hour: the prompt cache expires, so the next turn re-reads\n" +
33302
+ " your whole context at full input price. A fresh session is cheaper."
33303
+ )
33304
+ );
33305
+ console.log(ui.color.gray("\n " + out.note + "\n"));
33306
+ }
33307
+
33308
+ function waitLanesCmd() {
33309
+ const asJson = wantsJson();
33310
+ const lanes = WAIT_LANE_SHAPES.map((row) => {
33311
+ const plain = row.checkpoint === "none";
33312
+ return {
33313
+ lane: row.lane,
33314
+ checkpoint: row.checkpoint,
33315
+ safe_point: row.safe_point,
33316
+ // On a lane with nothing to checkpoint the three modes ARE the same
33317
+ // thing. Saying so is the point; pretending to a distinction is not.
33318
+ modes: WAIT_MODES,
33319
+ modes_differ: !plain,
33320
+ detail: plain
33321
+ ? "nothing to checkpoint — one dispatch, or a read. safe, soft and hard behave identically here."
33322
+ : `soft forces the ${row.checkpoint} checkpoint before it stops; hard writes RESUME.md only and can lose an in-flight return.`,
33323
+ };
33324
+ });
33325
+ if (asJson)
33326
+ emitJson(
33327
+ {
33328
+ ok: true,
33329
+ modes: WAIT_MODES,
33330
+ checkpoints: WAIT_CHECKPOINTS,
33331
+ lanes,
33332
+ note: "A lane not in this list does not support a wait.",
33333
+ },
33334
+ 0
33335
+ );
33336
+
33337
+ console.log(ui.header("ORC · wait — which lanes support a wait, and what they save"));
33338
+ console.log("");
33339
+ for (const l of lanes) {
33340
+ // Pad the PLAIN text, then colour it — an ANSI escape has width 0 on screen
33341
+ // and width 5+ to padEnd, so colouring first mis-aligns every row.
33342
+ const cp = l.checkpoint.padEnd(9);
33343
+ const mark = l.modes_differ ? ui.color.cyan(cp) : ui.color.gray(cp);
33344
+ console.log(` ${ui.color.cyan(l.lane.padEnd(17))} ${mark} ${l.safe_point}`);
33345
+ console.log(" " + ui.color.gray(l.detail));
33346
+ }
33347
+ console.log(ui.color.gray("\n A lane not listed here does not support a wait.\n"));
33348
+ }
33349
+
33350
+ // ── `orc usage check` (v1.1.0 W4) ───────────────────────────────────────────
33351
+ // THE one reader of the statusline's usage bridge. No skill reads that file, so
33352
+ // the threshold and the freshness rule exist in exactly one place.
33353
+ //
33354
+ // Exit codes are the contract, and the third one carries the whole design:
33355
+ // 0 ok — enough quota. Continue.
33356
+ // 1 low — at or below usage_stop_pct on the WORST window.
33357
+ // 2 unknown — no reading, or one older than the freshness window.
33358
+ //
33359
+ // UNKNOWN IS NOT LOW, and it never stops a run. An absent number is an absent
33360
+ // number: older Claude Code, no headers, or simply no statusline render since
33361
+ // the last dispatch. A gate that blocks on a missing reading is a gate people
33362
+ // switch off.
33363
+ function usageResetMs(v) {
33364
+ if (v == null) return NaN;
33365
+ const n = Number(v);
33366
+ if (Number.isFinite(n)) return n < 1e12 ? n * 1000 : n;
33367
+ const p = Date.parse(String(v));
33368
+ return Number.isFinite(p) ? p : NaN;
33369
+ }
33370
+
33371
+ // ── `orc usage report` — where the window went (v1.2.0) ─────────────────────
33372
+ //
33373
+ // `orc usage check` answers "is there room". This answers the question a user
33374
+ // actually asks mid-run: "how much has THIS session eaten, and what ate it".
33375
+ //
33376
+ // THE HONEST PART, and it decides the whole shape of this command. Claude Code
33377
+ // records NO token usage for a dispatched subagent — `isSidechain` is never set
33378
+ // and no sidechain message carries a usage block, in any transcript on disk. So
33379
+ // a per-executor TOKEN figure cannot be measured, and inventing one would be
33380
+ // the same class of bug as a fake validator. What IS measured, exactly, is WALL
33381
+ // TIME, from the trace hook's own SPAWN/RETURN lines. Foreign workers are the
33382
+ // one exception: `orc extra` records real four-kind vectors, so those rows
33383
+ // carry tokens and say so. Every other row reports `tokens: null` plus the
33384
+ // reason — never 0. Unknown is not zero.
33385
+ const USAGE_TOP_N = 5;
33386
+
33387
+ // "12m43s" | "1m7s" | "45s" → seconds. Anything else → null, never 0.
33388
+ function usageDurSeconds(s) {
33389
+ if (!s) return null;
33390
+ const m = /^(?:(\d+)m)?(\d+)s$/.exec(String(s).trim());
33391
+ if (!m) return null;
33392
+ return (m[1] ? Number(m[1]) * 60 : 0) + Number(m[2]);
33393
+ }
33394
+
33395
+ // Per-agent wall time for the run the trace pointer names. Returns null when
33396
+ // there is no open run — an absent trace is an absent measurement, not zero.
33397
+ function usageRunConsumers(claudeDir) {
33398
+ const dir = resolveLogDir(claudeDir);
33399
+ let cur = null;
33400
+ try {
33401
+ cur = fs.readFileSync(path.join(dir, ".current"), "utf8").trim();
33402
+ } catch (_) {}
33403
+ if (!cur) return null;
33404
+ let text = "";
33405
+ try {
33406
+ text = fs.readFileSync(path.join(dir, cur), "utf8");
33407
+ } catch (_) {
33408
+ return null;
33409
+ }
33410
+ const nameMatch = TRACE_NAME.exec(cur);
33411
+ const by = new Map();
33412
+ const bump = (agent, secs, running) => {
33413
+ const k = agent || "(unnamed)";
33414
+ const r = by.get(k) || { agent: k, dispatches: 0, wall_seconds: 0, running: 0, unmeasured: 0 };
33415
+ r.dispatches += 1;
33416
+ if (running) r.running += 1;
33417
+ if (secs == null) r.unmeasured += 1;
33418
+ else r.wall_seconds += secs;
33419
+ by.set(k, r);
33420
+ };
33421
+ for (const line of text.split("\n")) {
33422
+ // Only the hook's own skeleton lines. `~agent :: unattributed` is a
33423
+ // bookkeeping artefact of >=2 in flight and is never a dispatch.
33424
+ const r = /\] hook\s+RETURN ~?([^\s:]+) :: /.exec(line);
33425
+ if (!r) continue;
33426
+ if (r[1] === "agent") continue;
33427
+ const d = /\bdur=(\S+)/.exec(line);
33428
+ bump(r[1], usageDurSeconds(d && d[1]), false);
33429
+ }
33430
+ // Anything still open is real spend happening RIGHT NOW — the exact case the
33431
+ // v1.2.0 in-flight guard exists for, and the one a user most wants to see.
33432
+ let pending = [];
33433
+ try {
33434
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, cur + ".pending.json"), "utf8"));
33435
+ if (Array.isArray(raw)) pending = raw;
33436
+ } catch (_) {}
33437
+ const now = Date.now();
33438
+ for (const p of pending) {
33439
+ const secs = typeof p.ts === "number" ? Math.round((now - p.ts) / 1000) : null;
33440
+ bump(p.agent, secs, true);
33441
+ }
33442
+ return {
33443
+ trace: cur,
33444
+ lane: nameMatch ? nameMatch[1] : null,
33445
+ slug: nameMatch ? nameMatch[2] : null,
33446
+ agents: [...by.values()],
33447
+ in_flight: pending.length,
33448
+ };
33449
+ }
33450
+
33451
+ // Foreign dispatches DO carry measured tokens. Same file the spend report reads.
33452
+ function usageForeignSpend(claudeDir) {
33453
+ const f = path.join(claudeDir, "orc", "extra-spend.jsonl");
33454
+ let lines = [];
33455
+ try {
33456
+ lines = fs.readFileSync(f, "utf8").split("\n").filter(Boolean);
33457
+ } catch (_) {
33458
+ return { rows: [], unreadable: 0 };
33459
+ }
33460
+ const rows = [];
33461
+ let unreadable = 0;
33462
+ for (const l of lines) {
33463
+ try {
33464
+ rows.push(JSON.parse(l));
33465
+ } catch (_) {
33466
+ unreadable += 1;
33467
+ }
33468
+ }
33469
+ return { rows, unreadable };
33470
+ }
33471
+
33472
+ function usageReportCmd(claudeDir) {
33473
+ const asJson = wantsJson();
33474
+ const now = Date.now();
33475
+ const cfg = resolvedConfig(claudeDir);
33476
+ const stopPct = Math.min(50, Math.max(1, Number(cfg.usage_stop_pct) || 10));
33477
+
33478
+ // Read the bridge RAW here rather than through readUsageBridge: a stale
33479
+ // reading is still worth SHOWING, with its age, where a gate would rightly
33480
+ // discard it. The state word still comes from freshness — age is displayed,
33481
+ // never ignored.
33482
+ let raw = null;
33483
+ try {
33484
+ raw = JSON.parse(fs.readFileSync(path.join(claudeDir, "orc", "usage.json"), "utf8"));
33485
+ } catch (_) {}
33486
+ const ageMin =
33487
+ raw && typeof raw.written_at === "number" ? Math.round((now - raw.written_at) / 60000) : null;
33488
+ const stale = ageMin == null ? true : now - raw.written_at > WAIT_BRIDGE_MAX_AGE_MS;
33489
+
33490
+ const view = (o, label) => {
33491
+ if (!o || typeof o.used_percentage !== "number") return null;
33492
+ const used = Math.round(o.used_percentage);
33493
+ const at = usageResetMs(o.resets_at);
33494
+ return {
33495
+ window: label,
33496
+ used_percentage: used,
33497
+ remaining_percentage: Math.max(0, 100 - used),
33498
+ resets_at: Number.isFinite(at) ? new Date(at).toISOString() : null,
33499
+ resets_in_minutes: Number.isFinite(at) && at > now ? Math.round((at - now) / 60000) : null,
33500
+ low: 100 - used <= stopPct,
33501
+ };
33502
+ };
33503
+ const fh = raw ? view(raw.five_hour, "5h") : null;
33504
+ const sd = raw ? view(raw.seven_day, "wk") : null;
33505
+
33506
+ // Session consumption — the ledger the statusline keeps.
33507
+ let led = null;
33508
+ try {
33509
+ led = JSON.parse(fs.readFileSync(path.join(claudeDir, "orc", "usage-session.json"), "utf8"));
33510
+ } catch (_) {}
33511
+ const consumed = (w) =>
33512
+ !w
33513
+ ? null
33514
+ : {
33515
+ baseline_percentage: w.baseline,
33516
+ now_percentage: w.last,
33517
+ consumed_percentage: Math.max(0, w.accumulated + Math.max(0, w.last - w.baseline)),
33518
+ window_resets: w.resets,
33519
+ };
33520
+ const session = !led
33521
+ ? null
33522
+ : {
33523
+ session_id: led.session_id || null,
33524
+ started_at: led.started_at ? new Date(led.started_at).toISOString() : null,
33525
+ running_minutes: led.started_at ? Math.round((now - led.started_at) / 60000) : null,
33526
+ five_hour: consumed(led.five_hour),
33527
+ seven_day: consumed(led.seven_day),
33528
+ still_counting: true,
33529
+ // Never overclaim. The window is per ACCOUNT: a second Claude Code
33530
+ // window, a cloud session, or anyone else on the same key moves it too.
33531
+ caveat:
33532
+ "this is how far the window moved while this session ran — other sessions on the same account share it",
33533
+ };
33534
+
33535
+ const run = usageRunConsumers(claudeDir);
33536
+ const foreign = usageForeignSpend(claudeDir);
33537
+
33538
+ // Rank by measured wall time. A row with nothing measured keeps its slot.
33539
+ const top = [];
33540
+ if (run) {
33541
+ const sorted = run.agents.slice().sort((a, b) => b.wall_seconds - a.wall_seconds);
33542
+ for (const a of sorted.slice(0, USAGE_TOP_N))
33543
+ top.push({
33544
+ agent: a.agent,
33545
+ dispatches: a.dispatches,
33546
+ running: a.running,
33547
+ wall_seconds: a.wall_seconds,
33548
+ unmeasured_dispatches: a.unmeasured,
33549
+ tokens: null,
33550
+ tokens_source: "unavailable",
33551
+ });
33552
+ }
33553
+ for (const r of foreign.rows.slice(-USAGE_TOP_N)) {
33554
+ top.push({
33555
+ agent: "extra:" + (r.profile || "?") + "/" + (r.model || "?"),
33556
+ dispatches: 1,
33557
+ running: 0,
33558
+ wall_seconds: usageDurSeconds(r.dur),
33559
+ unmeasured_dispatches: 0,
33560
+ tokens: r.usage || null,
33561
+ tokens_source: r.usage ? "measured" : "not reported by the worker",
33562
+ });
33563
+ }
33564
+
33565
+ const windows = [fh, sd].filter(Boolean);
33566
+ const state = !windows.length || stale ? "unknown" : windows.some((w) => w.low) ? "low" : "ok";
33567
+ const code = state === "low" ? 1 : state === "unknown" ? 2 : 0;
33568
+ const out = {
33569
+ ok: true,
33570
+ state,
33571
+ five_hour: fh,
33572
+ seven_day: sd,
33573
+ context_used_percentage:
33574
+ raw && typeof raw.context_used_percentage === "number" ? raw.context_used_percentage : null,
33575
+ reading_age_minutes: ageMin,
33576
+ reading_stale: stale,
33577
+ stop_pct: stopPct,
33578
+ gate: String(cfg.usage_gate || "off"),
33579
+ session,
33580
+ run: run
33581
+ ? {
33582
+ trace: run.trace,
33583
+ lane: run.lane,
33584
+ slug: run.slug,
33585
+ dispatches: run.agents.reduce((n, a) => n + a.dispatches, 0),
33586
+ in_flight: run.in_flight,
33587
+ wall_seconds: run.agents.reduce((n, a) => n + a.wall_seconds, 0),
33588
+ }
33589
+ : null,
33590
+ top,
33591
+ foreign_spend_rows: foreign.rows.length,
33592
+ unreadable_spend_lines: foreign.unreadable,
33593
+ // Say it on every emission. A reader who does not know this reads a
33594
+ // wall-time ranking as a token ranking.
33595
+ tokens_note:
33596
+ "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.",
33597
+ };
33598
+ if (asJson) emitJson(out, code);
33599
+
33600
+ console.log(ui.header("ORC · usage"));
33601
+ console.log("");
33602
+ const pctLine = (w) =>
33603
+ !w
33604
+ ? ui.color.gray(" — (no reading)")
33605
+ : " " +
33606
+ String(w.used_percentage).padStart(3) +
33607
+ "% used · " +
33608
+ w.remaining_percentage +
33609
+ "% left" +
33610
+ (w.resets_in_minutes != null ? " · resets in " + w.resets_in_minutes + "m" : "") +
33611
+ (w.low ? " " + ui.color.yellow("LOW") : "");
33612
+ console.log(" 5 hours");
33613
+ console.log(pctLine(fh));
33614
+ console.log(" 7 days");
33615
+ console.log(pctLine(sd));
33616
+ if (out.context_used_percentage != null)
33617
+ console.log(" context " + out.context_used_percentage + "% of the window");
33618
+ if (stale)
33619
+ console.log(
33620
+ ui.color.gray(
33621
+ " reading " +
33622
+ (ageMin == null ? "none" : ageMin + "m old") +
33623
+ " — treated as unknown, and a run is never stopped on it"
33624
+ )
33625
+ );
33626
+ console.log("");
33627
+ if (session && session.five_hour) {
33628
+ const c = session.five_hour;
33629
+ console.log(
33630
+ " " +
33631
+ ui.color.bold(
33632
+ "This session has consumed " +
33633
+ c.consumed_percentage +
33634
+ "% of the 5-hour window and is still counting"
33635
+ )
33636
+ );
33637
+ console.log(
33638
+ ui.color.gray(
33639
+ " " +
33640
+ c.baseline_percentage +
33641
+ "% → " +
33642
+ c.now_percentage +
33643
+ "%" +
33644
+ (c.window_resets ? " across " + c.window_resets + " window reset(s)" : "") +
33645
+ (session.running_minutes != null ? " · " + session.running_minutes + "m in session" : "")
33646
+ )
33647
+ );
33648
+ console.log(ui.color.gray(" " + session.caveat));
33649
+ } else {
33650
+ console.log(ui.color.gray(" session consumption: no reading yet (the statusline writes it)"));
33651
+ }
33652
+ console.log("");
33653
+ if (top.length) {
33654
+ console.log(" " + ui.color.bold("Top " + USAGE_TOP_N + " by measured wall time"));
33655
+ for (const t of top.slice(0, USAGE_TOP_N)) {
33656
+ const secs = t.wall_seconds;
33657
+ const w =
33658
+ secs == null
33659
+ ? "—"
33660
+ : Math.floor(secs / 60) + "m" + String(secs % 60).padStart(2, "0") + "s";
33661
+ const tok = t.tokens ? "tokens measured" : ui.color.gray("tokens —");
33662
+ console.log(
33663
+ " " +
33664
+ w.padStart(8) +
33665
+ " " +
33666
+ t.agent +
33667
+ " " +
33668
+ ui.color.gray("x" + t.dispatches) +
33669
+ (t.running ? " " + ui.color.yellow(t.running + " RUNNING") : "") +
33670
+ " " +
33671
+ tok
33672
+ );
33673
+ }
33674
+ console.log(
33675
+ ui.color.gray(" wall time, not tokens — Claude Code does not record a subagent's tokens")
33676
+ );
33677
+ } else {
33678
+ console.log(ui.color.gray(" no dispatches in the open run (or no run is open)"));
33679
+ }
33680
+ process.exit(code);
33681
+ }
33682
+
33683
+ function usageCheckCmd(claudeDir) {
33684
+ const asJson = wantsJson();
33685
+ const now = Date.now();
33686
+ const cfg = resolvedConfig(claudeDir);
33687
+ const stopPct = Math.min(50, Math.max(1, Number(cfg.usage_stop_pct) || 10));
33688
+ const gate = String(cfg.usage_gate || "off");
33689
+ const j = readUsageBridge(claudeDir, now);
33690
+
33691
+ const view = (o, label) => {
33692
+ if (!o || typeof o.used_percentage !== "number") return null;
33693
+ const used = Math.round(o.used_percentage);
33694
+ const at = usageResetMs(o.resets_at);
33695
+ const mins = Number.isFinite(at) && at > now ? Math.round((at - now) / 60000) : null;
33696
+ return {
33697
+ window: label,
33698
+ used_percentage: used,
33699
+ remaining_percentage: Math.max(0, 100 - used),
33700
+ resets_at: Number.isFinite(at) ? new Date(at).toISOString() : null,
33701
+ resets_in_minutes: mins,
33702
+ low: 100 - used <= stopPct,
33703
+ };
33704
+ };
33705
+
33706
+ if (!j) {
33707
+ const out = {
33708
+ ok: true,
33709
+ state: "unknown",
33710
+ reason: "no reading in the last 30 minutes",
33711
+ five_hour: null,
33712
+ seven_day: null,
33713
+ context: null,
33714
+ stop_pct: stopPct,
33715
+ gate,
33716
+ // Say what unknown MEANS, every time. Silence here is what makes a user
33717
+ // believe the gate is watching when it is not.
33718
+ note: "unknown is not low — a run is never stopped on a missing reading. Claude Code before v2.1.80 sends no usage headers.",
33719
+ };
33720
+ if (asJson) emitJson(out, 2);
33721
+ console.log(ui.color.gray("usage: unknown (no reading in the last 30 minutes) · a run is never stopped on this"));
33722
+ process.exit(2);
33723
+ }
33724
+
33725
+ const fh = view(j.five_hour, "5h");
33726
+ const sd = view(j.seven_day, "wk");
33727
+ const windows = [fh, sd].filter(Boolean);
33728
+ if (!windows.length) {
33729
+ const out = { ok: true, state: "unknown", reason: "the reading carries no window", stop_pct: stopPct, gate };
33730
+ if (asJson) emitJson(out, 2);
33731
+ console.log(ui.color.gray("usage: unknown (the reading carries no window)"));
33732
+ process.exit(2);
33733
+ }
33734
+
33735
+ // THE WORST WINDOW DECIDES. A 7-day window at 96% with a 5-hour window at 20%
33736
+ // is not a green light: the run continues and dies twenty minutes later.
33737
+ const worst = windows.reduce((a, b) => (a.remaining_percentage <= b.remaining_percentage ? a : b));
33738
+ const low = windows.some((w) => w.low);
33739
+ const out = {
33740
+ ok: true,
33741
+ state: low ? "low" : "ok",
33742
+ five_hour: fh,
33743
+ seven_day: sd,
33744
+ worst: worst.window,
33745
+ context: typeof j.context_used_percentage === "number" ? j.context_used_percentage : null,
33746
+ stop_pct: stopPct,
33747
+ gate,
33748
+ reading_age_minutes: Math.round((now - j.written_at) / 60000),
33749
+ note: "read BEFORE a wave; the wave then spends tokens, so this is a reading and not a promise.",
33750
+ };
33751
+ if (asJson) emitJson(out, low ? 1 : 0);
33752
+
33753
+ const fmt = (w) =>
33754
+ w ? `${w.window} ${w.used_percentage}%${w.resets_in_minutes != null ? ` (${w.resets_in_minutes}m)` : ""}` : null;
33755
+ const line = [fmt(fh), fmt(sd)].filter(Boolean).join(" · ");
33756
+ console.log((low ? ui.mark.warn("usage: " + line) : ui.color.gray("usage: " + line)) +
33757
+ ui.color.gray(` · gate at ${stopPct}% left · ${gate}`));
33758
+ process.exit(low ? 1 : 0);
33759
+ }
33760
+
33761
+ // ── the wait's run state (v1.1.0 W2) ────────────────────────────────────────
33762
+ // ONE file, ONE writer — this CLI. The skill never writes it, so it can never
33763
+ // be behind the disk: the v0.49.5 hand-back lesson, and the fifth time this
33764
+ // repo has applied it (v0.32.0 narration · v0.53.2 spend log · v0.54.0 journal
33765
+ // · v1.0.0 W5 demotion).
33766
+ //
33767
+ // It lives BESIDE RESUME.md in {run_dir}/{slug}/, so it is deleted with the run
33768
+ // and can never outlive the decision it records.
33769
+ const WAIT_STATE_FILE = "wait.json";
33770
+
33771
+ function waitStatePath(claudeDir, slug) {
33772
+ return path.join(resolveRunDir(claudeDir), slug, WAIT_STATE_FILE);
33773
+ }
33774
+
33775
+ function readWaitState(claudeDir, slug) {
33776
+ try {
33777
+ const j = JSON.parse(fs.readFileSync(waitStatePath(claudeDir, slug), "utf8"));
33778
+ return j && typeof j === "object" ? j : null;
33779
+ } catch (_) {
33780
+ return null;
33781
+ }
33782
+ }
33783
+
33784
+ function writeWaitState(claudeDir, slug, patch) {
33785
+ const file = waitStatePath(claudeDir, slug);
33786
+ const cur = readWaitState(claudeDir, slug) || { v: 1, slug };
33787
+ const next = { ...cur, ...patch, v: 1, slug };
33788
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33789
+ fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
33790
+ return next;
33791
+ }
33792
+
33793
+ // The run this command is about. An explicit slug always wins; otherwise the
33794
+ // trace pointer names the run in flight. NEVER guess from "the newest folder" —
33795
+ // a wrong run is worse than no run, because the block would silently protect
33796
+ // something the user is not looking at.
33797
+ function waitResolveRun(claudeDir, slug) {
33798
+ if (slug) {
33799
+ const dir = path.join(resolveRunDir(claudeDir), slug);
33800
+ return fs.existsSync(dir) ? { slug, dir } : null;
33801
+ }
33802
+ const cur = extraCurrentRun(claudeDir);
33803
+ if (!cur || !cur.slug) return null;
33804
+ const dir = path.join(resolveRunDir(claudeDir), cur.slug);
33805
+ return fs.existsSync(dir) ? { slug: cur.slug, dir } : null;
33806
+ }
33807
+
33808
+ // CLI-COMPOSED AND CLI-WRITTEN. A wait that leaves no line cannot be counted,
33809
+ // and a block that leaves no line hides that a run continued through a gate on
33810
+ // the user's authority. Best effort ALWAYS: a trace that cannot be written must
33811
+ // never take the command down with it.
33812
+ function waitTraceLine(claudeDir, verb, tail) {
33813
+ try {
33814
+ const cur = extraCurrentRun(claudeDir);
33815
+ if (!cur || !cur.trace) return null;
33816
+ const dir = resolveLogDir(claudeDir);
33817
+ const d = new Date();
33818
+ const p = (n, w) => String(n).padStart(w || 2, "0");
33819
+ const stamp =
33820
+ `${p(d.getDate())}${p(d.getMonth() + 1)}${p(d.getFullYear() % 100)} ` +
33821
+ `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
33822
+ const line = `[${stamp}] ${"cli".padEnd(8)} ${verb}${tail ? ` :: ${tail}` : ""}\n`;
33823
+ fs.appendFileSync(path.join(dir, cur.trace), line);
33824
+ return line.trimEnd();
33825
+ } catch (_) {
33826
+ return null;
33827
+ }
33828
+ }
33829
+
33830
+ function waitBlockCmd(claudeDir, unblock) {
33831
+ const asJson = wantsJson();
33832
+ const slug = positionals()[2];
33833
+ const reason = typeof flag("--reason") === "string" ? String(flag("--reason")).trim() : "";
33834
+ const run = waitResolveRun(claudeDir, slug);
33835
+
33836
+ if (!run) {
33837
+ const hint = slug
33838
+ ? `no run named "${slug}" under ${resolveRunDir(claudeDir)}`
33839
+ : "no run is in flight, and no slug was given";
33840
+ if (asJson) emitJson({ ok: false, reason: "no-run", slug: slug || null, hint }, 2);
33841
+ console.error(ui.mark.err("I cannot find the run this is about."));
33842
+ console.error(" " + hint + ".");
33843
+ process.exit(2);
33844
+ }
33845
+
33846
+ if (unblock) {
33847
+ const cur = readWaitState(claudeDir, run.slug);
33848
+ if (!cur || !cur.blocked_at) {
33849
+ if (asJson) emitJson({ ok: false, reason: "not-blocked", slug: run.slug }, 1);
33850
+ console.error(ui.mark.warn(`${run.slug} is not blocked.`));
33851
+ process.exit(1);
33852
+ }
33853
+ const st = writeWaitState(claudeDir, run.slug, {
33854
+ blocked_at: null,
33855
+ block_reason: null,
33856
+ unblocked_at: new Date().toISOString(),
33857
+ });
33858
+ const trace = waitTraceLine(claudeDir, "WAIT", "unblock");
33859
+ if (asJson) emitJson({ ok: true, slug: run.slug, blocked: false, trace_line: trace }, 0);
33860
+ console.log(ui.mark.ok(`${run.slug}: the gate is live again.`));
33861
+ return;
33862
+ }
33863
+
33864
+ // THE REASON IS REQUIRED. A recorded reason is what makes the risk
33865
+ // demonstrably the user's — the same rule the run-close and doc-ship writers
33866
+ // already follow. Refuse BY NAME rather than storing an empty string.
33867
+ if (!reason) {
33868
+ if (asJson)
33869
+ emitJson(
33870
+ {
33871
+ ok: false,
33872
+ reason: "no-reason",
33873
+ slug: run.slug,
33874
+ hint: 'a block records why you accepted the risk. Pass --reason "<why>".',
33875
+ },
33876
+ 1
33877
+ );
33878
+ console.error(ui.mark.err("A block needs a reason."));
33879
+ console.error(' orc wait block ' + run.slug + ' --reason "window resets in 5m, task needs 10"');
33880
+ process.exit(1);
33881
+ }
33882
+
33883
+ const st = writeWaitState(claudeDir, run.slug, {
33884
+ blocked_at: new Date().toISOString(),
33885
+ block_reason: reason,
33886
+ unblocked_at: null,
33887
+ });
33888
+ const trace = waitTraceLine(claudeDir, "WAIT", `block reason="${reason}" by=user`);
33889
+ if (asJson)
33890
+ emitJson(
33891
+ { ok: true, slug: run.slug, blocked: true, blocked_at: st.blocked_at, reason, trace_line: trace },
33892
+ 0
33893
+ );
33894
+ console.log(ui.mark.warn(`${run.slug}: computed waits are blocked for the rest of this run.`));
33895
+ console.log(" reason " + reason);
33896
+ console.log(
33897
+ ui.color.gray(
33898
+ " The risk is yours: if the window empties mid-wave, the wave stops in the\n" +
33899
+ " middle. A typed `/orc-wait` still waits. `orc wait unblock` restores the gate."
33900
+ )
33901
+ );
33902
+ }
33903
+
33904
+ function waitCancelCmd(claudeDir) {
33905
+ const asJson = wantsJson();
33906
+ const run = waitResolveRun(claudeDir, positionals()[2]);
33907
+ if (!run) {
33908
+ if (asJson) emitJson({ ok: false, reason: "no-run", hint: "no run is in flight" }, 1);
33909
+ console.error(ui.mark.warn("No run is in flight, so there is no wait to cancel."));
33910
+ process.exit(1);
33911
+ }
33912
+ const cur = readWaitState(claudeDir, run.slug);
33913
+ if (!cur || !cur.wait_started_at || cur.wait_ended_at) {
33914
+ if (asJson) emitJson({ ok: false, reason: "no-wait", slug: run.slug }, 1);
33915
+ console.error(ui.mark.warn(`${run.slug} has no wait running.`));
33916
+ process.exit(1);
33917
+ }
33918
+ writeWaitState(claudeDir, run.slug, { cancel_requested_at: new Date().toISOString() });
33919
+ if (asJson) emitJson({ ok: true, slug: run.slug, cancelled: true }, 0);
33920
+ console.log(ui.mark.ok(`${run.slug}: the wait stops at the next hop.`));
33921
+ console.log(ui.color.gray(" The hand-back is already on disk, so nothing is lost either way."));
33922
+ }
33923
+
33924
+ function waitStatusCmd(claudeDir) {
33925
+ const asJson = wantsJson();
33926
+ const run = waitResolveRun(claudeDir, positionals()[2]);
33927
+ if (!run) {
33928
+ // An empty result is an ANSWER, so it still returns its object.
33929
+ if (asJson)
33930
+ emitJson({ ok: true, run: null, waiting: false, blocked: false, note: "no run is in flight" }, 1);
33931
+ console.log(ui.color.gray("No run is in flight."));
33932
+ process.exit(1);
33933
+ }
33934
+ const st = readWaitState(claudeDir, run.slug) || {};
33935
+ const blocked = !!st.blocked_at;
33936
+ const waiting = !!st.wait_started_at && !st.wait_ended_at;
33937
+ const ageMin = blocked ? Math.round((Date.now() - Date.parse(st.blocked_at)) / 60000) : null;
33938
+ const out = {
33939
+ ok: true,
33940
+ run: run.slug,
33941
+ waiting,
33942
+ mode: st.mode || null,
33943
+ hop: st.hop || null,
33944
+ hops_done: st.hops_done || 0,
33945
+ hops_planned: st.hops_planned || null,
33946
+ ends_at: st.ends_at || null,
33947
+ cancel_requested: !!st.cancel_requested_at,
33948
+ blocked,
33949
+ block_reason: blocked ? st.block_reason : null,
33950
+ blocked_at: st.blocked_at || null,
33951
+ // The AGE is what keeps an old block from applying invisibly — there is no
33952
+ // auto-expiry, because ORC does not decide a user's reason stopped being true.
33953
+ block_age_minutes: ageMin,
33954
+ };
33955
+ if (asJson) emitJson(out, 0);
33956
+
33957
+ console.log(ui.header("ORC · wait — " + run.slug));
33958
+ console.log("");
33959
+ console.log(
33960
+ ` ${ui.color.cyan("wait")} ` +
33961
+ (waiting
33962
+ ? `${st.mode || "?"} · hop ${out.hops_done} of ${out.hops_planned || "?"} · ends ${out.ends_at || "?"}`
33963
+ : ui.color.gray("none"))
33964
+ );
33965
+ console.log(
33966
+ ` ${ui.color.cyan("block")} ` +
33967
+ (blocked ? ui.mark.warn(`${ageMin}m ago — "${st.block_reason}"`) : ui.color.gray("not blocked"))
33968
+ );
33969
+ if (out.cancel_requested) console.log(ui.color.gray(" a cancel is pending — the wait ends at the next hop"));
33970
+ console.log("");
33971
+ }
33972
+
33973
+ function waitCmd() {
33974
+ const claudeDir = resolveClaudeDir();
33975
+ const sub = positionals()[1];
33976
+ switch (sub) {
33977
+ case "lanes":
33978
+ return waitLanesCmd();
33979
+ case "plan":
33980
+ return waitPlanCmd(claudeDir);
33981
+ case "status":
33982
+ return waitStatusCmd(claudeDir);
33983
+ case "block":
33984
+ return waitBlockCmd(claudeDir, false);
33985
+ case "unblock":
33986
+ return waitBlockCmd(claudeDir, true);
33987
+ case "cancel":
33988
+ return waitCancelCmd(claudeDir);
33989
+ default:
33990
+ console.error(`usage: orc wait lanes [--json]
33991
+ orc wait plan <30|90m|2h|until 18:41|reset> [--hop <min>] [--max-hops <n>] [--json]
33992
+ orc wait status [<slug>] [--json]
33993
+ orc wait block <slug> --reason "<why>" [--json]
33994
+ orc wait unblock [<slug>] [--json]
33995
+ orc wait cancel [<slug>] [--json]`);
33996
+ process.exit(1);
33997
+ }
33998
+ }
33999
+
32904
34000
  // ── the `--json` crash envelope (v0.49.2) ───────────────────────────────────
32905
34001
  // A read asked for JSON must answer in JSON or not at all. Before this, an
32906
34002
  // unexpected throw inside any `--json` route printed a Node stack to stderr and
@@ -33007,6 +34103,20 @@ function jsonCrash(err) {
33007
34103
  case "extra":
33008
34104
  await extra();
33009
34105
  break;
34106
+ // v1.1.0 W1 — the deterministic half of `/orc-wait`. The skill asks; it
34107
+ // never computes hops or the lane table itself.
34108
+ case "wait":
34109
+ waitCmd();
34110
+ break;
34111
+ // v1.1.0 W4 — the ONE reader of the statusline's usage bridge.
34112
+ case "usage":
34113
+ if (positionals()[1] === "check") usageCheckCmd(resolveClaudeDir());
34114
+ else if (positionals()[1] === "report") usageReportCmd(resolveClaudeDir());
34115
+ else {
34116
+ console.error("usage: orc usage check|report [--json]");
34117
+ process.exit(1);
34118
+ }
34119
+ break;
33010
34120
  case "ui":
33011
34121
  uiCmd();
33012
34122
  break;