@irythill/orq 0.8.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 +39 -10
- package/README.pt-BR.md +39 -10
- package/dist/commands/claim.js +3 -1
- package/dist/commands/completion.js +3 -2
- package/dist/commands/conductor.js +123 -33
- package/dist/commands/gc.js +18 -13
- package/dist/commands/instructions.js +54 -12
- package/dist/commands/pick.js +2 -0
- package/dist/commands/setup.js +19 -6
- package/dist/harness.js +69 -0
- package/dist/install.js +24 -8
- package/dist/pick.js +4 -2
- package/dist/suggest.js +1 -1
- package/dist/testrun.js +2 -0
- package/dist/worktree-processes.js +102 -0
- package/package.json +1 -1
- package/skills/issue/SKILL.md +19 -13
- package/skills/orq-setup/SKILL.md +3 -3
- package/templates/conductor.instructions.md +12 -9
- package/templates/workspace.CLAUDE.md +2 -2
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** (
|
|
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 (
|
|
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`);
|
|
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 `
|
|
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).
|
|
@@ -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
|
-
# [
|
|
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.
|
|
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
|
|
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
|
|
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** (
|
|
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 (
|
|
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`);
|
|
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 `
|
|
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
|
|
|
@@ -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
|
-
# [
|
|
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
|
|
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
|
|
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
|
|
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) |
|
package/dist/commands/claim.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
/**
|
|
22
|
-
export
|
|
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 === "
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
}
|
package/dist/commands/gc.js
CHANGED
|
@@ -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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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)
|
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
// claude <workspace>/.claude/skills/issue/SKILL.md (Claude Code loads it as the /issue skill)
|
|
3
3
|
// agents <workspace>/AGENTS.md (Codex, OpenCode and others that read AGENTS.md)
|
|
4
4
|
// AGENTS.md may hold the user's own text: orq only replaces its block between the orq:issue markers.
|
|
5
|
-
// orq instructions claude|agents [-w <workspace>] [--print]
|
|
5
|
+
// orq instructions claude|agents [-w <workspace>] [--conductor <agent>] [--print]
|
|
6
|
+
// orq instructions strategy [-w <workspace>] [--conductor <agent>] [--json] which wait/resume strategy applies (#119)
|
|
6
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
8
|
import { join, resolve } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import { writeAtomic } from "../fsutil.js";
|
|
11
|
+
import { getPath, loadConfig } from "../config.js";
|
|
12
|
+
import { capabilities, loopTimeout, strategy, strategyText } from "../harness.js";
|
|
10
13
|
import { m } from "../i18n.js";
|
|
11
|
-
export const USAGE =
|
|
14
|
+
export const USAGE = `orq instructions claude|agents [-w <workspace>] [--conductor <agent>] [--print] write the /issue playbook for the conductor
|
|
15
|
+
orq instructions strategy [-w <workspace>] [--conductor <agent>] [--json] show the conductor's wait/resume strategy`;
|
|
12
16
|
const SRC = fileURLToPath(new URL("../../skills/issue/SKILL.md", import.meta.url));
|
|
13
17
|
const BEGIN = "<!-- orq:issue begin (generated by `orq instructions agents`; edits inside are overwritten) -->";
|
|
14
18
|
const END = "<!-- orq:issue end -->";
|
|
15
|
-
/** The playbook for harnesses without skills: no frontmatter, a trigger line, and
|
|
16
|
-
export function agentsBlock(skill) {
|
|
19
|
+
/** The playbook for harnesses without skills: no frontmatter, a trigger line, and the conductor's waiting strategy. */
|
|
20
|
+
export function agentsBlock(skill, waiting = strategyText("unknown", capabilities("unknown"))) {
|
|
17
21
|
const body = skill.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
|
|
18
22
|
return `${BEGIN}
|
|
19
23
|
When the user says "/issue <ID>", "take issue X" or "run ABC-123", follow this playbook.
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
Read \`ORQ.md\` for the workspace map and orchestration settings (legacy workspaces: \`CLAUDE.md\`).
|
|
25
|
+
|
|
26
|
+
${waiting}
|
|
22
27
|
|
|
23
28
|
${body}
|
|
24
29
|
${END}
|
|
@@ -31,15 +36,30 @@ export function mergeBlock(cur, block) {
|
|
|
31
36
|
return cur.slice(0, a) + block.trimEnd() + cur.slice(b + END.length);
|
|
32
37
|
return cur ? `${cur.trimEnd()}\n\n${block}` : block;
|
|
33
38
|
}
|
|
39
|
+
/** Conductor agent: --conductor, else roles.conductor.agent, else claude. */
|
|
40
|
+
function conductorOf(cfg, flag) {
|
|
41
|
+
return flag || String(getPath(cfg, "roles.conductor.agent") ?? "claude");
|
|
42
|
+
}
|
|
43
|
+
/** AGENTS.md is read by non-Claude harnesses: a Claude conductor's wake-up must not leak into it unless asked for. */
|
|
44
|
+
function agentsWaiting(agent, caps, cfg, flag) {
|
|
45
|
+
if (agent === "claude" && !flag)
|
|
46
|
+
return strategyText("AGENTS.md harness", capabilities("unknown", cfg, process.env));
|
|
47
|
+
return strategyText(agent, caps);
|
|
48
|
+
}
|
|
34
49
|
export async function instructions(argv) {
|
|
35
|
-
let ws = process.cwd(), print = false, fmt;
|
|
50
|
+
let ws = process.cwd(), print = false, json = false, fmt, who;
|
|
36
51
|
for (let i = 0; i < argv.length; i++) {
|
|
37
|
-
|
|
52
|
+
const a = argv[i];
|
|
53
|
+
if (a === "-w")
|
|
38
54
|
ws = resolve(argv[++i]);
|
|
39
|
-
else if (
|
|
55
|
+
else if (a === "--print")
|
|
40
56
|
print = true;
|
|
41
|
-
else if (
|
|
42
|
-
|
|
57
|
+
else if (a === "--json")
|
|
58
|
+
json = true;
|
|
59
|
+
else if (a === "--conductor" && argv[i + 1])
|
|
60
|
+
who = argv[++i];
|
|
61
|
+
else if (a === "claude" || a === "agents" || a === "strategy")
|
|
62
|
+
fmt = a;
|
|
43
63
|
else {
|
|
44
64
|
console.error(USAGE);
|
|
45
65
|
return 2;
|
|
@@ -49,13 +69,35 @@ export async function instructions(argv) {
|
|
|
49
69
|
console.error(USAGE);
|
|
50
70
|
return 2;
|
|
51
71
|
}
|
|
72
|
+
let cfg = {};
|
|
73
|
+
try {
|
|
74
|
+
cfg = loadConfig(ws).cfg;
|
|
75
|
+
}
|
|
76
|
+
catch { /* no config: built-in defaults */ }
|
|
77
|
+
const agent = conductorOf(cfg, who), caps = capabilities(agent, cfg, process.env), s = strategy(caps);
|
|
78
|
+
if (fmt === "strategy") {
|
|
79
|
+
const fallback = s === "background-task" ? "foreground-loop" : "none";
|
|
80
|
+
if (json) {
|
|
81
|
+
console.log(JSON.stringify({ conductor: agent, strategy: s, fallback, waitTimeout: loopTimeout(caps), capabilities: caps }));
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
console.log(`conductor ${agent}: ${s}`);
|
|
85
|
+
console.log(` background=${caps.background} resume_on_completion=${caps.resumeOnCompletion} foreground_wait=${caps.foregroundWait} prompt_delivery=${caps.promptDelivery} foreground_timeout=${caps.foregroundTimeout}s`);
|
|
86
|
+
console.log(s === "background-task"
|
|
87
|
+
? m(" ends its turn after dispatch; orq wait in the background wakes it (fallback: foreground loop)", " encerra o turno após o dispatch; o orq wait em background o acorda (fallback: loop em foreground)")
|
|
88
|
+
: s === "manual"
|
|
89
|
+
? m(" cannot continue by itself: reports pending prefixes and waits for the user's message", " não continua sozinho: informa os prefixos pendentes e espera a mensagem do usuário")
|
|
90
|
+
: m(` stays active: orq wait --any --timeout ${loopTimeout(caps)} in a loop; it does NOT resume by itself after ending a turn`, ` fica ativo: orq wait --any --timeout ${loopTimeout(caps)} em loop; NÃO retoma sozinho depois de encerrar o turno`));
|
|
91
|
+
console.log(m(` override: agents.${agent}.conductor.capabilities`, ` ajuste: agents.${agent}.conductor.capabilities`));
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
52
94
|
if (!existsSync(SRC)) {
|
|
53
95
|
console.error(m(`playbook not found: ${SRC}`, `playbook não encontrado: ${SRC}`));
|
|
54
96
|
return 2;
|
|
55
97
|
}
|
|
56
98
|
const skill = readFileSync(SRC, "utf8");
|
|
57
99
|
const file = fmt === "claude" ? join(ws, ".claude", "skills", "issue", "SKILL.md") : join(ws, "AGENTS.md");
|
|
58
|
-
const text = fmt === "claude" ? skill : mergeBlock(existsSync(file) ? readFileSync(file, "utf8") : "", agentsBlock(skill));
|
|
100
|
+
const text = fmt === "claude" ? skill : mergeBlock(existsSync(file) ? readFileSync(file, "utf8") : "", agentsBlock(skill, agentsWaiting(agent, caps, cfg, who)));
|
|
59
101
|
if (print) {
|
|
60
102
|
process.stdout.write(text);
|
|
61
103
|
return 0;
|
package/dist/commands/pick.js
CHANGED
|
@@ -75,6 +75,8 @@ export async function pick(argv) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
const { cfg } = loadConfig(ws);
|
|
78
|
+
if (process.env.ORQ_CONDUCTOR_AGENT)
|
|
79
|
+
cfg._conductorAgent = process.env.ORQ_CONDUCTOR_AGENT;
|
|
78
80
|
const agents = (cfg.agents ?? {});
|
|
79
81
|
if (implAgent && !implVendor)
|
|
80
82
|
implVendor = vendorOf(implAgent, agents[implAgent]);
|
package/dist/commands/setup.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// orq setup --detect installed agent CLIs, as JSON (for a conductor driving the setup)
|
|
5
5
|
// orq setup --answers <file|-> [--write] proposal from answers JSON (see Answers in src/suggest.ts); --write saves it
|
|
6
6
|
// orq setup --install no questions: what orq found, and the skills linked for Claude Code
|
|
7
|
-
// orq setup --workspace <dir> [--conductor <agent>] .orquestra,
|
|
7
|
+
// orq setup --workspace <dir> [--conductor <agent>] .orquestra, ORQ.md map, /issue playbook in its format
|
|
8
8
|
// --preset economy|quality|single-agent
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import { copyFileSync, existsSync, readFileSync } from "node:fs";
|
|
@@ -18,6 +18,7 @@ import { getPath, loadConfig } from "../config.js";
|
|
|
18
18
|
import { m } from "../i18n.js";
|
|
19
19
|
import { environment, installSkills, prepareWorkspace } from "../install.js";
|
|
20
20
|
import { expand, userConfigFile } from "../paths.js";
|
|
21
|
+
import { setTomlString } from "../toml-set.js";
|
|
21
22
|
import { suggestRoles, toToml } from "../suggest.js";
|
|
22
23
|
export const USAGE = `orq setup [--preset economy|quality|single-agent] guided first configuration (roles, weights, memory)
|
|
23
24
|
orq setup --detect | --answers <file|-> [--write] | --install | --workspace <dir> [--conductor <agent>]`;
|
|
@@ -125,18 +126,30 @@ function workspace(dir, conductor) {
|
|
|
125
126
|
let c = conductor;
|
|
126
127
|
if (!c) {
|
|
127
128
|
try {
|
|
128
|
-
c = getPath(loadConfig().cfg, "roles.conductor.agent");
|
|
129
|
+
c = getPath(loadConfig(expand(dir)).cfg, "roles.conductor.agent");
|
|
129
130
|
}
|
|
130
131
|
catch { /* none */ }
|
|
131
132
|
}
|
|
132
|
-
c ??=
|
|
133
|
+
c ??= detect()[0] ?? "claude";
|
|
133
134
|
const d = expand(dir); // typed at the prompt: ~ is not expanded by any shell
|
|
134
135
|
console.log(`Workspace ${d}`);
|
|
135
|
-
for (const l of prepareWorkspace(d, c === "claude" ? "claude" : "agents"))
|
|
136
|
+
for (const l of prepareWorkspace(d, c === "claude" ? "claude" : "agents", undefined, c))
|
|
136
137
|
console.log(l);
|
|
138
|
+
if (c !== "agents") {
|
|
139
|
+
const toml = join(d, ".orq", "workspace.toml"), json = join(d, ".orq", "workspace.json");
|
|
140
|
+
const file = existsSync(toml) || !existsSync(json) ? toml : json;
|
|
141
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
142
|
+
if (file.endsWith(".json")) {
|
|
143
|
+
const cfg = text ? JSON.parse(text) : {};
|
|
144
|
+
cfg.roles = { ...cfg.roles, conductor: { ...cfg.roles?.conductor, agent: c } };
|
|
145
|
+
writeAtomic(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
146
|
+
}
|
|
147
|
+
else
|
|
148
|
+
writeAtomic(file, setTomlString(text, "roles.conductor", "agent", c));
|
|
149
|
+
}
|
|
137
150
|
}
|
|
138
|
-
const next = () => console.log(m(`Next: fill in the workspace
|
|
139
|
-
Board: orq board · quota: orq quota · completion: orq completion <shell>`, `Próximo: preencha o
|
|
151
|
+
const next = () => console.log(m(`Next: fill in the workspace ORQ.md, open your conductor in that folder and say "/issue <ID>".
|
|
152
|
+
Board: orq board · quota: orq quota · completion: orq completion <shell>`, `Próximo: preencha o ORQ.md do workspace, abra o seu conductor nessa pasta e diga "/issue <ID>".
|
|
140
153
|
Painel: orq board · cota: orq quota · autocompletar: orq completion <shell>`));
|
|
141
154
|
export async function setup(argv) {
|
|
142
155
|
let preset, answers, doWrite = false, ws, conductor;
|
package/dist/harness.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Conductor harness capabilities and the wait/resume strategy they allow (#119).
|
|
2
|
+
// A harness is where the conductor talks to the user (Claude Code, Codex, OpenCode...). Being able to start a process in
|
|
3
|
+
// the background is not the same as being woken up when it ends: only `resume_on_completion` lets the conductor end its
|
|
4
|
+
// turn after dispatching. Everything else keeps an active, bounded `orq wait --timeout` loop.
|
|
5
|
+
// Config (any field optional, overrides the built-in): agents.<agent>.conductor.capabilities =
|
|
6
|
+
// { background, resume_on_completion, foreground_wait, foreground_timeout, prompt_delivery }
|
|
7
|
+
import { getPath } from "./config.js";
|
|
8
|
+
const LOOP = { background: false, resumeOnCompletion: false, foregroundWait: true, foregroundTimeout: 240, promptDelivery: false };
|
|
9
|
+
/** Built-in knowledge. Unknown harnesses get the conservative loop: never promise a wake-up nobody verified. */
|
|
10
|
+
export const BUILTIN_CAPABILITIES = {
|
|
11
|
+
claude: { background: true, resumeOnCompletion: true, foregroundWait: true, foregroundTimeout: 540, promptDelivery: false },
|
|
12
|
+
codex: { ...LOOP },
|
|
13
|
+
opencode: { ...LOOP },
|
|
14
|
+
copilot: { ...LOOP },
|
|
15
|
+
antigravity: { ...LOOP },
|
|
16
|
+
};
|
|
17
|
+
const bool = (v, d) => typeof v === "boolean" ? v : d;
|
|
18
|
+
/** Capabilities of `agent` as conductor: built-in, then agents.<agent>.conductor.capabilities, then the environment. */
|
|
19
|
+
export function capabilities(agent, cfg = {}, env = {}) {
|
|
20
|
+
const base = BUILTIN_CAPABILITIES[agent] ?? LOOP;
|
|
21
|
+
const u = (getPath(cfg, `agents.${agent}.conductor.capabilities`) ?? {});
|
|
22
|
+
const t = Number(u.foreground_timeout);
|
|
23
|
+
return {
|
|
24
|
+
background: bool(u.background, base.background),
|
|
25
|
+
resumeOnCompletion: bool(u.resume_on_completion, base.resumeOnCompletion),
|
|
26
|
+
foregroundWait: bool(u.foreground_wait, base.foregroundWait),
|
|
27
|
+
foregroundTimeout: Number.isFinite(t) && t > 0 ? Math.floor(t) : base.foregroundTimeout,
|
|
28
|
+
promptDelivery: bool(u.prompt_delivery, base.promptDelivery || env.HERDR_ENV === "1"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Ending the turn after dispatch is only allowed with a channel that wakes the conductor. */
|
|
32
|
+
export function strategy(c) {
|
|
33
|
+
if (c.background && c.resumeOnCompletion)
|
|
34
|
+
return "background-task";
|
|
35
|
+
return c.foregroundWait ? "foreground-loop" : "manual";
|
|
36
|
+
}
|
|
37
|
+
/** Timeout for `orq wait` inside one foreground call: leave a margin under the harness limit. */
|
|
38
|
+
export const loopTimeout = (c) => Math.max(30, c.foregroundTimeout - 30);
|
|
39
|
+
/** The "Waiting strategy" section embedded in the generated conductor instructions. */
|
|
40
|
+
export function strategyText(agent, c) {
|
|
41
|
+
const herdr = "In herdr (`HERDR_ENV=1`) the done line is typed into your pane by herdr: that is your wake-up; dispatch, report in one line and end your turn.";
|
|
42
|
+
if (strategy(c) === "background-task")
|
|
43
|
+
return `## Waiting strategy: background task (${agent})
|
|
44
|
+
${herdr}
|
|
45
|
+
Elsewhere: after dispatching, run \`orq wait --any <prefix>...\` as a **background task** of your harness and end your
|
|
46
|
+
turn. Its exit starts your next turn; its output is the done line. Wait again for the prefixes still pending.`;
|
|
47
|
+
if (strategy(c) === "manual")
|
|
48
|
+
return `## Waiting strategy: manual (${agent})
|
|
49
|
+
${herdr}
|
|
50
|
+
Elsewhere your harness can neither wake you when a child ends nor keep a foreground wait open, so you cannot continue
|
|
51
|
+
by yourself. After dispatching, tell the user plainly: which prefixes are pending, that you will **not** continue on
|
|
52
|
+
your own, and that they resume you by messaging you (e.g. "status") once \`orq wait --any <pending prefixes>\` has
|
|
53
|
+
returned. On that message, check \`<prefix>.done\` and act; never dispatch a child again just because it is still running.`;
|
|
54
|
+
const t = loopTimeout(c);
|
|
55
|
+
return `## Waiting strategy: foreground loop (${agent})
|
|
56
|
+
${herdr}
|
|
57
|
+
Elsewhere your harness does **not** resume you when a background process ends, so never end your turn expecting a
|
|
58
|
+
wake-up. After dispatching, report in one line, then loop:
|
|
59
|
+
1. Run \`orq wait --any --timeout ${t} <every pending prefix>\` in the **foreground** (if your shell tool has a timeout
|
|
60
|
+
setting, give it more than ${t} s; if the tool still kills the command, use a shorter \`--timeout\`). If the tool
|
|
61
|
+
returns a running session/process id instead of an exit code, keep polling **that same** session until it exits;
|
|
62
|
+
don't start another \`orq wait\` while it runs.
|
|
63
|
+
2. Exit 0/1 = a done line: act on it (verify, review, fix) and keep looping over what is still pending.
|
|
64
|
+
3. Exit 124 = nothing finished yet: give the user a one-line status and go back to 1. A timeout is not a failure:
|
|
65
|
+
never dispatch the same child again because of it (\`<prefix>.done\` missing = still running).
|
|
66
|
+
The loop is the one allowed blocking wait. If the user writes in between, answer, then resume the loop. If you must end
|
|
67
|
+
your turn with children still pending, say plainly that you will **not** continue by yourself and that the user has to
|
|
68
|
+
message you (e.g. "status") to resume.`;
|
|
69
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// skills orq's skills (skills/*/SKILL.md, shipped in the package) linked into ~/.claude/skills and each Claude
|
|
3
3
|
// account's skills folder — a junction on Windows (no admin rights), a copy if linking fails. A real
|
|
4
4
|
// folder, or a link to another live copy (a clone of the repo), is never touched.
|
|
5
|
-
// workspace <folder>/.orquestra, .worktrees,
|
|
5
|
+
// workspace <folder>/.orquestra, .worktrees, ORQ.md map, pointers and the /issue playbook
|
|
6
6
|
// in the conductor's format (orq instructions)
|
|
7
7
|
// environment what orq found: git, terminal host, memory cap, keyring, the shell's completion line
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
@@ -11,7 +11,9 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { basename, join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { claudeAccounts } from "./accounts.js";
|
|
14
|
+
import { loadConfig } from "./config.js";
|
|
14
15
|
import { writeAtomic } from "./fsutil.js";
|
|
16
|
+
import { capabilities, strategyText } from "./harness.js";
|
|
15
17
|
import { m } from "./i18n.js";
|
|
16
18
|
import { backend } from "./keyring.js";
|
|
17
19
|
import { hasKernelCap } from "./process.js";
|
|
@@ -77,25 +79,39 @@ export function installSkills(roots = skillRoots()) {
|
|
|
77
79
|
return out;
|
|
78
80
|
}
|
|
79
81
|
/** Prepare a conductor's workspace folder; `fmt` = claude (SKILL.md) | agents (AGENTS.md). */
|
|
80
|
-
export function prepareWorkspace(dir, fmt, name = basename(resolve(dir))) {
|
|
82
|
+
export function prepareWorkspace(dir, fmt, name = basename(resolve(dir)), conductor = fmt === "claude" ? "claude" : "unknown") {
|
|
81
83
|
const ws = resolve(dir), out = [];
|
|
82
84
|
for (const d of [".orquestra", ".worktrees"])
|
|
83
85
|
mkdirSync(join(ws, d), { recursive: true });
|
|
84
|
-
const map = join(ws, "CLAUDE.md");
|
|
86
|
+
const map = join(ws, "ORQ.md"), legacy = join(ws, "CLAUDE.md");
|
|
85
87
|
if (!existsSync(map)) {
|
|
86
|
-
const t =
|
|
87
|
-
|
|
88
|
+
const t = existsSync(legacy) ? readFileSync(legacy, "utf8") :
|
|
89
|
+
readFileSync(join(PKG, "templates", "workspace.CLAUDE.md"), "utf8").split("{{WORKSPACE}}").join(ws).split("{{NAME}}").join(name);
|
|
90
|
+
writeAtomic(map, t);
|
|
88
91
|
out.push(m(` created ${map} — fill in repos, group, base branch, Plane and issue standard`, ` criado ${map} — preencha repos, grupo, branch base, Plane e padrão de issue`));
|
|
89
92
|
}
|
|
90
93
|
else
|
|
91
94
|
out.push(m(` ok ${map} (kept)`, ` ok ${map} (mantido)`));
|
|
95
|
+
if (!existsSync(legacy))
|
|
96
|
+
writeAtomic(legacy, "# orq workspace\n\nRead `ORQ.md` for the workspace map and orchestration settings.\n");
|
|
92
97
|
// The conductor's instructions (what `orq conductor` looks for): regenerated on every run.
|
|
93
98
|
const ci = join(ws, ".orquestra-conductor.md");
|
|
94
99
|
const t = readFileSync(join(PKG, "templates", "conductor.instructions.md"), "utf8");
|
|
95
|
-
|
|
100
|
+
let cfg = {};
|
|
101
|
+
try {
|
|
102
|
+
cfg = loadConfig(ws).cfg;
|
|
103
|
+
}
|
|
104
|
+
catch { /* no config yet */ }
|
|
105
|
+
const waiting = strategyText(conductor, capabilities(conductor, cfg, process.env));
|
|
106
|
+
writeAtomic(ci, t.split("{{WORKSPACE}}").join(ws).split("{{NAME}}").join(name).split("{{WAIT_STRATEGY}}").join(waiting));
|
|
96
107
|
out.push(` ${m("wrote", "escrevi")} ${ci}`);
|
|
97
|
-
const
|
|
98
|
-
|
|
108
|
+
for (const format of fmt === "claude" ? ["agents", "claude"] : ["agents"]) {
|
|
109
|
+
const r = spawnSync(process.execPath, [join(PKG, "dist", "cli.js"), "instructions", format, "-w", ws,
|
|
110
|
+
...(format === "agents" && conductor !== "claude" && conductor !== "unknown" ? ["--conductor", conductor] : [])], { encoding: "utf8" });
|
|
111
|
+
if (r.status !== 0)
|
|
112
|
+
throw new Error(r.stderr || `orq instructions ${format} failed`);
|
|
113
|
+
out.push(` ${r.stdout.trim()}`);
|
|
114
|
+
}
|
|
99
115
|
return out;
|
|
100
116
|
}
|
|
101
117
|
const SHELLS = ["bash", "zsh", "fish", "powershell"];
|
package/dist/pick.js
CHANGED
|
@@ -145,6 +145,8 @@ function takesPrompt(cfg, name) {
|
|
|
145
145
|
export function pick(inp) {
|
|
146
146
|
const { cfg, role, level } = inp;
|
|
147
147
|
const agents = (cfg.agents ?? {});
|
|
148
|
+
const conductor = String(cfg._conductorAgent ?? cfg.roles?.conductor?.agent ?? "");
|
|
149
|
+
const conductorHeadroom = Number(cfg.conductor?.headroom ?? 0);
|
|
148
150
|
const pol = (cfg.policy ?? {});
|
|
149
151
|
const [base5, baseWeek] = limitsFor(pol, level);
|
|
150
152
|
const w = weights(cfg);
|
|
@@ -201,7 +203,7 @@ export function pick(inp) {
|
|
|
201
203
|
}
|
|
202
204
|
// Quota: an agent with several accounts (claude) prefers one not projecting past the limit, then the least used.
|
|
203
205
|
const qn = a.quota ?? [];
|
|
204
|
-
const headroom = Math.max(0, Number(a.headroom ?? 0));
|
|
206
|
+
const headroom = Math.max(0, Number(a.headroom ?? (name === conductor ? conductorHeadroom : 0)));
|
|
205
207
|
const limit5 = base5 - headroom, limitWeek = baseWeek - headroom;
|
|
206
208
|
const accs = qn.filter((n) => !inp.blocks.has(n)).map((n) => [n, n in inp.quota ? inp.quota[n] : -1]);
|
|
207
209
|
if (qn.length && !accs.length) {
|
|
@@ -329,7 +331,7 @@ export function pick(inp) {
|
|
|
329
331
|
return { v, txt: `${label}=${v.toFixed(2)}` + (obs === undefined ? "" : `(${prior.toFixed(2)}→${obs.toFixed(2)})`) };
|
|
330
332
|
};
|
|
331
333
|
const cands = pass.map((c) => {
|
|
332
|
-
const prior = Number(c.a.prior ?? 0.6), n = c.st?.n ?? 0, reserve = Boolean(c.a.reserve);
|
|
334
|
+
const prior = Number(c.a.prior ?? 0.6), n = c.st?.n ?? 0, reserve = Boolean(c.a.reserve ?? c.agent === conductor);
|
|
333
335
|
const appr = role === "impl" && c.st?.approved != null && c.st.n_appr >= 3;
|
|
334
336
|
const deliv = appr ? c.st.approved / 100 : c.st ? c.st.ok / 100 : undefined;
|
|
335
337
|
const d = axis(m("delivery", "entrega"), prior, deliv, appr ? c.st.n_appr : n);
|
package/dist/suggest.js
CHANGED
|
@@ -98,7 +98,7 @@ export function toToml(p) {
|
|
|
98
98
|
if (a.quota)
|
|
99
99
|
out.push(`quota = ${list(a.quota)}`);
|
|
100
100
|
if (a.reserve)
|
|
101
|
-
out.push("reserve
|
|
101
|
+
out.push("# reserve follows roles.conductor.agent (or the running conductor)");
|
|
102
102
|
out.push("# models.<role>.<level> = \"model[@effort]\" — without models, the CLI's default model is used", "");
|
|
103
103
|
}
|
|
104
104
|
for (const [r, v] of Object.entries(p.roles)) {
|
package/dist/testrun.js
CHANGED
|
@@ -12,6 +12,7 @@ import { acquire, release } from "./lock.js";
|
|
|
12
12
|
import { handoffFile, notify } from "./notice.js";
|
|
13
13
|
import { killTree, parseSize, runCapped } from "./process.js";
|
|
14
14
|
import { closeOwnTab, detectHost, launch } from "./terminal.js";
|
|
15
|
+
import { stopWorktreeProcesses } from "./worktree-processes.js";
|
|
15
16
|
export const JS_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;
|
|
16
17
|
export const git = (dir, ...a) => {
|
|
17
18
|
const r = spawnSync("git", ["-C", dir, ...a], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
@@ -109,6 +110,7 @@ export function linkModules(from, to) {
|
|
|
109
110
|
* (and must never walk into the main clone's node_modules through it).
|
|
110
111
|
*/
|
|
111
112
|
export function dropWorktree(repo, dir) {
|
|
113
|
+
stopWorktreeProcesses(dir);
|
|
112
114
|
const nm = join(dir, "node_modules");
|
|
113
115
|
try {
|
|
114
116
|
if (lstatSync(nm).isSymbolicLink() || process.platform === "win32")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Stop processes whose current working directory is inside a worktree before git removes its files.
|
|
2
|
+
import { readFileSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
function inside(dir, path) {
|
|
5
|
+
const rel = relative(dir, path);
|
|
6
|
+
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
7
|
+
}
|
|
8
|
+
function stat(pid) {
|
|
9
|
+
try {
|
|
10
|
+
return readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** orq itself and every ancestor (the shell or tmux pane that launched it), which must never be stopped. */
|
|
17
|
+
function ownChain() {
|
|
18
|
+
const chain = new Set();
|
|
19
|
+
for (let pid = process.pid; pid > 1 && !chain.has(pid);) {
|
|
20
|
+
chain.add(pid);
|
|
21
|
+
const ppid = Number(/\) \S+ (\d+)/.exec(stat(pid) ?? "")?.[1]);
|
|
22
|
+
if (!ppid)
|
|
23
|
+
break;
|
|
24
|
+
pid = ppid;
|
|
25
|
+
}
|
|
26
|
+
return chain;
|
|
27
|
+
}
|
|
28
|
+
/** Processes whose cwd is under `dir`, excluding orq and its ancestors; other platforms skip this check. */
|
|
29
|
+
export function worktreeProcesses(dir) {
|
|
30
|
+
if (process.platform !== "linux")
|
|
31
|
+
return [];
|
|
32
|
+
let root;
|
|
33
|
+
try {
|
|
34
|
+
root = realpathSync(dir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
root = resolve(dir);
|
|
38
|
+
}
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = readdirSync("/proc");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const skip = ownChain(), pids = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (!/^\d+$/.test(entry))
|
|
49
|
+
continue;
|
|
50
|
+
const pid = Number(entry);
|
|
51
|
+
if (skip.has(pid))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
if (inside(root, resolve(readlinkSync(`/proc/${entry}/cwd`))))
|
|
55
|
+
pids.push(pid);
|
|
56
|
+
}
|
|
57
|
+
catch { /* process exited or inaccessible */ }
|
|
58
|
+
}
|
|
59
|
+
return pids;
|
|
60
|
+
}
|
|
61
|
+
/** `pid (name)` for output, so the user sees what is (or would be) stopped. */
|
|
62
|
+
export function describe(pid) {
|
|
63
|
+
try {
|
|
64
|
+
return `${pid} (${readFileSync(`/proc/${pid}/comm`, "utf8").trim()})`;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return String(pid);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** SIGTERM matching processes, wait up to five seconds, then SIGKILL survivors. Returns `describe`d entries. */
|
|
71
|
+
export function stopWorktreeProcesses(dir) {
|
|
72
|
+
const pids = worktreeProcesses(dir), names = pids.map(describe);
|
|
73
|
+
for (const pid of pids)
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, "SIGTERM");
|
|
76
|
+
}
|
|
77
|
+
catch { /* already exited */ }
|
|
78
|
+
const deadline = Date.now() + 5_000;
|
|
79
|
+
while (Date.now() < deadline && pids.some(alive))
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
81
|
+
for (const pid of pids)
|
|
82
|
+
if (alive(pid))
|
|
83
|
+
try {
|
|
84
|
+
process.kill(pid, "SIGKILL");
|
|
85
|
+
}
|
|
86
|
+
catch { /* already exited */ }
|
|
87
|
+
return names;
|
|
88
|
+
}
|
|
89
|
+
function alive(pid) {
|
|
90
|
+
if (process.platform === "linux") {
|
|
91
|
+
const s = stat(pid);
|
|
92
|
+
if (s === undefined || /^\d+ \(.*\) Z /.test(s))
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, 0);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/package.json
CHANGED
package/skills/issue/SKILL.md
CHANGED
|
@@ -8,7 +8,7 @@ description: Runs a Plane issue end to end by orchestrating coding agents in her
|
|
|
8
8
|
You are the **orchestrator**: you plan, dispatch, follow up and report. **Do not implement anything yourself** (it
|
|
9
9
|
burns the conductor's model). Talk to the user in the user's language.
|
|
10
10
|
|
|
11
|
-
**Ecosystem configuration:**
|
|
11
|
+
**Ecosystem configuration:** in the folder named in your conductor instructions, read `ORQ.md` (legacy: `CLAUDE.md`). It defines
|
|
12
12
|
the repositories and their groups (= herdr workspace or just a label, `-g`), **each repo's base branch**, verification commands,
|
|
13
13
|
Plane projects (identifier → project_id), default assignee, labels/modules and MCP limitations. Read it before you
|
|
14
14
|
start. Below, `$WS` = workspace.
|
|
@@ -17,11 +17,17 @@ herdr (`HERDR_ENV=1`), in tmux windows inside tmux, else in the background (thei
|
|
|
17
17
|
still `<prefix>.out.md`; `-T`, the agent's own screen, needs herdr or tmux). Set `ORQ_CONDUCTOR_ID` to a fixed name per
|
|
18
18
|
conductor outside herdr.
|
|
19
19
|
- **Completion signal:** in herdr, one line `[HERDR-AGENT DONE] <title> status=ok|fail ... answer: <file>` is typed into
|
|
20
|
-
your pane after each child. Anywhere else no line arrives:
|
|
21
|
-
task** (Claude Code: Bash with `run_in_background`); its exit wakes you and its output is the line
|
|
20
|
+
your pane after each child. Anywhere else no line arrives: `orq wait <prefix>` prints
|
|
22
21
|
`[ORQ AGENT DONE] <title> status=ok|fail ... answer: <file>`. Treat both lines the same. Several children in parallel:
|
|
23
|
-
`orq wait --any <prefix1> <prefix2>` (one
|
|
24
|
-
|
|
22
|
+
`orq wait --any <prefix1> <prefix2>` (one finish per call; wait again for the rest).
|
|
23
|
+
- **Waiting strategy** (your conductor instructions name it; `orq instructions strategy` shows it). It depends on what
|
|
24
|
+
your harness can do, not on whether it can start a background process:
|
|
25
|
+
- *background task* — only when a finished background task starts a new turn by itself (Claude Code: Bash with
|
|
26
|
+
`run_in_background`): run `orq wait --any ...` that way and end your turn.
|
|
27
|
+
- *foreground loop* — every other harness (Codex, OpenCode, ...): stay active. Run
|
|
28
|
+
`orq wait --any --timeout <s> <pending prefixes>` in the foreground; exit 124 = nothing finished yet → one-line
|
|
29
|
+
status to the user and run it again (never re-dispatch on a timeout). Don't end your turn expecting a wake-up; if
|
|
30
|
+
you must, tell the user you won't continue until they message you.
|
|
25
31
|
- `orq verify` and `orq baseline` run like children (herdr tab, tmux window or background) and you wait for them the same
|
|
26
32
|
way, e.g. `orq wait $O/verify-<repo>`.
|
|
27
33
|
- Peeking at a child: `<prefix>.log` (in herdr also `herdr pane read $(cat <prefix>.pane) --source recent`).
|
|
@@ -42,7 +48,7 @@ Issue artifacts (prompts, notes) live in `$WS/.orquestra/<ID>/`.
|
|
|
42
48
|
- `mcp__plane__workitem` `retrieve_by_identifier` with `workitem_identifier: "<ID>"`; then `workitem_comment list`.
|
|
43
49
|
- Extract: goal, **acceptance criteria**, affected repos, links. No acceptance criteria → write a short proposal and
|
|
44
50
|
**ask the user** before dispatching.
|
|
45
|
-
- Issue missing assignee, priority, labels or module → **fill them in** following the workspace `
|
|
51
|
+
- Issue missing assignee, priority, labels or module → **fill them in** following the workspace `ORQ.md` "Issue
|
|
46
52
|
standard". The same standard applies to any new issue you create.
|
|
47
53
|
- Move it to **In Progress** (`workitem update` with the state_id from `state list`).
|
|
48
54
|
|
|
@@ -93,7 +99,7 @@ Write the prompt to `$WS/.orquestra/<ID>/impl-<repo>.md` (short: goal, acceptanc
|
|
|
93
99
|
verification commands, "base: origin/<BASE>; don't rebase/merge other branches; local commits, no push; commit
|
|
94
100
|
messages following <path of the repo's commit conventions, see step 7>").
|
|
95
101
|
Always include:
|
|
96
|
-
- **Repo checklist:** copy the repo's "Checklist" column from the workspace `
|
|
102
|
+
- **Repo checklist:** copy the repo's "Checklist" column from the workspace `ORQ.md`.
|
|
97
103
|
- **Decisions:** "end your final answer with a `## Decisions` section, at most 10 lines: what you chose and what you
|
|
98
104
|
ruled out, trade-offs, and what you left out on purpose and why". The reviewer reads it (step 5), so a deliberate
|
|
99
105
|
choice doesn't come back as a blocker.
|
|
@@ -102,7 +108,7 @@ Always include:
|
|
|
102
108
|
and report; after the rebase repeat lint/tsc". An agent without network (Codex) can't fetch: `orq agent` fetches before
|
|
103
109
|
the run and adds a note to its prompt, so it only rebases locally.
|
|
104
110
|
|
|
105
|
-
**Base = the repo's base branch from the workspace `
|
|
111
|
+
**Base = the repo's base branch from the workspace `ORQ.md`, always from the updated remote (`origin/<BASE>`).**
|
|
106
112
|
Worktree with `git worktree add` (not `herdr worktree create`: that opens a separate herdr workspace per worktree, and
|
|
107
113
|
children are grouped by the repo's `-g`).
|
|
108
114
|
```bash
|
|
@@ -139,7 +145,7 @@ orq agent $(cat $O/impl-<repo>.pick) -g <group> -t "<ID> impl <repo>" -d $WT -o
|
|
|
139
145
|
- Every child prompt ends with: "Don't try to talk to the conductor or use herdr; finish with your complete final
|
|
140
146
|
answer (that is what I read)."
|
|
141
147
|
- **Verification in the child: lint and tsc only, one command at a time** (the cap is 4 GB per agent and jest/vitest
|
|
142
|
-
blow it; tests are yours, step 4b): use the Verification column of the workspace `
|
|
148
|
+
blow it; tests are yours, step 4b): use the Verification column of the workspace `ORQ.md` and put in the prompt
|
|
143
149
|
"verify sequentially, never in parallel or watch mode: (1) lint only the changed files (`git diff --name-only
|
|
144
150
|
origin/<BASE>...HEAD`); (2) `NODE_OPTIONS=--max-old-space-size=2048 tsc --noEmit` for the project. Don't run tests".
|
|
145
151
|
Full suite only in CI (or a manual `orq baseline` if the user asks).
|
|
@@ -148,7 +154,7 @@ orq agent $(cat $O/impl-<repo>.pick) -g <group> -t "<ID> impl <repo>" -d $WT -o
|
|
|
148
154
|
arriving), run `orq notice` (delivery log) and `<prefix>.done` for that issue, then handle the failure; running
|
|
149
155
|
`orq wait`, `orq verify` or `orq agent` for the issue acknowledges it.
|
|
150
156
|
- **Never block waiting** (`herdr agent wait`, `pane wait-output`, `sleep`, `timeout`): completion arrives as a
|
|
151
|
-
`[HERDR-AGENT DONE]` line in your pane (herdr) or through
|
|
157
|
+
`[HERDR-AGENT DONE]` line in your pane (herdr) or through `orq wait` per your waiting strategy. Dispatch, report in one line, then end your turn (herdr / background task) or keep looping (foreground loop). "Done yet?" = `<prefix>.done`
|
|
152
158
|
exists (never `pgrep`).
|
|
153
159
|
- Investigation only (no code): "READ ONLY" prompt, in the repo itself, no worktree. `orq agent -r` (Claude:
|
|
154
160
|
`--permission-mode plan`; the others: throwaway worktree, no editing).
|
|
@@ -169,7 +175,7 @@ something is red, new or old, and never widen a baseline or allowlist to silence
|
|
|
169
175
|
outside the issue's domain: ask the user first (then it ships as its own PR, not as a note).
|
|
170
176
|
**Mandatory order every round (impl and each fix): child `ok` → verify → review.** Never dispatch the review (or an
|
|
171
177
|
"early" fix) while this round's verify is pending, even if it is queued behind another verify: wait for
|
|
172
|
-
the `... verify ...` done line
|
|
178
|
+
the `... verify ...` done line (end your turn or keep looping, per your waiting strategy).
|
|
173
179
|
|
|
174
180
|
## 5. Cross review (always another vendor)
|
|
175
181
|
```bash
|
|
@@ -265,7 +271,7 @@ rules live) commits and writes the PR. Find and cite in the prompt, by path, wha
|
|
|
265
271
|
high severity, `file:line` and a concrete failure, not already decided in the rounds, goes into a fix round (it
|
|
266
272
|
counts towards `policy.max_rounds`); the rest goes into the Plane comment or a follow-up issue. Don't re-request the
|
|
267
273
|
bot's review to "get it clean".
|
|
268
|
-
- **CI after every push:** `gh pr checks <n>` (no `--watch`; if pending,
|
|
274
|
+
- **CI after every push:** `gh pr checks <n>` (no `--watch`; if pending, check again on the next notice or loop iteration).
|
|
269
275
|
Red → `gh run view <id> --log-failed | tail -40`: error from the diff → fix round; error already on the base → create
|
|
270
276
|
a Plane issue (workspace standard) and tell the user. "Ready to merge" only with green checks or an explained failure
|
|
271
277
|
+ issue.
|
|
@@ -279,4 +285,4 @@ rules live) commits and writes the PR. Find and cite in the prompt, by path, wha
|
|
|
279
285
|
## Saving tokens
|
|
280
286
|
Short prompts (never paste the whole `CLAUDE.md` — the agent reads the repo's own). Reviews judge only the diff, but the prompt must say the reviewer **may open files for context** (cat, rg, git
|
|
281
287
|
show — read only): Codex read "review only the diff" as "use only the attached diff" and returned an empty review. Don't re-read
|
|
282
|
-
the whole issue at every step. Respect the MCP limitations listed in the workspace `
|
|
288
|
+
the whole issue at every step. Respect the MCP limitations listed in the workspace `ORQ.md`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orq-setup
|
|
3
|
-
description: Guided first-time setup of orq for a new developer — detects and smoke-tests the installed agent CLIs, asks which agents take which roles, models, Claude accounts and pick weights, writes ~/.config/orq/config.toml, then prepares a conductor workspace (orq setup --workspace + workspace
|
|
3
|
+
description: Guided first-time setup of orq for a new developer — detects and smoke-tests the installed agent CLIs, asks which agents take which roles, models, Claude accounts and pick weights, writes ~/.config/orq/config.toml, then prepares a conductor workspace (orq setup --workspace + workspace ORQ.md) and checks Plane. Use when the user says "/orq-setup", "set up orq", "configure orq" or just installed orq (npm i -g @irythill/orq, or install.sh).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /orq-setup — configure orq for this developer
|
|
@@ -85,10 +85,10 @@ Explain simply, then suggest the example's values and ask if they want to change
|
|
|
85
85
|
## 6. Workspace
|
|
86
86
|
- Ask for the folder that groups their repos (e.g. `~/Code/acme-platform`). After the OK:
|
|
87
87
|
`orq setup --workspace <folder> --conductor <the conductor agent>` — `.orquestra/`, `.worktrees/`, the map
|
|
88
|
-
`<folder>/
|
|
88
|
+
`<folder>/ORQ.md` from the template (never overwrites one) and the `/issue` playbook in the conductor's format
|
|
89
89
|
(`.claude/skills/issue/SKILL.md` for Claude Code, `AGENTS.md` for the others). Conductor in herdr (clone only):
|
|
90
90
|
`$ORQ/setup/new-conductor.sh <name> <folder>` also writes its instructions and permissions.
|
|
91
|
-
- Fill the repo table in `<folder>/
|
|
91
|
+
- Fill the repo table in `<folder>/ORQ.md` by inspecting each git repo inside it:
|
|
92
92
|
- base branch: `git -C <repo> symbolic-ref --short refs/remotes/origin/HEAD` (strip `origin/`); ask if the team
|
|
93
93
|
branches from another one (e.g. `dev`).
|
|
94
94
|
- group: the repo folder name in lower case (the herdr workspace or tmux group for its children).
|
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
You orchestrate agents in the `{{WORKSPACE}}` ecosystem, in herdr (`HERDR_ENV=1`) or in any other terminal.
|
|
4
4
|
Don't write code yourself. Opened by `orq conductor`. Talk to the user in the user's language.
|
|
5
|
-
1. Read `{{WORKSPACE}}/
|
|
6
|
-
|
|
5
|
+
1. Read `{{WORKSPACE}}/ORQ.md` (repos, groups, base branch, Plane, issue standard, MCP limitations).
|
|
6
|
+
In an older workspace without `ORQ.md`, read `{{WORKSPACE}}/CLAUDE.md`.
|
|
7
|
+
2. For every Plane issue, follow the `/issue <ID>` playbook (a skill or the generated block in `AGENTS.md`).
|
|
7
8
|
3. Never push, open a PR, merge or deploy unless the user asks. When unsure about scope or cost, ask.
|
|
8
9
|
4. **Children only through `orq agent`** (implementer, fix, review), with the agent chosen by `orq pick`; tests through
|
|
9
10
|
`orq verify` (it checks the base by itself; no `orq baseline` in the flow), as the skill says. NEVER
|
|
10
11
|
`herdr agent start` for children (no memory cap, no answer file); to watch the agent's own screen, `orq agent -T` (herdr or tmux).
|
|
11
12
|
5. **Completion = one single signal:** in herdr, the line `[HERDR-AGENT DONE] <title> status=ok|fail ... answer: <file>`
|
|
12
|
-
arriving in your pane; elsewhere, `orq wait <
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
arriving in your pane; elsewhere, `orq wait` printing `[ORQ AGENT DONE] <title> status=ok|fail ... answer: <file>`
|
|
14
|
+
(plus the `<prefix>.done` file either way), run as the **Waiting strategy** below says. When it arrives: read the
|
|
15
|
+
answer file and act. Nothing else means done.
|
|
15
16
|
6. **"Is it still running?"** is answered by the file only: `<prefix>.done` exists → finished; it doesn't → running;
|
|
16
17
|
check `<prefix>.log` (last write time) or, in herdr, peek at the pane: `herdr pane read $(cat <prefix>.pane) --source recent`.
|
|
17
18
|
NEVER `pgrep`/`ps` (they match their own command line).
|
|
18
|
-
7. **Never block waiting** (`herdr agent wait`, `pane wait-output`, `sleep`)
|
|
19
|
-
|
|
19
|
+
7. **Never block waiting** (`herdr agent wait`, `pane wait-output`, `sleep`) except through `orq wait` as the Waiting
|
|
20
|
+
strategy below says. End your turn after dispatching only when that strategy gives you a wake-up.
|
|
20
21
|
8. Worktrees always from `origin/<repo base>`, created with `git worktree add` (never `herdr worktree create`).
|
|
21
|
-
9. Between batches of unrelated issues, suggest
|
|
22
|
+
9. Between batches of unrelated issues, suggest a fresh conversation: state lives in Plane and in `.orquestra/`.
|
|
23
|
+
|
|
24
|
+
{{WAIT_STRATEGY}}
|
|
22
25
|
|
|
23
26
|
## How you are used
|
|
24
27
|
- **Dispatcher, not consultant.** Your job: issue → dispatch → report → PR/cleanup. Long questions about architecture,
|
|
@@ -26,6 +29,6 @@ Don't write code yourself. Opened by `orq conductor`. Talk to the user in the us
|
|
|
26
29
|
to a design session; come back with the decision recorded in the issue".
|
|
27
30
|
- **The issue is the source.** A decision made in chat → record it in Plane (description or comment) before
|
|
28
31
|
dispatching; the child's prompt comes from the issue, not from the conversation.
|
|
29
|
-
- **Context:** past ~150k tokens or after closing a batch of related issues → suggest
|
|
32
|
+
- **Context:** past ~150k tokens or after closing a batch of related issues → suggest a fresh conversation (state in Plane and in
|
|
30
33
|
`.orquestra/`).
|
|
31
34
|
- 1–3 line answers. Final issue report: 3–5 lines + the PR link.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ORQ.md — {{NAME}} ecosystem ({{WORKSPACE}})
|
|
2
2
|
|
|
3
3
|
This folder groups the ecosystem's repositories. Each repo has its own `CLAUDE.md`/`AGENTS.md` with commands and
|
|
4
4
|
rules — read the repo's own before touching it. This file only holds the map and the orchestration settings.
|
|
5
5
|
|
|
6
6
|
## Orchestration (read by the `/issue` skill)
|
|
7
7
|
- **Workspace:** `{{WORKSPACE}}`
|
|
8
|
-
- **Conductor:** opened with `orq conductor [account] [model] [-r <session-id>]` in this folder, in any terminal (inside a
|
|
8
|
+
- **Conductor:** opened with `orq conductor [agent] [account|-] [model] [-r <session-id>]` in this folder, in any terminal (inside a
|
|
9
9
|
herdr pane too; `herdr-conductor` still works there). Instructions in `.orquestra-conductor.md`; the **Group** in the
|
|
10
10
|
table below is the herdr workspace (`-g`) or just a label outside herdr.
|
|
11
11
|
Tests from the Verification column (jest/vitest) **don't run in the child**: the conductor runs `orq verify`
|