@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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* panels/wait.js — orc ui client
|
|
3
|
+
The usage window, and the wait that answers it.
|
|
4
|
+
|
|
5
|
+
THE PANEL DERIVES NOTHING. Not the state word, not which window is worst,
|
|
6
|
+
not the threshold, not a lane's checkpoint kind, not whether a wait is
|
|
7
|
+
running. It draws `orc usage check --json` and `orc wait … --json`. (The
|
|
8
|
+
Flow-stepper rule: a second idea of the mechanic is exactly the drift this
|
|
9
|
+
panel exists to make impossible.)
|
|
10
|
+
|
|
11
|
+
AND IT CANNOT START A WAIT. A wait lives in a Claude Code session, and
|
|
12
|
+
`orc ui` never runs a lane. What it can do is show the reading, show a wait
|
|
13
|
+
that is running, cancel one, and lift a block. Everything that COSTS a
|
|
14
|
+
decision — starting a wait, blocking a gate — is a copy-able command.
|
|
15
|
+
|
|
16
|
+
Loaded by app.html in the order its numeric prefix names. Classic script,
|
|
17
|
+
no import/export: an ES module import carries no query string, and every
|
|
18
|
+
static request here needs the per-launch session token. */
|
|
19
|
+
|
|
20
|
+
/* ------------------------------------------------------------------- WAIT */
|
|
21
|
+
|
|
22
|
+
// The CLI's own state words, and only those. A friendlier synonym would be a
|
|
23
|
+
// state that does not exist.
|
|
24
|
+
const USAGE_KIND = { ok: "ok", low: "warn", unknown: null };
|
|
25
|
+
|
|
26
|
+
PANELS.wait = function (host) {
|
|
27
|
+
head(host, t("wait.title"), t("wait.sub"));
|
|
28
|
+
|
|
29
|
+
section(
|
|
30
|
+
host,
|
|
31
|
+
() =>
|
|
32
|
+
Promise.all([
|
|
33
|
+
read("/api/usage").then((r) => r.data),
|
|
34
|
+
read("/api/wait/status").then((r) => r.data),
|
|
35
|
+
read("/api/wait/lanes").then((r) => r.data),
|
|
36
|
+
]),
|
|
37
|
+
([usage, status, lanes]) => {
|
|
38
|
+
const out = frag();
|
|
39
|
+
out.append(usageCard(usage));
|
|
40
|
+
out.append(runCard(status));
|
|
41
|
+
out.append(lanesCard(lanes));
|
|
42
|
+
out.append(settingsCard(usage));
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── the reading ────────────────────────────────────────────────────────────
|
|
49
|
+
function usageCard(u) {
|
|
50
|
+
const c = card(t("wait.window"));
|
|
51
|
+
if (!u) return c;
|
|
52
|
+
|
|
53
|
+
// UNKNOWN IS A STATE, NOT A GAP. It gets the same card, the same size and a
|
|
54
|
+
// sentence saying a run is never stopped on it — because the alternative is a
|
|
55
|
+
// user believing the gate is watching when Claude Code sends no headers.
|
|
56
|
+
if (u.state === "unknown") {
|
|
57
|
+
const row = el("div", "row-actions");
|
|
58
|
+
row.append(chip(u.state, null));
|
|
59
|
+
c.append(row);
|
|
60
|
+
c.append(el("div", "note", u.reason || ""));
|
|
61
|
+
c.append(el("div", "note", u.note || ""));
|
|
62
|
+
c.append(gateNote(u));
|
|
63
|
+
return c;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const row = el("div", "row-actions");
|
|
67
|
+
row.append(chip(u.state, USAGE_KIND[u.state]));
|
|
68
|
+
if (u.worst) row.append(chip(t("wait.worst", { w: u.worst }), u.state === "low" ? "warn" : null));
|
|
69
|
+
if (typeof u.reading_age_minutes === "number")
|
|
70
|
+
row.append(chip(tn(u.reading_age_minutes, "wait.age"), null));
|
|
71
|
+
c.append(row);
|
|
72
|
+
|
|
73
|
+
const rows = [];
|
|
74
|
+
for (const w of [u.five_hour, u.seven_day]) {
|
|
75
|
+
if (!w) continue;
|
|
76
|
+
rows.push([
|
|
77
|
+
w.window,
|
|
78
|
+
// The bar is a WIDTH INSIDE the row, so nothing can fight it for space.
|
|
79
|
+
barFor(w),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
const grid = el("div", "wait-windows");
|
|
83
|
+
for (const [label, node] of rows) {
|
|
84
|
+
const r = el("div", "wait-win");
|
|
85
|
+
r.append(el("span", "mono wait-win-label", label));
|
|
86
|
+
r.append(node);
|
|
87
|
+
grid.append(r);
|
|
88
|
+
}
|
|
89
|
+
c.append(grid);
|
|
90
|
+
|
|
91
|
+
// A context figure the CLI could not compute is an em dash, never a guess.
|
|
92
|
+
const ctx = el("div", "note");
|
|
93
|
+
ctx.textContent =
|
|
94
|
+
typeof u.context === "number"
|
|
95
|
+
? t("wait.context", { n: String(u.context) })
|
|
96
|
+
: t("wait.contextUnknown");
|
|
97
|
+
c.append(ctx);
|
|
98
|
+
if (typeof u.context === "number" && u.context >= 70)
|
|
99
|
+
c.append(el("div", "note warn", t("wait.contextLarge")));
|
|
100
|
+
|
|
101
|
+
c.append(el("div", "note", u.note || ""));
|
|
102
|
+
c.append(gateNote(u));
|
|
103
|
+
return c;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function barFor(w) {
|
|
107
|
+
const wrap = el("div", "wait-bar-wrap");
|
|
108
|
+
const bar = el("div", "wait-bar" + (w.low ? " low" : ""));
|
|
109
|
+
const fill = el("div", "wait-bar-fill");
|
|
110
|
+
fill.style.width = Math.max(0, Math.min(100, w.used_percentage)) + "%";
|
|
111
|
+
bar.append(fill);
|
|
112
|
+
wrap.append(bar);
|
|
113
|
+
const txt = el("span", "wait-bar-text");
|
|
114
|
+
txt.textContent =
|
|
115
|
+
w.used_percentage +
|
|
116
|
+
"% · " +
|
|
117
|
+
t("wait.left", { n: String(w.remaining_percentage) }) +
|
|
118
|
+
(w.resets_in_minutes != null ? " · " + t("wait.resets", { t: mins(w.resets_in_minutes) }) : "");
|
|
119
|
+
wrap.append(txt);
|
|
120
|
+
return wrap;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mins = (n) => (n < 60 ? n + "m" : Math.floor(n / 60) + "h" + (n % 60 ? (n % 60) + "m" : ""));
|
|
124
|
+
|
|
125
|
+
function gateNote(u) {
|
|
126
|
+
const n = el("div", "note");
|
|
127
|
+
n.textContent =
|
|
128
|
+
u.gate === "off"
|
|
129
|
+
? t("wait.gateOff")
|
|
130
|
+
: t("wait.gateOn", { gate: u.gate, pct: String(u.stop_pct) });
|
|
131
|
+
return n;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── the run: a wait running, a block standing, or neither ──────────────────
|
|
135
|
+
function runCard(s) {
|
|
136
|
+
const c = card(t("wait.run"));
|
|
137
|
+
if (!s || !s.run) {
|
|
138
|
+
// KEEPS ITS SLOT. "No run in flight" is an answer.
|
|
139
|
+
c.append(empty(t("wait.noRun"), t("wait.noRunHint")));
|
|
140
|
+
c.append(laneCommand("/orc-wait 30", t("wait.startWhy")));
|
|
141
|
+
return c;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const row = el("div", "row-actions");
|
|
145
|
+
row.append(el("span", "mono", s.run));
|
|
146
|
+
if (s.waiting) row.append(chip(t("wait.waiting"), "warn", true));
|
|
147
|
+
if (s.blocked) row.append(chip(t("wait.blocked"), "bad"));
|
|
148
|
+
if (!s.waiting && !s.blocked) row.append(chip(t("wait.idle"), null));
|
|
149
|
+
c.append(row);
|
|
150
|
+
|
|
151
|
+
if (s.waiting) {
|
|
152
|
+
c.append(
|
|
153
|
+
el(
|
|
154
|
+
"div",
|
|
155
|
+
"note",
|
|
156
|
+
t("wait.hops", {
|
|
157
|
+
mode: s.mode || "?",
|
|
158
|
+
done: String(s.hops_done || 0),
|
|
159
|
+
planned: String(s.hops_planned || "?"),
|
|
160
|
+
ends: s.ends_at ? new Date(s.ends_at).toLocaleTimeString() : "?",
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
c.append(el("div", "note", t("wait.zeroTokens")));
|
|
165
|
+
if (s.cancel_requested) c.append(el("div", "note warn", t("wait.cancelPending")));
|
|
166
|
+
// FREE action → a real button.
|
|
167
|
+
else c.append(actionRow("wait.cancel", "/api/wait/cancel", { slug: s.run }, t("wait.cancelWhy")));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (s.blocked) {
|
|
171
|
+
const b = el("div", "wait-block");
|
|
172
|
+
b.append(el("div", "wait-block-head", t("wait.blockHead")));
|
|
173
|
+
// The reason VERBATIM. It is the record that makes the risk the user's.
|
|
174
|
+
b.append(el("div", "wait-block-reason", s.block_reason || ""));
|
|
175
|
+
// The AGE is what keeps an old block from applying invisibly — there is no
|
|
176
|
+
// auto-expiry, so the panel must always show how old it is.
|
|
177
|
+
if (typeof s.block_age_minutes === "number")
|
|
178
|
+
b.append(el("div", "note", tn(s.block_age_minutes, "wait.blockAge")));
|
|
179
|
+
b.append(el("div", "note", t("wait.blockRisk")));
|
|
180
|
+
b.append(actionRow("wait.unblock", "/api/wait/unblock", { slug: s.run }, t("wait.unblockWhy")));
|
|
181
|
+
c.append(b);
|
|
182
|
+
} else {
|
|
183
|
+
// A block CANNOT be created here: it needs a reason typed in the moment.
|
|
184
|
+
c.append(laneCommand("/orc-wait block <reason>", t("wait.blockWhy")));
|
|
185
|
+
}
|
|
186
|
+
return c;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// NOTE the parameter name: `route` is the ROUTER's function, and shadowing it
|
|
190
|
+
// here made the post-action refresh call a string.
|
|
191
|
+
function actionRow(labelKey, endpoint, body, why) {
|
|
192
|
+
const wrap = el("div", null);
|
|
193
|
+
const row = el("div", "row-actions");
|
|
194
|
+
const b = el("button", "btn btn-sm", t(labelKey));
|
|
195
|
+
b.type = "button";
|
|
196
|
+
b.addEventListener("click", async () => {
|
|
197
|
+
b.disabled = true;
|
|
198
|
+
try {
|
|
199
|
+
const r = await post(endpoint, body);
|
|
200
|
+
toast(r && r.ok ? t("wait.done") : t("wait.failed"), r && r.ok ? "ok" : "bad");
|
|
201
|
+
route();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
toast(t("wait.failed"), "bad", String(e));
|
|
204
|
+
b.disabled = false;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
row.append(b);
|
|
208
|
+
wrap.append(row);
|
|
209
|
+
if (why) wrap.append(el("div", "note", why));
|
|
210
|
+
return wrap;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── which lanes support a wait ─────────────────────────────────────────────
|
|
214
|
+
function lanesCard(d) {
|
|
215
|
+
const c = card(t("wait.lanes"));
|
|
216
|
+
if (!d || !d.lanes) return c;
|
|
217
|
+
c.append(el("div", "note", t("wait.lanesWhy")));
|
|
218
|
+
const list = el("div", "wait-lanes");
|
|
219
|
+
for (const l of d.lanes) {
|
|
220
|
+
const r = el("div", "wait-lane");
|
|
221
|
+
r.append(el("span", "mono wait-lane-name", l.lane));
|
|
222
|
+
// `none` KEEPS ITS SLOT and reads as its own state, never as a blank.
|
|
223
|
+
r.append(chip(l.checkpoint, l.modes_differ ? "ok" : null));
|
|
224
|
+
r.append(el("span", "wait-lane-safe", l.safe_point));
|
|
225
|
+
r.append(el("div", "note wait-lane-detail", l.detail));
|
|
226
|
+
list.append(r);
|
|
227
|
+
}
|
|
228
|
+
c.append(list);
|
|
229
|
+
c.append(el("div", "note", d.note || ""));
|
|
230
|
+
return c;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── settings: read-only here, edited where every other key is edited ───────
|
|
234
|
+
function settingsCard(u) {
|
|
235
|
+
const c = card(t("wait.settings"));
|
|
236
|
+
c.append(
|
|
237
|
+
kvList([
|
|
238
|
+
["usage_gate", u && u.gate ? u.gate : "—"],
|
|
239
|
+
["usage_stop_pct", u && u.stop_pct != null ? String(u.stop_pct) : "—"],
|
|
240
|
+
])
|
|
241
|
+
);
|
|
242
|
+
// A SECOND editor for keys Settings already stages would be a second idea of
|
|
243
|
+
// the same thing — the drift this panel exists to prevent. Route to the panel
|
|
244
|
+
// that can change it instead (the FINDING_ROUTE rule).
|
|
245
|
+
c.append(el("div", "note", t("wait.settingsWhy")));
|
|
246
|
+
const b = el("button", "btn btn-ghost btn-sm", t("wait.openSettings"));
|
|
247
|
+
b.type = "button";
|
|
248
|
+
b.addEventListener("click", () => (location.hash = "#/settings"));
|
|
249
|
+
const row = el("div", "row-actions");
|
|
250
|
+
row.append(b);
|
|
251
|
+
c.append(row);
|
|
252
|
+
return c;
|
|
253
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-id/orc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "ORC — an orchestrator skill constellation for Claude Code: intake, planning, scored parallel subagents, code-pattern matching, review, verify, ship, plus a project knowledge-base wiki.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"orc": "bin/cli.js"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Wait for wall-clock time to pass — usually a quota window reset — without losing the run you are in
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run the `orc-wait` skill.
|
|
6
|
+
|
|
7
|
+
The user typed: `$ARGUMENTS`
|
|
8
|
+
|
|
9
|
+
Parse the arguments as `<spec> [mode]`, where:
|
|
10
|
+
|
|
11
|
+
- `<spec>` is `30` · `90m` · `2h` · `2h30m` · `until 18:41` · `reset`
|
|
12
|
+
- `[mode]` is `safe` · `soft` · `hard`, or absent
|
|
13
|
+
- `block <reason>` and `unblock` are the veto, not a wait — see the skill
|
|
14
|
+
|
|
15
|
+
Never compute the hops yourself. `orc wait plan <spec> --json` does that, and it
|
|
16
|
+
is the only place that arithmetic exists.
|
|
17
|
+
|
|
18
|
+
A wait is a STOP: write the hand-back before you wait, every time, in every
|
|
19
|
+
mode. `a lane that waits without a hand-back` has broken the contract.
|
|
@@ -90,6 +90,45 @@ process.stdin.on("end", () => {
|
|
|
90
90
|
}
|
|
91
91
|
} catch (_) {}
|
|
92
92
|
|
|
93
|
+
// ── Usage bridge (v1.1.0 W4, fail-silent) ─────────────────────────────────
|
|
94
|
+
// The `rate_limits` block below reaches ONLY this process: the statusline
|
|
95
|
+
// renders a string and exits, so nothing else in ORC has ever been able to
|
|
96
|
+
// see how full the window is. A lane therefore started a wave with no idea it
|
|
97
|
+
// was about to run out, and the wave stopped in the middle.
|
|
98
|
+
//
|
|
99
|
+
// Persist the RAW numbers (never a computed word like `LOW` — a stored state
|
|
100
|
+
// is wrong one minute later; `orc usage check` computes it on read) plus
|
|
101
|
+
// `context_window`, which the wait needs to decide whether continuing
|
|
102
|
+
// in-session is cheaper than a fresh one. Same fail-silent contract as the
|
|
103
|
+
// session-model bridge above: any error is swallowed, and a reading older
|
|
104
|
+
// than its freshness window reads as `unknown`, never as `low`.
|
|
105
|
+
try {
|
|
106
|
+
const rl0 = d.rate_limits;
|
|
107
|
+
const cw0 = d.context_window;
|
|
108
|
+
if (rl0 || cw0) {
|
|
109
|
+
const fs = require("fs");
|
|
110
|
+
const path = require("path");
|
|
111
|
+
const projectDir =
|
|
112
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
113
|
+
const orcDir = path.join(projectDir, ".claude", "orc");
|
|
114
|
+
const win = (o) =>
|
|
115
|
+
o && typeof o.used_percentage === "number"
|
|
116
|
+
? { used_percentage: o.used_percentage, resets_at: o.resets_at == null ? null : o.resets_at }
|
|
117
|
+
: null;
|
|
118
|
+
fs.mkdirSync(orcDir, { recursive: true });
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(orcDir, "usage.json"),
|
|
121
|
+
JSON.stringify({
|
|
122
|
+
five_hour: win(rl0 && rl0.five_hour),
|
|
123
|
+
seven_day: win(rl0 && rl0.seven_day),
|
|
124
|
+
context_used_percentage:
|
|
125
|
+
cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null,
|
|
126
|
+
written_at: Date.now(),
|
|
127
|
+
}) + "\n"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
} catch (_) {}
|
|
131
|
+
|
|
93
132
|
// ── Subscription usage (Claude Code v2.1.80+) ──────────────────────────────
|
|
94
133
|
// Official 5-hour + 7-day usage, surfaced by Claude Code straight from
|
|
95
134
|
// Anthropic's API headers into this payload's `rate_limits`. Display-only,
|
|
@@ -55,6 +55,28 @@ never a fifth step before step 2.
|
|
|
55
55
|
ledger yet, this is a first run"; `orc gotcha status` exit 1 is an empty
|
|
56
56
|
ledger. Say what it means, not that it failed.
|
|
57
57
|
|
|
58
|
+
## The usage line (v1.1.0) — printed whenever `usage_gate` is armed
|
|
59
|
+
|
|
60
|
+
`usage_gate` resolves in step 1 like any other key. When it is anything but
|
|
61
|
+
`off`, run `orc usage check --json` in step 3 and print ONE line:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
usage: 5h 71% · wk 38% · gate at 10% left · warn
|
|
65
|
+
usage: unknown (no reading in the last 30 minutes) · a run is never stopped on this
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both spellings are mandatory in their state. A gate that prints only when it
|
|
69
|
+
fires is a gate the user cannot tell from a gate that is not running — and the
|
|
70
|
+
`unknown` line is what stops someone believing they are protected when Claude
|
|
71
|
+
Code is sending no usage headers at all.
|
|
72
|
+
|
|
73
|
+
**Exit 2 (`unknown`) NEVER stops a run.** Absent is not low.
|
|
74
|
+
|
|
75
|
+
A `blocked` run (`orc wait status --json`) prints the block and its AGE here too,
|
|
76
|
+
and again at every gate it suppresses. The rest of the mechanic — the modes, the
|
|
77
|
+
hops, what `stop` and `wait` actually do — is `../wait.md`; this file only says
|
|
78
|
+
that the line is printed and when.
|
|
79
|
+
|
|
58
80
|
<!-- /orc:layer -->
|
|
59
81
|
|
|
60
82
|
<!-- orc:layer full -->
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Shared contract — the WAIT (`/orc-wait`, and the computed gate)
|
|
2
|
+
|
|
3
|
+
Canonical file: `_shared/wait.md`. THE canonical mechanic for a lane that must
|
|
4
|
+
**stop where it stands, wait for wall-clock time to pass, and carry on from the
|
|
5
|
+
same place**. Load it wherever a lane can be interrupted by a wait — which,
|
|
6
|
+
since v1.1.0, is every lane in the table below.
|
|
7
|
+
|
|
8
|
+
## Why a wait is not a suspend and not a fallback
|
|
9
|
+
|
|
10
|
+
ORC already has two shapes for leaving a run. This is a third, and conflating
|
|
11
|
+
them loses work.
|
|
12
|
+
|
|
13
|
+
| | `FALLBACK-FROM` | `RETURN-TO` | **WAIT** |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Why it leaves | it cannot proceed | another lane must settle something | **wall-clock time must pass** |
|
|
16
|
+
| Who finishes | the receiver | the sender, after coming back | **the same lane, same run** |
|
|
17
|
+
| Another lane runs | yes | yes | **no — nothing runs** |
|
|
18
|
+
| Traces | one | two | **one** |
|
|
19
|
+
|
|
20
|
+
A wait dispatches nothing and decides nothing. It is the only ORC mechanic
|
|
21
|
+
whose entire purpose is that **no model is running**.
|
|
22
|
+
|
|
23
|
+
## The one rule
|
|
24
|
+
|
|
25
|
+
> **`a lane that waits without a hand-back` has broken this contract.**
|
|
26
|
+
|
|
27
|
+
A wait is a stop. Every stop in ORC writes its hand-back before it ends, because
|
|
28
|
+
the thing that resumes the run may not be this session — the user can close the
|
|
29
|
+
terminal, the machine can sleep, and the wake-up message is a Claude Code
|
|
30
|
+
behaviour ORC cannot promise. `RESUME.md` on disk is what makes a lost wake-up
|
|
31
|
+
cost nothing.
|
|
32
|
+
|
|
33
|
+
This is `_shared/phases/stop-resume.md` applied to a stop nobody asked a
|
|
34
|
+
question about. It does not replace that phase; it scales it.
|
|
35
|
+
|
|
36
|
+
## The three modes
|
|
37
|
+
|
|
38
|
+
A wait is requested with a mode. The modes differ in ONE thing: how much the
|
|
39
|
+
lane finishes before it stops.
|
|
40
|
+
|
|
41
|
+
| Mode | Stops at | Hand-back | Dispatches | Can lose |
|
|
42
|
+
|---|---|---|---|---|
|
|
43
|
+
| `safe` | the next **safe point** | full stop sequence | yes (checkpoint) | nothing |
|
|
44
|
+
| `soft` | the next **model turn** | full stop sequence, **forced** | yes (checkpoint) | an in-flight return |
|
|
45
|
+
| `hard` | the next **model turn** | `RESUME.md` only, best effort | **no** | an in-flight return, the checkpoint, the phase's trace packet |
|
|
46
|
+
|
|
47
|
+
**`soft` is forced.** On a lane the table below marks as checkpointing, `soft`
|
|
48
|
+
does not merely attempt the hand-back: if the checkpoint write fails, **`soft`
|
|
49
|
+
does not stop**. It reports the failure and stays in the run. That is
|
|
50
|
+
`stop-resume.md` step 2 unchanged — *stopping without a good checkpoint is the
|
|
51
|
+
one thing that loses work* — and it is the whole reason the mode exists.
|
|
52
|
+
|
|
53
|
+
**`hard` is the dispatch-free stop.** It writes only what ORC can write with its
|
|
54
|
+
own hand (`RESUME.md`, per stop-resume step 3b — never a dispatched agent). It
|
|
55
|
+
is fast BECAUSE it dispatches nothing, not in spite of it. It is the one mode
|
|
56
|
+
that can lose work, and it says so every time it runs.
|
|
57
|
+
|
|
58
|
+
### "the next model turn" is the honest promise
|
|
59
|
+
|
|
60
|
+
A typed message reaches ORC at a turn boundary. `hard` therefore **cannot**
|
|
61
|
+
interrupt a dispatch that is already in flight. What it promises is:
|
|
62
|
+
|
|
63
|
+
> stop at the first moment ORC can act, and do not wait for the current wave,
|
|
64
|
+
> phase or gate to finish.
|
|
65
|
+
|
|
66
|
+
Never write "immediately". A user who reads "immediately" and sees a wave finish
|
|
67
|
+
believes the command failed.
|
|
68
|
+
|
|
69
|
+
## Safe points
|
|
70
|
+
|
|
71
|
+
A safe point is a place where the run can stop with no loss. `safe` waits for
|
|
72
|
+
one. `soft` and `hard` do not — that is what they are for, and what they risk.
|
|
73
|
+
|
|
74
|
+
**Never begin a wait at any of these, in any mode:**
|
|
75
|
+
|
|
76
|
+
- between a dispatch and its validated return
|
|
77
|
+
- inside the stop sequence itself
|
|
78
|
+
- during a file write, a `splice`, or a wiki registration write
|
|
79
|
+
- before the smoke gate has reported
|
|
80
|
+
|
|
81
|
+
These are not a style preference. Each one leaves an artifact that no resume can
|
|
82
|
+
reconstruct.
|
|
83
|
+
|
|
84
|
+
## Which lanes support a wait
|
|
85
|
+
|
|
86
|
+
The machine-readable copy of this table is `WAIT_LANE_SHAPES` in `bin/cli.js`,
|
|
87
|
+
rendered by `orc wait lanes`. A golden test compares the two IN BOTH DIRECTIONS
|
|
88
|
+
— the `EXTRA_LANE_SHAPES` / `DIY_STEPS` precedent. A lane added to one and not
|
|
89
|
+
the other fails the suite.
|
|
90
|
+
|
|
91
|
+
| Lane | Checkpoint | Safe point |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `/orc` | full | wave or phase edge |
|
|
94
|
+
| `/orc-ultra` | full | wave or judge gate |
|
|
95
|
+
| `/orc-mini` | full | after the executor returns |
|
|
96
|
+
| `/orc-fast` | full | after the executor returns |
|
|
97
|
+
| `/orc-diy` | full | compiled phase edge |
|
|
98
|
+
| `/orc-doc` | full | wave edge |
|
|
99
|
+
| `/orc-wiki` | full | scan-task boundary |
|
|
100
|
+
| `/orc-analyze` | full | after the analyst returns |
|
|
101
|
+
| `/orc-poly` | docset | after a per-repo plan is written |
|
|
102
|
+
| `/orc-quick` | entry | after an entry closes |
|
|
103
|
+
| `/orc-challenge` | cycle | after a cycle records |
|
|
104
|
+
| `/orc-brainstorm` | snapshot | phase edge |
|
|
105
|
+
| `/orc-grill` | snapshot | round edge |
|
|
106
|
+
| `/orc-learn` | none | single dispatch |
|
|
107
|
+
| `/orc-plan` | none | single dispatch |
|
|
108
|
+
| `/orc-verify` | none | single dispatch |
|
|
109
|
+
| `/orc-pattern` | none | single dispatch |
|
|
110
|
+
| `/orc-claude` | none | single dispatch |
|
|
111
|
+
| `/orc-explain` | none | read-only, seconds long |
|
|
112
|
+
| `/orc-route` | none | read-only, seconds long |
|
|
113
|
+
| `/orc-boundary` | none | read-only, seconds long |
|
|
114
|
+
| `/orc-budget` | none | read-only, seconds long |
|
|
115
|
+
| `/orc-aftermath` | none | read-only, seconds long |
|
|
116
|
+
| `/orc-export` | none | read-only, seconds long |
|
|
117
|
+
| `/orc-retro` | none | read-only, seconds long |
|
|
118
|
+
| `/orc-pact` | none | read-only, seconds long |
|
|
119
|
+
|
|
120
|
+
**`checkpoint: none` is an ANSWER, not a gap.** A single-dispatch lane has
|
|
121
|
+
nothing to checkpoint, so a wait there is a plain wait and the message says so.
|
|
122
|
+
On such a lane `safe`, `soft` and `hard` are the SAME thing, and
|
|
123
|
+
`orc wait lanes` states that rather than pretending to a distinction. A row that
|
|
124
|
+
reads `none` must never render like a row that is missing.
|
|
125
|
+
|
|
126
|
+
## The hop loop
|
|
127
|
+
|
|
128
|
+
The lane does not sleep. A **detached** command sleeps. It costs zero tokens and
|
|
129
|
+
no model runs during it.
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
1. Write the hand-back for the mode (above).
|
|
133
|
+
2. remaining = the requested time, or resets_at - now
|
|
134
|
+
3. hop = min(wait_hop_minutes, remaining)
|
|
135
|
+
4. Run a DETACHED command that waits hop seconds.
|
|
136
|
+
5. On wake: `orc usage check --json`
|
|
137
|
+
6. Exit 0, or the requested time has elapsed → continue. Else go to 3.
|
|
138
|
+
7. wait_max_hops reached → stop, keep the hand-back, say why.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**A hop is short on purpose.** Each wake-up is session activity, and session
|
|
142
|
+
activity is the only thing that makes the statusline run again — so each hop
|
|
143
|
+
buys a fresh reading. A single long sleep wakes into a reading as stale as the
|
|
144
|
+
sleep was long.
|
|
145
|
+
|
|
146
|
+
## After the wait — ORC does not drag a large context forward
|
|
147
|
+
|
|
148
|
+
`stop-resume.md` step 6 already requires offering both continue paths. This
|
|
149
|
+
decides which one ORC takes without asking:
|
|
150
|
+
|
|
151
|
+
- **context small** → continue here, and say so in one line.
|
|
152
|
+
- **context large** → STOP and offer both paths, recommending the fresh session.
|
|
153
|
+
|
|
154
|
+
A wait longer than one hour has already expired the prompt cache, so continuing
|
|
155
|
+
in-session re-reads the whole context at full input price — exactly when quota
|
|
156
|
+
is lowest. Auto-continuing into a bloated context is the cost the wait existed
|
|
157
|
+
to avoid.
|
|
158
|
+
|
|
159
|
+
**ORC cannot clear its own context.** `/clear` is the user's action. The wait
|
|
160
|
+
offers the swap; it never performs it.
|
|
161
|
+
|
|
162
|
+
## The computed gate (`usage_gate`)
|
|
163
|
+
|
|
164
|
+
The same engine, triggered by the CLI instead of by a typed command. It is
|
|
165
|
+
**`off` by default** — nothing below happens until the user turns it on.
|
|
166
|
+
|
|
167
|
+
Check **before a wave, never during one**: `orc usage check --json`.
|
|
168
|
+
|
|
169
|
+
| exit | state | `warn` | `stop` | `wait` |
|
|
170
|
+
|---|---|---|---|---|
|
|
171
|
+
| 0 | ok | continue | continue | continue |
|
|
172
|
+
| 1 | low | print and continue | hand back and stop | hand back, hop, come back |
|
|
173
|
+
| 2 | unknown | print and continue | print and continue | print and continue |
|
|
174
|
+
|
|
175
|
+
**Exit 2 never stops a run**, in any mode. An absent reading is absent, not low:
|
|
176
|
+
older Claude Code sends no usage headers, and a long dispatch leaves the reading
|
|
177
|
+
stale by exactly its own length. A gate that blocks on a missing number is a
|
|
178
|
+
gate people switch off.
|
|
179
|
+
|
|
180
|
+
**The worst window decides.** `orc usage check` already resolves that; never
|
|
181
|
+
re-derive it from one window.
|
|
182
|
+
|
|
183
|
+
A computed stop offers the cheaper answers before the expensive one — a lower
|
|
184
|
+
band for this wave, or `orc extra` if a profile is ready — because a wait is the
|
|
185
|
+
only one of them that costs wall-clock time.
|
|
186
|
+
|
|
187
|
+
**A typed `/orc-wait` is never suppressed by any of this**, and a computed wait
|
|
188
|
+
is suppressed entirely while a block is active.
|
|
189
|
+
|
|
190
|
+
## The block — the user's veto
|
|
191
|
+
|
|
192
|
+
`/orc-wait block <reason>` suppresses every COMPUTED wait for the rest of the
|
|
193
|
+
run. It is for the case where stopping costs more than continuing: the window
|
|
194
|
+
resets in five minutes and the task needs ten.
|
|
195
|
+
|
|
196
|
+
1. **The reason is REQUIRED.** A block with no reason is refused by name. The
|
|
197
|
+
recorded reason is what makes the risk demonstrably the user's — the same
|
|
198
|
+
`--reason` rule the run-close and doc-ship writers already use.
|
|
199
|
+
2. **Run-scoped. It NEVER writes the user's config.** The same rule the ultra
|
|
200
|
+
lane's forced run-scoped mode already follows. A veto set today must not
|
|
201
|
+
apply to a run started next month.
|
|
202
|
+
3. **It is ANNOUNCED at every gate it suppresses, with its age.** A shadowed
|
|
203
|
+
setting must never be silent. There is no auto-expiry — ORC does not decide
|
|
204
|
+
that a user's reason stopped being true — so the age is what keeps an old
|
|
205
|
+
block from applying invisibly.
|
|
206
|
+
4. **It blocks what ORC COMPUTES, never what the user TYPES.** A typed
|
|
207
|
+
`/orc-wait 30 hard` still waits while a block is active. `/orc-boundary`'s
|
|
208
|
+
rule, unchanged: a gate constrains ORC's own dispatch, never an explicit
|
|
209
|
+
instruction.
|
|
210
|
+
5. It survives a resume, and is re-announced on the first gate after it.
|
|
211
|
+
|
|
212
|
+
`orc wait cancel` is a DIFFERENT command: it ends a wait that is already
|
|
213
|
+
running. Block is before, cancel is during. Never conflate them in prose or in a
|
|
214
|
+
menu.
|
|
215
|
+
|
|
216
|
+
## What a wait never does
|
|
217
|
+
|
|
218
|
+
- It never dispatches an agent to do the waiting. An agent runs on the same
|
|
219
|
+
account and consumes the same window the wait exists to protect.
|
|
220
|
+
- It never runs another lane.
|
|
221
|
+
- It never writes the user's config.
|
|
222
|
+
- It never widens or narrows the work: the same tasks, the same slice, the same
|
|
223
|
+
agent resolve after the wait as before it.
|
|
224
|
+
- It never decides on its own that a user's block has expired.
|
|
225
|
+
|
|
226
|
+
## Trace
|
|
227
|
+
|
|
228
|
+
The wait writes CLI-composed lines into the trace that is ALREADY open, and
|
|
229
|
+
nothing when no run is active. `/orc-wait` opens no run, so it is **not a lane**
|
|
230
|
+
in the trace enum and has no `run-<lane>-<slug>` pointer — the `/orc-explain`
|
|
231
|
+
precedent, a stated blind spot rather than an oversight.
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
WAIT mode=hard requested=30m start=18:44 end=19:14 hops=1/4 trigger=user
|
|
235
|
+
WAIT block reason="window resets in 5m, task needs 10" by=user
|
|
236
|
+
WAIT unblock
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A wait that leaves no line cannot be counted, and a block that leaves no line
|
|
240
|
+
hides the fact that a run continued through a gate on the user's authority.
|
|
@@ -235,4 +235,9 @@ a `trim` layer beside the `full` one.
|
|
|
235
235
|
| 8 | Ship | `../_shared/phases/ship.md` | `full` | `PHASE ship`, `FINISH` |
|
|
236
236
|
|
|
237
237
|
Phase 4 runs only in worktree mode. Phases 5.5, 6.5 and 6.7 are opt-in and
|
|
238
|
-
their config key is resolved by `orc lane config orc --json`, never read raw.
|
|
238
|
+
their config key is resolved by `orc lane config orc --json`, never read raw.
|
|
239
|
+
|
|
240
|
+
## Waiting mid-run (`/orc-wait`)
|
|
241
|
+
|
|
242
|
+
Canonical: `../_shared/wait.md`. **`a lane that waits without a hand-back` has broken this contract.**
|
|
243
|
+
Checkpoint **full** · safe point **wave or phase edge**. `soft` FORCES that checkpoint and does NOT stop if the write fails; `hard` skips it and can lose an in-flight return. Never begin a wait between a dispatch and its validated return, or before the smoke gate has reported.
|
|
@@ -150,4 +150,9 @@ exit code, and never re-derive a state word — the CLI's state words are the on
|
|
|
150
150
|
state words, and **an exit code is an ANSWER wherever that contract says so, not
|
|
151
151
|
a failure**. A call the answer does not name is a call this lane does not make.
|
|
152
152
|
Exit ≠ 0 from the catalogue itself → say the CLI is unavailable and name the
|
|
153
|
-
command you are about to run, out loud, before running it.
|
|
153
|
+
command you are about to run, out loud, before running it.
|
|
154
|
+
|
|
155
|
+
## Waiting mid-run (`/orc-wait`)
|
|
156
|
+
|
|
157
|
+
Canonical: `../_shared/wait.md`. **`a lane that waits without a hand-back` has broken this contract.**
|
|
158
|
+
Checkpoint **none** · safe point **read-only, seconds long**. Nothing here to checkpoint, so all three modes behave identically — say so rather than asking. Never begin a wait between a dispatch and its validated return, or before the smoke gate has reported.
|
|
@@ -244,4 +244,9 @@ exit code, and never re-derive a state word — the CLI's state words are the on
|
|
|
244
244
|
state words, and **an exit code is an ANSWER wherever that contract says so, not
|
|
245
245
|
a failure**. A call the answer does not name is a call this lane does not make.
|
|
246
246
|
Exit ≠ 0 from the catalogue itself → say the CLI is unavailable and name the
|
|
247
|
-
command you are about to run, out loud, before running it.
|
|
247
|
+
command you are about to run, out loud, before running it.
|
|
248
|
+
|
|
249
|
+
## Waiting mid-run (`/orc-wait`)
|
|
250
|
+
|
|
251
|
+
Canonical: `../_shared/wait.md`. **`a lane that waits without a hand-back` has broken this contract.**
|
|
252
|
+
Checkpoint **full** · safe point **after the analyst returns**. `soft` FORCES that checkpoint and does NOT stop if the write fails; `hard` skips it and can lose an in-flight return. Never begin a wait between a dispatch and its validated return, or before the smoke gate has reported.
|
|
@@ -241,4 +241,9 @@ exit code, and never re-derive a state word — the CLI's state words are the on
|
|
|
241
241
|
state words, and **an exit code is an ANSWER wherever that contract says so, not
|
|
242
242
|
a failure**. A call the answer does not name is a call this lane does not make.
|
|
243
243
|
Exit ≠ 0 from the catalogue itself → say the CLI is unavailable and name the
|
|
244
|
-
command you are about to run, out loud, before running it.
|
|
244
|
+
command you are about to run, out loud, before running it.
|
|
245
|
+
|
|
246
|
+
## Waiting mid-run (`/orc-wait`)
|
|
247
|
+
|
|
248
|
+
Canonical: `../_shared/wait.md`. **`a lane that waits without a hand-back` has broken this contract.**
|
|
249
|
+
Checkpoint **none** · safe point **read-only, seconds long**. Nothing here to checkpoint, so all three modes behave identically — say so rather than asking. Never begin a wait between a dispatch and its validated return, or before the smoke gate has reported.
|
|
@@ -369,4 +369,9 @@ exit code, and never re-derive a state word — the CLI's state words are the on
|
|
|
369
369
|
state words, and **an exit code is an ANSWER wherever that contract says so, not
|
|
370
370
|
a failure**. A call the answer does not name is a call this lane does not make.
|
|
371
371
|
Exit ≠ 0 from the catalogue itself → say the CLI is unavailable and name the
|
|
372
|
-
command you are about to run, out loud, before running it.
|
|
372
|
+
command you are about to run, out loud, before running it.
|
|
373
|
+
|
|
374
|
+
## Waiting mid-run (`/orc-wait`)
|
|
375
|
+
|
|
376
|
+
Canonical: `../_shared/wait.md`. **`a lane that waits without a hand-back` has broken this contract.**
|
|
377
|
+
Checkpoint **snapshot** · safe point **phase edge**. `soft` FORCES that checkpoint and does NOT stop if the write fails; `hard` skips it and can lose an in-flight return. Never begin a wait between a dispatch and its validated return, or before the smoke gate has reported.
|
|
@@ -240,4 +240,9 @@ exit code, and never re-derive a state word — the CLI's state words are the on
|
|
|
240
240
|
state words, and **an exit code is an ANSWER wherever that contract says so, not
|
|
241
241
|
a failure**. A call the answer does not name is a call this lane does not make.
|
|
242
242
|
Exit ≠ 0 from the catalogue itself → say the CLI is unavailable and name the
|
|
243
|
-
command you are about to run, out loud, before running it.
|
|
243
|
+
command you are about to run, out loud, before running it.
|
|
244
|
+
|
|
245
|
+
## Waiting mid-run (`/orc-wait`)
|
|
246
|
+
|
|
247
|
+
Canonical: `../_shared/wait.md`. **`a lane that waits without a hand-back` has broken this contract.**
|
|
248
|
+
Checkpoint **none** · safe point **read-only, seconds long**. Nothing here to checkpoint, so all three modes behave identically — say so rather than asking. Never begin a wait between a dispatch and its validated return, or before the smoke gate has reported.
|