@markjaquith/agency 2.3.0 → 2.5.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 (37) hide show
  1. package/README.md +45 -16
  2. package/cli.ts +21 -0
  3. package/package.json +1 -1
  4. package/skills/agency/SKILL.md +31 -4
  5. package/src/cli.test.ts +1 -0
  6. package/src/commands/archive.test.ts +69 -0
  7. package/src/commands/archive.ts +70 -0
  8. package/src/commands/init.test.ts +3 -0
  9. package/src/commands/phase.ts +23 -1
  10. package/src/commands/pr.ts +1 -0
  11. package/src/commands/task-phase.test.ts +46 -1
  12. package/src/commands/task.test.ts +94 -0
  13. package/src/commands/task.ts +160 -17
  14. package/src/commands/work.test.ts +63 -34
  15. package/src/commands/work.ts +20 -3
  16. package/src/services/ArchiveService.test.ts +334 -0
  17. package/src/services/ArchiveService.ts +246 -0
  18. package/src/services/FileSystemService.ts +7 -2
  19. package/src/services/PhaseService.ts +28 -0
  20. package/src/services/PullRequestService.ts +3 -0
  21. package/src/services/TaskPhaseService.test.ts +79 -0
  22. package/src/services/TaskService.ts +31 -1
  23. package/src/services/WorkbaseService.test.ts +66 -0
  24. package/src/services/WorkbaseService.ts +32 -2
  25. package/src/services/WorktreeService.test.ts +210 -2
  26. package/src/services/WorktreeService.ts +143 -1
  27. package/src/test-utils.ts +2 -0
  28. package/src/utils/process.test.ts +38 -1
  29. package/src/utils/process.ts +30 -6
  30. package/src/utils/progress.test.ts +37 -0
  31. package/src/utils/progress.ts +36 -0
  32. package/src/workbase/AGENTS.md +3 -0
  33. package/src/workbase/opencode-file.ts +37 -0
  34. package/src/workbase/schemas.test.ts +50 -0
  35. package/src/workbase/schemas.ts +6 -2
  36. package/src/workbase/work-target.test.ts +9 -9
  37. package/src/workbase/work-target.ts +36 -6
package/README.md CHANGED
@@ -43,6 +43,8 @@ frontmatter; prose below it supplies human and agent context.
43
43
  ```text
44
44
  workbase/
45
45
  AGENTS.md # managed workbase instructions
46
+ .opencode/
47
+ opencode.jsonc # managed task and epic references
46
48
  agency.json
47
49
  repos/
48
50
  frontend/ # bare Git repository or symlink
@@ -69,10 +71,11 @@ workbase/
69
71
  backend/
70
72
  ```
71
73
 
72
- Agency creates `AGENTS.md` during initialization and ensures it exists whenever
73
- the workbase is discovered. A checksum in the generated file lets newer Agency
74
- versions refresh unmodified instructions while preserving custom or edited
75
- files.
74
+ Agency creates `AGENTS.md` and `.opencode/opencode.jsonc` during initialization
75
+ and ensures they exist whenever the workbase is discovered. The OpenCode config
76
+ grants external-directory access to task and epic references. Checksums in the
77
+ generated files let newer Agency versions refresh unmodified content while
78
+ preserving custom or edited files.
76
79
 
77
80
  Repository metadata comes directly from Git under `repos/{alias}`. Workbase
78
81
  configuration may provide a custom writable-worktree creation command.
@@ -177,6 +180,7 @@ repos:
177
180
  branch: task/refresh-copy
178
181
  base: main
179
182
  pr: null
183
+ status: open
180
184
  ---
181
185
  ```
182
186
 
@@ -207,6 +211,7 @@ repos:
207
211
  branch: task/checkout-ui
208
212
  base: task/checkout-api
209
213
  pr: null
214
+ status: open
210
215
  ---
211
216
  ```
212
217
 
@@ -222,13 +227,7 @@ cd ~/work
222
227
  agency repo add frontend git@github.com:example/frontend.git
223
228
  agency repo link backend ~/Dev/backend
224
229
 
225
- agency task create refresh-copy \
226
- --ticket-url https://example.com/tickets/refresh-copy \
227
- --description "Refresh user-facing checkout copy" \
228
- --repo frontend \
229
- --reference backend:main \
230
- --branch task/refresh-copy \
231
- --base main
230
+ agency task new
232
231
 
233
232
  agency validate
234
233
  agency work refresh-copy
@@ -266,19 +265,28 @@ back-reference.
266
265
 
267
266
  ### Tasks
268
267
 
268
+ Create a task interactively. Text prompts identify optional values, and known
269
+ choices use fzf:
270
+
271
+ ```text
272
+ agency task new [id]
273
+ ```
274
+
269
275
  Create a single-phase task:
270
276
 
271
277
  ```text
272
- agency task create <id> --ticket-url <url> --repo <alias>
273
- --branch <name> --base <name>
274
- [--description <text>] [--epic <id>] [--reference <alias>:<ref>...] [--json]
278
+ agency task create <id> --repo <alias>
279
+ [--ticket-url <url>] [--description <text>] [--epic <id>]
280
+ [--reference <alias>:<ref>...] [--branch <name>] [--base <name>] [--json]
275
281
  ```
276
282
 
283
+ The branch defaults to `task/<id>` and the base defaults to `main`.
284
+
277
285
  Create a multi-phase task container:
278
286
 
279
287
  ```text
280
- agency task create <id> --ticket-url <url> --multi-phase
281
- [--description <text>] [--epic <id>] [--json]
288
+ agency task create <id> --multi-phase
289
+ [--ticket-url <url>] [--description <text>] [--epic <id>] [--json]
282
290
  ```
283
291
 
284
292
  Inspect tasks:
@@ -286,6 +294,7 @@ Inspect tasks:
286
294
  ```text
287
295
  agency task list [--json]
288
296
  agency task show <id> [--json]
297
+ agency task status <id> <open|working|done|dropped> [--json]
289
298
  ```
290
299
 
291
300
  To add a phase to an existing single-phase task, name the phase that will own
@@ -312,8 +321,28 @@ agency phase create <task-id> <phase-id>
312
321
 
313
322
  agency phase list <task-id> [--json]
314
323
  agency phase show <task-id> <phase-id> [--json]
324
+ agency phase status <task-id> <phase-id> <open|working|done|dropped> [--json]
315
325
  ```
316
326
 
327
+ Single-phase tasks and phases store status in YAML. New execution units start
328
+ `open`, and `agency work` marks the selected execution unit `working` immediately
329
+ before launch. Use the status subcommands to mark work `done`, `dropped`, or open
330
+ it again. The interactive work selector displays status markers before execution
331
+ units.
332
+
333
+ ### Archive
334
+
335
+ ```text
336
+ agency archive epic <epic-id> [--json]
337
+ agency archive task <task-id> [--json]
338
+ agency archive phase <task-id> <phase-id> [--json]
339
+ ```
340
+
341
+ Archived work keeps its hierarchy under `archive/`. Epic archiving includes its
342
+ listed tasks. Task and phase archiving update the active parent document and
343
+ reject items that active siblings depend on. Agency removes registered worktrees
344
+ before moving files, refuses dirty worktrees, and preserves branches.
345
+
317
346
  ### Work and Pull Requests
318
347
 
319
348
  ```text
package/cli.ts CHANGED
@@ -11,6 +11,7 @@ import { validate, help as validateHelp } from "./src/commands/validate"
11
11
  import { repo, help as repoHelp } from "./src/commands/repo"
12
12
  import { epic, help as epicHelp } from "./src/commands/epic"
13
13
  import { phase, help as phaseHelp } from "./src/commands/phase"
14
+ import { archive, help as archiveHelp } from "./src/commands/archive"
14
15
  import type { Command } from "./src/types"
15
16
  import { FileSystemService } from "./src/services/FileSystemService"
16
17
  import { WorkbaseService } from "./src/services/WorkbaseService"
@@ -20,6 +21,7 @@ import { TaskService } from "./src/services/TaskService"
20
21
  import { PhaseService } from "./src/services/PhaseService"
21
22
  import { WorktreeService } from "./src/services/WorktreeService"
22
23
  import { PullRequestService } from "./src/services/PullRequestService"
24
+ import { ArchiveService } from "./src/services/ArchiveService"
23
25
 
24
26
  // Create CLI layer with all services
25
27
  const CliLayer = Layer.mergeAll(
@@ -31,6 +33,7 @@ const CliLayer = Layer.mergeAll(
31
33
  PhaseService.Default,
32
34
  WorktreeService.Default,
33
35
  PullRequestService.Default,
36
+ ArchiveService.Default,
34
37
  )
35
38
 
36
39
  /**
@@ -153,6 +156,23 @@ const commands: Record<string, Command> = {
153
156
  )
154
157
  },
155
158
  },
159
+ archive: {
160
+ run: async (args: string[], options: Record<string, any>) => {
161
+ if (options.help) {
162
+ console.log(archiveHelp)
163
+ return
164
+ }
165
+ await runCommand(
166
+ archive({
167
+ type: args[0],
168
+ args: args.slice(1),
169
+ json: options.json,
170
+ silent: options.silent,
171
+ verbose: options.verbose,
172
+ }),
173
+ )
174
+ },
175
+ },
156
176
  repo: {
157
177
  run: async (args: string[], options: Record<string, any>) => {
158
178
  if (options.help) {
@@ -257,6 +277,7 @@ Commands:
257
277
  init [path] Initialize an Agency workbase
258
278
  epic <subcommand> Manage epics
259
279
  phase <subcommand> Manage task phases
280
+ archive <type> Archive a work item
260
281
  task <subcommand> Manage tasks
261
282
  work [task] [phase] Work on an epic, task, or phase
262
283
  pr create Create a pull request for an execution unit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -100,17 +100,22 @@ Epic task ordering and dependencies live in `EPIC.md`. Creating a task with
100
100
 
101
101
  ## Create Single-Phase Tasks
102
102
 
103
+ For guided creation, run `agency task new`. It prompts for text input, uses fzf
104
+ for known choices, and allows optional inputs to be skipped.
105
+
103
106
  ```bash
104
107
  agency task create <id> \
105
- --ticket-url <url> \
108
+ [--ticket-url <url>] \
106
109
  [--description <text>] \
107
110
  [--epic <epic-id>] \
108
111
  --repo <writable-alias> \
109
112
  [--reference <read-only-alias>:<ref>] \
110
- --branch <branch> \
111
- --base <base>
113
+ [--branch <branch>] \
114
+ [--base <base>]
112
115
  ```
113
116
 
117
+ The branch defaults to `task/<id>` and the base defaults to `main`.
118
+
114
119
  The task itself is the execution unit. Its worktrees live under
115
120
  `tasks/{id}/code/{alias}`.
116
121
 
@@ -120,7 +125,7 @@ Create the task container:
120
125
 
121
126
  ```bash
122
127
  agency task create <id> \
123
- --ticket-url <url> \
128
+ [--ticket-url <url>] \
124
129
  [--description <text>] \
125
130
  [--epic <epic-id>] \
126
131
  --multi-phase
@@ -170,6 +175,7 @@ repos:
170
175
  branch: task/example
171
176
  base: main
172
177
  pr: null
178
+ status: open
173
179
  ---
174
180
  ```
175
181
 
@@ -189,6 +195,8 @@ output, including mutations, entity inspection, status, validation, and PR creat
189
195
  - Multi-phase task frontmatter owns the phase dependency graph.
190
196
  - Epic frontmatter owns the child-task dependency graph.
191
197
  - `pr` is either a GitHub PR URL string or `null`.
198
+ - Execution-unit `status` is `open`, `working`, `done`, or `dropped`. New work
199
+ starts open, and `agency work` marks it working before agent launch.
192
200
  - Keep directory IDs stable; encode sequencing with `dependsOn`, not numeric
193
201
  directory prefixes.
194
202
  - Do not use YAML duplicate keys, anchors, aliases, or custom tags.
@@ -196,6 +204,25 @@ output, including mutations, entity inspection, status, validation, and PR creat
196
204
  Prefer Agency commands for creation. When manually editing dependencies or
197
205
  prose, preserve backlinks and run validation immediately afterward.
198
206
 
207
+ Update execution status with:
208
+
209
+ ```bash
210
+ agency task status <task-id> <open|working|done|dropped>
211
+ agency phase status <task-id> <phase-id> <open|working|done|dropped>
212
+ ```
213
+
214
+ ## Archive Completed Work
215
+
216
+ ```bash
217
+ agency archive epic <epic-id>
218
+ agency archive task <task-id>
219
+ agency archive phase <task-id> <phase-id>
220
+ ```
221
+
222
+ Use these commands instead of moving work item folders manually. Agency mirrors
223
+ their hierarchy under `archive/`, removes registered worktrees first, and keeps
224
+ branches. It refuses dirty worktrees and active sibling dependencies.
225
+
199
226
  ## Validate Every Structural Change
200
227
 
201
228
  ```bash
package/src/cli.test.ts CHANGED
@@ -80,6 +80,7 @@ describe("CLI", () => {
80
80
  ["epic", "Usage: agency epic"],
81
81
  ["task", "Usage: agency task"],
82
82
  ["phase", "Usage: agency phase"],
83
+ ["archive", "Usage: agency archive"],
83
84
  ["work", "Usage: agency work"],
84
85
  ["pr", "Usage: agency pr"],
85
86
  ["status", "Usage: agency status"],
@@ -0,0 +1,69 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { join } from "node:path"
3
+ import {
4
+ captureLogs,
5
+ cleanupTempDir,
6
+ createTempDir,
7
+ runTestEffect,
8
+ } from "../test-utils"
9
+ import { archive } from "./archive"
10
+ import { task } from "./task"
11
+
12
+ describe("archive command", () => {
13
+ let root: string
14
+
15
+ beforeEach(async () => {
16
+ root = await createTempDir()
17
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
18
+ const initialized = Bun.spawnSync([
19
+ "git",
20
+ "init",
21
+ "--bare",
22
+ join(root, "repos/agency"),
23
+ ])
24
+ if (initialized.exitCode !== 0) {
25
+ throw new Error(new TextDecoder().decode(initialized.stderr))
26
+ }
27
+ await runTestEffect(
28
+ task({
29
+ subcommand: "create",
30
+ args: ["example"],
31
+ ticketUrl: "https://example.com/task",
32
+ repo: "agency",
33
+ branch: "task/example",
34
+ base: "main",
35
+ cwd: root,
36
+ silent: true,
37
+ }),
38
+ )
39
+ })
40
+
41
+ afterEach(async () => cleanupTempDir(root))
42
+
43
+ test("archives a task and outputs the result as JSON", async () => {
44
+ const logs = await captureLogs(() =>
45
+ runTestEffect(
46
+ archive({
47
+ type: "task",
48
+ args: ["example"],
49
+ cwd: root,
50
+ json: true,
51
+ }),
52
+ ),
53
+ )
54
+
55
+ expect(JSON.parse(logs[0]!)).toEqual({
56
+ kind: "task",
57
+ id: "example",
58
+ path: join(root, "archive/tasks/example"),
59
+ archivedPaths: [join(root, "archive/tasks/example")],
60
+ removedWorktrees: [],
61
+ })
62
+ })
63
+
64
+ test("requires a supported work item type", async () => {
65
+ await expect(
66
+ runTestEffect(archive({ args: [], cwd: root, silent: true })),
67
+ ).rejects.toThrow("Available: epic, task, phase")
68
+ })
69
+ })
@@ -0,0 +1,70 @@
1
+ import { Effect } from "effect"
2
+ import { ArchiveService } from "../services/ArchiveService"
3
+ import type { BaseCommandOptions } from "../utils/command"
4
+ import { createLoggers } from "../utils/effect"
5
+
6
+ interface ArchiveOptions extends BaseCommandOptions {
7
+ readonly type?: string
8
+ readonly args: readonly string[]
9
+ readonly json?: boolean
10
+ }
11
+
12
+ export const archive = (options: ArchiveOptions) =>
13
+ Effect.gen(function* () {
14
+ const archives = yield* ArchiveService
15
+ const { log } = createLoggers(options)
16
+ const cwd = options.cwd ?? process.cwd()
17
+ const [id, phaseId] = options.args
18
+
19
+ let result
20
+ switch (options.type) {
21
+ case "epic":
22
+ if (!id) {
23
+ return yield* Effect.fail(
24
+ new Error("Usage: agency archive epic <epic-id>"),
25
+ )
26
+ }
27
+ result = yield* archives.archiveEpic(id, cwd)
28
+ break
29
+ case "task":
30
+ if (!id) {
31
+ return yield* Effect.fail(
32
+ new Error("Usage: agency archive task <task-id>"),
33
+ )
34
+ }
35
+ result = yield* archives.archiveTask(id, cwd)
36
+ break
37
+ case "phase":
38
+ if (!id || !phaseId) {
39
+ return yield* Effect.fail(
40
+ new Error("Usage: agency archive phase <task-id> <phase-id>"),
41
+ )
42
+ }
43
+ result = yield* archives.archivePhase(id, phaseId, cwd)
44
+ break
45
+ default:
46
+ return yield* Effect.fail(
47
+ new Error("Work item type is required. Available: epic, task, phase"),
48
+ )
49
+ }
50
+
51
+ log(
52
+ options.json
53
+ ? JSON.stringify(result, null, 2)
54
+ : `Archived ${result.kind} '${result.id}' to ${result.path}`,
55
+ )
56
+ })
57
+
58
+ export const help = `
59
+ Usage: agency archive <type> <id>
60
+
61
+ Archive a work item after removing its worktrees. Branches are preserved.
62
+
63
+ Types:
64
+ epic <epic-id> Archive an epic and its tasks
65
+ task <task-id> Archive a task
66
+ phase <task-id> <phase-id> Archive a phase
67
+
68
+ Options:
69
+ --json Output results as JSON
70
+ `
@@ -36,6 +36,9 @@ describe("init command", () => {
36
36
  expect(await Bun.file(join(root, "AGENTS.md")).text()).toContain(
37
37
  "# Agency Workbase",
38
38
  )
39
+ expect(
40
+ await Bun.file(join(root, ".opencode/opencode.jsonc")).text(),
41
+ ).toContain('"path": "../tasks"')
39
42
  })
40
43
 
41
44
  test("preserves existing gitignore entries", async () => {
@@ -90,9 +90,29 @@ export const phase = (options: PhaseOptions) =>
90
90
  )
91
91
  return
92
92
  }
93
+ case "status": {
94
+ const status = options.args[2]
95
+ if (!taskId || !phaseId || !status) {
96
+ return yield* Effect.fail(
97
+ new Error(
98
+ "Usage: agency phase status <task-id> <phase-id> <status>",
99
+ ),
100
+ )
101
+ }
102
+ const record = yield* phases.setStatus(taskId, phaseId, status, cwd)
103
+ const { content: _, ...output } = record
104
+ log(
105
+ options.json
106
+ ? JSON.stringify(output, null, 2)
107
+ : `Marked phase '${phaseId}' as ${record.data.status}`,
108
+ )
109
+ return
110
+ }
93
111
  default:
94
112
  return yield* Effect.fail(
95
- new Error("Subcommand is required. Available: create, list, show"),
113
+ new Error(
114
+ "Subcommand is required. Available: create, list, show, status",
115
+ ),
96
116
  )
97
117
  }
98
118
  })
@@ -104,6 +124,8 @@ Subcommands:
104
124
  create <task> <phase> Create a phase
105
125
  list <task> List task phases
106
126
  show <task> <phase> Show a phase
127
+ status <task> <phase> <status>
128
+ Set open, working, done, or dropped
107
129
 
108
130
  Create options:
109
131
  --description <text> Short description of the phase
@@ -24,6 +24,7 @@ export const pr = (options: PrOptions) =>
24
24
  options.phaseId,
25
25
  options.draft,
26
26
  options.cwd ?? process.cwd(),
27
+ options,
27
28
  )
28
29
  log(options.json ? JSON.stringify({ url }, null, 2) : url)
29
30
  })
@@ -95,6 +95,7 @@ describe("task and phase command JSON output", () => {
95
95
  branch: "task/first",
96
96
  base: "main",
97
97
  pr: null,
98
+ status: "open",
98
99
  },
99
100
  },
100
101
  ])
@@ -118,6 +119,7 @@ describe("task and phase command JSON output", () => {
118
119
  branch: "task/first",
119
120
  base: "main",
120
121
  pr: null,
122
+ status: "open",
121
123
  },
122
124
  })
123
125
  })
@@ -156,10 +158,53 @@ describe("task and phase command JSON output", () => {
156
158
  expect(JSON.parse(phaseLogs[0]!)).toMatchObject({
157
159
  taskId: "another",
158
160
  id: "first",
159
- data: { repo: "agency", branch: "task/another-first", base: "main" },
161
+ data: {
162
+ repo: "agency",
163
+ branch: "task/another-first",
164
+ base: "main",
165
+ status: "open",
166
+ },
160
167
  })
161
168
  })
162
169
 
170
+ test("sets task and phase status", async () => {
171
+ const phaseLogs = await captureLogs(() =>
172
+ runTestEffect(
173
+ phase({
174
+ subcommand: "status",
175
+ args: ["multi", "first", "done"],
176
+ cwd: root,
177
+ json: true,
178
+ }),
179
+ ),
180
+ )
181
+ expect(JSON.parse(phaseLogs[0]!).data.status).toBe("done")
182
+
183
+ await runTestEffect(
184
+ task({
185
+ subcommand: "create",
186
+ args: ["single-status"],
187
+ ticketUrl: "https://example.com/task",
188
+ repo: "agency",
189
+ branch: "task/single-status",
190
+ base: "main",
191
+ cwd: root,
192
+ silent: true,
193
+ }),
194
+ )
195
+ const taskLogs = await captureLogs(() =>
196
+ runTestEffect(
197
+ task({
198
+ subcommand: "status",
199
+ args: ["single-status", "dropped"],
200
+ cwd: root,
201
+ json: true,
202
+ }),
203
+ ),
204
+ )
205
+ expect(JSON.parse(taskLogs[0]!).data.status).toBe("dropped")
206
+ })
207
+
163
208
  test("converts a single-phase task with an explicit first phase ID", async () => {
164
209
  await runTestEffect(
165
210
  task({
@@ -0,0 +1,94 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import { mkdir } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
+ import { task, type TaskInteraction } from "./task"
7
+
8
+ describe("task creation input", () => {
9
+ let root: string
10
+
11
+ beforeEach(async () => {
12
+ root = await createTempDir()
13
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
14
+ await mkdir(join(root, "repos/agency"), { recursive: true })
15
+ })
16
+
17
+ afterEach(async () => cleanupTempDir(root))
18
+
19
+ test("guides task creation and defaults branch metadata", async () => {
20
+ await mkdir(join(root, "epics/delivery"), { recursive: true })
21
+ await Bun.write(
22
+ join(root, "epics/delivery/EPIC.md"),
23
+ "---\nticketUrl: https://example.com/delivery\nrepos:\n - repo: agency\n ref: main\ntasks: []\n---\n\n# Delivery\n",
24
+ )
25
+ const textAnswers = ["guided-task", "", ""]
26
+ const prompts: string[] = []
27
+ const interaction: TaskInteraction = {
28
+ text: (prompt) => {
29
+ prompts.push(prompt)
30
+ return Effect.succeed(textAnswers.shift() ?? "")
31
+ },
32
+ select: (prompt, choices) => {
33
+ prompts.push(`${prompt}: ${choices.join(",")}`)
34
+ return Effect.succeed(
35
+ prompt === "Parent epic"
36
+ ? "(none)"
37
+ : prompt === "Task type"
38
+ ? "single-phase"
39
+ : "agency",
40
+ )
41
+ },
42
+ }
43
+
44
+ await runTestEffect(
45
+ task(
46
+ { subcommand: "new", args: [], cwd: root, silent: true },
47
+ interaction,
48
+ ),
49
+ )
50
+
51
+ const content = await Bun.file(
52
+ join(root, "tasks/guided-task/TASK.md"),
53
+ ).text()
54
+ expect(content).toContain("ticketUrl: null")
55
+ expect(content).toContain("repo: agency")
56
+ expect(content).toContain("branch: task/guided-task")
57
+ expect(content).toContain("base: main")
58
+ expect(prompts).toEqual([
59
+ "Task ID: ",
60
+ "Ticket URL (optional): ",
61
+ "Description (optional): ",
62
+ "Parent epic: (none),delivery",
63
+ "Task type: single-phase,multi-phase",
64
+ "Writable repository: agency",
65
+ ])
66
+ })
67
+
68
+ test("keeps scripted creation non-interactive and permits no ticket URL", async () => {
69
+ const interaction: TaskInteraction = {
70
+ text: () => Effect.fail(new Error("unexpected text prompt")),
71
+ select: () => Effect.fail(new Error("unexpected selection prompt")),
72
+ }
73
+
74
+ await runTestEffect(
75
+ task(
76
+ {
77
+ subcommand: "create",
78
+ args: ["scripted-task"],
79
+ repo: "agency",
80
+ cwd: root,
81
+ silent: true,
82
+ },
83
+ interaction,
84
+ ),
85
+ )
86
+
87
+ const content = await Bun.file(
88
+ join(root, "tasks/scripted-task/TASK.md"),
89
+ ).text()
90
+ expect(content).toContain("ticketUrl: null")
91
+ expect(content).toContain("branch: task/scripted-task")
92
+ expect(content).toContain("base: main")
93
+ })
94
+ })