@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.
- package/README.md +302 -113
- package/cli.ts +123 -415
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +258 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +172 -419
- package/src/commands/work.ts +47 -166
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -518
- package/src/commands/push.ts +0 -434
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1183
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- package/templates/opencode.json +0 -4
package/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export * from "./src/workbase/schemas"
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
9
|
-
"
|
|
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
|
-
"
|
|
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'
|
|
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
|
-
"
|
|
57
|
-
"ora": "^8.2.0"
|
|
54
|
+
"yaml": "^2.9.0"
|
|
58
55
|
},
|
|
59
56
|
"devDependencies": {
|
|
60
57
|
"@types/bun": "^1.3.6",
|
|
61
|
-
"knip": "^
|
|
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": "^
|
|
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`.
|
package/src/cli.test.ts
ADDED
|
@@ -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
|
+
})
|