@azure-id/orc 1.0.0 → 1.1.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.
- package/CHANGELOG.md +2437 -2367
- package/README.md +694 -631
- package/bin/cli.js +639 -0
- package/bin/verify-contracts.js +74 -0
- package/bin/verify-package.js +568 -563
- package/bin/webui/api.js +15 -0
- package/bin/webui/app.html +210 -207
- package/bin/webui/css/panels/wait.css +123 -0
- package/bin/webui/fixtures/index.js +7 -0
- package/bin/webui/fixtures/wait.js +97 -0
- package/bin/webui/i18n/en/nav.json +21 -20
- package/bin/webui/i18n/en/wait.json +41 -0
- package/bin/webui/i18n/id/nav.json +21 -20
- package/bin/webui/i18n/id/wait.json +41 -0
- package/bin/webui/js/01-i18n.js +151 -150
- package/bin/webui/js/panels/wait.js +253 -0
- package/package.json +1 -1
- package/templates/commands/orc-wait.md +19 -0
- package/templates/hooks/orc-statusline.js +39 -0
- package/templates/skills/_shared/phases/preflight.md +22 -0
- package/templates/skills/_shared/wait.md +240 -0
- package/templates/skills/orc/SKILL.md +6 -1
- package/templates/skills/orc-aftermath/SKILL.md +6 -1
- package/templates/skills/orc-analyze/SKILL.md +6 -1
- package/templates/skills/orc-boundary/SKILL.md +6 -1
- package/templates/skills/orc-brainstorm/SKILL.md +6 -1
- package/templates/skills/orc-budget/SKILL.md +6 -1
- package/templates/skills/orc-challenge/SKILL.md +6 -1
- package/templates/skills/orc-claude/SKILL.md +6 -1
- package/templates/skills/orc-diy/SKILL.md +6 -1
- package/templates/skills/orc-doc/SKILL.md +6 -1
- package/templates/skills/orc-explain/SKILL.md +5 -0
- package/templates/skills/orc-export/SKILL.md +5 -0
- package/templates/skills/orc-fast/SKILL.md +6 -1
- package/templates/skills/orc-grill/SKILL.md +6 -1
- package/templates/skills/orc-learn/SKILL.md +6 -1
- package/templates/skills/orc-mini/SKILL.md +6 -1
- package/templates/skills/orc-pact/SKILL.md +6 -1
- package/templates/skills/orc-pattern/SKILL.md +6 -1
- package/templates/skills/orc-poly/SKILL.md +6 -1
- package/templates/skills/orc-quick/SKILL.md +6 -1
- package/templates/skills/orc-retro/SKILL.md +6 -1
- package/templates/skills/orc-route/SKILL.md +6 -1
- package/templates/skills/orc-verify/SKILL.md +6 -1
- package/templates/skills/orc-wait/SKILL.md +163 -0
- package/templates/skills/orc-wiki/SKILL.md +6 -1
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
|
|
@@ -32847,6 +32881,12 @@ Usage:
|
|
|
32847
32881
|
orc run reopen <slug|n> put it back — it is waiting again [--json]
|
|
32848
32882
|
orc stats [--since YYYY-MM-DD] [--json] how much you actually use each lane and agent, counted
|
|
32849
32883
|
from the trace filenames — no model, instant, free
|
|
32884
|
+
orc wait the deterministic half of /orc-wait — a wait costs zero
|
|
32885
|
+
tokens, because a detached command does it, not a model
|
|
32886
|
+
orc wait lanes [--json] which lanes support a wait, what each one checkpoints,
|
|
32887
|
+
and where its safe point is
|
|
32888
|
+
orc wait plan <spec> [--json] turn 30 · 90m · 2h · until 18:41 · reset into hops
|
|
32889
|
+
[--hop <min>] [--max-hops <n>] (exit 0 planned / 1 unparsable / 2 no usage reading)
|
|
32850
32890
|
orc onboarding [<topic>] guided walkthrough (menu on a TTY; prints all when piped)
|
|
32851
32891
|
topics: overview, install, first-run, lanes,
|
|
32852
32892
|
config, knowledge, upgrade, troubleshooting
|
|
@@ -32901,6 +32941,592 @@ update vs upgrade:
|
|
|
32901
32941
|
Skills installed: ${listSkillNames().join(", ")}`);
|
|
32902
32942
|
}
|
|
32903
32943
|
|
|
32944
|
+
// ── `orc wait` (v1.1.0 W1) ──────────────────────────────────────────────────
|
|
32945
|
+
// The deterministic half of `/orc-wait`. The skill never does this arithmetic
|
|
32946
|
+
// itself: a pipeline the model recomputes is a pipeline that drifts (the
|
|
32947
|
+
// `orc doc next` / Flow-stepper rule). Canonical prose is
|
|
32948
|
+
// `templates/skills/_shared/wait.md`, and WAIT_LANE_SHAPES below is the
|
|
32949
|
+
// machine-readable copy of its `## Which lanes support a wait` table — a golden
|
|
32950
|
+
// test compares the two IN BOTH DIRECTIONS, the EXTRA_LANE_SHAPES precedent.
|
|
32951
|
+
//
|
|
32952
|
+
// `checkpoint: "none"` is an ANSWER, not a gap: a single-dispatch lane has
|
|
32953
|
+
// nothing to checkpoint, so all three modes collapse to the same plain wait
|
|
32954
|
+
// there and this table says so rather than omitting the row.
|
|
32955
|
+
const WAIT_LANE_SHAPES = [
|
|
32956
|
+
{ lane: "/orc", checkpoint: "full", safe_point: "wave or phase edge" },
|
|
32957
|
+
{ lane: "/orc-ultra", checkpoint: "full", safe_point: "wave or judge gate" },
|
|
32958
|
+
{ lane: "/orc-mini", checkpoint: "full", safe_point: "after the executor returns" },
|
|
32959
|
+
{ lane: "/orc-fast", checkpoint: "full", safe_point: "after the executor returns" },
|
|
32960
|
+
{ lane: "/orc-diy", checkpoint: "full", safe_point: "compiled phase edge" },
|
|
32961
|
+
{ lane: "/orc-doc", checkpoint: "full", safe_point: "wave edge" },
|
|
32962
|
+
{ lane: "/orc-wiki", checkpoint: "full", safe_point: "scan-task boundary" },
|
|
32963
|
+
{ lane: "/orc-analyze", checkpoint: "full", safe_point: "after the analyst returns" },
|
|
32964
|
+
{ lane: "/orc-poly", checkpoint: "docset", safe_point: "after a per-repo plan is written" },
|
|
32965
|
+
{ lane: "/orc-quick", checkpoint: "entry", safe_point: "after an entry closes" },
|
|
32966
|
+
{ lane: "/orc-challenge", checkpoint: "cycle", safe_point: "after a cycle records" },
|
|
32967
|
+
{ lane: "/orc-brainstorm", checkpoint: "snapshot", safe_point: "phase edge" },
|
|
32968
|
+
{ lane: "/orc-grill", checkpoint: "snapshot", safe_point: "round edge" },
|
|
32969
|
+
{ lane: "/orc-learn", checkpoint: "none", safe_point: "single dispatch" },
|
|
32970
|
+
{ lane: "/orc-plan", checkpoint: "none", safe_point: "single dispatch" },
|
|
32971
|
+
{ lane: "/orc-verify", checkpoint: "none", safe_point: "single dispatch" },
|
|
32972
|
+
{ lane: "/orc-pattern", checkpoint: "none", safe_point: "single dispatch" },
|
|
32973
|
+
{ lane: "/orc-claude", checkpoint: "none", safe_point: "single dispatch" },
|
|
32974
|
+
{ lane: "/orc-explain", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32975
|
+
{ lane: "/orc-route", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32976
|
+
{ lane: "/orc-boundary", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32977
|
+
{ lane: "/orc-budget", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32978
|
+
{ lane: "/orc-aftermath", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32979
|
+
{ lane: "/orc-export", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32980
|
+
{ lane: "/orc-retro", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32981
|
+
{ lane: "/orc-pact", checkpoint: "none", safe_point: "read-only, seconds long" },
|
|
32982
|
+
];
|
|
32983
|
+
const WAIT_MODES = ["safe", "soft", "hard"];
|
|
32984
|
+
const WAIT_CHECKPOINTS = ["full", "docset", "entry", "cycle", "snapshot", "none"];
|
|
32985
|
+
const WAIT_HOP_DEFAULT_MIN = 30;
|
|
32986
|
+
const WAIT_MAX_HOPS_DEFAULT = 5;
|
|
32987
|
+
// The same freshness window the effort guard already uses for the session-model
|
|
32988
|
+
// bridge. A reading older than this is `unknown`, and unknown never blocks.
|
|
32989
|
+
const WAIT_BRIDGE_MAX_AGE_MS = 30 * 60 * 1000;
|
|
32990
|
+
|
|
32991
|
+
// The reader half of the statusline usage bridge. The WRITER lands in W4; until
|
|
32992
|
+
// then every caller correctly reports `unknown`, which is the honest answer and
|
|
32993
|
+
// never a stop. Unknown is not zero.
|
|
32994
|
+
function readUsageBridge(claudeDir, now) {
|
|
32995
|
+
const t = typeof now === "number" ? now : Date.now();
|
|
32996
|
+
const file = path.join(claudeDir, "orc", "usage.json");
|
|
32997
|
+
try {
|
|
32998
|
+
const j = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
32999
|
+
if (!j || typeof j.written_at !== "number") return null;
|
|
33000
|
+
if (t - j.written_at > WAIT_BRIDGE_MAX_AGE_MS) return null;
|
|
33001
|
+
return j;
|
|
33002
|
+
} catch (_) {
|
|
33003
|
+
return null;
|
|
33004
|
+
}
|
|
33005
|
+
}
|
|
33006
|
+
|
|
33007
|
+
// `30` `90m` `2h` `2h30m` `until 18:41` `reset` → minutes.
|
|
33008
|
+
// Returns { ok, kind, minutes, ... } or { ok:false, reason }.
|
|
33009
|
+
function waitParseSpec(spec, now) {
|
|
33010
|
+
const t = typeof now === "number" ? now : Date.now();
|
|
33011
|
+
const s = String(spec == null ? "" : spec).trim().toLowerCase().replace(/\s+/g, " ");
|
|
33012
|
+
if (!s) return { ok: false, reason: "empty", hint: "say how long. Example: orc wait plan 30" };
|
|
33013
|
+
if (s === "reset") return { ok: true, kind: "reset", minutes: null };
|
|
33014
|
+
let m = /^until (\d{1,2}):(\d{2})$/.exec(s);
|
|
33015
|
+
if (m) {
|
|
33016
|
+
const hh = Number(m[1]);
|
|
33017
|
+
const mm = Number(m[2]);
|
|
33018
|
+
if (hh > 23 || mm > 59)
|
|
33019
|
+
return { ok: false, reason: "bad-time", hint: `${m[1]}:${m[2]} is not a time of day` };
|
|
33020
|
+
const d = new Date(t);
|
|
33021
|
+
const target = new Date(d.getFullYear(), d.getMonth(), d.getDate(), hh, mm, 0, 0);
|
|
33022
|
+
// A time already past today means the next one — never a negative wait.
|
|
33023
|
+
if (target.getTime() <= t) target.setDate(target.getDate() + 1);
|
|
33024
|
+
return {
|
|
33025
|
+
ok: true,
|
|
33026
|
+
kind: "until",
|
|
33027
|
+
minutes: Math.max(1, Math.round((target.getTime() - t) / 60000)),
|
|
33028
|
+
until: target.toISOString(),
|
|
33029
|
+
};
|
|
33030
|
+
}
|
|
33031
|
+
m = /^(\d+) ?h(?: ?(\d+) ?m?)?$/.exec(s);
|
|
33032
|
+
if (m) {
|
|
33033
|
+
const mins = Number(m[1]) * 60 + Number(m[2] || 0);
|
|
33034
|
+
return mins > 0
|
|
33035
|
+
? { ok: true, kind: "duration", minutes: mins }
|
|
33036
|
+
: { ok: false, reason: "zero", hint: "a wait of zero is not a wait" };
|
|
33037
|
+
}
|
|
33038
|
+
m = /^(\d+) ?m$/.exec(s) || /^(\d+)$/.exec(s);
|
|
33039
|
+
if (m) {
|
|
33040
|
+
const mins = Number(m[1]);
|
|
33041
|
+
return mins > 0
|
|
33042
|
+
? { ok: true, kind: "duration", minutes: mins }
|
|
33043
|
+
: { ok: false, reason: "zero", hint: "a wait of zero is not a wait" };
|
|
33044
|
+
}
|
|
33045
|
+
return {
|
|
33046
|
+
ok: false,
|
|
33047
|
+
reason: "unparsable",
|
|
33048
|
+
hint: "use 30 · 90m · 2h · 2h30m · until 18:41 · reset",
|
|
33049
|
+
};
|
|
33050
|
+
}
|
|
33051
|
+
|
|
33052
|
+
// The LAST hop is the remainder, never a full hop. A wait that overshoots its
|
|
33053
|
+
// own reset time has waited for nothing.
|
|
33054
|
+
function waitHops(totalMinutes, hopMinutes, maxHops) {
|
|
33055
|
+
const hop = Math.max(1, Number(hopMinutes) || WAIT_HOP_DEFAULT_MIN);
|
|
33056
|
+
const cap = Math.max(1, Number(maxHops) || WAIT_MAX_HOPS_DEFAULT);
|
|
33057
|
+
const hops = [];
|
|
33058
|
+
let left = Math.max(0, Math.round(Number(totalMinutes) || 0));
|
|
33059
|
+
while (left > 0 && hops.length < cap) {
|
|
33060
|
+
const h = Math.min(hop, left);
|
|
33061
|
+
hops.push(h);
|
|
33062
|
+
left -= h;
|
|
33063
|
+
}
|
|
33064
|
+
return { hops, uncovered_minutes: left, truncated: left > 0 };
|
|
33065
|
+
}
|
|
33066
|
+
|
|
33067
|
+
function waitPlanCmd(claudeDir) {
|
|
33068
|
+
const asJson = wantsJson();
|
|
33069
|
+
const pos = positionals().slice(2);
|
|
33070
|
+
const spec = pos.join(" ");
|
|
33071
|
+
const now = Date.now();
|
|
33072
|
+
const cfg = resolvedConfig(claudeDir);
|
|
33073
|
+
const hopMin = Number(flag("--hop")) || Number(cfg.wait_hop_minutes) || WAIT_HOP_DEFAULT_MIN;
|
|
33074
|
+
const maxHops = Number(flag("--max-hops")) || Number(cfg.wait_max_hops) || WAIT_MAX_HOPS_DEFAULT;
|
|
33075
|
+
|
|
33076
|
+
const parsed = waitParseSpec(spec, now);
|
|
33077
|
+
if (!parsed.ok) {
|
|
33078
|
+
if (asJson) emitJson({ ok: false, reason: parsed.reason, spec, hint: parsed.hint }, 1);
|
|
33079
|
+
console.error(ui.mark.err(`I cannot read "${spec}" as a wait.`));
|
|
33080
|
+
console.error(" " + parsed.hint);
|
|
33081
|
+
process.exit(1);
|
|
33082
|
+
}
|
|
33083
|
+
|
|
33084
|
+
let minutes = parsed.minutes;
|
|
33085
|
+
let source = parsed.kind;
|
|
33086
|
+
let reading = null;
|
|
33087
|
+
if (parsed.kind === "reset") {
|
|
33088
|
+
reading = readUsageBridge(claudeDir, now);
|
|
33089
|
+
const resets = reading && reading.five_hour && reading.five_hour.resets_at;
|
|
33090
|
+
const at = resets == null ? NaN : Number(resets) < 1e12 ? Number(resets) * 1000 : Number(resets);
|
|
33091
|
+
if (!Number.isFinite(at) || at <= now) {
|
|
33092
|
+
const why = reading
|
|
33093
|
+
? "the reading has no usable reset time"
|
|
33094
|
+
: "there is no reading in the last 30 minutes";
|
|
33095
|
+
if (asJson)
|
|
33096
|
+
emitJson(
|
|
33097
|
+
{ ok: false, reason: "no-reading", spec, hint: `${why}. Type a time instead. Example: orc wait plan 45` },
|
|
33098
|
+
2
|
|
33099
|
+
);
|
|
33100
|
+
console.error(ui.mark.err("I cannot read the reset time."));
|
|
33101
|
+
console.error(" " + why + ".");
|
|
33102
|
+
console.error(" Type a time instead. Example: " + ui.color.cyan("orc wait plan 45"));
|
|
33103
|
+
process.exit(2);
|
|
33104
|
+
}
|
|
33105
|
+
minutes = Math.max(1, Math.round((at - now) / 60000));
|
|
33106
|
+
source = "reset";
|
|
33107
|
+
}
|
|
33108
|
+
|
|
33109
|
+
const h = waitHops(minutes, hopMin, maxHops);
|
|
33110
|
+
const endsAt = new Date(now + minutes * 60000);
|
|
33111
|
+
const out = {
|
|
33112
|
+
ok: true,
|
|
33113
|
+
spec,
|
|
33114
|
+
source,
|
|
33115
|
+
minutes,
|
|
33116
|
+
hop_minutes: hopMin,
|
|
33117
|
+
max_hops: maxHops,
|
|
33118
|
+
hops: h.hops,
|
|
33119
|
+
hop_count: h.hops.length,
|
|
33120
|
+
uncovered_minutes: h.uncovered_minutes,
|
|
33121
|
+
truncated: h.truncated,
|
|
33122
|
+
ends_at: endsAt.toISOString(),
|
|
33123
|
+
// A wait longer than the prompt cache TTL re-reads the whole context on the
|
|
33124
|
+
// next turn, at full input price. The caller must be able to say so.
|
|
33125
|
+
crosses_cache_ttl: minutes > 60,
|
|
33126
|
+
modes: WAIT_MODES,
|
|
33127
|
+
note: "A detached command does the waiting. No model runs, and no tokens are spent.",
|
|
33128
|
+
};
|
|
33129
|
+
if (asJson) emitJson(out, 0);
|
|
33130
|
+
|
|
33131
|
+
console.log(ui.header("ORC · wait — the plan"));
|
|
33132
|
+
console.log("");
|
|
33133
|
+
console.log(` ${ui.color.cyan("length")} ${minutes} min (from ${source})`);
|
|
33134
|
+
console.log(` ${ui.color.cyan("ends at")} ${endsAt.toTimeString().slice(0, 5)}`);
|
|
33135
|
+
console.log(` ${ui.color.cyan("hops")} ${h.hops.join(" + ")} (${h.hops.length} of ${maxHops} max)`);
|
|
33136
|
+
if (h.truncated)
|
|
33137
|
+
console.log(
|
|
33138
|
+
ui.mark.warn(` ${h.uncovered_minutes} min are NOT covered — wait_max_hops is ${maxHops}.`)
|
|
33139
|
+
);
|
|
33140
|
+
if (out.crosses_cache_ttl)
|
|
33141
|
+
console.log(
|
|
33142
|
+
ui.color.gray(
|
|
33143
|
+
" Longer than one hour: the prompt cache expires, so the next turn re-reads\n" +
|
|
33144
|
+
" your whole context at full input price. A fresh session is cheaper."
|
|
33145
|
+
)
|
|
33146
|
+
);
|
|
33147
|
+
console.log(ui.color.gray("\n " + out.note + "\n"));
|
|
33148
|
+
}
|
|
33149
|
+
|
|
33150
|
+
function waitLanesCmd() {
|
|
33151
|
+
const asJson = wantsJson();
|
|
33152
|
+
const lanes = WAIT_LANE_SHAPES.map((row) => {
|
|
33153
|
+
const plain = row.checkpoint === "none";
|
|
33154
|
+
return {
|
|
33155
|
+
lane: row.lane,
|
|
33156
|
+
checkpoint: row.checkpoint,
|
|
33157
|
+
safe_point: row.safe_point,
|
|
33158
|
+
// On a lane with nothing to checkpoint the three modes ARE the same
|
|
33159
|
+
// thing. Saying so is the point; pretending to a distinction is not.
|
|
33160
|
+
modes: WAIT_MODES,
|
|
33161
|
+
modes_differ: !plain,
|
|
33162
|
+
detail: plain
|
|
33163
|
+
? "nothing to checkpoint — one dispatch, or a read. safe, soft and hard behave identically here."
|
|
33164
|
+
: `soft forces the ${row.checkpoint} checkpoint before it stops; hard writes RESUME.md only and can lose an in-flight return.`,
|
|
33165
|
+
};
|
|
33166
|
+
});
|
|
33167
|
+
if (asJson)
|
|
33168
|
+
emitJson(
|
|
33169
|
+
{
|
|
33170
|
+
ok: true,
|
|
33171
|
+
modes: WAIT_MODES,
|
|
33172
|
+
checkpoints: WAIT_CHECKPOINTS,
|
|
33173
|
+
lanes,
|
|
33174
|
+
note: "A lane not in this list does not support a wait.",
|
|
33175
|
+
},
|
|
33176
|
+
0
|
|
33177
|
+
);
|
|
33178
|
+
|
|
33179
|
+
console.log(ui.header("ORC · wait — which lanes support a wait, and what they save"));
|
|
33180
|
+
console.log("");
|
|
33181
|
+
for (const l of lanes) {
|
|
33182
|
+
// Pad the PLAIN text, then colour it — an ANSI escape has width 0 on screen
|
|
33183
|
+
// and width 5+ to padEnd, so colouring first mis-aligns every row.
|
|
33184
|
+
const cp = l.checkpoint.padEnd(9);
|
|
33185
|
+
const mark = l.modes_differ ? ui.color.cyan(cp) : ui.color.gray(cp);
|
|
33186
|
+
console.log(` ${ui.color.cyan(l.lane.padEnd(17))} ${mark} ${l.safe_point}`);
|
|
33187
|
+
console.log(" " + ui.color.gray(l.detail));
|
|
33188
|
+
}
|
|
33189
|
+
console.log(ui.color.gray("\n A lane not listed here does not support a wait.\n"));
|
|
33190
|
+
}
|
|
33191
|
+
|
|
33192
|
+
// ── `orc usage check` (v1.1.0 W4) ───────────────────────────────────────────
|
|
33193
|
+
// THE one reader of the statusline's usage bridge. No skill reads that file, so
|
|
33194
|
+
// the threshold and the freshness rule exist in exactly one place.
|
|
33195
|
+
//
|
|
33196
|
+
// Exit codes are the contract, and the third one carries the whole design:
|
|
33197
|
+
// 0 ok — enough quota. Continue.
|
|
33198
|
+
// 1 low — at or below usage_stop_pct on the WORST window.
|
|
33199
|
+
// 2 unknown — no reading, or one older than the freshness window.
|
|
33200
|
+
//
|
|
33201
|
+
// UNKNOWN IS NOT LOW, and it never stops a run. An absent number is an absent
|
|
33202
|
+
// number: older Claude Code, no headers, or simply no statusline render since
|
|
33203
|
+
// the last dispatch. A gate that blocks on a missing reading is a gate people
|
|
33204
|
+
// switch off.
|
|
33205
|
+
function usageResetMs(v) {
|
|
33206
|
+
if (v == null) return NaN;
|
|
33207
|
+
const n = Number(v);
|
|
33208
|
+
if (Number.isFinite(n)) return n < 1e12 ? n * 1000 : n;
|
|
33209
|
+
const p = Date.parse(String(v));
|
|
33210
|
+
return Number.isFinite(p) ? p : NaN;
|
|
33211
|
+
}
|
|
33212
|
+
|
|
33213
|
+
function usageCheckCmd(claudeDir) {
|
|
33214
|
+
const asJson = wantsJson();
|
|
33215
|
+
const now = Date.now();
|
|
33216
|
+
const cfg = resolvedConfig(claudeDir);
|
|
33217
|
+
const stopPct = Math.min(50, Math.max(1, Number(cfg.usage_stop_pct) || 10));
|
|
33218
|
+
const gate = String(cfg.usage_gate || "off");
|
|
33219
|
+
const j = readUsageBridge(claudeDir, now);
|
|
33220
|
+
|
|
33221
|
+
const view = (o, label) => {
|
|
33222
|
+
if (!o || typeof o.used_percentage !== "number") return null;
|
|
33223
|
+
const used = Math.round(o.used_percentage);
|
|
33224
|
+
const at = usageResetMs(o.resets_at);
|
|
33225
|
+
const mins = Number.isFinite(at) && at > now ? Math.round((at - now) / 60000) : null;
|
|
33226
|
+
return {
|
|
33227
|
+
window: label,
|
|
33228
|
+
used_percentage: used,
|
|
33229
|
+
remaining_percentage: Math.max(0, 100 - used),
|
|
33230
|
+
resets_at: Number.isFinite(at) ? new Date(at).toISOString() : null,
|
|
33231
|
+
resets_in_minutes: mins,
|
|
33232
|
+
low: 100 - used <= stopPct,
|
|
33233
|
+
};
|
|
33234
|
+
};
|
|
33235
|
+
|
|
33236
|
+
if (!j) {
|
|
33237
|
+
const out = {
|
|
33238
|
+
ok: true,
|
|
33239
|
+
state: "unknown",
|
|
33240
|
+
reason: "no reading in the last 30 minutes",
|
|
33241
|
+
five_hour: null,
|
|
33242
|
+
seven_day: null,
|
|
33243
|
+
context: null,
|
|
33244
|
+
stop_pct: stopPct,
|
|
33245
|
+
gate,
|
|
33246
|
+
// Say what unknown MEANS, every time. Silence here is what makes a user
|
|
33247
|
+
// believe the gate is watching when it is not.
|
|
33248
|
+
note: "unknown is not low — a run is never stopped on a missing reading. Claude Code before v2.1.80 sends no usage headers.",
|
|
33249
|
+
};
|
|
33250
|
+
if (asJson) emitJson(out, 2);
|
|
33251
|
+
console.log(ui.color.gray("usage: unknown (no reading in the last 30 minutes) · a run is never stopped on this"));
|
|
33252
|
+
process.exit(2);
|
|
33253
|
+
}
|
|
33254
|
+
|
|
33255
|
+
const fh = view(j.five_hour, "5h");
|
|
33256
|
+
const sd = view(j.seven_day, "wk");
|
|
33257
|
+
const windows = [fh, sd].filter(Boolean);
|
|
33258
|
+
if (!windows.length) {
|
|
33259
|
+
const out = { ok: true, state: "unknown", reason: "the reading carries no window", stop_pct: stopPct, gate };
|
|
33260
|
+
if (asJson) emitJson(out, 2);
|
|
33261
|
+
console.log(ui.color.gray("usage: unknown (the reading carries no window)"));
|
|
33262
|
+
process.exit(2);
|
|
33263
|
+
}
|
|
33264
|
+
|
|
33265
|
+
// THE WORST WINDOW DECIDES. A 7-day window at 96% with a 5-hour window at 20%
|
|
33266
|
+
// is not a green light: the run continues and dies twenty minutes later.
|
|
33267
|
+
const worst = windows.reduce((a, b) => (a.remaining_percentage <= b.remaining_percentage ? a : b));
|
|
33268
|
+
const low = windows.some((w) => w.low);
|
|
33269
|
+
const out = {
|
|
33270
|
+
ok: true,
|
|
33271
|
+
state: low ? "low" : "ok",
|
|
33272
|
+
five_hour: fh,
|
|
33273
|
+
seven_day: sd,
|
|
33274
|
+
worst: worst.window,
|
|
33275
|
+
context: typeof j.context_used_percentage === "number" ? j.context_used_percentage : null,
|
|
33276
|
+
stop_pct: stopPct,
|
|
33277
|
+
gate,
|
|
33278
|
+
reading_age_minutes: Math.round((now - j.written_at) / 60000),
|
|
33279
|
+
note: "read BEFORE a wave; the wave then spends tokens, so this is a reading and not a promise.",
|
|
33280
|
+
};
|
|
33281
|
+
if (asJson) emitJson(out, low ? 1 : 0);
|
|
33282
|
+
|
|
33283
|
+
const fmt = (w) =>
|
|
33284
|
+
w ? `${w.window} ${w.used_percentage}%${w.resets_in_minutes != null ? ` (${w.resets_in_minutes}m)` : ""}` : null;
|
|
33285
|
+
const line = [fmt(fh), fmt(sd)].filter(Boolean).join(" · ");
|
|
33286
|
+
console.log((low ? ui.mark.warn("usage: " + line) : ui.color.gray("usage: " + line)) +
|
|
33287
|
+
ui.color.gray(` · gate at ${stopPct}% left · ${gate}`));
|
|
33288
|
+
process.exit(low ? 1 : 0);
|
|
33289
|
+
}
|
|
33290
|
+
|
|
33291
|
+
// ── the wait's run state (v1.1.0 W2) ────────────────────────────────────────
|
|
33292
|
+
// ONE file, ONE writer — this CLI. The skill never writes it, so it can never
|
|
33293
|
+
// be behind the disk: the v0.49.5 hand-back lesson, and the fifth time this
|
|
33294
|
+
// repo has applied it (v0.32.0 narration · v0.53.2 spend log · v0.54.0 journal
|
|
33295
|
+
// · v1.0.0 W5 demotion).
|
|
33296
|
+
//
|
|
33297
|
+
// It lives BESIDE RESUME.md in {run_dir}/{slug}/, so it is deleted with the run
|
|
33298
|
+
// and can never outlive the decision it records.
|
|
33299
|
+
const WAIT_STATE_FILE = "wait.json";
|
|
33300
|
+
|
|
33301
|
+
function waitStatePath(claudeDir, slug) {
|
|
33302
|
+
return path.join(resolveRunDir(claudeDir), slug, WAIT_STATE_FILE);
|
|
33303
|
+
}
|
|
33304
|
+
|
|
33305
|
+
function readWaitState(claudeDir, slug) {
|
|
33306
|
+
try {
|
|
33307
|
+
const j = JSON.parse(fs.readFileSync(waitStatePath(claudeDir, slug), "utf8"));
|
|
33308
|
+
return j && typeof j === "object" ? j : null;
|
|
33309
|
+
} catch (_) {
|
|
33310
|
+
return null;
|
|
33311
|
+
}
|
|
33312
|
+
}
|
|
33313
|
+
|
|
33314
|
+
function writeWaitState(claudeDir, slug, patch) {
|
|
33315
|
+
const file = waitStatePath(claudeDir, slug);
|
|
33316
|
+
const cur = readWaitState(claudeDir, slug) || { v: 1, slug };
|
|
33317
|
+
const next = { ...cur, ...patch, v: 1, slug };
|
|
33318
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33319
|
+
fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
|
|
33320
|
+
return next;
|
|
33321
|
+
}
|
|
33322
|
+
|
|
33323
|
+
// The run this command is about. An explicit slug always wins; otherwise the
|
|
33324
|
+
// trace pointer names the run in flight. NEVER guess from "the newest folder" —
|
|
33325
|
+
// a wrong run is worse than no run, because the block would silently protect
|
|
33326
|
+
// something the user is not looking at.
|
|
33327
|
+
function waitResolveRun(claudeDir, slug) {
|
|
33328
|
+
if (slug) {
|
|
33329
|
+
const dir = path.join(resolveRunDir(claudeDir), slug);
|
|
33330
|
+
return fs.existsSync(dir) ? { slug, dir } : null;
|
|
33331
|
+
}
|
|
33332
|
+
const cur = extraCurrentRun(claudeDir);
|
|
33333
|
+
if (!cur || !cur.slug) return null;
|
|
33334
|
+
const dir = path.join(resolveRunDir(claudeDir), cur.slug);
|
|
33335
|
+
return fs.existsSync(dir) ? { slug: cur.slug, dir } : null;
|
|
33336
|
+
}
|
|
33337
|
+
|
|
33338
|
+
// CLI-COMPOSED AND CLI-WRITTEN. A wait that leaves no line cannot be counted,
|
|
33339
|
+
// and a block that leaves no line hides that a run continued through a gate on
|
|
33340
|
+
// the user's authority. Best effort ALWAYS: a trace that cannot be written must
|
|
33341
|
+
// never take the command down with it.
|
|
33342
|
+
function waitTraceLine(claudeDir, verb, tail) {
|
|
33343
|
+
try {
|
|
33344
|
+
const cur = extraCurrentRun(claudeDir);
|
|
33345
|
+
if (!cur || !cur.trace) return null;
|
|
33346
|
+
const dir = resolveLogDir(claudeDir);
|
|
33347
|
+
const d = new Date();
|
|
33348
|
+
const p = (n, w) => String(n).padStart(w || 2, "0");
|
|
33349
|
+
const stamp =
|
|
33350
|
+
`${p(d.getDate())}${p(d.getMonth() + 1)}${p(d.getFullYear() % 100)} ` +
|
|
33351
|
+
`${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
|
|
33352
|
+
const line = `[${stamp}] ${"cli".padEnd(8)} ${verb}${tail ? ` :: ${tail}` : ""}\n`;
|
|
33353
|
+
fs.appendFileSync(path.join(dir, cur.trace), line);
|
|
33354
|
+
return line.trimEnd();
|
|
33355
|
+
} catch (_) {
|
|
33356
|
+
return null;
|
|
33357
|
+
}
|
|
33358
|
+
}
|
|
33359
|
+
|
|
33360
|
+
function waitBlockCmd(claudeDir, unblock) {
|
|
33361
|
+
const asJson = wantsJson();
|
|
33362
|
+
const slug = positionals()[2];
|
|
33363
|
+
const reason = typeof flag("--reason") === "string" ? String(flag("--reason")).trim() : "";
|
|
33364
|
+
const run = waitResolveRun(claudeDir, slug);
|
|
33365
|
+
|
|
33366
|
+
if (!run) {
|
|
33367
|
+
const hint = slug
|
|
33368
|
+
? `no run named "${slug}" under ${resolveRunDir(claudeDir)}`
|
|
33369
|
+
: "no run is in flight, and no slug was given";
|
|
33370
|
+
if (asJson) emitJson({ ok: false, reason: "no-run", slug: slug || null, hint }, 2);
|
|
33371
|
+
console.error(ui.mark.err("I cannot find the run this is about."));
|
|
33372
|
+
console.error(" " + hint + ".");
|
|
33373
|
+
process.exit(2);
|
|
33374
|
+
}
|
|
33375
|
+
|
|
33376
|
+
if (unblock) {
|
|
33377
|
+
const cur = readWaitState(claudeDir, run.slug);
|
|
33378
|
+
if (!cur || !cur.blocked_at) {
|
|
33379
|
+
if (asJson) emitJson({ ok: false, reason: "not-blocked", slug: run.slug }, 1);
|
|
33380
|
+
console.error(ui.mark.warn(`${run.slug} is not blocked.`));
|
|
33381
|
+
process.exit(1);
|
|
33382
|
+
}
|
|
33383
|
+
const st = writeWaitState(claudeDir, run.slug, {
|
|
33384
|
+
blocked_at: null,
|
|
33385
|
+
block_reason: null,
|
|
33386
|
+
unblocked_at: new Date().toISOString(),
|
|
33387
|
+
});
|
|
33388
|
+
const trace = waitTraceLine(claudeDir, "WAIT", "unblock");
|
|
33389
|
+
if (asJson) emitJson({ ok: true, slug: run.slug, blocked: false, trace_line: trace }, 0);
|
|
33390
|
+
console.log(ui.mark.ok(`${run.slug}: the gate is live again.`));
|
|
33391
|
+
return;
|
|
33392
|
+
}
|
|
33393
|
+
|
|
33394
|
+
// THE REASON IS REQUIRED. A recorded reason is what makes the risk
|
|
33395
|
+
// demonstrably the user's — the same rule the run-close and doc-ship writers
|
|
33396
|
+
// already follow. Refuse BY NAME rather than storing an empty string.
|
|
33397
|
+
if (!reason) {
|
|
33398
|
+
if (asJson)
|
|
33399
|
+
emitJson(
|
|
33400
|
+
{
|
|
33401
|
+
ok: false,
|
|
33402
|
+
reason: "no-reason",
|
|
33403
|
+
slug: run.slug,
|
|
33404
|
+
hint: 'a block records why you accepted the risk. Pass --reason "<why>".',
|
|
33405
|
+
},
|
|
33406
|
+
1
|
|
33407
|
+
);
|
|
33408
|
+
console.error(ui.mark.err("A block needs a reason."));
|
|
33409
|
+
console.error(' orc wait block ' + run.slug + ' --reason "window resets in 5m, task needs 10"');
|
|
33410
|
+
process.exit(1);
|
|
33411
|
+
}
|
|
33412
|
+
|
|
33413
|
+
const st = writeWaitState(claudeDir, run.slug, {
|
|
33414
|
+
blocked_at: new Date().toISOString(),
|
|
33415
|
+
block_reason: reason,
|
|
33416
|
+
unblocked_at: null,
|
|
33417
|
+
});
|
|
33418
|
+
const trace = waitTraceLine(claudeDir, "WAIT", `block reason="${reason}" by=user`);
|
|
33419
|
+
if (asJson)
|
|
33420
|
+
emitJson(
|
|
33421
|
+
{ ok: true, slug: run.slug, blocked: true, blocked_at: st.blocked_at, reason, trace_line: trace },
|
|
33422
|
+
0
|
|
33423
|
+
);
|
|
33424
|
+
console.log(ui.mark.warn(`${run.slug}: computed waits are blocked for the rest of this run.`));
|
|
33425
|
+
console.log(" reason " + reason);
|
|
33426
|
+
console.log(
|
|
33427
|
+
ui.color.gray(
|
|
33428
|
+
" The risk is yours: if the window empties mid-wave, the wave stops in the\n" +
|
|
33429
|
+
" middle. A typed `/orc-wait` still waits. `orc wait unblock` restores the gate."
|
|
33430
|
+
)
|
|
33431
|
+
);
|
|
33432
|
+
}
|
|
33433
|
+
|
|
33434
|
+
function waitCancelCmd(claudeDir) {
|
|
33435
|
+
const asJson = wantsJson();
|
|
33436
|
+
const run = waitResolveRun(claudeDir, positionals()[2]);
|
|
33437
|
+
if (!run) {
|
|
33438
|
+
if (asJson) emitJson({ ok: false, reason: "no-run", hint: "no run is in flight" }, 1);
|
|
33439
|
+
console.error(ui.mark.warn("No run is in flight, so there is no wait to cancel."));
|
|
33440
|
+
process.exit(1);
|
|
33441
|
+
}
|
|
33442
|
+
const cur = readWaitState(claudeDir, run.slug);
|
|
33443
|
+
if (!cur || !cur.wait_started_at || cur.wait_ended_at) {
|
|
33444
|
+
if (asJson) emitJson({ ok: false, reason: "no-wait", slug: run.slug }, 1);
|
|
33445
|
+
console.error(ui.mark.warn(`${run.slug} has no wait running.`));
|
|
33446
|
+
process.exit(1);
|
|
33447
|
+
}
|
|
33448
|
+
writeWaitState(claudeDir, run.slug, { cancel_requested_at: new Date().toISOString() });
|
|
33449
|
+
if (asJson) emitJson({ ok: true, slug: run.slug, cancelled: true }, 0);
|
|
33450
|
+
console.log(ui.mark.ok(`${run.slug}: the wait stops at the next hop.`));
|
|
33451
|
+
console.log(ui.color.gray(" The hand-back is already on disk, so nothing is lost either way."));
|
|
33452
|
+
}
|
|
33453
|
+
|
|
33454
|
+
function waitStatusCmd(claudeDir) {
|
|
33455
|
+
const asJson = wantsJson();
|
|
33456
|
+
const run = waitResolveRun(claudeDir, positionals()[2]);
|
|
33457
|
+
if (!run) {
|
|
33458
|
+
// An empty result is an ANSWER, so it still returns its object.
|
|
33459
|
+
if (asJson)
|
|
33460
|
+
emitJson({ ok: true, run: null, waiting: false, blocked: false, note: "no run is in flight" }, 1);
|
|
33461
|
+
console.log(ui.color.gray("No run is in flight."));
|
|
33462
|
+
process.exit(1);
|
|
33463
|
+
}
|
|
33464
|
+
const st = readWaitState(claudeDir, run.slug) || {};
|
|
33465
|
+
const blocked = !!st.blocked_at;
|
|
33466
|
+
const waiting = !!st.wait_started_at && !st.wait_ended_at;
|
|
33467
|
+
const ageMin = blocked ? Math.round((Date.now() - Date.parse(st.blocked_at)) / 60000) : null;
|
|
33468
|
+
const out = {
|
|
33469
|
+
ok: true,
|
|
33470
|
+
run: run.slug,
|
|
33471
|
+
waiting,
|
|
33472
|
+
mode: st.mode || null,
|
|
33473
|
+
hop: st.hop || null,
|
|
33474
|
+
hops_done: st.hops_done || 0,
|
|
33475
|
+
hops_planned: st.hops_planned || null,
|
|
33476
|
+
ends_at: st.ends_at || null,
|
|
33477
|
+
cancel_requested: !!st.cancel_requested_at,
|
|
33478
|
+
blocked,
|
|
33479
|
+
block_reason: blocked ? st.block_reason : null,
|
|
33480
|
+
blocked_at: st.blocked_at || null,
|
|
33481
|
+
// The AGE is what keeps an old block from applying invisibly — there is no
|
|
33482
|
+
// auto-expiry, because ORC does not decide a user's reason stopped being true.
|
|
33483
|
+
block_age_minutes: ageMin,
|
|
33484
|
+
};
|
|
33485
|
+
if (asJson) emitJson(out, 0);
|
|
33486
|
+
|
|
33487
|
+
console.log(ui.header("ORC · wait — " + run.slug));
|
|
33488
|
+
console.log("");
|
|
33489
|
+
console.log(
|
|
33490
|
+
` ${ui.color.cyan("wait")} ` +
|
|
33491
|
+
(waiting
|
|
33492
|
+
? `${st.mode || "?"} · hop ${out.hops_done} of ${out.hops_planned || "?"} · ends ${out.ends_at || "?"}`
|
|
33493
|
+
: ui.color.gray("none"))
|
|
33494
|
+
);
|
|
33495
|
+
console.log(
|
|
33496
|
+
` ${ui.color.cyan("block")} ` +
|
|
33497
|
+
(blocked ? ui.mark.warn(`${ageMin}m ago — "${st.block_reason}"`) : ui.color.gray("not blocked"))
|
|
33498
|
+
);
|
|
33499
|
+
if (out.cancel_requested) console.log(ui.color.gray(" a cancel is pending — the wait ends at the next hop"));
|
|
33500
|
+
console.log("");
|
|
33501
|
+
}
|
|
33502
|
+
|
|
33503
|
+
function waitCmd() {
|
|
33504
|
+
const claudeDir = resolveClaudeDir();
|
|
33505
|
+
const sub = positionals()[1];
|
|
33506
|
+
switch (sub) {
|
|
33507
|
+
case "lanes":
|
|
33508
|
+
return waitLanesCmd();
|
|
33509
|
+
case "plan":
|
|
33510
|
+
return waitPlanCmd(claudeDir);
|
|
33511
|
+
case "status":
|
|
33512
|
+
return waitStatusCmd(claudeDir);
|
|
33513
|
+
case "block":
|
|
33514
|
+
return waitBlockCmd(claudeDir, false);
|
|
33515
|
+
case "unblock":
|
|
33516
|
+
return waitBlockCmd(claudeDir, true);
|
|
33517
|
+
case "cancel":
|
|
33518
|
+
return waitCancelCmd(claudeDir);
|
|
33519
|
+
default:
|
|
33520
|
+
console.error(`usage: orc wait lanes [--json]
|
|
33521
|
+
orc wait plan <30|90m|2h|until 18:41|reset> [--hop <min>] [--max-hops <n>] [--json]
|
|
33522
|
+
orc wait status [<slug>] [--json]
|
|
33523
|
+
orc wait block <slug> --reason "<why>" [--json]
|
|
33524
|
+
orc wait unblock [<slug>] [--json]
|
|
33525
|
+
orc wait cancel [<slug>] [--json]`);
|
|
33526
|
+
process.exit(1);
|
|
33527
|
+
}
|
|
33528
|
+
}
|
|
33529
|
+
|
|
32904
33530
|
// ── the `--json` crash envelope (v0.49.2) ───────────────────────────────────
|
|
32905
33531
|
// A read asked for JSON must answer in JSON or not at all. Before this, an
|
|
32906
33532
|
// unexpected throw inside any `--json` route printed a Node stack to stderr and
|
|
@@ -33007,6 +33633,19 @@ function jsonCrash(err) {
|
|
|
33007
33633
|
case "extra":
|
|
33008
33634
|
await extra();
|
|
33009
33635
|
break;
|
|
33636
|
+
// v1.1.0 W1 — the deterministic half of `/orc-wait`. The skill asks; it
|
|
33637
|
+
// never computes hops or the lane table itself.
|
|
33638
|
+
case "wait":
|
|
33639
|
+
waitCmd();
|
|
33640
|
+
break;
|
|
33641
|
+
// v1.1.0 W4 — the ONE reader of the statusline's usage bridge.
|
|
33642
|
+
case "usage":
|
|
33643
|
+
if (positionals()[1] === "check") usageCheckCmd(resolveClaudeDir());
|
|
33644
|
+
else {
|
|
33645
|
+
console.error("usage: orc usage check [--json]");
|
|
33646
|
+
process.exit(1);
|
|
33647
|
+
}
|
|
33648
|
+
break;
|
|
33010
33649
|
case "ui":
|
|
33011
33650
|
uiCmd();
|
|
33012
33651
|
break;
|