@irythill/orq 0.3.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
@@ -31,8 +31,12 @@ You ── "/issue DEMO-123" ──► conductor (Claude in herdr)
31
31
 
32
32
  - **Every step in a herdr tab.** You watch it live and the tab closes when it's done. Results stay in
33
33
  `.orquestra/<ID>/` (`.out.md`, `.log`, `.done`).
34
- - **Fixed order every round:** implementer ok → tests with no new failure → review. A blocking problem becomes a fix
35
- round in the implementer's own session (configurable maximum).
34
+ - **Fixed order every round:** implementer ok → tests with no new failure → review. Only a `[blocker]` with
35
+ `file:line` and a concrete failure becomes a fix round; the rest goes into the Plane comment (configurable maximum).
36
+ - **Fix rounds converge.** A fix continues the implementer's session on the same model (`orq pick fix --same`, effort
37
+ one level up), fixes only the listed blockers with a test each, and the next review, by the same reviewer, checks
38
+ those blockers and the fix's own diff instead of starting over. The implementer ends with a `## Decisions` section, so
39
+ a deliberate choice doesn't come back as a blocker.
36
40
  - **Nobody picks an agent by gut feeling.** `orq-pick` decides from data: what each agent delivered in the history,
37
41
  how many tokens it spent, how long it took, the current quota, and a rotation so no agent gets all the work.
38
42
  - **The machine stays up.** Each agent runs under its own memory cap; tests wait in a queue.
@@ -42,8 +46,8 @@ You ── "/issue DEMO-123" ──► conductor (Claude in herdr)
42
46
  | Agent | CLI | Possible roles | Notes |
43
47
  |---|---|---|---|
44
48
  | Claude Code | `claude` | conductor, implement, review | several accounts (`claude-accounts`); Opus as conductor |
45
- | Codex | `codex` | implement, review | sandbox without network: the conductor runs `git fetch` |
46
- | OpenCode | `opencode` | implement, review | any provider (e.g. MiniMax) |
49
+ | Codex | `codex` | implement, review | sandbox without network: the conductor runs `git fetch`; quota from its session files (the free plan's 30-day window included) |
50
+ | OpenCode | `opencode` | implement, review | any provider (e.g. MiniMax); on MiniMax, quota from the Token Plan API with your key |
47
51
  | Antigravity | `agy` | review | headless denies commands outside its allowlist: it reviews by reading the attached diff |
48
52
  | GitHub Copilot | `copilot` | second opinion | small allowances on some plans: occasional use |
49
53
 
@@ -105,7 +109,7 @@ your config.
105
109
  | Flag | What it does |
106
110
  |---|---|
107
111
  | `--dry-run` | print what it would do, change nothing |
108
- | `--uninstall` | remove only the links it creates, and only those pointing into this repo (config and history stay) |
112
+ | `--uninstall` | remove only the links it creates, and only those pointing into this repo, and disable the orq systemd user timers (`systemctl --user disable --now`); config and history stay |
109
113
 
110
114
  ## Configuration
111
115
 
@@ -128,8 +132,13 @@ vendor = "openai"
128
132
  roles = ["impl", "fix", "review"]
129
133
  quota = ["codex"] # name in agent-quota's output
130
134
  [agents.codex.models.impl]
131
- simple = "gpt-6-sol@low" # model@effort
132
- heavy = "gpt-6.1-sol@medium"
135
+ simple = "gpt-6-sol@low" # model@effort, per issue level
136
+ medium = "gpt-6-sol@medium"
137
+ heavy = "gpt-6.1-sol@high"
138
+ [agents.codex.models.review]
139
+ simple = "gpt-6-sol@low" # reviews scale with the issue too
140
+ default = "gpt-6-sol@medium"
141
+ heavy = "gpt-6-sol@high"
133
142
 
134
143
  [agents.opencode]
135
144
  vendor = "minimax"
@@ -137,8 +146,25 @@ roles = ["review"] # here it only reviews; for someone els
137
146
  [agents.opencode.models.review]
138
147
  default = "minimax/MiniMax-M3"
139
148
 
149
+ [agents.revtool] # a review tool, not a chat: no prompt, no session
150
+ vendor = "minimax" # the vendor of the model it runs, for the cross-vendor rule
151
+ roles = ["review"]
152
+ run = "revtool --markdown -b {base}" # {base} = orq agent -b; it reads the diff itself
153
+ write = false
154
+ ok_exit = [0, 1] # exit 1 = findings, still a verdict
155
+ no_review = "Nothing to review" # matched on the answer's first line (or a short answer): not a verdict, status=fail
156
+ # filter = "jq -r '...'" # optional: its output → [blocker]/[suggestion] lines
157
+
158
+ [conductor]
159
+ handoff_at = 85 # % of the window → orq handoff moves the conductor to another account
160
+ max_per_account = 2 # an account already holding this many conductors is never a handoff target
161
+ reset_grace_min = 15 # every window over the trigger resetting within this many minutes skips the move
162
+
140
163
  [policy]
141
- quota_limit = 90 # ≥ 90% used → agent out
164
+ quota_week = 95 # ≥ 95% of the week → agent out
165
+ quota_5h = { simple = 90, medium = 85, heavy = 80 } # same for the 5h window, by task level
166
+ pacing_penalty = 0.3 # score penalty when usage at the current pace would pass the pick limit (5h by level, week: quota_week) by the reset (0 = off)
167
+ # [agents.claude] headroom = 25 # points kept for conductors: children leave the account at limit − 25
142
168
  [policy.weights]
143
169
  quality = 0.40 # did it deliver? (impl: approved on the first review)
144
170
  tokens = 0.45 # did it spend little?
@@ -148,7 +174,8 @@ rotation = 0.20 # penalty for having been picked recent
148
174
 
149
175
  ### How `orq-pick` chooses
150
176
  1. **Filters:** only agents with that role, under the quota limit, not blocked (`orq-block`) and, for reviews, from a
151
- vendor other than the implementer's. With several accounts, it takes the least used one.
177
+ vendor other than the implementer's. With several accounts, it prefers one whose pace stays within the limit, then
178
+ the least used.
152
179
  2. **Scores:** `quality × delivery + tokens × economy + speed × quickness − rotation × recent picks`. The numbers come
153
180
  from the history (`orq-stats`); an agent with few runs uses the config's `prior` and gets an exploration bonus.
154
181
  3. **`reserve = true` agents** (e.g. Claude, which shares quota with the conductor) only come in when nothing else fits.
@@ -157,6 +184,7 @@ rotation = 0.20 # penalty for having been picked recent
157
184
  orq-pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
158
185
  orq-stats --days 30 # scoreboard: delivery, approval, time and tokens per agent and model
159
186
  orq-block antigravity --days 3 # out of the picks (e.g. weekly quota gone, no collector for it)
187
+ orq pick fix --same .orquestra/<ID>/impl-<repo>.pick # a fix round: the implementer's agent/model/account, effort +1
160
188
  ```
161
189
 
162
190
  ### Language and theme
@@ -186,8 +214,20 @@ You only come back when it asks for a decision or says it's done.
186
214
 
187
215
  To see everything at once, keep **`orq board`** open in a tab or window (any terminal, any OS; `herdr-board` calls it):
188
216
  test queue, running agents (model, memory against the cap, last action), conductors and their issues, memory, quota,
189
- blocks and the 7-day scoreboard. `--once` prints it once, `--json` for scripts. With
217
+ CI/CD per claimed issue, blocks and the 7-day scoreboard. `--once` prints it once, `--json` for scripts. With
190
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).
225
+ For quota without opening the board, `orq quota --line` fits herdr's tab bar (a `tab_bar_right` command entry,
226
+ [docs/herdr.md](docs/herdr.md#quota-in-the-tab-bar)). `orq ci` shows each claimed issue's PR checks (queued, running,
227
+ passed, failed or none, read from `gh` right then — it is a status check the user asked for, so unlike the board's
228
+ CI column it does not just read the cache); a run turning red or green also pops a herdr notification, and a
229
+ failure pings the owning conductor once per commit. After merges, `orq gc` lists and removes the issue worktrees
230
+ left behind.
191
231
 
192
232
  Several issues at once: one conductor per issue (ideally on different accounts). `herdr-claim` keeps two conductors
193
233
  off the same issue. Details, shortcuts and scenarios: [docs/herdr.md](docs/herdr.md).
@@ -208,6 +248,7 @@ off the same issue. Details, shortcuts and scenarios: [docs/herdr.md](docs/herdr
208
248
  |---|---|
209
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) |
210
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`) |
211
252
  | `orq wait <prefix>...` | Blocks until those agents finish; prints `status=` and the answer file (any conductor, any terminal) |
212
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 |
213
254
  | `orq board` (`herdr-board`) | Live board: queue, agents, conductors, quota, blocks, scoreboard |
@@ -215,7 +256,10 @@ off the same issue. Details, shortcuts and scenarios: [docs/herdr.md](docs/herdr
215
256
  | `herdr-claim` | Owner of each issue among conductors |
216
257
  | `herdr-deps` | Links the worktree's dependencies to the main repo's (no reinstall) |
217
258
  | `orq baseline ...` (`herdr-baseline`) | Manual snapshot of the tests failing on the base (outside the flow) |
218
- | `orq gc [-w <workspace>] [--yes]` | Lists issue worktrees that are safe to remove; `--yes` removes them |
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` |
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 |
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 ⚠`) |
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 |
219
263
  | `orq setup` | First configuration and install: environment, skills for Claude, config, conductor workspace |
220
264
  | `orq completion bash\|zsh\|fish\|powershell` | Prints the shell completion script (see Install) |
221
265
  | `orq <config\|set\|lang\|block\|quota>` | The cross-platform CLI (TypeScript); the `orq-*` and `agent-quota` names below call it |
package/README.pt-BR.md CHANGED
@@ -31,8 +31,13 @@ You ── "/issue DEMO-123" ──► conductor (Claude in herdr)
31
31
 
32
32
  - **Cada etapa numa aba do herdr.** Você acompanha ao vivo e a aba fecha quando termina. Os resultados ficam em
33
33
  `.orquestra/<ID>/` (`.out.md`, `.log`, `.done`).
34
- - **Ordem fixa em toda rodada:** implementador ok → testes sem falha nova → review. Um problema bloqueante vira uma
35
- rodada de correção na própria sessão do implementador (com máximo configurável).
34
+ - **Ordem fixa em toda rodada:** implementador ok → testes sem falha nova → review. Só um `[blocker]` com
35
+ `arquivo:linha` e uma falha concreta vira rodada de correção; o resto vai para o comentário no Plane (com máximo
36
+ configurável).
37
+ - **As rodadas convergem.** O fix continua a sessão do implementador no mesmo modelo (`orq pick fix --same`, esforço
38
+ um nível acima), corrige só os blockers listados, com um teste para cada, e o review seguinte, do mesmo reviewer,
39
+ confere esses blockers e o diff do próprio fix em vez de recomeçar. O implementador termina com uma seção
40
+ `## Decisions`, para que uma escolha proposital não volte como blocker.
36
41
  - **Ninguém escolhe agente no feeling.** O `orq-pick` decide com base em dados: o que cada agente entregou no
37
42
  histórico, quantos tokens gastou, quanto tempo levou, a quota atual e um rodízio para nenhum agente ficar com todo o
38
43
  trabalho.
@@ -43,8 +48,8 @@ You ── "/issue DEMO-123" ──► conductor (Claude in herdr)
43
48
  | Agente | CLI | Papéis possíveis | Observações |
44
49
  |---|---|---|---|
45
50
  | Claude Code | `claude` | conductor, implement, review | várias contas (`claude-accounts`); Opus como conductor |
46
- | Codex | `codex` | implement, review | sandbox sem rede: o conductor roda o `git fetch` |
47
- | OpenCode | `opencode` | implement, review | qualquer provider (ex.: MiniMax) |
51
+ | Codex | `codex` | implement, review | sandbox sem rede: o conductor roda o `git fetch`; cota lida dos arquivos de sessão (inclusive a janela de 30 dias do plano free) |
52
+ | OpenCode | `opencode` | implement, review | qualquer provider (ex.: MiniMax); na MiniMax, cota pela API do Token Plan com a sua chave |
48
53
  | Antigravity | `agy` | review | em modo headless nega comandos fora da allowlist: faz a review lendo o diff anexado |
49
54
  | GitHub Copilot | `copilot` | segunda opinião | cota pequena em alguns planos: uso ocasional |
50
55
 
@@ -130,8 +135,13 @@ vendor = "openai"
130
135
  roles = ["impl", "fix", "review"]
131
136
  quota = ["codex"] # name in agent-quota's output
132
137
  [agents.codex.models.impl]
133
- simple = "gpt-6-sol@low" # model@effort
134
- heavy = "gpt-6.1-sol@medium"
138
+ simple = "gpt-6-sol@low" # model@effort, per issue level
139
+ medium = "gpt-6-sol@medium"
140
+ heavy = "gpt-6.1-sol@high"
141
+ [agents.codex.models.review]
142
+ simple = "gpt-6-sol@low" # reviews scale with the issue too
143
+ default = "gpt-6-sol@medium"
144
+ heavy = "gpt-6-sol@high"
135
145
 
136
146
  [agents.opencode]
137
147
  vendor = "minimax"
@@ -139,8 +149,15 @@ roles = ["review"] # here it only reviews; for someone els
139
149
  [agents.opencode.models.review]
140
150
  default = "minimax/MiniMax-M3"
141
151
 
152
+ [conductor]
153
+ handoff_at = 85 # % da janela → o orq handoff move o conductor para outra conta
154
+ max_per_account = 2 # conta que já tem esse tanto de conductors nunca é alvo do handoff
155
+ reset_grace_min = 15 # toda janela acima do gatilho renova dentro desses minutos → pula a mudança
156
+
142
157
  [policy]
143
- quota_limit = 90 # ≥ 90% used → agent out
158
+ quota_limit = 90 # caps the 5h pick limit; also sets ! in `orq quota --line`
159
+ pacing_penalty = 0.3 # score penalty when usage at the current pace would pass the pick limit (5h by level, week: quota_week) by the reset (0 = off)
160
+ # [agents.claude] headroom = 25 # points kept for conductors: children leave the account at limit − 25
144
161
  [policy.weights]
145
162
  quality = 0.40 # did it deliver? (impl: approved on the first review)
146
163
  tokens = 0.45 # did it spend little?
@@ -150,7 +167,8 @@ rotation = 0.20 # penalty for having been picked recent
150
167
 
151
168
  ### Como o `orq-pick` escolhe
152
169
  1. **Filtros:** só agentes com aquele papel, abaixo do limite de quota, não bloqueados (`orq-block`) e, para reviews,
153
- de um vendor diferente do implementador. Com várias contas, pega a menos usada.
170
+ de um vendor diferente do implementador. Com várias contas, prefere uma cujo ritmo fique dentro do limite e, como
171
+ desempate, a menos usada.
154
172
  2. **Pontuação:** `quality × delivery + tokens × economy + speed × quickness − rotation × recent picks`. Os números vêm
155
173
  do histórico (`orq-stats`); um agente com poucas execuções usa o `prior` da config e ganha um bônus de exploração.
156
174
  3. **Agentes com `reserve = true`** (ex.: Claude, que divide quota com o conductor) só entram quando nada mais serve.
@@ -159,6 +177,7 @@ rotation = 0.20 # penalty for having been picked recent
159
177
  orq-pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
160
178
  orq-stats --days 30 # scoreboard: delivery, approval, time and tokens per agent and model
161
179
  orq-block antigravity --days 3 # out of the picks (e.g. weekly quota gone, no collector for it)
180
+ orq pick fix --same .orquestra/<ID>/impl-<repo>.pick # a fix round: the implementer's agent/model/account, effort +1
162
181
  ```
163
182
 
164
183
  ### Idioma e tema
@@ -187,8 +206,21 @@ quando ele pede uma decisão ou avisa que terminou.
187
206
 
188
207
  Para ver tudo de uma vez, deixe o **`orq board`** aberto numa aba ou janela (qualquer terminal, qualquer SO; o
189
208
  `herdr-board` o chama): fila de testes, agentes rodando (modelo, memória contra o limite, última ação), conductors e
190
- suas issues, memória, quota, bloqueios e o placar de 7 dias. `--once` imprime uma vez, `--json` para scripts. Com
191
- `ui.toast.delivery = "system"` na config do herdr, cada etapa concluída também gera uma notificação nativa do sistema.
209
+ suas issues, memória, quota, CI/CD de cada issue reivindicada, bloqueios e o placar de 7 dias. `--once` imprime uma
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. 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
218
+ herdr (uma entrada `command` no `tab_bar_right`, [docs/pt-BR/herdr.md](docs/pt-BR/herdr.md#cota-na-barra-de-abas)).
219
+ O `orq ci` mostra os checks do PR de cada issue reivindicada (na fila, rodando, passou, falhou ou nenhum, lido via
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
221
+ cache); quando um deles vira verde ou vermelho sai uma notificação do herdr, e uma falha também avisa o conductor
222
+ dono num aviso só por commit. Depois dos merges, o `orq gc` lista e remove as worktrees de issues que ficaram para
223
+ trás.
192
224
 
193
225
  Várias issues ao mesmo tempo: um conductor por issue (de preferência em contas diferentes). O `herdr-claim` impede que
194
226
  dois conductors peguem a mesma issue. Detalhes, atalhos e cenários: [docs/pt-BR/herdr.md](docs/pt-BR/herdr.md).
@@ -209,6 +241,7 @@ dois conductors peguem a mesma issue. Detalhes, atalhos e cenários: [docs/pt-BR
209
241
  |---|---|
210
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) |
211
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`) |
212
245
  | `orq wait <prefixo>...` | Bloqueia até esses agentes terminarem; imprime `status=` e o arquivo da resposta (qualquer conductor, qualquer terminal) |
213
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 |
214
247
  | `orq board` (`herdr-board`) | Painel ao vivo: fila, agentes, conductors, cota, bloqueios, placar |
@@ -216,7 +249,10 @@ dois conductors peguem a mesma issue. Detalhes, atalhos e cenários: [docs/pt-BR
216
249
  | `herdr-claim` | Dono de cada issue entre os conductors |
217
250
  | `herdr-deps` | Liga as dependências do worktree às do repo principal (sem reinstalar) |
218
251
  | `orq baseline ...` (`herdr-baseline`) | Snapshot manual dos testes que falham na base (fora do fluxo) |
219
- | `orq gc [-w <workspace>] [--yes]` | Lista worktrees de issues seguras para remover; `--yes` as remove |
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 |
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 |
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 ⚠`) |
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 |
220
256
  | `orq setup` | Primeira configuração e instalação: ambiente, skills do Claude, config, workspace do conductor |
221
257
  | `orq completion bash\|zsh\|fish\|powershell` | Imprime o script de autocompletar do shell (veja Instalação) |
222
258
  | `orq <config\|set\|lang\|block\|quota>` | O CLI multiplataforma (TypeScript); os nomes `orq-*` e `agent-quota` abaixo chamam ele |
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 ADDED
@@ -0,0 +1,241 @@
1
+ // CI/CD signal per claimed issue, for `orq ci` and the board's CI column: for each issue with a live owner
2
+ // (.orquestra/<ID>/owner, as orq claim keeps it), the branch its worktree carries (as orq gc matches it:
3
+ // <repo>-plane-<id-lower> under .worktrees) and that branch's PR checks via `gh pr view`, aggregated into
4
+ // queued | running | passed | failed | none. Cached in the state dir (board/ci.json), like quota — orq board
5
+ // never blocks on `gh`; a stale cache starts a background collection the same way quota does. `orq ci` itself is
6
+ // the exception: it is the user explicitly asking for CI status right now, so it runs collectCi() in the
7
+ // foreground (network and all) and prints the result — only `orq board` reads the cache.
8
+ // On a transition to passed or failed: a herdr desktop notification; on failed, one line to the owning
9
+ // conductor via testrun.ts's notify(), tracked by `${state}:${head SHA}` in the cache so a repeat collection of
10
+ // the same commit never repeats it, but a new commit that is still red does.
11
+ import { spawnSync } from "node:child_process";
12
+ import { createHash } from "node:crypto";
13
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { aliveOwner } from "./commands/claim.js";
16
+ import { writeAtomic } from "./fsutil.js";
17
+ import { m } from "./i18n.js";
18
+ import { alive } from "./lock.js";
19
+ import { stateDir } from "./paths.js";
20
+ import { notify } from "./testrun.js";
21
+ const git = (cwd, ...args) => spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
22
+ const output = (cwd, ...args) => { const r = git(cwd, ...args); return r.status === 0 ? r.stdout.trim() : undefined; };
23
+ // ORQ_GH may be a .js file (tests: a fake gh that also runs on Windows), as orq gc already does it.
24
+ const gh = (cwd, args) => {
25
+ const bin = process.env.ORQ_GH || "gh";
26
+ return bin.endsWith(".js") ? spawnSync(process.execPath, [bin, ...args], { cwd, encoding: "utf8", timeout: 15_000 })
27
+ : spawnSync(bin, args, { cwd, encoding: "utf8", timeout: 15_000 });
28
+ };
29
+ export const cacheFile = () => join(stateDir(), "board", "ci.json");
30
+ export function loadCache() {
31
+ try {
32
+ return JSON.parse(readFileSync(cacheFile(), "utf8"));
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ }
38
+ export function saveCache(entries) { writeAtomic(cacheFile(), JSON.stringify(entries, null, 1)); }
39
+ // A per-workspace "collected at" marker, separate from ci.json: collectCi() writes no cache entry for a workspace
40
+ // with no claimed issue that has a matching worktree, so ci.json alone can't tell "never collected" apart from
41
+ // "collected, found nothing" — the board would otherwise relaunch the collector every redraw forever in the
42
+ // latter case. Keyed by a hash of the workspace path since it doubles as a file name.
43
+ const wsKey = (ws) => createHash("sha1").update(ws).digest("hex").slice(0, 16);
44
+ const collectedFile = (ws) => join(stateDir(), "board", "ci", `${wsKey(ws)}.at`);
45
+ export function markCollected(ws, at) { writeAtomic(collectedFile(ws), String(at)); }
46
+ /** When `ws` was last collected (collectCi ran for it, even if it found nothing), else undefined if never. */
47
+ export function collectedAt(ws) {
48
+ try {
49
+ const n = Number(readFileSync(collectedFile(ws), "utf8"));
50
+ return Number.isFinite(n) ? n : undefined;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /** Claimed issues of a workspace with a live owner (as `orq claim -l` lists them), issue id → owner pane + kind
57
+ * ("herdr" for a pane herdr-notify can reach, else a plain terminal conductor — see handoff.ts's same check). */
58
+ export function claimedIssues(ws) {
59
+ const O = join(ws, ".orquestra"), out = [];
60
+ for (const d of existsSync(O) ? readdirSync(O).filter((x) => !x.startsWith(".")) : []) {
61
+ const f = join(O, d, "owner");
62
+ if (!existsSync(f))
63
+ continue;
64
+ const t = readFileSync(f, "utf8").trim();
65
+ if (aliveOwner(t))
66
+ out.push({ id: d, owner: /pane=(\S+)/.exec(t)?.[1] ?? "", kind: /kind=(\S+)/.exec(t)?.[1] ?? "herdr" });
67
+ }
68
+ return out;
69
+ }
70
+ /** Every worktree branch in the workspace's repos, one `git worktree list` per repo — called once per workspace
71
+ * per collection (collectCi memoizes it across issues) rather than once per claimed issue. */
72
+ export function listWorktrees(ws) {
73
+ if (!existsSync(join(ws, ".worktrees")))
74
+ return [];
75
+ const out = [];
76
+ for (const repoName of readdirSync(ws, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)) {
77
+ const repo = join(ws, repoName);
78
+ if (!existsSync(join(repo, ".git")))
79
+ continue;
80
+ const listings = output(repo, "worktree", "list", "--porcelain");
81
+ if (!listings)
82
+ continue;
83
+ for (const block of listings.split(/\n\n/)) {
84
+ const rec = Object.fromEntries(block.split("\n").map((line) => {
85
+ const i = line.indexOf(" ");
86
+ return [i < 0 ? line : line.slice(0, i), i < 0 ? "" : line.slice(i + 1)];
87
+ }));
88
+ const branch = rec.branch?.replace(/^refs\/heads\//, "") ?? "";
89
+ if (branch)
90
+ out.push({ repo: repoName, branch });
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+ /** The repo + branch an issue's worktree carries, the same match `orq gc` uses: a worktree branch ending in
96
+ * "plane-<id, lowercase>" under <ws>/.worktrees. An issue can have one per repo (several repos in play).
97
+ * `all` lets a caller looping over several issues pass its own listWorktrees(ws) once instead of one per issue. */
98
+ export function issueBranches(ws, issue, all = listWorktrees(ws)) {
99
+ const suffix = `plane-${issue.toLowerCase()}`;
100
+ return all.filter((b) => b.branch.toLowerCase().endsWith(suffix));
101
+ }
102
+ // GitHub's statusCheckRollup mixes CheckRun (status/conclusion, the GitHub Actions / Checks API shape) with the
103
+ // legacy commit-status shape (StatusContext: just `state`, no status/conclusion at all) for repos whose CI still
104
+ // posts plain commit statuses (Jenkins, CircleCI, old GitLab integrations). Left unmapped, every StatusContext
105
+ // item reads as still-running forever: conclusion is undefined (never "failed"), status is undefined (never
106
+ // "queued"), and `status !== "COMPLETED"` is always true.
107
+ const STATUS_STATE = {
108
+ SUCCESS: { status: "COMPLETED", conclusion: "SUCCESS" }, FAILURE: { status: "COMPLETED", conclusion: "FAILURE" },
109
+ ERROR: { status: "COMPLETED", conclusion: "ERROR" }, PENDING: { status: "PENDING" },
110
+ };
111
+ const normalize = (c) => c.__typename === "StatusContext" ? (STATUS_STATE[c.state ?? ""] ?? { status: c.status, conclusion: c.conclusion }) : c;
112
+ /** The worst signal across a PR's checks: any failure beats a pending one, which beats still running, which beats
113
+ * all green; no checks at all (none configured, or not registered yet) reads as "none", not a false "passed". */
114
+ export function aggregateChecks(rollup) {
115
+ if (!rollup.length)
116
+ return "none";
117
+ const checks = rollup.map(normalize);
118
+ if (checks.some((c) => ["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "CANCELLED", "ERROR"].includes(c.conclusion ?? "")))
119
+ return "failed";
120
+ if (checks.some((c) => c.status === "QUEUED" || c.status === "PENDING" || c.status === "WAITING" || c.status === "REQUESTED"))
121
+ return "queued";
122
+ if (checks.some((c) => c.status !== "COMPLETED"))
123
+ return "running";
124
+ return "passed";
125
+ }
126
+ /** `gh pr view --json number,headRefOid,statusCheckRollup`'s stdout → the PR's number, head SHA and CI state;
127
+ * no PR (empty/garbage JSON, or no "number") reads as "none". Pure: no network, so it needs no fake gh to test. */
128
+ export function parsePrStatus(json) {
129
+ try {
130
+ const j = JSON.parse(json);
131
+ if (!j.number)
132
+ return { state: "none" };
133
+ return { pr: j.number, sha: j.headRefOid, state: aggregateChecks(j.statusCheckRollup ?? []) };
134
+ }
135
+ catch {
136
+ return { state: "none" };
137
+ }
138
+ }
139
+ /** The branch's PR and CI state; no `gh`, no PR, or a `gh` failure all read as "none" — never an error. */
140
+ function prStatus(repo, branch) {
141
+ const r = gh(repo, ["pr", "view", branch, "--json", "number,headRefOid,statusCheckRollup"]);
142
+ return r.status === 0 ? parsePrStatus(r.stdout) : { state: "none" };
143
+ }
144
+ // `orq ci -w ws2` (foreground) and the board's detached `orq board --collect-ci` for ws1 can run at the same
145
+ // time; both do a read-modify-write of the single shared ci.json, so without a lock the last one to save erases
146
+ // the other's entries for its own workspace (and can drop a `notified` mark, causing a repeat notification).
147
+ // A short-lived exclusive lock file around just the read-merge-write (never around the `gh`/network calls above
148
+ // it) keeps that atomic; a stale lock (holder process died mid-write) is detected via lock.ts's `alive()` and
149
+ // cleared instead of being waited on forever.
150
+ function withCacheLock(fn) {
151
+ const dir = join(stateDir(), "board"), lf = join(dir, "ci.lock");
152
+ mkdirSync(dir, { recursive: true });
153
+ for (;;) {
154
+ try {
155
+ writeFileSync(lf, String(process.pid), { flag: "wx" });
156
+ break;
157
+ }
158
+ catch (e) {
159
+ if (e.code !== "EEXIST")
160
+ throw e;
161
+ try {
162
+ if (!alive(Number(readFileSync(lf, "utf8").trim() || "0"))) {
163
+ rmSync(lf, { force: true });
164
+ continue;
165
+ }
166
+ }
167
+ catch {
168
+ continue;
169
+ } // lock file vanished mid-check: retry the exclusive create
170
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
171
+ }
172
+ }
173
+ try {
174
+ return fn();
175
+ }
176
+ finally {
177
+ rmSync(lf, { force: true });
178
+ }
179
+ }
180
+ function notifyDesktop(issue, repo, state) {
181
+ const title = state === "passed" ? m("CI passed", "CI passou") : m("CI failed", "CI falhou");
182
+ spawnSync("herdr", ["notification", "show", title, "--body", `${issue} ${repo}`], { stdio: "ignore" });
183
+ }
184
+ /**
185
+ * This workspace's current CI entries, merged into the cache (other workspaces' entries untouched). On a genuine
186
+ * transition (a known previous sighting, now a different head commit) to passed or failed: a herdr desktop
187
+ * notification, and on failed one line to the owning conductor — gated by `${state}:${sha}` in `notified`, so a
188
+ * repeat collection of the same head commit never notifies twice, but a new commit that is still red does (the
189
+ * mark carries the sha, so it is never redundant with the `prev` gate that skips a first sighting).
190
+ * The `prev` read, the notify decision and the `notified` mark all happen inside `withCacheLock`, alongside the
191
+ * save — otherwise a concurrent collector of the same workspace (`orq ci` in the foreground racing the board's
192
+ * detached collector) reads the same stale `prev`, both decide to notify and whichever saves last overwrites the
193
+ * other's freshly written `notified` mark, causing a third notification on the next collection.
194
+ * The notifications themselves fire after the lock is released: they must never hold up another collector's
195
+ * read-modify-write while they run.
196
+ * A non-herdr owner (a plain terminal conductor, same check as handoff.ts) has no pane herdr-notify can reach, so
197
+ * the line goes through `hooks.notice` (console.log by default) instead — never silently dropped, and never a
198
+ * crash from `herdr agent get` failing. The owner notify is always detached (background=true): collectCi() must
199
+ * never block on a conductor stuck in a dialog.
200
+ * `hooks` lets tests swap the notifiers for ones with no side effects, lets `orq ci --json` keep its stdout to
201
+ * just the JSON, and lets the board's detached collector (stdio: "ignore") persist `notice` to a file instead of
202
+ * the default console.log, which would otherwise vanish.
203
+ */
204
+ export function collectCi(ws, hooks = {}) {
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)), notice = hooks.notice ?? ((l) => console.log(l));
207
+ const now = Date.now(), worktrees = listWorktrees(ws);
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
+ const toNotify = [];
210
+ const mine = withCacheLock(() => {
211
+ const cache = loadCache(), mine = [];
212
+ for (const { id, owner, kind, repo, branch, pr, sha, state } of found) {
213
+ const prev = cache.find((e) => e.ws === ws && e.issue === id && e.repo === repo && e.branch === branch);
214
+ let notified = prev?.notified;
215
+ const mark = sha ? `${state}:${sha}` : undefined;
216
+ if (prev && (state === "passed" || state === "failed") && mark && notified !== mark) {
217
+ toNotify.push({ owner, kind, id, repo, state, sha: sha });
218
+ notified = mark;
219
+ }
220
+ if (!prev && mark)
221
+ notified = mark; // first sighting: known, not news
222
+ mine.push({ ws, issue: id, repo, branch, ...(pr !== undefined ? { pr } : {}), ...(sha ? { sha } : {}), state, ...(notified ? { notified } : {}), at: now });
223
+ }
224
+ saveCache([...cache.filter((e) => e.ws !== ws), ...mine]);
225
+ return mine;
226
+ });
227
+ for (const { owner, kind, id, repo, state, sha } of toNotify) {
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);
231
+ if (state === "failed" && owner) {
232
+ const line = `[ORQ CI] ${id} ${repo} failed (${sha.slice(0, 7)})`;
233
+ if (kind === "herdr")
234
+ toOwner(owner, line);
235
+ else
236
+ notice(line);
237
+ }
238
+ }
239
+ markCollected(ws, now);
240
+ return mine;
241
+ }
package/dist/cli.js CHANGED
@@ -9,22 +9,26 @@ import { board, USAGE as BOARD } from "./commands/board.js";
9
9
  import { baseline, USAGE as BASELINE } from "./commands/baseline.js";
10
10
  import { block, USAGE as BLOCK } from "./commands/block.js";
11
11
  import { claim, USAGE as CLAIM } from "./commands/claim.js";
12
+ import { ci, USAGE as CI } from "./commands/ci.js";
12
13
  import { completion, USAGE as COMPLETION } from "./commands/completion.js";
13
14
  import { config, USAGE as CONFIG } from "./commands/config.js";
14
15
  import { deps, USAGE as DEPS } from "./commands/deps.js";
15
16
  import { gc, USAGE as GC } from "./commands/gc.js";
17
+ import { handoff, USAGE as HANDOFF } from "./commands/handoff.js";
16
18
  import { instructions, USAGE as INSTRUCTIONS } from "./commands/instructions.js";
17
19
  import { lang, USAGE as LANG } from "./commands/lang.js";
18
20
  import { lock, USAGE as LOCK } from "./commands/lock.js";
21
+ import { noticeCmd, USAGE as NOTICE } from "./notice.js";
19
22
  import { pick, USAGE as PICK } from "./commands/pick.js";
20
23
  import { quota, USAGE as QUOTA } from "./commands/quota.js";
24
+ import { rounds, USAGE as ROUNDS } from "./commands/rounds.js";
21
25
  import { run, USAGE as RUN } from "./commands/run.js";
22
26
  import { set, USAGE as SET } from "./commands/set.js";
23
27
  import { setup, USAGE as SETUP } from "./commands/setup.js";
24
28
  import { stats, USAGE as STATS } from "./commands/stats.js";
25
29
  import { verify, USAGE as VERIFY } from "./commands/verify.js";
26
30
  import { wait, USAGE as WAIT } from "./commands/wait.js";
27
- const COMMANDS = { config, set, board, lang, block, quota, pick, stats, setup, lock, claim, deps, gc, 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,
28
32
  completion: (a) => completion(a, Object.keys(COMMANDS)) };
29
33
  const usage = () => `orq <command> [args]
30
34
 
@@ -35,17 +39,21 @@ ${LANG}
35
39
  ${QUOTA}
36
40
  ${PICK}
37
41
  ${STATS}
42
+ ${ROUNDS}
38
43
  ${BOARD}
39
44
  ${LOCK}
40
45
  ${RUN}
41
46
  ${AGENT}
42
47
  ${WAIT}
48
+ ${HANDOFF}
43
49
  ${VERIFY}
44
50
  ${BASELINE}
45
51
  ${INSTRUCTIONS}
52
+ ${NOTICE}
46
53
  ${CLAIM}
47
54
  ${DEPS}
48
55
  ${GC}
56
+ ${CI}
49
57
  ${COMPLETION}
50
58
  ${BLOCK.split("\n")[0]}
51
59
  (orq block --help ${m("for more", "para mais")})`;