@markjaquith/agency 2.30.1 → 2.32.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.
Files changed (38) hide show
  1. package/README.md +56 -38
  2. package/cli.ts +2 -1
  3. package/package.json +5 -2
  4. package/skills/agency/SKILL.md +7 -6
  5. package/skills/agency/references/commands.md +17 -14
  6. package/skills/agency/references/contracts.md +4 -5
  7. package/skills/agency/references/recipes.md +13 -10
  8. package/src/cli-parser.test.ts +12 -2
  9. package/src/cli-parser.ts +5 -2
  10. package/src/cli.test.ts +34 -10
  11. package/src/commands/doctor.test.ts +11 -1
  12. package/src/commands/phase.ts +1 -1
  13. package/src/commands/task.test.ts +34 -1
  14. package/src/commands/task.ts +16 -20
  15. package/src/commands/work.test.ts +65 -65
  16. package/src/commands/work.ts +43 -35
  17. package/src/opentui.d.ts +1 -0
  18. package/src/services/DoctorService.ts +22 -0
  19. package/src/services/EpicService.test.ts +3 -0
  20. package/src/services/EpicService.ts +2 -1
  21. package/src/services/IntegrationService.test.ts +2 -1
  22. package/src/services/PhaseService.ts +5 -4
  23. package/src/services/PullRequestService.test.ts +4 -1
  24. package/src/services/ReadinessService.test.ts +54 -0
  25. package/src/services/ReadinessService.ts +39 -2
  26. package/src/services/TaskPhaseService.test.ts +38 -11
  27. package/src/services/TaskService.ts +4 -3
  28. package/src/utils/chooser.test.ts +49 -12
  29. package/src/utils/chooser.ts +20 -37
  30. package/src/utils/interactive-loader.ts +6 -0
  31. package/src/utils/interactive.test.tsx +258 -0
  32. package/src/utils/interactive.tsx +290 -0
  33. package/src/workbase/AGENTS.md +5 -5
  34. package/src/workbase/frontmatter.ts +17 -0
  35. package/src/workbase/runner-command.test.ts +30 -4
  36. package/src/workbase/runner-command.ts +21 -6
  37. package/src/workbase/schemas.test.ts +4 -2
  38. package/src/workbase/schemas.ts +4 -0
package/README.md CHANGED
@@ -37,6 +37,12 @@ For development, run `bun link` from this repository.
37
37
  Entity IDs come from directory names. Structured metadata lives in YAML 1.2
38
38
  frontmatter; prose below it supplies human and agent context.
39
39
 
40
+ New epic, task, and phase documents use the same core prose sections:
41
+ `Outcome` states the intended result, `Plan` describes the current approach, and
42
+ `Important Decisions` preserves consequential choices and their rationale. These
43
+ sections are creation defaults rather than validation requirements, so existing
44
+ and customized documents remain valid.
45
+
40
46
  ## Workbase Layout
41
47
 
42
48
  ```text
@@ -177,7 +183,8 @@ OpenCode and Claude Code are built-in runner presets. Select either preset or a
177
183
  configured runner with `agency work --runner <name>`. A launch is fresh unless
178
184
  `AGENCY_SESSION_ID` is already set; resumed launches use the runner's
179
185
  `resumeCommand` when configured. The built-in presets use `--continue` only for
180
- resumed launches.
186
+ resumed launches. By default Agency opens the runner without a prompt. `--auto`
187
+ uses its autonomous command and sends the generated task, phase, or epic prompt.
181
188
 
182
189
  Custom runners are direct argv commands, never shell snippets:
183
190
 
@@ -186,8 +193,10 @@ Custom runners are direct argv commands, never shell snippets:
186
193
  "version": 2,
187
194
  "runners": {
188
195
  "custom": {
189
- "command": ["my-agent", "--prompt", "{prompt}"],
190
- "resumeCommand": ["my-agent", "resume", "{sessionId}", "{prompt}"],
196
+ "command": ["my-agent"],
197
+ "autoCommand": ["my-agent", "--prompt", "{prompt}"],
198
+ "resumeCommand": ["my-agent", "resume", "{sessionId}"],
199
+ "autoResumeCommand": ["my-agent", "resume", "{sessionId}", "{prompt}"],
191
200
  "environment": { "MY_AGENT_TARGET": "{target}" }
192
201
  }
193
202
  }
@@ -196,24 +205,32 @@ Custom runners are direct argv commands, never shell snippets:
196
205
 
197
206
  Available placeholders are `{prompt}`, `{workbase}`, `{target}`, `{task}`,
198
207
  `{phase}`, `{claimant}`, `{sessionId}`, and `{claimRevision}`. Task and phase
199
- placeholders are empty when they do not apply. If `resumeCommand` is omitted,
200
- the fresh command is also used for resumed sessions.
208
+ placeholders are empty when they do not apply. `{prompt}` is empty unless
209
+ `--auto` is set. If `resumeCommand` is omitted, the fresh command is also used
210
+ for resumed sessions. If `autoResumeCommand` is omitted, `autoCommand` is used;
211
+ configured runners without `autoCommand` reject `--auto`.
201
212
 
202
213
  Every runner receives the same `AGENCY_RUNNER`, `AGENCY_CLAIMANT`,
203
214
  `AGENCY_SESSION_ID`, `AGENCY_CLAIM_REVISION`, `AGENCY_WORKBASE`, `AGENCY_TARGET`,
204
215
  `AGENCY_TASK_ID`, `AGENCY_PHASE_ID`, and `AGENCY_PROMPT` environment. Configured
205
216
  environment is added without overriding these normalized values.
217
+ `AGENCY_CLAIM_REVISION` is empty for local `agency work` launches.
218
+ `AGENCY_PROMPT` is empty unless `--auto` is set.
206
219
  The `opencode` runner additionally receives `OPENCODE_CONFIG` for the workbase's
207
220
  managed integration and `OPENCODE_CONFIG_CONTENT` with runtime-only,
208
221
  workbase-scoped access and edit rules. These values keep the Git-synced config
209
222
  portable while providing whole-workbase read access at every launch location.
223
+ `AGENCY_CLAIM_REVISION` is empty for local `agency work` launches.
224
+ `AGENCY_PROMPT` is empty unless `--auto` is set.
210
225
  `--print-command` prints the exact cwd and argv plus non-secret environment keys
211
226
  without launching the runner.
212
227
 
213
228
  ### Custom Chooser Command
214
229
 
215
- Interactive selection uses a native numbered chooser by default. To use an
216
- external chooser, configure an argv command in `agency.json`:
230
+ Interactive selection uses an OpenTUI Solid split footer by default. Type to
231
+ fuzzy-filter choices, use arrow keys or Ctrl-N/Ctrl-P to move, press Enter to
232
+ select, and press Escape or Ctrl-C to cancel. To use an external chooser
233
+ instead, configure an argv command in `agency.json`:
217
234
 
218
235
  ```json
219
236
  {
@@ -225,13 +242,14 @@ external chooser, configure an argv command in `agency.json`:
225
242
  Agency writes one `key<TAB>label` record per choice to the command's stdin. The
226
243
  command must write the selected opaque key or selected record to stdout; commands
227
244
  such as `["gum", "filter"]` therefore work without wrappers. Exit codes 1 and
228
- 130, empty stdout, native `q`, and an empty native response cancel selection.
229
- Other nonzero exits, unknown keys, and invalid native numbers are errors.
245
+ 130 or empty stdout cancel external selection. Other nonzero exits and unknown
246
+ keys are errors.
230
247
 
231
- Selectors are opened only when stdin and stderr are terminals and neither
232
- `--no-input` nor JSON output is active. Labels use color only when stderr is a
233
- terminal, `TERM` is not `dumb`, and `NO_COLOR` is unset; otherwise selectors use
234
- plain labels without ANSI styling or icon-font dependencies.
248
+ Selectors are opened only when stdin and stdout are terminals and neither
249
+ `--no-input` nor JSON output is active. External chooser labels use color only
250
+ when stdout is a terminal, `TERM` is not `dumb`, and `NO_COLOR` is unset. The
251
+ native OpenTUI selector uses plain labels without ANSI styling or icon-font
252
+ dependencies.
235
253
 
236
254
  ## Frontmatter
237
255
 
@@ -494,8 +512,9 @@ back-reference.
494
512
 
495
513
  ### Tasks
496
514
 
497
- Create a task interactively. Text prompts identify optional values, and known
498
- choices use fzf. This command requires a TTY and fails with `--no-input`:
515
+ Create a task interactively with the OpenTUI Solid footer. When exactly one
516
+ repository is available, Agency selects it without presenting a redundant
517
+ choice. This command requires a TTY and fails with `--no-input`:
499
518
 
500
519
  ```text
501
520
  agency task new [id]
@@ -600,12 +619,13 @@ writing anything.
600
619
 
601
620
  Single-phase tasks and phases store status in YAML. New execution units start
602
621
  `open`, and `agency work` marks the selected execution unit `working` immediately
603
- before launch. Use claims for coordinated ownership and the status subcommands
604
- for manual lifecycle overrides. The interactive work selector displays status
605
- markers before execution units. Existing working and delegated work may be
606
- released to `open` or assigned a terminal outcome. Done and dropped work are
607
- terminal and may only remain unchanged or transition to open; reopen terminal
608
- work before changing its outcome.
622
+ before launch. Running `agency work` again can relaunch unclaimed `working` work.
623
+ Use explicit claims only when an external orchestrator needs coordinated
624
+ ownership. The interactive work selector displays status markers before
625
+ execution units. Existing working and delegated work may be released to `open`
626
+ or assigned a terminal outcome. Done and dropped work are terminal and may only
627
+ remain unchanged or transition to open; reopen terminal work before changing its
628
+ outcome.
609
629
 
610
630
  `delegated` remains readable for existing workbases but cannot be newly assigned.
611
631
  Delegation is now explicit: the claimant identifies the orchestrator and the
@@ -639,11 +659,10 @@ frontmatter. Conflicts return the current revision and complete ownership record
639
659
  in the machine error envelope rather than overwriting it. Expired claims may be
640
660
  replaced with a revision-guarded claim.
641
661
 
642
- `agency work` claims an execution unit before launching its agent. Set
643
- `AGENCY_CLAIMANT`, `AGENCY_RUNNER`, or `AGENCY_SESSION_ID` to supply orchestrator
644
- identities; otherwise Agency derives them from the user and process. The selected
645
- runner name is recorded on the claim. The launched agent receives the normalized
646
- runner environment documented above for a later release or finish operation.
662
+ `agency work` does not claim execution units. It refuses active explicit claims,
663
+ marks open execution work `working`, and launches the runner. External
664
+ orchestrators use `agency claim`, launch and monitor their runner separately, and
665
+ later call `agency release` or `agency finish`.
647
666
 
648
667
  ### Archive
649
668
 
@@ -670,21 +689,21 @@ for restoration. Archived IDs are reserved until restored.
670
689
  ### Work and Pull Requests
671
690
 
672
691
  ```text
673
- agency work [<directory> | --epic <epic-id>] [--runner <name>] [--print-command]
692
+ agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
674
693
  agency work prepare [target] [--dry-run] [--json]
675
694
  agency worktree <list|inspect|prepare|remove|rebuild|repair>
676
695
  agency pr create <task-id> [phase-id] [--draft] [--json]
677
696
  ```
678
697
 
679
- `agency work` presents the full hierarchy in `fzf`. Pass a directory, including
680
- `.` for the current directory, to infer its epic, task, or phase. Outside a
681
- workbase, Agency first presents the registered workbases, then the selected
682
- workbase's hierarchy. If `fzf` is not installed, Agency prints the available
683
- choices and asks for an explicit directory.
698
+ `agency work` presents the full hierarchy in the native OpenTUI selector or the
699
+ configured external chooser. Pass a directory, including `.` for the current
700
+ directory, to infer its epic, task, or phase. Outside a workbase, Agency first
701
+ presents the registered workbases, then the selected workbase's hierarchy.
684
702
 
685
703
  OpenCode is the default runner, with automatic Claude fallback when neither is
686
704
  explicitly selected. `--opencode` and `--claude` remain aliases for requiring
687
- their built-in presets.
705
+ their built-in presets. Launches are interactive and promptless by default; use
706
+ `--auto` to send Agency's generated context prompt.
688
707
 
689
708
  `agency work prepare` resolves an execution unit and creates or reuses its
690
709
  writable and reference worktrees without launching an agent or changing status.
@@ -701,11 +720,10 @@ conflicting worktrees. Repair is deliberately conservative: it repairs safe Git
701
720
  registration issues and materializes missing checkouts, but never switches a
702
721
  branch, resets a commit, or discards uncommitted work.
703
722
 
704
- Epic and multi-phase task targets launch orchestration agents beside their
705
- documents. Single-phase tasks and phases fetch repositories, create or reuse
706
- worktrees under `code/`, and launch an execution agent in the writable checkout
707
- with absolute context paths. An explicit directory or `--epic` target bypasses
708
- the hierarchy chooser.
723
+ Agency launches every agent beside its epic or task document. Single-phase tasks
724
+ and phases first fetch repositories and create or reuse worktrees under `code/`,
725
+ then launch the execution agent from the task directory with absolute context
726
+ paths. An explicit directory or `--epic` target bypasses the hierarchy chooser.
709
727
 
710
728
  Each writable `(repo, branch)` pair may belong to only one task or phase. Agency
711
729
  validation reports duplicate ownership, and `agency work` checks Git's worktree
package/cli.ts CHANGED
@@ -478,6 +478,7 @@ const commands: Record<string, Command> = {
478
478
  opencode: options.opencode,
479
479
  claude: options.claude,
480
480
  runner: options.runner,
481
+ auto: options.auto,
481
482
  printCommand: options["print-command"],
482
483
  force: options.force,
483
484
  inputAllowed: options.inputAllowed,
@@ -720,7 +721,7 @@ try {
720
721
  const inputAllowed =
721
722
  !values.json &&
722
723
  !values["no-input"] &&
723
- Boolean(process.stdin.isTTY && process.stderr.isTTY)
724
+ Boolean(process.stdin.isTTY && process.stdout.isTTY)
724
725
  const cwd = await resolveInvocationCwd(commandName, values)
725
726
  if (values.json || (values.jsonl && values.help)) {
726
727
  const result = await collectCommandResult(() =>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.30.1",
3
+ "version": "2.32.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -58,7 +58,7 @@
58
58
  "tag": "latest"
59
59
  },
60
60
  "scripts": {
61
- "test": "find src -name '*.test.ts' -print0 | xargs -0 -n 1 -P 4 bun test",
61
+ "test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
62
62
  "format": "oxfmt",
63
63
  "format:check": "oxfmt --check",
64
64
  "knip": "knip --production",
@@ -68,7 +68,10 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@effect/schema": "^0.75.5",
71
+ "@opentui/core": "0.4.5",
72
+ "@opentui/solid": "0.4.5",
71
73
  "effect": "^3.19.15",
74
+ "solid-js": "1.9.12",
72
75
  "yaml": "^2.9.0"
73
76
  },
74
77
  "devDependencies": {
@@ -102,8 +102,8 @@ independently meaningful tasks need coordination.
102
102
  2. Confirm `target`, `graph.readiness`, `authority`, `workspace`, and `validation`.
103
103
  3. Read the returned task and phase document paths for prose requirements.
104
104
  4. Stop on validation errors, dependency blockers, an unexpected writable
105
- repository, or a conflicting active owner. For an active agent, a `working`
106
- status blocker is expected only when the current session owns the claim.
105
+ repository, or a conflicting active owner. A local `agency work` launch has
106
+ no claim; an externally assigned agent must own the active claim.
107
107
 
108
108
  ### Work
109
109
 
@@ -129,9 +129,10 @@ independently meaningful tasks need coordination.
129
129
 
130
130
  `agency work` is a human/orchestrator launch flow. It first reconciles managed
131
131
  integration files, then selects work and checks readiness. For an execution unit,
132
- it materializes managed checkouts, claims the unit, marks it working, and starts
132
+ it materializes managed checkouts, marks it working without a claim, and starts
133
133
  the selected built-in or configured runner. Epic and multi-phase task launches
134
- start in orchestration context without materializing or claiming execution work.
134
+ start in orchestration context without materializing execution work. The runner
135
+ opens without a prompt unless the human explicitly selects `--auto`.
135
136
  OpenCode launches expose the managed config file and inject runtime-only access
136
137
  rules scoped to the workbase. These grant visibility from orchestration
137
138
  directories and nested execution checkouts while denying direct edits outside
@@ -140,8 +141,8 @@ or replace write authority from `agency context`.
140
141
 
141
142
  An agent already running in an Agency checkout must not call `agency work` to
142
143
  start itself again. It should inspect context, perform the assigned work, and
143
- finish or release its existing claim. Launch a nested or replacement agent only
144
- when the user explicitly asks.
144
+ finish its claim or update unclaimed status directly. Launch a nested or
145
+ replacement agent only when the user explicitly asks.
145
146
 
146
147
  Use [`references/recipes.md`](references/recipes.md) for human setup, agent
147
148
  execution, claim, PR, conversion, and recovery workflows. Use
@@ -109,8 +109,9 @@ agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id>
109
109
  [--if-revision <hash>] [--json]
110
110
  ```
111
111
 
112
- `task new` is interactive and requires a TTY. Agents and scripts use
113
- noninteractive `task create`. Mutation commands that accept `--if-revision`
112
+ `task new` uses the OpenTUI Solid footer and requires a TTY. A sole repository is
113
+ selected automatically. Agents and scripts use noninteractive `task create`.
114
+ Mutation commands that accept `--if-revision`
114
115
  return a revision conflict instead of overwriting changed documents.
115
116
 
116
117
  ## Ownership And Lifecycle
@@ -143,7 +144,7 @@ worktrees when needed, preserve branches, and retain lifecycle provenance.
143
144
 
144
145
  ```text
145
146
  agency work [<directory-or-task-id> | --epic <id> | --task <id> [--phase <id>]]
146
- [--runner <name> | --opencode | --claude] [--print-command] [--force]
147
+ [--runner <name> | --opencode | --claude] [--auto] [--print-command] [--force]
147
148
  agency work prepare [target] [--dry-run] [--json]
148
149
  agency worktree list [--json]
149
150
  agency worktree inspect <task-id> [phase-id] [--json]
@@ -154,17 +155,19 @@ agency worktree repair <task-id> [phase-id] [--dry-run] [--json]
154
155
  agency pr create <task-id> [phase-id] [--draft] [--force] [--json]
155
156
  ```
156
157
 
157
- `work` is a launch flow, not an active-agent step. It synchronizes managed
158
- integration files before launch. Execution targets are materialized and claimed;
159
- epics and multi-phase tasks launch in orchestration context without those steps.
158
+ `work` is a local launch flow, not an active-agent step. It synchronizes managed
159
+ integration files before launch. Execution targets are materialized and marked
160
+ working without a claim; epics and multi-phase tasks launch in orchestration
161
+ context without those steps. Unclaimed working targets can be launched again.
162
+ The runner opens without a generated prompt unless `--auto` is set.
160
163
  The OpenCode runner receives a runtime managed-config path plus absolute access
161
164
  and edit rules scoped to the workbase; none are persisted, and `context` still
162
165
  defines write authority.
163
- `--print-command` suppresses only the final launch, so execution targets are still
164
- materialized and claimed before the command is printed. `work prepare`
165
- materializes without launching or changing status. Destructive remove and
166
- rebuild operations refuse dirty or conflicting state. Conservative repair may
167
- correct registration while preserving dirty files, but never discards changes.
166
+ `--print-command` materializes execution targets but does not launch or change
167
+ status. `work prepare` materializes without launching or changing status.
168
+ Destructive remove and rebuild operations refuse dirty or conflicting state.
169
+ Conservative repair may correct registration while preserving dirty files, but
170
+ never discards changes.
168
171
  `pr create` materializes a missing workspace, requires the resulting writable
169
172
  checkout to be clean, pushes the declared branch, invokes the configured delivery
170
173
  provider or falls back to `gh pr create --fill`, and records the returned pull
@@ -178,6 +181,6 @@ perform cwd inference elsewhere. Targeted commands accept `--epic`, `--task`,
178
181
  and, with a task, `--phase`. `--json`, `--no-input`, or non-TTY execution disables
179
182
  prompts; provide all selectors and required inputs explicitly.
180
183
 
181
- Selectors are only a resolution mechanism; they do not make discovery and claim
182
- atomic. There is no `assign` command. Use `work` for a local human launch, or use
183
- `claim` and manage an external runner separately.
184
+ Selectors are only a resolution mechanism. There is no `assign` command. Use
185
+ `work` for a local human launch, or use `claim` and manage an external runner
186
+ separately.
@@ -299,11 +299,10 @@ node set.
299
299
  and `claim` does not enforce dependency readiness. Inspect readiness, then
300
300
  claim with the observed revision and handle conflicts.
301
301
  - There is no `assign` command, remote queue, scheduler, heartbeat, claim renewal,
302
- runner monitor, or cancellation API. For an execution unit, `work` can claim
303
- and launch one local built-in or configured runner, but it is a
304
- process-launching, non-JSON flow rather than a machine assignment API. External
305
- orchestrators claim with claimant and runner IDs, then manage their runner
306
- themselves.
302
+ runner monitor, or cancellation API. `work` launches one local built-in or
303
+ configured runner without a claim; it is a process-launching, non-JSON flow
304
+ rather than a machine assignment API. External orchestrators claim with
305
+ claimant and runner IDs, then manage their runner themselves.
307
306
  - Agency does not edit code, create commits, run repository checks, wait for PR
308
307
  checks, merge PRs, or verify that a requested completion condition is true.
309
308
  `finish` records the caller's asserted outcome after ownership checks.
@@ -88,10 +88,13 @@ agency work tasks/checkout/phases/api
88
88
  ```
89
89
 
90
90
  `agency work` is intentionally last: it synchronizes managed integration files,
91
- checks readiness, materializes worktrees, creates a claim, marks this execution
92
- unit working, and launches the runner. OpenCode receives whole-workbase read
93
- visibility through runtime-only config and permission values; this does not
94
- expand the writable checkout reported by `agency context`.
91
+ checks readiness, materializes worktrees, marks this execution unit working
92
+ without a claim, and launches the runner. Run it again to relaunch unclaimed
93
+ working work. The runner opens without a prompt by default; add `--auto` only
94
+ when the generated context prompt should start autonomous execution.
95
+ OpenCode receives whole-workbase read visibility through runtime-only config and
96
+ permission values; this does not expand the writable checkout reported by
97
+ `agency context`.
95
98
 
96
99
  ## Active Agent: Execute Assigned Work
97
100
 
@@ -99,10 +102,9 @@ expand the writable checkout reported by `agency context`.
99
102
  agency context . --json
100
103
  ```
101
104
 
102
- 1. Verify context reports the expected execution target, valid structure, the
103
- current checkout as `authority.writable.checkoutPath`, and a claim owned by
104
- the current session. Reject dependency or validation blockers and conflicting
105
- claims; the owned unit's `working` status blocker is expected.
105
+ 1. Verify context reports the expected execution target, valid structure, and
106
+ the current checkout as `authority.writable.checkoutPath`. Reject dependency
107
+ or validation blockers and conflicting active claims.
106
108
  2. Read `TASK.md`, and `PHASE.md` for phase work.
107
109
  3. Implement only in the writable checkout; treat all reference checkouts as
108
110
  read-only.
@@ -110,7 +112,8 @@ agency context . --json
110
112
  5. Review and commit the diff.
111
113
  6. If requested, run `agency validate`, then
112
114
  `agency pr create <task-id> [phase-id]`.
113
- 7. Finish the current claim only after its completion condition is true.
115
+ 7. Finish an active claim after its completion condition is true; otherwise set
116
+ the task or phase status directly.
114
117
 
115
118
  Never invoke `agency work` merely because an execution checkout already exists;
116
119
  that would start another agent rather than continue the current assignment.
@@ -145,7 +148,7 @@ worktrees manually.
145
148
  ## Assign A Runner
146
149
 
147
150
  For a human-operated local launch, Agency combines readiness checks, prepare,
148
- claim, status mutation, and runner launch:
151
+ status mutation, and runner launch without creating a claim:
149
152
 
150
153
  ```bash
151
154
  agency work tasks/checkout/phases/ui --runner opencode
@@ -453,11 +453,18 @@ describe("strict CLI parsing", () => {
453
453
 
454
454
  test("accepts runner selection and command inspection for work", () => {
455
455
  expect(
456
- parseCli(["work", "example", "--runner", "custom", "--print-command"]),
456
+ parseCli([
457
+ "work",
458
+ "example",
459
+ "--runner",
460
+ "custom",
461
+ "--auto",
462
+ "--print-command",
463
+ ]),
457
464
  ).toMatchObject({
458
465
  commandName: "work",
459
466
  args: ["example"],
460
- values: { runner: "custom", "print-command": true },
467
+ values: { runner: "custom", auto: true, "print-command": true },
461
468
  })
462
469
  expect(() =>
463
470
  parseCli(["work", "example", "--runner", "custom", "--claude"]),
@@ -465,6 +472,9 @@ describe("strict CLI parsing", () => {
465
472
  expect(() => parseCli(["work", "prepare", "--print-command"])).toThrow(
466
473
  "cannot be combined",
467
474
  )
475
+ expect(() => parseCli(["work", "prepare", "--auto"])).toThrow(
476
+ "cannot be combined",
477
+ )
468
478
  })
469
479
 
470
480
  test("accepts repeatable graph filters and rejects output conflicts", () => {
package/src/cli-parser.ts CHANGED
@@ -740,13 +740,14 @@ const commands = {
740
740
  },
741
741
  work: {
742
742
  usage:
743
- "agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] | agency work prepare [target] [--dry-run] [--json]",
743
+ "agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] [--auto] | agency work prepare [target] [--dry-run] [--json]",
744
744
  options: {
745
745
  ...commonOptions,
746
746
  ...entitySelectorOptions,
747
747
  json: { type: "boolean" },
748
748
  "dry-run": { type: "boolean" },
749
749
  runner: { type: "string" },
750
+ auto: { type: "boolean" },
750
751
  "print-command": { type: "boolean" },
751
752
  opencode: { type: "boolean" },
752
753
  claude: { type: "boolean" },
@@ -754,7 +755,7 @@ const commands = {
754
755
  },
755
756
  command: {
756
757
  usage:
757
- "agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] | agency work prepare [target] [--dry-run] [--json]",
758
+ "agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>] [--auto] | agency work prepare [target] [--dry-run] [--json]",
758
759
  minArgs: 0,
759
760
  maxArgs: 2,
760
761
  options: [
@@ -764,6 +765,7 @@ const commands = {
764
765
  "task",
765
766
  "phase",
766
767
  "runner",
768
+ "auto",
767
769
  "print-command",
768
770
  "opencode",
769
771
  "claude",
@@ -1361,6 +1363,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
1361
1363
  parsed.values.opencode ||
1362
1364
  parsed.values.claude ||
1363
1365
  parsed.values.runner ||
1366
+ parsed.values.auto ||
1364
1367
  parsed.values["print-command"] ||
1365
1368
  parsed.values.force))
1366
1369
  ) {
package/src/cli.test.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { afterAll, afterEach, describe, expect, test } from "bun:test"
2
2
  import { access, mkdir, realpath } from "node:fs/promises"
3
- import { join } from "node:path"
3
+ import { join, relative } from "node:path"
4
4
  import errorFixture from "../fixtures/protocol/error.json"
5
5
  import successFixture from "../fixtures/protocol/success.json"
6
6
  import { cleanupTempDir, createTempDir } from "./test-utils"
@@ -92,8 +92,15 @@ async function startGitDaemon(basePath: string) {
92
92
 
93
93
  describe("CLI", () => {
94
94
  const tempDirs: string[] = []
95
+ const daemons: Bun.Subprocess[] = []
95
96
 
96
97
  afterEach(async () => {
98
+ await Promise.all(
99
+ daemons.splice(0).map(async (daemon) => {
100
+ daemon.kill()
101
+ await daemon.exited
102
+ }),
103
+ )
97
104
  await Promise.all(tempDirs.splice(0).map(cleanupTempDir))
98
105
  })
99
106
 
@@ -445,7 +452,7 @@ describe("CLI", () => {
445
452
 
446
453
  const after = await runCli(["status", "--silent"], root)
447
454
  expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
448
- }, 10_000)
455
+ }, 30_000)
449
456
 
450
457
  test("lists ready work and exposes excluded blockers through one result", async () => {
451
458
  const root = await createTempDir()
@@ -632,7 +639,7 @@ describe("CLI", () => {
632
639
  expect(JSON.parse(explicitOutside.stdout).error.message).toContain(
633
640
  "No Agency workbase found from",
634
641
  )
635
- })
642
+ }, 20_000)
636
643
 
637
644
  test("exports equivalent JSON and JSONL graph contracts", async () => {
638
645
  const root = await createTempDir()
@@ -849,6 +856,10 @@ status: open
849
856
  ]) {
850
857
  expect(Bun.spawnSync(["git", "-C", source, ...args]).exitCode).toBe(0)
851
858
  }
859
+ await runGit(["clone", "--bare", source, join(parent, "source.git")])
860
+ const daemon = await startGitDaemon(parent)
861
+ daemons.push(daemon.process)
862
+ await runGit(["-C", source, "remote", "add", "origin", daemon.remote])
852
863
 
853
864
  parseJson(await runCli(["init", root, "--json"], parent))
854
865
  parseJson(
@@ -959,23 +970,23 @@ status: open
959
970
  const launches = [
960
971
  {
961
972
  args: ["--task", "example"],
962
- cwd: taskWorkspace.writablePath,
963
- writable: true,
973
+ cwd: join(workbaseRoot, "tasks/example"),
974
+ writable: taskWorkspace.writablePath,
964
975
  },
965
976
  {
966
977
  args: ["--task", "pipeline", "--phase", "build"],
967
- cwd: phaseWorkspace.writablePath,
968
- writable: true,
978
+ cwd: join(workbaseRoot, "tasks/pipeline"),
979
+ writable: phaseWorkspace.writablePath,
969
980
  },
970
981
  {
971
982
  args: ["--epic", "delivery"],
972
983
  cwd: join(workbaseRoot, "epics/delivery"),
973
- writable: false,
984
+ writable: null,
974
985
  },
975
986
  {
976
987
  args: ["--task", "pipeline"],
977
988
  cwd: join(workbaseRoot, "tasks/pipeline"),
978
- writable: false,
989
+ writable: null,
979
990
  },
980
991
  ]
981
992
  for (const launch of launches) {
@@ -1015,11 +1026,24 @@ status: open
1015
1026
  expect.arrayContaining([
1016
1027
  expect.objectContaining({
1017
1028
  permission: "edit",
1018
- pattern: launch.writable ? "../**" : "*",
1029
+ pattern: "*",
1019
1030
  action: "deny",
1020
1031
  }),
1021
1032
  ]),
1022
1033
  )
1034
+ if (launch.writable) {
1035
+ expect(agent.permission).toEqual(
1036
+ expect.arrayContaining(
1037
+ [workbaseRoot, launch.cwd].map((base) =>
1038
+ expect.objectContaining({
1039
+ permission: "edit",
1040
+ pattern: join(relative(base, launch.writable!), "**"),
1041
+ action: "allow",
1042
+ }),
1043
+ ),
1044
+ ),
1045
+ )
1046
+ }
1023
1047
 
1024
1048
  if (launch === launches[0]) {
1025
1049
  for (const document of documents) {
@@ -39,7 +39,12 @@ describe("doctor command", () => {
39
39
  "agency.json",
40
40
  JSON.stringify({
41
41
  version: 2,
42
- runners: { missing: { command: ["definitely-not-installed"] } },
42
+ runners: {
43
+ missing: {
44
+ command: ["definitely-not-installed"],
45
+ autoCommand: ["also-not-installed", "{prompt}"],
46
+ },
47
+ },
43
48
  }),
44
49
  )
45
50
  await mkdir(join(root, "repos"), { recursive: true })
@@ -86,6 +91,11 @@ status: open
86
91
  level: "error",
87
92
  status: "fail",
88
93
  }),
94
+ expect.objectContaining({
95
+ id: "integration.runner.missing.auto",
96
+ level: "error",
97
+ status: "fail",
98
+ }),
89
99
  expect.objectContaining({
90
100
  id: "ref.agency.main",
91
101
  status: "pass",
@@ -259,7 +259,7 @@ Subcommands:
259
259
  list <task> List task phases
260
260
  show <task> <phase> Show a phase
261
261
  status <task> <phase> <status>
262
- Set open, done, or dropped
262
+ Set open, working, done, or dropped
263
263
  update <task> <phase> Update phase metadata
264
264
  rename <task> <phase> <new-id>
265
265
  Rename a phase and update dependencies