@irythill/orq 0.3.0 → 0.4.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 +48 -11
- package/README.pt-BR.md +40 -11
- package/dist/ci.js +239 -0
- package/dist/cli.js +7 -1
- package/dist/commands/agent.js +61 -6
- package/dist/commands/block.js +20 -3
- package/dist/commands/board.js +87 -8
- package/dist/commands/ci.js +48 -0
- package/dist/commands/claim.js +148 -12
- package/dist/commands/completion.js +3 -2
- package/dist/commands/gc.js +1 -1
- package/dist/commands/handoff.js +259 -0
- package/dist/commands/pick.js +18 -3
- package/dist/commands/quota.js +34 -9
- package/dist/commands/rounds.js +58 -0
- package/dist/config.js +4 -1
- package/dist/identity.js +55 -0
- package/dist/pick.js +106 -9
- package/dist/quota.js +83 -21
- package/dist/rounds.js +167 -0
- package/dist/testrun.js +18 -1
- package/package.json +1 -1
- package/skills/issue/SKILL.md +42 -5
- package/skills/orq-setup/SKILL.md +1 -1
- package/templates/workspace.CLAUDE.md +7 -0
package/dist/commands/agent.js
CHANGED
|
@@ -15,7 +15,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { claudeAccounts } from "../accounts.js";
|
|
17
17
|
import { agentDef, ALIASES, expandString, expandTemplate, FORMATS } from "../agents.js";
|
|
18
|
-
import { loadConfig } from "../config.js";
|
|
18
|
+
import { getPath, loadConfig } from "../config.js";
|
|
19
19
|
import { writeAtomic } from "../fsutil.js";
|
|
20
20
|
import { m } from "../i18n.js";
|
|
21
21
|
import { alive } from "../lock.js";
|
|
@@ -23,7 +23,7 @@ import { stateDir } from "../paths.js";
|
|
|
23
23
|
import { killTree, parseSize, resolveMode, runCapped } from "../process.js";
|
|
24
24
|
import { closeOwnTab, detectHost, launch, screenOpen, shq, typeInto } from "../terminal.js";
|
|
25
25
|
import { dropWorktree, notify } from "../testrun.js";
|
|
26
|
-
export const USAGE = "orq agent -a <agent> -t <title> -d <dir> -o <prefix> [-g group] [-m model] [-e effort] [-c account] [-i file]... [-b base] [-r] [-R session] [-T] -- <prompt.md> run a child agent";
|
|
26
|
+
export const USAGE = "orq agent -a <agent> -t <title> -d <dir> -o <prefix> [-g group] [-m model] [-e effort] [-c account] [-i file]... [-b base] [-r] [-R session [-F]] [-T] -- <prompt.md> run a child agent";
|
|
27
27
|
const CLI = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
28
28
|
const PF = ".herdr-agent-prompt.md"; // prompt_in_dir file name (kept from herdr-agent)
|
|
29
29
|
const git = (dir, ...a) => {
|
|
@@ -36,7 +36,7 @@ export async function agent(argv) {
|
|
|
36
36
|
if (argv[0] === "--turn")
|
|
37
37
|
return turn(argv[1], argv[2], argv.slice(3));
|
|
38
38
|
let a = process.env.AGENT || "codex", g = "", t = "", d = "", o = "", model = "", effort = "", acc = "", resume = "", base = "";
|
|
39
|
-
let ro = false, tui = false, prompt;
|
|
39
|
+
let ro = false, tui = false, force = false, prompt;
|
|
40
40
|
const inc = [];
|
|
41
41
|
for (let i = 0; i < argv.length; i++) {
|
|
42
42
|
const x = argv[i], v = () => argv[++i] ?? "";
|
|
@@ -72,6 +72,8 @@ export async function agent(argv) {
|
|
|
72
72
|
ro = true;
|
|
73
73
|
else if (x === "-T")
|
|
74
74
|
tui = true;
|
|
75
|
+
else if (x === "-F")
|
|
76
|
+
force = true;
|
|
75
77
|
else if (!x.startsWith("-") && prompt === undefined)
|
|
76
78
|
prompt = x;
|
|
77
79
|
else {
|
|
@@ -121,6 +123,22 @@ export async function agent(argv) {
|
|
|
121
123
|
return 2;
|
|
122
124
|
}
|
|
123
125
|
const repo = resolve(d), O = resolve(o);
|
|
126
|
+
// Fix rounds: every -R on the same session counts, whatever the prefix is called. policy.max_rounds is enforced here,
|
|
127
|
+
// not left to the conductor to remember (a 7-round fix loop is what burns the tokens). -F = the user said go on.
|
|
128
|
+
if (resume) {
|
|
129
|
+
const max = Number(getPath(cfg, "policy.max_rounds") ?? 2), cf = join(dirname(O), `.rounds-${resume}`);
|
|
130
|
+
let n = 0;
|
|
131
|
+
try {
|
|
132
|
+
n = Number(readFileSync(cf, "utf8")) || 0;
|
|
133
|
+
}
|
|
134
|
+
catch { /* first follow-up */ }
|
|
135
|
+
if (n >= max && !force) {
|
|
136
|
+
console.error(m(`round limit: session ${resume} already had ${n} follow-up(s) (policy.max_rounds = ${max}). Stop and tell the user; with their OK, repeat with -F.`, `limite de rodadas: a sessão ${resume} já teve ${n} follow-up(s) (policy.max_rounds = ${max}). Pare e avise o usuário; com o OK dele, repita com -F.`));
|
|
137
|
+
return 4;
|
|
138
|
+
}
|
|
139
|
+
mkdirSync(dirname(O), { recursive: true });
|
|
140
|
+
writeFileSync(cf, String(n + 1));
|
|
141
|
+
}
|
|
124
142
|
let P = resolve(prompt);
|
|
125
143
|
if (!existsSync(repo)) {
|
|
126
144
|
console.error(m(`no folder ${repo}`, `pasta ${repo} não existe`));
|
|
@@ -199,7 +217,7 @@ export async function agent(argv) {
|
|
|
199
217
|
const vars = {
|
|
200
218
|
prompt: P, prompt_text: tpl.includes("{prompt_text}") ? readFileSync(P, "utf8") : "",
|
|
201
219
|
prompt_hint: `Read the file ${PF} (in the current folder, with your file-reading tool): it holds your complete instructions for this task. Follow them. ${PF} is not part of the code: don't review or edit it.`,
|
|
202
|
-
out: `${O}.out.md`, dir, repo, title: t, model: model === "auto" ? "" : model, effort, session,
|
|
220
|
+
out: `${O}.out.md`, dir, repo, base, title: t, model: model === "auto" ? "" : model, effort, session,
|
|
203
221
|
gitdir: gd ?? "", gitroots: roots, gitroots_json: JSON.stringify(roots), mode: [], opencode_config: JSON.stringify(ocCfg),
|
|
204
222
|
hook_cmd: hook.map(shq).join(" "), hook_json: JSON.stringify(hook),
|
|
205
223
|
hook_settings: JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: "command", command: hook.map(shq).join(" ") }] }] } }),
|
|
@@ -229,6 +247,7 @@ export async function agent(argv) {
|
|
|
229
247
|
swap: parseSize(mach.agent_swap_max), mode: mach.mem_enforce ?? "auto",
|
|
230
248
|
throwaway, promptInDir: Boolean(def.prompt_in_dir), closeAfter,
|
|
231
249
|
keep: process.env.HERDR_KEEP_TAB === "1" || process.env.ORQ_KEEP_TAB === "1" || closeAfter <= 0, tui,
|
|
250
|
+
okExit: def.ok_exit, filter: def.filter, noReview: def.no_review,
|
|
232
251
|
};
|
|
233
252
|
writeAtomic(`${O}.job.json`, JSON.stringify(job, null, 1));
|
|
234
253
|
// Readable copy of the command (also what orq stats and herdr-board read).
|
|
@@ -303,7 +322,8 @@ function turn(file, kind, rest) {
|
|
|
303
322
|
writeFileSync(`${O}.out.md`, answer);
|
|
304
323
|
if (sid)
|
|
305
324
|
writeFileSync(`${O}.id`, `${sid}\n`);
|
|
306
|
-
const
|
|
325
|
+
const noReview = noReviewMatch(j.noReview, answer);
|
|
326
|
+
const st = /\S/.test(answer) && !noReview ? "ok" : "fail";
|
|
307
327
|
writeAtomic(`${O}.done`, `status=${st} rc=0 at=${new Date().toISOString()} agent=${j.agent} mode=tui\n`);
|
|
308
328
|
if (j.parent)
|
|
309
329
|
notify(j.parent, `[HERDR-AGENT DONE] ${j.title} status=${st} rc=0 answer: ${O}.out.md`, true);
|
|
@@ -332,6 +352,27 @@ function lastClaudeText(transcript) {
|
|
|
332
352
|
catch { /* no transcript */ }
|
|
333
353
|
return text;
|
|
334
354
|
}
|
|
355
|
+
/** agents.<name>.no_review tested against the start of the answer (first line) or, for a short answer, the whole
|
|
356
|
+
* thing — not anywhere in a long one, so a review that quotes the phrase while discussing real code doesn't match.
|
|
357
|
+
* An invalid pattern is logged and ignored rather than killing the runner. */
|
|
358
|
+
function noReviewMatch(pattern, answer) {
|
|
359
|
+
if (pattern === undefined)
|
|
360
|
+
return false;
|
|
361
|
+
if (typeof pattern !== "string" || !pattern.trim()) {
|
|
362
|
+
console.error(`orq agent: ignoring invalid agents.<name>.no_review ${JSON.stringify(pattern)}: expected a non-empty regex string`);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
let re;
|
|
366
|
+
try {
|
|
367
|
+
re = new RegExp(pattern, "m");
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
console.error(`orq agent: ignoring invalid agents.<name>.no_review pattern ${JSON.stringify(pattern)}: ${e.message}`);
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const first = answer.split(/\r?\n/, 1)[0] ?? "";
|
|
374
|
+
return re.test(first) || (answer.trim().length <= 200 && re.test(answer));
|
|
375
|
+
}
|
|
335
376
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
336
377
|
/** Splits a stream into lines for `each`. */
|
|
337
378
|
function lines(s, each) {
|
|
@@ -500,12 +541,26 @@ async function runJob(file) {
|
|
|
500
541
|
writeFileSync(`${O}.id`, `${sid}\n`);
|
|
501
542
|
if (rc === 0 && res.rc)
|
|
502
543
|
rc = res.rc;
|
|
544
|
+
// A review tool exits non-zero on findings: an ok_exit code is a verdict (rc stays in .done), not a failure.
|
|
545
|
+
let verdict = rc === 0 || (j.okExit ?? []).includes(rc);
|
|
546
|
+
if (j.filter && verdict) {
|
|
547
|
+
const f = spawnSync("sh", ["-c", j.filter], { cwd: j.dir, input: readFileSync(`${O}.out.md`, "utf8"), encoding: "utf8", maxBuffer: 1 << 30 });
|
|
548
|
+
if (f.status === 0)
|
|
549
|
+
writeFileSync(`${O}.out.md`, f.stdout);
|
|
550
|
+
else {
|
|
551
|
+
appendFileSync(`${O}.log`, `[orq agent] filter failed (exit ${f.status}): ${f.stderr}\n`);
|
|
552
|
+
verdict = false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
503
555
|
let answer = "";
|
|
504
556
|
try {
|
|
505
557
|
answer = readFileSync(`${O}.out.md`, "utf8");
|
|
506
558
|
}
|
|
507
559
|
catch { /* none */ }
|
|
508
|
-
|
|
560
|
+
// A reviewer whose answer says nothing was reviewed (agents.<name>.no_review) is not a verdict either: pick's
|
|
561
|
+
// fallback and the conductor must see it as not run, same as a whitespace-only answer.
|
|
562
|
+
const noReview = noReviewMatch(j.noReview, answer);
|
|
563
|
+
const st = verdict && !noReview && /\S/.test(answer) ? "ok" : "fail"; // whitespace-only answer = failure
|
|
509
564
|
writeAtomic(`${O}.done`, `status=${st} rc=${rc} at=${new Date().toISOString()} agent=${j.agent}\n`);
|
|
510
565
|
rmSync(`${O}.pid`, { force: true });
|
|
511
566
|
state({ status: st, rc, end: new Date().toISOString() });
|
package/dist/commands/block.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// orq block — takes an agent (or account) out of orq-pick until a date. For quotas without a collector
|
|
2
2
|
// (Antigravity, Copilot, MiniMax) or any outage you know about before the agent fails.
|
|
3
|
-
// State in <state dir>/blocks.json ({name: {until: <epoch seconds>, reason}}), shared with orq-pick.
|
|
3
|
+
// State in <state dir>/blocks.json ({name: {until: <epoch seconds>, reason, acct}}), shared with orq-pick. `acct` is the
|
|
4
|
+
// login at block time (src/identity.ts): if the agent/account is logged into another one later, the block is dropped.
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { writeAtomic } from "../fsutil.js";
|
|
8
|
+
import { loginId } from "../identity.js";
|
|
7
9
|
import { m } from "../i18n.js";
|
|
8
10
|
import { stateDir } from "../paths.js";
|
|
9
11
|
export const USAGE = `orq block <agent|account> [--until YYYY-MM-DD[THH:MM] | --days N] [--reason "text"] default: 7 days
|
|
@@ -19,7 +21,21 @@ export function load() {
|
|
|
19
21
|
b = JSON.parse(readFileSync(file(), "utf8"));
|
|
20
22
|
}
|
|
21
23
|
catch { /* missing or broken: no blocks */ }
|
|
22
|
-
|
|
24
|
+
const live = Object.fromEntries(Object.entries(b).filter(([, v]) => (v?.until ?? 0) > now()));
|
|
25
|
+
// A block made under another login (the account was switched) no longer applies: the new one has its own quota.
|
|
26
|
+
const kept = Object.fromEntries(Object.entries(live).filter(([k, v]) => {
|
|
27
|
+
if (!v.acct)
|
|
28
|
+
return true;
|
|
29
|
+
const cur = loginId(k);
|
|
30
|
+
return cur === undefined || cur === v.acct;
|
|
31
|
+
}));
|
|
32
|
+
if (Object.keys(kept).length !== Object.keys(live).length) {
|
|
33
|
+
try {
|
|
34
|
+
save(kept);
|
|
35
|
+
}
|
|
36
|
+
catch { /* best effort */ }
|
|
37
|
+
}
|
|
38
|
+
return kept;
|
|
23
39
|
}
|
|
24
40
|
const save = (b) => writeAtomic(file(), JSON.stringify(b, null, 2));
|
|
25
41
|
/** "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM" in local time (a bare date in JS would be UTC). */
|
|
@@ -67,7 +83,8 @@ export function block(argv) {
|
|
|
67
83
|
console.error(USAGE);
|
|
68
84
|
return 2;
|
|
69
85
|
}
|
|
70
|
-
|
|
86
|
+
const acct = loginId(name);
|
|
87
|
+
b[name] = { until: until.getTime() / 1000, reason, ...(acct ? { acct } : {}) };
|
|
71
88
|
save(b);
|
|
72
89
|
console.log(`${name} ${m("out of orq-pick until", "fora do orq-pick até")} ${fmt(until)}`);
|
|
73
90
|
return 0;
|
package/dist/commands/board.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// Refreshes every --interval s on a TTY; otherwise (or with --once) prints once; --json for scripts.
|
|
8
8
|
// Keys: q quits · r refreshes the quota · l language · t theme (ui.theme: gunmetal | chrome | lcd | terminal).
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
|
-
import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync } from "node:fs";
|
|
10
|
+
import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { freemem, homedir, totalmem } from "node:os";
|
|
12
12
|
import { basename, dirname, join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { colorMode } from "../banner.js";
|
|
15
|
+
import { collectCi, collectedAt, loadCache as loadCiCache } from "../ci.js";
|
|
15
16
|
import { getPath, loadConfig } from "../config.js";
|
|
16
17
|
import { writeAtomic } from "../fsutil.js";
|
|
17
18
|
import { m, setLang } from "../i18n.js";
|
|
@@ -19,8 +20,10 @@ import { alive, status as lockStatus } from "../lock.js";
|
|
|
19
20
|
import { expand, stateDir } from "../paths.js";
|
|
20
21
|
import { processTableAsync, treeMem } from "../process.js";
|
|
21
22
|
import { collect } from "../quota.js";
|
|
23
|
+
import { collectRounds, mark, roundsSig } from "../rounds.js";
|
|
22
24
|
import { collectRuns, rows } from "../stats.js";
|
|
23
25
|
import { load as loadBlocks } from "./block.js";
|
|
26
|
+
import { herdrNamesAsync, nameOf } from "./claim.js";
|
|
24
27
|
import { setValue } from "./set.js";
|
|
25
28
|
export const USAGE = "orq board [-w <workspace>]... [--once] [--json] [--interval S] live board: queue, agents, conductors, quota";
|
|
26
29
|
const CLI = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
@@ -124,16 +127,40 @@ function agents(table, now) {
|
|
|
124
127
|
}
|
|
125
128
|
const field = (t, k) => new RegExp(`${k}=(\\S+)`).exec(t)?.[1] ?? "";
|
|
126
129
|
const herdrAlive = (id) => new Promise((res) => execFile("herdr", ["agent", "get", id], { timeout: 5000, windowsHide: true }, (e) => res(!e)));
|
|
130
|
+
// The board redraws every few seconds: reread an issue's reviews only when its folder changed.
|
|
131
|
+
const roundsCache = new Map();
|
|
132
|
+
function cachedRounds(ws, id, max) {
|
|
133
|
+
const key = `${ws}\0${id}\0${max}`, sig = roundsSig(ws, id), hit = roundsCache.get(key);
|
|
134
|
+
if (hit && hit.sig === sig)
|
|
135
|
+
return hit.x;
|
|
136
|
+
const x = collectRounds(ws, id, max);
|
|
137
|
+
roundsCache.set(key, { sig, x });
|
|
138
|
+
return x;
|
|
139
|
+
}
|
|
127
140
|
async function conductors(wss) {
|
|
128
141
|
const by = new Map();
|
|
129
142
|
for (const ws of wss) {
|
|
130
143
|
const O = join(ws, ".orquestra");
|
|
131
|
-
|
|
144
|
+
const max = Number((() => { try {
|
|
145
|
+
return getPath(loadConfig(ws).cfg, "policy.max_rounds");
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
} })() ?? 2);
|
|
150
|
+
for (const id of existsSync(O) ? readdirSync(O).filter((x) => !x.startsWith(".")) : []) {
|
|
132
151
|
const t = read(join(O, id, "owner")).trim();
|
|
133
152
|
if (!t)
|
|
134
153
|
continue;
|
|
135
154
|
const who = field(t, "pane"), at = Date.parse(field(t, "at")) || 0;
|
|
136
|
-
const c = by.get(who) ?? { id: who, kind: field(t, "kind") || "herdr", account: field(t, "account"), alive: false, issues: [], at };
|
|
155
|
+
const c = by.get(who) ?? { id: who, kind: field(t, "kind") || "herdr", account: field(t, "account"), alive: false, issues: [], at, name: nameOf(t) };
|
|
156
|
+
if (!c.name)
|
|
157
|
+
c.name = nameOf(t);
|
|
158
|
+
try {
|
|
159
|
+
const x = cachedRounds(ws, id, max);
|
|
160
|
+
if (x)
|
|
161
|
+
(c.rounds ??= {})[id] = { rounds: x.rounds, max: x.max, level: x.level };
|
|
162
|
+
}
|
|
163
|
+
catch { /* unreadable issue folder: no marker */ }
|
|
137
164
|
c.issues.push(id);
|
|
138
165
|
c.at = Math.max(c.at, at);
|
|
139
166
|
by.set(who, c);
|
|
@@ -141,7 +168,15 @@ async function conductors(wss) {
|
|
|
141
168
|
}
|
|
142
169
|
const ttl = Number(process.env.ORQ_CLAIM_TTL_H ?? 24) * 3600_000;
|
|
143
170
|
const out = [...by.values()];
|
|
144
|
-
|
|
171
|
+
// One `herdr workspace list` per refresh, shared by every conductor (not 2 spawns each), off the event
|
|
172
|
+
// loop (execFile, not spawnSync) so a slow/busy herdr daemon delays only this redraw, never key handling.
|
|
173
|
+
const names = out.some((c) => c.kind === "herdr") ? await herdrNamesAsync(out.filter((c) => c.kind === "herdr").map((c) => c.id)) : new Map();
|
|
174
|
+
await Promise.all(out.map(async (c) => {
|
|
175
|
+
c.alive = c.kind === "herdr" ? await herdrAlive(c.id) : Date.now() - c.at < ttl;
|
|
176
|
+
// Alive herdr conductor: the workspace's current label (the user may have renamed it since the claim).
|
|
177
|
+
if (c.alive && c.kind === "herdr")
|
|
178
|
+
c.name = names.get(c.id) || c.name;
|
|
179
|
+
}));
|
|
145
180
|
return out.filter((c) => c.alive).map(({ at: _, ...c }) => ({ ...c, issues: c.issues.sort() }));
|
|
146
181
|
}
|
|
147
182
|
// Quota: the board never waits for the network. A stale cache starts `orq board --collect-quota` detached.
|
|
@@ -149,8 +184,9 @@ const qdir = () => join(stateDir(), "board");
|
|
|
149
184
|
const qfile = () => join(qdir(), "quota.txt");
|
|
150
185
|
export function parseQuota(text) {
|
|
151
186
|
return text.split(/\r?\n/).flatMap((l) => {
|
|
152
|
-
const r = /^(\S+) 5h=(\S+) week=(\S+)(?: \[(.*)\])?/.exec(l.trim());
|
|
153
|
-
return r ? [{ name: r[1], five: r[2], week: r[3], ...(r[4] ? {
|
|
187
|
+
const r = /^(\S+) 5h=(\S+) week=(\S+)(?: reset5h=(\d+))?(?: resetweek=(\d+))?(?: span5h=\d+)?(?: spanweek=\d+)?(?: \[(.*)\])?/.exec(l.trim());
|
|
188
|
+
return r ? [{ name: r[1], five: r[2], week: r[3], ...(r[4] ? { reset5h: Number(r[4]) } : {}),
|
|
189
|
+
...(r[5] ? { resetWeek: Number(r[5]) } : {}), ...(r[6] ? { note: r[6] } : {}) }] : [];
|
|
154
190
|
});
|
|
155
191
|
}
|
|
156
192
|
export function quota(now) {
|
|
@@ -179,6 +215,41 @@ async function collectQuota() {
|
|
|
179
215
|
writeAtomic(qfile(), lines.join("\n") + "\n");
|
|
180
216
|
return 0;
|
|
181
217
|
}
|
|
218
|
+
// CI: the same never-block-the-board rule as quota — a stale cache starts `orq board --collect-ci -w <ws>...`
|
|
219
|
+
// detached, reusing this same timer instead of a new one; the board itself only ever reads the cache file.
|
|
220
|
+
function startCollectCi(wss) {
|
|
221
|
+
const lock = join(qdir(), "ci.pid");
|
|
222
|
+
const pid = Number(read(lock));
|
|
223
|
+
if (pid > 0 && alive(pid))
|
|
224
|
+
return;
|
|
225
|
+
mkdirSync(qdir(), { recursive: true });
|
|
226
|
+
const c = spawn(process.execPath, [CLI, "board", ...wss.flatMap((w) => ["-w", w]), "--collect-ci"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
227
|
+
if (c.pid)
|
|
228
|
+
writeAtomic(lock, String(c.pid));
|
|
229
|
+
c.unref();
|
|
230
|
+
}
|
|
231
|
+
// Age per requested workspace, not the shared cache file's mtime: ci.json holds every workspace's entries and
|
|
232
|
+
// collectCi() rewrites the whole file on every run, so one workspace's collection would otherwise renew the
|
|
233
|
+
// mtime for all of them and starve a second workspace that was never actually collected. collectedAt() reads a
|
|
234
|
+
// per-workspace marker that collectCi() writes even when it finds zero entries (no claimed issue with a matching
|
|
235
|
+
// worktree) — without it, a workspace with nothing to collect would look permanently stale (entries' own `at`
|
|
236
|
+
// would never exist for it) and the board would relaunch the collector every redraw forever.
|
|
237
|
+
function ci(wss, now) {
|
|
238
|
+
const entries = loadCiCache().filter((e) => wss.includes(e.ws));
|
|
239
|
+
const ages = wss.map((w) => { const at = collectedAt(w); return at === undefined ? undefined : now - at; });
|
|
240
|
+
const age = ages.some((a) => a === undefined) ? undefined : Math.max(...ages);
|
|
241
|
+
if (age === undefined || age > QTTL)
|
|
242
|
+
startCollectCi(wss);
|
|
243
|
+
return { entries, age };
|
|
244
|
+
}
|
|
245
|
+
// Detached (stdio: "ignore"): collectCi()'s default notice (console.log) would otherwise vanish into /dev/null,
|
|
246
|
+
// so a non-herdr conductor's "CI failed" line is appended to a log file instead.
|
|
247
|
+
function collectCiAll(wss) {
|
|
248
|
+
const notice = (l) => appendFileSync(join(qdir(), "ci.log"), `${l}\n`);
|
|
249
|
+
for (const ws of wss)
|
|
250
|
+
collectCi(ws, { notice });
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
182
253
|
let sb;
|
|
183
254
|
function scoreboard(ws, now) {
|
|
184
255
|
if (!sb || sb.ws !== ws || now - sb.at > 60_000) {
|
|
@@ -197,7 +268,7 @@ export async function snapshot(wss, table) {
|
|
|
197
268
|
return {
|
|
198
269
|
at: new Date(now).toISOString(), mem: { used: totalmem() - freemem(), total: totalmem() }, workspaces: wss,
|
|
199
270
|
queue: { holders: q.holders.map(item), waiters: q.waiters.map(item) }, agents: agents(table, now),
|
|
200
|
-
conductors: await conductors(wss), quota: quota(now), blocks: loadBlocks(), scoreboard: scoreboard(wss[0], now),
|
|
271
|
+
conductors: await conductors(wss), quota: quota(now), ci: ci(wss, now), blocks: loadBlocks(), scoreboard: scoreboard(wss[0], now),
|
|
201
272
|
};
|
|
202
273
|
}
|
|
203
274
|
// --- render -----------------------------------------------------------------------------------------------------
|
|
@@ -276,9 +347,15 @@ export function render(s, theme, cols, mode = colorMode()) {
|
|
|
276
347
|
out.push(` ${c.mut}${m("none", "nenhum")}${c.r}`);
|
|
277
348
|
section("MASTER", m("conductors", "conductors"));
|
|
278
349
|
for (const k of s.conductors)
|
|
279
|
-
out.push(` ${c.ok}●${c.r} ${c.mut}${fit(k.id, 22)}${c.r} ${c.ink2}${fit(k.account || "-", 14)}${c.r} ${c.ink}${k.issues.join("
|
|
350
|
+
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}`);
|
|
280
351
|
if (!s.conductors.length)
|
|
281
352
|
out.push(` ${c.mut}${m("no claimed issues", "nenhuma issue reivindicada")}${c.r}`);
|
|
353
|
+
section("MONITOR", m("CI/CD", "CI/CD"));
|
|
354
|
+
const ciMark = { passed: `${c.ok}✓`, failed: `${c.err}✗`, running: `${c.acc}●`, queued: `${c.mut}‖`, none: `${c.mut}·` };
|
|
355
|
+
for (const e of s.ci.entries)
|
|
356
|
+
out.push(` ${ciMark[e.state]}${c.r} ${c.ink}${fit(e.issue, 10)}${c.r} ${c.ink2}${fit(e.repo, 14)}${c.r} ${c.mut}${fit(e.branch, 24)}${c.r} ${c.ink}${(e.pr ? `#${e.pr}` : "-").padEnd(6)}${c.r} ${c.ink2}${e.state}${c.r}`);
|
|
357
|
+
if (!s.ci.entries.length)
|
|
358
|
+
out.push(` ${c.mut}${s.ci.age === undefined ? m("collecting...", "coletando...") : m("no claimed issue has a PR", "nenhuma issue reivindicada tem PR")}${c.r}`);
|
|
282
359
|
section("MIXER", m("quota", "cota"));
|
|
283
360
|
const win = (v, tag) => {
|
|
284
361
|
const n = /^(\d+)%?$/.exec(v)?.[1];
|
|
@@ -329,6 +406,8 @@ export async function board(argv) {
|
|
|
329
406
|
interval = Math.max(1, Number(argv[++i]) || 3);
|
|
330
407
|
else if (a === "--collect-quota")
|
|
331
408
|
return collectQuota();
|
|
409
|
+
else if (a === "--collect-ci")
|
|
410
|
+
return collectCiAll(wss);
|
|
332
411
|
else if (a === "-h" || a === "--help") {
|
|
333
412
|
console.log(USAGE);
|
|
334
413
|
return 0;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// orq ci [-w <workspace>]... [--json] CI/CD signal per claimed issue with a PR: queued | running | passed |
|
|
2
|
+
// failed | none. This is the one caller of src/ci.ts's collectCi() that runs it in the foreground (network and
|
|
3
|
+
// all) instead of reading its cache — the user asked for CI status right now, so it collects right now; `orq
|
|
4
|
+
// board`'s CI column is the one that stays off the network and only reads the cache this same collectCi() fills.
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { collectCi } from "../ci.js";
|
|
7
|
+
import { getPath, loadConfig } from "../config.js";
|
|
8
|
+
import { m } from "../i18n.js";
|
|
9
|
+
import { expand } from "../paths.js";
|
|
10
|
+
export const USAGE = "orq ci [-w <workspace>]... [--json] CI/CD status per claimed issue (queued|running|passed|failed|none)";
|
|
11
|
+
const label = (s) => ({ queued: m("queued", "na fila"), running: m("running", "rodando"),
|
|
12
|
+
passed: m("passed", "passou"), failed: m("failed", "falhou"), none: m("none", "nenhum") })[s];
|
|
13
|
+
export async function ci(argv) {
|
|
14
|
+
const wss = [];
|
|
15
|
+
let json = false;
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
const a = argv[i];
|
|
18
|
+
if (a === "-w")
|
|
19
|
+
wss.push(resolve(expand(argv[++i] ?? ".")));
|
|
20
|
+
else if (a === "--json")
|
|
21
|
+
json = true;
|
|
22
|
+
else if (a === "-h" || a === "--help") {
|
|
23
|
+
console.log(USAGE);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.error(USAGE);
|
|
28
|
+
return 2;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!wss.length)
|
|
32
|
+
wss.push(getPath(loadConfig().cfg, "_workspace") ?? process.cwd());
|
|
33
|
+
// --json's stdout must be only the JSON: a non-herdr owner's notice goes to stderr instead of the default
|
|
34
|
+
// stdout, which would otherwise interleave a stray line in front of the JSON.
|
|
35
|
+
const hooks = json ? { notice: (line) => console.error(line) } : {};
|
|
36
|
+
const entries = wss.flatMap((ws) => collectCi(ws, hooks));
|
|
37
|
+
if (json) {
|
|
38
|
+
console.log(JSON.stringify(entries));
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (!entries.length) {
|
|
42
|
+
console.log(m("no claimed issue has a PR", "nenhuma issue reivindicada tem PR"));
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
for (const e of entries)
|
|
46
|
+
console.log(`${e.issue} ${e.repo} ${e.branch} ${e.pr ? `#${e.pr}` : "-"} ${label(e.state)}`);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
package/dist/commands/claim.js
CHANGED
|
@@ -1,36 +1,112 @@
|
|
|
1
1
|
// orq claim — owner of each issue among conductors of the same workspace (port of herdr-claim).
|
|
2
|
-
// .orquestra/<ID>/owner = "pane=<conductor id> account=<a> at=<iso>
|
|
3
|
-
// alive; a dead owner is taken over automatically.
|
|
2
|
+
// .orquestra/<ID>/owner = "pane=<conductor id> kind=<k> account=<a> at=<iso> name=<what the user calls it>". Another
|
|
3
|
+
// conductor is refused while the owner is alive; a dead owner is taken over automatically.
|
|
4
|
+
// name: the herdr workspace's label (the sidebar name, e.g. "emp: orq-cms-dsgn"), else ORQ_CONDUCTOR_ID. It is the
|
|
5
|
+
// last field and may hold spaces. `--from <name>` finds a conductor's issues by any part of that name (or its pane).
|
|
4
6
|
// conductor id: HERDR_PANE_ID (alive = `herdr agent get` answers) or ORQ_CONDUCTOR_ID (alive = claimed or refreshed
|
|
5
7
|
// in the last ORQ_CLAIM_TTL_H hours, default 24 — any conductor, any terminal); neither → <user>@<host>.
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
7
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
8
10
|
import { homedir, hostname, userInfo } from "node:os";
|
|
9
11
|
import { basename, join, resolve } from "node:path";
|
|
10
12
|
import { writeAtomic } from "../fsutil.js";
|
|
11
13
|
import { m } from "../i18n.js";
|
|
12
14
|
import { acquire, release } from "../lock.js";
|
|
13
|
-
export const USAGE = `orq claim [-w <workspace>] <ID> | -f <ID> | -r <ID> | -l issue owner among conductors (exit 1 = someone else's)
|
|
15
|
+
export const USAGE = `orq claim [-w <workspace>] <ID> | -f <ID> | -r <ID> | -l | --register issue owner among conductors (exit 1 = someone else's)
|
|
16
|
+
orq claim [-w <workspace>] --from <name> [-f] a conductor's issues by its name (part of it is enough); -f takes them all`;
|
|
14
17
|
const field = (text, k) => new RegExp(`${k}=(\\S+)`).exec(text)?.[1] ?? "";
|
|
18
|
+
export const nameOf = (text) => / name=(.*)$/m.exec(text)?.[1].trim() ?? "";
|
|
19
|
+
const herdrList = (args) => {
|
|
20
|
+
const r = spawnSync("herdr", args, { encoding: "utf8", timeout: 5_000 });
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(r.stdout).result;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** pane_id -> the label of the herdr workspace holding it (the name the user sees), for every live pane. One
|
|
29
|
+
* `pane list` + one `workspace list` call, however many panes are being looked up. */
|
|
30
|
+
export function herdrNames() {
|
|
31
|
+
const panes = herdrList(["pane", "list"])?.panes;
|
|
32
|
+
const workspaces = herdrList(["workspace", "list"])?.workspaces;
|
|
33
|
+
const labelOf = new Map((workspaces ?? []).map((w) => [w.workspace_id, w.label ?? ""]));
|
|
34
|
+
const out = new Map();
|
|
35
|
+
for (const p of panes ?? []) {
|
|
36
|
+
const label = p.workspace_id ? labelOf.get(p.workspace_id) : undefined;
|
|
37
|
+
if (label)
|
|
38
|
+
out.set(p.pane_id, label);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** The label of the herdr workspace holding `pane` (the name the user sees), or "". */
|
|
43
|
+
export function herdrName(pane) {
|
|
44
|
+
return herdrNames().get(pane) ?? "";
|
|
45
|
+
}
|
|
46
|
+
const herdrListAsync = (args) => new Promise((res) => execFile("herdr", args, { timeout: 5_000, windowsHide: true }, (_e, stdout) => {
|
|
47
|
+
try {
|
|
48
|
+
res(JSON.parse(stdout).result);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
res(undefined);
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
54
|
+
/** Same as herdrNames() for the given pane ids, but off the event loop (execFile, not spawnSync) — for a live,
|
|
55
|
+
* repeatedly-redrawn caller like the board, where blocking would freeze the whole UI while herdr answers. One
|
|
56
|
+
* `workspace list` is enough when the ids carry their workspace ("w8K:p1" -> "w8K"); `pane list` is only the
|
|
57
|
+
* fallback for ids that don't. */
|
|
58
|
+
export async function herdrNamesAsync(ids) {
|
|
59
|
+
const workspaces = (await herdrListAsync(["workspace", "list"]))?.workspaces;
|
|
60
|
+
const labelOf = new Map((workspaces ?? []).map((w) => [w.workspace_id, w.label ?? ""]));
|
|
61
|
+
const out = new Map();
|
|
62
|
+
const rest = [];
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
const label = labelOf.get(id.split(":")[0]);
|
|
65
|
+
if (label)
|
|
66
|
+
out.set(id, label);
|
|
67
|
+
else
|
|
68
|
+
rest.push(id);
|
|
69
|
+
}
|
|
70
|
+
if (rest.length) {
|
|
71
|
+
const panes = (await herdrListAsync(["pane", "list"]))?.panes;
|
|
72
|
+
for (const p of panes ?? []) {
|
|
73
|
+
const label = p.workspace_id ? labelOf.get(p.workspace_id) : undefined;
|
|
74
|
+
if (label && rest.includes(p.pane_id))
|
|
75
|
+
out.set(p.pane_id, label);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** A `herdr agent get` result → owner alive. A timeout (busy daemon, slow herdr) must not read as a dead owner:
|
|
81
|
+
* only a clean non-zero exit means "not found". */
|
|
82
|
+
export function ownerAlive(r) {
|
|
83
|
+
if (r.error?.code === "ETIMEDOUT")
|
|
84
|
+
return true;
|
|
85
|
+
return r.status === 0;
|
|
86
|
+
}
|
|
15
87
|
export function aliveOwner(text) {
|
|
16
88
|
const id = field(text, "pane"), kind = field(text, "kind") || "herdr";
|
|
17
89
|
if (kind === "herdr")
|
|
18
|
-
return spawnSync("herdr", ["agent", "get", id], { stdio: "ignore" })
|
|
90
|
+
return ownerAlive(spawnSync("herdr", ["agent", "get", id], { stdio: "ignore", timeout: 15_000 }));
|
|
19
91
|
const ttl = Number(process.env.ORQ_CLAIM_TTL_H ?? 24) * 3600_000;
|
|
20
92
|
return Date.now() - Date.parse(field(text, "at")) < ttl;
|
|
21
93
|
}
|
|
22
94
|
export async function claim(argv) {
|
|
23
|
-
let ws = process.cwd(), mode = "claim", id;
|
|
95
|
+
let ws = process.cwd(), mode = "claim", id, from;
|
|
24
96
|
for (let i = 0; i < argv.length; i++) {
|
|
25
97
|
const a = argv[i];
|
|
26
98
|
if (a === "-w")
|
|
27
99
|
ws = resolve(argv[++i]);
|
|
100
|
+
else if (a === "--from")
|
|
101
|
+
from = argv[++i];
|
|
28
102
|
else if (a === "-f")
|
|
29
103
|
mode = "force";
|
|
30
104
|
else if (a === "-r")
|
|
31
105
|
mode = "release";
|
|
32
106
|
else if (a === "-l")
|
|
33
107
|
mode = "list";
|
|
108
|
+
else if (a === "--register")
|
|
109
|
+
mode = "register";
|
|
34
110
|
else if (!a.startsWith("-"))
|
|
35
111
|
id = a;
|
|
36
112
|
else {
|
|
@@ -40,21 +116,82 @@ export async function claim(argv) {
|
|
|
40
116
|
}
|
|
41
117
|
const O = join(ws, ".orquestra");
|
|
42
118
|
if (mode === "list") {
|
|
43
|
-
|
|
119
|
+
const names = herdrNames(); // one herdr lookup for every issue in this listing, not one per issue
|
|
120
|
+
for (const d of existsSync(O) ? readdirSync(O).filter((x) => !x.startsWith(".")) : []) {
|
|
44
121
|
const f = join(O, d, "owner");
|
|
45
122
|
if (!existsSync(f))
|
|
46
123
|
continue;
|
|
47
124
|
const t = readFileSync(f, "utf8").trim();
|
|
48
|
-
|
|
125
|
+
const alive = aliveOwner(t);
|
|
126
|
+
// The herdr workspace's current label while the pane is alive; the name stored at claim time otherwise
|
|
127
|
+
// (the user may have renamed the workspace since, or it may not be a herdr conductor at all).
|
|
128
|
+
const n = (alive && (field(t, "kind") || "herdr") === "herdr" ? names.get(field(t, "pane")) ?? "" : "") || nameOf(t);
|
|
129
|
+
console.log(`${d} ${n ? `"${n}" ` : ""}${t.replace(/ name=.*$/, "")} (${alive ? "alive" : "dead"})`);
|
|
49
130
|
}
|
|
50
131
|
return 0;
|
|
51
132
|
}
|
|
133
|
+
const kind = process.env.HERDR_PANE_ID ? "herdr" : "id";
|
|
134
|
+
const me = process.env.HERDR_PANE_ID || process.env.ORQ_CONDUCTOR_ID || `${userInfo().username}@${hostname()}`;
|
|
135
|
+
const myName = (kind === "herdr" ? herdrName(me) : "") || process.env.ORQ_CONDUCTOR_ID || "";
|
|
136
|
+
const account = basename(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"));
|
|
137
|
+
const ownerLine = () => `pane=${me} kind=${kind} account=${account} at=${new Date().toISOString()}`
|
|
138
|
+
+ (myName ? ` name=${myName}` : "") + "\n";
|
|
139
|
+
if (mode === "register") {
|
|
140
|
+
if (kind !== "herdr")
|
|
141
|
+
return 0;
|
|
142
|
+
const dir = join(O, ".conductors");
|
|
143
|
+
mkdirSync(dir, { recursive: true });
|
|
144
|
+
writeAtomic(join(dir, me.replace(/[^A-Za-z0-9_-]/g, "_")), `pane=${me} kind=herdr account=${account} at=${new Date().toISOString()} name=${myName || me}\n`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
if (from !== undefined) {
|
|
148
|
+
if (!from) {
|
|
149
|
+
console.error(USAGE);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
const q = from.toLowerCase();
|
|
153
|
+
const liveNames = herdrNames(); // one herdr lookup for every issue being searched, not one per issue
|
|
154
|
+
const hits = (existsSync(O) ? readdirSync(O).filter((x) => !x.startsWith(".")) : []).flatMap((d) => {
|
|
155
|
+
const f = join(O, d, "owner");
|
|
156
|
+
if (!existsSync(f))
|
|
157
|
+
return [];
|
|
158
|
+
const t = readFileSync(f, "utf8").trim(), pane = field(t, "pane"), alive = aliveOwner(t);
|
|
159
|
+
const live = alive && (field(t, "kind") || "herdr") === "herdr" ? liveNames.get(pane) ?? "" : "";
|
|
160
|
+
const stored = nameOf(t), name = live || stored;
|
|
161
|
+
const names = [live, stored].filter(Boolean);
|
|
162
|
+
return pane !== me && (names.some((n) => n.toLowerCase().includes(q)) || pane.toLowerCase().includes(q))
|
|
163
|
+
? [{ id: d, f, pane, name, alive }] : [];
|
|
164
|
+
});
|
|
165
|
+
if (!hits.length) {
|
|
166
|
+
console.log(m(`no issue owned by a conductor matching "${from}"`, `nenhuma issue de um conductor que bata com "${from}"`));
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
const owners = [...new Set(hits.map((h) => h.pane))];
|
|
170
|
+
if (owners.length > 1) {
|
|
171
|
+
console.log(m(`"${from}" matches more than one conductor; be more specific:`, `"${from}" bate com mais de um conductor; seja mais específico:`));
|
|
172
|
+
for (const o of owners)
|
|
173
|
+
console.log(` ${hits.find((h) => h.pane === o).name || o} (${o})`);
|
|
174
|
+
return 3;
|
|
175
|
+
}
|
|
176
|
+
for (const h of hits)
|
|
177
|
+
console.log(`${h.id} "${h.name || h.pane}" (${h.alive ? "alive" : "dead"})`);
|
|
178
|
+
if (mode !== "force")
|
|
179
|
+
return 0;
|
|
180
|
+
await acquire("claims", process.pid, `claim --from ${from}`);
|
|
181
|
+
try {
|
|
182
|
+
for (const h of hits)
|
|
183
|
+
writeAtomic(h.f, ownerLine());
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
release("claims", process.pid);
|
|
187
|
+
}
|
|
188
|
+
console.log(m(`taken over: ${hits.map((h) => h.id).join(" ")} (now ${myName || me})`, `assumidas: ${hits.map((h) => h.id).join(" ")} (agora ${myName || me})`));
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
52
191
|
if (!id) {
|
|
53
192
|
console.error(m("give the issue ID", "informe o ID da issue"));
|
|
54
193
|
return 2;
|
|
55
194
|
}
|
|
56
|
-
const kind = process.env.HERDR_PANE_ID ? "herdr" : "id";
|
|
57
|
-
const me = process.env.HERDR_PANE_ID || process.env.ORQ_CONDUCTOR_ID || `${userInfo().username}@${hostname()}`;
|
|
58
195
|
mkdirSync(join(O, id), { recursive: true });
|
|
59
196
|
const F = join(O, id, "owner");
|
|
60
197
|
await acquire("claims", process.pid, `claim ${id}`); // one claim at a time
|
|
@@ -77,8 +214,7 @@ export async function claim(argv) {
|
|
|
77
214
|
}
|
|
78
215
|
console.log(m(`previous owner (${owner}) is gone; taking over ${id}`, `o dono anterior (${owner}) não existe mais; assumindo ${id}`));
|
|
79
216
|
}
|
|
80
|
-
|
|
81
|
-
writeAtomic(F, `pane=${me} kind=${kind} account=${account} at=${new Date().toISOString()}\n`);
|
|
217
|
+
writeAtomic(F, ownerLine());
|
|
82
218
|
console.log(cur && owner === me && mode === "claim" ? m(`${id} is already yours`, `${id} já é sua`) : m(`${id} is yours (${me})`, `${id} é sua (${me})`));
|
|
83
219
|
return 0;
|
|
84
220
|
}
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
export const USAGE = "orq completion bash|zsh|fish|powershell print the shell completion script (see docs)";
|
|
9
9
|
const DESC = {
|
|
10
10
|
setup: "guided first configuration", config: "show the merged config", set: "write one config value", lang: "language of orq's messages",
|
|
11
|
-
quota: "quota per agent/account", pick: "pick agent, model and account for a role", stats: "scoreboard per agent",
|
|
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",
|
|
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",
|
|
15
15
|
block: "block an agent or account", completion: "shell completion script",
|
|
16
16
|
};
|
|
17
17
|
/** Fixed words after a subcommand. */
|
|
@@ -20,6 +20,7 @@ export const WORDS = {
|
|
|
20
20
|
config: ["get", "files", "-w"], lang: ["pt", "en", "auto", "toggle"], lock: ["wait", "release", "run", "status"],
|
|
21
21
|
pick: ["impl", "fix", "worker", "review", "opinion"], 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
24
|
};
|
|
24
25
|
const q = (s) => `'${s.replace(/'/g, "''")}'`; // PowerShell single quotes
|
|
25
26
|
const qf = (s) => `'${s.replace(/[\\']/g, "\\$&")}'`; // fish single quotes
|