@irythill/orq 0.6.0 → 0.7.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
@@ -48,9 +48,9 @@ You ── "/issue DEMO-123" ──► conductor (Claude, in any terminal)
48
48
  | Agent | CLI | Possible roles | Notes |
49
49
  |---|---|---|---|
50
50
  | Claude Code | `claude` | conductor, implement, review | several accounts (`claude-accounts`); Opus as conductor |
51
- | 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) |
51
+ | Codex | `codex` | implement, review | sandbox without network: orq runs `git fetch` before a write run (`network = false`); quota from its session files (the free plan's 30-day window included) |
52
52
  | OpenCode | `opencode` | implement, review | any provider (e.g. MiniMax); on MiniMax, quota from the Token Plan API with your key |
53
- | Antigravity | `agy` | review | headless denies commands outside its allowlist: it reviews by reading the attached diff |
53
+ | Antigravity | `agy` | review (implement with an allowlist) | headless denies commands outside its allowlist: it reviews by reading the attached diff; it can't commit, so orq commits its write runs (`commit = false`) |
54
54
  | GitHub Copilot | `copilot` | second opinion | small allowances on some plans: occasional use |
55
55
 
56
56
  You declare only the ones you have, and the roles each may take.
@@ -58,7 +58,7 @@ You declare only the ones you have, and the roles each may take.
58
58
  ## Install
59
59
 
60
60
  Linux, macOS or Windows (native, not only WSL), in any shell — bash, zsh, fish, PowerShell, cmd.
61
- **Requirements:** Node 20+ with `npm`, `git`, and at least one agent from the table above, logged in. Optional:
61
+ **Requirements:** Node 22+ with `npm`, `git`, and at least one agent from the table above, logged in. Optional:
62
62
  [herdr](https://herdr.dev) or tmux, so each child gets its own tab or window (otherwise children run in the
63
63
  background and `orq board` shows them).
64
64
 
@@ -100,7 +100,7 @@ git clone https://github.com/irythill/orq ~/Code/orq && cd ~/Code/orq
100
100
  ./install.sh # links in ~/.local/bin, herdr service, skills for Claude
101
101
  ```
102
102
 
103
- Needs `git`, `jq`, Node 20+ with `npm`, `python3` (3.11+ for TOML config; older → JSON config or
103
+ Needs `git`, `jq`, Node 22+ with `npm`, `python3` (3.11+ for TOML config; older → JSON config or
104
104
  `pip install --user tomli`) and user `systemd` (WSL2: Ubuntu 22.04+ with systemd enabled). `install.sh` checks the
105
105
  requirements (table of what's found and missing), builds the `orq` CLI (`src/` → `dist/`), then **symlinks** (never
106
106
  copies) the scripts into `~/.local/bin`, `setup/herdr.service` into `~/.config/systemd/user/` (and enables it), and
@@ -120,10 +120,10 @@ Two layers, to keep **your way of working** apart from **the team's project**:
120
120
  | File | Whose | What it holds |
121
121
  |---|---|---|
122
122
  | `~/.config/orq/config.toml` | yours, stays on your machine | agents, accounts, models per role and level, pick weights, memory limits |
123
- | `<workspace>/.orq/workspace.toml` 🚧 | the team's, versioned | Plane (projects, issue standard), repos, base branch, verification, tests, dependencies |
123
+ | `<workspace>/.orq/workspace.toml` | the team's, versioned | Plane (projects, issue standard), repos, base branch, verification, tests, dependencies |
124
124
 
125
- The more specific one wins, key by key. JSON works too (`config.json`). To see the merged result: `orq-config`
126
- (everything), `orq-config get policy.weights`, `orq-config files`.
125
+ The more specific one wins, key by key. JSON works too (`config.json`). To see the merged result: `orq config`
126
+ (everything), `orq config get policy.weights`, `orq config files`.
127
127
 
128
128
  ### Example: personal config
129
129
  Complete, commented example: [`examples/irythill/config.toml`](examples/irythill/config.toml). The essentials:
@@ -155,6 +155,9 @@ run = "revtool --markdown -b {base}" # {base} = orq agent -b; it reads the
155
155
  write = false
156
156
  ok_exit = [0, 1] # exit 1 = findings, still a verdict
157
157
  no_review = "Nothing to review" # matched on the answer's first line (or a short answer): not a verdict, status=fail
158
+ # commit = false # can't git commit: orq commits a write run's changes
159
+ # network = false # no network: orq fetches origin before a write run
160
+ # base_commit = false # can't review `-b <sha>` (default for a tool): pick skips it on a round delta, agent falls back to the branch base
158
161
  # filter = "jq -r '...'" # optional: its output → [blocker]/[suggestion] lines
159
162
 
160
163
  [conductor]
@@ -162,6 +165,9 @@ handoff_at = 85 # % of the window → orq handoff moves
162
165
  max_per_account = 2 # an account already holding this many conductors is never a handoff target
163
166
  reset_grace_min = 15 # every window over the trigger resetting within this many minutes skips the move
164
167
 
168
+ [git]
169
+ issue_pattern = "(?<![a-z0-9])(?<issue>[a-z][a-z0-9]*-\\d+)(?![a-z0-9])" # Branch/worktree name → issue ID, for `orq gc` and `orq ci` — default shown; regex, named group `issue` (else group 1)
170
+
165
171
  [policy]
166
172
  quota_week = 95 # ≥ 95% of the week → agent out
167
173
  quota_5h = { simple = 90, medium = 85, heavy = 80 } # same for the 5h window, by task level
@@ -174,18 +180,19 @@ speed = 0.15
174
180
  rotation = 0.20 # penalty for having been picked recently
175
181
  ```
176
182
 
177
- ### How `orq-pick` chooses
178
- 1. **Filters:** only agents with that role, under the quota limit, not blocked (`orq-block`) and, for reviews, from a
179
- vendor other than the implementer's. With several accounts, it prefers one whose pace stays within the limit, then
183
+ ### How `orq pick` chooses
184
+ 1. **Filters:** only agents with that role whose CLI is on the PATH, under the quota limit, not blocked (`orq block`)
185
+ and, for reviews, from a vendor other than the implementer's (and, for a round's delta, able to review a commit:
186
+ `base_commit`). With several accounts, it prefers one whose pace stays within the limit, then
180
187
  the least used.
181
188
  2. **Scores:** `quality × delivery + tokens × economy + speed × quickness − rotation × recent picks`. The numbers come
182
- from the history (`orq-stats`); an agent with few runs uses the config's `prior` and gets an exploration bonus.
189
+ from the history (`orq stats`); an agent with few runs uses the config's `prior` and gets an exploration bonus.
183
190
  3. **`reserve = true` agents** (e.g. Claude, which shares quota with the conductor) only come in when nothing else fits.
184
191
 
185
192
  ```bash
186
- orq-pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
187
- orq-stats --days 30 # scoreboard: delivery, approval, time and tokens per agent and model
188
- orq-block antigravity --days 3 # out of the picks (e.g. weekly quota gone, no collector for it)
193
+ orq pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
194
+ orq stats --days 30 # scoreboard per agent and model: delivery, approval, time (to the first turn), tokens
195
+ orq block antigravity --days 3 # out of the picks (e.g. weekly quota gone, no collector for it)
189
196
  orq pick fix --same .orquestra/<ID>/impl-<repo>.pick # a fix round: the implementer's agent/model/account, effort +1
190
197
  ```
191
198
 
@@ -193,7 +200,7 @@ orq pick fix --same .orquestra/<ID>/impl-<repo>.pick # a fix round: the implem
193
200
  orq speaks English and Portuguese: CLI messages, `orq board`, notices and `install.sh`. It follows
194
201
  `[ui] lang` in your config (`"auto"` = system locale, `"en"`, `"pt"`), or the `ORQ_LANG` environment variable.
195
202
  Switch it any time:
196
- - `orq-lang pt` / `orq-lang en` / `orq-lang toggle` (`orq-lang` alone shows the current one);
203
+ - `orq lang pt` / `orq lang en` / `orq lang toggle` (`orq lang` alone shows the current one);
197
204
  - `l` inside `orq board`;
198
205
  - in herdr, a key for it: see [With herdr](#with-herdr).
199
206
 
@@ -210,7 +217,9 @@ Open the conductor in any terminal, in your workspace folder (the one `orq setup
210
217
  ```bash
211
218
  cd ~/Code/<workspace> && orq conductor [account] [model] # any terminal: loads the conductor instructions, the workspace CLAUDE.md and the /issue skill
212
219
  ```
213
- With several conductors, give each a fixed name (`export ORQ_CONDUCTOR_ID=<name>`) so `orq claim` can tell them apart.
220
+ With several conductors, give each a fixed name (`export ORQ_CONDUCTOR_ID=<name>`; in herdr, the workspace label is
221
+ used) so `orq claim` can tell them apart: the Claude session is named `conductor-<workspace>:<name>`, and
222
+ `orq claim -l` ends each line with that `session=`, so conductors can message each other.
214
223
  Variants:
215
224
  - **herdr:** run `herdr` (the server runs as a service), then the same `orq conductor` in a pane (`herdr-conductor` still works).
216
225
  - **tmux:** run `orq conductor` in a pane; children open as tmux windows.
@@ -296,6 +305,7 @@ Day to day, shortcuts and several issues at once: [docs/herdr.md](docs/herdr.md)
296
305
  - [docs/omarchy.md](docs/omarchy.md) — extras for Omarchy users
297
306
  - [docs/portability.md](docs/portability.md) — the roadmap: Linux, macOS and native Windows, any agent as conductor, user-defined roles, guided setup
298
307
  - [CHANGELOG.md](CHANGELOG.md) — what changed, release by release
308
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — branches, PR titles, how releases are cut
299
309
  - `examples/` — real configurations to copy
300
310
 
301
311
  ## License
package/README.pt-BR.md CHANGED
@@ -50,9 +50,9 @@ You ── "/issue DEMO-123" ──► conductor (Claude, in any terminal)
50
50
  | Agente | CLI | Papéis possíveis | Observações |
51
51
  |---|---|---|---|
52
52
  | Claude Code | `claude` | conductor, implement, review | várias contas (`claude-accounts`); Opus como conductor |
53
- | 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) |
53
+ | Codex | `codex` | implement, review | sandbox sem rede: o orq roda o `git fetch` antes de uma execução de escrita (`network = false`); cota lida dos arquivos de sessão (inclusive a janela de 30 dias do plano free) |
54
54
  | OpenCode | `opencode` | implement, review | qualquer provider (ex.: MiniMax); na MiniMax, cota pela API do Token Plan com a sua chave |
55
- | Antigravity | `agy` | review | em modo headless nega comandos fora da allowlist: faz a review lendo o diff anexado |
55
+ | Antigravity | `agy` | review (implement com allowlist) | em modo headless nega comandos fora da allowlist: faz a review lendo o diff anexado; não consegue commitar, então o orq commita as execuções de escrita dele (`commit = false`) |
56
56
  | GitHub Copilot | `copilot` | segunda opinião | cota pequena em alguns planos: uso ocasional |
57
57
 
58
58
  Você declara só os que tem, e os papéis que cada um pode assumir.
@@ -60,7 +60,7 @@ Você declara só os que tem, e os papéis que cada um pode assumir.
60
60
  ## Instalação
61
61
 
62
62
  Linux, macOS ou Windows (nativo, não só WSL), em qualquer shell — bash, zsh, fish, PowerShell, cmd.
63
- **Requisitos:** Node 20+ com `npm`, `git`, e pelo menos um agente da tabela acima, logado. Opcional:
63
+ **Requisitos:** Node 22+ com `npm`, `git`, e pelo menos um agente da tabela acima, logado. Opcional:
64
64
  [herdr](https://herdr.dev) ou tmux, para cada filho ter sua própria aba ou janela (sem eles, os filhos rodam em
65
65
  segundo plano e o `orq board` mostra todos).
66
66
 
@@ -103,7 +103,7 @@ git clone https://github.com/irythill/orq ~/Code/orq && cd ~/Code/orq
103
103
  ./install.sh # links em ~/.local/bin, serviço do herdr, skills do Claude
104
104
  ```
105
105
 
106
- Precisa de `git`, `jq`, Node 20+ com `npm`, `python3` (3.11+ para config em TOML; mais antigo → config em JSON ou
106
+ Precisa de `git`, `jq`, Node 22+ com `npm`, `python3` (3.11+ para config em TOML; mais antigo → config em JSON ou
107
107
  `pip install --user tomli`) e `systemd` de usuário (WSL2: Ubuntu 22.04+ com systemd habilitado). O `install.sh`
108
108
  confere os requisitos (tabela do que foi encontrado e do que falta), compila o CLI `orq` (`src/` → `dist/`) e depois
109
109
  cria **symlinks** (nunca copia) dos scripts em `~/.local/bin`, do `setup/herdr.service` em `~/.config/systemd/user/`
@@ -114,7 +114,7 @@ sobrescreve um arquivo de verdade (avisa e pula) e nunca escreve a sua config.
114
114
  | Flag | O que faz |
115
115
  |---|---|
116
116
  | `--dry-run` | mostra o que faria, sem mudar nada |
117
- | `--uninstall` | remove só os links que ele cria, e só os que apontam para este repo (config e histórico ficam) |
117
+ | `--uninstall` | remove só os links que ele cria, e só os que apontam para este repo, e desativa os timers de usuário do orq no systemd (`systemctl --user disable --now`); config e histórico ficam |
118
118
 
119
119
  ## Configuração
120
120
 
@@ -123,10 +123,10 @@ Duas camadas, para separar **o seu jeito de trabalhar** do **projeto do time**:
123
123
  | Arquivo | De quem | O que guarda |
124
124
  |---|---|---|
125
125
  | `~/.config/orq/config.toml` | seu, fica na sua máquina | agentes, contas, modelos por papel e nível, pesos do pick, limites de memória |
126
- | `<workspace>/.orq/workspace.toml` 🚧 | do time, versionado | Plane (projetos, padrão de issue), repos, branch base, verificação, testes, dependências |
126
+ | `<workspace>/.orq/workspace.toml` | do time, versionado | Plane (projetos, padrão de issue), repos, branch base, verificação, testes, dependências |
127
127
 
128
128
  O mais específico ganha, chave por chave. JSON também funciona (`config.json`). Para ver o resultado mesclado:
129
- `orq-config` (tudo), `orq-config get policy.weights`, `orq-config files`.
129
+ `orq config` (tudo), `orq config get policy.weights`, `orq config files`.
130
130
 
131
131
  ### Exemplo: config pessoal
132
132
  Exemplo completo e comentado: [`examples/irythill/config.toml`](examples/irythill/config.toml). O essencial:
@@ -151,13 +151,29 @@ roles = ["review"] # here it only reviews; for someone els
151
151
  [agents.opencode.models.review]
152
152
  default = "minimax/MiniMax-M3"
153
153
 
154
+ [agents.revtool] # a review tool, not a chat: no prompt, no session
155
+ vendor = "minimax" # the vendor of the model it runs, for the cross-vendor rule
156
+ roles = ["review"]
157
+ run = "revtool --markdown -b {base}" # {base} = orq agent -b; it reads the diff itself
158
+ write = false
159
+ ok_exit = [0, 1] # exit 1 = findings, still a verdict
160
+ no_review = "Nothing to review" # matched on the answer's first line (or a short answer): not a verdict, status=fail
161
+ # commit = false # can't git commit: orq commits a write run's changes
162
+ # network = false # no network: orq fetches origin before a write run
163
+ # base_commit = false # can't review `-b <sha>` (default for a tool): pick skips it on a round delta, agent falls back to the branch base
164
+ # filter = "jq -r '...'" # optional: its output → [blocker]/[suggestion] lines
165
+
154
166
  [conductor]
155
167
  handoff_at = 85 # % da janela → o orq handoff move o conductor para outra conta
156
168
  max_per_account = 2 # conta que já tem esse tanto de conductors nunca é alvo do handoff
157
169
  reset_grace_min = 15 # toda janela acima do gatilho renova dentro desses minutos → pula a mudança
158
170
 
171
+ [git]
172
+ issue_pattern = "(?<![a-z0-9])(?<issue>[a-z][a-z0-9]*-\\d+)(?![a-z0-9])" # Nome de branch/worktree → ID da issue, para `orq gc` e `orq ci` — padrão mostrado; regex, grupo nomeado `issue` (senão o grupo 1)
173
+
159
174
  [policy]
160
- quota_limit = 90 # caps the 5h pick limit; also sets ! in `orq quota --line`
175
+ quota_week = 95 # ≥ 95% of the week → agent out
176
+ quota_5h = { simple = 90, medium = 85, heavy = 80 } # same for the 5h window, by task level
161
177
  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)
162
178
  # [agents.claude] headroom = 25 # points kept for conductors: children leave the account at limit − 25
163
179
  [policy.weights]
@@ -167,18 +183,19 @@ speed = 0.15
167
183
  rotation = 0.20 # penalty for having been picked recently
168
184
  ```
169
185
 
170
- ### Como o `orq-pick` escolhe
171
- 1. **Filtros:** só agentes com aquele papel, abaixo do limite de quota, não bloqueados (`orq-block`) e, para reviews,
172
- de um vendor diferente do implementador. Com várias contas, prefere uma cujo ritmo fique dentro do limite e, como
186
+ ### Como o `orq pick` escolhe
187
+ 1. **Filtros:** só agentes com aquele papel cuja CLI está no PATH, abaixo do limite de quota, não bloqueados
188
+ (`orq block`) e, para reviews, de um vendor diferente do implementador (e, para o delta de uma rodada, capazes de
189
+ revisar um commit: `base_commit`). Com várias contas, prefere uma cujo ritmo fique dentro do limite e, como
173
190
  desempate, a menos usada.
174
191
  2. **Pontuação:** `quality × delivery + tokens × economy + speed × quickness − rotation × recent picks`. Os números vêm
175
- do histórico (`orq-stats`); um agente com poucas execuções usa o `prior` da config e ganha um bônus de exploração.
192
+ do histórico (`orq stats`); um agente com poucas execuções usa o `prior` da config e ganha um bônus de exploração.
176
193
  3. **Agentes com `reserve = true`** (ex.: Claude, que divide quota com o conductor) só entram quando nada mais serve.
177
194
 
178
195
  ```bash
179
- orq-pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
180
- orq-stats --days 30 # scoreboard: delivery, approval, time and tokens per agent and model
181
- orq-block antigravity --days 3 # out of the picks (e.g. weekly quota gone, no collector for it)
196
+ orq pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
197
+ orq stats --days 30 # scoreboard per agent and model: delivery, approval, time (to the first turn), tokens
198
+ orq block antigravity --days 3 # out of the picks (e.g. weekly quota gone, no collector for it)
182
199
  orq pick fix --same .orquestra/<ID>/impl-<repo>.pick # a fix round: the implementer's agent/model/account, effort +1
183
200
  ```
184
201
 
@@ -186,7 +203,7 @@ orq pick fix --same .orquestra/<ID>/impl-<repo>.pick # a fix round: the implem
186
203
  O orq fala inglês e português: mensagens da CLI, `orq board`, avisos e `install.sh`. Ele segue `[ui] lang` na sua
187
204
  config (`"auto"` = idioma do sistema, `"en"`, `"pt"`) ou a variável de ambiente `ORQ_LANG`. Para trocar a qualquer
188
205
  momento:
189
- - `orq-lang pt` / `orq-lang en` / `orq-lang toggle` (`orq-lang` sozinho mostra o atual);
206
+ - `orq lang pt` / `orq lang en` / `orq lang toggle` (`orq lang` sozinho mostra o atual);
190
207
  - a tecla `l` dentro do `orq board`;
191
208
  - no herdr, uma tecla para isso: veja [Com o herdr](#com-o-herdr).
192
209
 
@@ -202,7 +219,9 @@ Abra o conductor em qualquer terminal, na pasta do seu workspace (a que o `orq s
202
219
  ```bash
203
220
  cd ~/Code/<workspace> && orq conductor [conta] [modelo] # qualquer terminal: carrega as instruções do conductor, o CLAUDE.md do workspace e a skill /issue
204
221
  ```
205
- Com vários conductors, dê a cada um um nome fixo (`export ORQ_CONDUCTOR_ID=<nome>`) para o `orq claim` distingui-los.
222
+ Com vários conductors, dê a cada um um nome fixo (`export ORQ_CONDUCTOR_ID=<nome>`; no herdr, vale o label do
223
+ workspace) para o `orq claim` distingui-los: a sessão do Claude se chama `conductor-<workspace>:<nome>`, e o
224
+ `orq claim -l` termina cada linha com esse `session=`, para os conductors conseguirem se falar.
206
225
  Variantes:
207
226
  - **herdr:** rode `herdr` (o servidor roda como serviço) e, num pane, o mesmo `orq conductor` (o `herdr-conductor` continua funcionando).
208
227
  - **tmux:** rode `orq conductor` num pane; os filhos abrem como janelas do tmux.
@@ -290,6 +309,7 @@ Dia a dia, atalhos e várias issues ao mesmo tempo: [docs/pt-BR/herdr.md](docs/p
290
309
  - [docs/pt-BR/omarchy.md](docs/pt-BR/omarchy.md) — extras para quem usa Omarchy
291
310
  - [docs/pt-BR/portability.md](docs/pt-BR/portability.md) — o roadmap: Linux, macOS e Windows nativo, qualquer agente como conductor, papéis definidos pelo usuário, setup guiado
292
311
  - [CHANGELOG.pt-BR.md](CHANGELOG.pt-BR.md) — o que mudou, versão a versão
312
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — branches, títulos de PR, como sai uma versão (em inglês)
293
313
  - `examples/` — configurações reais para copiar
294
314
 
295
315
  ## Licença
package/dist/agents.js CHANGED
@@ -1,6 +1,59 @@
1
+ // AgentDef — how to run each agent, as command templates in the config. The five built-in agents below are defaults
2
+ // the user may override key by key in [agents.<name>]; any other CLI becomes usable by declaring `run` there.
3
+ //
4
+ // [agents.acme]
5
+ // run = "acme exec [--model {model}] --prompt-file {prompt} {mode}" # new session
6
+ // resume = "acme exec --resume {session} --prompt-file {prompt} {mode}" # follow-up (-R)
7
+ // readonly = "--sandbox read-only" # {mode} in read-only runs (-r); appended to `run` when it has no {mode}
8
+ // write = "--sandbox workspace-write" # {mode} otherwise; false = the agent only runs read only
9
+ // stdin = true # the prompt goes on stdin (else use {prompt} or {prompt_text})
10
+ // format = "text" # text (answer = stdout) | claude | codex | opencode | antigravity
11
+ // session = { from_log = "session: (\\S+)" } # or "uuid": orq makes one and passes it as {session}
12
+ // answer = "stdout" # stdout | file (the agent writes {out} itself)
13
+ // env = { ACME_NO_MCP = "1" }
14
+ // tui = "acme --on-turn-end {hook_cmd} {prompt_text}" # -T: the agent's own screen (new session)
15
+ // tui_resume = "acme --resume {session} --on-turn-end {hook_cmd} {prompt_text}" # -T -R with the screen closed
16
+ // ok_exit = [0, 1] # exit codes that still count as a verdict (a review tool exits 1 on findings)
17
+ // filter = "jq -r '.findings[] | ...'" # shell line: gets the answer on stdin, its stdout becomes the answer
18
+ // no_review = "Nothing to review" # regex: a match on the answer's first line (or a short answer, 200 chars) = nothing reviewed → status=fail
19
+ // commit = false # the agent can't run git commit: orq commits a write run's changes for it
20
+ // network = false # the agent has no network: orq fetches origin before a write run (no fetch/pull/push for it)
21
+ // base_commit = false # can't review `-b <commit>` (diffs against origin/<base> itself): pick skips it, agent falls back to the branch base (default: follows usesPrompt: true if the agent takes the prompt, false for a tool that ignores it)
22
+ //
23
+ // Templates: words split on spaces ('...' or "..." keep spaces). {name} or {name:default}; a [group] is dropped when
24
+ // any placeholder in it is empty, and repeated for a list placeholder ({gitroots}). Placeholders: prompt (file),
25
+ // prompt_text, prompt_hint, out (answer file), dir (where the agent runs), repo (-d), base (-b), model, effort, session, gitdir,
26
+ // gitroots, gitroots_json, opencode_config, title, mode; in tui templates also the end-of-turn hook as hook_cmd (a
27
+ // shell line), hook_json (an argv array, Codex's notify) and hook_settings (Claude's --settings).
28
+ // The hook (`orq agent --turn <job> <turn_hook>`) reads the turn in the agent's own shape: claude (Stop hook JSON on
29
+ // stdin), codex (notify JSON as the last argument), opencode (<session id> <answer file>, from orq's plugin) or text
30
+ // (the answer on stdin, the session id as an argument).
31
+ import { spawnSync } from "node:child_process";
32
+ import { existsSync } from "node:fs";
33
+ import { delimiter, join } from "node:path";
1
34
  import { m } from "./i18n.js";
2
35
  /** 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
36
  export const usesPrompt = (a) => Boolean(a.stdin || a.prompt_in_dir || /\{prompt(_text|_hint)?\}/.test(a.run ?? ""));
37
+ /** Can the agent review the delta from a commit (`-b <sha>`)? A tool that ignores the prompt diffs against origin/<base> itself, so by default it can't. */
38
+ export const baseCommit = (def) => def.base_commit ?? usesPrompt(def);
39
+ /** Does `base` name a commit rather than a branch (no origin/<base>, but it resolves)? A fix round's round-<n>.sha does. */
40
+ export function isCommitBase(repo, base) {
41
+ const has = (ref) => spawnSync("git", ["-C", repo, "rev-parse", "--verify", "-q", `${ref}^{commit}`]).status === 0;
42
+ return !has(`origin/${base}`) && has(base);
43
+ }
44
+ /** What orq does for a write run in a git repo that the agent itself can't: commit its changes, fetch origin. */
45
+ export const stepIns = (def, ro, inGit) => ({ commit: !ro && inGit && def.commit === false, fetch: !ro && inGit && def.network === false });
46
+ /** The note added to the prompt so the agent doesn't try what orq does for it. */
47
+ export function stepInNote(s) {
48
+ const l = [];
49
+ if (s.fetch)
50
+ l.push("- You have no network access. orq already ran `git fetch origin`, so the origin/* refs are fresh: rebase or diff against them locally. Don't run git fetch, pull or push.");
51
+ if (s.commit)
52
+ l.push("- You can't run `git commit`. Leave your changes in the working tree; orq commits them after you finish. Start your final answer with a one-line commit message in the form `Commit: <message>`.");
53
+ return l.length ? `\n\n## Environment (from orq)\n${l.join("\n")}\n` : "";
54
+ }
55
+ /** The commit message an agent proposed (`Commit: ...` line in its answer), else the fallback. */
56
+ export const commitMessage = (answer, fallback) => /^\W*commit:\s*`?([^`\n]+?)`?\s*$/im.exec(answer)?.[1]?.trim() || fallback;
4
57
  /** Read only runs of a `throwaway` agent get a worktree copy of HEAD, but only where there is a git repo to copy. */
5
58
  export const useThrowaway = (def, ro, resume, inGit) => Boolean(def.throwaway && ro && !resume && inGit);
6
59
  const NOMCP_CODEX = "-c mcp_servers.plane.enabled=false -c mcp_servers.shadcn.enabled=false";
@@ -29,7 +82,7 @@ export const BUILTIN_AGENTS = {
29
82
  `--skip-git-repo-check {mode} ${NOMCP_CODEX} -o {out} -`,
30
83
  resume: `codex exec resume {session} -c model_reasoning_effort={effort:low} ` +
31
84
  `-c sandbox_workspace_write.writable_roots={gitroots_json} ${NOMCP_CODEX} -o {out} -`,
32
- readonly: "", write: "[--add-dir {gitroots}]", stdin: true, format: "codex", answer: "file", throwaway: true,
85
+ readonly: "", write: "[--add-dir {gitroots}]", stdin: true, format: "codex", answer: "file", throwaway: true, network: false,
33
86
  // The screen: no approvals (nobody to approve), the same sandbox as exec; notify = end of turn.
34
87
  tui: `codex [-m {model}] -c model_reasoning_effort={effort:low} -s workspace-write -a never -C {dir} {mode} ` +
35
88
  `${NOMCP_CODEX} -c notify={hook_json} {prompt_text}`,
@@ -51,7 +104,7 @@ export const BUILTIN_AGENTS = {
51
104
  // a run that only had commands denied still counts when the worktree changed (see the antigravity format). The prompt goes in a file in its folder (excluded from git) and -p only says "read it".
52
105
  run: `${AGY} {mode} -p {prompt_hint}`, resume: `${AGY} --conversation {session} {mode} -p {prompt_hint}`,
53
106
  readonly: "--mode plan", write: "--mode accept-edits --add-dir {gitdir}",
54
- format: "antigravity", throwaway: true, prompt_in_dir: true,
107
+ format: "antigravity", throwaway: true, prompt_in_dir: true, commit: false,
55
108
  },
56
109
  copilot: {
57
110
  // Second opinion, always read only: git reads allowed, anything else denied by -p without asking.
@@ -68,6 +121,26 @@ export function agentDef(cfg, name) {
68
121
  throw new Error(m(`unknown agent: ${name} (built in: ${Object.keys(BUILTIN_AGENTS).join(", ")}; others need agents.${name}.run in the config)`, `agente desconhecido: ${name} (embutidos: ${Object.keys(BUILTIN_AGENTS).join(", ")}; outros precisam de agents.${name}.run no config)`));
69
122
  return def;
70
123
  }
124
+ /** Is `bin` on PATH? (PATHEXT on Windows: claude.cmd, codex.exe…) A path (with a slash) must exist itself. */
125
+ export function onPath(bin) {
126
+ if (/[\\/]/.test(bin))
127
+ return existsSync(bin);
128
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
129
+ return (process.env.PATH ?? "").split(delimiter).some((d) => d && exts.some((e) => existsSync(join(d, bin + e))));
130
+ }
131
+ /** Why `name` can't start here (its CLI doesn't resolve on the PATH the child inherits), or undefined when it can. */
132
+ export function missingBinary(cfg, name) {
133
+ let bin;
134
+ try {
135
+ bin = tokenize(agentDef(cfg, name).run)[0]?.text;
136
+ }
137
+ catch {
138
+ return undefined;
139
+ }
140
+ if (!bin || onPath(bin))
141
+ return undefined;
142
+ return m(`\`${bin}\` not found on PATH (installed for another Node version? reinstall it)`, `\`${bin}\` não está no PATH (instalado para outra versão do Node? reinstale)`);
143
+ }
71
144
  /** Split a template into words, remembering which [group] each belongs to. */
72
145
  export function tokenize(t) {
73
146
  const out = [];
@@ -154,7 +227,14 @@ export function expandTemplate(t, vars) {
154
227
  if (g >= 0)
155
228
  while (j < words.length && words[j].group === g)
156
229
  j++;
157
- out.push(...expandWords(words.slice(i, j).map((w) => w.text), vars, g >= 0));
230
+ const got = expandWords(words.slice(i, j).map((w) => w.text), vars, g >= 0);
231
+ // `--flag {placeholder}` with the placeholder empty: drop the flag too, or it would swallow the next argument.
232
+ const ph = g < 0 ? /^\{(\w+)(?::[^}]*)?\}$/.exec(words[i].text) : null, prev = words[i - 1];
233
+ if (!got.length && ph && !Array.isArray(vars[ph[1]]) && prev && prev.group < 0 && /^-[^\s{]/.test(prev.text)
234
+ && out[out.length - 1] === prev.text)
235
+ out.pop();
236
+ else
237
+ out.push(...got);
158
238
  i = j;
159
239
  }
160
240
  return out;
@@ -255,11 +335,34 @@ export const FORMATS = {
255
335
  }
256
336
  if (errs.length)
257
337
  answer += (answer ? "\n" : "") + errs.join("\n") + "\n";
258
- return { session: lastOf(res.map((r) => r.conversation_id).filter(Boolean)), answer, rc: errs.length || wasDenied ? 3 : undefined,
338
+ const exhausted = agyExhausted(log, res);
339
+ return { exhausted, session: lastOf(res.map((r) => r.conversation_id).filter(Boolean)), answer, rc: errs.length || wasDenied ? 3 : undefined,
259
340
  denied: wasDenied ? (denied.length ? denied : ["(command)"]) : undefined };
260
341
  },
261
342
  },
262
343
  };
344
+ /** "Resets in 123h12m32s" → seconds (undefined without such a duration). */
345
+ export function parseResetIn(text) {
346
+ const r = /Resets in\s+(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?/i.exec(text);
347
+ if (!r || !(r[1] || r[2] || r[3]))
348
+ return undefined;
349
+ return Number(r[1] ?? 0) * 3600 + Number(r[2] ?? 0) * 60 + Number(r[3] ?? 0);
350
+ }
351
+ /** agy's quota failure (RESOURCE_EXHAUSTED / "quota reached"), from the result's error or the AGY_ERROR line. */
352
+ function agyExhausted(log, res) {
353
+ const msgs = res.filter((r) => r.status === "ERROR" || r.error).map((r) => String(r.error ?? ""));
354
+ for (const l of log.split(/\r?\n/)) {
355
+ if (!l.startsWith("AGY_ERROR:"))
356
+ continue;
357
+ const j = json(l.slice(10).trim());
358
+ if (j)
359
+ msgs.push(`${j.status ?? ""} ${j.short_error ?? ""}`);
360
+ }
361
+ const text = msgs.find((t) => /RESOURCE_EXHAUSTED|quota reached/i.test(t));
362
+ if (!text)
363
+ return undefined;
364
+ return { secs: parseResetIn(text), message: text.replace(/^.*?(?:\(code 429\):\s*)/, "").trim() };
365
+ }
263
366
  /** The model in an agent's argv (-m / --model / --model=x), without the provider prefix. */
264
367
  export function argvModel(a) {
265
368
  const i = a.findIndex((x) => x === "-m" || x === "--model");
package/dist/ci.js CHANGED
@@ -15,6 +15,7 @@ import { join } from "node:path";
15
15
  import { aliveOwner } from "./commands/claim.js";
16
16
  import { writeAtomic } from "./fsutil.js";
17
17
  import { m } from "./i18n.js";
18
+ import { issueFromName, issuePattern } from "./issue.js";
18
19
  import { alive } from "./lock.js";
19
20
  import { stateDir } from "./paths.js";
20
21
  import { notify } from "./testrun.js";
@@ -93,11 +94,10 @@ export function listWorktrees(ws) {
93
94
  return out;
94
95
  }
95
96
  /** 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
+ * the issue ID (git.issue_pattern; any "plane-" prefix or suffix is ignored) under <ws>/.worktrees. An issue can have one per repo (several repos in play).
97
98
  * `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));
99
+ export function issueBranches(ws, issue, all = listWorktrees(ws), pattern) {
100
+ return all.filter((b) => issueFromName(b.branch, pattern)?.toLowerCase() === issue.toLowerCase());
101
101
  }
102
102
  // GitHub's statusCheckRollup mixes CheckRun (status/conclusion, the GitHub Actions / Checks API shape) with the
103
103
  // legacy commit-status shape (StatusContext: just `state`, no status/conclusion at all) for repos whose CI still
@@ -204,8 +204,8 @@ function notifyDesktop(issue, repo, state) {
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
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) })));
207
+ const now = Date.now(), worktrees = listWorktrees(ws), pattern = issuePattern(ws);
208
+ const found = claimedIssues(ws).flatMap(({ id, owner, kind }) => issueBranches(ws, id, worktrees, pattern).map(({ repo, branch }) => ({ id, owner, kind, repo, branch, ...prStatus(join(ws, repo), branch) })));
209
209
  const toNotify = [];
210
210
  const mine = withCacheLock(() => {
211
211
  const cache = loadCache(), mine = [];