@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.
- package/CHANGELOG.md +151 -0
- package/README.md +84 -34
- package/bin/cli.js +1110 -0
- package/bin/verify-contracts.js +112 -1
- 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 +227 -1
- package/templates/skills/_shared/phases/execution.md +2 -0
- package/templates/skills/_shared/phases/preflight.md +22 -0
- package/templates/skills/_shared/return-validation.md +222 -145
- package/templates/skills/_shared/wait.md +240 -0
- package/templates/skills/orc/SKILL.md +247 -238
- 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 +490 -481
- 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 +222 -215
- 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 +252 -244
- 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 +353 -346
- 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 +180 -171
|
@@ -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.2.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,84 @@ 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
|
+
// -- Session consumption (v1.2.0) -------------------------------------
|
|
130
|
+
// `usage.json` is a SNAPSHOT of the window. It cannot answer "how much
|
|
131
|
+
// has THIS session eaten", which is the question a user actually asks
|
|
132
|
+
// mid-run -- and the one they could otherwise only answer by remembering
|
|
133
|
+
// what the number was an hour ago.
|
|
134
|
+
//
|
|
135
|
+
// So keep a per-session ledger beside it: the reading when this session
|
|
136
|
+
// first rendered, and the reading now. Same rules as every other bridge
|
|
137
|
+
// here -- RAW numbers only, never a computed word, fail-silent, and the
|
|
138
|
+
// reader decides what it means.
|
|
139
|
+
//
|
|
140
|
+
// A window RESET mid-session (used_percentage drops) is not a refund:
|
|
141
|
+
// bank what was consumed before the reset into `accumulated` and
|
|
142
|
+
// re-baseline, so the running total keeps counting across the boundary.
|
|
143
|
+
const sid = String(d.session_id || d.sessionId || "");
|
|
144
|
+
const sfile = path.join(orcDir, "usage-session.json");
|
|
145
|
+
let led = null;
|
|
146
|
+
try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
|
|
147
|
+
const pctOf = (o) => (o && typeof o.used_percentage === "number" ? o.used_percentage : null);
|
|
148
|
+
const track = (prev, cur) => {
|
|
149
|
+
if (cur == null) return prev || null;
|
|
150
|
+
if (!prev) return { baseline: cur, last: cur, accumulated: 0, resets: 0 };
|
|
151
|
+
if (cur < prev.baseline)
|
|
152
|
+
return {
|
|
153
|
+
baseline: cur,
|
|
154
|
+
last: cur,
|
|
155
|
+
accumulated: prev.accumulated + Math.max(0, prev.last - prev.baseline),
|
|
156
|
+
resets: prev.resets + 1,
|
|
157
|
+
};
|
|
158
|
+
return { baseline: prev.baseline, last: cur, accumulated: prev.accumulated, resets: prev.resets };
|
|
159
|
+
};
|
|
160
|
+
if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
|
|
161
|
+
led.five_hour = track(led.five_hour, pctOf(rl0 && rl0.five_hour));
|
|
162
|
+
led.seven_day = track(led.seven_day, pctOf(rl0 && rl0.seven_day));
|
|
163
|
+
led.context_used_percentage =
|
|
164
|
+
cw0 && typeof cw0.used_percentage === "number" ? cw0.used_percentage : null;
|
|
165
|
+
led.updated_at = Date.now();
|
|
166
|
+
fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} catch (_) {}
|
|
170
|
+
|
|
93
171
|
// ── Subscription usage (Claude Code v2.1.80+) ──────────────────────────────
|
|
94
172
|
// Official 5-hour + 7-day usage, surfaced by Claude Code straight from
|
|
95
173
|
// Anthropic's API headers into this payload's `rate_limits`. Display-only,
|
|
@@ -197,6 +275,24 @@ process.stdin.on("end", () => {
|
|
|
197
275
|
// older Claude Code that doesn't surface `rate_limits`.
|
|
198
276
|
if (rlSeg) line += " · " + rlSeg;
|
|
199
277
|
|
|
278
|
+
// How far the window moved while THIS session ran (v1.2.0). The ledger below
|
|
279
|
+
// keeps the raw numbers; this renders the delta. Never shown as "this session
|
|
280
|
+
// used X%" — the window is per ACCOUNT, and a second terminal moves it too.
|
|
281
|
+
try {
|
|
282
|
+
const fs = require("fs");
|
|
283
|
+
const path = require("path");
|
|
284
|
+
const projectDir =
|
|
285
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
286
|
+
const led = JSON.parse(
|
|
287
|
+
fs.readFileSync(path.join(projectDir, ".claude", "orc", "usage-session.json"), "utf8")
|
|
288
|
+
);
|
|
289
|
+
const w = led && led.five_hour;
|
|
290
|
+
if (w && typeof w.last === "number" && typeof w.baseline === "number") {
|
|
291
|
+
const used = Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline));
|
|
292
|
+
if (used > 0) line += " · sess +" + used + "%";
|
|
293
|
+
}
|
|
294
|
+
} catch (_) {}
|
|
295
|
+
|
|
200
296
|
// Wiki freshness tier (computed on read from wiki-meta.json — zero model
|
|
201
297
|
// tokens; the manifest is written only by `orc wiki sync`). Fail-silent: no
|
|
202
298
|
// wiki / no git / any error → no segment. Thresholds mirror the config
|
|
@@ -301,5 +397,135 @@ process.stdin.on("end", () => {
|
|
|
301
397
|
} catch (_) {}
|
|
302
398
|
}
|
|
303
399
|
|
|
304
|
-
|
|
400
|
+
// ── Session line (v1.2.0) ──────────────────────────────────────────────────
|
|
401
|
+
// Line 1 answers "what tier am I on and how full is the window". This second
|
|
402
|
+
// line answers "what has this session actually been DOING" — how many agents
|
|
403
|
+
// it spawned, which lanes ran, whether work can leave Claude, and how long it
|
|
404
|
+
// has been going. All of it is read from disk; none of it costs a model call.
|
|
405
|
+
//
|
|
406
|
+
// The dispatch count is the one that earns its place. v1.2.0 exists because a
|
|
407
|
+
// retry cloned a live agent three times over and nothing surfaced it. A count
|
|
408
|
+
// that says `7 (2 running)` makes that visible from the status bar.
|
|
409
|
+
//
|
|
410
|
+
// Fail-silent and THROTTLED: the statusline re-renders on every keystroke, so
|
|
411
|
+
// the trace scan runs at most every 5s and its answer is cached in the same
|
|
412
|
+
// per-session ledger. Any error → no second line, never a broken one.
|
|
413
|
+
let line2 = "";
|
|
414
|
+
try {
|
|
415
|
+
const fs = require("fs");
|
|
416
|
+
const path = require("path");
|
|
417
|
+
const projectDir =
|
|
418
|
+
(d.workspace && d.workspace.project_dir) || d.cwd || process.cwd();
|
|
419
|
+
const orcDir = path.join(projectDir, ".claude", "orc");
|
|
420
|
+
const sfile = path.join(orcDir, "usage-session.json");
|
|
421
|
+
const sid = String(d.session_id || d.sessionId || "");
|
|
422
|
+
|
|
423
|
+
let led = null;
|
|
424
|
+
try { led = JSON.parse(fs.readFileSync(sfile, "utf8")); } catch (_) {}
|
|
425
|
+
if (!led || led.session_id !== sid) led = { session_id: sid, started_at: Date.now() };
|
|
426
|
+
|
|
427
|
+
// The hook cannot read the RESOLVED config — that is the lane resolver's
|
|
428
|
+
// job, and a hook has no lane — so this reads the two raw keys it needs
|
|
429
|
+
// straight from the file and takes the documented default
|
|
430
|
+
// otherwise — the same caveat the wiki segment above already carries. A
|
|
431
|
+
// user override shifts skill behaviour; this label follows the file.
|
|
432
|
+
let logRel = ".claude/orc/logs";
|
|
433
|
+
let extraOn = false;
|
|
434
|
+
try {
|
|
435
|
+
const raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
|
|
436
|
+
const ld = /^[ \t]*log_dir:[ \t]*["']?([^"'#\r\n]+)/m.exec(raw);
|
|
437
|
+
if (ld) logRel = ld[1].trim();
|
|
438
|
+
extraOn = /^[ \t]*extra_enabled:[ \t]*true[ \t]*$/m.test(raw);
|
|
439
|
+
} catch (_) {}
|
|
440
|
+
|
|
441
|
+
const now = Date.now();
|
|
442
|
+
// The scan interval is the ONE seam over this budget, on the
|
|
443
|
+
// ORC_TEST_PROBE_MS precedent: a test that proves the throttle by SLEEPING
|
|
444
|
+
// past it is a test that fails on a loaded machine, and a flake is recorded
|
|
445
|
+
// and removed, never retried away. Unset, this is byte-identical to a
|
|
446
|
+
// hardcoded 5000, and nothing in ORC ever sets it.
|
|
447
|
+
const scanEvery = (() => {
|
|
448
|
+
const n = Number(process.env.ORC_STATUSLINE_SCAN_MS);
|
|
449
|
+
return Number.isFinite(n) && n >= 0 ? n : 5000;
|
|
450
|
+
})();
|
|
451
|
+
const stale = !led.dispatch || typeof led.dispatch.scanned_at !== "number" ||
|
|
452
|
+
now - led.dispatch.scanned_at >= scanEvery;
|
|
453
|
+
if (stale) {
|
|
454
|
+
const logDir = path.isAbsolute(logRel) ? logRel : path.join(projectDir, logRel);
|
|
455
|
+
const sessionFloor = Math.floor((led.started_at || 0) / 1000) * 1000;
|
|
456
|
+
let spawns = 0;
|
|
457
|
+
let running = 0;
|
|
458
|
+
const lanes = [];
|
|
459
|
+
try {
|
|
460
|
+
for (const f of fs.readdirSync(logDir)) {
|
|
461
|
+
if (!f.startsWith("run-") || !f.endsWith(".txt")) continue;
|
|
462
|
+
const full = path.join(logDir, f);
|
|
463
|
+
// Only traces touched since this session began. A trace from last
|
|
464
|
+
// week is not this session's spend.
|
|
465
|
+
let st;
|
|
466
|
+
try { st = fs.statSync(full); } catch (_) { continue; }
|
|
467
|
+
if (st.mtimeMs < (led.started_at || 0)) continue;
|
|
468
|
+
const text = fs.readFileSync(full, "utf8");
|
|
469
|
+
// Count by the trace's OWN line timestamps, not the file's mtime. A
|
|
470
|
+
// run that was already going when this session started shares its
|
|
471
|
+
// file with the session before it, and mtime cannot tell the two
|
|
472
|
+
// apart — it would attribute the whole file to whoever looked last.
|
|
473
|
+
let mine = 0;
|
|
474
|
+
for (const raw of text.split("\n")) {
|
|
475
|
+
const t = /^\[(\d{2})(\d{2})(\d{2}) (\d{2}):(\d{2}):(\d{2})/.exec(raw);
|
|
476
|
+
if (!t) continue;
|
|
477
|
+
if (raw.indexOf("] hook") === -1 || raw.indexOf(" SPAWN ") === -1) continue;
|
|
478
|
+
const at = new Date(
|
|
479
|
+
2000 + Number(t[3]), Number(t[2]) - 1, Number(t[1]),
|
|
480
|
+
Number(t[4]), Number(t[5]), Number(t[6])
|
|
481
|
+
).getTime();
|
|
482
|
+
// Trace stamps have SECOND resolution and started_at has
|
|
483
|
+
// milliseconds, so a dispatch in the same second as the
|
|
484
|
+
// session start compares as earlier than it. Floor the
|
|
485
|
+
// boundary to the second the trace could actually express.
|
|
486
|
+
if (at >= sessionFloor) mine += 1;
|
|
487
|
+
}
|
|
488
|
+
spawns += mine;
|
|
489
|
+
let openHere = 0;
|
|
490
|
+
try {
|
|
491
|
+
const pend = JSON.parse(fs.readFileSync(full + ".pending.json", "utf8"));
|
|
492
|
+
if (Array.isArray(pend)) openHere = pend.length;
|
|
493
|
+
} catch (_) {}
|
|
494
|
+
running += openHere;
|
|
495
|
+
// A lane earns its name by having actually dispatched in this
|
|
496
|
+
// session — listing a lane that contributed nothing is noise.
|
|
497
|
+
if (mine > 0 || openHere > 0) {
|
|
498
|
+
const m = /^run-([a-z0-9-]+?)-.+-\d{6}-\d{6}\.txt$/.exec(f);
|
|
499
|
+
if (m && lanes.indexOf(m[1]) === -1) lanes.push(m[1]);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (_) {}
|
|
503
|
+
led.dispatch = { spawns, running, lanes, scanned_at: now };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
led.updated_at = now;
|
|
507
|
+
try {
|
|
508
|
+
fs.mkdirSync(orcDir, { recursive: true });
|
|
509
|
+
fs.writeFileSync(sfile, JSON.stringify(led) + "\n");
|
|
510
|
+
} catch (_) {}
|
|
511
|
+
|
|
512
|
+
const dsp = led.dispatch || { spawns: 0, running: 0, lanes: [] };
|
|
513
|
+
const parts = [];
|
|
514
|
+
// `running` is never hidden, because an agent still in flight is the thing
|
|
515
|
+
// a user most needs to see (v1.2.0). Zero is simply not printed.
|
|
516
|
+
parts.push(
|
|
517
|
+
"agents " + dsp.spawns + (dsp.running ? " (" + dsp.running + " running)" : "")
|
|
518
|
+
);
|
|
519
|
+
parts.push("orc-extra: " + (extraOn ? "on" : "off"));
|
|
520
|
+
// An empty lane list means no ORC lane has dispatched yet this session —
|
|
521
|
+
// an ANSWER, not a gap, so it keeps its slot and says so.
|
|
522
|
+
parts.push("lanes: " + (dsp.lanes.length ? dsp.lanes.join(", ") : "none yet"));
|
|
523
|
+
if (led.started_at)
|
|
524
|
+
parts.push(Math.max(0, Math.round((now - led.started_at) / 60000)) + "m");
|
|
525
|
+
line2 = " " + parts.join(" · ");
|
|
526
|
+
} catch (_) {
|
|
527
|
+
line2 = "";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
process.stdout.write(line2 ? line + "\n" + line2 : line);
|
|
305
531
|
});
|
|
@@ -91,6 +91,8 @@ the codifier); hold resolved patterns in run state.
|
|
|
91
91
|
invalidates a DONE task → re-run once, then set every reverse-`depends_on`
|
|
92
92
|
consumer to `stale_review`. **Worker failure/garbage/timeout:** flag +
|
|
93
93
|
continue the wave; audit and re-dispatch at the next batch checkpoint
|
|
94
|
+
|
|
95
|
+
**Before any re-dispatch, run `orc run inflight`** (0 clear · 1 in-flight · 2 unknown). A Task error does not kill the agent behind it, and exit 2 REFUSES by default — `a lane that re-dispatches over a live attempt` has broken the contract. Canonical: `../return-validation.md`.
|
|
94
96
|
(`requeued`, retry_count++). Hard retry cap 2 → STOP and surface.
|
|
95
97
|
|
|
96
98
|
<!-- /orc:layer -->
|
|
@@ -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 -->
|