@markjaquith/agency 3.2.2 → 3.2.4

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.
Files changed (48) hide show
  1. package/README.md +30 -62
  2. package/cli-main.ts +20 -83
  3. package/fixtures/protocol/orchestration-recipes.json +9 -34
  4. package/package.json +1 -4
  5. package/schemas/agency-graph-v1.schema.json +2 -31
  6. package/src/cli-parser.test.ts +34 -132
  7. package/src/cli-parser.ts +45 -102
  8. package/src/cli.test.ts +60 -117
  9. package/src/commands/act.ts +2 -6
  10. package/src/commands/sync.ts +3 -3
  11. package/src/commands/validate.ts +3 -1
  12. package/src/commands/work.test.ts +70 -8
  13. package/src/commands/work.ts +15 -8
  14. package/src/graph-schema.ts +0 -2
  15. package/src/protocol.test.ts +0 -25
  16. package/src/protocol.ts +0 -11
  17. package/src/readiness.test.ts +2 -2
  18. package/src/services/ArchiveBulkService.test.ts +0 -88
  19. package/src/services/ArchiveService.ts +0 -32
  20. package/src/services/GraphMutationService.ts +0 -26
  21. package/src/services/GraphService.test.ts +1 -1
  22. package/src/services/IntegrationService.test.ts +7 -4
  23. package/src/services/LifecycleTransaction.ts +5 -1
  24. package/src/services/PhaseService.ts +1 -8
  25. package/src/services/ReadinessService.test.ts +0 -34
  26. package/src/services/ReadinessService.ts +1 -20
  27. package/src/services/ReviewService.test.ts +0 -64
  28. package/src/services/ReviewService.ts +0 -5
  29. package/src/services/SyncService.test.ts +75 -88
  30. package/src/services/SyncService.ts +52 -123
  31. package/src/services/TaskPhaseService.test.ts +13 -1
  32. package/src/services/TaskService.ts +1 -7
  33. package/src/services/WorkbaseService.ts +0 -3
  34. package/src/services/WorktreeService.test.ts +192 -1
  35. package/src/services/WorktreeService.ts +169 -34
  36. package/src/test-utils.ts +0 -2
  37. package/src/usage-log.test.ts +89 -5
  38. package/src/usage-log.ts +139 -31
  39. package/src/workbase/AGENTS.md +9 -15
  40. package/src/workbase/agent-command.test.ts +1 -3
  41. package/src/workbase/agent-command.ts +1 -6
  42. package/src/workbase/document-revision.ts +0 -4
  43. package/src/workbase/opencode-plugin-file.ts +1 -0
  44. package/src/workbase/schemas.test.ts +12 -25
  45. package/src/workbase/schemas.ts +0 -16
  46. package/src/commands/claim.ts +0 -122
  47. package/src/services/ClaimService.test.ts +0 -415
  48. package/src/services/ClaimService.ts +0 -608
@@ -7,6 +7,25 @@ const expectUsageError = (args: string[], usage: string) => {
7
7
  }
8
8
 
9
9
  describe("strict CLI parsing", () => {
10
+ test("returns canonical paths without positional values", () => {
11
+ expect(parseCli(["task", "show", "private-task-id"]).commandPath).toBe(
12
+ "task/show",
13
+ )
14
+ expect(parseCli(["validate", "/private/customer/path"]).commandPath).toBe(
15
+ "validate",
16
+ )
17
+ expect(parseCli(["work", "prepare", "private-task-id"]).commandPath).toBe(
18
+ "work/prepare",
19
+ )
20
+ expect(parseCli(["pr", "view", "private-task-id"]).commandPath).toBe("pr")
21
+ try {
22
+ parseCli(["task", "create", "private-task-id"])
23
+ expect.unreachable()
24
+ } catch (error) {
25
+ expect(error).toMatchObject({ commandPath: "task/create" })
26
+ }
27
+ })
28
+
10
29
  test("parses act selectors, dry-run, and JSON options", () => {
11
30
  expect(
12
31
  parseCli([
@@ -338,51 +357,6 @@ describe("strict CLI parsing", () => {
338
357
  [["graph", "extra"], "agency graph"],
339
358
  [["next", "extra"], "agency next"],
340
359
  [["sync", "one", "two", "three"], "agency sync"],
341
- [
342
- [
343
- "claim",
344
- "one",
345
- "two",
346
- "three",
347
- "--claimant",
348
- "a",
349
- "--agent",
350
- "r",
351
- "--session-id",
352
- "s",
353
- "--revision",
354
- "0".repeat(64),
355
- ],
356
- "agency claim",
357
- ],
358
- [
359
- [
360
- "release",
361
- "one",
362
- "two",
363
- "three",
364
- "--session-id",
365
- "s",
366
- "--revision",
367
- "0".repeat(64),
368
- ],
369
- "agency release",
370
- ],
371
- [
372
- [
373
- "finish",
374
- "one",
375
- "two",
376
- "three",
377
- "--session-id",
378
- "s",
379
- "--revision",
380
- "0".repeat(64),
381
- "--outcome",
382
- "done",
383
- ],
384
- "agency finish",
385
- ],
386
360
  ] as const) {
387
361
  expectUsageError([...args], usage)
388
362
  }
@@ -408,6 +382,7 @@ describe("strict CLI parsing", () => {
408
382
  const args = ["create", "--title", "two words", "--", "--literal"]
409
383
  expect(parseCli(["--cwd", "/workbase", "pr", ...args])).toEqual({
410
384
  commandName: "pr",
385
+ commandPath: "pr",
411
386
  args,
412
387
  passthrough: true,
413
388
  values: { cwd: "/workbase" },
@@ -427,6 +402,7 @@ describe("strict CLI parsing", () => {
427
402
  ]),
428
403
  ).toEqual({
429
404
  commandName: "pr",
405
+ commandPath: "pr/create",
430
406
  args: ["create", "ship", "release"],
431
407
  values: { draft: true, force: true, json: true },
432
408
  })
@@ -500,76 +476,10 @@ describe("strict CLI parsing", () => {
500
476
  )
501
477
  })
502
478
 
503
- test("validates revision-guarded claim operations", () => {
504
- const revision = "0".repeat(64)
505
- expect(
506
- parseCli([
507
- "claim",
508
- "task",
509
- "phase",
510
- "--claimant",
511
- "orchestrator",
512
- "--agent",
513
- "agent",
514
- "--session-id",
515
- "job-1",
516
- "--revision",
517
- revision,
518
- "--expires-at",
519
- "2026-07-17T13:00:00.000Z",
520
- ]),
521
- ).toMatchObject({ commandName: "claim", args: ["task", "phase"] })
522
- expect(() =>
523
- parseCli(["release", "task", "--session-id", "job-1"]),
524
- ).toThrow("--revision' is required")
525
- expect(() =>
526
- parseCli([
527
- "finish",
528
- "task",
529
- "--session-id",
530
- "job-1",
531
- "--revision",
532
- revision,
533
- "--outcome",
534
- "working",
535
- ]),
536
- ).toThrow("must be 'done' or 'dropped'")
537
- expect(
538
- parseCli([
539
- "finish",
540
- "task",
541
- "--session-id",
542
- "job-1",
543
- "--revision",
544
- revision,
545
- "--outcome",
546
- "done",
547
- "--no-pull-request",
548
- "--summary",
549
- "Investigation completed",
550
- "--evidence-url",
551
- "https://example.com/result",
552
- ]),
553
- ).toMatchObject({
554
- values: {
555
- "no-pull-request": true,
556
- summary: "Investigation completed",
557
- "evidence-url": "https://example.com/result",
558
- },
559
- })
560
- expect(() =>
561
- parseCli([
562
- "finish",
563
- "task",
564
- "--session-id",
565
- "job-1",
566
- "--revision",
567
- revision,
568
- "--outcome",
569
- "done",
570
- "--no-pull-request",
571
- ]),
572
- ).toThrow("--summary' is required")
479
+ test("rejects removed claim protocol commands", () => {
480
+ for (const command of ["claim", "release", "finish"]) {
481
+ expect(() => parseCli([command])).toThrow(`Unknown command '${command}'`)
482
+ }
573
483
  expect(() =>
574
484
  parseCli([
575
485
  "task",
@@ -649,6 +559,15 @@ describe("strict CLI parsing", () => {
649
559
  expect(() => parseCli(["work", "prepare", "--opencode"])).toThrow(
650
560
  "cannot be combined",
651
561
  )
562
+ expect(() => parseCli(["work", "prepare", "first", "second"])).toThrow(
563
+ "Expected 0-2 positional arguments",
564
+ )
565
+ expect(() =>
566
+ parseCli(["work", "prepare", "example", "--task", "other"]),
567
+ ).toThrow("Entity selector options cannot be combined")
568
+ expect(() => parseCli(["work", "prepare", "--phase", "build"])).toThrow(
569
+ "requires '--task'",
570
+ )
652
571
  })
653
572
 
654
573
  test("parses worktree lifecycle commands and selectors", () => {
@@ -977,23 +896,6 @@ describe("strict CLI parsing", () => {
977
896
  commandName: "phase",
978
897
  args: ["status", "ship", "release", "done"],
979
898
  })
980
- expect(
981
- parseCli([
982
- "claim",
983
- "--task",
984
- "ship",
985
- "--phase",
986
- "release",
987
- "--claimant",
988
- "agent",
989
- "--agent",
990
- "opencode",
991
- "--session-id",
992
- "session",
993
- "--revision",
994
- "0".repeat(64),
995
- ]),
996
- ).toMatchObject({ args: ["ship", "release"] })
997
899
  expect(parseCli(["context", "--epic", "delivery"]).values.epic).toBe(
998
900
  "delivery",
999
901
  )
package/src/cli-parser.ts CHANGED
@@ -114,21 +114,6 @@ const mutationOptions = {
114
114
  "no-epic": { type: "boolean" },
115
115
  } satisfies OptionConfig
116
116
 
117
- const claimOptions = {
118
- ...outputOptions,
119
- claimant: { type: "string" },
120
- agent: { type: "string" },
121
- "session-id": { type: "string" },
122
- revision: { type: "string" },
123
- "expires-at": { type: "string" },
124
- } satisfies OptionConfig
125
-
126
- const ownedClaimOptions = {
127
- ...outputOptions,
128
- "session-id": { type: "string" },
129
- revision: { type: "string" },
130
- } satisfies OptionConfig
131
-
132
117
  const nonPrCompletionOptions = {
133
118
  "no-pull-request": { type: "boolean" },
134
119
  summary: { type: "string" },
@@ -679,75 +664,6 @@ const commands = {
679
664
  },
680
665
  },
681
666
  },
682
- claim: {
683
- usage: "agency claim <task-id> [phase-id] [options]",
684
- options: {
685
- ...claimOptions,
686
- task: { type: "string" },
687
- phase: { type: "string" },
688
- },
689
- command: {
690
- usage:
691
- "agency claim <task-id> [phase-id] --claimant <id> --agent <id> --session-id <id> --revision <sha256> [--expires-at <timestamp>] [--json]",
692
- minArgs: 1,
693
- maxArgs: 2,
694
- options: [
695
- "claimant",
696
- "agent",
697
- "session-id",
698
- "revision",
699
- "expires-at",
700
- "json",
701
- "task",
702
- "phase",
703
- ],
704
- required: ["claimant", "agent", "session-id", "revision"],
705
- },
706
- },
707
- release: {
708
- usage: "agency release <task-id> [phase-id] [options]",
709
- options: {
710
- ...ownedClaimOptions,
711
- task: { type: "string" },
712
- phase: { type: "string" },
713
- },
714
- command: {
715
- usage:
716
- "agency release <task-id> [phase-id] --session-id <id> --revision <sha256> [--json]",
717
- minArgs: 1,
718
- maxArgs: 2,
719
- options: ["session-id", "revision", "json", "task", "phase"],
720
- required: ["session-id", "revision"],
721
- },
722
- },
723
- finish: {
724
- usage: "agency finish <task-id> [phase-id] [options]",
725
- options: {
726
- ...ownedClaimOptions,
727
- ...nonPrCompletionOptions,
728
- outcome: { type: "string" },
729
- task: { type: "string" },
730
- phase: { type: "string" },
731
- },
732
- command: {
733
- usage:
734
- "agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [options] [--json]",
735
- minArgs: 1,
736
- maxArgs: 2,
737
- options: [
738
- "session-id",
739
- "revision",
740
- "outcome",
741
- "no-pull-request",
742
- "summary",
743
- "evidence-url",
744
- "json",
745
- "task",
746
- "phase",
747
- ],
748
- required: ["session-id", "revision", "outcome"],
749
- },
750
- },
751
667
  sync: {
752
668
  usage: "agency sync [<task-id> [phase-id]] [--dry-run] [--json]",
753
669
  options: {
@@ -1116,6 +1032,7 @@ const preCommandValueOptions = new Set(["--workbase", "--cwd"])
1116
1032
 
1117
1033
  export interface ParsedCli {
1118
1034
  readonly commandName?: keyof typeof commands
1035
+ readonly commandPath: string
1119
1036
  readonly args: string[]
1120
1037
  readonly passthrough?: boolean
1121
1038
  readonly values: Record<
@@ -1124,8 +1041,14 @@ export interface ParsedCli {
1124
1041
  >
1125
1042
  }
1126
1043
 
1044
+ const canonicalCommandPath = (
1045
+ commandName: string,
1046
+ subcommand: string | undefined,
1047
+ ) => (subcommand ? `${commandName}/${subcommand}` : commandName)
1048
+
1127
1049
  class CliUsageError extends Error {
1128
1050
  readonly _tag = "CliUsageError"
1051
+ readonly commandPath: string
1129
1052
 
1130
1053
  constructor(
1131
1054
  readonly detail: string,
@@ -1133,6 +1056,15 @@ class CliUsageError extends Error {
1133
1056
  ) {
1134
1057
  super(`${detail}\n\nUsage: ${usage}`)
1135
1058
  this.name = "CliUsageError"
1059
+ const [, commandName, subcommand] = usage.split(/\s+/)
1060
+ const definition: CommandDefinition | undefined =
1061
+ commands[commandName as keyof typeof commands]
1062
+ this.commandPath = !definition
1063
+ ? "invalid"
1064
+ : subcommand && definition.subcommands?.[subcommand]
1065
+ ? `${commandName}/${subcommand}`
1066
+ : commandName!
1067
+ Object.defineProperty(this, "commandPath", { enumerable: false })
1136
1068
  }
1137
1069
  }
1138
1070
 
@@ -1177,8 +1109,6 @@ const targetSlots = (
1177
1109
  return ["task"]
1178
1110
  if (commandName === "phase")
1179
1111
  return subcommand === "list" ? ["task"] : ["task", "phase"]
1180
- if (["claim", "release", "finish"].includes(commandName))
1181
- return ["task", "phase"]
1182
1112
  if (["archive", "restore"].includes(commandName))
1183
1113
  return subcommand === "epic"
1184
1114
  ? ["epic"]
@@ -1409,7 +1339,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
1409
1339
  "agency <command> [options]",
1410
1340
  )
1411
1341
  }
1412
- return { args: [], values: parsed.values }
1342
+ return { commandPath: "root", args: [], values: parsed.values }
1413
1343
  }
1414
1344
 
1415
1345
  const commandName = args[commandIndex]!
@@ -1454,6 +1384,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
1454
1384
  }
1455
1385
  return {
1456
1386
  commandName,
1387
+ commandPath: "pr",
1457
1388
  args: prArgs,
1458
1389
  passthrough: true,
1459
1390
  values: parsed.values,
@@ -1477,6 +1408,7 @@ export function parseCli(args: readonly string[]): ParsedCli {
1477
1408
  assertNoDuplicateOptions(parsed.tokens, new Set(), definition.usage)
1478
1409
  return {
1479
1410
  commandName: commandName as keyof typeof commands,
1411
+ commandPath: commandName,
1480
1412
  args: parsed.positionals,
1481
1413
  values: parsed.values,
1482
1414
  }
@@ -1527,6 +1459,10 @@ export function parseCli(args: readonly string[]): ParsedCli {
1527
1459
  if (parsed.values.version) {
1528
1460
  return {
1529
1461
  commandName: commandName as keyof typeof commands,
1462
+ commandPath: canonicalCommandPath(
1463
+ commandName,
1464
+ selectedSubcommand ? subcommand : undefined,
1465
+ ),
1530
1466
  args: parsed.positionals,
1531
1467
  values: parsed.values,
1532
1468
  }
@@ -1534,6 +1470,10 @@ export function parseCli(args: readonly string[]): ParsedCli {
1534
1470
  if (parsed.values.help) {
1535
1471
  return {
1536
1472
  commandName: commandName as keyof typeof commands,
1473
+ commandPath: canonicalCommandPath(
1474
+ commandName,
1475
+ selectedSubcommand ? subcommand : undefined,
1476
+ ),
1537
1477
  args: parsed.positionals,
1538
1478
  values: parsed.values,
1539
1479
  }
@@ -1637,16 +1577,6 @@ export function parseCli(args: readonly string[]): ParsedCli {
1637
1577
  ) {
1638
1578
  validateViewOptions(parsed.values, spec)
1639
1579
  }
1640
- if (
1641
- commandName === "finish" &&
1642
- parsed.values.outcome !== "done" &&
1643
- parsed.values.outcome !== "dropped"
1644
- ) {
1645
- throw usageError(
1646
- "Option '--outcome' must be 'done' or 'dropped'.",
1647
- spec.usage,
1648
- )
1649
- }
1650
1580
  const nonPrCompletion = parsed.values["no-pull-request"] === true
1651
1581
  const completionSummary = parsed.values.summary
1652
1582
  const completionEvidenceUrl = parsed.values["evidence-url"]
@@ -1670,19 +1600,24 @@ export function parseCli(args: readonly string[]): ParsedCli {
1670
1600
  )
1671
1601
  }
1672
1602
  const completesDone =
1673
- (commandName === "finish" && parsed.values.outcome === "done") ||
1674
- (["task", "phase"].includes(commandName) &&
1675
- subcommand === "status" &&
1676
- commandPositionals.at(-1) === "done")
1603
+ ["task", "phase"].includes(commandName) &&
1604
+ subcommand === "status" &&
1605
+ commandPositionals.at(-1) === "done"
1677
1606
  if (!completesDone) {
1678
1607
  throw usageError(
1679
- "Option '--no-pull-request' is valid only with a done status or outcome.",
1608
+ "Option '--no-pull-request' is valid only with a done status.",
1680
1609
  spec.usage,
1681
1610
  )
1682
1611
  }
1683
1612
  }
1684
1613
  if (commandName === "work") {
1685
1614
  const preparing = commandPositionals[0] === "prepare"
1615
+ if (preparing && parsed.values.phase && !parsed.values.task) {
1616
+ throw usageError(
1617
+ "Option '--phase' requires '--task' with work preparation.",
1618
+ spec.usage,
1619
+ )
1620
+ }
1686
1621
  if (
1687
1622
  (!preparing && commandPositionals.length > 1) ||
1688
1623
  (preparing &&
@@ -1713,6 +1648,14 @@ export function parseCli(args: readonly string[]): ParsedCli {
1713
1648
 
1714
1649
  return {
1715
1650
  commandName: commandName as keyof typeof commands,
1651
+ commandPath: canonicalCommandPath(
1652
+ commandName,
1653
+ selectedSubcommand
1654
+ ? subcommand
1655
+ : commandName === "work" && commandPositionals[0] === "prepare"
1656
+ ? "prepare"
1657
+ : undefined,
1658
+ ),
1716
1659
  args: selectedSubcommand
1717
1660
  ? [subcommand!, ...commandPositionals]
1718
1661
  : commandPositionals,
package/src/cli.test.ts CHANGED
@@ -179,10 +179,31 @@ describe("CLI", () => {
179
179
  XDG_STATE_HOME: state,
180
180
  AGENCY_SESSION_ID: "cli-session",
181
181
  AGENCY_NO_USAGE_LOG: "0",
182
+ AGENCY_INVOCATION_SOURCE: "automation",
183
+ AGENCY_USAGE_TEST: "1",
182
184
  }
183
185
  expect((await runCli(["--version"], projectRoot, env)).exitCode).toBe(0)
184
186
  expect(
185
- (await runCli(["unknown", "--cwd", "/private/value"], projectRoot, env))
187
+ (
188
+ await runCli(
189
+ [
190
+ "task",
191
+ "create",
192
+ "private-customer-id",
193
+ "--repo",
194
+ "private-repository",
195
+ "--description",
196
+ "private free-form input",
197
+ "--cwd",
198
+ "/private/customer/path",
199
+ ],
200
+ projectRoot,
201
+ env,
202
+ )
203
+ ).exitCode,
204
+ ).toBe(1)
205
+ expect(
206
+ (await runCli(["unknown", "--private-flag=value"], projectRoot, env))
186
207
  .exitCode,
187
208
  ).toBe(1)
188
209
 
@@ -194,147 +215,60 @@ describe("CLI", () => {
194
215
  .map((line) => JSON.parse(line))
195
216
  expect(events).toEqual([
196
217
  expect.objectContaining({
197
- sessionId: "cli-session",
198
- sessionSequence: 1,
218
+ journeyId: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
219
+ journeySequence: 1,
220
+ invocationSource: "automation",
221
+ isTest: true,
199
222
  commandPath: "version",
200
223
  flagNames: ["version"],
201
224
  outcome: "success",
225
+ outcomeCode: "SUCCESS",
202
226
  }),
203
227
  expect.objectContaining({
204
- sessionSequence: 2,
228
+ journeySequence: 2,
229
+ commandPath: "task/create",
230
+ flagNames: ["cwd", "description", "repo"],
231
+ outcome: "failure",
232
+ outcomeCode: "WORKBASE_NOT_FOUND",
233
+ }),
234
+ expect.objectContaining({
235
+ journeySequence: 3,
205
236
  commandPath: "invalid",
206
- flagNames: ["cwd"],
237
+ flagNames: [],
207
238
  outcome: "failure",
239
+ outcomeCode: "CLI_USAGE",
208
240
  }),
209
241
  ])
210
- expect(exported.stdout).not.toContain("/private/value")
242
+ for (const value of [
243
+ "cli-session",
244
+ "private-customer-id",
245
+ "private-repository",
246
+ "private free-form input",
247
+ "/private/customer/path",
248
+ "private-flag",
249
+ "value",
250
+ ]) {
251
+ expect(exported.stdout).not.toContain(value)
252
+ }
211
253
  })
212
254
 
213
- test("coordinates claims through revision-guarded machine commands", async () => {
255
+ test("records status-based non-PR completion", async () => {
214
256
  const root = await createTempDir()
215
257
  tempDirs.push(root)
216
258
  await Bun.write(join(root, "agency.json"), '{"version":2}\n')
217
259
  await mkdir(join(root, "repos", "agency"), { recursive: true })
218
- parseJson(
219
- await runCli(
220
- ["task", "create", "claimed", "--repo", "agency", "--json"],
221
- root,
222
- ),
223
- )
224
- const context = parseJson(
225
- await runCli(["context", "tasks/claimed", "--json"], root),
226
- )
227
- const revision = context.documents.task.sha256
228
- const claimed = parseJson(
229
- await runCli(
230
- [
231
- "claim",
232
- "claimed",
233
- "--claimant",
234
- "orchestrator",
235
- "--agent",
236
- "agent",
237
- "--session-id",
238
- "job-1",
239
- "--revision",
240
- revision,
241
- "--json",
242
- ],
243
- root,
244
- ),
245
- )
246
- expect(claimed.claim).toMatchObject({
247
- claimant: "orchestrator",
248
- agent: "agent",
249
- sessionId: "job-1",
250
- state: "active",
251
- })
252
-
253
- const conflict = await runCli(
254
- [
255
- "claim",
256
- "claimed",
257
- "--claimant",
258
- "other",
259
- "--agent",
260
- "other-agent",
261
- "--session-id",
262
- "job-2",
263
- "--revision",
264
- claimed.revision,
265
- "--json",
266
- ],
267
- root,
268
- )
269
- expect(conflict.exitCode).toBe(1)
270
- expect(JSON.parse(conflict.stdout)).toMatchObject({
271
- ok: false,
272
- error: {
273
- code: "CLAIM_CONFLICT",
274
- retryable: true,
275
- fields: { claim: { agent: "agent", sessionId: "job-1" } },
276
- },
277
- })
278
-
279
- const finished = parseJson(
280
- await runCli(
281
- [
282
- "finish",
283
- "claimed",
284
- "--session-id",
285
- "job-1",
286
- "--revision",
287
- claimed.revision,
288
- "--outcome",
289
- "done",
290
- "--json",
291
- ],
292
- root,
293
- ),
294
- )
295
- expect(finished.claim).toMatchObject({ state: "finished", outcome: "done" })
296
- expect(
297
- parseJson(await runCli(["task", "show", "claimed", "--json"], root)).data
298
- .status,
299
- ).toBe("working")
300
-
301
260
  parseJson(
302
261
  await runCli(
303
262
  ["task", "create", "non-pr", "--repo", "agency", "--json"],
304
263
  root,
305
264
  ),
306
265
  )
307
- const nonPrContext = parseJson(
308
- await runCli(["context", "tasks/non-pr", "--json"], root),
309
- )
310
- const nonPrClaim = parseJson(
311
- await runCli(
312
- [
313
- "claim",
314
- "non-pr",
315
- "--claimant",
316
- "orchestrator",
317
- "--agent",
318
- "agent",
319
- "--session-id",
320
- "job-2",
321
- "--revision",
322
- nonPrContext.documents.task.sha256,
323
- "--json",
324
- ],
325
- root,
326
- ),
327
- )
328
266
  parseJson(
329
267
  await runCli(
330
268
  [
331
- "finish",
269
+ "task",
270
+ "status",
332
271
  "non-pr",
333
- "--session-id",
334
- "job-2",
335
- "--revision",
336
- nonPrClaim.revision,
337
- "--outcome",
338
272
  "done",
339
273
  "--no-pull-request",
340
274
  "--summary",
@@ -1073,6 +1007,15 @@ status: open
1073
1007
  ],
1074
1008
  }),
1075
1009
  ])
1010
+ const preparedAgain = parseJson(
1011
+ await runCli(
1012
+ ["work", "prepare", join(root, "tasks/example/TASK.md"), "--json"],
1013
+ parent,
1014
+ ),
1015
+ )
1016
+ expect(preparedAgain.checkouts).toEqual([
1017
+ expect.objectContaining({ action: "reused", kind: "writable" }),
1018
+ ])
1076
1019
  const rebuild = parseJson(
1077
1020
  await runCli(
1078
1021
  ["worktree", "rebuild", "example", "--dry-run", "--json"],