@markjaquith/agency 2.48.1 → 2.49.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 +18 -8
- package/cli.ts +9 -0
- package/package.json +1 -1
- package/src/cli-parser.test.ts +58 -0
- package/src/cli-parser.ts +67 -6
- package/src/cli.test.ts +71 -0
- package/src/commands/claim.ts +26 -2
- package/src/commands/phase.ts +23 -2
- package/src/commands/task.ts +22 -2
- package/src/services/ClaimService.test.ts +59 -0
- package/src/services/ClaimService.ts +34 -1
- package/src/services/GraphMutationService.ts +16 -0
- package/src/services/IntegrationService.test.ts +4 -1
- package/src/services/PhaseService.ts +50 -3
- package/src/services/PullRequestService.test.ts +20 -0
- package/src/services/PullRequestService.ts +24 -3
- package/src/services/SyncService.test.ts +66 -0
- package/src/services/SyncService.ts +13 -0
- package/src/services/TaskPhaseService.test.ts +182 -0
- package/src/services/TaskService.ts +47 -3
- package/src/services/WorkbaseService.test.ts +58 -0
- package/src/services/WorkbaseService.ts +20 -0
- package/src/workbase/AGENTS.md +7 -2
- package/src/workbase/completion.ts +28 -0
- package/src/workbase/schemas.ts +9 -0
package/README.md
CHANGED
|
@@ -610,7 +610,8 @@ Inspect tasks:
|
|
|
610
610
|
```text
|
|
611
611
|
agency task list [filters] [--json]
|
|
612
612
|
agency task show <id> [--json]
|
|
613
|
-
agency task status <id> <open|working|dropped>
|
|
613
|
+
agency task status <id> <open|working|done|dropped>
|
|
614
|
+
[--no-pull-request --summary <text> [--evidence-url <url>]] [--json]
|
|
614
615
|
agency task update <id> [metadata options] [--json]
|
|
615
616
|
agency task rename <id> <new-id> [--json]
|
|
616
617
|
agency task move <id> (--epic <epic-id> | --no-epic) [--json]
|
|
@@ -649,7 +650,8 @@ agency phase create <task-id> <phase-id>
|
|
|
649
650
|
|
|
650
651
|
agency phase list <task-id> [filters] [--json]
|
|
651
652
|
agency phase show <task-id> <phase-id> [--json]
|
|
652
|
-
agency phase status <task-id> <phase-id> <open|working|dropped>
|
|
653
|
+
agency phase status <task-id> <phase-id> <open|working|done|dropped>
|
|
654
|
+
[--no-pull-request --summary <text> [--evidence-url <url>]] [--json]
|
|
653
655
|
agency phase update <task-id> <phase-id> [metadata options] [--json]
|
|
654
656
|
agency phase rename <task-id> <phase-id> <new-id> [--json]
|
|
655
657
|
agency phase dependency <add|remove> <task-id> <phase-id> <dependency-id>
|
|
@@ -672,8 +674,12 @@ writing anything.
|
|
|
672
674
|
Single-phase tasks and phases store status in YAML. New execution units start
|
|
673
675
|
`open`, and `agency work` marks the selected execution unit `working` immediately
|
|
674
676
|
before launch. Running `agency work` again can relaunch unclaimed `working` work.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
+
By default, `done` requires an authoritative merged pull request and is applied
|
|
678
|
+
by `agency sync --apply`. Work whose intended outcome genuinely requires no pull
|
|
679
|
+
request may instead use an explicit `--no-pull-request --summary <text>` status
|
|
680
|
+
transition. Agency records the summary, completion time, and optional evidence
|
|
681
|
+
URL durably; reopening removes that evidence. This exceptional path refuses work
|
|
682
|
+
that already has a recorded pull request.
|
|
677
683
|
Use explicit claims only when an external orchestrator needs coordinated
|
|
678
684
|
ownership. The interactive work selector displays status markers before
|
|
679
685
|
execution units. Existing working and delegated work may be released to `open`
|
|
@@ -704,15 +710,19 @@ agency claim <task-id> [phase-id] --claimant <id> --runner <id>
|
|
|
704
710
|
agency release <task-id> [phase-id] --session-id <id>
|
|
705
711
|
--revision <sha256> [--json]
|
|
706
712
|
agency finish <task-id> [phase-id] --session-id <id>
|
|
707
|
-
--revision <sha256> --outcome <done|dropped>
|
|
713
|
+
--revision <sha256> --outcome <done|dropped>
|
|
714
|
+
[--no-pull-request --summary <text> [--evidence-url <url>]] [--json]
|
|
708
715
|
```
|
|
709
716
|
|
|
710
717
|
An active claim sets status to `working`. Release returns it to `open`. Finish
|
|
711
718
|
records the claim outcome and ownership history; a `done` claim outcome leaves
|
|
712
719
|
the execution unit `working` until its pull request is merged, while `dropped`
|
|
713
|
-
remains terminal.
|
|
714
|
-
|
|
715
|
-
|
|
720
|
+
remains terminal. For a genuine non-PR outcome, `--no-pull-request` atomically
|
|
721
|
+
records completion evidence, finishes the claim, and sets the execution unit to
|
|
722
|
+
`done`; `--summary` is required and `--evidence-url` is optional. Conflicts return
|
|
723
|
+
the current revision and complete ownership record in the machine error envelope
|
|
724
|
+
rather than overwriting it. Expired claims may be replaced with a
|
|
725
|
+
revision-guarded claim.
|
|
716
726
|
|
|
717
727
|
`agency work` does not claim execution units. It refuses active explicit claims,
|
|
718
728
|
marks open execution work `working`, and launches the runner. External
|
package/cli.ts
CHANGED
|
@@ -205,6 +205,9 @@ const commands: Record<string, Command> = {
|
|
|
205
205
|
sessionId: options["session-id"],
|
|
206
206
|
revision: options.revision,
|
|
207
207
|
outcome: options.outcome,
|
|
208
|
+
noPullRequest: options["no-pull-request"],
|
|
209
|
+
summary: options.summary,
|
|
210
|
+
evidenceUrl: options["evidence-url"],
|
|
208
211
|
json: options.json,
|
|
209
212
|
silent: options.silent,
|
|
210
213
|
verbose: options.verbose,
|
|
@@ -298,6 +301,9 @@ const commands: Record<string, Command> = {
|
|
|
298
301
|
clearReferences: options["clear-references"],
|
|
299
302
|
prUrl: options["pr-url"],
|
|
300
303
|
clearPr: options["clear-pr"],
|
|
304
|
+
noPullRequest: options["no-pull-request"],
|
|
305
|
+
summary: options.summary,
|
|
306
|
+
evidenceUrl: options["evidence-url"],
|
|
301
307
|
ifRevision: options["if-revision"],
|
|
302
308
|
dependsOn: options["depends-on"],
|
|
303
309
|
firstPhase: options["first-phase"],
|
|
@@ -449,6 +455,9 @@ const commands: Record<string, Command> = {
|
|
|
449
455
|
clearReferences: options["clear-references"],
|
|
450
456
|
prUrl: options["pr-url"],
|
|
451
457
|
clearPr: options["clear-pr"],
|
|
458
|
+
noPullRequest: options["no-pull-request"],
|
|
459
|
+
summary: options.summary,
|
|
460
|
+
evidenceUrl: options["evidence-url"],
|
|
452
461
|
ifRevision: options["if-revision"],
|
|
453
462
|
noEpic: options["no-epic"],
|
|
454
463
|
multiPhase: options["multi-phase"],
|
package/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -362,6 +362,64 @@ describe("strict CLI parsing", () => {
|
|
|
362
362
|
"working",
|
|
363
363
|
]),
|
|
364
364
|
).toThrow("must be 'done' or 'dropped'")
|
|
365
|
+
expect(
|
|
366
|
+
parseCli([
|
|
367
|
+
"finish",
|
|
368
|
+
"task",
|
|
369
|
+
"--session-id",
|
|
370
|
+
"job-1",
|
|
371
|
+
"--revision",
|
|
372
|
+
revision,
|
|
373
|
+
"--outcome",
|
|
374
|
+
"done",
|
|
375
|
+
"--no-pull-request",
|
|
376
|
+
"--summary",
|
|
377
|
+
"Investigation completed",
|
|
378
|
+
"--evidence-url",
|
|
379
|
+
"https://example.com/result",
|
|
380
|
+
]),
|
|
381
|
+
).toMatchObject({
|
|
382
|
+
values: {
|
|
383
|
+
"no-pull-request": true,
|
|
384
|
+
summary: "Investigation completed",
|
|
385
|
+
"evidence-url": "https://example.com/result",
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
expect(() =>
|
|
389
|
+
parseCli([
|
|
390
|
+
"finish",
|
|
391
|
+
"task",
|
|
392
|
+
"--session-id",
|
|
393
|
+
"job-1",
|
|
394
|
+
"--revision",
|
|
395
|
+
revision,
|
|
396
|
+
"--outcome",
|
|
397
|
+
"done",
|
|
398
|
+
"--no-pull-request",
|
|
399
|
+
]),
|
|
400
|
+
).toThrow("--summary' is required")
|
|
401
|
+
expect(() =>
|
|
402
|
+
parseCli([
|
|
403
|
+
"task",
|
|
404
|
+
"status",
|
|
405
|
+
"example",
|
|
406
|
+
"working",
|
|
407
|
+
"--no-pull-request",
|
|
408
|
+
"--summary",
|
|
409
|
+
"Not done",
|
|
410
|
+
]),
|
|
411
|
+
).toThrow("valid only with a done status")
|
|
412
|
+
expect(() =>
|
|
413
|
+
parseCli([
|
|
414
|
+
"phase",
|
|
415
|
+
"status",
|
|
416
|
+
"task",
|
|
417
|
+
"phase",
|
|
418
|
+
"done",
|
|
419
|
+
"--summary",
|
|
420
|
+
"Missing explicit flag",
|
|
421
|
+
]),
|
|
422
|
+
).toThrow("require '--no-pull-request'")
|
|
365
423
|
})
|
|
366
424
|
|
|
367
425
|
test("accepts valid mutation revisions and rejects malformed hashes", () => {
|
package/src/cli-parser.ts
CHANGED
|
@@ -120,6 +120,12 @@ const ownedClaimOptions = {
|
|
|
120
120
|
revision: { type: "string" },
|
|
121
121
|
} satisfies OptionConfig
|
|
122
122
|
|
|
123
|
+
const nonPrCompletionOptions = {
|
|
124
|
+
"no-pull-request": { type: "boolean" },
|
|
125
|
+
summary: { type: "string" },
|
|
126
|
+
"evidence-url": { type: "string" },
|
|
127
|
+
} satisfies OptionConfig
|
|
128
|
+
|
|
123
129
|
const commands = {
|
|
124
130
|
init: {
|
|
125
131
|
usage: "agency init [path] [--json]",
|
|
@@ -360,6 +366,7 @@ const commands = {
|
|
|
360
366
|
...newWorkOptions,
|
|
361
367
|
...viewOptions,
|
|
362
368
|
...mutationOptions,
|
|
369
|
+
...nonPrCompletionOptions,
|
|
363
370
|
"pr-url": { type: "string" },
|
|
364
371
|
task: { type: "string" },
|
|
365
372
|
},
|
|
@@ -417,10 +424,10 @@ const commands = {
|
|
|
417
424
|
options: ["json", "task"],
|
|
418
425
|
},
|
|
419
426
|
status: {
|
|
420
|
-
usage: "agency task status <id> <status> [--json]",
|
|
427
|
+
usage: "agency task status <id> <status> [options] [--json]",
|
|
421
428
|
minArgs: 2,
|
|
422
429
|
maxArgs: 2,
|
|
423
|
-
options: ["json", "task"],
|
|
430
|
+
options: ["json", "task", "no-pull-request", "summary", "evidence-url"],
|
|
424
431
|
},
|
|
425
432
|
update: {
|
|
426
433
|
usage: "agency task update <id> [options] [--json]",
|
|
@@ -479,6 +486,7 @@ const commands = {
|
|
|
479
486
|
...newWorkOptions,
|
|
480
487
|
...viewOptions,
|
|
481
488
|
...mutationOptions,
|
|
489
|
+
...nonPrCompletionOptions,
|
|
482
490
|
"pr-url": { type: "string" },
|
|
483
491
|
task: { type: "string" },
|
|
484
492
|
phase: { type: "string" },
|
|
@@ -542,10 +550,18 @@ const commands = {
|
|
|
542
550
|
options: ["json", "task", "phase"],
|
|
543
551
|
},
|
|
544
552
|
status: {
|
|
545
|
-
usage:
|
|
553
|
+
usage:
|
|
554
|
+
"agency phase status <task-id> <phase-id> <status> [options] [--json]",
|
|
546
555
|
minArgs: 3,
|
|
547
556
|
maxArgs: 3,
|
|
548
|
-
options: [
|
|
557
|
+
options: [
|
|
558
|
+
"json",
|
|
559
|
+
"task",
|
|
560
|
+
"phase",
|
|
561
|
+
"no-pull-request",
|
|
562
|
+
"summary",
|
|
563
|
+
"evidence-url",
|
|
564
|
+
],
|
|
549
565
|
},
|
|
550
566
|
update: {
|
|
551
567
|
usage: "agency phase update <task-id> <phase-id> [options] [--json]",
|
|
@@ -631,16 +647,27 @@ const commands = {
|
|
|
631
647
|
usage: "agency finish <task-id> [phase-id] [options]",
|
|
632
648
|
options: {
|
|
633
649
|
...ownedClaimOptions,
|
|
650
|
+
...nonPrCompletionOptions,
|
|
634
651
|
outcome: { type: "string" },
|
|
635
652
|
task: { type: "string" },
|
|
636
653
|
phase: { type: "string" },
|
|
637
654
|
},
|
|
638
655
|
command: {
|
|
639
656
|
usage:
|
|
640
|
-
"agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--json]",
|
|
657
|
+
"agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [options] [--json]",
|
|
641
658
|
minArgs: 1,
|
|
642
659
|
maxArgs: 2,
|
|
643
|
-
options: [
|
|
660
|
+
options: [
|
|
661
|
+
"session-id",
|
|
662
|
+
"revision",
|
|
663
|
+
"outcome",
|
|
664
|
+
"no-pull-request",
|
|
665
|
+
"summary",
|
|
666
|
+
"evidence-url",
|
|
667
|
+
"json",
|
|
668
|
+
"task",
|
|
669
|
+
"phase",
|
|
670
|
+
],
|
|
644
671
|
required: ["session-id", "revision", "outcome"],
|
|
645
672
|
},
|
|
646
673
|
},
|
|
@@ -1409,6 +1436,40 @@ export function parseCli(args: readonly string[]): ParsedCli {
|
|
|
1409
1436
|
spec.usage,
|
|
1410
1437
|
)
|
|
1411
1438
|
}
|
|
1439
|
+
const nonPrCompletion = parsed.values["no-pull-request"] === true
|
|
1440
|
+
const completionSummary = parsed.values.summary
|
|
1441
|
+
const completionEvidenceUrl = parsed.values["evidence-url"]
|
|
1442
|
+
if (
|
|
1443
|
+
(completionSummary !== undefined || completionEvidenceUrl !== undefined) &&
|
|
1444
|
+
!nonPrCompletion
|
|
1445
|
+
) {
|
|
1446
|
+
throw usageError(
|
|
1447
|
+
"Options '--summary' and '--evidence-url' require '--no-pull-request'.",
|
|
1448
|
+
spec.usage,
|
|
1449
|
+
)
|
|
1450
|
+
}
|
|
1451
|
+
if (nonPrCompletion) {
|
|
1452
|
+
if (
|
|
1453
|
+
typeof completionSummary !== "string" ||
|
|
1454
|
+
completionSummary.trim().length === 0
|
|
1455
|
+
) {
|
|
1456
|
+
throw usageError(
|
|
1457
|
+
"Option '--summary' is required with '--no-pull-request'.",
|
|
1458
|
+
spec.usage,
|
|
1459
|
+
)
|
|
1460
|
+
}
|
|
1461
|
+
const completesDone =
|
|
1462
|
+
(commandName === "finish" && parsed.values.outcome === "done") ||
|
|
1463
|
+
(["task", "phase"].includes(commandName) &&
|
|
1464
|
+
subcommand === "status" &&
|
|
1465
|
+
commandPositionals.at(-1) === "done")
|
|
1466
|
+
if (!completesDone) {
|
|
1467
|
+
throw usageError(
|
|
1468
|
+
"Option '--no-pull-request' is valid only with a done status or outcome.",
|
|
1469
|
+
spec.usage,
|
|
1470
|
+
)
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1412
1473
|
if (commandName === "work") {
|
|
1413
1474
|
const preparing = commandPositionals[0] === "prepare"
|
|
1414
1475
|
if (
|
package/src/cli.test.ts
CHANGED
|
@@ -248,6 +248,77 @@ describe("CLI", () => {
|
|
|
248
248
|
parseJson(await runCli(["task", "show", "claimed", "--json"], root)).data
|
|
249
249
|
.status,
|
|
250
250
|
).toBe("working")
|
|
251
|
+
|
|
252
|
+
parseJson(
|
|
253
|
+
await runCli(
|
|
254
|
+
["task", "create", "non-pr", "--repo", "agency", "--json"],
|
|
255
|
+
root,
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
const nonPrContext = parseJson(
|
|
259
|
+
await runCli(["context", "tasks/non-pr", "--json"], root),
|
|
260
|
+
)
|
|
261
|
+
const nonPrClaim = parseJson(
|
|
262
|
+
await runCli(
|
|
263
|
+
[
|
|
264
|
+
"claim",
|
|
265
|
+
"non-pr",
|
|
266
|
+
"--claimant",
|
|
267
|
+
"orchestrator",
|
|
268
|
+
"--runner",
|
|
269
|
+
"agent",
|
|
270
|
+
"--session-id",
|
|
271
|
+
"job-2",
|
|
272
|
+
"--revision",
|
|
273
|
+
nonPrContext.documents.task.sha256,
|
|
274
|
+
"--json",
|
|
275
|
+
],
|
|
276
|
+
root,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
parseJson(
|
|
280
|
+
await runCli(
|
|
281
|
+
[
|
|
282
|
+
"finish",
|
|
283
|
+
"non-pr",
|
|
284
|
+
"--session-id",
|
|
285
|
+
"job-2",
|
|
286
|
+
"--revision",
|
|
287
|
+
nonPrClaim.revision,
|
|
288
|
+
"--outcome",
|
|
289
|
+
"done",
|
|
290
|
+
"--no-pull-request",
|
|
291
|
+
"--summary",
|
|
292
|
+
"Investigation completed without changes.",
|
|
293
|
+
"--evidence-url",
|
|
294
|
+
"https://example.com/result",
|
|
295
|
+
"--json",
|
|
296
|
+
],
|
|
297
|
+
root,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
expect(
|
|
301
|
+
parseJson(await runCli(["task", "show", "non-pr", "--json"], root)).data,
|
|
302
|
+
).toMatchObject({
|
|
303
|
+
status: "done",
|
|
304
|
+
completion: {
|
|
305
|
+
mode: "non-pr",
|
|
306
|
+
summary: "Investigation completed without changes.",
|
|
307
|
+
evidenceUrl: "https://example.com/result",
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
const prUpdate = await runCli(
|
|
311
|
+
[
|
|
312
|
+
"task",
|
|
313
|
+
"update",
|
|
314
|
+
"non-pr",
|
|
315
|
+
"--pr-url",
|
|
316
|
+
"https://github.com/example/agency/pull/1",
|
|
317
|
+
],
|
|
318
|
+
root,
|
|
319
|
+
)
|
|
320
|
+
expect(prUpdate.exitCode).toBe(1)
|
|
321
|
+
expect(prUpdate.stderr).toContain("Reopen non-PR completed work")
|
|
251
322
|
})
|
|
252
323
|
|
|
253
324
|
test("routes graph mutations through structured output", async () => {
|
package/src/commands/claim.ts
CHANGED
|
@@ -13,6 +13,9 @@ interface ClaimCommandOptions extends BaseCommandOptions {
|
|
|
13
13
|
readonly revision?: string
|
|
14
14
|
readonly expiresAt?: string
|
|
15
15
|
readonly outcome?: string
|
|
16
|
+
readonly noPullRequest?: boolean
|
|
17
|
+
readonly summary?: string
|
|
18
|
+
readonly evidenceUrl?: string
|
|
16
19
|
readonly json?: boolean
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -32,6 +35,16 @@ export const claimCommand = (options: ClaimCommandOptions) =>
|
|
|
32
35
|
new Error("Claimant and runner identities are required"),
|
|
33
36
|
)
|
|
34
37
|
}
|
|
38
|
+
if (options.noPullRequest && !options.summary?.trim()) {
|
|
39
|
+
return yield* Effect.fail(
|
|
40
|
+
new Error("Non-PR completion requires a non-empty summary"),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
if (options.noPullRequest && options.outcome !== "done") {
|
|
44
|
+
return yield* Effect.fail(
|
|
45
|
+
new Error("Non-PR completion is valid only with a done outcome"),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
35
48
|
if (
|
|
36
49
|
options.operation === "finish" &&
|
|
37
50
|
options.outcome !== "done" &&
|
|
@@ -65,6 +78,16 @@ export const claimCommand = (options: ClaimCommandOptions) =>
|
|
|
65
78
|
{
|
|
66
79
|
...common,
|
|
67
80
|
outcome: options.outcome as "done" | "dropped",
|
|
81
|
+
...(options.noPullRequest
|
|
82
|
+
? {
|
|
83
|
+
nonPrCompletion: {
|
|
84
|
+
summary: options.summary!,
|
|
85
|
+
...(options.evidenceUrl
|
|
86
|
+
? { evidenceUrl: options.evidenceUrl }
|
|
87
|
+
: {}),
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
68
91
|
},
|
|
69
92
|
cwd,
|
|
70
93
|
)
|
|
@@ -91,8 +114,9 @@ Release an execution unit owned by the session.
|
|
|
91
114
|
`
|
|
92
115
|
|
|
93
116
|
export const finishHelp = `
|
|
94
|
-
Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped>
|
|
117
|
+
Usage: agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--no-pull-request --summary <text> [--evidence-url <url>]]
|
|
95
118
|
|
|
96
119
|
Finish a claim owned by the session. A done claim outcome leaves unmerged work
|
|
97
|
-
working; agency sync --apply marks the execution unit done after merge.
|
|
120
|
+
working; agency sync --apply marks the execution unit done after merge. Use
|
|
121
|
+
--no-pull-request with a summary for an explicit non-PR completion.
|
|
98
122
|
`
|
package/src/commands/phase.ts
CHANGED
|
@@ -31,6 +31,9 @@ interface PhaseOptions extends BaseCommandOptions {
|
|
|
31
31
|
readonly pr?: boolean
|
|
32
32
|
readonly work?: boolean
|
|
33
33
|
readonly auto?: boolean
|
|
34
|
+
readonly noPullRequest?: boolean
|
|
35
|
+
readonly summary?: string
|
|
36
|
+
readonly evidenceUrl?: string
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export const phase = (options: PhaseOptions, work: StartWork = startWork) =>
|
|
@@ -166,7 +169,20 @@ export const phase = (options: PhaseOptions, work: StartWork = startWork) =>
|
|
|
166
169
|
),
|
|
167
170
|
)
|
|
168
171
|
}
|
|
169
|
-
const record = yield* phases.setStatus(
|
|
172
|
+
const record = yield* phases.setStatus(
|
|
173
|
+
taskId,
|
|
174
|
+
phaseId,
|
|
175
|
+
status,
|
|
176
|
+
cwd,
|
|
177
|
+
options.noPullRequest
|
|
178
|
+
? {
|
|
179
|
+
summary: options.summary ?? "",
|
|
180
|
+
...(options.evidenceUrl
|
|
181
|
+
? { evidenceUrl: options.evidenceUrl }
|
|
182
|
+
: {}),
|
|
183
|
+
}
|
|
184
|
+
: undefined,
|
|
185
|
+
)
|
|
170
186
|
const { content: _, ...output } = record
|
|
171
187
|
log(
|
|
172
188
|
options.json
|
|
@@ -275,7 +291,7 @@ Subcommands:
|
|
|
275
291
|
list <task> List task phases
|
|
276
292
|
show <task> <phase> Show a phase
|
|
277
293
|
status <task> <phase> <status>
|
|
278
|
-
Set open, working, or
|
|
294
|
+
Set open, working, dropped, or explicit non-PR done
|
|
279
295
|
update <task> <phase> Update phase metadata
|
|
280
296
|
rename <task> <phase> <new-id>
|
|
281
297
|
Rename a phase and update dependencies
|
|
@@ -285,6 +301,11 @@ Subcommands:
|
|
|
285
301
|
Mutation option:
|
|
286
302
|
--if-revision <hash> Require the target's current revision
|
|
287
303
|
|
|
304
|
+
Non-PR completion options (status done only):
|
|
305
|
+
--no-pull-request Complete without a pull request
|
|
306
|
+
--summary <text> Required durable outcome summary
|
|
307
|
+
--evidence-url <url> Optional supporting record
|
|
308
|
+
|
|
288
309
|
Create options:
|
|
289
310
|
--description <text> Short description of the phase
|
|
290
311
|
--repo <alias> Writable repository
|
package/src/commands/task.ts
CHANGED
|
@@ -38,6 +38,9 @@ interface TaskOptions extends BaseCommandOptions {
|
|
|
38
38
|
readonly pr?: boolean
|
|
39
39
|
readonly work?: boolean
|
|
40
40
|
readonly auto?: boolean
|
|
41
|
+
readonly noPullRequest?: boolean
|
|
42
|
+
readonly summary?: string
|
|
43
|
+
readonly evidenceUrl?: string
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export interface TaskInteraction {
|
|
@@ -304,7 +307,19 @@ export const task = (
|
|
|
304
307
|
new Error("Usage: agency task status <id> <status>"),
|
|
305
308
|
)
|
|
306
309
|
}
|
|
307
|
-
const record = yield* tasks.setStatus(
|
|
310
|
+
const record = yield* tasks.setStatus(
|
|
311
|
+
id,
|
|
312
|
+
status,
|
|
313
|
+
cwd,
|
|
314
|
+
options.noPullRequest
|
|
315
|
+
? {
|
|
316
|
+
summary: options.summary ?? "",
|
|
317
|
+
...(options.evidenceUrl
|
|
318
|
+
? { evidenceUrl: options.evidenceUrl }
|
|
319
|
+
: {}),
|
|
320
|
+
}
|
|
321
|
+
: undefined,
|
|
322
|
+
)
|
|
308
323
|
const { content: _, ...output } = record
|
|
309
324
|
log(
|
|
310
325
|
options.json
|
|
@@ -423,7 +438,7 @@ Subcommands:
|
|
|
423
438
|
create <id> Create a task without prompting
|
|
424
439
|
list List tasks
|
|
425
440
|
show <id> Show a task
|
|
426
|
-
status <id> <status> Set open, working, or
|
|
441
|
+
status <id> <status> Set open, working, dropped, or explicit non-PR done
|
|
427
442
|
update <id> Update task metadata
|
|
428
443
|
rename <id> <new-id> Rename a task and update graph references
|
|
429
444
|
move <id> Move a task with --epic or --no-epic
|
|
@@ -433,6 +448,11 @@ Subcommands:
|
|
|
433
448
|
Mutation option:
|
|
434
449
|
--if-revision <hash> Require the target's current revision
|
|
435
450
|
|
|
451
|
+
Non-PR completion options (status done only):
|
|
452
|
+
--no-pull-request Complete without a pull request
|
|
453
|
+
--summary <text> Required durable outcome summary
|
|
454
|
+
--evidence-url <url> Optional supporting record
|
|
455
|
+
|
|
436
456
|
Create options:
|
|
437
457
|
--ticket-url <url> External ticket URL (optional)
|
|
438
458
|
--description <text> Short description of the task
|
|
@@ -147,6 +147,65 @@ describe("claim service", () => {
|
|
|
147
147
|
})
|
|
148
148
|
})
|
|
149
149
|
|
|
150
|
+
test("finishes claimed non-PR work with durable completion evidence", async () => {
|
|
151
|
+
const initial = await inspect()
|
|
152
|
+
const acquired = await claim(initial.revision)
|
|
153
|
+
const finished = await runTestEffect(
|
|
154
|
+
ClaimService.pipe(
|
|
155
|
+
Effect.flatMap((service) =>
|
|
156
|
+
service.finish(
|
|
157
|
+
{
|
|
158
|
+
taskId: "single",
|
|
159
|
+
sessionId: "session-1",
|
|
160
|
+
revision: acquired.revision,
|
|
161
|
+
outcome: "done",
|
|
162
|
+
nonPrCompletion: {
|
|
163
|
+
summary: "Review completed; no code change was needed.",
|
|
164
|
+
evidenceUrl: "https://example.com/review",
|
|
165
|
+
},
|
|
166
|
+
now: at("2026-07-17T12:45:00.000Z"),
|
|
167
|
+
},
|
|
168
|
+
root,
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
expect(finished.data).toMatchObject({
|
|
175
|
+
status: "done",
|
|
176
|
+
claim: { state: "finished", outcome: "done" },
|
|
177
|
+
completion: {
|
|
178
|
+
mode: "non-pr",
|
|
179
|
+
completedAt: "2026-07-17T12:45:00.000Z",
|
|
180
|
+
summary: "Review completed; no code change was needed.",
|
|
181
|
+
evidenceUrl: "https://example.com/review",
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test("rejects invalid claimed non-PR completion", async () => {
|
|
187
|
+
const initial = await inspect()
|
|
188
|
+
const acquired = await claim(initial.revision)
|
|
189
|
+
await expect(
|
|
190
|
+
runTestEffect(
|
|
191
|
+
ClaimService.pipe(
|
|
192
|
+
Effect.flatMap((service) =>
|
|
193
|
+
service.finish(
|
|
194
|
+
{
|
|
195
|
+
taskId: "single",
|
|
196
|
+
sessionId: "session-1",
|
|
197
|
+
revision: acquired.revision,
|
|
198
|
+
outcome: "done",
|
|
199
|
+
nonPrCompletion: { summary: " " },
|
|
200
|
+
},
|
|
201
|
+
root,
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
).rejects.toThrow("summary must not be empty")
|
|
207
|
+
})
|
|
208
|
+
|
|
150
209
|
test("returns structured ownership and revision conflicts", async () => {
|
|
151
210
|
const initial = await inspect()
|
|
152
211
|
const acquired = await claim(initial.revision)
|
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
type PhaseFrontmatter as PhaseData,
|
|
32
32
|
type TaskFrontmatter as TaskData,
|
|
33
33
|
} from "../workbase/schemas"
|
|
34
|
+
import {
|
|
35
|
+
buildNonPrCompletion,
|
|
36
|
+
type NonPrCompletionInput,
|
|
37
|
+
} from "../workbase/completion"
|
|
34
38
|
|
|
35
39
|
class ClaimError extends Data.TaggedError("ClaimError")<{
|
|
36
40
|
readonly message: string
|
|
@@ -83,6 +87,7 @@ interface OwnedClaimInput {
|
|
|
83
87
|
|
|
84
88
|
interface FinishInput extends OwnedClaimInput {
|
|
85
89
|
readonly outcome: "done" | "dropped"
|
|
90
|
+
readonly nonPrCompletion?: NonPrCompletionInput
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
interface ExpireClaimInput {
|
|
@@ -507,6 +512,11 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
507
512
|
finish: (input: FinishInput, startPath: string = process.cwd()) =>
|
|
508
513
|
Effect.gen(function* () {
|
|
509
514
|
assertIdentity("Session ID", input.sessionId)
|
|
515
|
+
if (input.nonPrCompletion && input.outcome !== "done") {
|
|
516
|
+
return yield* new ClaimError({
|
|
517
|
+
message: "Non-PR completion is valid only with a done outcome",
|
|
518
|
+
})
|
|
519
|
+
}
|
|
510
520
|
const service = yield* ClaimService
|
|
511
521
|
const inspected = yield* service.inspect(
|
|
512
522
|
input.taskId,
|
|
@@ -531,6 +541,22 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
531
541
|
message: `Session '${input.sessionId}' does not own ${inspected.target.label}`,
|
|
532
542
|
})
|
|
533
543
|
}
|
|
544
|
+
if (input.nonPrCompletion && data.pr !== null) {
|
|
545
|
+
throw new ClaimError({
|
|
546
|
+
target: inspected.target.label,
|
|
547
|
+
message:
|
|
548
|
+
"Cannot complete without a pull request while an authoritative pull request is recorded",
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
const completionResult = input.nonPrCompletion
|
|
552
|
+
? buildNonPrCompletion(input.nonPrCompletion, now)
|
|
553
|
+
: undefined
|
|
554
|
+
if (completionResult && "error" in completionResult) {
|
|
555
|
+
throw new ClaimError({
|
|
556
|
+
target: inspected.target.label,
|
|
557
|
+
message: completionResult.error,
|
|
558
|
+
})
|
|
559
|
+
}
|
|
534
560
|
const claim: ClaimRecord = {
|
|
535
561
|
...data.claim,
|
|
536
562
|
state: "finished",
|
|
@@ -540,8 +566,15 @@ export class ClaimService extends Effect.Service<ClaimService>()(
|
|
|
540
566
|
return {
|
|
541
567
|
data: {
|
|
542
568
|
...data,
|
|
543
|
-
status:
|
|
569
|
+
status: completionResult
|
|
570
|
+
? "done"
|
|
571
|
+
: input.outcome === "done"
|
|
572
|
+
? "working"
|
|
573
|
+
: "dropped",
|
|
544
574
|
claim,
|
|
575
|
+
...(completionResult
|
|
576
|
+
? { completion: completionResult.value }
|
|
577
|
+
: {}),
|
|
545
578
|
},
|
|
546
579
|
claim,
|
|
547
580
|
}
|