@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 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.corvus] # a review tool, not a chat: no prompt, no session
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 = "corvus --markdown -b {base}" # {base} = orq agent -b; it reads the diff itself
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 = "Nada para revisar" # matched on the answer's first line (or a short answer): not a verdict, status=fail
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 without an 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 ⚠`) |
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. Para ver a cota sem abrir o board, o `orq quota --line` cabe na barra de abas do
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, true)), notice = hooks.notice ?? ((l) => console.log(l));
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
- desktop(id, repo, state);
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}
@@ -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 herdr-notify.
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 { agentDef, ALIASES, expandString, expandTemplate, FORMATS } from "../agents.js";
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 || "codex", g = "", t = "", d = "", o = "", model = "", effort = "", acc = "", resume = "", base = "";
39
- let ro = false, tui = false, force = false, prompt;
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 = true;
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 = Boolean(def.throwaway && ro && !resume);
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`, true);
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`, true);
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";
@@ -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
- export const USAGE = `orq block <agent|account> [--until YYYY-MM-DD[THH:MM] | --days N] [--reason "text"] default: 7 days
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
- export function block(argv) {
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);
@@ -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 was started with, from its job file (-m / --model in the argv). */
39
- function modelOf(prefix) {
40
- if (!models.has(prefix)) {
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 a = (JSON.parse(read(`${prefix}.job.json`)).argv ?? []);
44
- const i = a.findIndex((x) => x === "-m" || x === "--model");
45
- v = i >= 0 ? a[i + 1] : a.find((x) => x.startsWith("--model="))?.slice(8);
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(prefix, v?.split("/").pop());
51
+ models.set(unit, v);
49
52
  }
50
- return models.get(prefix);
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
@@ -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.
@@ -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
- export const USAGE = "orq rounds <ID> [-w <workspace>] [--json] rounds vs max_rounds, time per round, repeated findings, alert level";
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
  }
@@ -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++) {
@@ -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.
@@ -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" },