@markjaquith/agency 2.74.0 → 3.1.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 +29 -122
- package/cli-main.ts +10 -28
- package/cli.ts +1 -17
- package/package.json +1 -2
- package/schemas/agency-graph-v1.schema.json +1 -1
- package/src/cli-parser.test.ts +5 -19
- package/src/cli-parser.ts +12 -30
- package/src/cli.test.ts +6 -6
- package/src/commands/archive.test.ts +73 -1
- package/src/commands/archive.ts +18 -7
- package/src/commands/context.test.ts +0 -67
- package/src/commands/init.test.ts +1 -2
- package/src/commands/pr.test.ts +2 -87
- package/src/commands/pr.ts +2 -79
- package/src/commands/push.test.ts +1 -2
- package/src/graph-schema.ts +1 -1
- package/src/services/ArchiveBulkService.test.ts +1 -142
- package/src/services/ArchiveService.test.ts +1 -172
- package/src/services/ArchiveService.ts +59 -27
- package/src/services/ContextService.ts +3 -47
- package/src/services/DoctorService.ts +8 -24
- package/src/services/GraphService.test.ts +1 -0
- package/src/services/GraphService.ts +2 -5
- package/src/services/PhaseService.ts +70 -191
- package/src/services/PullRequestService.test.ts +1 -45
- package/src/services/PullRequestService.ts +1 -22
- package/src/services/PushService.test.ts +29 -295
- package/src/services/PushService.ts +10 -239
- package/src/services/RepositoryService.test.ts +1 -61
- package/src/services/RepositoryService.ts +2 -13
- package/src/services/ReviewService.test.ts +1 -84
- package/src/services/ReviewService.ts +9 -48
- package/src/services/SyncService.test.ts +0 -81
- package/src/services/SyncService.ts +12 -39
- package/src/services/TaskPhaseService.test.ts +0 -80
- package/src/services/TaskService.ts +1 -9
- package/src/services/VersionControlService.test.ts +4 -117
- package/src/services/VersionControlService.ts +3 -426
- package/src/services/WorkbaseService.test.ts +0 -20
- package/src/services/WorkbaseService.ts +1 -18
- package/src/services/WorktreeService.test.ts +1 -705
- package/src/services/WorktreeService.ts +395 -1604
- package/src/services/push-validation.ts +0 -7
- package/src/test-utils.ts +0 -4
- package/src/workbase/AGENTS.md +4 -5
- package/src/workbase/checkout-command.test.ts +1 -1
- package/src/workbase/checkout-command.ts +1 -1
- package/src/workbase/delivery-command.test.ts +4 -35
- package/src/workbase/delivery-command.ts +2 -15
- package/src/workbase/schemas.test.ts +0 -19
- package/src/workbase/schemas.ts +1 -2
- package/src/commands/vcs.test.ts +0 -75
- package/src/commands/vcs.ts +0 -72
- package/src/services/VcsMigrationService.test.ts +0 -348
- package/src/services/VcsMigrationService.ts +0 -857
- package/src/vcs-status-fast.ts +0 -310
- package/src/workbase/version-control.ts +0 -5
- package/src/workbase/workspace-command.test.ts +0 -70
- package/src/workbase/workspace-command.ts +0 -63
package/README.md
CHANGED
|
@@ -9,7 +9,6 @@ read or write.
|
|
|
9
9
|
|
|
10
10
|
- [Bun](https://bun.sh) 1.0 or newer
|
|
11
11
|
- Git
|
|
12
|
-
- [Jujutsu](https://jj-vcs.github.io/jj/) is preferred when available
|
|
13
12
|
- [GitHub CLI](https://cli.github.com/) for `agency pr`
|
|
14
13
|
- OpenCode, Claude Code, or a configured agent for `agency work`
|
|
15
14
|
|
|
@@ -151,15 +150,16 @@ the writable checkout's `.claude/skills`, `.agents/skills`,
|
|
|
151
150
|
`.opencode/{skill,skills}`, and `.pi/skills` directories. `agency context`
|
|
152
151
|
remains the authority for writes; reference checkouts remain read-only.
|
|
153
152
|
|
|
154
|
-
Repository aliases
|
|
155
|
-
|
|
156
|
-
`repos/{alias}`. A declaration contains no local path, symlink target,
|
|
157
|
-
or credential
|
|
153
|
+
Repository aliases and canonical fetch remotes are declared in tracked
|
|
154
|
+
`agency.json`; local Git clones and symlinks remain ignored under
|
|
155
|
+
`repos/{alias}`. A declaration contains no local path, symlink target, worktree,
|
|
156
|
+
or credential. The optional `vcs` field accepts only `"git"`; omitting it also
|
|
157
|
+
selects Git:
|
|
158
158
|
|
|
159
159
|
```json
|
|
160
160
|
{
|
|
161
161
|
"version": 2,
|
|
162
|
-
"vcs": "
|
|
162
|
+
"vcs": "git",
|
|
163
163
|
"repositories": {
|
|
164
164
|
"frontend": {
|
|
165
165
|
"remote": "git@example.com:team/frontend.git"
|
|
@@ -168,39 +168,9 @@ or credential:
|
|
|
168
168
|
}
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
non-colocated managed clones and jj workspaces, while Git workbases continue to
|
|
175
|
-
use bare repositories and Git worktrees. Existing colocated jj repositories are
|
|
176
|
-
also supported.
|
|
177
|
-
|
|
178
|
-
Agency uses jj-native commands for repository validity, workspace registration,
|
|
179
|
-
working-copy state, revisions, bookmarks, ancestry, fetch, and push. Raw Git is
|
|
180
|
-
limited to backing-store plumbing such as durable review refs and integrations
|
|
181
|
-
that require Git, including GitHub CLI commit discovery. For those operations,
|
|
182
|
-
Agency obtains the backing repository with `jj git root` and supplies it as
|
|
183
|
-
`GIT_DIR`; it does not use raw Git to infer jj workspace state.
|
|
184
|
-
|
|
185
|
-
Inspect the current backend and migration readiness with:
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
agency vcs status
|
|
189
|
-
agency vcs migrate jj # dry-run
|
|
190
|
-
agency vcs migrate jj --apply
|
|
191
|
-
agency vcs migrate git --apply
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Migration is workbase-wide and transactional. Agency locks every execution
|
|
195
|
-
unit, requires clean registered workspaces, blocks active claims and working or
|
|
196
|
-
delegated units, converts repository metadata, recreates checkout paths with the
|
|
197
|
-
target backend, and updates `agency.json` last. Migration from jj to Git also
|
|
198
|
-
blocks jj-only heads that are not preserved by a bookmark or workspace. Failed
|
|
199
|
-
migrations restore the source repositories and workspaces when rollback is
|
|
200
|
-
possible and report explicit manual recovery paths otherwise.
|
|
201
|
-
Converting a non-colocated jj workbase to Git is currently blocked before any
|
|
202
|
-
mutation because its backing Git object store is inside `.jj`; first convert the
|
|
203
|
-
repositories to colocated jj so the Git store survives metadata removal.
|
|
171
|
+
Agency materializes declared remotes as bare Git repositories and creates
|
|
172
|
+
managed Git worktrees for execution units. Linked aliases remain symlinks to
|
|
173
|
+
existing Git repositories.
|
|
204
174
|
|
|
205
175
|
Existing version 2 workbases without `repositories` remain valid. Run
|
|
206
176
|
`agency repo setup` to preview deterministic adoption of legacy local aliases;
|
|
@@ -274,63 +244,13 @@ Agency also reconciles known tool-owned artifacts such as Worktrunk's
|
|
|
274
244
|
new and existing managed worktrees clean without changing the repository's
|
|
275
245
|
tracked ignore configuration or overwriting user-maintained local excludes.
|
|
276
246
|
|
|
277
|
-
The configured command applies only to the writable checkout
|
|
247
|
+
The configured command applies only to the writable checkout.
|
|
278
248
|
Supplemental read-only repositories remain detached Git worktrees at their
|
|
279
|
-
declared refs so they do not acquire writable branches.
|
|
280
|
-
jj workspaces and ignore this Git-specific customization.
|
|
281
|
-
|
|
282
|
-
### Custom Jj Workspace Command
|
|
283
|
-
|
|
284
|
-
Jj workbases normally create managed workspaces with `jj workspace add`. Set
|
|
285
|
-
`workspaceCreateCommand` to an argv template when another tool should create or
|
|
286
|
-
adopt a prepared workspace directly at Agency's destination:
|
|
287
|
-
|
|
288
|
-
```json
|
|
289
|
-
{
|
|
290
|
-
"version": 2,
|
|
291
|
-
"vcs": "jj",
|
|
292
|
-
"workspaceCreateCommand": [
|
|
293
|
-
"my-prewarm-tool",
|
|
294
|
-
"adopt",
|
|
295
|
-
"--repo",
|
|
296
|
-
"{repo}",
|
|
297
|
-
"--destination",
|
|
298
|
-
"{workspace}",
|
|
299
|
-
"--name",
|
|
300
|
-
"{name}",
|
|
301
|
-
"--revision",
|
|
302
|
-
"{revision}"
|
|
303
|
-
]
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Available placeholders are:
|
|
308
|
-
|
|
309
|
-
- `{repo}`: absolute repository alias path under `repos/`
|
|
310
|
-
- `{workspace}`: absolute managed workspace path Agency requires
|
|
311
|
-
- `{name}`: unique jj workspace name Agency requires
|
|
312
|
-
- `{revision}`: exact commit the new working copy must be based on
|
|
313
|
-
- `{kind}`: `writable` or `reference`
|
|
314
|
-
- `{requestedRef}`: configured branch, reference, or review commit
|
|
315
|
-
|
|
316
|
-
`{repo}`, `{workspace}`, `{name}`, and `{revision}` are required. Agency invokes
|
|
317
|
-
the command directly without a shell and sets matching `AGENCY_REPO`,
|
|
318
|
-
`AGENCY_WORKSPACE`, `AGENCY_WORKSPACE_NAME`, `AGENCY_REVISION`,
|
|
319
|
-
`AGENCY_CHECKOUT_KIND`, and `AGENCY_REQUESTED_REF` environment variables. The
|
|
320
|
-
command applies to each new jj checkout and must leave `{workspace}` registered
|
|
321
|
-
under `{name}` with its working-copy parent at `{revision}`. This lets a prewarm
|
|
322
|
-
tool move or adopt a prepared workspace without first paying for Agency's normal
|
|
323
|
-
full checkout.
|
|
324
|
-
|
|
325
|
-
Agency validates the registration, name, path, and revision before running any
|
|
326
|
-
`postCheckoutCommand`. A failed command or validation removes a partially
|
|
327
|
-
created workspace when possible and otherwise reports manual recovery. Resume
|
|
328
|
-
restoration continues to use Agency's built-in exact-target recovery path.
|
|
329
|
-
Git workbases ignore this jj-specific customization.
|
|
249
|
+
declared refs so they do not acquire writable branches.
|
|
330
250
|
|
|
331
251
|
### Post-checkout Commands
|
|
332
252
|
|
|
333
|
-
Each repository declaration may provide a
|
|
253
|
+
Each repository declaration may provide a `postCheckoutCommand` argv
|
|
334
254
|
template for repository-specific setup. Agency invokes it directly, without a
|
|
335
255
|
shell, with the new checkout as its working directory:
|
|
336
256
|
|
|
@@ -346,11 +266,10 @@ shell, with the new checkout as its working directory:
|
|
|
346
266
|
}
|
|
347
267
|
```
|
|
348
268
|
|
|
349
|
-
The hook runs for each newly created managed
|
|
350
|
-
reference
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
`workspaceCreateCommand` completes and is validated before this hook runs.
|
|
269
|
+
The hook runs for each newly created managed Git worktree, including writable
|
|
270
|
+
and reference worktrees, after Agency has validated it. It does not run for a
|
|
271
|
+
reused worktree or for inspection-only commands. A custom
|
|
272
|
+
`worktreeCreateCommand` completes and is validated before this hook runs.
|
|
354
273
|
|
|
355
274
|
Available placeholders and matching environment variables are:
|
|
356
275
|
|
|
@@ -362,7 +281,7 @@ Available placeholders and matching environment variables are:
|
|
|
362
281
|
| `{checkoutKind}` | `AGENCY_CHECKOUT_KIND` | `writable` or `reference` |
|
|
363
282
|
| `{requestedRef}` | `AGENCY_REQUESTED_REF` | Requested branch, reference, or review commit |
|
|
364
283
|
| `{base}` | `AGENCY_BASE` | Configured execution base |
|
|
365
|
-
| `{vcs}` | `AGENCY_VCS` | `git`
|
|
284
|
+
| `{vcs}` | `AGENCY_VCS` | `git` |
|
|
366
285
|
| `{workbaseRoot}` | `AGENCY_WORKBASE_ROOT` | Absolute workbase root |
|
|
367
286
|
| `{taskId}` | `AGENCY_TASK_ID` | Task ID |
|
|
368
287
|
| `{phaseId}` | `AGENCY_PHASE_ID` | Phase ID |
|
|
@@ -1076,12 +995,17 @@ agency archive epic <epic-id> [--dry-run] [--json]
|
|
|
1076
995
|
agency archive task <task-id> [--dry-run] [--json]
|
|
1077
996
|
agency archive tasks [--dry-run] [--json]
|
|
1078
997
|
agency archive phase <task-id> <phase-id> [--dry-run] [--json]
|
|
998
|
+
agency archive <path> [--dry-run] [--json]
|
|
1079
999
|
agency restore epic <epic-id> [--dry-run] [--json]
|
|
1080
1000
|
agency restore task <task-id> [--dry-run] [--json]
|
|
1081
1001
|
agency restore phase <task-id> <phase-id> [--dry-run] [--json]
|
|
1082
1002
|
```
|
|
1083
1003
|
|
|
1084
|
-
|
|
1004
|
+
An existing path within an active epic or task infers that work item, so
|
|
1005
|
+
`agency archive .` works from its directory. Collection roots are ambiguous,
|
|
1006
|
+
phase paths require the explicit `archive phase` form, and paths outside active
|
|
1007
|
+
epic or task trees are rejected. Archived work keeps its hierarchy under
|
|
1008
|
+
`archive/`. Epic archiving includes its
|
|
1085
1009
|
listed tasks. A task can be archived only when its effective status is terminal
|
|
1086
1010
|
(`done` or `dropped`). Multi-phase task status is derived from its phases, every
|
|
1087
1011
|
phase must be terminal, and a task with no phases is not eligible.
|
|
@@ -1162,15 +1086,6 @@ conflicting worktrees. Repair is deliberately conservative: it repairs safe Git
|
|
|
1162
1086
|
registration issues and materializes missing checkouts, but never switches a
|
|
1163
1087
|
branch, resets a commit, or discards uncommitted work.
|
|
1164
1088
|
|
|
1165
|
-
For jj, explicit removal is a suspend operation. Agency records each workspace's
|
|
1166
|
-
exact `@` commit and stable change ID in `.agency-jj-resume.json`, roots that
|
|
1167
|
-
commit with an internal local bookmark, and only then forgets the workspace.
|
|
1168
|
-
The next `work prepare` validates all three identities, restores the workspace by
|
|
1169
|
-
editing the recorded commit, and consumes the resume metadata and bookmarks.
|
|
1170
|
-
Missing, ambiguous, or conflicting resume state stops instead of falling back to
|
|
1171
|
-
the declared delivery bookmark, `@-`, or the base. Unknown checkout cleanliness
|
|
1172
|
-
also stops removal. Git worktree removal and preparation are unchanged.
|
|
1173
|
-
|
|
1174
1089
|
Agency launches every agent beside its epic or task document. Single-phase tasks
|
|
1175
1090
|
and phases first fetch repositories and create or reuse worktrees under `code/`,
|
|
1176
1091
|
then launch the execution agent from the task directory with absolute context
|
|
@@ -1195,18 +1110,11 @@ that the declared base is in the publication history, validates every outgoing
|
|
|
1195
1110
|
commit's description, author, and conflict state, and refuses non-fast-forward
|
|
1196
1111
|
updates.
|
|
1197
1112
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
changes remain intentional publication tips. Missing descriptions or authors
|
|
1204
|
-
stop with exact change IDs and remediation commands. Agency creates or safely
|
|
1205
|
-
advances only the declared bookmark and never invents a `push-*` bookmark.
|
|
1206
|
-
If the fetched remote base advanced outside the local stack, Agency prints the
|
|
1207
|
-
exact `jj rebase` command needed to move the stack onto `<base>@<remote>`. Push
|
|
1208
|
-
reports deterministic fetch, inspection, validation, and publication progress
|
|
1209
|
-
on stderr, including while `--json` reserves stdout for one machine result.
|
|
1113
|
+
YAML `branch` must exactly match the checked-out local branch, the worktree must
|
|
1114
|
+
be clean, and `HEAD` is pushed with upstream tracking. Missing commit descriptions
|
|
1115
|
+
or authors stop publication with exact commits and remediation commands. Push
|
|
1116
|
+
reports deterministic fetch, inspection, validation, and publication progress on
|
|
1117
|
+
stderr, including while `--json` reserves stdout for one machine result.
|
|
1210
1118
|
|
|
1211
1119
|
Task-aware `agency pr create <task-id> [phase-id]` uses Agency's delivery flow,
|
|
1212
1120
|
including readiness checks and durable PR recording. It accepts draft, title,
|
|
@@ -1216,8 +1124,7 @@ Other `agency pr`
|
|
|
1216
1124
|
invocations forward every argument to `gh pr`. From an execution task or phase
|
|
1217
1125
|
directory, including descendants, passthrough runs in that execution unit's
|
|
1218
1126
|
authoritative writable checkout. Otherwise it runs in the caller's current
|
|
1219
|
-
directory.
|
|
1220
|
-
item's declared branch to subcommands that would otherwise infer Git context.
|
|
1127
|
+
directory.
|
|
1221
1128
|
|
|
1222
1129
|
### Status and Validation
|
|
1223
1130
|
|
package/cli-main.ts
CHANGED
|
@@ -48,12 +48,9 @@ import { DoctorService } from "./src/services/DoctorService"
|
|
|
48
48
|
import { ReviewService } from "./src/services/ReviewService"
|
|
49
49
|
import {
|
|
50
50
|
GitVersionControlService,
|
|
51
|
-
JjVersionControlService,
|
|
52
51
|
VersionControlService,
|
|
53
52
|
} from "./src/services/VersionControlService"
|
|
54
53
|
import { review, help as reviewHelp } from "./src/commands/review"
|
|
55
|
-
import { vcs, help as vcsHelp } from "./src/commands/vcs"
|
|
56
|
-
import { VcsMigrationService } from "./src/services/VcsMigrationService"
|
|
57
54
|
import { act, help as actHelp } from "./src/commands/act"
|
|
58
55
|
import {
|
|
59
56
|
claimCommand,
|
|
@@ -74,9 +71,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
74
71
|
FileSystemService.Default,
|
|
75
72
|
WorkbaseService.Default,
|
|
76
73
|
GitVersionControlService.Default,
|
|
77
|
-
JjVersionControlService.Default,
|
|
78
74
|
VersionControlService.Default,
|
|
79
|
-
VcsMigrationService.Default,
|
|
80
75
|
RepositoryService.Default,
|
|
81
76
|
EpicService.Default,
|
|
82
77
|
TaskService.Default,
|
|
@@ -401,10 +396,18 @@ const commands: Record<string, Command> = {
|
|
|
401
396
|
console.log(archiveHelp)
|
|
402
397
|
return
|
|
403
398
|
}
|
|
399
|
+
const explicitType = [
|
|
400
|
+
"list",
|
|
401
|
+
"show",
|
|
402
|
+
"epic",
|
|
403
|
+
"task",
|
|
404
|
+
"tasks",
|
|
405
|
+
"phase",
|
|
406
|
+
].includes(args[0] ?? "")
|
|
404
407
|
await runCommand(
|
|
405
408
|
archive({
|
|
406
|
-
type: args[0],
|
|
407
|
-
args: args.slice(1),
|
|
409
|
+
type: explicitType ? args[0] : undefined,
|
|
410
|
+
args: explicitType ? args.slice(1) : args,
|
|
408
411
|
json: options.json,
|
|
409
412
|
dryRun: options["dry-run"],
|
|
410
413
|
kinds: options.kind,
|
|
@@ -623,26 +626,6 @@ const commands: Record<string, Command> = {
|
|
|
623
626
|
)
|
|
624
627
|
},
|
|
625
628
|
},
|
|
626
|
-
vcs: {
|
|
627
|
-
run: async (args: string[], options: Record<string, any>) => {
|
|
628
|
-
if (options.help) {
|
|
629
|
-
console.log(vcsHelp)
|
|
630
|
-
return
|
|
631
|
-
}
|
|
632
|
-
await runCommand(
|
|
633
|
-
vcs({
|
|
634
|
-
subcommand: args[0],
|
|
635
|
-
target: args[1],
|
|
636
|
-
apply: options.apply,
|
|
637
|
-
dryRun: options["dry-run"],
|
|
638
|
-
json: options.json,
|
|
639
|
-
silent: options.silent,
|
|
640
|
-
verbose: options.verbose,
|
|
641
|
-
cwd: options.cwd,
|
|
642
|
-
}),
|
|
643
|
-
)
|
|
644
|
-
},
|
|
645
|
-
},
|
|
646
629
|
next: {
|
|
647
630
|
run: async (_args: string[], options: Record<string, any>) => {
|
|
648
631
|
if (options.help) return console.log(nextHelp)
|
|
@@ -802,7 +785,6 @@ Commands:
|
|
|
802
785
|
task <subcommand> Manage tasks
|
|
803
786
|
work [directory|task] Work on an epic, task, or phase
|
|
804
787
|
worktree <subcommand> Inspect and maintain managed workspaces
|
|
805
|
-
vcs <subcommand> Inspect or migrate the version-control backend
|
|
806
788
|
next List or select ready execution units
|
|
807
789
|
pr create / pr [...] Create an Agency PR or run gh pr with repository focus
|
|
808
790
|
push Validate and publish the current execution unit
|
package/cli.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const fastVcsStatus =
|
|
5
|
-
args[0] === "vcs" &&
|
|
6
|
-
args[1] === "status" &&
|
|
7
|
-
(args.length === 2 || (args.length === 3 && args[2] === "--json"))
|
|
8
|
-
|
|
9
|
-
if (fastVcsStatus) {
|
|
10
|
-
try {
|
|
11
|
-
const { runVcsStatusFast } = await import("./src/vcs-status-fast")
|
|
12
|
-
if (!(await runVcsStatusFast(args[2] === "--json")))
|
|
13
|
-
await import("./cli-main")
|
|
14
|
-
} catch {
|
|
15
|
-
await import("./cli-main")
|
|
16
|
-
}
|
|
17
|
-
} else {
|
|
18
|
-
await import("./cli-main")
|
|
19
|
-
}
|
|
3
|
+
await import("./cli-main")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -83,7 +83,6 @@
|
|
|
83
83
|
"benchmark:graph": "bun scripts/benchmark-graph.ts",
|
|
84
84
|
"benchmark:push": "bun scripts/benchmark-push.ts",
|
|
85
85
|
"benchmark:archive": "bun scripts/benchmark-archive.ts",
|
|
86
|
-
"benchmark:vcs": "bun scripts/benchmark-vcs.ts",
|
|
87
86
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
88
87
|
"benchmark:task": "bun scripts/benchmark-task.ts",
|
|
89
88
|
"benchmark:validate": "bun scripts/benchmark-validate.ts",
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -478,6 +478,11 @@ describe("strict CLI parsing", () => {
|
|
|
478
478
|
})
|
|
479
479
|
|
|
480
480
|
test("accepts archive dry-run", () => {
|
|
481
|
+
expect(parseCli(["archive", ".", "--dry-run"])).toMatchObject({
|
|
482
|
+
commandName: "archive",
|
|
483
|
+
args: ["."],
|
|
484
|
+
values: { "dry-run": true },
|
|
485
|
+
})
|
|
481
486
|
expect(parseCli(["archive", "task", "example", "--dry-run"])).toMatchObject(
|
|
482
487
|
{
|
|
483
488
|
commandName: "archive",
|
|
@@ -674,25 +679,6 @@ describe("strict CLI parsing", () => {
|
|
|
674
679
|
)
|
|
675
680
|
})
|
|
676
681
|
|
|
677
|
-
test("parses VCS status and migration commands", () => {
|
|
678
|
-
expect(parseCli(["vcs", "status", "--json"])).toMatchObject({
|
|
679
|
-
commandName: "vcs",
|
|
680
|
-
args: ["status"],
|
|
681
|
-
values: { json: true },
|
|
682
|
-
})
|
|
683
|
-
expect(
|
|
684
|
-
parseCli(["vcs", "migrate", "jj", "--apply", "--json"]),
|
|
685
|
-
).toMatchObject({
|
|
686
|
-
commandName: "vcs",
|
|
687
|
-
args: ["migrate", "jj"],
|
|
688
|
-
values: { apply: true, json: true },
|
|
689
|
-
})
|
|
690
|
-
expectUsageError(["vcs", "migrate"], "agency vcs migrate")
|
|
691
|
-
expect(() =>
|
|
692
|
-
parseCli(["vcs", "migrate", "jj", "--dry-run", "--apply"]),
|
|
693
|
-
).toThrow("cannot be combined")
|
|
694
|
-
})
|
|
695
|
-
|
|
696
682
|
test("parses push without accepting an alternate target", () => {
|
|
697
683
|
expect(parseCli(["push", "--json"])).toMatchObject({
|
|
698
684
|
commandName: "push",
|
package/src/cli-parser.ts
CHANGED
|
@@ -763,7 +763,7 @@ const commands = {
|
|
|
763
763
|
},
|
|
764
764
|
},
|
|
765
765
|
archive: {
|
|
766
|
-
usage: "agency archive <list|show|epic|task|tasks|phase>",
|
|
766
|
+
usage: "agency archive <path|list|show|epic|task|tasks|phase>",
|
|
767
767
|
options: {
|
|
768
768
|
...outputOptions,
|
|
769
769
|
...entitySelectorOptions,
|
|
@@ -772,6 +772,12 @@ const commands = {
|
|
|
772
772
|
status: { type: "string", multiple: true },
|
|
773
773
|
repository: { type: "string", multiple: true },
|
|
774
774
|
},
|
|
775
|
+
command: {
|
|
776
|
+
usage: "agency archive <path> [--dry-run] [--json]",
|
|
777
|
+
minArgs: 1,
|
|
778
|
+
maxArgs: 1,
|
|
779
|
+
options: ["dry-run", "json"],
|
|
780
|
+
},
|
|
775
781
|
subcommands: {
|
|
776
782
|
list: {
|
|
777
783
|
usage:
|
|
@@ -908,29 +914,6 @@ const commands = {
|
|
|
908
914
|
},
|
|
909
915
|
},
|
|
910
916
|
},
|
|
911
|
-
vcs: {
|
|
912
|
-
usage: "agency vcs <status|migrate>",
|
|
913
|
-
options: {
|
|
914
|
-
...outputOptions,
|
|
915
|
-
apply: { type: "boolean" },
|
|
916
|
-
"dry-run": { type: "boolean" },
|
|
917
|
-
},
|
|
918
|
-
subcommands: {
|
|
919
|
-
status: {
|
|
920
|
-
usage: "agency vcs status [--json]",
|
|
921
|
-
minArgs: 0,
|
|
922
|
-
maxArgs: 0,
|
|
923
|
-
options: ["json"],
|
|
924
|
-
},
|
|
925
|
-
migrate: {
|
|
926
|
-
usage: "agency vcs migrate <git|jj> [--dry-run | --apply] [--json]",
|
|
927
|
-
minArgs: 1,
|
|
928
|
-
maxArgs: 1,
|
|
929
|
-
options: ["apply", "dry-run", "json"],
|
|
930
|
-
conflicts: [["apply", "dry-run"]],
|
|
931
|
-
},
|
|
932
|
-
},
|
|
933
|
-
},
|
|
934
917
|
work: {
|
|
935
918
|
usage:
|
|
936
919
|
"agency work [<directory-or-task-id> | --epic <epic-id>] [--agent <name>] [--auto] | agency work prepare [target] [--dry-run] [--json]",
|
|
@@ -1497,9 +1480,8 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1497
1480
|
values: parsed.values,
|
|
1498
1481
|
}
|
|
1499
1482
|
}
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
: definition.command
|
|
1483
|
+
const selectedSubcommand = definition.subcommands?.[subcommand ?? ""]
|
|
1484
|
+
const spec = selectedSubcommand ?? definition.command
|
|
1503
1485
|
if (!spec) {
|
|
1504
1486
|
const message = subcommand
|
|
1505
1487
|
? `Unknown subcommand '${subcommand}' for 'agency ${commandName}'.`
|
|
@@ -1507,7 +1489,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1507
1489
|
throw usageError(message, definition.usage)
|
|
1508
1490
|
}
|
|
1509
1491
|
|
|
1510
|
-
let commandPositionals =
|
|
1492
|
+
let commandPositionals = selectedSubcommand
|
|
1511
1493
|
? parsed.positionals.slice(1)
|
|
1512
1494
|
: parsed.positionals
|
|
1513
1495
|
const allowed = new Set([...commonOptionNames, ...(spec.options ?? [])])
|
|
@@ -1558,7 +1540,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1558
1540
|
|
|
1559
1541
|
commandPositionals = applyEntitySelectors(
|
|
1560
1542
|
commandName,
|
|
1561
|
-
subcommand,
|
|
1543
|
+
selectedSubcommand ? subcommand : undefined,
|
|
1562
1544
|
commandPositionals,
|
|
1563
1545
|
parsed.values,
|
|
1564
1546
|
spec,
|
|
@@ -1731,7 +1713,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1731
1713
|
|
|
1732
1714
|
return {
|
|
1733
1715
|
commandName: commandName as keyof typeof commands,
|
|
1734
|
-
args:
|
|
1716
|
+
args: selectedSubcommand
|
|
1735
1717
|
? [subcommand!, ...commandPositionals]
|
|
1736
1718
|
: commandPositionals,
|
|
1737
1719
|
values: parsed.values,
|
package/src/cli.test.ts
CHANGED
|
@@ -531,9 +531,9 @@ status: dropped
|
|
|
531
531
|
`,
|
|
532
532
|
)
|
|
533
533
|
|
|
534
|
-
expect(
|
|
535
|
-
|
|
536
|
-
)
|
|
534
|
+
expect(
|
|
535
|
+
(await runCli(["archive", "."], join(root, "tasks/example"))).exitCode,
|
|
536
|
+
).toBe(0)
|
|
537
537
|
const result = await runCli(["archive", "task", "example", "--json"], root)
|
|
538
538
|
|
|
539
539
|
expect(result.exitCode).toBe(1)
|
|
@@ -1929,9 +1929,9 @@ status: open
|
|
|
1929
1929
|
alias: "agency",
|
|
1930
1930
|
status: "applied",
|
|
1931
1931
|
})
|
|
1932
|
-
expect(
|
|
1933
|
-
|
|
1934
|
-
)
|
|
1932
|
+
expect((await stat(join(restored, "repos/agency/HEAD"))).isFile()).toBe(
|
|
1933
|
+
true,
|
|
1934
|
+
)
|
|
1935
1935
|
|
|
1936
1936
|
const prepared = parseJson(
|
|
1937
1937
|
await runCli(["work", "prepare", "portable", "--json"], restored),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
2
3
|
import { join } from "node:path"
|
|
3
4
|
import {
|
|
4
5
|
captureLogs,
|
|
@@ -93,6 +94,75 @@ describe("archive command", () => {
|
|
|
93
94
|
)
|
|
94
95
|
})
|
|
95
96
|
|
|
97
|
+
test("infers a task from a filesystem path", async () => {
|
|
98
|
+
const logs = await captureLogs(() =>
|
|
99
|
+
runTestEffect(
|
|
100
|
+
archive({
|
|
101
|
+
args: ["."],
|
|
102
|
+
cwd: join(root, "tasks/example"),
|
|
103
|
+
dryRun: true,
|
|
104
|
+
json: true,
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
110
|
+
operation: "archive",
|
|
111
|
+
kind: "task",
|
|
112
|
+
id: "example",
|
|
113
|
+
dryRun: true,
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("infers an epic from a filesystem path", async () => {
|
|
118
|
+
const directory = join(root, "epics/delivery")
|
|
119
|
+
await mkdir(directory, { recursive: true })
|
|
120
|
+
await Bun.write(
|
|
121
|
+
join(directory, "EPIC.md"),
|
|
122
|
+
`---
|
|
123
|
+
ticketUrl: https://example.com/epic
|
|
124
|
+
repos:
|
|
125
|
+
- repo: agency
|
|
126
|
+
ref: main
|
|
127
|
+
tasks: []
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
# Delivery
|
|
131
|
+
`,
|
|
132
|
+
)
|
|
133
|
+
const logs = await captureLogs(() =>
|
|
134
|
+
runTestEffect(
|
|
135
|
+
archive({ args: ["."], cwd: directory, dryRun: true, json: true }),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
140
|
+
operation: "archive",
|
|
141
|
+
kind: "epic",
|
|
142
|
+
id: "delivery",
|
|
143
|
+
dryRun: true,
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("rejects ambiguous and unsupported archive paths", async () => {
|
|
148
|
+
await expect(
|
|
149
|
+
runTestEffect(archive({ args: ["."], cwd: root, silent: true })),
|
|
150
|
+
).rejects.toThrow("Archive path is ambiguous")
|
|
151
|
+
await expect(
|
|
152
|
+
runTestEffect(
|
|
153
|
+
archive({ args: ["repos/agency"], cwd: root, silent: true }),
|
|
154
|
+
),
|
|
155
|
+
).rejects.toThrow("Archive path must be within an active epic or task")
|
|
156
|
+
|
|
157
|
+
const phaseDirectory = join(root, "tasks/example/phases/build")
|
|
158
|
+
await mkdir(phaseDirectory, { recursive: true })
|
|
159
|
+
await expect(
|
|
160
|
+
runTestEffect(
|
|
161
|
+
archive({ args: ["."], cwd: phaseDirectory, silent: true }),
|
|
162
|
+
),
|
|
163
|
+
).rejects.toThrow("Archive path identifies a phase")
|
|
164
|
+
})
|
|
165
|
+
|
|
96
166
|
test("reports an already archived task", async () => {
|
|
97
167
|
await runTestEffect(
|
|
98
168
|
archive({ type: "task", args: ["example"], cwd: root, silent: true }),
|
|
@@ -143,7 +213,9 @@ describe("archive command", () => {
|
|
|
143
213
|
test("requires a supported work item type", async () => {
|
|
144
214
|
await expect(
|
|
145
215
|
runTestEffect(archive({ args: [], cwd: root, silent: true })),
|
|
146
|
-
).rejects.toThrow(
|
|
216
|
+
).rejects.toThrow(
|
|
217
|
+
"Provide a path or use: list, show, epic, task, tasks, phase",
|
|
218
|
+
)
|
|
147
219
|
})
|
|
148
220
|
|
|
149
221
|
test("rejects an extra archive show identifier", async () => {
|
package/src/commands/archive.ts
CHANGED
|
@@ -24,6 +24,14 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
24
24
|
const { log } = createLoggers(options)
|
|
25
25
|
const cwd = options.cwd ?? process.cwd()
|
|
26
26
|
const [id, phaseId] = options.args
|
|
27
|
+
let archiveType = options.type
|
|
28
|
+
let archiveId = id
|
|
29
|
+
|
|
30
|
+
if (!archiveType && id) {
|
|
31
|
+
const target = yield* archives.resolvePathTarget(id, cwd)
|
|
32
|
+
archiveType = target.kind
|
|
33
|
+
archiveId = target.id
|
|
34
|
+
}
|
|
27
35
|
|
|
28
36
|
if (options.type === "list") {
|
|
29
37
|
const records = yield* archives.list(
|
|
@@ -73,22 +81,22 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
let result
|
|
76
|
-
switch (
|
|
84
|
+
switch (archiveType) {
|
|
77
85
|
case "epic":
|
|
78
|
-
if (!
|
|
86
|
+
if (!archiveId)
|
|
79
87
|
return yield* Effect.fail(
|
|
80
88
|
new Error("Usage: agency archive epic <epic-id>"),
|
|
81
89
|
)
|
|
82
|
-
result = yield* archives.archiveEpic(
|
|
90
|
+
result = yield* archives.archiveEpic(archiveId, cwd, {
|
|
83
91
|
dryRun: options.dryRun,
|
|
84
92
|
})
|
|
85
93
|
break
|
|
86
94
|
case "task":
|
|
87
|
-
if (!
|
|
95
|
+
if (!archiveId)
|
|
88
96
|
return yield* Effect.fail(
|
|
89
97
|
new Error("Usage: agency archive task <task-id>"),
|
|
90
98
|
)
|
|
91
|
-
result = yield* archives.archiveTask(
|
|
99
|
+
result = yield* archives.archiveTask(archiveId, cwd, {
|
|
92
100
|
dryRun: options.dryRun,
|
|
93
101
|
})
|
|
94
102
|
break
|
|
@@ -109,7 +117,7 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
109
117
|
default:
|
|
110
118
|
return yield* Effect.fail(
|
|
111
119
|
new Error(
|
|
112
|
-
"Archive
|
|
120
|
+
"Archive target is required. Provide a path or use: list, show, epic, task, tasks, phase",
|
|
113
121
|
),
|
|
114
122
|
)
|
|
115
123
|
}
|
|
@@ -141,11 +149,14 @@ export const archive = (options: ArchiveOptions) =>
|
|
|
141
149
|
})
|
|
142
150
|
|
|
143
151
|
export const help = `
|
|
144
|
-
Usage: agency archive <list|show|epic|task|tasks|phase>
|
|
152
|
+
Usage: agency archive <path|list|show|epic|task|tasks|phase>
|
|
145
153
|
|
|
146
154
|
Browse or archive work items after preflighting worktrees and graph references.
|
|
147
155
|
|
|
156
|
+
An existing path within an active epic or task infers that work item.
|
|
157
|
+
|
|
148
158
|
Commands:
|
|
159
|
+
<path> Archive the containing epic or task
|
|
149
160
|
list [filters] List archived work
|
|
150
161
|
show <type> <id> Show an archived epic or task
|
|
151
162
|
show phase <task-id> <phase-id> Show an archived phase
|