@markjaquith/agency 2.13.0 → 2.14.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 +39 -7
- 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 +1 -1
- package/src/commands/work.test.ts +26 -0
- package/src/commands/work.ts +34 -5
- 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/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 +27 -0
- package/src/workbase/schemas.ts +21 -0
package/README.md
CHANGED
|
@@ -357,7 +357,7 @@ Inspect tasks:
|
|
|
357
357
|
```text
|
|
358
358
|
agency task list [--json]
|
|
359
359
|
agency task show <id> [--json]
|
|
360
|
-
agency task status <id> <open|
|
|
360
|
+
agency task status <id> <open|done|dropped> [--json]
|
|
361
361
|
```
|
|
362
362
|
|
|
363
363
|
To add a phase to an existing single-phase task, name the phase that will own
|
|
@@ -384,16 +384,48 @@ agency phase create <task-id> <phase-id>
|
|
|
384
384
|
|
|
385
385
|
agency phase list <task-id> [--json]
|
|
386
386
|
agency phase show <task-id> <phase-id> [--json]
|
|
387
|
-
agency phase status <task-id> <phase-id> <open|
|
|
387
|
+
agency phase status <task-id> <phase-id> <open|done|dropped> [--json]
|
|
388
388
|
```
|
|
389
389
|
|
|
390
390
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
391
391
|
`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
|
|
392
|
+
before launch. Use claims for coordinated ownership and the status subcommands
|
|
393
|
+
for manual lifecycle overrides. The interactive work selector displays status
|
|
394
|
+
markers before execution units. Existing working and delegated work may be
|
|
395
|
+
released to `open` or assigned a terminal outcome. Done and dropped work are
|
|
396
|
+
terminal and may only remain unchanged or transition to open; reopen terminal
|
|
397
|
+
work before changing its outcome.
|
|
398
|
+
|
|
399
|
+
`delegated` remains readable for existing workbases but cannot be newly assigned.
|
|
400
|
+
Delegation is now explicit: the claimant identifies the orchestrator and the
|
|
401
|
+
runner identifies the assigned agent.
|
|
402
|
+
|
|
403
|
+
### Claims
|
|
404
|
+
|
|
405
|
+
Claim mutations require the SHA-256 revision exposed by `agency context` or
|
|
406
|
+
`agency graph`. Every operation compares that revision while holding an exclusive
|
|
407
|
+
document lock and atomically replaces the execution document.
|
|
408
|
+
|
|
409
|
+
```text
|
|
410
|
+
agency claim <task-id> [phase-id] --claimant <id> --runner <id>
|
|
411
|
+
--session-id <id> --revision <sha256> [--expires-at <timestamp>] [--json]
|
|
412
|
+
agency release <task-id> [phase-id] --session-id <id>
|
|
413
|
+
--revision <sha256> [--json]
|
|
414
|
+
agency finish <task-id> [phase-id] --session-id <id>
|
|
415
|
+
--revision <sha256> --outcome <done|dropped> [--json]
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
An active claim sets status to `working`. Release returns it to `open`; finish
|
|
419
|
+
sets the terminal outcome. Released and finished ownership metadata remains in
|
|
420
|
+
frontmatter. Conflicts return the current revision and complete ownership record
|
|
421
|
+
in the machine error envelope rather than overwriting it. Expired claims may be
|
|
422
|
+
replaced with a revision-guarded claim.
|
|
423
|
+
|
|
424
|
+
`agency work` claims an execution unit before launching its agent. Set
|
|
425
|
+
`AGENCY_CLAIMANT`, `AGENCY_RUNNER`, or `AGENCY_SESSION_ID` to supply orchestrator
|
|
426
|
+
identities; otherwise Agency derives them from the user, selected agent, and
|
|
427
|
+
process. The launched agent receives `AGENCY_SESSION_ID` and
|
|
428
|
+
`AGENCY_CLAIM_REVISION` for a later release or finish operation.
|
|
397
429
|
|
|
398
430
|
### Archive
|
|
399
431
|
|
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
|
|
@@ -172,13 +172,13 @@ describe("task and phase command JSON output", () => {
|
|
|
172
172
|
runTestEffect(
|
|
173
173
|
phase({
|
|
174
174
|
subcommand: "status",
|
|
175
|
-
args: ["multi", "first", "
|
|
175
|
+
args: ["multi", "first", "done"],
|
|
176
176
|
cwd: root,
|
|
177
177
|
json: true,
|
|
178
178
|
}),
|
|
179
179
|
),
|
|
180
180
|
)
|
|
181
|
-
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("
|
|
181
|
+
expect(JSON.parse(phaseLogs[0]!).data.status).toBe("done")
|
|
182
182
|
|
|
183
183
|
await runTestEffect(
|
|
184
184
|
task({
|
|
@@ -196,13 +196,13 @@ describe("task and phase command JSON output", () => {
|
|
|
196
196
|
runTestEffect(
|
|
197
197
|
task({
|
|
198
198
|
subcommand: "status",
|
|
199
|
-
args: ["single-status", "
|
|
199
|
+
args: ["single-status", "done"],
|
|
200
200
|
cwd: root,
|
|
201
201
|
json: true,
|
|
202
202
|
}),
|
|
203
203
|
),
|
|
204
204
|
)
|
|
205
|
-
expect(JSON.parse(taskLogs[0]!).data.status).toBe("
|
|
205
|
+
expect(JSON.parse(taskLogs[0]!).data.status).toBe("done")
|
|
206
206
|
})
|
|
207
207
|
|
|
208
208
|
test("converts a single-phase task with an explicit first phase ID", async () => {
|
package/src/commands/task.ts
CHANGED
|
@@ -269,7 +269,7 @@ Subcommands:
|
|
|
269
269
|
create <id> Create a task without prompting
|
|
270
270
|
list List tasks
|
|
271
271
|
show <id> Show a task
|
|
272
|
-
status <id> <status> Set open,
|
|
272
|
+
status <id> <status> Set open, done, or dropped
|
|
273
273
|
|
|
274
274
|
Create options:
|
|
275
275
|
--ticket-url <url> External ticket URL (optional)
|