@irythill/orq 0.4.0 → 0.5.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 +11 -4
- package/README.pt-BR.md +9 -2
- package/dist/agents.js +10 -0
- package/dist/ci.js +4 -2
- package/dist/cli.js +3 -1
- package/dist/commands/agent.js +29 -11
- package/dist/commands/block.js +26 -3
- package/dist/commands/board.js +23 -11
- package/dist/commands/completion.js +2 -2
- 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 +2 -0
- package/dist/config.js +1 -1
- package/dist/diagnose.js +105 -0
- package/dist/notice.js +282 -0
- package/dist/pick.js +33 -4
- package/dist/roles.js +11 -2
- package/dist/rounds.js +14 -5
- package/dist/stats.js +4 -3
- package/dist/testrun.js +4 -27
- package/package.json +1 -1
- package/skills/issue/SKILL.md +9 -4
- package/templates/workspace.CLAUDE.md +2 -1
package/README.md
CHANGED
|
@@ -146,13 +146,13 @@ roles = ["review"] # here it only reviews; for someone els
|
|
|
146
146
|
[agents.opencode.models.review]
|
|
147
147
|
default = "minimax/MiniMax-M3"
|
|
148
148
|
|
|
149
|
-
[agents.
|
|
149
|
+
[agents.revtool] # a review tool, not a chat: no prompt, no session
|
|
150
150
|
vendor = "minimax" # the vendor of the model it runs, for the cross-vendor rule
|
|
151
151
|
roles = ["review"]
|
|
152
|
-
run = "
|
|
152
|
+
run = "revtool --markdown -b {base}" # {base} = orq agent -b; it reads the diff itself
|
|
153
153
|
write = false
|
|
154
154
|
ok_exit = [0, 1] # exit 1 = findings, still a verdict
|
|
155
|
-
no_review = "
|
|
155
|
+
no_review = "Nothing to review" # matched on the answer's first line (or a short answer): not a verdict, status=fail
|
|
156
156
|
# filter = "jq -r '...'" # optional: its output → [blocker]/[suggestion] lines
|
|
157
157
|
|
|
158
158
|
[conductor]
|
|
@@ -216,6 +216,12 @@ To see everything at once, keep **`orq board`** open in a tab or window (any ter
|
|
|
216
216
|
test queue, running agents (model, memory against the cap, last action), conductors and their issues, memory, quota,
|
|
217
217
|
CI/CD per claimed issue, blocks and the 7-day scoreboard. `--once` prints it once, `--json` for scripts. With
|
|
218
218
|
`ui.toast.delivery = "system"` in herdr's config, each finished step also pops up a native OS notification.
|
|
219
|
+
A failure (tests red, agent `status=fail`, CI red) is also **loud**: besides the line typed into the conductor's pane
|
|
220
|
+
(delivered by orq with retries and a log, `orq notice`), you get a herdr notification with sound, a `✗` title on the
|
|
221
|
+
conductor's pane and, on Linux, a desktop notification (`[ui] desktop_notify = false` turns that one off; needs
|
|
222
|
+
`notify-send`). Successes are quiet. The board's red top line lists failures from the last hour whose notice was not
|
|
223
|
+
delivered or that the conductor has not yet acted on (it clears when the conductor runs `orq wait`, `orq verify` or
|
|
224
|
+
`orq agent` for the issue).
|
|
219
225
|
For quota without opening the board, `orq quota --line` fits herdr's tab bar (a `tab_bar_right` command entry,
|
|
220
226
|
[docs/herdr.md](docs/herdr.md#quota-in-the-tab-bar)). `orq ci` shows each claimed issue's PR checks (queued, running,
|
|
221
227
|
passed, failed or none, read from `gh` right then — it is a status check the user asked for, so unlike the board's
|
|
@@ -242,6 +248,7 @@ off the same issue. Details, shortcuts and scenarios: [docs/herdr.md](docs/herdr
|
|
|
242
248
|
|---|---|
|
|
243
249
|
| `orq agent -a <agent> ...` (`herdr-agent`) | Runs an agent with a memory cap in a herdr tab, a tmux window or the background; `-r` read only, `-R <id>` continue the session, `-i` attach a file, `-b <base>` attach the diff, `-T` the agent's own screen (herdr or tmux; `-R` with it open types the follow-up into it) |
|
|
244
250
|
| `orq instructions claude\|agents` | Writes the `/issue` playbook as `SKILL.md` (Claude Code) or into `AGENTS.md` (Codex, OpenCode…) |
|
|
251
|
+
| `orq notice [--list] [-n N]` | The delivery log of notices to conductors: each attempt, retry and failure (`<state>/notices.jsonl`) |
|
|
245
252
|
| `orq wait <prefix>...` | Blocks until those agents finish; prints `status=` and the answer file (any conductor, any terminal) |
|
|
246
253
|
| `orq verify ...` (`herdr-verify`) | Tests related to the diff, queued and memory-capped; failures that also fail on the base count as pre-existing |
|
|
247
254
|
| `orq board` (`herdr-board`) | Live board: queue, agents, conductors, quota, blocks, scoreboard |
|
|
@@ -251,7 +258,7 @@ off the same issue. Details, shortcuts and scenarios: [docs/herdr.md](docs/herdr
|
|
|
251
258
|
| `orq baseline ...` (`herdr-baseline`) | Manual snapshot of the tests failing on the base (outside the flow) |
|
|
252
259
|
| `orq gc [-w <workspace>] [--yes] [--days N] [--json]` | Lists issue worktrees that are safe to remove (PR merged, or content already in the base, squash merges included); `--yes` removes them. Never touches uncommitted work, unpushed commits, a live claim or a running agent. Optional daily `orq-gc.timer` |
|
|
253
260
|
| `orq ci [-w <workspace>] [--json]` | CI/CD status per claimed issue with a PR: queued, running, passed, failed or none, collected live via `gh` right then (the cache it fills is what `orq board`'s CI column reads, never blocking on the network). A run turning red or green pops a herdr notification, and a failure also pings the owning conductor |
|
|
254
|
-
| `orq rounds <ID> [-w <workspace>] [--json]` | Watchdog
|
|
261
|
+
| `orq rounds <ID> [-w <workspace>] [--json] [--diagnose]` | Watchdog: `--diagnose` (warn/stop only) has a cheap read only agent of another vendor (pick role `watchdog`, falls back to `opinion`, then `review`) diagnose why rounds don't converge → `.orquestra/<ID>/diagnosis-<n>.md`, advisory; `orq agent -R -F` past `max_rounds` needs it (`-F -F` bypasses). Without it, no LLM: fix rounds vs `policy.max_rounds`, time per round, reviewer findings repeated across rounds, alert ok / warn / stop. The board marks each issue (`r2/2 ⚠`) |
|
|
255
262
|
| `orq quota --line` | One line of quota for a status bar (`work 64 · home 94! · codex 81`), from the board's cache, never the network |
|
|
256
263
|
| `orq setup` | First configuration and install: environment, skills for Claude, config, conductor workspace |
|
|
257
264
|
| `orq completion bash\|zsh\|fish\|powershell` | Prints the shell completion script (see Install) |
|
package/README.pt-BR.md
CHANGED
|
@@ -208,7 +208,13 @@ Para ver tudo de uma vez, deixe o **`orq board`** aberto numa aba ou janela (qua
|
|
|
208
208
|
`herdr-board` o chama): fila de testes, agentes rodando (modelo, memória contra o limite, última ação), conductors e
|
|
209
209
|
suas issues, memória, quota, CI/CD de cada issue reivindicada, bloqueios e o placar de 7 dias. `--once` imprime uma
|
|
210
210
|
vez, `--json` para scripts. Com `ui.toast.delivery = "system"` na config do herdr, cada etapa concluída também gera
|
|
211
|
-
uma notificação nativa do sistema.
|
|
211
|
+
uma notificação nativa do sistema. Uma falha (testes vermelhos, agente `status=fail`, CI vermelho) também é **barulhenta**: além da linha digitada no pane do
|
|
212
|
+
conductor (entregue pelo orq com novas tentativas e log, `orq notice`), você recebe uma notificação do herdr com som, um
|
|
213
|
+
título `✗` no pane do conductor e, no Linux, uma notificação de desktop (`[ui] desktop_notify = false` desliga essa;
|
|
214
|
+
precisa do `notify-send`). Sucessos ficam quietos. A linha vermelha no topo do board lista as falhas da última hora
|
|
215
|
+
cujo aviso não foi entregue ou que o conductor ainda não tratou (some quando ele roda `orq wait`, `orq verify` ou
|
|
216
|
+
`orq agent` para a issue).
|
|
217
|
+
Para ver a cota sem abrir o board, o `orq quota --line` cabe na barra de abas do
|
|
212
218
|
herdr (uma entrada `command` no `tab_bar_right`, [docs/pt-BR/herdr.md](docs/pt-BR/herdr.md#cota-na-barra-de-abas)).
|
|
213
219
|
O `orq ci` mostra os checks do PR de cada issue reivindicada (na fila, rodando, passou, falhou ou nenhum, lido via
|
|
214
220
|
`gh` na hora — é uma consulta que o usuário pediu agora, então, diferente da coluna de CI do board, não lê só o
|
|
@@ -235,6 +241,7 @@ dois conductors peguem a mesma issue. Detalhes, atalhos e cenários: [docs/pt-BR
|
|
|
235
241
|
|---|---|
|
|
236
242
|
| `orq agent -a <agent> ...` (`herdr-agent`) | Roda um agente com limite de memória numa aba do herdr, numa janela do tmux ou em segundo plano; `-r` só leitura, `-R <id>` continua a sessão, `-i` anexa um arquivo, `-b <base>` anexa o diff, `-T` a tela do próprio agente (herdr ou tmux; `-R` com ela aberta digita o follow-up nela) |
|
|
237
243
|
| `orq instructions claude\|agents` | Escreve o playbook do `/issue` como `SKILL.md` (Claude Code) ou dentro do `AGENTS.md` (Codex, OpenCode…) |
|
|
244
|
+
| `orq notice [--list] [-n N]` | O log de entrega dos avisos aos conductors: cada tentativa, nova tentativa e falha (`<state>/notices.jsonl`) |
|
|
238
245
|
| `orq wait <prefixo>...` | Bloqueia até esses agentes terminarem; imprime `status=` e o arquivo da resposta (qualquer conductor, qualquer terminal) |
|
|
239
246
|
| `orq verify ...` (`herdr-verify`) | Testes relacionados ao diff, em fila e com limite de memória; falhas que também falham na base contam como pré-existentes |
|
|
240
247
|
| `orq board` (`herdr-board`) | Painel ao vivo: fila, agentes, conductors, cota, bloqueios, placar |
|
|
@@ -244,7 +251,7 @@ dois conductors peguem a mesma issue. Detalhes, atalhos e cenários: [docs/pt-BR
|
|
|
244
251
|
| `orq baseline ...` (`herdr-baseline`) | Snapshot manual dos testes que falham na base (fora do fluxo) |
|
|
245
252
|
| `orq gc [-w <workspace>] [--yes] [--days N] [--json]` | Lista worktrees de issues que podem ser removidas (PR mergeado, ou conteúdo já na base, inclusive squash merge); `--yes` as remove. Nunca mexe em trabalho não commitado, commit fora do remote, claim ativo ou agente rodando. `orq-gc.timer` diário opcional |
|
|
246
253
|
| `orq ci [-w <workspace>] [--json]` | Status de CI/CD de cada issue reivindicada com PR: na fila, rodando, passou, falhou ou nenhum, coletado na hora via `gh` (o cache que ele preenche é o que a coluna de CI do `orq board` lê, sem nunca travar na rede). Quando vira verde ou vermelho sai uma notificação do herdr, e uma falha também avisa o conductor dono |
|
|
247
|
-
| `orq rounds <ID> [-w <workspace>] [--json]` | Watchdog sem LLM: rodadas de fix vs `policy.max_rounds`, tempo por rodada, achados do revisor repetidos entre rodadas, alerta ok / warn / stop. O board marca cada issue (`r2/2 ⚠`) |
|
|
254
|
+
| `orq rounds <ID> [-w <workspace>] [--json] [--diagnose]` | Watchdog: `--diagnose` (só warn/stop) pede a um agente barato, somente leitura e de outro fornecedor (papel de pick `watchdog`, cai para `opinion`, depois `review`) o diagnóstico de por que as rodadas não convergem → `.orquestra/<ID>/diagnosis-<n>.md`, consultivo; `orq agent -R -F` além de `max_rounds` exige (`-F -F` ignora). Sem ele, sem LLM: rodadas de fix vs `policy.max_rounds`, tempo por rodada, achados do revisor repetidos entre rodadas, alerta ok / warn / stop. O board marca cada issue (`r2/2 ⚠`) |
|
|
248
255
|
| `orq quota --line` | Uma linha de cota para barra de status (`work 64 · home 94! · codex 81`), do cache do board, nunca da rede |
|
|
249
256
|
| `orq setup` | Primeira configuração e instalação: ambiente, skills do Claude, config, workspace do conductor |
|
|
250
257
|
| `orq completion bash\|zsh\|fish\|powershell` | Imprime o script de autocompletar do shell (veja Instalação) |
|
package/dist/agents.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { m } from "./i18n.js";
|
|
2
|
+
/** Does the agent's command take the prompt (stdin, a file in its folder or a {prompt*} placeholder)? A review tool like revtool does not. */
|
|
3
|
+
export const usesPrompt = (a) => Boolean(a.stdin || a.prompt_in_dir || /\{prompt(_text|_hint)?\}/.test(a.run ?? ""));
|
|
4
|
+
/** Read only runs of a `throwaway` agent get a worktree copy of HEAD, but only where there is a git repo to copy. */
|
|
5
|
+
export const useThrowaway = (def, ro, resume, inGit) => Boolean(def.throwaway && ro && !resume && inGit);
|
|
2
6
|
const NOMCP_CODEX = "-c mcp_servers.plane.enabled=false -c mcp_servers.shadcn.enabled=false";
|
|
3
7
|
const CLAUDE = "claude -p --strict-mcp-config --output-format stream-json --verbose [--model {model}] --effort {effort:low}";
|
|
4
8
|
const CLAUDE_TUI = "claude --strict-mcp-config [--model {model}] --effort {effort:low}";
|
|
@@ -212,3 +216,9 @@ export const FORMATS = {
|
|
|
212
216
|
},
|
|
213
217
|
},
|
|
214
218
|
};
|
|
219
|
+
/** The model in an agent's argv (-m / --model / --model=x), without the provider prefix. */
|
|
220
|
+
export function argvModel(a) {
|
|
221
|
+
const i = a.findIndex((x) => x === "-m" || x === "--model");
|
|
222
|
+
const v = i >= 0 ? a[i + 1] : a.find((x) => x.startsWith("--model="))?.slice(8);
|
|
223
|
+
return v?.split("/").pop();
|
|
224
|
+
}
|
package/dist/ci.js
CHANGED
|
@@ -203,7 +203,7 @@ function notifyDesktop(issue, repo, state) {
|
|
|
203
203
|
*/
|
|
204
204
|
export function collectCi(ws, hooks = {}) {
|
|
205
205
|
// Detached: collectCi() must never block on a conductor stuck in a dialog (herdr-notify waits up to 30 min).
|
|
206
|
-
const desktop = hooks.desktop ?? notifyDesktop, toOwner = hooks.owner ?? ((pane, line) => notify(pane, line
|
|
206
|
+
const desktop = hooks.desktop ?? notifyDesktop, toOwner = hooks.owner ?? ((pane, line) => notify(pane, line)), notice = hooks.notice ?? ((l) => console.log(l));
|
|
207
207
|
const now = Date.now(), worktrees = listWorktrees(ws);
|
|
208
208
|
const found = claimedIssues(ws).flatMap(({ id, owner, kind }) => issueBranches(ws, id, worktrees).map(({ repo, branch }) => ({ id, owner, kind, repo, branch, ...prStatus(join(ws, repo), branch) })));
|
|
209
209
|
const toNotify = [];
|
|
@@ -225,7 +225,9 @@ export function collectCi(ws, hooks = {}) {
|
|
|
225
225
|
return mine;
|
|
226
226
|
});
|
|
227
227
|
for (const { owner, kind, id, repo, state, sha } of toNotify) {
|
|
228
|
-
|
|
228
|
+
// A failure the owner is told about already pops up (src/notice.ts signals it loudly): no second popup.
|
|
229
|
+
if (!(state === "failed" && owner && kind === "herdr"))
|
|
230
|
+
desktop(id, repo, state);
|
|
229
231
|
if (state === "failed" && owner) {
|
|
230
232
|
const line = `[ORQ CI] ${id} ${repo} failed (${sha.slice(0, 7)})`;
|
|
231
233
|
if (kind === "herdr")
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { handoff, USAGE as HANDOFF } from "./commands/handoff.js";
|
|
|
18
18
|
import { instructions, USAGE as INSTRUCTIONS } from "./commands/instructions.js";
|
|
19
19
|
import { lang, USAGE as LANG } from "./commands/lang.js";
|
|
20
20
|
import { lock, USAGE as LOCK } from "./commands/lock.js";
|
|
21
|
+
import { noticeCmd, USAGE as NOTICE } from "./notice.js";
|
|
21
22
|
import { pick, USAGE as PICK } from "./commands/pick.js";
|
|
22
23
|
import { quota, USAGE as QUOTA } from "./commands/quota.js";
|
|
23
24
|
import { rounds, USAGE as ROUNDS } from "./commands/rounds.js";
|
|
@@ -27,7 +28,7 @@ import { setup, USAGE as SETUP } from "./commands/setup.js";
|
|
|
27
28
|
import { stats, USAGE as STATS } from "./commands/stats.js";
|
|
28
29
|
import { verify, USAGE as VERIFY } from "./commands/verify.js";
|
|
29
30
|
import { wait, USAGE as WAIT } from "./commands/wait.js";
|
|
30
|
-
const COMMANDS = { config, set, board, lang, block, quota, pick, stats, rounds, setup, lock, claim, deps, gc, ci, handoff, run, agent, wait, instructions, verify, baseline,
|
|
31
|
+
const COMMANDS = { config, set, board, lang, block, quota, pick, stats, rounds, setup, lock, claim, deps, gc, ci, handoff, run, agent, wait, instructions, verify, baseline, notice: noticeCmd,
|
|
31
32
|
completion: (a) => completion(a, Object.keys(COMMANDS)) };
|
|
32
33
|
const usage = () => `orq <command> [args]
|
|
33
34
|
|
|
@@ -48,6 +49,7 @@ ${HANDOFF}
|
|
|
48
49
|
${VERIFY}
|
|
49
50
|
${BASELINE}
|
|
50
51
|
${INSTRUCTIONS}
|
|
52
|
+
${NOTICE}
|
|
51
53
|
${CLAIM}
|
|
52
54
|
${DEPS}
|
|
53
55
|
${GC}
|
package/dist/commands/agent.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// under its memory cap (src/process.ts), in a herdr tab, a tmux window or the background (src/terminal.ts).
|
|
3
3
|
// Writes <prefix>.out.md (final answer) / .log / .id (session, for -R) / .pane / .done ("status=ok|fail rc=N at=...").
|
|
4
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
|
|
5
|
+
// "[HERDR-AGENT DONE] <title> status=... answer: <prefix>.out.md" typed into its pane by src/notice.ts.
|
|
6
6
|
// The launcher writes <prefix>.job.json and starts `orq agent --run <job>` on the terminal host; that runner does the rest.
|
|
7
7
|
// -T (or machine.tui / HERDR_AGENT_TUI=1): the agent's own screen (its `tui` template) in a herdr tab or tmux window,
|
|
8
8
|
// same cap and files. The agent calls `orq agent --turn <job> <kind>` at every end of turn, which writes .out.md/.id/
|
|
@@ -14,7 +14,8 @@ import { appendFileSync, copyFileSync, createWriteStream, existsSync, mkdirSync,
|
|
|
14
14
|
import { basename, dirname, join, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { claudeAccounts } from "../accounts.js";
|
|
17
|
-
import {
|
|
17
|
+
import { gateFor } from "../diagnose.js";
|
|
18
|
+
import { argvModel, useThrowaway, agentDef, ALIASES, expandString, expandTemplate, FORMATS } from "../agents.js";
|
|
18
19
|
import { getPath, loadConfig } from "../config.js";
|
|
19
20
|
import { writeAtomic } from "../fsutil.js";
|
|
20
21
|
import { m } from "../i18n.js";
|
|
@@ -22,8 +23,9 @@ import { alive } from "../lock.js";
|
|
|
22
23
|
import { stateDir } from "../paths.js";
|
|
23
24
|
import { killTree, parseSize, resolveMode, runCapped } from "../process.js";
|
|
24
25
|
import { closeOwnTab, detectHost, launch, screenOpen, shq, typeInto } from "../terminal.js";
|
|
26
|
+
import { ackPrefix } from "../notice.js";
|
|
25
27
|
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 [-F]] [-T] -- <prompt.md> run a child agent";
|
|
28
|
+
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 [-F]]] [-T] -- <prompt.md> run a child agent";
|
|
27
29
|
const CLI = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
28
30
|
const PF = ".herdr-agent-prompt.md"; // prompt_in_dir file name (kept from herdr-agent)
|
|
29
31
|
const git = (dir, ...a) => {
|
|
@@ -35,8 +37,8 @@ export async function agent(argv) {
|
|
|
35
37
|
return runJob(argv[1]);
|
|
36
38
|
if (argv[0] === "--turn")
|
|
37
39
|
return turn(argv[1], argv[2], argv.slice(3));
|
|
38
|
-
let a = process.env.AGENT || "
|
|
39
|
-
let ro = false, tui = false, force =
|
|
40
|
+
let a = process.env.AGENT || "", g = "", t = "", d = "", o = "", model = "", effort = "", acc = "", resume = "", base = "";
|
|
41
|
+
let ro = false, tui = false, force = 0, prompt;
|
|
40
42
|
const inc = [];
|
|
41
43
|
for (let i = 0; i < argv.length; i++) {
|
|
42
44
|
const x = argv[i], v = () => argv[++i] ?? "";
|
|
@@ -73,7 +75,7 @@ export async function agent(argv) {
|
|
|
73
75
|
else if (x === "-T")
|
|
74
76
|
tui = true;
|
|
75
77
|
else if (x === "-F")
|
|
76
|
-
force
|
|
78
|
+
force++;
|
|
77
79
|
else if (!x.startsWith("-") && prompt === undefined)
|
|
78
80
|
prompt = x;
|
|
79
81
|
else {
|
|
@@ -90,6 +92,11 @@ export async function agent(argv) {
|
|
|
90
92
|
console.error(`${m("missing", "faltou")} -${f}`);
|
|
91
93
|
return 2;
|
|
92
94
|
}
|
|
95
|
+
// No default agent: an empty $(orq pick ...) means no candidate, and must not start anything.
|
|
96
|
+
if (!a) {
|
|
97
|
+
console.error(m("missing -a (if it came from orq pick, it found no candidate): nothing started", "faltou -a (se veio do orq pick, ele não achou candidato): nada foi iniciado"));
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
93
100
|
a = ALIASES[a] ?? a;
|
|
94
101
|
const { cfg } = loadConfig();
|
|
95
102
|
const def = agentDef(cfg, a);
|
|
@@ -133,9 +140,18 @@ export async function agent(argv) {
|
|
|
133
140
|
}
|
|
134
141
|
catch { /* first follow-up */ }
|
|
135
142
|
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.`));
|
|
143
|
+
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, run \`orq rounds <ID> --diagnose\`, then 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, rode \`orq rounds <ID> --diagnose\` e repita com -F.`));
|
|
137
144
|
return 4;
|
|
138
145
|
}
|
|
146
|
+
// -F past the limit also needs the watchdog's diagnosis of this round count (-F -F skips it: the user's own call).
|
|
147
|
+
// The issue folder is the prefix's folder, .orquestra/<ID>/ (where the .rounds-<session> counter lives).
|
|
148
|
+
if (n >= max && force === 1 && basename(dirname(dirname(O))) === ".orquestra") {
|
|
149
|
+
const g = gateFor(dirname(O)), id = basename(dirname(O));
|
|
150
|
+
if (g.verdict !== "ok") {
|
|
151
|
+
console.error(m(`no current diagnosis: ${g.verdict === "stale" ? `diagnosis-${g.rounds}.md is older than the latest fix/review` : `diagnosis-${g.rounds}.md does not exist`}. Run \`orq rounds ${id} --diagnose\` first, read it, and ask the user. -F -F skips this check.`, `sem diagnóstico atual: ${g.verdict === "stale" ? `diagnosis-${g.rounds}.md é mais antigo que o último fix/revisão` : `diagnosis-${g.rounds}.md não existe`}. Rode \`orq rounds ${id} --diagnose\` antes, leia e consulte o usuário. -F -F pula esta checagem.`));
|
|
152
|
+
return 4;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
139
155
|
mkdirSync(dirname(O), { recursive: true });
|
|
140
156
|
writeFileSync(cf, String(n + 1));
|
|
141
157
|
}
|
|
@@ -176,6 +192,7 @@ export async function agent(argv) {
|
|
|
176
192
|
}
|
|
177
193
|
catch { /* first run or ended */ }
|
|
178
194
|
if (tui && resume && alive(pid) && screenOpen(open)) {
|
|
195
|
+
ackPrefix(O); // a follow-up on the issue: its failure notices were seen
|
|
179
196
|
for (const s of [".done", ".out.md"])
|
|
180
197
|
rmSync(O + s, { force: true });
|
|
181
198
|
typeInto(open, readFileSync(P, "utf8"));
|
|
@@ -184,7 +201,7 @@ export async function agent(argv) {
|
|
|
184
201
|
}
|
|
185
202
|
for (const s of [".done", ".out.md", ".pid"])
|
|
186
203
|
rmSync(O + s, { force: true });
|
|
187
|
-
const throwaway =
|
|
204
|
+
const throwaway = useThrowaway(def, ro, Boolean(resume), git(repo, "rev-parse", "--git-dir") !== undefined);
|
|
188
205
|
const dir = throwaway ? `${repo}.review-${process.pid}` : repo;
|
|
189
206
|
const gd = git(repo, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
190
207
|
const wg = git(repo, "rev-parse", "--absolute-git-dir");
|
|
@@ -253,6 +270,7 @@ export async function agent(argv) {
|
|
|
253
270
|
// Readable copy of the command (also what orq stats and herdr-board read).
|
|
254
271
|
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` +
|
|
255
272
|
`cd ${shq(dir)}\n${Object.entries(env).map(([k, val]) => `${k}=${shq(val)} `).join("")}${cmd.map(shq).join(" ")}${job.stdin ? ` < ${shq(P)}` : ""}\n`);
|
|
273
|
+
ackPrefix(O); // starting work on the issue again: its failure notices were seen
|
|
256
274
|
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 });
|
|
257
275
|
writeFileSync(`${O}.pane`, `${where}\n`);
|
|
258
276
|
console.log(`[orq agent] ${t} → ${where}${tui ? " (TUI)" : ""}`);
|
|
@@ -326,7 +344,7 @@ function turn(file, kind, rest) {
|
|
|
326
344
|
const st = /\S/.test(answer) && !noReview ? "ok" : "fail";
|
|
327
345
|
writeAtomic(`${O}.done`, `status=${st} rc=0 at=${new Date().toISOString()} agent=${j.agent} mode=tui\n`);
|
|
328
346
|
if (j.parent)
|
|
329
|
-
notify(j.parent, `[HERDR-AGENT DONE] ${j.title} status=${st} rc=0 answer: ${O}.out.md
|
|
347
|
+
notify(j.parent, `[HERDR-AGENT DONE] ${j.title} status=${st} rc=0 answer: ${O}.out.md`);
|
|
330
348
|
}
|
|
331
349
|
catch (e) {
|
|
332
350
|
console.error(`orq agent --turn: ${e.message}`);
|
|
@@ -396,7 +414,7 @@ async function runJob(file) {
|
|
|
396
414
|
const stateF = join(stateDir(), "agents", `${j.unit}.json`);
|
|
397
415
|
const at = new Date().toISOString();
|
|
398
416
|
const state = (extra = {}) => { try {
|
|
399
|
-
writeAtomic(stateF, JSON.stringify({ pid: process.pid, prefix: O, title: j.title, agent: j.agent, host: j.host, cap: j.cap, at, ...extra }));
|
|
417
|
+
writeAtomic(stateF, JSON.stringify({ pid: process.pid, prefix: O, title: j.title, agent: j.agent, model: argvModel(j.argv), host: j.host, cap: j.cap, at, ...extra }));
|
|
400
418
|
}
|
|
401
419
|
catch { /* best effort */ } };
|
|
402
420
|
state();
|
|
@@ -449,7 +467,7 @@ async function runJob(file) {
|
|
|
449
467
|
if (!existsSync(`${O}.done`)) {
|
|
450
468
|
writeAtomic(`${O}.done`, `status=fail rc=${code} at=${new Date().toISOString()} agent=${j.agent} mode=tui\n`);
|
|
451
469
|
if (j.parent)
|
|
452
|
-
notify(j.parent, `[HERDR-AGENT DONE] ${j.title} status=fail rc=${code} answer: ${O}.out.md
|
|
470
|
+
notify(j.parent, `[HERDR-AGENT DONE] ${j.title} status=fail rc=${code} answer: ${O}.out.md`);
|
|
453
471
|
}
|
|
454
472
|
rmSync(`${O}.pid`, { force: true });
|
|
455
473
|
return ok ? "ok" : "fail";
|
package/dist/commands/block.js
CHANGED
|
@@ -8,7 +8,9 @@ import { writeAtomic } from "../fsutil.js";
|
|
|
8
8
|
import { loginId } from "../identity.js";
|
|
9
9
|
import { m } from "../i18n.js";
|
|
10
10
|
import { stateDir } from "../paths.js";
|
|
11
|
-
|
|
11
|
+
import { getPath, loadConfig } from "../config.js";
|
|
12
|
+
import { collect } from "../quota.js";
|
|
13
|
+
export const USAGE = `orq block <agent|account> [--until YYYY-MM-DD[THH:MM] | --days N | --quota] [--reason "text"] default: 7 days; --quota: until the quota window resets
|
|
12
14
|
orq block -r <agent|account> unblock early
|
|
13
15
|
orq block -l list active blocks (expired ones disappear by themselves)`;
|
|
14
16
|
const file = () => join(stateDir(), "blocks.json");
|
|
@@ -45,7 +47,16 @@ export function parseLocal(s) {
|
|
|
45
47
|
throw new Error(m(`invalid date: ${s}`, `data inválida: ${s}`));
|
|
46
48
|
return d;
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
/** When a quota line's exhausted window resets, in epoch seconds: the week if it is used up, else the 5h window (the
|
|
51
|
+
* reading can lag behind the limit: Codex's comes from its last session). undefined = no reset known. */
|
|
52
|
+
export function quotaUntil(line) {
|
|
53
|
+
const reset = (k) => { const x = new RegExp(`\\b${k}=(\\d+)`).exec(line); return x ? Number(x[1]) : undefined; };
|
|
54
|
+
const week = /\bweek=(\d+)%/.exec(line);
|
|
55
|
+
if (week && Number(week[1]) >= 100 && reset("resetweek"))
|
|
56
|
+
return reset("resetweek");
|
|
57
|
+
return reset("reset5h") ?? reset("resetweek");
|
|
58
|
+
}
|
|
59
|
+
export async function block(argv) {
|
|
49
60
|
const b = load();
|
|
50
61
|
if (!argv.length || argv[0] === "-h" || argv[0] === "--help") {
|
|
51
62
|
console.log(USAGE);
|
|
@@ -69,7 +80,7 @@ export function block(argv) {
|
|
|
69
80
|
return 0;
|
|
70
81
|
}
|
|
71
82
|
const [name, ...rest] = argv;
|
|
72
|
-
let until = new Date(Date.now() + 7 * 86400_000), reason = "";
|
|
83
|
+
let until = new Date(Date.now() + 7 * 86400_000), reason = "", byQuota = false;
|
|
73
84
|
while (rest.length) {
|
|
74
85
|
const o = rest.shift();
|
|
75
86
|
if (o === "--until")
|
|
@@ -78,6 +89,18 @@ export function block(argv) {
|
|
|
78
89
|
until = new Date(Date.now() + Number(rest.shift()) * 86400_000);
|
|
79
90
|
else if (o === "--reason")
|
|
80
91
|
reason = rest.shift() ?? "";
|
|
92
|
+
else if (o === "--quota")
|
|
93
|
+
byQuota = true;
|
|
94
|
+
}
|
|
95
|
+
if (byQuota) {
|
|
96
|
+
const sources = (getPath(loadConfig().cfg, "quota") ?? {});
|
|
97
|
+
const line = (await collect(sources, [name])).find((l) => l.startsWith(`${name} `));
|
|
98
|
+
const r = line && quotaUntil(line);
|
|
99
|
+
if (!r) {
|
|
100
|
+
console.error(m(`${name}: no quota reset known; use --until or --days`, `${name}: sem horário de renovação da cota; use --until ou --days`));
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
until = new Date(r * 1000);
|
|
81
104
|
}
|
|
82
105
|
if (Number.isNaN(until.getTime())) {
|
|
83
106
|
console.error(USAGE);
|
package/dist/commands/board.js
CHANGED
|
@@ -16,12 +16,14 @@ 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
22
|
import { processTableAsync, treeMem } 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";
|
|
@@ -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) {
|
|
@@ -110,7 +113,7 @@ function agents(table, now) {
|
|
|
110
113
|
continue;
|
|
111
114
|
if ((st === "ok" || st === "fail") && now - (end ?? at) > RECENT_MS)
|
|
112
115
|
continue;
|
|
113
|
-
const a = { unit: f.slice(0, -5), title: String(s.title ?? ""), agent: String(s.agent ?? "?"), model: modelOf(prefix),
|
|
116
|
+
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
117
|
host: s.host, pid, issue: basename(dirname(prefix)), label: basename(prefix), state: st,
|
|
115
118
|
secs: Math.max(0, Math.round(((end ?? now) - at) / 1000)), cap: typeof s.cap === "number" ? s.cap : undefined };
|
|
116
119
|
if (s.rc !== undefined)
|
|
@@ -268,7 +271,7 @@ export async function snapshot(wss, table) {
|
|
|
268
271
|
return {
|
|
269
272
|
at: new Date(now).toISOString(), mem: { used: totalmem() - freemem(), total: totalmem() }, workspaces: wss,
|
|
270
273
|
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),
|
|
274
|
+
conductors: await conductors(wss), quota: quota(now), ci: ci(wss, now), blocks: loadBlocks(), scoreboard: scoreboard(wss[0], now), notices: pendingNotices(readLog(), now),
|
|
272
275
|
};
|
|
273
276
|
}
|
|
274
277
|
// --- render -----------------------------------------------------------------------------------------------------
|
|
@@ -322,6 +325,15 @@ export function render(s, theme, cols, mode = colorMode()) {
|
|
|
322
325
|
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
326
|
const mp = Math.round(100 * s.mem.used / s.mem.total);
|
|
324
327
|
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}`);
|
|
328
|
+
// Red line(s) first: a failure the conductor may not know about is what a person must see before anything else.
|
|
329
|
+
if (s.notices.length) {
|
|
330
|
+
const ago = (at) => dur(Math.max(0, Math.round((new Date(s.at).getTime() - at) / 1000)));
|
|
331
|
+
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}`);
|
|
332
|
+
for (const n of s.notices.slice(0, 5)) {
|
|
333
|
+
const what = n.line.replace(/^\[[^\]]+\] /, "").replace(/ answer: .*/, "");
|
|
334
|
+
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}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
325
337
|
section("TRANSPORT", m("test queue", "fila de testes"));
|
|
326
338
|
for (const h of s.queue.holders)
|
|
327
339
|
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}`
|
|
@@ -332,7 +344,7 @@ export function render(s, theme, cols, mode = colorMode()) {
|
|
|
332
344
|
section("TRACKS", m("agents", "agentes"));
|
|
333
345
|
s.agents.forEach((a, i) => {
|
|
334
346
|
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}`;
|
|
347
|
+
+ `${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
|
|
336
348
|
if (a.state === "running") {
|
|
337
349
|
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
350
|
const warn = a.secs > 3 * 3600 ? ` ${c.err}⚠ ${m("for", "há")} ${dur(a.secs)}${c.r}` : "";
|
|
@@ -18,9 +18,9 @@ const DESC = {
|
|
|
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
|
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++) {
|
package/dist/commands/verify.js
CHANGED
|
@@ -11,6 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { getPath, loadConfig } from "../config.js";
|
|
13
13
|
import { m } from "../i18n.js";
|
|
14
|
+
import { ackPrefix } from "../notice.js";
|
|
14
15
|
import { dropWorktree, failures, git, inJob, JS_EXT, launchJob, linkModules, runnerArgs, runnerOf, runTests, testCap, today, unsupported } from "../testrun.js";
|
|
15
16
|
export const USAGE = "orq verify -w <workspace> -r <repo> -d <worktree> -b <base> -o <prefix> [-g group] [-x] tests of the diff, new vs pre-existing failures";
|
|
16
17
|
export async function verify(argv) {
|
|
@@ -36,6 +37,7 @@ export async function verify(argv) {
|
|
|
36
37
|
}
|
|
37
38
|
const out = resolve(o.o);
|
|
38
39
|
mkdirSync(dirname(out), { recursive: true });
|
|
40
|
+
ackPrefix(out); // re-running the tests of an issue: its failure notices were seen
|
|
39
41
|
rmSync(`${out}.done`, { force: true });
|
|
40
42
|
rmSync(`${out}.txt`, { force: true });
|
|
41
43
|
// Title "<ID> verify <repo>" (ID = the prefix's folder, .orquestra/<ID>/) so closing the issue finds the tab.
|
package/dist/commands/wait.js
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { basename } from "node:path";
|
|
7
7
|
import { m } from "../i18n.js";
|
|
8
8
|
import { alive } from "../lock.js";
|
|
9
|
+
import { ackPrefix } from "../notice.js";
|
|
9
10
|
export const USAGE = "orq wait [--any] [--timeout <s>] [--answer] <prefix>... block until child agents finish (exit 1 = one failed)";
|
|
10
11
|
const read = (f) => { try {
|
|
11
12
|
return readFileSync(f, "utf8");
|
|
@@ -61,6 +62,7 @@ export async function wait(argv) {
|
|
|
61
62
|
continue;
|
|
62
63
|
pending.splice(pending.indexOf(p), 1);
|
|
63
64
|
console.log(d.line);
|
|
65
|
+
ackPrefix(p); // the conductor has the result in hand: this issue's failure notice is acknowledged
|
|
64
66
|
if (answer) {
|
|
65
67
|
const t = read(`${p}.out.md`);
|
|
66
68
|
if (t)
|
package/dist/config.js
CHANGED
|
@@ -27,7 +27,7 @@ export const DEFAULTS = {
|
|
|
27
27
|
max_rounds: 2, // fix rounds before stopping and calling the user
|
|
28
28
|
review: "other-vendor", // other-vendor | any
|
|
29
29
|
},
|
|
30
|
-
ui: { lang: "auto", theme: "gunmetal" }, // lang: auto | en | pt · theme: gunmetal | chrome | lcd | terminal
|
|
30
|
+
ui: { lang: "auto", theme: "gunmetal", desktop_notify: true }, // lang: auto | en | pt · theme: gunmetal | chrome | lcd | terminal · desktop_notify: desktop popup for failures (Linux, notify-send)
|
|
31
31
|
// Where quota comes from, per agent: "builtin" | "none" | false | { command = "..." } (see src/quota.ts).
|
|
32
32
|
// "claude" covers every account in claude-accounts.
|
|
33
33
|
quota: { claude: "builtin", codex: "builtin", opencode: "builtin", antigravity: "builtin", copilot: "builtin" },
|