@irythill/orq 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -3
- package/README.pt-BR.md +241 -0
- package/dist/accounts.js +18 -0
- package/dist/agents.js +202 -0
- package/dist/banner.js +87 -0
- package/dist/cli.js +90 -0
- package/dist/commands/agent.js +334 -0
- package/dist/commands/baseline.js +101 -0
- package/dist/commands/block.js +74 -0
- package/dist/commands/board.js +435 -0
- package/dist/commands/claim.js +88 -0
- package/dist/commands/completion.js +88 -0
- package/dist/commands/config.js +35 -0
- package/dist/commands/deps.js +82 -0
- package/dist/commands/instructions.js +66 -0
- package/dist/commands/lang.js +35 -0
- package/dist/commands/lock.js +69 -0
- package/dist/commands/pick.js +91 -0
- package/dist/commands/quota.js +10 -0
- package/dist/commands/run.js +46 -0
- package/dist/commands/set.js +27 -0
- package/dist/commands/setup.js +187 -0
- package/dist/commands/stats.js +41 -0
- package/dist/commands/verify.js +123 -0
- package/dist/commands/wait.js +81 -0
- package/dist/config.js +93 -0
- package/dist/fsutil.js +9 -0
- package/dist/i18n.js +21 -0
- package/dist/install.js +137 -0
- package/dist/keyring.js +51 -0
- package/dist/lock.js +81 -0
- package/dist/paths.js +34 -0
- package/dist/pick.js +115 -0
- package/dist/process.js +147 -0
- package/dist/quota.js +276 -0
- package/dist/roles.js +28 -0
- package/dist/stats.js +159 -0
- package/dist/suggest.js +121 -0
- package/dist/terminal.js +83 -0
- package/dist/testrun.js +203 -0
- package/dist/toml-set.js +46 -0
- package/package.json +27 -6
- package/skills/issue/SKILL.md +197 -0
- package/skills/orq-setup/SKILL.md +114 -0
- package/skills/orq-status/SKILL.md +37 -0
- package/templates/workspace.CLAUDE.md +49 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// orq agent — run a headless agent as a child of the conductor (port of herdr-agent), on its AgentDef (src/agents.ts),
|
|
2
|
+
// under its memory cap (src/process.ts), in a herdr tab, a tmux window or the background (src/terminal.ts).
|
|
3
|
+
// Writes <prefix>.out.md (final answer) / .log / .id (session, for -R) / .pane / .done ("status=ok|fail rc=N at=...").
|
|
4
|
+
// Returns at once; `orq wait <prefix>` blocks until .done. A conductor in herdr (HERDR_PANE_ID) also gets ONE line
|
|
5
|
+
// "[HERDR-AGENT DONE] <title> status=... answer: <prefix>.out.md" typed into its pane by herdr-notify.
|
|
6
|
+
// The launcher writes <prefix>.job.json and starts `orq agent --run <job>` on the terminal host; that runner does the rest.
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { appendFileSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { claudeAccounts } from "../accounts.js";
|
|
13
|
+
import { agentDef, ALIASES, expandString, expandTemplate, FORMATS } from "../agents.js";
|
|
14
|
+
import { loadConfig } from "../config.js";
|
|
15
|
+
import { writeAtomic } from "../fsutil.js";
|
|
16
|
+
import { m } from "../i18n.js";
|
|
17
|
+
import { stateDir } from "../paths.js";
|
|
18
|
+
import { killTree, parseSize, resolveMode, runCapped } from "../process.js";
|
|
19
|
+
import { closeOwnTab, detectHost, launch, shq } from "../terminal.js";
|
|
20
|
+
import { dropWorktree, notify } from "../testrun.js";
|
|
21
|
+
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] -- <prompt.md> run a child agent";
|
|
22
|
+
const CLI = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
23
|
+
const PF = ".herdr-agent-prompt.md"; // prompt_in_dir file name (kept from herdr-agent)
|
|
24
|
+
const git = (dir, ...a) => {
|
|
25
|
+
const r = spawnSync("git", ["-C", dir, ...a], { encoding: "utf8" });
|
|
26
|
+
return r.status === 0 ? r.stdout.trim() : undefined;
|
|
27
|
+
};
|
|
28
|
+
export async function agent(argv) {
|
|
29
|
+
if (argv[0] === "--run")
|
|
30
|
+
return runJob(argv[1]);
|
|
31
|
+
let a = process.env.AGENT || "codex", g = "", t = "", d = "", o = "", model = "", effort = "", acc = "", resume = "", base = "";
|
|
32
|
+
let ro = false, prompt;
|
|
33
|
+
const inc = [];
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const x = argv[i], v = () => argv[++i] ?? "";
|
|
36
|
+
if (x === "--") {
|
|
37
|
+
prompt = argv[i + 1];
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (x === "-a")
|
|
41
|
+
a = v();
|
|
42
|
+
else if (x === "-g")
|
|
43
|
+
g = v();
|
|
44
|
+
else if (x === "-t")
|
|
45
|
+
t = v();
|
|
46
|
+
else if (x === "-d")
|
|
47
|
+
d = v();
|
|
48
|
+
else if (x === "-o")
|
|
49
|
+
o = v();
|
|
50
|
+
else if (x === "-m")
|
|
51
|
+
model = v();
|
|
52
|
+
else if (x === "-e")
|
|
53
|
+
effort = v();
|
|
54
|
+
else if (x === "-c")
|
|
55
|
+
acc = v();
|
|
56
|
+
else if (x === "-i")
|
|
57
|
+
inc.push(resolve(v()));
|
|
58
|
+
else if (x === "-R")
|
|
59
|
+
resume = v();
|
|
60
|
+
else if (x === "-b")
|
|
61
|
+
base = v();
|
|
62
|
+
else if (x === "-p")
|
|
63
|
+
v();
|
|
64
|
+
else if (x === "-r")
|
|
65
|
+
ro = true;
|
|
66
|
+
else if (x === "-T") {
|
|
67
|
+
console.error(m("-T (the agent's own TUI) only exists in herdr: use herdr-agent -T", "-T (a TUI do próprio agente) só existe no herdr: use herdr-agent -T"));
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
else if (!x.startsWith("-") && prompt === undefined)
|
|
71
|
+
prompt = x;
|
|
72
|
+
else {
|
|
73
|
+
console.error(USAGE);
|
|
74
|
+
return 2;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!prompt) {
|
|
78
|
+
console.error(m("give the prompt file", "informe o arquivo de prompt"));
|
|
79
|
+
return 2;
|
|
80
|
+
}
|
|
81
|
+
for (const [f, val] of [["t", t], ["d", d], ["o", o]])
|
|
82
|
+
if (!val) {
|
|
83
|
+
console.error(`${m("missing", "faltou")} -${f}`);
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
a = ALIASES[a] ?? a;
|
|
87
|
+
const { cfg } = loadConfig();
|
|
88
|
+
const def = agentDef(cfg, a);
|
|
89
|
+
if (!ro && def.write === false) {
|
|
90
|
+
console.error(m(`${a} only runs read only (-r)`, `o ${a} só roda em modo leitura (-r)`));
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
if (acc && !def.accounts) {
|
|
94
|
+
console.error(m(`-c only applies to agents with Claude accounts (claude)`, `-c só vale para agentes com contas Claude (claude)`));
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
if (resume && !def.resume) {
|
|
98
|
+
console.error(m(`${a} has no resume template (agents.${a}.resume)`, `${a} não tem template de resume (agents.${a}.resume)`));
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
const repo = resolve(d), O = resolve(o);
|
|
102
|
+
let P = resolve(prompt);
|
|
103
|
+
if (!existsSync(repo)) {
|
|
104
|
+
console.error(m(`no folder ${repo}`, `pasta ${repo} não existe`));
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
if (!existsSync(P)) {
|
|
108
|
+
console.error(m(`no prompt file ${P}`, `arquivo de prompt ${P} não existe`));
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
mkdirSync(dirname(O), { recursive: true });
|
|
112
|
+
// -b <base>: attaches `git diff origin/<base>...HEAD` (review without running git; required for Antigravity).
|
|
113
|
+
if (base) {
|
|
114
|
+
const r = spawnSync("git", ["-C", repo, "diff", `origin/${base}...HEAD`], { encoding: "utf8", maxBuffer: 1 << 30 });
|
|
115
|
+
if (r.status !== 0) {
|
|
116
|
+
console.error(m(`could not build the diff against origin/${base}`, `não consegui gerar o diff contra origin/${base}`));
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
writeFileSync(`${O}.diff`, r.stdout);
|
|
120
|
+
inc.push(`${O}.diff`);
|
|
121
|
+
}
|
|
122
|
+
// Follow-ups (-R) reuse the prefix: remove the previous round's result so ".done exists" means "this one finished".
|
|
123
|
+
for (const s of [".done", ".out.md", ".pid"])
|
|
124
|
+
rmSync(O + s, { force: true });
|
|
125
|
+
if (inc.length) {
|
|
126
|
+
let text = readFileSync(P, "utf8");
|
|
127
|
+
for (const f of inc)
|
|
128
|
+
text += `\n--- ${basename(f)} ---\n${readFileSync(f, "utf8")}\n--- end of ${basename(f)} ---\n`;
|
|
129
|
+
writeFileSync(`${O}.prompt.md`, text);
|
|
130
|
+
P = `${O}.prompt.md`;
|
|
131
|
+
}
|
|
132
|
+
const throwaway = Boolean(def.throwaway && ro && !resume);
|
|
133
|
+
const dir = throwaway ? `${repo}.review-${process.pid}` : repo;
|
|
134
|
+
const gd = git(repo, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
135
|
+
const wg = git(repo, "rev-parse", "--absolute-git-dir");
|
|
136
|
+
const roots = gd && wg ? [...new Set([wg, `${gd}/objects`, `${gd}/refs`, `${gd}/logs`])].sort() : [];
|
|
137
|
+
let nm = "";
|
|
138
|
+
try {
|
|
139
|
+
nm = realpathSync(join(repo, "node_modules"));
|
|
140
|
+
}
|
|
141
|
+
catch { /* none */ }
|
|
142
|
+
const session = resume || (def.session === "uuid" ? randomUUID() : "");
|
|
143
|
+
const vars = {
|
|
144
|
+
prompt: P, prompt_text: def.run.includes("{prompt_text}") || def.resume?.includes("{prompt_text}") ? readFileSync(P, "utf8") : "",
|
|
145
|
+
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.`,
|
|
146
|
+
out: `${O}.out.md`, dir, repo, title: t, model: model === "auto" ? "" : model, effort, session,
|
|
147
|
+
gitdir: gd ?? "", gitroots: roots, gitroots_json: JSON.stringify(roots), mode: [],
|
|
148
|
+
opencode_config: JSON.stringify({
|
|
149
|
+
mcp: { plane: { enabled: false }, shadcn: { enabled: false }, "linear-server": { enabled: false } },
|
|
150
|
+
permission: { bash: "allow", question: "deny", webfetch: "deny",
|
|
151
|
+
external_directory: { "*": "deny", ...(gd ? { [`${gd}/*`]: "allow" } : {}), "/tmp/*": "allow", ...(nm ? { [`${nm}/*`]: "allow" } : {}) },
|
|
152
|
+
edit: ro ? "deny" : "allow" },
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
vars.mode = expandTemplate((ro ? def.readonly : def.write) || "", vars);
|
|
156
|
+
const tpl = resume ? def.resume : def.run;
|
|
157
|
+
const cmd = expandTemplate(tpl, vars);
|
|
158
|
+
if (!tpl.includes("{mode}") && !resume)
|
|
159
|
+
cmd.push(...vars.mode);
|
|
160
|
+
const env = {};
|
|
161
|
+
for (const [k, val] of Object.entries(def.env ?? {}))
|
|
162
|
+
env[k] = expandString(String(val), vars);
|
|
163
|
+
if (acc) {
|
|
164
|
+
const ac = claudeAccounts().find((x) => x.name === acc);
|
|
165
|
+
if (!ac) {
|
|
166
|
+
console.error(m(`account '${acc}' does not exist`, `a conta '${acc}' não existe`));
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
env.CLAUDE_CONFIG_DIR = ac.dir;
|
|
170
|
+
}
|
|
171
|
+
const mach = (cfg.machine ?? {});
|
|
172
|
+
const closeAfter = Number(process.env.HERDR_CLOSE_DELAY ?? mach.close_tab_after ?? 20);
|
|
173
|
+
const host = detectHost(cfg);
|
|
174
|
+
const unit = `herdr-agent-${Math.floor(Date.now() / 1000)}-${process.pid}`; // herdr-board lists herdr-agent-* scopes
|
|
175
|
+
const job = {
|
|
176
|
+
agent: a, title: t, repo, dir, prefix: O, prompt: P, argv: cmd, env, unset: def.unset_env ?? [], stdin: Boolean(def.stdin),
|
|
177
|
+
format: def.format ?? "text", answer: def.answer ?? "stdout", session: session || undefined,
|
|
178
|
+
fromLog: typeof def.session === "object" ? def.session.from_log : undefined, parent: process.env.HERDR_PANE_ID ?? "",
|
|
179
|
+
host, unit, cap: parseSize(def.mem_max ?? mach.agent_mem_max ?? "off"),
|
|
180
|
+
swap: parseSize(mach.agent_swap_max), mode: mach.mem_enforce ?? "auto",
|
|
181
|
+
throwaway, promptInDir: Boolean(def.prompt_in_dir), closeAfter,
|
|
182
|
+
keep: process.env.HERDR_KEEP_TAB === "1" || process.env.ORQ_KEEP_TAB === "1" || closeAfter <= 0,
|
|
183
|
+
};
|
|
184
|
+
writeAtomic(`${O}.job.json`, JSON.stringify(job, null, 1));
|
|
185
|
+
// Readable copy of the command (also what orq stats and herdr-board read).
|
|
186
|
+
writeFileSync(`${O}.run.sh`, `#!/usr/bin/env bash\n# orq agent (generated; the run itself is \`orq agent --run ${O}.job.json\`) --unit=${unit} \n` +
|
|
187
|
+
`cd ${shq(dir)}\n${Object.entries(env).map(([k, val]) => `${k}=${shq(val)} `).join("")}${cmd.map(shq).join(" ")}${job.stdin ? ` < ${shq(P)}` : ""}\n`);
|
|
188
|
+
const where = launch(host, { group: g || "agents", title: t, dir: repo, argv: [process.execPath, CLI, "agent", "--run", `${O}.job.json`], id: unit, keep: job.keep });
|
|
189
|
+
writeFileSync(`${O}.pane`, `${where}\n`);
|
|
190
|
+
console.log(`[orq agent] ${t} → ${where}`);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
194
|
+
/** Splits a stream into lines for `each`. */
|
|
195
|
+
function lines(s, each) {
|
|
196
|
+
return new Promise((res) => {
|
|
197
|
+
if (!s)
|
|
198
|
+
return res();
|
|
199
|
+
let buf = "";
|
|
200
|
+
s.setEncoding("utf8");
|
|
201
|
+
s.on("data", (c) => { buf += c; let i; while ((i = buf.indexOf("\n")) >= 0) {
|
|
202
|
+
each(buf.slice(0, i).replace(/\r$/, ""));
|
|
203
|
+
buf = buf.slice(i + 1);
|
|
204
|
+
} });
|
|
205
|
+
s.on("end", () => { if (buf)
|
|
206
|
+
each(buf); res(); });
|
|
207
|
+
s.on("error", () => res());
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function runJob(file) {
|
|
211
|
+
const j = JSON.parse(readFileSync(file, "utf8")), O = j.prefix;
|
|
212
|
+
writeFileSync(`${O}.pid`, String(process.pid));
|
|
213
|
+
const stateF = join(stateDir(), "agents", `${j.unit}.json`);
|
|
214
|
+
const at = new Date().toISOString();
|
|
215
|
+
const state = (extra = {}) => { try {
|
|
216
|
+
writeAtomic(stateF, JSON.stringify({ pid: process.pid, prefix: O, title: j.title, agent: j.agent, host: j.host, cap: j.cap, at, ...extra }));
|
|
217
|
+
}
|
|
218
|
+
catch { /* best effort */ } };
|
|
219
|
+
state();
|
|
220
|
+
const fmt = FORMATS[j.format] ?? FORMATS.text;
|
|
221
|
+
const log = createWriteStream(`${O}.log`);
|
|
222
|
+
let rc = 0, stdout = "", logText = "";
|
|
223
|
+
const pre = () => {
|
|
224
|
+
if (j.throwaway) {
|
|
225
|
+
// Throwaway worktree: detached copy of HEAD with the repo's node_modules linked; removed at the end.
|
|
226
|
+
if (spawnSync("git", ["-C", j.repo, "worktree", "add", "-q", "--detach", j.dir, "HEAD"], { stdio: "inherit" }).status !== 0)
|
|
227
|
+
return false;
|
|
228
|
+
try {
|
|
229
|
+
symlinkSync(realpathSync(join(j.repo, "node_modules")), join(j.dir, "node_modules"), process.platform === "win32" ? "junction" : "dir");
|
|
230
|
+
}
|
|
231
|
+
catch { /* none */ }
|
|
232
|
+
}
|
|
233
|
+
if (j.promptInDir) {
|
|
234
|
+
copyFileSync(j.prompt, join(j.dir, PF));
|
|
235
|
+
const ex = git(j.dir, "rev-parse", "--git-path", "info/exclude");
|
|
236
|
+
if (ex) {
|
|
237
|
+
const f = resolve(j.dir, ex);
|
|
238
|
+
let cur = "";
|
|
239
|
+
try {
|
|
240
|
+
cur = readFileSync(f, "utf8");
|
|
241
|
+
}
|
|
242
|
+
catch { /* new */ }
|
|
243
|
+
if (!cur.split(/\r?\n/).includes(PF)) {
|
|
244
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
245
|
+
appendFileSync(f, `${PF}\n`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
const post = () => {
|
|
252
|
+
if (j.promptInDir)
|
|
253
|
+
rmSync(join(j.dir, PF), { force: true });
|
|
254
|
+
if (j.throwaway)
|
|
255
|
+
dropWorktree(j.repo, j.dir);
|
|
256
|
+
};
|
|
257
|
+
if (!pre())
|
|
258
|
+
rc = 2;
|
|
259
|
+
else {
|
|
260
|
+
const env = { ...process.env, ...j.env };
|
|
261
|
+
for (const k of j.unset)
|
|
262
|
+
delete env[k];
|
|
263
|
+
const kmode = resolveMode(j.mode, j.cap);
|
|
264
|
+
let lastLine = "", lastAt = Date.now(), watch, streams = Promise.resolve();
|
|
265
|
+
const onLine = (l, isOut) => {
|
|
266
|
+
log.write(`${l}\n`);
|
|
267
|
+
logText += `${l}\n`;
|
|
268
|
+
lastLine = l;
|
|
269
|
+
lastAt = Date.now();
|
|
270
|
+
if (j.format === "text") {
|
|
271
|
+
if (isOut) {
|
|
272
|
+
stdout += `${l}\n`;
|
|
273
|
+
process.stdout.write(`${l}\n`);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const s = fmt.show(l);
|
|
278
|
+
if (s)
|
|
279
|
+
process.stdout.write(s);
|
|
280
|
+
};
|
|
281
|
+
try {
|
|
282
|
+
rc = await runCapped(j.argv[0], j.argv.slice(1), {
|
|
283
|
+
cap: j.cap, swap: j.swap, mode: j.mode, unit: j.unit, cwd: j.dir, env, quote: true,
|
|
284
|
+
stdio: [j.stdin ? openSync(j.prompt, "r") : "ignore", "pipe", "pipe"],
|
|
285
|
+
onSpawn: (c) => {
|
|
286
|
+
streams = Promise.all([lines(c.stdout, (l) => onLine(l, true)), lines(c.stderr, (l) => onLine(l, false))]);
|
|
287
|
+
// OpenCode sometimes doesn't exit after its final event (helpers hold the pipe): stop it after 30 s of quiet.
|
|
288
|
+
if (fmt.idle)
|
|
289
|
+
watch = setInterval(() => {
|
|
290
|
+
if (!fmt.idle(lastLine) || Date.now() - lastAt < 30_000)
|
|
291
|
+
return;
|
|
292
|
+
console.log(m("[orq agent] the agent finished but did not exit; stopping it", "[orq agent] o agente terminou mas não encerrou; parando"));
|
|
293
|
+
clearInterval(watch);
|
|
294
|
+
if (kmode === "kernel")
|
|
295
|
+
spawnSync("systemctl", ["--user", "stop", `${j.unit}.scope`], { stdio: "ignore" });
|
|
296
|
+
else if (c.pid)
|
|
297
|
+
killTree(c.pid);
|
|
298
|
+
}, 10_000);
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
clearInterval(watch);
|
|
304
|
+
}
|
|
305
|
+
await Promise.race([streams, sleep(5000)]);
|
|
306
|
+
}
|
|
307
|
+
await new Promise((r) => log.end(r));
|
|
308
|
+
post();
|
|
309
|
+
const res = fmt.finish(logText, stdout);
|
|
310
|
+
if (j.answer !== "file")
|
|
311
|
+
writeFileSync(`${O}.out.md`, res.answer ?? "");
|
|
312
|
+
const sid = j.fromLog ? new RegExp(j.fromLog, "m").exec(logText)?.[1] : res.session ?? j.session;
|
|
313
|
+
if (sid)
|
|
314
|
+
writeFileSync(`${O}.id`, `${sid}\n`);
|
|
315
|
+
if (rc === 0 && res.rc)
|
|
316
|
+
rc = res.rc;
|
|
317
|
+
let answer = "";
|
|
318
|
+
try {
|
|
319
|
+
answer = readFileSync(`${O}.out.md`, "utf8");
|
|
320
|
+
}
|
|
321
|
+
catch { /* none */ }
|
|
322
|
+
const st = rc === 0 && /\S/.test(answer) ? "ok" : "fail"; // whitespace-only answer = failure
|
|
323
|
+
writeAtomic(`${O}.done`, `status=${st} rc=${rc} at=${new Date().toISOString()} agent=${j.agent}\n`);
|
|
324
|
+
rmSync(`${O}.pid`, { force: true });
|
|
325
|
+
state({ status: st, rc, end: new Date().toISOString() });
|
|
326
|
+
if (j.parent)
|
|
327
|
+
notify(j.parent, `[HERDR-AGENT DONE] ${j.title} status=${st} rc=${rc} answer: ${O}.out.md`);
|
|
328
|
+
console.log(`\n[${j.agent} ${m("finished", "terminou")}: exit ${rc} status=${st}] ${m("answer", "resposta")}: ${O}.out.md`);
|
|
329
|
+
if (j.host !== "plain" && !j.keep) {
|
|
330
|
+
await sleep(j.closeAfter * 1000);
|
|
331
|
+
closeOwnTab(j.host);
|
|
332
|
+
}
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// orq baseline — which tests ALREADY fail on origin/<base> (port of herdr-baseline): the full suite in a temporary
|
|
2
|
+
// worktree detached at origin/<base> (node_modules = link to the main clone), in the same queue and under the same cap
|
|
3
|
+
// as orq verify. Outside the issue flow (orq verify checks the base on demand): a manual snapshot of the base.
|
|
4
|
+
// output: <workspace>/.orquestra/baseline/<repo>-<base>.txt (1st line "# sha <sha> <date> ..."; then 1 test per line)
|
|
5
|
+
// Already up to date for the current origin/<base> → says so and runs nothing. -p is accepted and ignored.
|
|
6
|
+
// Same protocol as orq agent: `orq wait <output>`, or "[HERDR-AGENT DONE] baseline <repo> ..." in the conductor's pane.
|
|
7
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { getPath, loadConfig } from "../config.js";
|
|
11
|
+
import { m } from "../i18n.js";
|
|
12
|
+
import { dropWorktree, failures, git, inJob, launchJob, linkModules, runnerArgs, runnerOf, runTests, testCap, today, unsupported } from "../testrun.js";
|
|
13
|
+
export const USAGE = "orq baseline -w <workspace> -r <repo> -b <base> [-g group] [-x] tests already failing on origin/<base> (full suite)";
|
|
14
|
+
const current = (out, sha) => {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(out, "utf8").split("\n", 1)[0].includes(`sha ${sha} `);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export async function baseline(argv) {
|
|
23
|
+
if (argv[0] === "--run")
|
|
24
|
+
return run(JSON.parse(readFileSync(argv[1], "utf8")));
|
|
25
|
+
const o = {};
|
|
26
|
+
let here = false;
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === "-x")
|
|
30
|
+
here = true;
|
|
31
|
+
else if (/^-[pwrbg]$/.test(a) && i + 1 < argv.length)
|
|
32
|
+
o[a[1]] = argv[++i];
|
|
33
|
+
else {
|
|
34
|
+
console.error(USAGE);
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const f of "wrb")
|
|
39
|
+
if (!o[f]) {
|
|
40
|
+
console.error(`${m("missing", "faltou")} -${f}\n${USAGE}`);
|
|
41
|
+
return 2;
|
|
42
|
+
}
|
|
43
|
+
const ws = resolve(o.w), repo = join(ws, o.r), dir = join(ws, ".orquestra", "baseline"), out = join(dir, `${o.r}-${o.b}.txt`);
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
const j = { cmd: "baseline", title: `baseline ${o.r} ${o.b}`, ws, repo: o.r, wt: join(ws, ".worktrees", `baseline-${o.r}`), base: o.b, out };
|
|
46
|
+
if (here)
|
|
47
|
+
return run({ ...j, parent: process.env.HERDR_NOTIFY_PANE ?? "", host: "plain", closeAfter: 0, keep: true });
|
|
48
|
+
if (git(repo, "fetch", "-q", "origin", o.b) === undefined) {
|
|
49
|
+
console.error(m(`fetching origin/${o.b} failed`, `o fetch de origin/${o.b} falhou`));
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const sha = git(repo, "rev-parse", "--short", `origin/${o.b}`) ?? "";
|
|
53
|
+
if (current(out, sha)) {
|
|
54
|
+
console.log(`${m("baseline already up to date", "baseline já está atual")} (${sha}): ${out}`);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
rmSync(`${out}.done`, { force: true });
|
|
58
|
+
console.log(`[orq baseline] ${j.title} → ${launchJob(loadConfig(ws).cfg, j, o.g || "agents")}`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
async function run(j) {
|
|
62
|
+
const OUT = j.out, R = j.repo, B = j.base, REPO = join(j.ws, R), WT = j.wt;
|
|
63
|
+
const { cfg } = loadConfig(j.ws);
|
|
64
|
+
let sha = git(REPO, "rev-parse", "--short", `origin/${B}`) ?? "?";
|
|
65
|
+
// Same queue as orq verify: one test suite at a time on the machine (each may use up to machine.test_mem_max).
|
|
66
|
+
return inJob(j, `baseline — ${R}`, Number(getPath(cfg, "machine.test_slots") ?? 1), async (c) => {
|
|
67
|
+
// An identical baseline may have finished while this one waited: check again before running the full suite.
|
|
68
|
+
git(REPO, "fetch", "-q", "origin", B);
|
|
69
|
+
sha = git(REPO, "rev-parse", "--short", `origin/${B}`) ?? sha;
|
|
70
|
+
if (current(OUT, sha)) {
|
|
71
|
+
c.say(`${m("baseline already up to date", "baseline já está atual")} (${sha})`);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const drop = () => dropWorktree(REPO, WT);
|
|
75
|
+
drop();
|
|
76
|
+
if (git(REPO, "worktree", "add", "-q", "--detach", WT, `origin/${B}`) === undefined)
|
|
77
|
+
throw new Error(m(`could not create a worktree at origin/${B}`, `não consegui criar um worktree em origin/${B}`));
|
|
78
|
+
c.cleanup.push(drop);
|
|
79
|
+
linkModules(REPO, WT);
|
|
80
|
+
const { runner, script } = runnerOf(WT);
|
|
81
|
+
if (!runner)
|
|
82
|
+
throw new Error(unsupported(script));
|
|
83
|
+
const json = join(tmpdir(), `orq-baseline-${process.pid}-${Date.now()}.json`);
|
|
84
|
+
let r;
|
|
85
|
+
try {
|
|
86
|
+
await runTests(WT, runner, runnerArgs(runner, "all", [], json), testCap(cfg), c, false); // full suite: too loud for the log
|
|
87
|
+
r = failures(json, WT);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
rmSync(json, { force: true });
|
|
91
|
+
}
|
|
92
|
+
if (!r)
|
|
93
|
+
throw new Error(m("no JSON result", "sem resultado JSON"));
|
|
94
|
+
const deps = git(REPO, "diff", "--quiet", "HEAD", `origin/${B}`, "--", "package.json", "package-lock.json", "bun.lock") === undefined
|
|
95
|
+
? ` (WARNING: the main clone's node_modules is behind origin/${B})` : "";
|
|
96
|
+
writeFileSync(`${OUT}.tmp`, [`# sha ${sha} ${today()} — tests already failing on origin/${B}${deps}`, ...r.failed].join("\n") + "\n");
|
|
97
|
+
renameSync(`${OUT}.tmp`, OUT);
|
|
98
|
+
c.say(`${r.failed.length} ${m("tests failing on the base", "testes falhando na base")}`);
|
|
99
|
+
return true;
|
|
100
|
+
}, (st) => ({ fields: `status=${st} sha=${sha}`, notice: `baseline ${R} ${B} status=${st} sha=${sha} answer: ${OUT}` }));
|
|
101
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// orq block — takes an agent (or account) out of orq-pick until a date. For quotas without a collector
|
|
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.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { writeAtomic } from "../fsutil.js";
|
|
7
|
+
import { m } from "../i18n.js";
|
|
8
|
+
import { stateDir } from "../paths.js";
|
|
9
|
+
export const USAGE = `orq block <agent|account> [--until YYYY-MM-DD[THH:MM] | --days N] [--reason "text"] default: 7 days
|
|
10
|
+
orq block -r <agent|account> unblock early
|
|
11
|
+
orq block -l list active blocks (expired ones disappear by themselves)`;
|
|
12
|
+
const file = () => join(stateDir(), "blocks.json");
|
|
13
|
+
const now = () => Date.now() / 1000;
|
|
14
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
15
|
+
const fmt = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
16
|
+
export function load() {
|
|
17
|
+
let b = {};
|
|
18
|
+
try {
|
|
19
|
+
b = JSON.parse(readFileSync(file(), "utf8"));
|
|
20
|
+
}
|
|
21
|
+
catch { /* missing or broken: no blocks */ }
|
|
22
|
+
return Object.fromEntries(Object.entries(b).filter(([, v]) => (v?.until ?? 0) > now()));
|
|
23
|
+
}
|
|
24
|
+
const save = (b) => writeAtomic(file(), JSON.stringify(b, null, 2));
|
|
25
|
+
/** "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM" in local time (a bare date in JS would be UTC). */
|
|
26
|
+
export function parseLocal(s) {
|
|
27
|
+
const d = new Date(/^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s}T00:00` : s);
|
|
28
|
+
if (Number.isNaN(d.getTime()))
|
|
29
|
+
throw new Error(m(`invalid date: ${s}`, `data inválida: ${s}`));
|
|
30
|
+
return d;
|
|
31
|
+
}
|
|
32
|
+
export function block(argv) {
|
|
33
|
+
const b = load();
|
|
34
|
+
if (!argv.length || argv[0] === "-h" || argv[0] === "--help") {
|
|
35
|
+
console.log(USAGE);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
if (argv[0] === "-l") {
|
|
39
|
+
for (const k of Object.keys(b).sort())
|
|
40
|
+
console.log(`${k.padEnd(14)} ${m("until", "até")} ${fmt(new Date(b[k].until * 1000))} ${b[k].reason ?? ""}`);
|
|
41
|
+
if (!Object.keys(b).length)
|
|
42
|
+
console.log(m("no active blocks", "nenhum bloqueio ativo"));
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
if (argv[0] === "-r") {
|
|
46
|
+
if (!argv[1]) {
|
|
47
|
+
console.error(USAGE);
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
delete b[argv[1]];
|
|
51
|
+
save(b);
|
|
52
|
+
console.log(`${argv[1]} ${m("unblocked", "liberado")}`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const [name, ...rest] = argv;
|
|
56
|
+
let until = new Date(Date.now() + 7 * 86400_000), reason = "";
|
|
57
|
+
while (rest.length) {
|
|
58
|
+
const o = rest.shift();
|
|
59
|
+
if (o === "--until")
|
|
60
|
+
until = parseLocal(rest.shift() ?? "");
|
|
61
|
+
else if (o === "--days")
|
|
62
|
+
until = new Date(Date.now() + Number(rest.shift()) * 86400_000);
|
|
63
|
+
else if (o === "--reason")
|
|
64
|
+
reason = rest.shift() ?? "";
|
|
65
|
+
}
|
|
66
|
+
if (Number.isNaN(until.getTime())) {
|
|
67
|
+
console.error(USAGE);
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
b[name] = { until: until.getTime() / 1000, reason };
|
|
71
|
+
save(b);
|
|
72
|
+
console.log(`${name} ${m("out of orq-pick until", "fora do orq-pick até")} ${fmt(until)}`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|