@welluable/orch 1.1.0 → 1.3.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 +224 -6
- package/agents/boundaries.js +26 -0
- package/agents/code-writer.js +5 -0
- package/agents/decomposer.js +62 -0
- package/agents/index.js +3 -0
- package/agents/integrator.js +41 -0
- package/agents/quick-fix.js +4 -0
- package/agents/test-writer.js +4 -0
- package/lib/agent.js +82 -6
- package/lib/commit.js +70 -0
- package/lib/config.js +115 -0
- package/lib/continue.js +132 -0
- package/lib/fanout.js +355 -0
- package/lib/file-tracker.js +89 -0
- package/lib/integrate.js +51 -0
- package/lib/job-lifecycle.js +53 -0
- package/lib/jobs.js +361 -0
- package/lib/parse-decomposition.js +33 -0
- package/lib/run-context.js +13 -1
- package/lib/stage-summary.js +55 -6
- package/lib/worktree.js +4 -2
- package/main.js +2739 -214
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,10 @@ npm install -g @welluable/orch
|
|
|
51
51
|
|
|
52
52
|
Make sure an agent CLI is on your `PATH` — orch defaults to `--agent cursor`
|
|
53
53
|
(the Cursor Agent CLI, command `agent`); `claude` and `agn` are also
|
|
54
|
-
supported.
|
|
54
|
+
supported. Pin a default with `orch config --agent <name>` so you don't need
|
|
55
|
+
`--agent` on every run (local `.orch/config` overrides global
|
|
56
|
+
`~/.orch/config`; CLI `--agent` still wins). See [Requirements](#requirements)
|
|
57
|
+
for details.
|
|
55
58
|
|
|
56
59
|
```bash
|
|
57
60
|
orch "fix the typo in the README"
|
|
@@ -123,6 +126,8 @@ you invoked `orch`, plus a persistent sibling git worktree and branch:
|
|
|
123
126
|
research.md
|
|
124
127
|
task.md
|
|
125
128
|
status.md
|
|
129
|
+
run.json # job state (state, phase, pid, ...); written for every run
|
|
130
|
+
orch.log # full stdout/stderr of the run; --detach only
|
|
126
131
|
|
|
127
132
|
<parent-of-repo>/<repo-name>-<slug> # worktree
|
|
128
133
|
orch/<slug> # branch
|
|
@@ -134,6 +139,11 @@ test-runner) run inside the worktree instead. The worktree is never deleted
|
|
|
134
139
|
automatically — it's left in place after the run so you can inspect,
|
|
135
140
|
continue, or merge the work whenever you're ready.
|
|
136
141
|
|
|
142
|
+
`run.json` is written for every invocation — default, `--quick`, `--ask`, and
|
|
143
|
+
`--detach` alike — so `orch list`/`status` always have something to show; see
|
|
144
|
+
[Headless runs](#headless-runs). `orch.log` is only written for `--detach`
|
|
145
|
+
runs, since foreground runs already stream their output to your terminal.
|
|
146
|
+
|
|
137
147
|
## Architecture
|
|
138
148
|
|
|
139
149
|
```text
|
|
@@ -161,14 +171,22 @@ agent CLI does all the actual reading and writing of files.
|
|
|
161
171
|
| `--quick` | Skips triage, runs `quick-fix` directly in the current tree; no artifacts, worktree, or commits. | You already know it's a small, direct edit. |
|
|
162
172
|
| `--ask` | Skips triage and all write pipelines; one read-only agent answers and orch prints the reply. | You want an answer about the codebase, not a change. |
|
|
163
173
|
| `--dry-run` | Checks the selected agent CLI is on `PATH` and exits without running the pipeline. | You want to sanity-check your setup before a real run. |
|
|
174
|
+
| `--detach` | Runs the pipeline in a background process and returns immediately, printing the run slug. Manage it with `orch list/status/pause/resume/stop/logs`. | You want to kick off a run and keep using your shell, or run several tasks concurrently. |
|
|
164
175
|
|
|
165
176
|
For `--ask`, Cursor uses `--mode ask`, Claude uses `--permission-mode plan`,
|
|
166
177
|
and `agn` is prompt-only best-effort (it has no dedicated read-only flag).
|
|
167
178
|
|
|
179
|
+
`--detach` only controls backgrounding, not whether a job record exists —
|
|
180
|
+
every non-`--dry-run` invocation gets one. Combining `--detach` with
|
|
181
|
+
`--ask`, `--quick`, or `--dry-run` is rejected outright (non-zero exit)
|
|
182
|
+
because there's no way to background those modes, not because they'd lack a
|
|
183
|
+
job record. Multiple `--detach` runs can execute concurrently in the same
|
|
184
|
+
directory, each with its own slug.
|
|
185
|
+
|
|
168
186
|
## CLI Reference
|
|
169
187
|
|
|
170
188
|
```text
|
|
171
|
-
Usage: orch [options] <task...>
|
|
189
|
+
Usage: orch [options] [command] <task...>
|
|
172
190
|
```
|
|
173
191
|
|
|
174
192
|
- `<task...>` — task description to use as the prompt (mention a file path
|
|
@@ -182,12 +200,50 @@ Usage: orch [options] <task...>
|
|
|
182
200
|
and exits (skips triage and all write pipelines).
|
|
183
201
|
- `--quick` — skips triage, runs `quick-fix` directly in the current working
|
|
184
202
|
tree; creates no artifacts, worktrees, or commits.
|
|
203
|
+
- `--detach` — runs the pipeline in the background and returns immediately,
|
|
204
|
+
printing `started <slug> (pid <pid>)`; rejects `--ask`/`--quick`/`--dry-run`.
|
|
185
205
|
- `--max-rounds <n>` — max writer⇄critic and writer⇄runner iterations per
|
|
186
206
|
implementer loop; defaults to `5`, ignored with `--ask` and `--quick`.
|
|
207
|
+
- `--fan-out` — decomposes the task into parallel workers coordinated by this
|
|
208
|
+
process instead of running the single-worktree pipeline (see
|
|
209
|
+
[Fan-out](#fan-out)); rejects `--ask`/`--quick`/`--dry-run`.
|
|
210
|
+
- `--max-workers <n>` — max number of parallel fan-out workers; defaults to
|
|
211
|
+
`4`; only meaningful with `--fan-out`.
|
|
212
|
+
- `--max-concurrency <n>` — optional hard ceiling on in-flight fan-out workers
|
|
213
|
+
at once; omit to let the coordinator choose (typically the current layer's
|
|
214
|
+
size); only meaningful with `--fan-out`.
|
|
187
215
|
- `--agent <cursor|claude|agn>` — selects the backend for the whole pipeline;
|
|
188
|
-
|
|
216
|
+
when omitted, uses local `.orch/config`, then global `~/.orch/config`, else
|
|
217
|
+
`cursor`.
|
|
189
218
|
- `-h, --help` — displays help for the command.
|
|
190
219
|
|
|
220
|
+
Config:
|
|
221
|
+
|
|
222
|
+
- `orch config` — prints the effective agent and which file(s) contributed
|
|
223
|
+
(local / global / default). Does not prompt.
|
|
224
|
+
- `orch config --agent <cursor|claude|agn> [--global|--local]` — writes the
|
|
225
|
+
default agent. Bare `--agent` (and `--global`) write `~/.orch/config`;
|
|
226
|
+
`--local` writes `<cwd>/.orch/config`. There is no `orch init`.
|
|
227
|
+
|
|
228
|
+
Job-control subcommands (see [Headless runs](#headless-runs)):
|
|
229
|
+
|
|
230
|
+
- `orch list` — lists all runs tracked under `.orch/` in the current directory.
|
|
231
|
+
- `orch status [slug]` — shows full status for a run; defaults to the most
|
|
232
|
+
recently started run.
|
|
233
|
+
- `orch pause <slug>` — requests a pause at the run's next stage-boundary
|
|
234
|
+
checkpoint.
|
|
235
|
+
- `orch resume <slug>` — unpauses a paused (or pausing) run.
|
|
236
|
+
- `orch continue <slug> "new task"` — starts a new complex pipeline on a
|
|
237
|
+
finished run's existing worktree/branch (same slug). Carries prior
|
|
238
|
+
stage/failure outcome into research/plan. For fan-out workers, continue the
|
|
239
|
+
worker slug, then `orch --integrate <parent>`. Not the same as `resume`.
|
|
240
|
+
- `orch stop <slug>` — sends `SIGTERM` to a running job (or reconciles a dead
|
|
241
|
+
one to `crashed`).
|
|
242
|
+
- `orch logs <slug> [-f]` — prints a run's `orch.log`; `-f` follows it until
|
|
243
|
+
the job reaches a terminal state.
|
|
244
|
+
- `orch jobs clean` — deletes every run tracked under `.orch/` in the current
|
|
245
|
+
directory, after a `y/N` confirmation prompt.
|
|
246
|
+
|
|
191
247
|
Examples:
|
|
192
248
|
|
|
193
249
|
```bash
|
|
@@ -197,8 +253,167 @@ orch "implement the local spec" --agent agn -v
|
|
|
197
253
|
orch --ask "where is the CLI entrypoint?" --agent claude
|
|
198
254
|
orch --quick "fix the typo in the README" --agent claude
|
|
199
255
|
orch "noop" --dry-run --agent cursor
|
|
256
|
+
orch config
|
|
257
|
+
orch config --agent claude
|
|
258
|
+
orch config --agent agn --local
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Headless runs
|
|
262
|
+
|
|
263
|
+
`--detach` starts the full pipeline in a background process and returns your
|
|
264
|
+
shell immediately:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
orch "implement the local spec" --agent claude --detach
|
|
268
|
+
# started swift-lagoon-49ea (pid 12345)
|
|
200
269
|
```
|
|
201
270
|
|
|
271
|
+
The parent process validates the agent binary and flag combination, eagerly
|
|
272
|
+
allocates `.orch/<slug>/`, writes an initial `run.json`, then re-invokes
|
|
273
|
+
itself (without `--detach`) as a detached child with `ORCH_JOB_SLUG` set; the
|
|
274
|
+
child runs the actual pipeline and keeps `run.json` up to date as it
|
|
275
|
+
progresses. Manage it with the subcommands below, all scoped to the current
|
|
276
|
+
directory's `.orch/`:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
orch list # SLUG ROLE STATE PHASE AGENT STARTED DURATION PID
|
|
280
|
+
orch status swift-lagoon-49ea # full record: state, phase, branch, worktree, exit code, ...
|
|
281
|
+
orch pause swift-lagoon-49ea # request a pause at the next stage-boundary checkpoint
|
|
282
|
+
orch resume swift-lagoon-49ea # unpause a paused/pausing run
|
|
283
|
+
orch continue swift-lagoon-49ea "follow-up polish" # new work on the same worktree
|
|
284
|
+
orch logs swift-lagoon-49ea -f # follow orch.log until the run finishes
|
|
285
|
+
orch stop swift-lagoon-49ea # SIGTERM the run
|
|
286
|
+
orch jobs clean # delete every tracked run under .orch/ (asks to confirm)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Pausing is cooperative and happens at stage boundaries (before the first
|
|
290
|
+
agent stage, and after each individual agent invocation) — it is not an OS
|
|
291
|
+
suspend, and a pause requested mid-stage takes effect only once that stage's
|
|
292
|
+
agent finishes. This means pause is not atomic across a writer⇄critic or
|
|
293
|
+
writer⇄runner pair: e.g. pausing during `test-writer` still lets
|
|
294
|
+
`test-writer` finish before the run actually pauses, ahead of `test-critic`.
|
|
295
|
+
|
|
296
|
+
`--detach` is rejected outright when combined with `--ask`, `--quick`, or
|
|
297
|
+
`--dry-run` — those modes have nothing to background — but that's a
|
|
298
|
+
restriction on backgrounding, not on job records: every non-`--dry-run`
|
|
299
|
+
invocation gets a `run.json`, `--detach` or not. Multiple `--detach` runs can
|
|
300
|
+
execute concurrently against the same directory, each tracked under its own
|
|
301
|
+
slug.
|
|
302
|
+
|
|
303
|
+
## Fan-out
|
|
304
|
+
|
|
305
|
+
`--fan-out` decomposes a complex task into independent workers that each run
|
|
306
|
+
the full pipeline (minus triage) in their own worktree, then hands the green
|
|
307
|
+
branches to a dedicated integration session:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
orch "implement the billing module" --fan-out --agent claude
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
triage: complex — fan-out requested
|
|
315
|
+
boundaries: partitionable into billing scaffold + 3 endpoint workers
|
|
316
|
+
decomposer: split into 4 workers (1 scaffold, 3 parallel)
|
|
317
|
+
schedule: concurrency 3
|
|
318
|
+
|
|
319
|
+
[01-scaffold rapid-fox-x7q2] done — commit b2c3d4e
|
|
320
|
+
[02-invoices merry-elk-r4b1] running
|
|
321
|
+
[03-charges wise-owl-k1a8] running
|
|
322
|
+
[02-invoices merry-elk-r4b1] done — commit c3d4e5f
|
|
323
|
+
[04-webhooks quirky-cedar-p8w3] done — commit d4e5f60
|
|
324
|
+
|
|
325
|
+
overlaps: none
|
|
326
|
+
[integrate tidy-heron-m2p9] merged 3 branches, full suite passed
|
|
327
|
+
commit: e5f6071 on orch/wise-pine-e904
|
|
328
|
+
merge: git merge orch/wise-pine-e904
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The flow, in order:
|
|
332
|
+
|
|
333
|
+
1. **Triage** runs once, same as any run. If it routes to quick-fix, fan-out
|
|
334
|
+
is skipped entirely — triage never opts into fan-out on its own.
|
|
335
|
+
2. **`boundaries`** (new agent) researches only how the work can be split —
|
|
336
|
+
what can run in parallel, where the coarse boundaries are, whether shared
|
|
337
|
+
scaffolding (types, registries, barrels) needs to land first — and writes
|
|
338
|
+
`boundaries.md`. It never plans implementation steps.
|
|
339
|
+
3. **`decomposer`** (new agent) reads `boundaries.md` and the task and emits
|
|
340
|
+
a worker list (or declines with a reason). orch validates the decomposition
|
|
341
|
+
itself — worker count, ownership, dependency graph, layer overlap, at most
|
|
342
|
+
one `scaffold` worker — and feeds validation failures back to the
|
|
343
|
+
decomposer for up to two repair attempts before giving up.
|
|
344
|
+
4. **Decline path.** If the decomposer declines, or validation still fails
|
|
345
|
+
after repairs, orch falls through to today's single-worktree pipeline
|
|
346
|
+
(research → plan → worktree → test loop → code loop → commit) — no
|
|
347
|
+
`fanout.json`, no workers scheduled.
|
|
348
|
+
5. **Scaffold first.** If one worker is marked `scaffold`, it runs alone to
|
|
349
|
+
completion before anything else starts; its commit becomes the base SHA
|
|
350
|
+
every other worker (and the integration session) branches from. A failed
|
|
351
|
+
scaffold aborts the whole fan-out before any parallel worker spawns.
|
|
352
|
+
6. **Parallel workers.** Each remaining worker is a full, cold orch pipeline
|
|
353
|
+
(research → plan → worktree → test loop → code loop → commit) running as
|
|
354
|
+
its own detached process in its own `.orch/<worker-slug>/` directory and
|
|
355
|
+
worktree — a sibling `orch` invocation, not in-process work. The
|
|
356
|
+
coordinator chooses how many run at once (capped by `--max-concurrency`
|
|
357
|
+
when set) based on the dependency layer currently in flight; a worker
|
|
358
|
+
whose dependency failed is recorded `skipped` and never started.
|
|
359
|
+
7. **Integration.** Once every worker has settled, the coordinator detects
|
|
360
|
+
file overlaps between the `done` workers and spawns a specialized
|
|
361
|
+
integration session (`--integrate`, a hidden flag) that merges their
|
|
362
|
+
branches in order, resolves any conflicts with a dedicated `integrator`
|
|
363
|
+
agent, then runs a runner-first verify loop (test-runner first,
|
|
364
|
+
code-writer only on failure) before committing to `orch/<parent-slug>`. If
|
|
365
|
+
no worker reached `done`, integration is skipped and the run exits
|
|
366
|
+
non-zero.
|
|
367
|
+
|
|
368
|
+
Artifacts, in addition to the coordinator's own `run.json`/`orch.log`:
|
|
369
|
+
|
|
370
|
+
```text
|
|
371
|
+
<invocation-cwd>/.orch/<parent-slug>/
|
|
372
|
+
boundaries.md # partitionability research only
|
|
373
|
+
fanout.json # decomposition + scheduling state
|
|
374
|
+
|
|
375
|
+
<invocation-cwd>/.orch/<worker-slug>/
|
|
376
|
+
research.md, task.md, status.md, run.json, orch.log # a normal run, per worker
|
|
377
|
+
|
|
378
|
+
<invocation-cwd>/.orch/<integration-slug>/
|
|
379
|
+
integration.md # merge log, overlaps, conflict repairs, final verdict
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The coordinator never creates its own worktree and never runs the
|
|
383
|
+
implementer stages itself outside of the decline path — after decomposition
|
|
384
|
+
it is pure orchestration: spawn, poll, report. Exit code is `0` only when
|
|
385
|
+
every worker succeeded and integration committed; any worker failure forces a
|
|
386
|
+
non-zero exit even if integration still commits on the green subset.
|
|
387
|
+
|
|
388
|
+
`orch list` renders an indented job tree: top-level rows are jobs with no
|
|
389
|
+
`parent` (coordinators and ordinary runs), children indent two spaces under
|
|
390
|
+
their coordinator with a `ROLE` column (`coordinator` / `worker` /
|
|
391
|
+
`integrate` / `-`). `orch status <parent>` expands each child's
|
|
392
|
+
state/phase/branch in the same tree order; `orch status <child>` shows a
|
|
393
|
+
`parent:` line and does not list siblings. Default `orch status` (no slug)
|
|
394
|
+
still picks the most recent job and uses the parent-vs-child view as
|
|
395
|
+
appropriate. `orch logs <parent>` tails the coordinator log only.
|
|
396
|
+
|
|
397
|
+
Cascade control is parent → children only: `orch pause|resume|stop <parent>`
|
|
398
|
+
applies to the coordinator and every live child (pause reports how many
|
|
399
|
+
children were signaled). `orch pause|resume|stop <child>` is leaf-only — no
|
|
400
|
+
upward cascade. While a parent is paused, the coordinator stops spawning at
|
|
401
|
+
its schedule checkpoints and does not kill live children; on resume it
|
|
402
|
+
re-attaches to still-live workers and spawns only still-pending ones (never
|
|
403
|
+
re-decomposing or duplicating finished/live work). A paused leaf is waited
|
|
404
|
+
on without failing it or starting dependents; a stopped/crashed/failed leaf
|
|
405
|
+
is marked `failed` in `fanout.json`, its dependents are skipped, and the
|
|
406
|
+
schedule continues. `SIGINT`/`SIGHUP`/`SIGTERM` on the coordinator (and
|
|
407
|
+
`orch stop <parent>`) cascade a `SIGTERM` to every live child it recorded —
|
|
408
|
+
the same as [Headless runs](#headless-runs)'s interrupt handling, just
|
|
409
|
+
extended to the whole fan-out tree; worktrees and branches are never
|
|
410
|
+
removed automatically.
|
|
411
|
+
|
|
412
|
+
Depth is capped at 1: every worker and the integration session run with
|
|
413
|
+
`ORCH_FANOUT_DEPTH=1` set, and `--fan-out` is rejected outright when that
|
|
414
|
+
variable is already present — a worker or integration session never fans out
|
|
415
|
+
again.
|
|
416
|
+
|
|
202
417
|
## Project structure
|
|
203
418
|
|
|
204
419
|
Complex runs create a run directory and a sibling worktree, reusing the
|
|
@@ -209,13 +424,16 @@ layout shown in [Artifacts and worktrees](#artifacts-and-worktrees):
|
|
|
209
424
|
research.md
|
|
210
425
|
task.md
|
|
211
426
|
status.md
|
|
427
|
+
run.json # written for every run
|
|
428
|
+
orch.log # --detach only
|
|
212
429
|
|
|
213
430
|
<parent-of-repo>/<repo-name>-<slug> # worktree
|
|
214
431
|
orch/<slug> # branch
|
|
215
432
|
```
|
|
216
433
|
|
|
217
|
-
Default quick
|
|
218
|
-
|
|
434
|
+
Default quick-fixes, `--quick`, and `--ask` runs still get a `.orch/<slug>/`
|
|
435
|
+
directory with a `run.json` job record (so `orch list`/`status` can see them),
|
|
436
|
+
but no `research.md`/`task.md`/`status.md`, no worktree, and no commits.
|
|
219
437
|
|
|
220
438
|
## Interrupts
|
|
221
439
|
|
|
@@ -234,7 +452,7 @@ pkill -f 'agn ' # --agent agn
|
|
|
234
452
|
|
|
235
453
|
| Backend | `--agent` value | Status | Notes |
|
|
236
454
|
| --- | --- | --- | --- |
|
|
237
|
-
| Cursor Agent CLI | `cursor` | Supported (default) | Command `agent` on `PATH`. |
|
|
455
|
+
| Cursor Agent CLI | `cursor` | Supported (builtin default) | Command `agent` on `PATH`. Override via `orch config` or `--agent`. |
|
|
238
456
|
| Claude Code CLI | `claude` | Supported | Command `claude` on `PATH`. |
|
|
239
457
|
| agn | `agn` | Supported | Requires `npm install -g @welluable/agn-cli` (`>= 0.0.12`) and `agn init`. |
|
|
240
458
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function boundariesAgentArgs({ prompt, cwd, boundariesPath }) {
|
|
2
|
+
return {
|
|
3
|
+
name: 'boundaries',
|
|
4
|
+
instructions: `
|
|
5
|
+
You are a Boundaries Agent.
|
|
6
|
+
|
|
7
|
+
* Research the codebase rooted at ${cwd} enough to answer: what pieces of
|
|
8
|
+
the user's request can be worked on in parallel, where the coarse
|
|
9
|
+
boundaries between them are, and whether shared scaffolding (types,
|
|
10
|
+
registries, barrels) must land before parallel work can start.
|
|
11
|
+
* Do not plan implementation steps and do not write a task checklist —
|
|
12
|
+
forbid implementation planning entirely. That is each worker's own job
|
|
13
|
+
later in the pipeline.
|
|
14
|
+
* Do not write any code. Write your findings only to the exact path:
|
|
15
|
+
${boundariesPath}
|
|
16
|
+
* Before the summary marker below, your message must contain only the
|
|
17
|
+
exact path: ${boundariesPath}
|
|
18
|
+
* After the path above, on a new line write the exact marker
|
|
19
|
+
\`<<<SUMMARY>>>\`, followed by one paragraph in natural, human-readable
|
|
20
|
+
language explaining what you did in this step and what happened — no
|
|
21
|
+
lists, no headers, just prose.
|
|
22
|
+
`,
|
|
23
|
+
prompt,
|
|
24
|
+
options: { cwd },
|
|
25
|
+
};
|
|
26
|
+
}
|
package/agents/code-writer.js
CHANGED
|
@@ -47,6 +47,11 @@ export function codeWriterAgentArgs({
|
|
|
47
47
|
characters, with no spaces), followed by one paragraph in natural,
|
|
48
48
|
human-readable language explaining what you did in this step and
|
|
49
49
|
what happened — no lists, no headers, just prose.
|
|
50
|
+
* If you changed any files, after that paragraph add a line reading
|
|
51
|
+
exactly "Files:" followed by one line per changed file formatted
|
|
52
|
+
as "<path>: <one-line description>" (e.g. "lib/agent.js: wired the
|
|
53
|
+
file tracker into onToolEvent"). Omit this section if you changed
|
|
54
|
+
nothing.
|
|
50
55
|
${feedbackBlock}
|
|
51
56
|
`,
|
|
52
57
|
prompt,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function feedbackBlock(feedback) {
|
|
2
|
+
if (!Array.isArray(feedback) || feedback.length === 0) return '';
|
|
3
|
+
const violations = feedback.map((violation) => `- ${violation}`).join('\n');
|
|
4
|
+
return `
|
|
5
|
+
|
|
6
|
+
[Validation Feedback]
|
|
7
|
+
Your previous decomposition was rejected for these reasons. Fix them and
|
|
8
|
+
try again:
|
|
9
|
+
${violations}
|
|
10
|
+
[/Validation Feedback]`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function decomposerAgentArgs({ prompt, cwd, boundariesOutput, maxWorkers, feedback }) {
|
|
14
|
+
return {
|
|
15
|
+
name: 'decomposer',
|
|
16
|
+
instructions: `
|
|
17
|
+
You are a Decomposer Agent.
|
|
18
|
+
|
|
19
|
+
* Read the boundaries research below and the original task, then decide
|
|
20
|
+
whether the work can be split into independent workers.
|
|
21
|
+
* At most ${maxWorkers} workers — that is a hard ceiling, not a target.
|
|
22
|
+
* "decomposable": false (with a "why") is a valid, expected answer when
|
|
23
|
+
you cannot find independent seams — do not force a split.
|
|
24
|
+
* Your final message MUST be valid JSON only — no markdown, no prose
|
|
25
|
+
outside JSON:
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
"decomposable": true,
|
|
29
|
+
"why": "short reason",
|
|
30
|
+
"workers": [
|
|
31
|
+
{
|
|
32
|
+
"id": "01-scaffold",
|
|
33
|
+
"title": "short title",
|
|
34
|
+
"subtask": "what this worker implements",
|
|
35
|
+
"area": "coarse directory or feature name",
|
|
36
|
+
"owns": ["path/prefix/or/file"],
|
|
37
|
+
"dependsOn": [],
|
|
38
|
+
"scaffold": true
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
or, when declining:
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
"decomposable": false,
|
|
47
|
+
"why": "short reason"
|
|
48
|
+
}
|
|
49
|
+
* After the JSON above, on a new line write the exact marker
|
|
50
|
+
\`<<<SUMMARY>>>\`, followed by one paragraph in natural, human-readable
|
|
51
|
+
language explaining what you did in this step and what happened — no
|
|
52
|
+
lists, no headers, just prose. The JSON itself must stay exactly as
|
|
53
|
+
specified above, before the summary marker.
|
|
54
|
+
|
|
55
|
+
[Boundaries Agent Output]
|
|
56
|
+
${boundariesOutput}
|
|
57
|
+
[/Boundaries Agent Output]${feedbackBlock(feedback)}
|
|
58
|
+
`,
|
|
59
|
+
prompt,
|
|
60
|
+
options: { cwd },
|
|
61
|
+
};
|
|
62
|
+
}
|
package/agents/index.js
CHANGED
|
@@ -7,3 +7,6 @@ export { testWriterAgentArgs } from './test-writer.js';
|
|
|
7
7
|
export { testCriticAgentArgs } from './test-critic.js';
|
|
8
8
|
export { codeWriterAgentArgs } from './code-writer.js';
|
|
9
9
|
export { testRunnerAgentArgs } from './test-runner.js';
|
|
10
|
+
export { integratorAgentArgs } from './integrator.js';
|
|
11
|
+
export { boundariesAgentArgs } from './boundaries.js';
|
|
12
|
+
export { decomposerAgentArgs } from './decomposer.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Conflict-repair-only agent for the `--integrate` driver: resolves merge conflict
|
|
2
|
+
* markers in the given files using the merge output and involved workers' subtask/area
|
|
3
|
+
* as context. Never touches git itself — orch owns the merge commit. */
|
|
4
|
+
export function integratorAgentArgs({ prompt, cwd, conflictedFiles, mergeOutput, involvedWorkers }) {
|
|
5
|
+
const fileList = (conflictedFiles || []).map((file) => `- ${file}`).join('\n');
|
|
6
|
+
const workerContext = (involvedWorkers || [])
|
|
7
|
+
.map((worker) => `- ${worker.title ?? worker.id}: ${worker.subtask} (area: ${worker.area})`)
|
|
8
|
+
.join('\n');
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
name: 'integrator',
|
|
12
|
+
instructions: `
|
|
13
|
+
You are an Integrator Agent.
|
|
14
|
+
|
|
15
|
+
* A merge is in progress in this worktree with unresolved conflict
|
|
16
|
+
markers in exactly these files — resolve markers only in these
|
|
17
|
+
files, and touch no other files:
|
|
18
|
+
${fileList}
|
|
19
|
+
* Combine what the involved workers already built; do not redesign or
|
|
20
|
+
reimplement anything beyond what is strictly needed to resolve the
|
|
21
|
+
conflict.
|
|
22
|
+
* Workers involved in this conflict:
|
|
23
|
+
${workerContext}
|
|
24
|
+
* Do not run \`git\` yourself — no \`git add\`, \`git commit\`,
|
|
25
|
+
\`git merge --continue\`, \`git merge --abort\`, or any other git
|
|
26
|
+
command. orch completes the merge commit itself; you only edit files.
|
|
27
|
+
* Do not report a pass/fail judgment of any kind; orch checks whether
|
|
28
|
+
the conflict markers are gone itself.
|
|
29
|
+
* After you finish editing, on a new line write the exact marker
|
|
30
|
+
\`<<<SUMMARY>>>\`, followed by one paragraph in natural,
|
|
31
|
+
human-readable language explaining what you did in this step and
|
|
32
|
+
what happened — no lists, no headers, just prose.
|
|
33
|
+
|
|
34
|
+
[Merge Output]
|
|
35
|
+
${mergeOutput}
|
|
36
|
+
[/Merge Output]
|
|
37
|
+
`,
|
|
38
|
+
prompt,
|
|
39
|
+
options: { cwd },
|
|
40
|
+
};
|
|
41
|
+
}
|
package/agents/quick-fix.js
CHANGED
|
@@ -22,6 +22,10 @@ export function quickFixAgentArgs({ prompt, cwd, fix_plan }) {
|
|
|
22
22
|
characters, with no spaces), followed by one paragraph in
|
|
23
23
|
natural, human-readable language explaining what you did in this
|
|
24
24
|
step and what happened — no lists, no headers, just prose.
|
|
25
|
+
* If you changed any files, after that paragraph add a line reading
|
|
26
|
+
exactly "Files:" followed by one line per changed file formatted
|
|
27
|
+
as "<path>: <one-line description>" (e.g. "README.md: documented
|
|
28
|
+
the new --files flag"). Omit this section if you changed nothing.
|
|
25
29
|
${fixPlan}
|
|
26
30
|
`,
|
|
27
31
|
prompt,
|
package/agents/test-writer.js
CHANGED
|
@@ -42,6 +42,10 @@ export function testWriterAgentArgs({
|
|
|
42
42
|
characters, with no spaces), followed by one paragraph in natural,
|
|
43
43
|
human-readable language explaining what you did in this step and
|
|
44
44
|
what happened — no lists, no headers, just prose.
|
|
45
|
+
* If you changed any files, after that paragraph add a line reading
|
|
46
|
+
exactly "Files:" followed by one line per changed file formatted
|
|
47
|
+
as "<path>: <one-line description>" (e.g. "test/agent.test.js:
|
|
48
|
+
added the dedupe case"). Omit this section if you changed nothing.
|
|
45
49
|
${criticBlock}
|
|
46
50
|
`,
|
|
47
51
|
prompt,
|
package/lib/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { formatActiveTools } from './tool-status.js';
|
|
5
|
+
import { patchJob, buildLastOutcome } from './jobs.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Every child process spawned by an agent, tracked so we can reap them all
|
|
@@ -29,6 +30,19 @@ function killChildGroup(child, signal) {
|
|
|
29
30
|
|
|
30
31
|
let shuttingDown = false;
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The in-process active job slug for foreground (non-detached) runs, which
|
|
35
|
+
* never spawn a separate process to carry `ORCH_JOB_SLUG` as an env var. Set
|
|
36
|
+
* by main.js via `setJobSlug(slug)` once the Commander action allocates a
|
|
37
|
+
* job for a non-detached invocation.
|
|
38
|
+
*/
|
|
39
|
+
let activeJobSlug = null;
|
|
40
|
+
|
|
41
|
+
/** Sets the in-process active job slug (see `activeJobSlug`). */
|
|
42
|
+
export function setJobSlug(slug) {
|
|
43
|
+
activeJobSlug = slug;
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
/** Conventional shell status: 128 + signal number. */
|
|
33
47
|
export function exitCodeForSignal(signal) {
|
|
34
48
|
if (signal === 'SIGINT') return 130;
|
|
@@ -38,12 +52,44 @@ export function exitCodeForSignal(signal) {
|
|
|
38
52
|
|
|
39
53
|
/**
|
|
40
54
|
* Reap every tracked child group, then exit. `exit` is injectable so tests can
|
|
41
|
-
* assert without tearing down the runner.
|
|
55
|
+
* assert without tearing down the runner. Persists terminal job state to
|
|
56
|
+
* `run.json` for whichever job is active — `process.env.ORCH_JOB_SLUG` (set
|
|
57
|
+
* on a detached child's own env) or the in-process `activeJobSlug` (set for a
|
|
58
|
+
* foreground run via `setJobSlug`), env taking precedence when both are set —
|
|
59
|
+
* before reaping children, so a concurrent `orch pause`/`resume`/`status`
|
|
60
|
+
* write stays lock-safe. `jobCwd` is a test seam only; real callers never
|
|
61
|
+
* pass it — it always resolves to the actual `process.cwd()`.
|
|
42
62
|
*/
|
|
43
|
-
export function shutdown(signal, { exit = (code) => process.exit(code) } = {}) {
|
|
63
|
+
export function shutdown(signal, { exit = (code) => process.exit(code), jobCwd = process.cwd() } = {}) {
|
|
44
64
|
if (shuttingDown) return;
|
|
45
65
|
shuttingDown = true;
|
|
46
66
|
|
|
67
|
+
const jobSlug = process.env.ORCH_JOB_SLUG ?? activeJobSlug;
|
|
68
|
+
if (jobSlug) {
|
|
69
|
+
try {
|
|
70
|
+
const exitCode = exitCodeForSignal(signal);
|
|
71
|
+
const finishedAt = new Date().toISOString();
|
|
72
|
+
patchJob(jobCwd, jobSlug, (current) => ({
|
|
73
|
+
state: 'stopped',
|
|
74
|
+
exitCode,
|
|
75
|
+
finishedAt,
|
|
76
|
+
lastOutcome: buildLastOutcome({
|
|
77
|
+
state: 'stopped',
|
|
78
|
+
phase: current.phase,
|
|
79
|
+
stage: current.stage,
|
|
80
|
+
round: current.round,
|
|
81
|
+
exitCode,
|
|
82
|
+
finishedAt,
|
|
83
|
+
task: current.task,
|
|
84
|
+
summary: '',
|
|
85
|
+
error: null,
|
|
86
|
+
}),
|
|
87
|
+
}));
|
|
88
|
+
} catch {
|
|
89
|
+
// Best-effort: don't let a job-state write failure block shutdown.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
47
93
|
for (const child of liveChildren) killChildGroup(child, 'SIGTERM');
|
|
48
94
|
|
|
49
95
|
const exitCode = exitCodeForSignal(signal);
|
|
@@ -74,10 +120,11 @@ export function trackLiveChild(child) {
|
|
|
74
120
|
child.once('error', () => liveChildren.delete(child));
|
|
75
121
|
}
|
|
76
122
|
|
|
77
|
-
/** Reset shutdown latch + child set between tests. */
|
|
123
|
+
/** Reset shutdown latch + child set + in-process active job slug between tests. */
|
|
78
124
|
export function resetShutdownState() {
|
|
79
125
|
shuttingDown = false;
|
|
80
126
|
liveChildren.clear();
|
|
127
|
+
activeJobSlug = null;
|
|
81
128
|
}
|
|
82
129
|
|
|
83
130
|
/**
|
|
@@ -124,21 +171,30 @@ function registerShutdownHandlers() {
|
|
|
124
171
|
});
|
|
125
172
|
}
|
|
126
173
|
|
|
127
|
-
/** Format milliseconds as `{s}s
|
|
174
|
+
/** Format milliseconds as `{s}s`, `{m}m {s}s`, or `{h}h {m}m` (whole seconds, no leading 0m/0h). */
|
|
128
175
|
export function formatElapsed(ms) {
|
|
129
176
|
const totalSec = Math.floor(ms / 1000);
|
|
130
|
-
const
|
|
177
|
+
const h = Math.floor(totalSec / 3600);
|
|
178
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
131
179
|
const s = totalSec % 60;
|
|
180
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
132
181
|
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
133
182
|
}
|
|
134
183
|
|
|
135
184
|
export class Agent {
|
|
136
|
-
constructor(name, instructions, prompt, {
|
|
185
|
+
constructor(name, instructions, prompt, {
|
|
186
|
+
cwd,
|
|
187
|
+
readOnly = false,
|
|
188
|
+
fileTracker = null,
|
|
189
|
+
onFileChange = null,
|
|
190
|
+
} = {}) {
|
|
137
191
|
this.name = name;
|
|
138
192
|
this.instructions = instructions;
|
|
139
193
|
this.prompt = prompt;
|
|
140
194
|
this.cwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
141
195
|
this.readOnly = Boolean(readOnly);
|
|
196
|
+
this.fileTracker = fileTracker;
|
|
197
|
+
this.onFileChange = onFileChange;
|
|
142
198
|
this.process = null;
|
|
143
199
|
this.spinner = null;
|
|
144
200
|
this.startedAt = 0;
|
|
@@ -157,8 +213,28 @@ export class Agent {
|
|
|
157
213
|
onToolEvent({ name, args, phase, callId }) {
|
|
158
214
|
if (phase === 'started') {
|
|
159
215
|
this.activeTools.set(callId, { name, args });
|
|
216
|
+
this.fileTracker?.record({ name, args, phase, callId });
|
|
160
217
|
} else if (phase === 'completed') {
|
|
218
|
+
// Recall before delete — Claude/agn completions often have empty args.
|
|
219
|
+
const prior = this.activeTools.get(callId);
|
|
161
220
|
this.activeTools.delete(callId);
|
|
221
|
+
|
|
222
|
+
if (this.fileTracker) {
|
|
223
|
+
const hasPath = Boolean(args?.path || args?.file_path);
|
|
224
|
+
const entry = this.fileTracker.record({
|
|
225
|
+
name: name || prior?.name || '',
|
|
226
|
+
args: hasPath ? args : (prior?.args ?? args ?? {}),
|
|
227
|
+
phase,
|
|
228
|
+
callId,
|
|
229
|
+
});
|
|
230
|
+
if (entry?.isNew) {
|
|
231
|
+
const wasSpinning = this.spinner?.isSpinning;
|
|
232
|
+
if (wasSpinning) this.spinner.stop();
|
|
233
|
+
console.log(` ${entry.marker} ${entry.path}`);
|
|
234
|
+
if (wasSpinning) this.spinner.start();
|
|
235
|
+
this.onFileChange?.(entry);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
162
238
|
}
|
|
163
239
|
this.refreshToolStatus();
|
|
164
240
|
}
|