@irythill/orq 0.4.0 → 0.6.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/README.md +62 -37
- package/README.pt-BR.md +59 -33
- package/dist/agents.js +59 -5
- package/dist/ci.js +4 -2
- package/dist/cli.js +5 -1
- package/dist/commands/agent.js +145 -31
- package/dist/commands/block.js +26 -3
- package/dist/commands/board.js +113 -31
- package/dist/commands/claim.js +2 -0
- package/dist/commands/completion.js +3 -3
- package/dist/commands/conductor.js +104 -0
- package/dist/commands/gc.js +48 -10
- package/dist/commands/handoff.js +1 -0
- package/dist/commands/pick.js +1 -1
- package/dist/commands/rounds.js +77 -3
- package/dist/commands/stats.js +1 -1
- package/dist/commands/verify.js +2 -0
- package/dist/commands/wait.js +7 -1
- package/dist/config.js +2 -1
- package/dist/diagnose.js +105 -0
- package/dist/notice.js +310 -0
- package/dist/pick.js +33 -4
- package/dist/process.js +7 -0
- package/dist/roles.js +11 -2
- package/dist/rounds.js +14 -5
- package/dist/stats.js +6 -4
- package/dist/testrun.js +4 -27
- package/package.json +1 -1
- package/skills/issue/SKILL.md +50 -45
- package/templates/workspace.CLAUDE.md +8 -6
package/dist/commands/board.js
CHANGED
|
@@ -16,18 +16,20 @@ import { collectCi, collectedAt, loadCache as loadCiCache } from "../ci.js";
|
|
|
16
16
|
import { getPath, loadConfig } from "../config.js";
|
|
17
17
|
import { writeAtomic } from "../fsutil.js";
|
|
18
18
|
import { m, setLang } from "../i18n.js";
|
|
19
|
+
import { argvModel } from "../agents.js";
|
|
19
20
|
import { alive, status as lockStatus } from "../lock.js";
|
|
20
21
|
import { expand, stateDir } from "../paths.js";
|
|
21
|
-
import { processTableAsync, treeMem } from "../process.js";
|
|
22
|
+
import { killedRc, processTableAsync, treeMem, treePids } from "../process.js";
|
|
22
23
|
import { collect } from "../quota.js";
|
|
23
24
|
import { collectRounds, mark, roundsSig } from "../rounds.js";
|
|
24
25
|
import { collectRuns, rows } from "../stats.js";
|
|
26
|
+
import { pendingNotices, readLog } from "../notice.js";
|
|
25
27
|
import { load as loadBlocks } from "./block.js";
|
|
26
28
|
import { herdrNamesAsync, nameOf } from "./claim.js";
|
|
27
29
|
import { setValue } from "./set.js";
|
|
28
30
|
export const USAGE = "orq board [-w <workspace>]... [--once] [--json] [--interval S] live board: queue, agents, conductors, quota";
|
|
29
31
|
const CLI = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
30
|
-
const QTTL = 300_000,
|
|
32
|
+
const QTTL = 300_000, HIDE_H = 6; // finished/dead rows older than ui.board_hide_hours (default 6) are folded into a count
|
|
31
33
|
const read = (f) => { try {
|
|
32
34
|
return readFileSync(f, "utf8");
|
|
33
35
|
}
|
|
@@ -35,19 +37,20 @@ catch {
|
|
|
35
37
|
return "";
|
|
36
38
|
} };
|
|
37
39
|
const models = new Map();
|
|
38
|
-
/** The model a run
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
/** The model of a run recorded before its state file had one, from its job file; a rerun on the same prefix
|
|
41
|
+
* overwrites that file, so another unit's job says nothing about this run. */
|
|
42
|
+
function modelOf(prefix, unit) {
|
|
43
|
+
if (!models.has(unit)) {
|
|
41
44
|
let v;
|
|
42
45
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const j = JSON.parse(read(`${prefix}.job.json`));
|
|
47
|
+
if (!j.unit || j.unit === unit)
|
|
48
|
+
v = argvModel(j.argv ?? []);
|
|
46
49
|
}
|
|
47
50
|
catch { /* no job file */ }
|
|
48
|
-
models.set(
|
|
51
|
+
models.set(unit, v);
|
|
49
52
|
}
|
|
50
|
-
return models.get(
|
|
53
|
+
return models.get(unit);
|
|
51
54
|
}
|
|
52
55
|
/** What the agent did last, from the tail of its log (stream JSON of claude/opencode, else the last text line). */
|
|
53
56
|
export function activity(log) {
|
|
@@ -88,9 +91,47 @@ function tail(file, bytes = 16_384) {
|
|
|
88
91
|
closeSync(fd);
|
|
89
92
|
}
|
|
90
93
|
}
|
|
94
|
+
// --- idle detection -----------------------------------------------------------------------------------------------
|
|
95
|
+
// An interactive (-T) agent that finished keeps its screen, and its pid, open: "running" for hours. Its tree's CPU time
|
|
96
|
+
// (/proc/<pid>/stat, utime+stime) barely moves while it only waits. Samples persist in <state>/board/cpu.json so a
|
|
97
|
+
// one-shot board and a restarted one see the same history: per unit, the CPU reading when it last did real work.
|
|
98
|
+
const IDLE_MIN = 30, ACTIVE_TICKS = 200; // 2 s of CPU (at 100 ticks/s) since the baseline counts as work
|
|
99
|
+
/** utime+stime, in clock ticks, summed over `pids` (Linux; undefined elsewhere or when nothing is readable). */
|
|
100
|
+
export function cpuTicks(pids) {
|
|
101
|
+
if (process.platform !== "linux")
|
|
102
|
+
return undefined;
|
|
103
|
+
let sum;
|
|
104
|
+
for (const p of pids) {
|
|
105
|
+
// comm (field 2) may hold spaces and parens: fields resume after the last ")"; state is field 3, utime/stime 14/15.
|
|
106
|
+
const raw = read(`/proc/${p}/stat`), f = raw.slice(raw.lastIndexOf(")") + 2).split(" ");
|
|
107
|
+
const t = Number(f[11]) + Number(f[12]);
|
|
108
|
+
if (!Number.isNaN(t))
|
|
109
|
+
sum = (sum ?? 0) + t;
|
|
110
|
+
}
|
|
111
|
+
return sum;
|
|
112
|
+
}
|
|
113
|
+
/** Seconds `unit` has been idle (no real CPU work since its baseline), or 0 while it is not; updates `base` in place. */
|
|
114
|
+
export function idleFor(base, unit, ticks, now, idleMin = IDLE_MIN) {
|
|
115
|
+
const b = base[unit];
|
|
116
|
+
if (!b || ticks < b.ticks || ticks - b.ticks >= ACTIVE_TICKS) {
|
|
117
|
+
base[unit] = { ticks, at: now };
|
|
118
|
+
return 0;
|
|
119
|
+
} // new, restarted tree, or worked
|
|
120
|
+
return now - b.at >= idleMin * 60_000 ? Math.round((now - b.at) / 1000) : 0;
|
|
121
|
+
}
|
|
122
|
+
const cpuFile = () => join(qdir(), "cpu.json");
|
|
123
|
+
function idleMinutes() {
|
|
124
|
+
const v = Number(getPath(loadConfig().cfg, "ui.board_idle_min") ?? IDLE_MIN);
|
|
125
|
+
return v > 0 ? v : IDLE_MIN;
|
|
126
|
+
}
|
|
91
127
|
function agents(table, now) {
|
|
92
|
-
const d = join(stateDir(), "agents"), out = []
|
|
93
|
-
|
|
128
|
+
const d = join(stateDir(), "agents"), out = [], running = new Set(), cpu = (() => { try {
|
|
129
|
+
return JSON.parse(read(cpuFile()));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return {};
|
|
133
|
+
} })();
|
|
134
|
+
let idleMin, files = [];
|
|
94
135
|
try {
|
|
95
136
|
files = readdirSync(d).filter((f) => f.endsWith(".json"));
|
|
96
137
|
}
|
|
@@ -105,24 +146,43 @@ function agents(table, now) {
|
|
|
105
146
|
}
|
|
106
147
|
const pid = Number(s.pid), at = Date.parse(String(s.at)), end = s.end ? Date.parse(String(s.end)) : undefined;
|
|
107
148
|
const prefix = String(s.prefix ?? "");
|
|
108
|
-
const st = s.status === "ok" || s.status === "fail" ? s.status : alive(pid) ? "running" : "died";
|
|
109
|
-
|
|
110
|
-
continue;
|
|
111
|
-
if ((st === "ok" || st === "fail") && now - (end ?? at) > RECENT_MS)
|
|
112
|
-
continue;
|
|
113
|
-
const a = { unit: f.slice(0, -5), title: String(s.title ?? ""), agent: String(s.agent ?? "?"), model: modelOf(prefix),
|
|
149
|
+
const st = s.status === "ok" && killedRc(Number(s.rc)) ? "killed" : s.status === "ok" || s.status === "fail" || s.status === "killed" ? s.status : alive(pid) ? "running" : "died";
|
|
150
|
+
const a = { unit: f.slice(0, -5), title: String(s.title ?? ""), agent: String(s.agent ?? "?"), model: typeof s.model === "string" ? s.model : modelOf(prefix, f.slice(0, -5)),
|
|
114
151
|
host: s.host, pid, issue: basename(dirname(prefix)), label: basename(prefix), state: st,
|
|
115
|
-
secs: Math.max(0, Math.round(((end ?? now) - at) / 1000)), cap: typeof s.cap === "number" ? s.cap : undefined };
|
|
152
|
+
secs: Math.max(0, Math.round(((end ?? now) - at) / 1000)), age: Math.max(0, Math.round((now - (end ?? at)) / 1000)), cap: typeof s.cap === "number" ? s.cap : undefined };
|
|
116
153
|
if (s.rc !== undefined)
|
|
117
154
|
a.rc = Number(s.rc);
|
|
118
155
|
if (st === "running") {
|
|
119
|
-
if (table)
|
|
156
|
+
if (table) {
|
|
120
157
|
a.mem = treeMem(pid, table);
|
|
158
|
+
const t = cpuTicks(treePids(pid, table));
|
|
159
|
+
if (t !== undefined) {
|
|
160
|
+
running.add(a.unit);
|
|
161
|
+
const i = idleFor(cpu, a.unit, t, now, idleMin ??= idleMinutes());
|
|
162
|
+
if (i) {
|
|
163
|
+
a.state = "idle";
|
|
164
|
+
a.idleSecs = i;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
121
168
|
a.activity = activity(tail(`${prefix}.log`))?.replace(/\s+/g, " ").replace(homedir(), "~").slice(0, 60);
|
|
122
169
|
}
|
|
123
170
|
out.push(a);
|
|
124
171
|
}
|
|
125
|
-
|
|
172
|
+
if (table) { // keep baselines of units still running; the rest are done or gone
|
|
173
|
+
for (const u of Object.keys(cpu))
|
|
174
|
+
if (!running.has(u))
|
|
175
|
+
delete cpu[u];
|
|
176
|
+
const text = JSON.stringify(cpu);
|
|
177
|
+
if (text !== read(cpuFile())) {
|
|
178
|
+
try {
|
|
179
|
+
mkdirSync(qdir(), { recursive: true });
|
|
180
|
+
writeAtomic(cpuFile(), text);
|
|
181
|
+
}
|
|
182
|
+
catch { /* best effort */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const rank = { running: 0, idle: 1, died: 1, killed: 2, fail: 2, ok: 3 };
|
|
126
186
|
return out.sort((x, y) => rank[x.state] - rank[y.state] || y.secs - x.secs);
|
|
127
187
|
}
|
|
128
188
|
const field = (t, k) => new RegExp(`${k}=(\\S+)`).exec(t)?.[1] ?? "";
|
|
@@ -268,7 +328,7 @@ export async function snapshot(wss, table) {
|
|
|
268
328
|
return {
|
|
269
329
|
at: new Date(now).toISOString(), mem: { used: totalmem() - freemem(), total: totalmem() }, workspaces: wss,
|
|
270
330
|
queue: { holders: q.holders.map(item), waiters: q.waiters.map(item) }, agents: agents(table, now),
|
|
271
|
-
conductors: await conductors(wss), quota: quota(now), ci: ci(wss, now), blocks: loadBlocks(), scoreboard: scoreboard(wss[0], now),
|
|
331
|
+
conductors: await conductors(wss), quota: quota(now), ci: ci(wss, now), blocks: loadBlocks(), scoreboard: scoreboard(wss[0], now), notices: pendingNotices(readLog(), now),
|
|
272
332
|
};
|
|
273
333
|
}
|
|
274
334
|
// --- render -----------------------------------------------------------------------------------------------------
|
|
@@ -307,7 +367,12 @@ const when = (sec) => {
|
|
|
307
367
|
};
|
|
308
368
|
const gb = (b) => (b ? `${(b / 2 ** 30).toFixed(1)} GB` : "-");
|
|
309
369
|
const fit = (s, n) => (s.length > n ? s.slice(0, Math.max(0, n - 1)) + "…" : s.padEnd(n));
|
|
310
|
-
|
|
370
|
+
/** Hours after which finished/dead agents leave the board (they stay in --json). */
|
|
371
|
+
export function hideHours() {
|
|
372
|
+
const v = Number(getPath(loadConfig().cfg, "ui.board_hide_hours") ?? HIDE_H);
|
|
373
|
+
return v > 0 ? v : HIDE_H;
|
|
374
|
+
}
|
|
375
|
+
export function render(s, theme, cols, mode = colorMode(), hideH = HIDE_H) {
|
|
311
376
|
const c = inks(theme, mode), W = Math.max(60, cols), out = [];
|
|
312
377
|
const meter = (pct, n) => {
|
|
313
378
|
const f = Math.min(n, Math.round(Math.max(0, pct) * n / 100)), k = pct >= 90 ? c.err : pct >= 70 ? c.wrn : c.ok;
|
|
@@ -322,6 +387,15 @@ export function render(s, theme, cols, mode = colorMode()) {
|
|
|
322
387
|
out.push(`${c.surf}${c.b}${c.acc} ORQ${c.b ? "\x1b[22m" : ""}${c.surf}${c.ink2} ${ws}${" ".repeat(Math.max(1, W - left.length - right.length))}${c.mut}${right}`);
|
|
323
388
|
const mp = Math.round(100 * s.mem.used / s.mem.total);
|
|
324
389
|
out.push(` ${c.mut}MEM${c.r} ${meter(mp, 24)} ${c.ink}${gb(s.mem.used)}${c.r} ${c.mut}/ ${gb(s.mem.total)}${c.r}`);
|
|
390
|
+
// Red line(s) first: a failure the conductor may not know about is what a person must see before anything else.
|
|
391
|
+
if (s.notices.length) {
|
|
392
|
+
const ago = (at) => dur(Math.max(0, Math.round((new Date(s.at).getTime() - at) / 1000)));
|
|
393
|
+
out.push(` ${c.err}${c.b}✗ ${m(`${s.notices.length} failure(s) the conductor may not know about`, `${s.notices.length} falha(s) que o conductor pode não saber`)}${c.r}`);
|
|
394
|
+
for (const n of s.notices.slice(0, 5)) {
|
|
395
|
+
const what = n.line.replace(/^\[[^\]]+\] /, "").replace(/ answer: .*/, "");
|
|
396
|
+
out.push(` ${c.err} ${fit(n.issue ?? "-", 10)}${c.r} ${c.ink}${fit(what, Math.max(20, W - 44))}${c.r} ${c.err}${n.why === "undelivered" ? m("not delivered", "não entregue") : m("not acknowledged", "sem resposta")}${c.r} ${c.mut}${ago(n.at)}${m(" ago", " atrás")}${c.r}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
325
399
|
section("TRANSPORT", m("test queue", "fila de testes"));
|
|
326
400
|
for (const h of s.queue.holders)
|
|
327
401
|
out.push(` ${c.acc}▶ REC${c.r} ${c.ink}${fit(h.label, 34)}${c.r} ${c.ok}${m("running", "rodando")}${c.r} ${c.ink}${dur(h.secs).padEnd(7)}${c.r} ${c.ink2}${gb(h.mem)}${c.r}`
|
|
@@ -330,21 +404,29 @@ export function render(s, theme, cols, mode = colorMode()) {
|
|
|
330
404
|
if (!s.queue.holders.length && !s.queue.waiters.length)
|
|
331
405
|
out.push(` ${c.mut}${m("empty", "vazia")}${c.r}`);
|
|
332
406
|
section("TRACKS", m("agents", "agentes"));
|
|
333
|
-
|
|
407
|
+
// A finished or dead agent stops being news after a while; running ones always show. age = since its end (a
|
|
408
|
+
// died row's end is unknown: since its start).
|
|
409
|
+
const old = (a) => a.state !== "running" && a.state !== "idle" && a.age > hideH * 3600;
|
|
410
|
+
const shownAgents = s.agents.filter((a) => !old(a)), hidden = s.agents.length - shownAgents.length;
|
|
411
|
+
shownAgents.forEach((a, i) => {
|
|
334
412
|
const n = `${c.mut}${String(i + 1).padStart(2, "0")}${c.r}`, who = `${c.ink}${fit(a.issue, 9)}${c.r} ${c.ink2}${fit(a.label, 22)}${c.r} `
|
|
335
|
-
+ `${c.ink}${fit(a.agent, 11)}${c.r}${c.mut}${fit(a.model ?? m("default", "padrão"), 17)}${c.r} ${c.ink}${dur(a.secs).padEnd(7)}${c.r}`;
|
|
336
|
-
if (a.state === "running") {
|
|
413
|
+
+ `${c.ink}${fit(a.agent, 11)}${c.r}${c.mut}${fit(a.model ?? m("default", "padrão"), 17)}${c.r} ${c.ink}${(a.state === "died" ? "?" : dur(a.secs)).padEnd(7)}${c.r}`; // died: when it stopped is unknown
|
|
414
|
+
if (a.state === "running" || a.state === "idle") {
|
|
337
415
|
const pct = a.cap && a.mem ? Math.round(100 * a.mem / a.cap) : 0, used = 1 + 2 + 2 + 10 + 23 + 11 + 17 + 1 + 8 + 6 + 1 + 7 + 1;
|
|
338
|
-
const
|
|
339
|
-
|
|
416
|
+
const idle = a.state === "idle", mark = idle ? `${c.wrn}◌` : `${c.acc}●`;
|
|
417
|
+
const warn = !idle && a.secs > 3 * 3600 ? ` ${c.err}⚠ ${m("for", "há")} ${dur(a.secs)}${c.r}` : "";
|
|
418
|
+
const what = idle ? `${c.wrn}${m("idle", "ocioso")} ${dur(a.idleSecs ?? 0)}` : fit(a.activity ?? "", Math.max(0, W - used - 2)).trimEnd();
|
|
419
|
+
out.push(` ${mark}${c.r} ${n} ${who} ${meter(pct, 4)} ${c.ink2}${gb(a.mem).padEnd(6)}${c.r} ${c.mut}${what}${c.r}${warn}`);
|
|
340
420
|
}
|
|
341
421
|
else if (a.state === "died")
|
|
342
422
|
out.push(` ${c.err}●${c.r} ${n} ${who} ${c.err}${m("runner gone without finishing", "runner sumiu sem terminar")}${c.r}`);
|
|
343
423
|
else
|
|
344
424
|
out.push(` ${a.state === "ok" ? c.ok : c.err}${a.state === "ok" ? "✓" : "✗"}${c.r} ${n} ${c.mut}${a.issue.padEnd(9).slice(0, 9)} ${fit(a.label, 22)} ${fit(a.agent, 11)}${fit(a.model ?? "", 17)} ${dur(a.secs).padEnd(7)} ${a.state} rc=${a.rc ?? "?"}${c.r}`);
|
|
345
425
|
});
|
|
346
|
-
if (!
|
|
426
|
+
if (!shownAgents.length)
|
|
347
427
|
out.push(` ${c.mut}${m("none", "nenhum")}${c.r}`);
|
|
428
|
+
if (hidden)
|
|
429
|
+
out.push(` ${c.mut}${m(`+${hidden} older finished hidden`, `+${hidden} antigos finalizados ocultos`)}${c.r}`);
|
|
348
430
|
section("MASTER", m("conductors", "conductors"));
|
|
349
431
|
for (const k of s.conductors)
|
|
350
432
|
out.push(` ${c.ok}●${c.r} ${c.mut}${fit(k.name || k.id, 22)}${c.r} ${c.ink2}${fit(k.account || "-", 14)}${c.r} ${c.ink}${k.issues.map((i) => (k.rounds?.[i] ? `${i} ${mark(k.rounds[i])}` : i)).join(" ")}${c.r}`);
|
|
@@ -429,7 +511,7 @@ export async function board(argv) {
|
|
|
429
511
|
}
|
|
430
512
|
const tty = Boolean(process.stdout.isTTY);
|
|
431
513
|
if (once || !tty) {
|
|
432
|
-
const lines = render(await snapshot(wss, await processTableAsync()), theme, process.stdout.columns ?? 100);
|
|
514
|
+
const lines = render(await snapshot(wss, await processTableAsync()), theme, process.stdout.columns ?? 100, colorMode(), hideHours());
|
|
433
515
|
const c = inks(theme);
|
|
434
516
|
process.stdout.write(lines.map((l) => c.base + l + (c.base ? "\x1b[K" : "")).join("\n") + (c.base ? "\x1b[0m" : "") + "\n");
|
|
435
517
|
return 0;
|
|
@@ -445,7 +527,7 @@ export async function board(argv) {
|
|
|
445
527
|
}
|
|
446
528
|
drawing = true;
|
|
447
529
|
try {
|
|
448
|
-
const c = inks(theme), lines = render(await snapshot(wss, table), theme, out.columns ?? 100);
|
|
530
|
+
const c = inks(theme), lines = render(await snapshot(wss, table), theme, out.columns ?? 100, colorMode(), hideHours());
|
|
449
531
|
out.write(`${c.base}\x1b[H` + lines.map((l) => c.base + l + "\x1b[K").join("\n") + `${c.base}\x1b[J`);
|
|
450
532
|
}
|
|
451
533
|
finally {
|
package/dist/commands/claim.js
CHANGED
|
@@ -12,6 +12,7 @@ import { basename, join, resolve } from "node:path";
|
|
|
12
12
|
import { writeAtomic } from "../fsutil.js";
|
|
13
13
|
import { m } from "../i18n.js";
|
|
14
14
|
import { acquire, release } from "../lock.js";
|
|
15
|
+
import { handoffFile } from "../notice.js";
|
|
15
16
|
export const USAGE = `orq claim [-w <workspace>] <ID> | -f <ID> | -r <ID> | -l | --register issue owner among conductors (exit 1 = someone else's)
|
|
16
17
|
orq claim [-w <workspace>] --from <name> [-f] a conductor's issues by its name (part of it is enough); -f takes them all`;
|
|
17
18
|
const field = (text, k) => new RegExp(`${k}=(\\S+)`).exec(text)?.[1] ?? "";
|
|
@@ -139,6 +140,7 @@ export async function claim(argv) {
|
|
|
139
140
|
if (mode === "register") {
|
|
140
141
|
if (kind !== "herdr")
|
|
141
142
|
return 0;
|
|
143
|
+
rmSync(handoffFile(me), { force: true }); // this pane is a live conductor again: drop a stale `orq handoff` redirect out of it
|
|
142
144
|
const dir = join(O, ".conductors");
|
|
143
145
|
mkdirSync(dir, { recursive: true });
|
|
144
146
|
writeAtomic(join(dir, me.replace(/[^A-Za-z0-9_-]/g, "_")), `pane=${me} kind=herdr account=${account} at=${new Date().toISOString()} name=${myName || me}\n`);
|
|
@@ -11,16 +11,16 @@ const DESC = {
|
|
|
11
11
|
quota: "quota per agent/account", pick: "pick agent, model and account for a role", stats: "scoreboard per agent", rounds: "rounds, time and repeated findings of an issue",
|
|
12
12
|
board: "live board", lock: "test queue", run: "run a command under a memory cap", agent: "run a child agent",
|
|
13
13
|
wait: "wait for child agents", verify: "tests of a diff", baseline: "tests already failing on the base",
|
|
14
|
-
instructions: "write the /issue playbook", claim: "issue owner among conductors", deps: "link node_modules into a worktree", gc: "remove merged issue worktrees", ci: "CI/CD status per claimed issue", handoff: "move a conductor low on quota to another account",
|
|
14
|
+
instructions: "write the /issue playbook", conductor: "open the conductor (Claude) in this terminal", claim: "issue owner among conductors", deps: "link node_modules into a worktree", gc: "remove merged issue worktrees", ci: "CI/CD status per claimed issue", handoff: "move a conductor low on quota to another account",
|
|
15
15
|
block: "block an agent or account", completion: "shell completion script",
|
|
16
16
|
};
|
|
17
17
|
/** Fixed words after a subcommand. */
|
|
18
18
|
export const WORDS = {
|
|
19
19
|
setup: ["--preset", "--detect", "--answers", "--write", "--install", "--workspace"],
|
|
20
20
|
config: ["get", "files", "-w"], lang: ["pt", "en", "auto", "toggle"], lock: ["wait", "release", "run", "status"],
|
|
21
|
-
pick: ["impl", "fix", "worker", "review", "opinion"], instructions: ["claude", "agents"], board: ["--once", "--json", "-w"],
|
|
21
|
+
pick: ["impl", "fix", "worker", "review", "opinion", "watchdog"], instructions: ["claude", "agents"], board: ["--once", "--json", "-w"],
|
|
22
22
|
completion: ["bash", "zsh", "fish", "powershell"], stats: ["--days", "--json", "-w"], wait: ["--any", "--timeout", "--answer"],
|
|
23
|
-
ci: ["--json", "-w"], rounds: ["--json", "-w"],
|
|
23
|
+
ci: ["--json", "-w"], rounds: ["--json", "--diagnose", "-w"],
|
|
24
24
|
};
|
|
25
25
|
const q = (s) => `'${s.replace(/'/g, "''")}'`; // PowerShell single quotes
|
|
26
26
|
const qf = (s) => `'${s.replace(/[\\']/g, "\\$&")}'`; // fish single quotes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// orq conductor — opens the conductor (Claude Code) in the current terminal, with the workspace's conductor
|
|
2
|
+
// instructions, the chosen account and model (port of setup/herdr-conductor, for any terminal and OS).
|
|
3
|
+
// orq conductor [account|-] [model] [-r <session>]
|
|
4
|
+
// Workspace: the current folder if it has .orquestra-conductor.md, else $CONDUCTOR_WS. $CONDUCTOR_FILE overrides the
|
|
5
|
+
// instructions file. The session is named conductor-<workspace folder>.
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { basename, join, resolve } from "node:path";
|
|
9
|
+
import { claudeAccounts } from "../accounts.js";
|
|
10
|
+
import { m } from "../i18n.js";
|
|
11
|
+
import { runCapped } from "../process.js";
|
|
12
|
+
import { claim } from "./claim.js";
|
|
13
|
+
export const USAGE = `orq conductor [account|-] [model] [-r <session>] open the conductor (Claude) here: instructions, account, model
|
|
14
|
+
model: sonnet | opus | haiku | a full id; "-" as account = default account; -r resumes that conversation`;
|
|
15
|
+
export const MODELS = {
|
|
16
|
+
sonnet: "claude-sonnet-5",
|
|
17
|
+
opus: "claude-opus-5-5",
|
|
18
|
+
haiku: "claude-haiku-4-5-20251001",
|
|
19
|
+
};
|
|
20
|
+
export const modelId = (alias) => MODELS[alias] ?? alias;
|
|
21
|
+
/** [account|-] [model] [-r|--resume <id>]; further positionals are ignored (as herdr-conductor did). */
|
|
22
|
+
export function parseArgs(argv) {
|
|
23
|
+
const out = { account: "", model: "", resume: "", help: false };
|
|
24
|
+
const pos = [];
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const a = argv[i];
|
|
27
|
+
if (a === "-r" || a === "--resume") {
|
|
28
|
+
const v = argv[++i];
|
|
29
|
+
if (!v) {
|
|
30
|
+
out.error = m("give the session id", "informe o id da sessão");
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
out.resume = v;
|
|
34
|
+
}
|
|
35
|
+
else if (a === "-h" || a === "--help")
|
|
36
|
+
out.help = true;
|
|
37
|
+
else
|
|
38
|
+
pos.push(a);
|
|
39
|
+
}
|
|
40
|
+
[out.account = "", out.model = ""] = pos;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** The workspace folder: cwd when it holds .orquestra-conductor.md, else $CONDUCTOR_WS (resolved); undefined if neither. */
|
|
44
|
+
export function findWorkspace(cwd, env, exists = existsSync) {
|
|
45
|
+
if (exists(join(cwd, ".orquestra-conductor.md")))
|
|
46
|
+
return cwd;
|
|
47
|
+
return env.CONDUCTOR_WS ? resolve(cwd, env.CONDUCTOR_WS) : undefined;
|
|
48
|
+
}
|
|
49
|
+
export const instructionsFile = (ws, env) => env.CONDUCTOR_FILE ? resolve(env.CONDUCTOR_FILE) : join(ws, ".orquestra-conductor.md");
|
|
50
|
+
/** The claude command line (without the executable). The instructions go by file, so no shell ever sees their text. */
|
|
51
|
+
export function claudeArgs(ws, a, file) {
|
|
52
|
+
return [...(a.resume ? ["--resume", a.resume] : []), "-n", `conductor-${basename(ws)}`,
|
|
53
|
+
...(a.model ? ["--model", modelId(a.model)] : []), "--append-system-prompt-file", file];
|
|
54
|
+
}
|
|
55
|
+
export async function conductor(argv) {
|
|
56
|
+
const a = parseArgs(argv);
|
|
57
|
+
if (a.help) {
|
|
58
|
+
console.log(USAGE);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
if (a.error) {
|
|
62
|
+
console.error(a.error);
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
const ws = findWorkspace(process.cwd(), process.env);
|
|
66
|
+
if (!ws) {
|
|
67
|
+
console.error(m("no .orquestra-conductor.md here: cd into a workspace (orq setup --workspace <dir>) or set CONDUCTOR_WS", "não há .orquestra-conductor.md aqui: entre num workspace (orq setup --workspace <pasta>) ou defina CONDUCTOR_WS"));
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
const file = instructionsFile(ws, process.env);
|
|
71
|
+
if (!existsSync(file)) {
|
|
72
|
+
console.error(`${m("conductor instructions not found", "instruções do conductor não encontradas")}: ${file}`);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
const env = { ...process.env };
|
|
76
|
+
if (a.account && a.account !== "-") {
|
|
77
|
+
const accounts = claudeAccounts(), found = accounts.find((x) => x.name === a.account);
|
|
78
|
+
if (!found) {
|
|
79
|
+
console.error(m(`account '${a.account}' does not exist. Accounts:`, `a conta '${a.account}' não existe. Contas:`));
|
|
80
|
+
for (const x of accounts)
|
|
81
|
+
console.error(`${x.name} ${x.dir}`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
env.CLAUDE_CONFIG_DIR = found.dir;
|
|
85
|
+
}
|
|
86
|
+
if (process.env.HERDR_ENV === "1" && process.env.HERDR_PANE_ID)
|
|
87
|
+
spawnSync("herdr", ["pane", "rename", process.env.HERDR_PANE_ID, "conductor"], { stdio: "ignore", timeout: 5_000 });
|
|
88
|
+
process.chdir(ws);
|
|
89
|
+
// Registration (.orquestra/.conductors) is a herdr thing (keyed by pane id; `claim --register` does nothing elsewhere),
|
|
90
|
+
// so only inside herdr. claim reads the account from this process's env (best effort, silent).
|
|
91
|
+
const saved = process.env.CLAUDE_CONFIG_DIR;
|
|
92
|
+
if (env.CLAUDE_CONFIG_DIR)
|
|
93
|
+
process.env.CLAUDE_CONFIG_DIR = env.CLAUDE_CONFIG_DIR;
|
|
94
|
+
if (process.env.HERDR_PANE_ID)
|
|
95
|
+
try {
|
|
96
|
+
await claim(["-w", ws, "--register"]);
|
|
97
|
+
}
|
|
98
|
+
catch { /* best effort */ }
|
|
99
|
+
if (saved === undefined)
|
|
100
|
+
delete process.env.CLAUDE_CONFIG_DIR;
|
|
101
|
+
else
|
|
102
|
+
process.env.CLAUDE_CONFIG_DIR = saved;
|
|
103
|
+
return runCapped("claude", claudeArgs(ws, a, file), { mode: "off", env, cwd: ws, quote: true, tty: true });
|
|
104
|
+
}
|
package/dist/commands/gc.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
// orq gc — find issue worktrees whose branch is already safe to remove.
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
-
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { m } from "../i18n.js";
|
|
7
|
+
import { alive } from "../lock.js";
|
|
8
|
+
import { stateDir } from "../paths.js";
|
|
7
9
|
import { aliveOwner } from "./claim.js";
|
|
8
|
-
export const USAGE = "orq gc [-w <workspace>] [--yes] [--days N] [--json] remove merged issue worktrees";
|
|
10
|
+
export const USAGE = "orq gc [-w <workspace>] [--yes] [--days N] [--json] remove merged issue worktrees and stale agent state";
|
|
9
11
|
const git = (cwd, ...args) => spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
|
|
10
12
|
// Paths as git prints them can differ from ours (macOS /var → /private/var, Windows C:/ vs C:\): compare real paths.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
13
|
+
// A path that does not exist (yet) is resolved through its deepest existing ancestor, so both sides get the same prefix.
|
|
14
|
+
const real = (p) => {
|
|
15
|
+
const abs = resolve(p);
|
|
16
|
+
try {
|
|
17
|
+
return realpathSync.native(abs);
|
|
18
|
+
}
|
|
19
|
+
catch { /* missing: resolve the parent */ }
|
|
20
|
+
const up = dirname(abs);
|
|
21
|
+
return up === abs ? abs : join(real(up), basename(abs));
|
|
22
|
+
};
|
|
23
|
+
const fold = (p) => (process.platform === "win32" ? p.toLowerCase() : p); // NTFS is case-insensitive
|
|
24
|
+
const inside = (dir, p) => { const r = relative(fold(dir), fold(p)); return !!r && !r.startsWith("..") && !isAbsolute(r); };
|
|
18
25
|
// ORQ_GH may be a .js file (tests: a fake gh that also runs on Windows).
|
|
19
26
|
const gh = (cwd, args) => {
|
|
20
27
|
const bin = process.env.ORQ_GH || "gh";
|
|
@@ -22,6 +29,30 @@ const gh = (cwd, args) => {
|
|
|
22
29
|
: spawnSync(bin, args, { cwd, encoding: "utf8" });
|
|
23
30
|
};
|
|
24
31
|
const output = (cwd, ...args) => { const r = git(cwd, ...args); return r.status === 0 ? r.stdout.trim() : undefined; };
|
|
32
|
+
/** Per-agent state files (<state>/agents/<unit>.json) of runs under `ws` that are finished or dead and older than `days`. */
|
|
33
|
+
function staleAgentStates(ws, days, now) {
|
|
34
|
+
const dir = join(stateDir(), "agents"), out = [];
|
|
35
|
+
let files = [];
|
|
36
|
+
try {
|
|
37
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
38
|
+
}
|
|
39
|
+
catch { /* no runs yet */ }
|
|
40
|
+
for (const f of files) {
|
|
41
|
+
try {
|
|
42
|
+
const s = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
43
|
+
if (!inside(real(ws), real(String(s.prefix ?? ""))))
|
|
44
|
+
continue;
|
|
45
|
+
const finished = s.status === "ok" || s.status === "fail" || s.status === "killed";
|
|
46
|
+
if (!finished && alive(Number(s.pid)))
|
|
47
|
+
continue;
|
|
48
|
+
const since = Date.parse(String(s.end ?? s.at));
|
|
49
|
+
if (now - since > days * 86400_000)
|
|
50
|
+
out.push(join(dir, f));
|
|
51
|
+
}
|
|
52
|
+
catch { /* unreadable state: leave it */ }
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
25
56
|
export async function gc(argv, clock = Date.now) {
|
|
26
57
|
let ws = process.cwd(), yes = false, json = false, days = 14;
|
|
27
58
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -153,6 +184,10 @@ export async function gc(argv, clock = Date.now) {
|
|
|
153
184
|
}
|
|
154
185
|
git(join(ws, row.repo), "worktree", "prune");
|
|
155
186
|
}
|
|
187
|
+
const stale = staleAgentStates(ws, days, clock());
|
|
188
|
+
if (yes)
|
|
189
|
+
for (const f of stale)
|
|
190
|
+
rmSync(f, { force: true });
|
|
156
191
|
if (json)
|
|
157
192
|
console.log(JSON.stringify(rows));
|
|
158
193
|
else {
|
|
@@ -160,5 +195,8 @@ export async function gc(argv, clock = Date.now) {
|
|
|
160
195
|
const w = table[0].map((_, i) => Math.max(...table.map((t) => t[i].length)));
|
|
161
196
|
console.log(table.map((t) => t.map((c, i) => i < t.length - 1 ? c.padEnd(w[i]) : c).join(" ")).join("\n"));
|
|
162
197
|
}
|
|
198
|
+
if (stale.length && !json)
|
|
199
|
+
console.log(yes ? m(`pruned ${stale.length} stale agent state file(s)`, `${stale.length} arquivo(s) de estado de agente antigo(s) removido(s)`)
|
|
200
|
+
: m(`${stale.length} stale agent state file(s) (finished/dead, older than ${days} days): --yes removes them`, `${stale.length} arquivo(s) de estado de agente antigo(s) (terminados/mortos, mais de ${days} dias): --yes remove`));
|
|
163
201
|
return failed ? 1 : 0;
|
|
164
202
|
}
|
package/dist/commands/handoff.js
CHANGED
|
@@ -163,6 +163,7 @@ async function move(ws, c, to, dir, model, mark) {
|
|
|
163
163
|
for (const id of c.issues)
|
|
164
164
|
writeAtomic(join(ws, ".orquestra", id, "owner"), `pane=${pane} kind=herdr account=${basename(dir)} at=${new Date().toISOString()} name=${label}\n`);
|
|
165
165
|
writeAtomic(handoffFile(c.pane), pane);
|
|
166
|
+
rmSync(handoffFile(pane), { force: true }); // the new pane is live: a leftover redirect out of it would make a cycle
|
|
166
167
|
const regs = join(ws, ".orquestra", ".conductors");
|
|
167
168
|
mkdirSync(regs, { recursive: true });
|
|
168
169
|
writeAtomic(join(regs, pane.replace(/[^A-Za-z0-9_-]/g, "_")), `pane=${pane} kind=herdr account=${basename(dir)} at=${new Date().toISOString()} name=${label}\n`);
|
package/dist/commands/pick.js
CHANGED
|
@@ -13,7 +13,7 @@ import { collect } from "../quota.js";
|
|
|
13
13
|
import { PICK_ROLES, resolveRole, vendorOf } from "../roles.js";
|
|
14
14
|
import { collectRuns, rows } from "../stats.js";
|
|
15
15
|
import { load as loadBlocks } from "./block.js";
|
|
16
|
-
export const USAGE = `orq pick <impl|fix|worker|review|opinion> [--level simple|medium|heavy] [--impl-vendor <v>|--impl-agent <a>]
|
|
16
|
+
export const USAGE = `orq pick <impl|fix|worker|review|opinion|watchdog> [--level simple|medium|heavy] [--impl-vendor <v>|--impl-agent <a>]
|
|
17
17
|
[--exclude-agent <a>]... [-w <workspace>] [--explain] [--no-record]
|
|
18
18
|
orq pick fix --same <impl pick file> same agent/model/account as the implementer, effort one level up`;
|
|
19
19
|
// The board refreshes this file; fresher than 10 min → no new quota requests.
|
package/dist/commands/rounds.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
// orq rounds <ID> [-w <workspace>] [--json] per-issue round metrics from .orquestra/<ID>/ (src/rounds.ts), no LLM
|
|
2
|
+
// orq rounds <ID> --diagnose [-w <workspace>] warn/stop only: a cheap read only "watchdog" agent (another vendor than the
|
|
3
|
+
// implementer when known) diagnoses why the rounds don't converge → .orquestra/<ID>/diagnosis-<rounds>.md (advisory)
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
2
9
|
import { getPath, loadConfig } from "../config.js";
|
|
10
|
+
import { buildPrompt, evidence, latestDiagnosis, recommendation } from "../diagnose.js";
|
|
3
11
|
import { m } from "../i18n.js";
|
|
4
12
|
import { expand } from "../paths.js";
|
|
5
13
|
import { collectRounds, mark } from "../rounds.js";
|
|
6
|
-
|
|
14
|
+
import { alive } from "../lock.js";
|
|
15
|
+
import { wait } from "./wait.js";
|
|
16
|
+
export const USAGE = `orq rounds <ID> [-w <workspace>] [--json] rounds vs max_rounds, time per round, repeated findings, alert level
|
|
17
|
+
orq rounds <ID> --diagnose [-w <workspace>] on warn/stop: an independent cheap agent diagnoses why rounds don't converge (advisory; needed for orq agent -R -F)`;
|
|
18
|
+
export const WATCHDOG_TIMEOUT = 900; // seconds `orq rounds --diagnose` waits for the agent
|
|
19
|
+
const CLI = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
7
20
|
const dur = (s) => (s >= 3600 ? `${Math.floor(s / 3600)}h${String(Math.floor(s / 60) % 60).padStart(2, "0")}m` : s >= 60 ? `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s` : `${s}s`);
|
|
8
21
|
const why = (r) => ({
|
|
9
22
|
over_max: m(`rounds ${r.n} > max_rounds ${r.max}`, `rodadas ${r.n} > max_rounds ${r.max}`),
|
|
@@ -12,8 +25,8 @@ const why = (r) => ({
|
|
|
12
25
|
repeated: m(`${r.n} repeated finding(s)`, `${r.n} achado(s) repetido(s)`),
|
|
13
26
|
slow: m(`round ${r.rounds?.join(", ")} much slower than the others`, `rodada ${r.rounds?.join(", ")} bem mais lenta que as outras`),
|
|
14
27
|
})[r.code];
|
|
15
|
-
export function rounds(argv) {
|
|
16
|
-
let ws, id, json = false;
|
|
28
|
+
export async function rounds(argv) {
|
|
29
|
+
let ws, id, json = false, diagnose = false;
|
|
17
30
|
for (let i = 0; i < argv.length; i++) {
|
|
18
31
|
if (argv[i] === "-w") {
|
|
19
32
|
if (!argv[i + 1]) {
|
|
@@ -24,6 +37,8 @@ export function rounds(argv) {
|
|
|
24
37
|
}
|
|
25
38
|
else if (argv[i] === "--json")
|
|
26
39
|
json = true;
|
|
40
|
+
else if (argv[i] === "--diagnose")
|
|
41
|
+
diagnose = true;
|
|
27
42
|
else if (!id && !argv[i].startsWith("-"))
|
|
28
43
|
id = argv[i];
|
|
29
44
|
else {
|
|
@@ -42,6 +57,8 @@ export function rounds(argv) {
|
|
|
42
57
|
console.error(m(`no rounds recorded for ${id} in ${ws}/.orquestra`, `nenhuma rodada registrada para ${id} em ${ws}/.orquestra`));
|
|
43
58
|
return 1;
|
|
44
59
|
}
|
|
60
|
+
if (diagnose)
|
|
61
|
+
return diagnoseIssue(ws, id, x);
|
|
45
62
|
if (json) {
|
|
46
63
|
console.log(JSON.stringify(x, null, 2));
|
|
47
64
|
return 0;
|
|
@@ -54,5 +71,62 @@ export function rounds(argv) {
|
|
|
54
71
|
console.log(m("repeated findings:", "achados repetidos:"));
|
|
55
72
|
for (const r of x.repeats)
|
|
56
73
|
console.log(` [${m("rounds", "rodadas")} ${r.rounds.join(",")}] ${r.title.slice(0, 100)}`);
|
|
74
|
+
const d = latestDiagnosis(join(ws, ".orquestra", id));
|
|
75
|
+
if (d)
|
|
76
|
+
console.log(`${m("diagnosis", "diagnóstico")} ${d.n}: ${d.line ?? m("(no RECOMMENDATION line)", "(sem linha RECOMMENDATION)")}`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const orq = (...a) => spawnSync(process.execPath, [CLI, ...a], { encoding: "utf8" });
|
|
80
|
+
/** --diagnose: pick a watchdog, run it read only on the evidence, keep its answer as diagnosis-<rounds>.md. */
|
|
81
|
+
async function diagnoseIssue(ws, id, x) {
|
|
82
|
+
if (x.level === "ok") {
|
|
83
|
+
console.log(m(`${id}: level is ok, no diagnosis needed`, `${id}: nível ok, não precisa de diagnóstico`));
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const dir = join(ws, ".orquestra", id), prefix = join(dir, `watchdog-${x.rounds}-${Date.now()}`), out = join(dir, `diagnosis-${x.rounds}.md`);
|
|
87
|
+
// The implementer, when known (impl-<repo>.pick saved by the playbook): the watchdog must be another vendor.
|
|
88
|
+
const implPick = readdirSync(dir).filter((n) => /^impl-.+\.pick$/.test(n)).map((n) => /(?:^|\s)-a (\S+)/.exec(readFileSync(join(dir, n), "utf8"))?.[1]).find(Boolean);
|
|
89
|
+
// A watchdog still running for this issue (a previous --diagnose that timed out or was interrupted): don't start another.
|
|
90
|
+
for (const n of readdirSync(dir).filter((f) => /^watchdog-.+\.pid$/.test(f))) {
|
|
91
|
+
const pid = Number(readFileSync(join(dir, n), "utf8"));
|
|
92
|
+
if (pid && alive(pid)) {
|
|
93
|
+
console.error(m(`a watchdog for ${id} is still running (${n}, pid ${pid}); wait for it or stop it first`, `um watchdog de ${id} ainda está rodando (${n}, pid ${pid}); espere ou encerre antes`));
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const ev = evidence(dir);
|
|
98
|
+
writeFileSync(`${prefix}.md`, buildPrompt(x, ev.fixes, ev.reviews));
|
|
99
|
+
const p = orq("pick", "watchdog", "--level", "simple", "-w", ws, ...(implPick ? ["--impl-agent", implPick] : []));
|
|
100
|
+
if (p.status !== 0) {
|
|
101
|
+
console.error(p.stderr.trim() || `orq pick watchdog: exit ${p.status}`);
|
|
102
|
+
return p.status ?? 1;
|
|
103
|
+
}
|
|
104
|
+
const picked = p.stdout.trim().split(/\s+/);
|
|
105
|
+
// The agent runs in a scratch folder (everything it needs is in the prompt): it can't touch the issue files, even write sandboxed.
|
|
106
|
+
const scratch = mkdtempSync(join(tmpdir(), "orq-watchdog-"));
|
|
107
|
+
try {
|
|
108
|
+
const a = orq("agent", ...picked, "-r", "-t", `${id} watchdog`, "-d", scratch, "-g", "watchdog", "-o", prefix, "--", `${prefix}.md`);
|
|
109
|
+
process.stderr.write(a.stderr);
|
|
110
|
+
process.stdout.write(a.stdout);
|
|
111
|
+
if (a.status !== 0)
|
|
112
|
+
return a.status ?? 1;
|
|
113
|
+
return await finish(await wait(["--timeout", String(WATCHDOG_TIMEOUT), prefix]), prefix, out);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function finish(rc, prefix, out) {
|
|
120
|
+
if (rc === 124) {
|
|
121
|
+
console.error(m(`diagnosis not written: the watchdog agent did not finish in ${WATCHDOG_TIMEOUT}s`, `diagnóstico não gravado: o agente watchdog não terminou em ${WATCHDOG_TIMEOUT}s`));
|
|
122
|
+
return 124;
|
|
123
|
+
}
|
|
124
|
+
if (rc !== 0 || !existsSync(`${prefix}.out.md`)) {
|
|
125
|
+
console.error(m("the watchdog agent failed; no diagnosis written", "o agente watchdog falhou; diagnóstico não gravado"));
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
copyFileSync(`${prefix}.out.md`, out);
|
|
129
|
+
console.log(m(`diagnosis saved to ${out}`, `diagnóstico gravado em ${out}`));
|
|
130
|
+
console.log(recommendation(readFileSync(out, "utf8")) ?? m("(no RECOMMENDATION line in the answer)", "(sem linha RECOMMENDATION na resposta)"));
|
|
57
131
|
return 0;
|
|
58
132
|
}
|
package/dist/commands/stats.js
CHANGED
|
@@ -5,7 +5,7 @@ import { expand } from "../paths.js";
|
|
|
5
5
|
import { collectRuns, rows } from "../stats.js";
|
|
6
6
|
export const USAGE = "orq stats [-w <workspace>] [--days N] [--json] scoreboard: delivery, time and tokens per agent";
|
|
7
7
|
const k = (n) => (n === null ? "-" : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
|
|
8
|
-
const ORDER = ["impl", "fix", "review", "other"];
|
|
8
|
+
const ORDER = ["impl", "fix", "review", "watchdog", "other"];
|
|
9
9
|
export function stats(argv) {
|
|
10
10
|
let ws, days = 30, json = false;
|
|
11
11
|
for (let i = 0; i < argv.length; i++) {
|