@markjaquith/agency 2.31.0 → 2.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -183,7 +183,8 @@ OpenCode and Claude Code are built-in runner presets. Select either preset or a
183
183
  configured runner with `agency work --runner <name>`. A launch is fresh unless
184
184
  `AGENCY_SESSION_ID` is already set; resumed launches use the runner's
185
185
  `resumeCommand` when configured. The built-in presets use `--continue` only for
186
- 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.
187
188
 
188
189
  Custom runners are direct argv commands, never shell snippets:
189
190
 
@@ -192,8 +193,10 @@ Custom runners are direct argv commands, never shell snippets:
192
193
  "version": 2,
193
194
  "runners": {
194
195
  "custom": {
195
- "command": ["my-agent", "--prompt", "{prompt}"],
196
- "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}"],
197
200
  "environment": { "MY_AGENT_TARGET": "{target}" }
198
201
  }
199
202
  }
@@ -202,24 +205,32 @@ Custom runners are direct argv commands, never shell snippets:
202
205
 
203
206
  Available placeholders are `{prompt}`, `{workbase}`, `{target}`, `{task}`,
204
207
  `{phase}`, `{claimant}`, `{sessionId}`, and `{claimRevision}`. Task and phase
205
- placeholders are empty when they do not apply. If `resumeCommand` is omitted,
206
- 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`.
207
212
 
208
213
  Every runner receives the same `AGENCY_RUNNER`, `AGENCY_CLAIMANT`,
209
214
  `AGENCY_SESSION_ID`, `AGENCY_CLAIM_REVISION`, `AGENCY_WORKBASE`, `AGENCY_TARGET`,
210
215
  `AGENCY_TASK_ID`, `AGENCY_PHASE_ID`, and `AGENCY_PROMPT` environment. Configured
211
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.
212
219
  The `opencode` runner additionally receives `OPENCODE_CONFIG` for the workbase's
213
220
  managed integration and `OPENCODE_CONFIG_CONTENT` with runtime-only,
214
221
  workbase-scoped access and edit rules. These values keep the Git-synced config
215
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.
216
225
  `--print-command` prints the exact cwd and argv plus non-secret environment keys
217
226
  without launching the runner.
218
227
 
219
228
  ### Custom Chooser Command
220
229
 
221
- Interactive selection uses a native numbered chooser by default. To use an
222
- 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`:
223
234
 
224
235
  ```json
225
236
  {
@@ -231,13 +242,14 @@ external chooser, configure an argv command in `agency.json`:
231
242
  Agency writes one `key<TAB>label` record per choice to the command's stdin. The
232
243
  command must write the selected opaque key or selected record to stdout; commands
233
244
  such as `["gum", "filter"]` therefore work without wrappers. Exit codes 1 and
234
- 130, empty stdout, native `q`, and an empty native response cancel selection.
235
- 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.
236
247
 
237
- Selectors are opened only when stdin and stderr are terminals and neither
238
- `--no-input` nor JSON output is active. Labels use color only when stderr is a
239
- terminal, `TERM` is not `dumb`, and `NO_COLOR` is unset; otherwise selectors use
240
- 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.
241
253
 
242
254
  ## Frontmatter
243
255
 
@@ -500,8 +512,9 @@ back-reference.
500
512
 
501
513
  ### Tasks
502
514
 
503
- Create a task interactively. Text prompts identify optional values, and known
504
- 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`:
505
518
 
506
519
  ```text
507
520
  agency task new [id]
@@ -606,12 +619,13 @@ writing anything.
606
619
 
607
620
  Single-phase tasks and phases store status in YAML. New execution units start
608
621
  `open`, and `agency work` marks the selected execution unit `working` immediately
609
- before launch. Use claims for coordinated ownership and the status subcommands
610
- for manual lifecycle overrides. The interactive work selector displays status
611
- markers before execution units. Existing working and delegated work may be
612
- released to `open` or assigned a terminal outcome. Done and dropped work are
613
- terminal and may only remain unchanged or transition to open; reopen terminal
614
- 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.
615
629
 
616
630
  `delegated` remains readable for existing workbases but cannot be newly assigned.
617
631
  Delegation is now explicit: the claimant identifies the orchestrator and the
@@ -645,11 +659,10 @@ frontmatter. Conflicts return the current revision and complete ownership record
645
659
  in the machine error envelope rather than overwriting it. Expired claims may be
646
660
  replaced with a revision-guarded claim.
647
661
 
648
- `agency work` claims an execution unit before launching its agent. Set
649
- `AGENCY_CLAIMANT`, `AGENCY_RUNNER`, or `AGENCY_SESSION_ID` to supply orchestrator
650
- identities; otherwise Agency derives them from the user and process. The selected
651
- runner name is recorded on the claim. The launched agent receives the normalized
652
- 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`.
653
666
 
654
667
  ### Archive
655
668
 
@@ -676,21 +689,21 @@ for restoration. Archived IDs are reserved until restored.
676
689
  ### Work and Pull Requests
677
690
 
678
691
  ```text
679
- agency work [<directory> | --epic <epic-id>] [--runner <name>] [--print-command]
692
+ agency work [<directory> | --epic <epic-id>] [--runner <name>] [--auto] [--print-command]
680
693
  agency work prepare [target] [--dry-run] [--json]
681
694
  agency worktree <list|inspect|prepare|remove|rebuild|repair>
682
695
  agency pr create <task-id> [phase-id] [--draft] [--json]
683
696
  ```
684
697
 
685
- `agency work` presents the full hierarchy in `fzf`. Pass a directory, including
686
- `.` for the current directory, to infer its epic, task, or phase. Outside a
687
- workbase, Agency first presents the registered workbases, then the selected
688
- workbase's hierarchy. If `fzf` is not installed, Agency prints the available
689
- 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.
690
702
 
691
703
  OpenCode is the default runner, with automatic Claude fallback when neither is
692
704
  explicitly selected. `--opencode` and `--claude` remain aliases for requiring
693
- 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.
694
707
 
695
708
  `agency work prepare` resolves an execution unit and creates or reuses its
696
709
  writable and reference worktrees without launching an agent or changing status.
@@ -707,11 +720,10 @@ conflicting worktrees. Repair is deliberately conservative: it repairs safe Git
707
720
  registration issues and materializes missing checkouts, but never switches a
708
721
  branch, resets a commit, or discards uncommitted work.
709
722
 
710
- Epic and multi-phase task targets launch orchestration agents beside their
711
- documents. Single-phase tasks and phases fetch repositories, create or reuse
712
- worktrees under `code/`, and launch an execution agent in the writable checkout
713
- with absolute context paths. An explicit directory or `--epic` target bypasses
714
- 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.
715
727
 
716
728
  Each writable `(repo, branch)` pair may belong to only one task or phase. Agency
717
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.31.0",
3
+ "version": "2.32.1",
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
@@ -61,10 +61,43 @@ describe("task creation input", () => {
61
61
  "Description (optional): ",
62
62
  "Parent epic: (none),delivery",
63
63
  "Task type: single-phase,multi-phase",
64
- "Writable repository: agency",
65
64
  ])
66
65
  })
67
66
 
67
+ test("asks for a repository only when more than one is available", async () => {
68
+ await mkdir(join(root, "repos/web"), { recursive: true })
69
+ const prompts: string[] = []
70
+ const interaction: TaskInteraction = {
71
+ text: () => Effect.fail(new Error("unexpected text prompt")),
72
+ select: (prompt, choices) => {
73
+ prompts.push(`${prompt}: ${choices.join(",")}`)
74
+ return Effect.succeed(prompt === "Task type" ? "single-phase" : "web")
75
+ },
76
+ }
77
+
78
+ await runTestEffect(
79
+ task(
80
+ {
81
+ subcommand: "new",
82
+ args: ["multi-repo"],
83
+ ticketUrl: "",
84
+ description: "",
85
+ cwd: root,
86
+ silent: true,
87
+ },
88
+ interaction,
89
+ ),
90
+ )
91
+
92
+ expect(prompts).toEqual([
93
+ "Task type: single-phase,multi-phase",
94
+ "Writable repository: agency,web",
95
+ ])
96
+ expect(
97
+ await Bun.file(join(root, "tasks/multi-repo/TASK.md")).text(),
98
+ ).toContain("repo: web")
99
+ })
100
+
68
101
  test("keeps scripted creation non-interactive and permits no ticket URL", async () => {
69
102
  const interaction: TaskInteraction = {
70
103
  text: () => Effect.fail(new Error("unexpected text prompt")),