@markjaquith/agency 2.13.0 → 2.15.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 +62 -8
- package/cli.ts +66 -0
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +28 -1
- package/skills/agency/SKILL.md +14 -3
- package/src/cli-parser.test.ts +81 -0
- package/src/cli-parser.ts +71 -0
- package/src/cli.test.ts +85 -0
- package/src/commands/claim.ts +97 -0
- package/src/commands/phase.ts +1 -1
- package/src/commands/task-phase.test.ts +4 -4
- package/src/commands/task.ts +32 -35
- package/src/commands/validate.ts +1 -6
- package/src/commands/work.test.ts +46 -12
- package/src/commands/work.ts +38 -19
- package/src/protocol.test.ts +25 -0
- package/src/protocol.ts +16 -0
- package/src/services/ClaimService.test.ts +270 -0
- package/src/services/ClaimService.ts +446 -0
- package/src/services/PhaseService.ts +13 -0
- package/src/services/TaskPhaseService.test.ts +10 -9
- package/src/services/TaskService.ts +11 -0
- package/src/test-utils.ts +2 -0
- package/src/utils/chooser.test.ts +108 -0
- package/src/utils/chooser.ts +222 -0
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/document-revision.ts +2 -0
- package/src/workbase/frontmatter.ts +59 -53
- package/src/workbase/schemas.test.ts +39 -0
- package/src/workbase/schemas.ts +22 -0
- package/src/workbase/work-target.test.ts +10 -0
- package/src/workbase/work-target.ts +45 -35
- package/src/workbase/workbase-choice.ts +12 -37
package/README.md
CHANGED
|
@@ -11,7 +11,6 @@ or write.
|
|
|
11
11
|
- Git
|
|
12
12
|
- [GitHub CLI](https://cli.github.com/) for `agency pr create`
|
|
13
13
|
- OpenCode or Claude Code for `agency work`
|
|
14
|
-
- [fzf](https://github.com/junegunn/fzf) for interactive work target selection
|
|
15
14
|
|
|
16
15
|
## Installation
|
|
17
16
|
|
|
@@ -146,6 +145,29 @@ The configured command applies only to the writable checkout. Supplemental
|
|
|
146
145
|
read-only repositories remain detached Git worktrees at their declared refs so
|
|
147
146
|
they do not acquire writable branches.
|
|
148
147
|
|
|
148
|
+
### Custom Chooser Command
|
|
149
|
+
|
|
150
|
+
Interactive selection uses a native numbered chooser by default. To use an
|
|
151
|
+
external chooser, configure an argv command in `agency.json`:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"version": 2,
|
|
156
|
+
"chooserCommand": ["fzf", "--ansi", "--delimiter=\\t", "--with-nth=2.."]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Agency writes one `key<TAB>label` record per choice to the command's stdin. The
|
|
161
|
+
command must write the selected opaque key or selected record to stdout; commands
|
|
162
|
+
such as `["gum", "filter"]` therefore work without wrappers. Exit codes 1 and
|
|
163
|
+
130, empty stdout, native `q`, and an empty native response cancel selection.
|
|
164
|
+
Other nonzero exits, unknown keys, and invalid native numbers are errors.
|
|
165
|
+
|
|
166
|
+
Selectors are opened only when stdin and stderr are terminals and neither
|
|
167
|
+
`--no-input` nor JSON output is active. Labels use color only when stderr is a
|
|
168
|
+
terminal, `TERM` is not `dumb`, and `NO_COLOR` is unset; otherwise selectors use
|
|
169
|
+
plain labels without ANSI styling or icon-font dependencies.
|
|
170
|
+
|
|
149
171
|
## Frontmatter
|
|
150
172
|
|
|
151
173
|
### Epic
|
|
@@ -357,7 +379,7 @@ Inspect tasks:
|
|
|
357
379
|
```text
|
|
358
380
|
agency task list [--json]
|
|
359
381
|
agency task show <id> [--json]
|
|
360
|
-
agency task status <id> <open|
|
|
382
|
+
agency task status <id> <open|done|dropped> [--json]
|
|
361
383
|
```
|
|
362
384
|
|
|
363
385
|
To add a phase to an existing single-phase task, name the phase that will own
|
|
@@ -384,16 +406,48 @@ agency phase create <task-id> <phase-id>
|
|
|
384
406
|
|
|
385
407
|
agency phase list <task-id> [--json]
|
|
386
408
|
agency phase show <task-id> <phase-id> [--json]
|
|
387
|
-
agency phase status <task-id> <phase-id> <open|
|
|
409
|
+
agency phase status <task-id> <phase-id> <open|done|dropped> [--json]
|
|
388
410
|
```
|
|
389
411
|
|
|
390
412
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
391
413
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
392
|
-
before launch. Use
|
|
393
|
-
|
|
394
|
-
markers before execution units.
|
|
395
|
-
to
|
|
396
|
-
or transition to open; reopen terminal
|
|
414
|
+
before launch. Use claims for coordinated ownership and the status subcommands
|
|
415
|
+
for manual lifecycle overrides. The interactive work selector displays status
|
|
416
|
+
markers before execution units. Existing working and delegated work may be
|
|
417
|
+
released to `open` or assigned a terminal outcome. Done and dropped work are
|
|
418
|
+
terminal and may only remain unchanged or transition to open; reopen terminal
|
|
419
|
+
work before changing its outcome.
|
|
420
|
+
|
|
421
|
+
`delegated` remains readable for existing workbases but cannot be newly assigned.
|
|
422
|
+
Delegation is now explicit: the claimant identifies the orchestrator and the
|
|
423
|
+
runner identifies the assigned agent.
|
|
424
|
+
|
|
425
|
+
### Claims
|
|
426
|
+
|
|
427
|
+
Claim mutations require the SHA-256 revision exposed by `agency context` or
|
|
428
|
+
`agency graph`. Every operation compares that revision while holding an exclusive
|
|
429
|
+
document lock and atomically replaces the execution document.
|
|
430
|
+
|
|
431
|
+
```text
|
|
432
|
+
agency claim <task-id> [phase-id] --claimant <id> --runner <id>
|
|
433
|
+
--session-id <id> --revision <sha256> [--expires-at <timestamp>] [--json]
|
|
434
|
+
agency release <task-id> [phase-id] --session-id <id>
|
|
435
|
+
--revision <sha256> [--json]
|
|
436
|
+
agency finish <task-id> [phase-id] --session-id <id>
|
|
437
|
+
--revision <sha256> --outcome <done|dropped> [--json]
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
An active claim sets status to `working`. Release returns it to `open`; finish
|
|
441
|
+
sets the terminal outcome. Released and finished ownership metadata remains in
|
|
442
|
+
frontmatter. Conflicts return the current revision and complete ownership record
|
|
443
|
+
in the machine error envelope rather than overwriting it. Expired claims may be
|
|
444
|
+
replaced with a revision-guarded claim.
|
|
445
|
+
|
|
446
|
+
`agency work` claims an execution unit before launching its agent. Set
|
|
447
|
+
`AGENCY_CLAIMANT`, `AGENCY_RUNNER`, or `AGENCY_SESSION_ID` to supply orchestrator
|
|
448
|
+
identities; otherwise Agency derives them from the user, selected agent, and
|
|
449
|
+
process. The launched agent receives `AGENCY_SESSION_ID` and
|
|
450
|
+
`AGENCY_CLAIM_REVISION` for a later release or finish operation.
|
|
397
451
|
|
|
398
452
|
### Archive
|
|
399
453
|
|
package/cli.ts
CHANGED
|
@@ -32,6 +32,13 @@ import { ArchiveService } from "./src/services/ArchiveService"
|
|
|
32
32
|
import { IntegrationService } from "./src/services/IntegrationService"
|
|
33
33
|
import { ContextService } from "./src/services/ContextService"
|
|
34
34
|
import { GraphService } from "./src/services/GraphService"
|
|
35
|
+
import { ClaimService } from "./src/services/ClaimService"
|
|
36
|
+
import {
|
|
37
|
+
claimCommand,
|
|
38
|
+
claimHelp,
|
|
39
|
+
releaseHelp,
|
|
40
|
+
finishHelp,
|
|
41
|
+
} from "./src/commands/claim"
|
|
35
42
|
import {
|
|
36
43
|
collectCommandResult,
|
|
37
44
|
errorEnvelope,
|
|
@@ -53,6 +60,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
53
60
|
IntegrationService.Default,
|
|
54
61
|
ContextService.Default,
|
|
55
62
|
GraphService.Default,
|
|
63
|
+
ClaimService.Default,
|
|
56
64
|
)
|
|
57
65
|
|
|
58
66
|
/**
|
|
@@ -99,6 +107,61 @@ const VERSION = packageJson.version
|
|
|
99
107
|
|
|
100
108
|
// Define commands
|
|
101
109
|
const commands: Record<string, Command> = {
|
|
110
|
+
claim: {
|
|
111
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
112
|
+
if (options.help) return console.log(claimHelp)
|
|
113
|
+
await runCommand(
|
|
114
|
+
claimCommand({
|
|
115
|
+
operation: "claim",
|
|
116
|
+
taskId: args[0],
|
|
117
|
+
phaseId: args[1],
|
|
118
|
+
claimant: options.claimant,
|
|
119
|
+
runner: options.runner,
|
|
120
|
+
sessionId: options["session-id"],
|
|
121
|
+
revision: options.revision,
|
|
122
|
+
expiresAt: options["expires-at"],
|
|
123
|
+
json: options.json,
|
|
124
|
+
silent: options.silent,
|
|
125
|
+
verbose: options.verbose,
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
release: {
|
|
131
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
132
|
+
if (options.help) return console.log(releaseHelp)
|
|
133
|
+
await runCommand(
|
|
134
|
+
claimCommand({
|
|
135
|
+
operation: "release",
|
|
136
|
+
taskId: args[0],
|
|
137
|
+
phaseId: args[1],
|
|
138
|
+
sessionId: options["session-id"],
|
|
139
|
+
revision: options.revision,
|
|
140
|
+
json: options.json,
|
|
141
|
+
silent: options.silent,
|
|
142
|
+
verbose: options.verbose,
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
finish: {
|
|
148
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
149
|
+
if (options.help) return console.log(finishHelp)
|
|
150
|
+
await runCommand(
|
|
151
|
+
claimCommand({
|
|
152
|
+
operation: "finish",
|
|
153
|
+
taskId: args[0],
|
|
154
|
+
phaseId: args[1],
|
|
155
|
+
sessionId: options["session-id"],
|
|
156
|
+
revision: options.revision,
|
|
157
|
+
outcome: options.outcome,
|
|
158
|
+
json: options.json,
|
|
159
|
+
silent: options.silent,
|
|
160
|
+
verbose: options.verbose,
|
|
161
|
+
}),
|
|
162
|
+
)
|
|
163
|
+
},
|
|
164
|
+
},
|
|
102
165
|
init: {
|
|
103
166
|
run: async (args: string[], options: Record<string, any>) => {
|
|
104
167
|
if (options.help) {
|
|
@@ -375,6 +438,9 @@ Commands:
|
|
|
375
438
|
integration <command> Inspect or sync managed integration files
|
|
376
439
|
epic <subcommand> Manage epics
|
|
377
440
|
phase <subcommand> Manage task phases
|
|
441
|
+
claim <task> [phase] Claim an execution unit
|
|
442
|
+
release <task> [phase] Release an execution unit
|
|
443
|
+
finish <task> [phase] Finish an execution unit
|
|
378
444
|
archive <type> Archive a work item
|
|
379
445
|
task <subcommand> Manage tasks
|
|
380
446
|
work [directory|task] Work on an epic, task, or phase
|
package/package.json
CHANGED
|
@@ -339,6 +339,7 @@
|
|
|
339
339
|
"base": { "type": "string" },
|
|
340
340
|
"pr": { "type": ["string", "null"] },
|
|
341
341
|
"status": { "$ref": "#/$defs/status" },
|
|
342
|
+
"claim": { "$ref": "#/$defs/claim" },
|
|
342
343
|
"sha256": { "type": "string" }
|
|
343
344
|
}
|
|
344
345
|
},
|
|
@@ -372,6 +373,7 @@
|
|
|
372
373
|
"base": { "type": "string" },
|
|
373
374
|
"pr": { "type": ["string", "null"] },
|
|
374
375
|
"status": { "$ref": "#/$defs/status" },
|
|
376
|
+
"claim": { "$ref": "#/$defs/claim" },
|
|
375
377
|
"sha256": { "type": "string" }
|
|
376
378
|
}
|
|
377
379
|
},
|
|
@@ -393,7 +395,32 @@
|
|
|
393
395
|
"branch": { "type": "string" },
|
|
394
396
|
"base": { "type": "string" },
|
|
395
397
|
"pr": { "type": ["string", "null"] },
|
|
396
|
-
"status": { "$ref": "#/$defs/status" }
|
|
398
|
+
"status": { "$ref": "#/$defs/status" },
|
|
399
|
+
"claim": { "$ref": "#/$defs/claim" }
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"claim": {
|
|
403
|
+
"type": "object",
|
|
404
|
+
"additionalProperties": false,
|
|
405
|
+
"required": [
|
|
406
|
+
"claimant",
|
|
407
|
+
"runner",
|
|
408
|
+
"sessionId",
|
|
409
|
+
"startedAt",
|
|
410
|
+
"targetRevision",
|
|
411
|
+
"state"
|
|
412
|
+
],
|
|
413
|
+
"properties": {
|
|
414
|
+
"claimant": { "type": "string", "minLength": 1 },
|
|
415
|
+
"runner": { "type": "string", "minLength": 1 },
|
|
416
|
+
"sessionId": { "type": "string", "minLength": 1 },
|
|
417
|
+
"startedAt": { "type": "string", "format": "date-time" },
|
|
418
|
+
"targetRevision": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
419
|
+
"expiresAt": { "type": "string", "format": "date-time" },
|
|
420
|
+
"state": { "enum": ["active", "released", "finished"] },
|
|
421
|
+
"releasedAt": { "type": "string", "format": "date-time" },
|
|
422
|
+
"finishedAt": { "type": "string", "format": "date-time" },
|
|
423
|
+
"outcome": { "enum": ["done", "dropped"] }
|
|
397
424
|
}
|
|
398
425
|
},
|
|
399
426
|
"repositoryData": {
|
package/skills/agency/SKILL.md
CHANGED
|
@@ -213,7 +213,9 @@ details explicitly with `--include bodies|workspace|git|pr`. For large workbases
|
|
|
213
213
|
- Epic frontmatter owns the child-task dependency graph.
|
|
214
214
|
- `pr` is either a GitHub PR URL string or `null`.
|
|
215
215
|
- Execution-unit `status` is `open`, `working`, `delegated`, `done`, or `dropped`.
|
|
216
|
-
New work starts open, and
|
|
216
|
+
New work starts open, and an active claim sets it working before agent launch.
|
|
217
|
+
`delegated` is readable legacy state; distinct claimant and runner identities
|
|
218
|
+
now represent delegation.
|
|
217
219
|
- Keep directory IDs stable; encode sequencing with `dependsOn`, not numeric
|
|
218
220
|
directory prefixes.
|
|
219
221
|
- Do not use YAML duplicate keys, anchors, aliases, or custom tags.
|
|
@@ -224,8 +226,17 @@ prose, preserve backlinks and run validation immediately afterward.
|
|
|
224
226
|
Update execution status with:
|
|
225
227
|
|
|
226
228
|
```bash
|
|
227
|
-
agency task status <task-id> <open|
|
|
228
|
-
agency phase status <task-id> <phase-id> <open|
|
|
229
|
+
agency task status <task-id> <open|done|dropped>
|
|
230
|
+
agency phase status <task-id> <phase-id> <open|done|dropped>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Coordinate execution ownership with a document revision from `agency context`
|
|
234
|
+
or `agency graph`:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
agency claim <task-id> [phase-id] --claimant <id> --runner <id> --session-id <id> --revision <sha256>
|
|
238
|
+
agency release <task-id> [phase-id] --session-id <id> --revision <sha256>
|
|
239
|
+
agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped>
|
|
229
240
|
```
|
|
230
241
|
|
|
231
242
|
## Archive Completed Work
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -141,11 +141,92 @@ describe("strict CLI parsing", () => {
|
|
|
141
141
|
[["validate", "one", "two"], "agency validate"],
|
|
142
142
|
[["context", "one", "two"], "agency context"],
|
|
143
143
|
[["graph", "extra"], "agency graph"],
|
|
144
|
+
[
|
|
145
|
+
[
|
|
146
|
+
"claim",
|
|
147
|
+
"one",
|
|
148
|
+
"two",
|
|
149
|
+
"three",
|
|
150
|
+
"--claimant",
|
|
151
|
+
"a",
|
|
152
|
+
"--runner",
|
|
153
|
+
"r",
|
|
154
|
+
"--session-id",
|
|
155
|
+
"s",
|
|
156
|
+
"--revision",
|
|
157
|
+
"0".repeat(64),
|
|
158
|
+
],
|
|
159
|
+
"agency claim",
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
[
|
|
163
|
+
"release",
|
|
164
|
+
"one",
|
|
165
|
+
"two",
|
|
166
|
+
"three",
|
|
167
|
+
"--session-id",
|
|
168
|
+
"s",
|
|
169
|
+
"--revision",
|
|
170
|
+
"0".repeat(64),
|
|
171
|
+
],
|
|
172
|
+
"agency release",
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
[
|
|
176
|
+
"finish",
|
|
177
|
+
"one",
|
|
178
|
+
"two",
|
|
179
|
+
"three",
|
|
180
|
+
"--session-id",
|
|
181
|
+
"s",
|
|
182
|
+
"--revision",
|
|
183
|
+
"0".repeat(64),
|
|
184
|
+
"--outcome",
|
|
185
|
+
"done",
|
|
186
|
+
],
|
|
187
|
+
"agency finish",
|
|
188
|
+
],
|
|
144
189
|
] as const) {
|
|
145
190
|
expectUsageError([...args], usage)
|
|
146
191
|
}
|
|
147
192
|
})
|
|
148
193
|
|
|
194
|
+
test("validates revision-guarded claim operations", () => {
|
|
195
|
+
const revision = "0".repeat(64)
|
|
196
|
+
expect(
|
|
197
|
+
parseCli([
|
|
198
|
+
"claim",
|
|
199
|
+
"task",
|
|
200
|
+
"phase",
|
|
201
|
+
"--claimant",
|
|
202
|
+
"orchestrator",
|
|
203
|
+
"--runner",
|
|
204
|
+
"agent",
|
|
205
|
+
"--session-id",
|
|
206
|
+
"job-1",
|
|
207
|
+
"--revision",
|
|
208
|
+
revision,
|
|
209
|
+
"--expires-at",
|
|
210
|
+
"2026-07-17T13:00:00.000Z",
|
|
211
|
+
]),
|
|
212
|
+
).toMatchObject({ commandName: "claim", args: ["task", "phase"] })
|
|
213
|
+
expect(() =>
|
|
214
|
+
parseCli(["release", "task", "--session-id", "job-1"]),
|
|
215
|
+
).toThrow("--revision' is required")
|
|
216
|
+
expect(() =>
|
|
217
|
+
parseCli([
|
|
218
|
+
"finish",
|
|
219
|
+
"task",
|
|
220
|
+
"--session-id",
|
|
221
|
+
"job-1",
|
|
222
|
+
"--revision",
|
|
223
|
+
revision,
|
|
224
|
+
"--outcome",
|
|
225
|
+
"working",
|
|
226
|
+
]),
|
|
227
|
+
).toThrow("must be 'done' or 'dropped'")
|
|
228
|
+
})
|
|
229
|
+
|
|
149
230
|
test("accepts context projections and keeps compact command-local", () => {
|
|
150
231
|
expect(parseCli(["context", ".", "--json", "--compact"])).toMatchObject({
|
|
151
232
|
commandName: "context",
|
package/src/cli-parser.ts
CHANGED
|
@@ -59,6 +59,21 @@ const phaseCreateOptions = {
|
|
|
59
59
|
"first-phase": { type: "string" },
|
|
60
60
|
} satisfies OptionConfig
|
|
61
61
|
|
|
62
|
+
const claimOptions = {
|
|
63
|
+
...outputOptions,
|
|
64
|
+
claimant: { type: "string" },
|
|
65
|
+
runner: { type: "string" },
|
|
66
|
+
"session-id": { type: "string" },
|
|
67
|
+
revision: { type: "string" },
|
|
68
|
+
"expires-at": { type: "string" },
|
|
69
|
+
} satisfies OptionConfig
|
|
70
|
+
|
|
71
|
+
const ownedClaimOptions = {
|
|
72
|
+
...outputOptions,
|
|
73
|
+
"session-id": { type: "string" },
|
|
74
|
+
revision: { type: "string" },
|
|
75
|
+
} satisfies OptionConfig
|
|
76
|
+
|
|
62
77
|
const commands = {
|
|
63
78
|
init: {
|
|
64
79
|
usage: "agency init [path] [--json]",
|
|
@@ -258,6 +273,52 @@ const commands = {
|
|
|
258
273
|
},
|
|
259
274
|
},
|
|
260
275
|
},
|
|
276
|
+
claim: {
|
|
277
|
+
usage: "agency claim <task-id> [phase-id] [options]",
|
|
278
|
+
options: claimOptions,
|
|
279
|
+
command: {
|
|
280
|
+
usage:
|
|
281
|
+
"agency claim <task-id> [phase-id] --claimant <id> --runner <id> --session-id <id> --revision <sha256> [--expires-at <timestamp>] [--json]",
|
|
282
|
+
minArgs: 1,
|
|
283
|
+
maxArgs: 2,
|
|
284
|
+
options: [
|
|
285
|
+
"claimant",
|
|
286
|
+
"runner",
|
|
287
|
+
"session-id",
|
|
288
|
+
"revision",
|
|
289
|
+
"expires-at",
|
|
290
|
+
"json",
|
|
291
|
+
],
|
|
292
|
+
required: ["claimant", "runner", "session-id", "revision"],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
release: {
|
|
296
|
+
usage: "agency release <task-id> [phase-id] [options]",
|
|
297
|
+
options: ownedClaimOptions,
|
|
298
|
+
command: {
|
|
299
|
+
usage:
|
|
300
|
+
"agency release <task-id> [phase-id] --session-id <id> --revision <sha256> [--json]",
|
|
301
|
+
minArgs: 1,
|
|
302
|
+
maxArgs: 2,
|
|
303
|
+
options: ["session-id", "revision", "json"],
|
|
304
|
+
required: ["session-id", "revision"],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
finish: {
|
|
308
|
+
usage: "agency finish <task-id> [phase-id] [options]",
|
|
309
|
+
options: {
|
|
310
|
+
...ownedClaimOptions,
|
|
311
|
+
outcome: { type: "string" },
|
|
312
|
+
},
|
|
313
|
+
command: {
|
|
314
|
+
usage:
|
|
315
|
+
"agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--json]",
|
|
316
|
+
minArgs: 1,
|
|
317
|
+
maxArgs: 2,
|
|
318
|
+
options: ["session-id", "revision", "outcome", "json"],
|
|
319
|
+
required: ["session-id", "revision", "outcome"],
|
|
320
|
+
},
|
|
321
|
+
},
|
|
261
322
|
archive: {
|
|
262
323
|
usage: "agency archive <epic|task|phase>",
|
|
263
324
|
options: outputOptions,
|
|
@@ -653,6 +714,16 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
653
714
|
if (commandName === "graph") {
|
|
654
715
|
validateGraphOptions(parsed.values, spec)
|
|
655
716
|
}
|
|
717
|
+
if (
|
|
718
|
+
commandName === "finish" &&
|
|
719
|
+
parsed.values.outcome !== "done" &&
|
|
720
|
+
parsed.values.outcome !== "dropped"
|
|
721
|
+
) {
|
|
722
|
+
throw usageError(
|
|
723
|
+
"Option '--outcome' must be 'done' or 'dropped'.",
|
|
724
|
+
spec.usage,
|
|
725
|
+
)
|
|
726
|
+
}
|
|
656
727
|
if (commandName === "work") {
|
|
657
728
|
const preparing = commandPositionals[0] === "prepare"
|
|
658
729
|
if (
|
package/src/cli.test.ts
CHANGED
|
@@ -81,6 +81,91 @@ describe("CLI", () => {
|
|
|
81
81
|
expect(taggedError.stderr).not.toContain("An error has occurred")
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
+
test("coordinates claims through revision-guarded machine commands", async () => {
|
|
85
|
+
const root = await createTempDir()
|
|
86
|
+
tempDirs.push(root)
|
|
87
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
88
|
+
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
89
|
+
parseJson(
|
|
90
|
+
await runCli(
|
|
91
|
+
["task", "create", "claimed", "--repo", "agency", "--json"],
|
|
92
|
+
root,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
const context = parseJson(
|
|
96
|
+
await runCli(["context", "tasks/claimed", "--json"], root),
|
|
97
|
+
)
|
|
98
|
+
const revision = context.documents.task.sha256
|
|
99
|
+
const claimed = parseJson(
|
|
100
|
+
await runCli(
|
|
101
|
+
[
|
|
102
|
+
"claim",
|
|
103
|
+
"claimed",
|
|
104
|
+
"--claimant",
|
|
105
|
+
"orchestrator",
|
|
106
|
+
"--runner",
|
|
107
|
+
"agent",
|
|
108
|
+
"--session-id",
|
|
109
|
+
"job-1",
|
|
110
|
+
"--revision",
|
|
111
|
+
revision,
|
|
112
|
+
"--json",
|
|
113
|
+
],
|
|
114
|
+
root,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
expect(claimed.claim).toMatchObject({
|
|
118
|
+
claimant: "orchestrator",
|
|
119
|
+
runner: "agent",
|
|
120
|
+
sessionId: "job-1",
|
|
121
|
+
state: "active",
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const conflict = await runCli(
|
|
125
|
+
[
|
|
126
|
+
"claim",
|
|
127
|
+
"claimed",
|
|
128
|
+
"--claimant",
|
|
129
|
+
"other",
|
|
130
|
+
"--runner",
|
|
131
|
+
"other-agent",
|
|
132
|
+
"--session-id",
|
|
133
|
+
"job-2",
|
|
134
|
+
"--revision",
|
|
135
|
+
claimed.revision,
|
|
136
|
+
"--json",
|
|
137
|
+
],
|
|
138
|
+
root,
|
|
139
|
+
)
|
|
140
|
+
expect(conflict.exitCode).toBe(1)
|
|
141
|
+
expect(JSON.parse(conflict.stdout)).toMatchObject({
|
|
142
|
+
ok: false,
|
|
143
|
+
error: {
|
|
144
|
+
code: "CLAIM_CONFLICT",
|
|
145
|
+
retryable: true,
|
|
146
|
+
fields: { claim: { runner: "agent", sessionId: "job-1" } },
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const finished = parseJson(
|
|
151
|
+
await runCli(
|
|
152
|
+
[
|
|
153
|
+
"finish",
|
|
154
|
+
"claimed",
|
|
155
|
+
"--session-id",
|
|
156
|
+
"job-1",
|
|
157
|
+
"--revision",
|
|
158
|
+
claimed.revision,
|
|
159
|
+
"--outcome",
|
|
160
|
+
"done",
|
|
161
|
+
"--json",
|
|
162
|
+
],
|
|
163
|
+
root,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
expect(finished.claim).toMatchObject({ state: "finished", outcome: "done" })
|
|
167
|
+
})
|
|
168
|
+
|
|
84
169
|
test("emits one versioned error envelope for usage and command failures", async () => {
|
|
85
170
|
const usage = await runCli(["unknown", "--json"])
|
|
86
171
|
expect(usage.exitCode).toBe(1)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { ClaimService } from "../services/ClaimService"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
interface ClaimCommandOptions extends BaseCommandOptions {
|
|
7
|
+
readonly operation: "claim" | "release" | "finish"
|
|
8
|
+
readonly taskId?: string
|
|
9
|
+
readonly phaseId?: string
|
|
10
|
+
readonly claimant?: string
|
|
11
|
+
readonly runner?: string
|
|
12
|
+
readonly sessionId?: string
|
|
13
|
+
readonly revision?: string
|
|
14
|
+
readonly expiresAt?: string
|
|
15
|
+
readonly outcome?: string
|
|
16
|
+
readonly json?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const claimCommand = (options: ClaimCommandOptions) =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const claims = yield* ClaimService
|
|
22
|
+
const { log } = createLoggers(options)
|
|
23
|
+
const cwd = options.cwd ?? process.cwd()
|
|
24
|
+
if (!options.taskId || !options.sessionId || !options.revision) {
|
|
25
|
+
return yield* Effect.fail(new Error("Missing required claim arguments"))
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
options.operation === "claim" &&
|
|
29
|
+
(!options.claimant || !options.runner)
|
|
30
|
+
) {
|
|
31
|
+
return yield* Effect.fail(
|
|
32
|
+
new Error("Claimant and runner identities are required"),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
options.operation === "finish" &&
|
|
37
|
+
options.outcome !== "done" &&
|
|
38
|
+
options.outcome !== "dropped"
|
|
39
|
+
) {
|
|
40
|
+
return yield* Effect.fail(
|
|
41
|
+
new Error("Finish outcome must be done or dropped"),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const common = {
|
|
46
|
+
taskId: options.taskId,
|
|
47
|
+
...(options.phaseId ? { phaseId: options.phaseId } : {}),
|
|
48
|
+
sessionId: options.sessionId,
|
|
49
|
+
revision: options.revision,
|
|
50
|
+
}
|
|
51
|
+
const result =
|
|
52
|
+
options.operation === "claim"
|
|
53
|
+
? yield* claims.claim(
|
|
54
|
+
{
|
|
55
|
+
...common,
|
|
56
|
+
claimant: options.claimant!,
|
|
57
|
+
runner: options.runner!,
|
|
58
|
+
...(options.expiresAt ? { expiresAt: options.expiresAt } : {}),
|
|
59
|
+
},
|
|
60
|
+
cwd,
|
|
61
|
+
)
|
|
62
|
+
: options.operation === "release"
|
|
63
|
+
? yield* claims.release(common, cwd)
|
|
64
|
+
: yield* claims.finish(
|
|
65
|
+
{
|
|
66
|
+
...common,
|
|
67
|
+
outcome: options.outcome as "done" | "dropped",
|
|
68
|
+
},
|
|
69
|
+
cwd,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const { data: _, ...output } = result
|
|
73
|
+
log(
|
|
74
|
+
options.json
|
|
75
|
+
? JSON.stringify(output, null, 2)
|
|
76
|
+
: `${options.operation === "claim" ? "Claimed" : options.operation === "release" ? "Released" : "Finished"} ${result.target} at revision ${result.revision}`,
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const claimHelp = `
|
|
81
|
+
Usage: agency claim <task-id> [phase-id] --claimant <id> --runner <id> --session-id <id> --revision <sha256>
|
|
82
|
+
|
|
83
|
+
Claim an execution unit. Use distinct claimant and runner identities for delegated
|
|
84
|
+
work. --expires-at accepts an optional future ISO-8601 timestamp.
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
export const releaseHelp = `
|
|
88
|
+
Usage: agency release <task-id> [phase-id] --session-id <id> --revision <sha256>
|
|
89
|
+
|
|
90
|
+
Release an execution unit owned by the session.
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
export const finishHelp = `
|
|
94
|
+
Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped>
|
|
95
|
+
|
|
96
|
+
Finish an execution unit owned by the session.
|
|
97
|
+
`
|
package/src/commands/phase.ts
CHANGED
|
@@ -125,7 +125,7 @@ Subcommands:
|
|
|
125
125
|
list <task> List task phases
|
|
126
126
|
show <task> <phase> Show a phase
|
|
127
127
|
status <task> <phase> <status>
|
|
128
|
-
Set open,
|
|
128
|
+
Set open, done, or dropped
|
|
129
129
|
|
|
130
130
|
Create options:
|
|
131
131
|
--description <text> Short description of the phase
|