@irythill/orq 0.7.0 → 0.9.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
@@ -4,7 +4,7 @@
4
4
 
5
5
  # orq — orchestrating coding agents
6
6
 
7
- You describe the work in a Plane issue. A **conductor** (Claude, ideally Opus) reads it, picks the right agent for
7
+ You describe the work in a Plane issue. A **conductor** (any agent that can run commands) reads it, picks the right agent for
8
8
  each step, dispatches the **implementer**, the **tests** and a **cross-vendor review** (each in its own tab or window
9
9
  in [herdr](https://herdr.dev) or tmux, or in the background), and hands the issue back *In Review* with a summary in
10
10
  Plane. Merge, PR and deploy stay with you.
@@ -20,7 +20,7 @@ that lives in your config.
20
20
  ## How it works
21
21
 
22
22
  ```
23
- You ── "/issue DEMO-123" ──► conductor (Claude, in any terminal)
23
+ You ── "/issue DEMO-123" ──► conductor (any agent CLI, in any terminal)
24
24
  │ orq pick impl ──► which agent/model implements (config + history + quota)
25
25
  ├─► implementer orq agent own tab/window or background · worktree from origin/<base> · memory cap
26
26
  ├─► tests orq verify only the diff's tests · queue of one suite at a time
@@ -47,9 +47,9 @@ You ── "/issue DEMO-123" ──► conductor (Claude, in any terminal)
47
47
 
48
48
  | Agent | CLI | Possible roles | Notes |
49
49
  |---|---|---|---|
50
- | Claude Code | `claude` | conductor, implement, review | several accounts (`claude-accounts`); Opus as conductor |
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
- | OpenCode | `opencode` | implement, review | any provider (e.g. MiniMax); on MiniMax, quota from the Token Plan API with your key |
50
+ | Claude Code | `claude` | conductor, implement, review | several accounts (`claude-accounts`); Claude as conductor |
51
+ | Codex | `codex` | conductor, 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
+ | OpenCode | `opencode` | conductor, implement, review | any provider (e.g. MiniMax); on MiniMax, quota from the Token Plan API with your key |
53
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
 
@@ -70,7 +70,7 @@ orq setup
70
70
  `orq setup` shows what it found (git, terminal, how memory is capped, keyring, your shell), links orq's skills into
71
71
  `~/.claude/skills/` and each account folder in `claude-accounts` (a junction on Windows; never over a folder of
72
72
  yours), asks about your agents and plans, writes `~/.config/orq/config.toml` (`%APPDATA%\orq\config.toml` on Windows)
73
- and prepares the folder that groups your repos for the conductor: `.orquestra/`, the map `CLAUDE.md` and the
73
+ and prepares the folder that groups your repos for the conductor: `.orquestra/`, the map `ORQ.md` and the
74
74
  `/issue` playbook in the conductor's format. Pieces on their own: `orq setup --install` (no questions),
75
75
  `orq setup --workspace <folder> --conductor <agent>`. Inside Claude, **`/orq-setup`** does the same as a
76
76
  conversation and also fills in the repo table. Plane: [docs/plane.md](docs/plane.md).
@@ -135,7 +135,7 @@ roles = ["impl", "fix", "review"]
135
135
  quota = ["codex"] # name in agent-quota's output
136
136
  [agents.codex.models.impl]
137
137
  simple = "gpt-6-sol@low" # model@effort, per issue level
138
- medium = "gpt-6-sol@medium"
138
+ medium = ["gpt-6-sol@medium", "gpt-6-luna@medium"] # several: each (agent, model) pair is scored on its own history
139
139
  heavy = "gpt-6.1-sol@high"
140
140
  [agents.codex.models.review]
141
141
  simple = "gpt-6-sol@low" # reviews scale with the issue too
@@ -172,7 +172,7 @@ issue_pattern = "(?<![a-z0-9])(?<issue>[a-z][a-z0-9]*-\\d+)(?![a-z0-9])" # Bra
172
172
  quota_week = 95 # ≥ 95% of the week → agent out
173
173
  quota_5h = { simple = 90, medium = 85, heavy = 80 } # same for the 5h window, by task level
174
174
  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)
175
- # [agents.claude] headroom = 25 # points kept for conductors: children leave the account at limit − 25
175
+ # [conductor] headroom = 25 # points kept for the active conductor: children stop at limit − 25
176
176
  [policy.weights]
177
177
  quality = 0.40 # did it deliver? (impl: approved on the first review)
178
178
  tokens = 0.45 # did it spend little?
@@ -187,7 +187,7 @@ rotation = 0.20 # penalty for having been picked recent
187
187
  the least used.
188
188
  2. **Scores:** `quality × delivery + tokens × economy + speed × quickness − rotation × recent picks`. The numbers come
189
189
  from the history (`orq stats`); an agent with few runs uses the config's `prior` and gets an exploration bonus.
190
- 3. **`reserve = true` agents** (e.g. Claude, which shares quota with the conductor) only come in when nothing else fits.
190
+ 3. **Reserved agents** (`reserve = true`, or the active conductor by default) receive a score penalty to keep quota for orchestration.
191
191
 
192
192
  ```bash
193
193
  orq pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
@@ -215,8 +215,37 @@ Whatever is already running keeps its language. Conductors always talk to you in
215
215
 
216
216
  Open the conductor in any terminal, in your workspace folder (the one `orq setup` prepared):
217
217
  ```bash
218
- cd ~/Code/<workspace> && orq conductor [account] [model] # any terminal: loads the conductor instructions, the workspace CLAUDE.md and the /issue skill
218
+ cd ~/Code/<workspace> && orq conductor [agent] [account|-] [model] # any terminal: loads the conductor instructions, the workspace ORQ.md and the /issue skill
219
219
  ```
220
+ Choose the conductor with `orq setup --workspace <folder> --conductor codex`, then run `orq conductor` in that folder.
221
+ `orq conductor codex - <model>` overrides the choice for one session; `-r <id>` resumes it. Claude Code is also a
222
+ tested conductor; the old `orq conductor <Claude account> <model>` syntax still works. If an account and an agent
223
+ have the same name, use `--agent <name>` to select the agent explicitly. The `herdr-conductor` wrapper keeps opening Claude.
224
+
225
+ `ORQ.md` holds the workspace map. Setup preserves an existing `CLAUDE.md`, copying it into `ORQ.md` on migration;
226
+ otherwise it creates a `CLAUDE.md` pointer. `AGENTS.md` points to the map and holds the generated issue playbook,
227
+ preserving text outside the generated block. Edit `ORQ.md` after migration. With no background commands, the conductor
228
+ uses `orq wait` in the foreground. `orq handoff` still moves Claude accounts only.
229
+
230
+ Defaults live in `[roles.conductor]` (`agent`, `account`, `model`). Interactive templates are separate from worker
231
+ templates, keeping MCPs and normal permission prompts enabled. Override any built-in or add another CLI:
232
+
233
+ ```toml
234
+ [agents.codex.conductor]
235
+ run = "codex [-m {model}] -C {dir} {prompt_text}"
236
+ resume = "codex resume {session} [-m {model}] -C {dir} {prompt_text}"
237
+ models = { fast = "your-model-id" }
238
+ accounts = { work = { CODEX_HOME = "/path/to/codex-work" } }
239
+
240
+ [conductor]
241
+ headroom = 25 # percentage points kept for whichever agent conducts
242
+ ```
243
+
244
+ Templates accept `{dir}`, `{instructions}` (file path), `{prompt_text}` (read-instructions prompt), `{model}`,
245
+ `{account}`, `{session}` and `{name}`. Account entries and `env` are environment-variable maps. The running conductor
246
+ exports `ORQ_CONDUCTOR_AGENT`, so `orq pick` applies the reserve penalty and `[conductor].headroom` to that agent,
247
+ even after a session override. Explicit `agents.<name>.reserve` and `headroom` take precedence.
248
+
220
249
  With several conductors, give each a fixed name (`export ORQ_CONDUCTOR_ID=<name>`; in herdr, the workspace label is
221
250
  used) so `orq claim` can tell them apart: the Claude session is named `conductor-<workspace>:<name>`, and
222
251
  `orq claim -l` ends each line with that `session=`, so conductors can message each other.
@@ -279,7 +308,7 @@ Day to day, shortcuts and several issues at once: [docs/herdr.md](docs/herdr.md)
279
308
  | `orq wait <prefix>...` | Blocks until those agents finish; prints `status=` and the answer file (any conductor, any terminal) |
280
309
  | `orq verify ...` (`herdr-verify`) | Tests related to the diff, queued and memory-capped; failures that also fail on the base count as pre-existing |
281
310
  | `orq board` (`herdr-board`) | Live board: queue, agents, conductors, quota, blocks, scoreboard |
282
- | `orq conductor [account] [model] [-r <id>]` (`herdr-conductor`) | Opens the conductor (Claude) in the current terminal with its instructions, account and model; `-r` resumes a session |
311
+ | `orq conductor [agent] [account|-] [model] [-r <id>]` (`herdr-conductor`) | Opens the configured conductor in the current terminal with its instructions, account and model; `-r` resumes a session |
283
312
  | `orq claim` (`herdr-claim`) | Owner of each issue among conductors |
284
313
  | `orq deps` (`herdr-deps`) | Links the worktree's dependencies to the main repo's (no reinstall) |
285
314
  | `orq baseline ...` (`herdr-baseline`) | Manual snapshot of the tests failing on the base (outside the flow) |
package/README.pt-BR.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # orq — orquestrando agentes de código
6
6
 
7
- Você descreve o trabalho numa issue do Plane. Um **conductor** (Claude, de preferência Opus) lê a issue, escolhe o
7
+ Você descreve o trabalho numa issue do Plane. Um **conductor** (qualquer agente que execute comandos) lê a issue, escolhe o
8
8
  agente certo para cada etapa, despacha o **implementador**, os **testes** e uma **review cross-vendor** (cada um
9
9
  em sua própria aba ou janela no [herdr](https://herdr.dev) ou no tmux, ou em segundo plano), e devolve a issue
10
10
  *In Review* com um resumo no Plane. Merge, PR e deploy continuam com você.
@@ -20,7 +20,7 @@ trabalhar. Tudo isso fica na sua config.
20
20
  ## Como funciona
21
21
 
22
22
  ```
23
- You ── "/issue DEMO-123" ──► conductor (Claude, in any terminal)
23
+ You ── "/issue DEMO-123" ──► conductor (any agent CLI, in any terminal)
24
24
  │ orq pick impl ──► which agent/model implements (config + history + quota)
25
25
  ├─► implementer orq agent own tab/window or background · worktree from origin/<base> · memory cap
26
26
  ├─► tests orq verify only the diff's tests · queue of one suite at a time
@@ -49,9 +49,9 @@ You ── "/issue DEMO-123" ──► conductor (Claude, in any terminal)
49
49
 
50
50
  | Agente | CLI | Papéis possíveis | Observações |
51
51
  |---|---|---|---|
52
- | Claude Code | `claude` | conductor, implement, review | várias contas (`claude-accounts`); Opus como conductor |
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
- | OpenCode | `opencode` | implement, review | qualquer provider (ex.: MiniMax); na MiniMax, cota pela API do Token Plan com a sua chave |
52
+ | Claude Code | `claude` | conductor, implement, review | várias contas (`claude-accounts`); Claude como conductor |
53
+ | Codex | `codex` | conductor, 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
+ | OpenCode | `opencode` | conductor, implement, review | qualquer provider (ex.: MiniMax); na MiniMax, cota pela API do Token Plan com a sua chave |
55
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
 
@@ -73,7 +73,7 @@ O `orq setup` mostra o que encontrou (git, terminal, como a memória é limitada
73
73
  do orq em `~/.claude/skills/` e em cada pasta de conta do `claude-accounts` (uma junction no Windows; nunca por cima
74
74
  de uma pasta sua), pergunta sobre seus agentes e planos, escreve o `~/.config/orq/config.toml`
75
75
  (`%APPDATA%\orq\config.toml` no Windows) e prepara a pasta que agrupa seus repos para o conductor: `.orquestra/`, o
76
- mapa `CLAUDE.md` e o playbook do `/issue` no formato do conductor. Partes avulsas: `orq setup --install` (sem
76
+ mapa `ORQ.md` e o playbook do `/issue` no formato do conductor. Partes avulsas: `orq setup --install` (sem
77
77
  perguntas), `orq setup --workspace <pasta> --conductor <agente>`. Dentro do Claude, o **`/orq-setup`** faz o mesmo
78
78
  como conversa e ainda preenche a tabela de repos. Plane: [docs/pt-BR/plane.md](docs/pt-BR/plane.md).
79
79
 
@@ -138,7 +138,7 @@ roles = ["impl", "fix", "review"]
138
138
  quota = ["codex"] # name in agent-quota's output
139
139
  [agents.codex.models.impl]
140
140
  simple = "gpt-6-sol@low" # model@effort, per issue level
141
- medium = "gpt-6-sol@medium"
141
+ medium = ["gpt-6-sol@medium", "gpt-6-luna@medium"] # several: each (agent, model) pair is scored on its own history
142
142
  heavy = "gpt-6.1-sol@high"
143
143
  [agents.codex.models.review]
144
144
  simple = "gpt-6-sol@low" # reviews scale with the issue too
@@ -175,7 +175,7 @@ issue_pattern = "(?<![a-z0-9])(?<issue>[a-z][a-z0-9]*-\\d+)(?![a-z0-9])" # Nom
175
175
  quota_week = 95 # ≥ 95% of the week → agent out
176
176
  quota_5h = { simple = 90, medium = 85, heavy = 80 } # same for the 5h window, by task level
177
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)
178
- # [agents.claude] headroom = 25 # points kept for conductors: children leave the account at limit − 25
178
+ # [conductor] headroom = 25 # pontos reservados para o conductor ativo: filhos param em limite − 25
179
179
  [policy.weights]
180
180
  quality = 0.40 # did it deliver? (impl: approved on the first review)
181
181
  tokens = 0.45 # did it spend little?
@@ -190,7 +190,7 @@ rotation = 0.20 # penalty for having been picked recent
190
190
  desempate, a menos usada.
191
191
  2. **Pontuação:** `quality × delivery + tokens × economy + speed × quickness − rotation × recent picks`. Os números vêm
192
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.
193
- 3. **Agentes com `reserve = true`** (ex.: Claude, que divide quota com o conductor) só entram quando nada mais serve.
193
+ 3. **Agentes reservados** (`reserve = true`, ou o conductor ativo por padrão) recebem uma penalidade na pontuação para preservar cota para a orquestração.
194
194
 
195
195
  ```bash
196
196
  orq pick review --impl-agent codex --explain # every candidate's score and who was left out, and why
@@ -217,8 +217,37 @@ O que já está rodando continua no idioma em que começou. Os conductors sempre
217
217
 
218
218
  Abra o conductor em qualquer terminal, na pasta do seu workspace (a que o `orq setup` preparou):
219
219
  ```bash
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
220
+ cd ~/Code/<workspace> && orq conductor [agente] [conta|-] [modelo] # qualquer terminal: carrega as instruções do conductor, o ORQ.md do workspace e a skill /issue
221
221
  ```
222
+ Escolha o conductor com `orq setup --workspace <pasta> --conductor codex` e rode `orq conductor` nessa pasta.
223
+ `orq conductor codex - <modelo>` troca o agente só nessa sessão; `-r <id>` a retoma. Claude Code também é um
224
+ conductor testado; a sintaxe antiga `orq conductor <conta Claude> <modelo>` continua funcionando. Se uma conta e um
225
+ agente têm o mesmo nome, use `--agent <nome>` para selecionar o agente explicitamente. O wrapper `herdr-conductor` continua abrindo Claude.
226
+
227
+ `ORQ.md` guarda o mapa do workspace. O setup preserva um `CLAUDE.md` existente e copia seu conteúdo para `ORQ.md`
228
+ na migração; se ele não existe, cria um `CLAUDE.md` apontando para o mapa. `AGENTS.md` aponta para o mapa e contém o
229
+ playbook gerado, preservando o texto fora do bloco gerado. Depois da migração, edite `ORQ.md`. Sem comandos em segundo
230
+ plano, o conductor usa `orq wait` em primeiro plano. `orq handoff` continua transferindo apenas contas Claude.
231
+
232
+ Os padrões ficam em `[roles.conductor]` (`agent`, `account`, `model`). Os templates interativos são separados dos
233
+ templates dos filhos, mantendo MCPs e os pedidos normais de permissão. Sobrescreva um agente embutido ou adicione outra CLI:
234
+
235
+ ```toml
236
+ [agents.codex.conductor]
237
+ run = "codex [-m {model}] -C {dir} {prompt_text}"
238
+ resume = "codex resume {session} [-m {model}] -C {dir} {prompt_text}"
239
+ models = { fast = "id-do-seu-modelo" }
240
+ accounts = { work = { CODEX_HOME = "/caminho/codex-work" } }
241
+
242
+ [conductor]
243
+ headroom = 25 # pontos percentuais reservados para o agente que estiver conduzindo
244
+ ```
245
+
246
+ Os templates aceitam `{dir}`, `{instructions}` (caminho do arquivo), `{prompt_text}` (prompt para ler as instruções),
247
+ `{model}`, `{account}`, `{session}` e `{name}`. Contas e `env` são mapas de variáveis de ambiente. O conductor exporta
248
+ `ORQ_CONDUCTOR_AGENT`, então o `orq pick` aplica a penalidade de reserva e `[conductor].headroom` ao agente em uso,
249
+ inclusive após uma troca só nessa sessão. Valores explícitos de `agents.<nome>.reserve` e `headroom` têm precedência.
250
+
222
251
  Com vários conductors, dê a cada um um nome fixo (`export ORQ_CONDUCTOR_ID=<nome>`; no herdr, vale o label do
223
252
  workspace) para o `orq claim` distingui-los: a sessão do Claude se chama `conductor-<workspace>:<nome>`, e o
224
253
  `orq claim -l` termina cada linha com esse `session=`, para os conductors conseguirem se falar.
@@ -283,7 +312,7 @@ Dia a dia, atalhos e várias issues ao mesmo tempo: [docs/pt-BR/herdr.md](docs/p
283
312
  | `orq wait <prefixo>...` | Bloqueia até esses agentes terminarem; imprime `status=` e o arquivo da resposta (qualquer conductor, qualquer terminal) |
284
313
  | `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 |
285
314
  | `orq board` (`herdr-board`) | Painel ao vivo: fila, agentes, conductors, cota, bloqueios, placar |
286
- | `orq conductor [conta] [modelo] [-r <id>]` (`herdr-conductor`) | Abre o conductor (Claude) no terminal atual com as instruções, conta e modelo; `-r` retoma uma sessão |
315
+ | `orq conductor [agente] [conta|-] [modelo] [-r <id>]` (`herdr-conductor`) | Abre o conductor configurado no terminal atual com as instruções, conta e modelo; `-r` retoma uma sessão |
287
316
  | `orq claim` (`herdr-claim`) | Dono de cada issue entre os conductors |
288
317
  | `orq deps` (`herdr-deps`) | Liga as dependências do worktree às do repo principal (sem reinstalar) |
289
318
  | `orq baseline ...` (`herdr-baseline`) | Snapshot manual dos testes que falham na base (fora do fluxo) |
@@ -141,7 +141,9 @@ export async function claim(argv) {
141
141
  const kind = process.env.HERDR_PANE_ID ? "herdr" : "id";
142
142
  const me = process.env.HERDR_PANE_ID || process.env.ORQ_CONDUCTOR_ID || `${userInfo().username}@${hostname()}`;
143
143
  const myName = (kind === "herdr" ? herdrName(me) : "") || process.env.ORQ_CONDUCTOR_ID || "";
144
- const account = basename(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"));
144
+ const account = process.env.ORQ_CONDUCTOR_AGENT && process.env.ORQ_CONDUCTOR_AGENT !== "claude"
145
+ ? `${process.env.ORQ_CONDUCTOR_AGENT}:${process.env.ORQ_CONDUCTOR_ACCOUNT || "default"}`
146
+ : basename(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"));
145
147
  const ownerLine = () => `pane=${me} kind=${kind} account=${account} at=${new Date().toISOString()}`
146
148
  + (myName ? ` name=${myName}` : "") + "\n";
147
149
  if (mode === "register") {
@@ -11,12 +11,13 @@ const DESC = {
11
11
  quota: "quota per agent/account", pick: "pick agent, model and account for a role", stats: "scoreboard per agent", rounds: "rounds, time and repeated findings of an issue",
12
12
  board: "live board", lock: "test queue", run: "run a command under a memory cap", agent: "run a child agent",
13
13
  wait: "wait for child agents", verify: "tests of a diff", baseline: "tests already failing on the base",
14
- instructions: "write the /issue playbook", conductor: "open the conductor (Claude) in this terminal", claim: "issue owner among conductors", deps: "link node_modules into a worktree", gc: "remove merged issue worktrees", ci: "CI/CD status per claimed issue", handoff: "move a conductor low on quota to another account",
14
+ instructions: "write the /issue playbook", conductor: "open the configured conductor in this terminal", claim: "issue owner among conductors", deps: "link node_modules into a worktree", gc: "remove merged issue worktrees", ci: "CI/CD status per claimed issue", handoff: "move a conductor low on quota to another account",
15
15
  block: "block an agent or account", completion: "shell completion script",
16
16
  };
17
17
  /** Fixed words after a subcommand. */
18
18
  export const WORDS = {
19
- setup: ["--preset", "--detect", "--answers", "--write", "--install", "--workspace"],
19
+ setup: ["--preset", "--detect", "--answers", "--write", "--install", "--workspace", "--conductor"],
20
+ conductor: ["claude", "codex", "opencode", "antigravity", "copilot", "--agent", "-r", "--help"],
20
21
  config: ["get", "files", "-w"], lang: ["pt", "en", "auto", "toggle"], lock: ["wait", "release", "run", "status"],
21
22
  pick: ["impl", "fix", "worker", "review", "opinion", "watchdog"], instructions: ["claude", "agents"], board: ["--once", "--json", "-w"],
22
23
  completion: ["bash", "zsh", "fish", "powershell"], stats: ["--days", "--json", "-w"], wait: ["--any", "--timeout", "--answer"],
@@ -1,32 +1,62 @@
1
- // orq conductor — opens the conductor (Claude Code) in the current terminal, with the workspace's conductor
1
+ // orq conductor — opens the configured agent in the current terminal, with the workspace's conductor
2
2
  // instructions, the chosen account and model (port of setup/herdr-conductor, for any terminal and OS).
3
- // orq conductor [account|-] [model] [-r <session>]
3
+ // orq conductor [agent] [account|-] [model] [-r <session>]
4
4
  // Workspace: the current folder if it has .orquestra-conductor.md, else $CONDUCTOR_WS. $CONDUCTOR_FILE overrides the
5
5
  // instructions file. The session is named conductor-<workspace folder>:<herdr label or ORQ_CONDUCTOR_ID> (see sessionName).
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { existsSync } from "node:fs";
8
8
  import { join, resolve } from "node:path";
9
9
  import { claudeAccounts } from "../accounts.js";
10
+ import { ALIASES, expandTemplate, expandString, onPath } from "../agents.js";
11
+ import { getPath, loadConfig } from "../config.js";
10
12
  import { m } from "../i18n.js";
11
13
  import { runCapped } from "../process.js";
12
14
  import { claim, herdrName, sessionName } from "./claim.js";
13
- export const USAGE = `orq conductor [account|-] [model] [-r <session>] open the conductor (Claude) here: instructions, account, model
14
- model: sonnet | opus | haiku | a full id; "-" as account = default account; -r resumes that conversation`;
15
+ import { instructions } from "./instructions.js";
16
+ export const USAGE = `orq conductor [agent] [account|-] [model] [-r <session>] open the configured conductor here
17
+ defaults: roles.conductor.agent/account/model; without config: claude
18
+ legacy: orq conductor <Claude account|-> [model]; -r resumes that conversation
19
+ --agent <name> disambiguates an agent from a Claude account with the same name
20
+ templates: agents.<agent>.conductor.run/resume/models/accounts/env`;
15
21
  export const MODELS = {
16
22
  sonnet: "claude-sonnet-5",
17
23
  opus: "claude-opus-5-5",
18
24
  haiku: "claude-haiku-4-5-20251001",
19
25
  };
20
26
  export const modelId = (alias) => MODELS[alias] ?? alias;
21
- /** [account|-] [model] [-r|--resume <id>]; further positionals are ignored (as herdr-conductor did). */
22
- export function parseArgs(argv) {
27
+ /** Interactive launch templates retain the conductor's MCPs and normal permission prompts. */
28
+ export const CONDUCTORS = {
29
+ claude: {
30
+ run: "claude -n {name} [--model {model}] --append-system-prompt-file {instructions}",
31
+ resume: "claude --resume {session} -n {name} [--model {model}] --append-system-prompt-file {instructions}",
32
+ models: MODELS,
33
+ },
34
+ codex: { run: "codex [-m {model}] -C {dir} {prompt_text}",
35
+ resume: "codex resume {session} [-m {model}] -C {dir} {prompt_text}" },
36
+ opencode: { run: "opencode [-m {model}] --prompt {prompt_text} {dir}",
37
+ resume: "opencode -s {session} [-m {model}] --prompt {prompt_text} {dir}" },
38
+ copilot: { run: "copilot -C {dir} [--model {model}] -i {prompt_text}",
39
+ resume: "copilot -C {dir} --resume={session} [--model {model}] -i {prompt_text}" },
40
+ antigravity: { run: "agy [--model {model}] -i {prompt_text}",
41
+ resume: "agy --conversation {session} [--model {model}] -i {prompt_text}" },
42
+ };
43
+ /** Optional agent, then [account|-] [model] [-r|--resume <id>]; old Claude positionals still parse. */
44
+ export function parseArgs(argv, names = Object.keys(CONDUCTORS), accounts = []) {
23
45
  const out = { account: "", model: "", resume: "", help: false };
24
46
  const pos = [];
25
47
  for (let i = 0; i < argv.length; i++) {
26
48
  const a = argv[i];
27
- if (a === "-r" || a === "--resume") {
49
+ if (a === "--agent") {
28
50
  const v = argv[++i];
29
- if (!v) {
51
+ if (!v || v.startsWith("-")) {
52
+ out.error = USAGE;
53
+ return out;
54
+ }
55
+ out.agent = ALIASES[v] ?? v;
56
+ }
57
+ else if (a === "-r" || a === "--resume") {
58
+ const v = argv[++i];
59
+ if (!v || v.startsWith("-")) {
30
60
  out.error = m("give the session id", "informe o id da sessão");
31
61
  return out;
32
62
  }
@@ -37,9 +67,53 @@ export function parseArgs(argv) {
37
67
  else
38
68
  pos.push(a);
39
69
  }
70
+ if (!out.agent && pos.length === 2 && pos[1] !== "-" && accounts.includes(pos[0])) {
71
+ out.agent = "claude";
72
+ }
73
+ else if (!out.agent && pos.length && names.includes(ALIASES[pos[0]] ?? pos[0])) {
74
+ const agent = pos.shift();
75
+ out.agent = ALIASES[agent] ?? agent;
76
+ }
40
77
  [out.account = "", out.model = ""] = pos;
78
+ if (pos.length > 2)
79
+ out.error = USAGE;
41
80
  return out;
42
81
  }
82
+ /** Resolve a conductor without reusing worker templates (which disable MCPs and permissions). */
83
+ export function launch(cfg, a, ws, file, name = "") {
84
+ const agent = a.agent ?? String(getPath(cfg, "roles.conductor.agent") ?? "claude");
85
+ const user = (getPath(cfg, `agents.${agent}.conductor`) ?? {});
86
+ const base = CONDUCTORS[agent];
87
+ const def = { ...base, ...user, models: { ...base?.models, ...user.models } };
88
+ const template = a.resume ? def.resume : def.run;
89
+ if (!template)
90
+ throw new Error(`orq conductor: ${agent} needs agents.${agent}.conductor.${a.resume ? "resume" : "run"}`);
91
+ const same = !a.agent || a.agent === getPath(cfg, "roles.conductor.agent");
92
+ const account = a.account || (same ? String(getPath(cfg, "roles.conductor.account") ?? "") : "");
93
+ const model = a.model || (same ? String(getPath(cfg, "roles.conductor.model") ?? "") : "");
94
+ const vars = { dir: ws, instructions: file, name: sessionName(ws, name), session: a.resume,
95
+ account: account === "-" ? "" : account, model: def.models[model] ?? model,
96
+ prompt_text: `Read ${JSON.stringify(file)} for your conductor instructions and follow them. Read AGENTS.md for the issue playbook. Read ORQ.md for the workspace map; in an older workspace without ORQ.md, read CLAUDE.md.` };
97
+ const env = { ...process.env };
98
+ for (const [k, v] of Object.entries(def.env ?? {}))
99
+ env[k] = expandString(v, vars);
100
+ if (account && account !== "-") {
101
+ const custom = def.accounts?.[account];
102
+ if (custom)
103
+ Object.assign(env, custom);
104
+ else if (agent === "claude") {
105
+ const accounts = claudeAccounts(), found = accounts.find((x) => x.name === account);
106
+ if (!found)
107
+ throw new Error(`account '${account}' does not exist. Accounts: ${accounts.map((x) => `${x.name} ${x.dir}`).join(", ")}`);
108
+ env.CLAUDE_CONFIG_DIR = found.dir;
109
+ }
110
+ else
111
+ throw new Error(`orq conductor: account '${account}' needs agents.${agent}.conductor.accounts.${account}`);
112
+ }
113
+ env.ORQ_CONDUCTOR_AGENT = agent;
114
+ env.ORQ_CONDUCTOR_ACCOUNT = account && account !== "-" ? account : "default";
115
+ return { agent, argv: expandTemplate(template, vars), env };
116
+ }
43
117
  /** The workspace folder: cwd when it holds .orquestra-conductor.md, else $CONDUCTOR_WS (resolved); undefined if neither. */
44
118
  export function findWorkspace(cwd, env, exists = existsSync) {
45
119
  if (exists(join(cwd, ".orquestra-conductor.md")))
@@ -53,7 +127,13 @@ export function claudeArgs(ws, a, file, name = "") {
53
127
  ...(a.model ? ["--model", modelId(a.model)] : []), "--append-system-prompt-file", file];
54
128
  }
55
129
  export async function conductor(argv) {
56
- const a = parseArgs(argv);
130
+ if (argv.includes("--help") || argv.includes("-h")) {
131
+ console.log(USAGE);
132
+ return 0;
133
+ }
134
+ const ws = findWorkspace(process.cwd(), process.env);
135
+ const { cfg } = loadConfig(ws);
136
+ const a = parseArgs(argv, [...Object.keys(CONDUCTORS), ...Object.keys((cfg.agents ?? {}))], claudeAccounts().map((a) => a.name));
57
137
  if (a.help) {
58
138
  console.log(USAGE);
59
139
  return 0;
@@ -62,7 +142,6 @@ export async function conductor(argv) {
62
142
  console.error(a.error);
63
143
  return 2;
64
144
  }
65
- const ws = findWorkspace(process.cwd(), process.env);
66
145
  if (!ws) {
67
146
  console.error(m("no .orquestra-conductor.md here: cd into a workspace (orq setup --workspace <dir>) or set CONDUCTOR_WS", "não há .orquestra-conductor.md aqui: entre num workspace (orq setup --workspace <pasta>) ou defina CONDUCTOR_WS"));
68
147
  return 2;
@@ -72,34 +151,45 @@ export async function conductor(argv) {
72
151
  console.error(`${m("conductor instructions not found", "instruções do conductor não encontradas")}: ${file}`);
73
152
  return 2;
74
153
  }
75
- const env = { ...process.env };
76
- if (a.account && a.account !== "-") {
77
- const accounts = claudeAccounts(), found = accounts.find((x) => x.name === a.account);
78
- if (!found) {
79
- console.error(m(`account '${a.account}' does not exist. Accounts:`, `a conta '${a.account}' não existe. Contas:`));
80
- for (const x of accounts)
81
- console.error(`${x.name} ${x.dir}`);
82
- return 1;
83
- }
84
- env.CLAUDE_CONFIG_DIR = found.dir;
154
+ const name = (process.env.HERDR_PANE_ID ? herdrName(process.env.HERDR_PANE_ID) : "") || process.env.ORQ_CONDUCTOR_ID || "";
155
+ let command;
156
+ try {
157
+ command = launch(cfg, a, ws, file, name);
158
+ }
159
+ catch (e) {
160
+ console.error(e.message);
161
+ return 1;
85
162
  }
163
+ const { env } = command;
164
+ if (!command.argv.length || !onPath(command.argv[0])) {
165
+ console.error(`orq conductor: executable not found: ${command.argv[0] ?? "(empty template)"}`);
166
+ return 1;
167
+ }
168
+ if (command.agent !== "claude")
169
+ await instructions(["agents", "-w", ws]);
86
170
  if (process.env.HERDR_ENV === "1" && process.env.HERDR_PANE_ID)
87
171
  spawnSync("herdr", ["pane", "rename", process.env.HERDR_PANE_ID, "conductor"], { stdio: "ignore", timeout: 5_000 });
88
172
  process.chdir(ws);
89
173
  // Registration (.orquestra/.conductors) is a herdr thing (keyed by pane id; `claim --register` does nothing elsewhere),
90
174
  // so only inside herdr. claim reads the account from this process's env (best effort, silent).
91
- const saved = process.env.CLAUDE_CONFIG_DIR;
92
- if (env.CLAUDE_CONFIG_DIR)
93
- process.env.CLAUDE_CONFIG_DIR = env.CLAUDE_CONFIG_DIR;
94
- if (process.env.HERDR_PANE_ID)
95
- try {
96
- await claim(["-w", ws, "--register"]);
175
+ const keys = ["CLAUDE_CONFIG_DIR", "ORQ_CONDUCTOR_AGENT", "ORQ_CONDUCTOR_ACCOUNT"];
176
+ const saved = keys.map((k) => process.env[k]);
177
+ try {
178
+ for (const k of keys) {
179
+ if (env[k] === undefined)
180
+ delete process.env[k];
181
+ else
182
+ process.env[k] = env[k];
97
183
  }
98
- catch { /* best effort */ }
99
- if (saved === undefined)
100
- delete process.env.CLAUDE_CONFIG_DIR;
101
- else
102
- process.env.CLAUDE_CONFIG_DIR = saved;
103
- const name = (process.env.HERDR_PANE_ID ? herdrName(process.env.HERDR_PANE_ID) : "") || process.env.ORQ_CONDUCTOR_ID || "";
104
- return runCapped("claude", claudeArgs(ws, a, file, name), { mode: "off", env, cwd: ws, quote: true, tty: true });
184
+ if (process.env.HERDR_PANE_ID)
185
+ await claim(["-w", ws, "--register"]);
186
+ }
187
+ catch { /* best effort */ }
188
+ finally {
189
+ keys.forEach((k, i) => { if (saved[i] === undefined)
190
+ delete process.env[k];
191
+ else
192
+ process.env[k] = saved[i]; });
193
+ }
194
+ return runCapped(command.argv[0], command.argv.slice(1), { mode: "off", env, cwd: ws, quote: true, tty: true });
105
195
  }
@@ -7,6 +7,7 @@ import { m } from "../i18n.js";
7
7
  import { issueFromName, issuePattern, issuesWithLiveAgent } from "../issue.js";
8
8
  import { alive } from "../lock.js";
9
9
  import { stateDir } from "../paths.js";
10
+ import { describe, stopWorktreeProcesses, worktreeProcesses } from "../worktree-processes.js";
10
11
  import { aliveOwner } from "./claim.js";
11
12
  export const USAGE = "orq gc [-w <workspace>] [--yes] [--days N] [--json] remove merged issue worktrees and stale agent state";
12
13
  const git = (cwd, ...args) => spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
@@ -168,20 +169,24 @@ export async function gc(argv, clock = Date.now) {
168
169
  if (ghUnavailable)
169
170
  console.error(m("NOTE: gh unavailable or failed; PR signals were skipped", "NOTA: gh indisponível ou falhou; sinais de PR ignorados"));
170
171
  let failed = false;
171
- if (yes)
172
- for (const row of rows.filter((r) => r.verdict === "REMOVABLE")) {
173
- const removed = git(join(ws, row.repo), "worktree", "remove", row.path);
174
- if (removed.status !== 0) {
175
- failed = true;
176
- row.reason += `; ${m("worktree removal failed", "falha ao remover worktree")}`;
177
- continue;
178
- }
179
- if (git(join(ws, row.repo), "branch", "-D", row.branch).status !== 0) {
180
- failed = true;
181
- row.reason += `; ${m("branch deletion failed", "falha ao excluir branch")}`;
182
- }
183
- git(join(ws, row.repo), "worktree", "prune");
172
+ for (const row of rows.filter((r) => r.verdict === "REMOVABLE")) {
173
+ const pids = yes ? stopWorktreeProcesses(row.path) : worktreeProcesses(row.path).map(describe);
174
+ if (pids.length)
175
+ row.reason += `; ${yes ? m("stopped processes", "processos encerrados") : m("processes to stop", "processos a encerrar")}: ${pids.join(", ")}`;
176
+ if (!yes)
177
+ continue;
178
+ const removed = git(join(ws, row.repo), "worktree", "remove", row.path);
179
+ if (removed.status !== 0) {
180
+ failed = true;
181
+ row.reason += `; ${m("worktree removal failed", "falha ao remover worktree")}`;
182
+ continue;
184
183
  }
184
+ if (git(join(ws, row.repo), "branch", "-D", row.branch).status !== 0) {
185
+ failed = true;
186
+ row.reason += `; ${m("branch deletion failed", "falha ao excluir branch")}`;
187
+ }
188
+ git(join(ws, row.repo), "worktree", "prune");
189
+ }
185
190
  const stale = staleAgentStates(ws, days, clock());
186
191
  if (yes)
187
192
  for (const f of stale)