@markjaquith/agency 1.11.0 → 2.0.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 (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +172 -419
  24. package/src/commands/work.ts +47 -166
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -518
  73. package/src/commands/push.ts +0 -434
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1183
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
package/index.ts CHANGED
@@ -1 +1 @@
1
- console.log("Hello via Bun!")
1
+ export * from "./src/workbase/schemas"
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "1.11.0",
4
- "description": "Manages personal agents files",
3
+ "version": "2.0.0",
4
+ "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
7
7
  "bun",
8
- "personal-agents",
9
- "typescript"
8
+ "typescript",
9
+ "worktrees"
10
10
  ],
11
11
  "license": "MIT",
12
12
  "author": "Mark Jaquith",
@@ -21,7 +21,7 @@
21
21
  "index.ts",
22
22
  "cli.ts",
23
23
  "src",
24
- "templates",
24
+ "skills",
25
25
  "README.md",
26
26
  "LICENSE"
27
27
  ],
@@ -40,9 +40,7 @@
40
40
  "tag": "latest"
41
41
  },
42
42
  "scripts": {
43
- "test": "find src -name '*.test.ts' ! -name '*.integration.test.ts' -print0 | xargs -0 -n 1 -P 4 bun test",
44
- "test:integration": "find src -name '*.integration.test.ts' -print0 | xargs -0 -n 1 -P 4 bun test",
45
- "test:all": "find src -name '*.test.ts' -print0 | xargs -0 -n 1 -P 4 bun test",
43
+ "test": "find src -name '*.test.ts' -print0 | xargs -0 -n 1 -P 4 bun test",
46
44
  "format": "oxfmt",
47
45
  "format:check": "oxfmt --check",
48
46
  "knip": "knip --production",
@@ -53,21 +51,18 @@
53
51
  "dependencies": {
54
52
  "@effect/schema": "^0.75.5",
55
53
  "effect": "^3.19.15",
56
- "jsonc-parser": "^3.3.1",
57
- "ora": "^8.2.0"
54
+ "yaml": "^2.9.0"
58
55
  },
59
56
  "devDependencies": {
60
57
  "@types/bun": "^1.3.6",
61
- "knip": "^5.82.1",
62
- "oxfmt": "^0.27.0"
58
+ "knip": "^6.27.0",
59
+ "oxfmt": "^0.27.0",
60
+ "typescript": "^7.0.2"
63
61
  },
64
62
  "peerDependencies": {
65
- "typescript": "^5.9.3"
63
+ "typescript": "^7.0.2"
66
64
  },
67
65
  "engines": {
68
66
  "bun": ">=1.0.0"
69
- },
70
- "trustedDependencies": [
71
- "@triggi/native-exec"
72
- ]
67
+ }
73
68
  }
@@ -0,0 +1,258 @@
1
+ ---
2
+ name: agency
3
+ description: >
4
+ Use Agency to manage filesystem-backed workbases, repository aliases, epics,
5
+ tasks, phases, execution worktrees, and GitHub pull requests. Use when the
6
+ user asks to create, inspect, validate, or work on Agency epics/tasks/phases;
7
+ coordinate work across repositories; materialize task worktrees; or create a
8
+ PR through Agency.
9
+ license: MIT
10
+ compatibility: Requires the agency CLI, Git, and optionally gh plus OpenCode or Claude Code.
11
+ ---
12
+
13
+ # Agency
14
+
15
+ Use Agency to coordinate durable work across one or more Git repositories.
16
+ Agency stores planning and orchestration as Markdown with YAML frontmatter while
17
+ keeping repository checkouts as local workbase state.
18
+
19
+ ## Mental Model
20
+
21
+ - A **workbase** is the root containing `agency.json`, `repos/`, `epics/`, and
22
+ `tasks/`.
23
+ - A repository alias is a bare Git repository or symlink at `repos/{alias}`.
24
+ - An **epic** orchestrates tasks and only reads repositories.
25
+ - A **task** describes one durable outcome. It may be standalone or belong to an
26
+ epic.
27
+ - A **phase** belongs to a multi-phase task and corresponds to one PR or intended
28
+ PR.
29
+ - An **execution unit** is either a single-phase task or a phase. It has one
30
+ writable `repo`, optional read-only `repos`, one branch, one base, and one
31
+ `pr` value.
32
+
33
+ IDs are directory names. Do not repeat IDs in document frontmatter.
34
+
35
+ ## Start By Inspecting
36
+
37
+ From anywhere beneath a workbase, run:
38
+
39
+ ```bash
40
+ agency status --json
41
+ agency validate --json
42
+ agency repo list --json
43
+ agency epic list --json
44
+ agency task list --json
45
+ ```
46
+
47
+ Use `show` before changing an existing entity:
48
+
49
+ ```bash
50
+ agency epic show <epic-id> --json
51
+ agency task show <task-id> --json
52
+ agency phase show <task-id> <phase-id> --json
53
+ ```
54
+
55
+ If no workbase is found, do not initialize one without user intent. When asked:
56
+
57
+ ```bash
58
+ agency init [path]
59
+ ```
60
+
61
+ ## Repository Aliases
62
+
63
+ Add a remote as an Agency-managed bare repository:
64
+
65
+ ```bash
66
+ agency repo add <alias> <git-remote>
67
+ ```
68
+
69
+ Link an existing local Git repository:
70
+
71
+ ```bash
72
+ agency repo link <alias> <path>
73
+ ```
74
+
75
+ Use aliases, never absolute paths or Git URLs, in epic/task/phase frontmatter.
76
+
77
+ ## Choose The Correct Work Shape
78
+
79
+ Use a single-phase task when one PR in one writable repository can deliver the
80
+ outcome. Use a multi-phase task when the outcome requires multiple intended PRs,
81
+ possibly across repositories or with sequencing dependencies. Use an epic when
82
+ several independently meaningful tasks need orchestration.
83
+
84
+ If phases are known up front, create a multi-phase task. To add a phase later,
85
+ use `--first-phase <id>` to name the phase created from the task's existing
86
+ execution fields and worktrees.
87
+
88
+ ## Create Epics
89
+
90
+ ```bash
91
+ agency epic create <id> \
92
+ --ticket-url <url> \
93
+ [--description <text>] \
94
+ --repo <read-only-alias>:<ref> \
95
+ [--repo <another-alias>:<ref>]
96
+ ```
97
+
98
+ Epic task ordering and dependencies live in `EPIC.md`. Creating a task with
99
+ `--epic` automatically writes both sides of the epic/task relationship.
100
+
101
+ ## Create Single-Phase Tasks
102
+
103
+ ```bash
104
+ agency task create <id> \
105
+ --ticket-url <url> \
106
+ [--description <text>] \
107
+ [--epic <epic-id>] \
108
+ --repo <writable-alias> \
109
+ [--reference <read-only-alias>:<ref>] \
110
+ --branch <branch> \
111
+ --base <base>
112
+ ```
113
+
114
+ The task itself is the execution unit. Its worktrees live under
115
+ `tasks/{id}/code/{alias}`.
116
+
117
+ ## Create Multi-Phase Tasks
118
+
119
+ Create the task container:
120
+
121
+ ```bash
122
+ agency task create <id> \
123
+ --ticket-url <url> \
124
+ [--description <text>] \
125
+ [--epic <epic-id>] \
126
+ --multi-phase
127
+ ```
128
+
129
+ Then create each phase:
130
+
131
+ ```bash
132
+ agency phase create <task-id> <phase-id> \
133
+ [--description <text>] \
134
+ --repo <writable-alias> \
135
+ [--reference <read-only-alias>:<ref>] \
136
+ --branch <branch> \
137
+ --base <base> \
138
+ [--depends-on <phase-id>]
139
+ ```
140
+
141
+ Repeat `--reference` and `--depends-on` when needed. Phase worktrees live under
142
+ `tasks/{task-id}/phases/{phase-id}/code/{alias}`.
143
+
144
+ Convert an existing single-phase task while adding another phase:
145
+
146
+ ```bash
147
+ agency phase create <task-id> <new-phase-id> \
148
+ --first-phase <existing-phase-id> \
149
+ --repo <writable-alias> \
150
+ --branch <new-branch> \
151
+ --base <base> \
152
+ [--depends-on <existing-phase-id>]
153
+ ```
154
+
155
+ The conversion preserves task-level metadata and prose, moves execution metadata
156
+ into the named existing phase, and relocates any materialized worktrees.
157
+
158
+ ## Frontmatter Rules
159
+
160
+ Agency documents use YAML 1.2 frontmatter:
161
+
162
+ ```yaml
163
+ ---
164
+ ticketUrl: https://example.com/tickets/example
165
+ description: Deliver the example outcome.
166
+ repo: application
167
+ repos:
168
+ - repo: api
169
+ ref: main
170
+ branch: task/example
171
+ base: main
172
+ pr: null
173
+ ---
174
+ ```
175
+
176
+ Follow these invariants:
177
+
178
+ - `ticketUrl` belongs to epics and tasks, not phases.
179
+ - `description` is an optional non-empty summary on epics, tasks, and phases.
180
+ - `repo` is the one writable repository for an execution unit.
181
+ - `repos` contains `{ repo, ref }` read-only references and must not repeat `repo`.
182
+ - Epics may declare `repos` but never `repo`.
183
+ - A writable `(repo, branch)` pair belongs to exactly one task or phase.
184
+ - Use a commit SHA for a read-only `ref` when reproducibility matters.
185
+
186
+ Commands that print Agency-owned results accept `--json` for machine-readable
187
+ output, including mutations, entity inspection, status, validation, and PR creation.
188
+
189
+ - Multi-phase task frontmatter owns the phase dependency graph.
190
+ - Epic frontmatter owns the child-task dependency graph.
191
+ - `pr` is either a GitHub PR URL string or `null`.
192
+ - Keep directory IDs stable; encode sequencing with `dependsOn`, not numeric
193
+ directory prefixes.
194
+ - Do not use YAML duplicate keys, anchors, aliases, or custom tags.
195
+
196
+ Prefer Agency commands for creation. When manually editing dependencies or
197
+ prose, preserve backlinks and run validation immediately afterward.
198
+
199
+ ## Validate Every Structural Change
200
+
201
+ ```bash
202
+ agency validate
203
+ ```
204
+
205
+ Use `--json` when diagnostics will be consumed programmatically. Resolve all
206
+ validation errors before materializing worktrees or creating PRs. Validation
207
+ checks schemas, aliases, backlinks, phase directories, duplicate references,
208
+ duplicate writable branch ownership, unknown dependencies, and dependency cycles.
209
+
210
+ ## Worktrees And Agent Launch
211
+
212
+ ```bash
213
+ agency work <task-id> [phase-id]
214
+ ```
215
+
216
+ Use `--opencode` or `--claude` to require a specific agent. This command fetches
217
+ repositories, creates or reuses the execution worktrees, changes into the
218
+ writable checkout, and replaces the current process with the selected agent.
219
+
220
+ The workbase may delegate writable checkout creation through
221
+ `worktreeCreateCommand` in `agency.json`. Do not bypass that command or create a
222
+ parallel worktree manually. Supplemental read-only checkouts are still detached
223
+ Git worktrees managed directly by Agency at their declared refs.
224
+
225
+ Agency inspects `git worktree list --porcelain` before materializing. It reuses a
226
+ writable checkout only when both path and branch match, and reuses a reference
227
+ checkout only when its commit matches the declared ref. If a branch is checked
228
+ out elsewhere, choose a different branch or remove the conflicting worktree;
229
+ never force a second checkout of the branch.
230
+
231
+ Do not run `agency work` from inside an active agent session unless the user
232
+ explicitly wants to launch a nested/replacement agent process. If already
233
+ working in an Agency checkout, read the owning `TASK.md` and optional `PHASE.md`
234
+ directly instead.
235
+
236
+ ## Create Pull Requests
237
+
238
+ Only create a PR when the user explicitly requests it:
239
+
240
+ ```bash
241
+ agency pr create <task-id> [phase-id] [--draft]
242
+ ```
243
+
244
+ Agency requires a clean writable worktree, pushes the execution branch, runs
245
+ `gh pr create --fill`, and records the returned URL in the owning document's
246
+ `pr` field. If PR creation fails, do not invent or manually write a URL.
247
+
248
+ ## Safety Rules
249
+
250
+ - Run `agency validate` before worktree or PR operations.
251
+ - Inspect existing entities before editing them.
252
+ - Never write through a repository listed in plural `repos`.
253
+ - Do not manually move generated `code/` worktrees.
254
+ - Do not manually edit bare repositories under `repos/`.
255
+ - Do not create a PR, initialize a workbase, clone a remote, or add a symlink
256
+ without user intent.
257
+ - Keep task-level decisions in `TASK.md` and delivery-specific context in
258
+ `PHASE.md`.
@@ -0,0 +1,290 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { realpath } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import { cleanupTempDir, createTempDir } from "./test-utils"
5
+
6
+ const projectRoot = join(import.meta.dir, "..")
7
+ const cliPath = join(projectRoot, "cli.ts")
8
+
9
+ interface CliResult {
10
+ exitCode: number
11
+ stdout: string
12
+ stderr: string
13
+ }
14
+
15
+ async function runCli(args: string[], cwd = projectRoot): Promise<CliResult> {
16
+ const subprocess = Bun.spawn([process.execPath, cliPath, ...args], {
17
+ cwd,
18
+ stdout: "pipe",
19
+ stderr: "pipe",
20
+ })
21
+ const [exitCode, stdout, stderr] = await Promise.all([
22
+ subprocess.exited,
23
+ new Response(subprocess.stdout).text(),
24
+ new Response(subprocess.stderr).text(),
25
+ ])
26
+ return { exitCode, stdout, stderr }
27
+ }
28
+
29
+ function parseJson(result: CliResult) {
30
+ expect(result.exitCode).toBe(0)
31
+ expect(result.stderr).toBe("")
32
+ return JSON.parse(result.stdout)
33
+ }
34
+
35
+ describe("CLI", () => {
36
+ const tempDirs: string[] = []
37
+
38
+ afterEach(async () => {
39
+ await Promise.all(tempDirs.splice(0).map(cleanupTempDir))
40
+ })
41
+
42
+ test("reports version and routes top-level help with intentional statuses", async () => {
43
+ const version = await runCli(["--version"])
44
+ expect(version).toEqual({
45
+ exitCode: 0,
46
+ stdout: "v2.0.0\n",
47
+ stderr: "",
48
+ })
49
+
50
+ const noArgs = await runCli([])
51
+ expect(noArgs.exitCode).toBe(1)
52
+ expect(noArgs.stdout).toContain("Usage: agency <command> [options]")
53
+ expect(noArgs.stderr).toBe("")
54
+
55
+ const help = await runCli(["--help"])
56
+ expect(help.exitCode).toBe(0)
57
+ expect(help.stdout).toContain("Usage: agency <command> [options]")
58
+ expect(help.stderr).toBe("")
59
+ })
60
+
61
+ test("reports unknown commands and preserves tagged error messages", async () => {
62
+ const unknown = await runCli(["unknown"])
63
+ expect(unknown.exitCode).toBe(1)
64
+ expect(unknown.stdout).toBe("")
65
+ expect(unknown.stderr).toContain("Error: Unknown command 'unknown'")
66
+
67
+ const cwd = await createTempDir()
68
+ tempDirs.push(cwd)
69
+ const taggedError = await runCli(["repo", "list"], cwd)
70
+ expect(taggedError.exitCode).toBe(1)
71
+ expect(taggedError.stdout).toBe("")
72
+ expect(taggedError.stderr).toContain("ⓘ No Agency workbase found from")
73
+ expect(taggedError.stderr).not.toContain("An error has occurred")
74
+ })
75
+
76
+ test("routes command help and global options on either side of commands", async () => {
77
+ for (const [command, usage] of [
78
+ ["init", "Usage: agency init"],
79
+ ["repo", "Usage: agency repo"],
80
+ ["epic", "Usage: agency epic"],
81
+ ["task", "Usage: agency task"],
82
+ ["phase", "Usage: agency phase"],
83
+ ["work", "Usage: agency work"],
84
+ ["pr", "Usage: agency pr"],
85
+ ["status", "Usage: agency status"],
86
+ ["validate", "Usage: agency validate"],
87
+ ] as const) {
88
+ const result = await runCli([command, "--help"])
89
+ expect(result.exitCode).toBe(0)
90
+ expect(result.stdout).toContain(usage)
91
+ expect(result.stderr).toBe("")
92
+ }
93
+
94
+ const helpBefore = await runCli(["--help", "task"])
95
+ expect(helpBefore.exitCode).toBe(0)
96
+ expect(helpBefore.stdout).toContain("Usage: agency task <subcommand>")
97
+ expect(helpBefore.stderr).toBe("")
98
+
99
+ const root = await createTempDir()
100
+ tempDirs.push(root)
101
+ const before = await runCli(["--silent", "init", root])
102
+ expect(before).toEqual({ exitCode: 0, stdout: "", stderr: "" })
103
+
104
+ const after = await runCli(["status", "--silent"], root)
105
+ expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
106
+ })
107
+
108
+ test("runs a multi-phase domain workflow through subprocesses", async () => {
109
+ const parent = await createTempDir()
110
+ tempDirs.push(parent)
111
+ const root = join(parent, "workbase")
112
+ const source = join(parent, "source")
113
+
114
+ const git = Bun.spawn(["git", "init", "--initial-branch=main", source], {
115
+ stdout: "ignore",
116
+ stderr: "pipe",
117
+ })
118
+ expect(await git.exited).toBe(0)
119
+
120
+ expect(parseJson(await runCli(["init", root, "--json"]))).toEqual({
121
+ root,
122
+ })
123
+ const workbaseRoot = await realpath(root)
124
+ expect(
125
+ parseJson(
126
+ await runCli(["repo", "link", "agency", source, "--json"], root),
127
+ ),
128
+ ).toEqual({
129
+ alias: "agency",
130
+ path: join(workbaseRoot, "repos/agency"),
131
+ })
132
+ for (const alias of ["effect", "tooling"]) {
133
+ parseJson(await runCli(["repo", "link", alias, source, "--json"], root))
134
+ }
135
+
136
+ const epic = parseJson(
137
+ await runCli(
138
+ [
139
+ "epic",
140
+ "create",
141
+ "delivery",
142
+ "--ticket-url",
143
+ "https://example.com/epics/delivery",
144
+ "--repo",
145
+ "agency:main",
146
+ "--json",
147
+ ],
148
+ root,
149
+ ),
150
+ )
151
+ expect(epic).toMatchObject({
152
+ id: "delivery",
153
+ data: { repos: [{ repo: "agency", ref: "main" }], tasks: [] },
154
+ })
155
+
156
+ const task = parseJson(
157
+ await runCli(
158
+ [
159
+ "task",
160
+ "create",
161
+ "ship",
162
+ "--ticket-url",
163
+ "https://example.com/tasks/ship",
164
+ "--epic",
165
+ "delivery",
166
+ "--multi-phase",
167
+ "--json",
168
+ ],
169
+ root,
170
+ ),
171
+ )
172
+ expect(task).toMatchObject({
173
+ id: "ship",
174
+ data: { epic: "delivery", phases: [] },
175
+ })
176
+
177
+ for (const [id, branch] of [
178
+ ["prepare", "task/ship-prepare"],
179
+ ["implement", "task/ship-implement"],
180
+ ] as const) {
181
+ const phase = parseJson(
182
+ await runCli(
183
+ [
184
+ "phase",
185
+ "create",
186
+ "ship",
187
+ id,
188
+ "--repo",
189
+ "agency",
190
+ "--branch",
191
+ branch,
192
+ "--base",
193
+ "main",
194
+ "--json",
195
+ ],
196
+ root,
197
+ ),
198
+ )
199
+ expect(phase).toMatchObject({ taskId: "ship", id })
200
+ }
201
+
202
+ const release = parseJson(
203
+ await runCli(
204
+ [
205
+ "phase",
206
+ "create",
207
+ "ship",
208
+ "release",
209
+ "--repo",
210
+ "agency",
211
+ "--branch",
212
+ "task/ship-release",
213
+ "--base",
214
+ "main",
215
+ "--depends-on",
216
+ "prepare",
217
+ "--depends-on",
218
+ "implement",
219
+ "--reference",
220
+ "effect:main",
221
+ "--reference",
222
+ "tooling:main",
223
+ "--json",
224
+ ],
225
+ root,
226
+ ),
227
+ )
228
+ expect(release.data).toMatchObject({
229
+ branch: "task/ship-release",
230
+ base: "main",
231
+ repos: [
232
+ { repo: "effect", ref: "main" },
233
+ { repo: "tooling", ref: "main" },
234
+ ],
235
+ })
236
+
237
+ const epicList = parseJson(await runCli(["epic", "list", "--json"], root))
238
+ expect(epicList).toHaveLength(1)
239
+ expect(epicList[0].data.tasks).toEqual([{ id: "ship" }])
240
+
241
+ const taskShow = parseJson(
242
+ await runCli(["task", "show", "ship", "--json"], root),
243
+ )
244
+ expect(taskShow.data.phases).toEqual([
245
+ { id: "prepare" },
246
+ { id: "implement" },
247
+ { id: "release", dependsOn: ["prepare", "implement"] },
248
+ ])
249
+ const plainTaskShow = await runCli(["task", "show", "ship"], root)
250
+ expect(plainTaskShow.exitCode).toBe(0)
251
+ expect(plainTaskShow.stdout).toContain(
252
+ "ticketUrl: https://example.com/tasks/ship",
253
+ )
254
+ expect(plainTaskShow.stderr).toBe("")
255
+
256
+ const phaseList = parseJson(
257
+ await runCli(["phase", "list", "ship", "--json"], root),
258
+ )
259
+ expect(phaseList.map((phase: { id: string }) => phase.id)).toEqual([
260
+ "implement",
261
+ "prepare",
262
+ "release",
263
+ ])
264
+
265
+ const phaseShow = parseJson(
266
+ await runCli(["phase", "show", "ship", "release", "--json"], root),
267
+ )
268
+ expect(phaseShow).toMatchObject({ taskId: "ship", id: "release" })
269
+
270
+ const status = parseJson(await runCli(["status", "--json"], root))
271
+ expect(status).toMatchObject({
272
+ root: workbaseRoot,
273
+ epicCount: 1,
274
+ taskCount: 1,
275
+ phaseCount: 3,
276
+ valid: true,
277
+ issues: [],
278
+ })
279
+
280
+ const validation = parseJson(await runCli(["validate", "--json"], root))
281
+ expect(validation).toEqual({
282
+ root: workbaseRoot,
283
+ issues: [],
284
+ epicCount: 1,
285
+ taskCount: 1,
286
+ phaseCount: 3,
287
+ valid: true,
288
+ })
289
+ }, 30_000)
290
+ })