@markjaquith/agency 2.48.1 → 2.50.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.
Files changed (44) hide show
  1. package/README.md +40 -9
  2. package/cli.ts +32 -0
  3. package/package.json +1 -1
  4. package/schemas/agency-graph-v1.schema.json +141 -38
  5. package/src/cli-parser.test.ts +120 -0
  6. package/src/cli-parser.ts +122 -7
  7. package/src/cli.test.ts +71 -0
  8. package/src/commands/claim.ts +26 -2
  9. package/src/commands/phase.ts +23 -2
  10. package/src/commands/review.ts +38 -0
  11. package/src/commands/task.ts +49 -7
  12. package/src/commands/work.test.ts +2 -0
  13. package/src/commands/work.ts +2 -2
  14. package/src/graph-schema.ts +35 -13
  15. package/src/protocol.ts +1 -0
  16. package/src/services/ArchiveService.test.ts +2 -2
  17. package/src/services/ArchiveService.ts +1 -0
  18. package/src/services/ClaimService.test.ts +59 -0
  19. package/src/services/ClaimService.ts +36 -2
  20. package/src/services/ContextService.ts +56 -4
  21. package/src/services/DoctorService.ts +45 -1
  22. package/src/services/GraphMutationService.ts +21 -0
  23. package/src/services/GraphService.ts +118 -54
  24. package/src/services/IntegrationService.test.ts +4 -1
  25. package/src/services/PhaseService.ts +55 -3
  26. package/src/services/PullRequestService.test.ts +22 -2
  27. package/src/services/PullRequestService.ts +44 -3
  28. package/src/services/ReadinessService.ts +7 -3
  29. package/src/services/ReviewService.test.ts +472 -0
  30. package/src/services/ReviewService.ts +404 -0
  31. package/src/services/SyncService.test.ts +68 -2
  32. package/src/services/SyncService.ts +123 -6
  33. package/src/services/TaskPhaseService.test.ts +182 -0
  34. package/src/services/TaskService.ts +128 -11
  35. package/src/services/WorkbaseService.test.ts +58 -0
  36. package/src/services/WorkbaseService.ts +22 -1
  37. package/src/services/WorktreeService.test.ts +16 -16
  38. package/src/services/WorktreeService.ts +76 -40
  39. package/src/test-utils.ts +2 -0
  40. package/src/work-view.ts +14 -6
  41. package/src/workbase/AGENTS.md +7 -2
  42. package/src/workbase/completion.ts +28 -0
  43. package/src/workbase/schemas.test.ts +57 -0
  44. package/src/workbase/schemas.ts +65 -0
package/src/cli-parser.ts CHANGED
@@ -82,6 +82,9 @@ const taskCreateOptions = {
82
82
  branch: { type: "string" },
83
83
  base: { type: "string" },
84
84
  "multi-phase": { type: "boolean" },
85
+ review: { type: "string" },
86
+ "pull-request": { type: "string" },
87
+ ref: { type: "string" },
85
88
  } satisfies OptionConfig
86
89
 
87
90
  const phaseCreateOptions = {
@@ -120,6 +123,12 @@ const ownedClaimOptions = {
120
123
  revision: { type: "string" },
121
124
  } satisfies OptionConfig
122
125
 
126
+ const nonPrCompletionOptions = {
127
+ "no-pull-request": { type: "boolean" },
128
+ summary: { type: "string" },
129
+ "evidence-url": { type: "string" },
130
+ } satisfies OptionConfig
131
+
123
132
  const commands = {
124
133
  init: {
125
134
  usage: "agency init [path] [--json]",
@@ -360,6 +369,7 @@ const commands = {
360
369
  ...newWorkOptions,
361
370
  ...viewOptions,
362
371
  ...mutationOptions,
372
+ ...nonPrCompletionOptions,
363
373
  "pr-url": { type: "string" },
364
374
  task: { type: "string" },
365
375
  },
@@ -386,7 +396,7 @@ const commands = {
386
396
  },
387
397
  create: {
388
398
  usage:
389
- "agency task create <id> (--repo <alias> | --multi-phase) [options]",
399
+ "agency task create <id> (--repo <alias> | --multi-phase | --review <alias> (--pull-request <value> | --ref <remote-ref>)) [options]",
390
400
  minArgs: 1,
391
401
  maxArgs: 1,
392
402
  options: [
@@ -398,6 +408,9 @@ const commands = {
398
408
  "branch",
399
409
  "base",
400
410
  "multi-phase",
411
+ "review",
412
+ "pull-request",
413
+ "ref",
401
414
  "json",
402
415
  ],
403
416
  repeatable: ["reference"],
@@ -417,10 +430,10 @@ const commands = {
417
430
  options: ["json", "task"],
418
431
  },
419
432
  status: {
420
- usage: "agency task status <id> <status> [--json]",
433
+ usage: "agency task status <id> <status> [options] [--json]",
421
434
  minArgs: 2,
422
435
  maxArgs: 2,
423
- options: ["json", "task"],
436
+ options: ["json", "task", "no-pull-request", "summary", "evidence-url"],
424
437
  },
425
438
  update: {
426
439
  usage: "agency task update <id> [options] [--json]",
@@ -479,6 +492,7 @@ const commands = {
479
492
  ...newWorkOptions,
480
493
  ...viewOptions,
481
494
  ...mutationOptions,
495
+ ...nonPrCompletionOptions,
482
496
  "pr-url": { type: "string" },
483
497
  task: { type: "string" },
484
498
  phase: { type: "string" },
@@ -542,10 +556,18 @@ const commands = {
542
556
  options: ["json", "task", "phase"],
543
557
  },
544
558
  status: {
545
- usage: "agency phase status <task-id> <phase-id> <status> [--json]",
559
+ usage:
560
+ "agency phase status <task-id> <phase-id> <status> [options] [--json]",
546
561
  minArgs: 3,
547
562
  maxArgs: 3,
548
- options: ["json", "task", "phase"],
563
+ options: [
564
+ "json",
565
+ "task",
566
+ "phase",
567
+ "no-pull-request",
568
+ "summary",
569
+ "evidence-url",
570
+ ],
549
571
  },
550
572
  update: {
551
573
  usage: "agency phase update <task-id> <phase-id> [options] [--json]",
@@ -631,16 +653,27 @@ const commands = {
631
653
  usage: "agency finish <task-id> [phase-id] [options]",
632
654
  options: {
633
655
  ...ownedClaimOptions,
656
+ ...nonPrCompletionOptions,
634
657
  outcome: { type: "string" },
635
658
  task: { type: "string" },
636
659
  phase: { type: "string" },
637
660
  },
638
661
  command: {
639
662
  usage:
640
- "agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [--json]",
663
+ "agency finish <task-id> [phase-id] --session-id <id> --revision <sha256> --outcome <done|dropped> [options] [--json]",
641
664
  minArgs: 1,
642
665
  maxArgs: 2,
643
- options: ["session-id", "revision", "outcome", "json", "task", "phase"],
666
+ options: [
667
+ "session-id",
668
+ "revision",
669
+ "outcome",
670
+ "no-pull-request",
671
+ "summary",
672
+ "evidence-url",
673
+ "json",
674
+ "task",
675
+ "phase",
676
+ ],
644
677
  required: ["session-id", "revision", "outcome"],
645
678
  },
646
679
  },
@@ -733,6 +766,22 @@ const commands = {
733
766
  },
734
767
  },
735
768
  },
769
+ review: {
770
+ usage: "agency review refresh <task-id> [--if-revision <hash>] [--json]",
771
+ options: {
772
+ ...outputOptions,
773
+ "if-revision": { type: "string" },
774
+ },
775
+ subcommands: {
776
+ refresh: {
777
+ usage:
778
+ "agency review refresh <task-id> [--if-revision <hash>] [--json]",
779
+ minArgs: 1,
780
+ maxArgs: 1,
781
+ options: ["if-revision", "json"],
782
+ },
783
+ },
784
+ },
736
785
  worktree: {
737
786
  usage: "agency worktree <list|inspect|prepare|remove|rebuild|repair>",
738
787
  options: {
@@ -1134,6 +1183,38 @@ function validateTaskCreate(
1134
1183
  spec: LeafCommand,
1135
1184
  requireRepo: boolean,
1136
1185
  ) {
1186
+ const review = values.review !== undefined
1187
+ const pullRequest = values["pull-request"] !== undefined
1188
+ const ref = values.ref !== undefined
1189
+ if (review) {
1190
+ if (pullRequest === ref) {
1191
+ throw usageError(
1192
+ "Option '--review' requires exactly one of '--pull-request' or '--ref'.",
1193
+ spec.usage,
1194
+ )
1195
+ }
1196
+ for (const option of [
1197
+ "repo",
1198
+ "reference",
1199
+ "branch",
1200
+ "base",
1201
+ "multi-phase",
1202
+ ] as const) {
1203
+ if (values[option] !== undefined) {
1204
+ throw usageError(
1205
+ `Option '--review' cannot be combined with '${optionLabel(option)}'.`,
1206
+ spec.usage,
1207
+ )
1208
+ }
1209
+ }
1210
+ return
1211
+ }
1212
+ if (pullRequest || ref) {
1213
+ throw usageError(
1214
+ `Option '${pullRequest ? "--pull-request" : "--ref"}' requires '--review'.`,
1215
+ spec.usage,
1216
+ )
1217
+ }
1137
1218
  if (values["multi-phase"]) {
1138
1219
  for (const option of ["repo", "reference", "branch", "base"] as const) {
1139
1220
  if (values[option] !== undefined) {
@@ -1409,6 +1490,40 @@ export function parseCli(args: readonly string[]): ParsedCli {
1409
1490
  spec.usage,
1410
1491
  )
1411
1492
  }
1493
+ const nonPrCompletion = parsed.values["no-pull-request"] === true
1494
+ const completionSummary = parsed.values.summary
1495
+ const completionEvidenceUrl = parsed.values["evidence-url"]
1496
+ if (
1497
+ (completionSummary !== undefined || completionEvidenceUrl !== undefined) &&
1498
+ !nonPrCompletion
1499
+ ) {
1500
+ throw usageError(
1501
+ "Options '--summary' and '--evidence-url' require '--no-pull-request'.",
1502
+ spec.usage,
1503
+ )
1504
+ }
1505
+ if (nonPrCompletion) {
1506
+ if (
1507
+ typeof completionSummary !== "string" ||
1508
+ completionSummary.trim().length === 0
1509
+ ) {
1510
+ throw usageError(
1511
+ "Option '--summary' is required with '--no-pull-request'.",
1512
+ spec.usage,
1513
+ )
1514
+ }
1515
+ const completesDone =
1516
+ (commandName === "finish" && parsed.values.outcome === "done") ||
1517
+ (["task", "phase"].includes(commandName) &&
1518
+ subcommand === "status" &&
1519
+ commandPositionals.at(-1) === "done")
1520
+ if (!completesDone) {
1521
+ throw usageError(
1522
+ "Option '--no-pull-request' is valid only with a done status or outcome.",
1523
+ spec.usage,
1524
+ )
1525
+ }
1526
+ }
1412
1527
  if (commandName === "work") {
1413
1528
  const preparing = commandPositionals[0] === "prepare"
1414
1529
  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 () => {
@@ -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
  `
@@ -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(taskId, phaseId, status, cwd)
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 dropped
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
@@ -0,0 +1,38 @@
1
+ import { Effect } from "effect"
2
+ import { ReviewService } from "../services/ReviewService"
3
+ import type { BaseCommandOptions } from "../utils/command"
4
+ import { createLoggers } from "../utils/effect"
5
+
6
+ interface ReviewOptions extends BaseCommandOptions {
7
+ readonly subcommand?: string
8
+ readonly taskId?: string
9
+ readonly ifRevision?: string
10
+ readonly json?: boolean
11
+ }
12
+
13
+ export const review = (options: ReviewOptions) =>
14
+ Effect.gen(function* () {
15
+ if (options.subcommand !== "refresh" || !options.taskId) {
16
+ return yield* Effect.fail(
17
+ new Error("Usage: agency review refresh <task>"),
18
+ )
19
+ }
20
+ const result = yield* (yield* ReviewService).refresh(
21
+ options.taskId,
22
+ options.cwd,
23
+ options.ifRevision,
24
+ )
25
+ const { log } = createLoggers(options)
26
+ log(
27
+ options.json
28
+ ? JSON.stringify(result, null, 2)
29
+ : `Refreshed review '${options.taskId}' at ${result.commit}`,
30
+ )
31
+ })
32
+
33
+ export const help = `
34
+ Usage: agency review refresh <task-id> [--if-revision <hash>] [--json]
35
+
36
+ Fetch the review source explicitly and replace the pinned commit and any clean,
37
+ detached review checkout. Review sources never move implicitly.
38
+ `
@@ -11,6 +11,7 @@ import { formatTable } from "../utils/table"
11
11
  import { getWorkViews } from "../work-view"
12
12
  import { GraphMutationService } from "../services/GraphMutationService"
13
13
  import { work as startWork, type StartWork } from "./work"
14
+ import { ReviewService } from "../services/ReviewService"
14
15
 
15
16
  interface TaskOptions extends BaseCommandOptions {
16
17
  readonly subcommand?: string
@@ -30,6 +31,9 @@ interface TaskOptions extends BaseCommandOptions {
30
31
  readonly ifRevision?: string
31
32
  readonly noEpic?: boolean
32
33
  readonly multiPhase?: boolean
34
+ readonly review?: string
35
+ readonly pullRequest?: string
36
+ readonly ref?: string
33
37
  readonly json?: boolean
34
38
  readonly statuses?: readonly string[]
35
39
  readonly repositories?: readonly string[]
@@ -38,6 +42,9 @@ interface TaskOptions extends BaseCommandOptions {
38
42
  readonly pr?: boolean
39
43
  readonly work?: boolean
40
44
  readonly auto?: boolean
45
+ readonly noPullRequest?: boolean
46
+ readonly summary?: string
47
+ readonly evidenceUrl?: string
41
48
  }
42
49
 
43
50
  export interface TaskInteraction {
@@ -82,6 +89,7 @@ export const task = (
82
89
  const repositories = yield* RepositoryService
83
90
  const workbase = yield* WorkbaseService
84
91
  const mutations = yield* GraphMutationService
92
+ const reviews = yield* ReviewService
85
93
  const { log } = createLoggers(options)
86
94
  const cwd = options.cwd ?? process.cwd()
87
95
 
@@ -208,11 +216,18 @@ export const task = (
208
216
  return yield* Effect.fail(new Error("Task ID is required"))
209
217
  }
210
218
  const multiPhase = options.multiPhase ?? false
211
- if (!multiPhase && !options.repo) {
219
+ if (!multiPhase && !options.repo && !options.review) {
212
220
  return yield* Effect.fail(
213
221
  new Error("Writable repository is required for task create"),
214
222
  )
215
223
  }
224
+ const review = options.review
225
+ ? yield* reviews.resolve(
226
+ options.review,
227
+ { pullRequest: options.pullRequest, ref: options.ref },
228
+ cwd,
229
+ )
230
+ : undefined
216
231
  const record = yield* tasks.create(
217
232
  {
218
233
  id,
@@ -220,10 +235,16 @@ export const task = (
220
235
  description: options.description?.trim() || undefined,
221
236
  epic: options.epic,
222
237
  multiPhase,
238
+ review,
223
239
  repo: options.repo,
224
- repos: parseRepositoryReferences(options.references),
225
- branch: multiPhase ? undefined : (options.branch ?? `task/${id}`),
226
- base: multiPhase ? undefined : (options.base ?? "main"),
240
+ repos: review
241
+ ? undefined
242
+ : parseRepositoryReferences(options.references),
243
+ branch:
244
+ multiPhase || review
245
+ ? undefined
246
+ : (options.branch ?? `task/${id}`),
247
+ base: multiPhase || review ? undefined : (options.base ?? "main"),
227
248
  },
228
249
  cwd,
229
250
  )
@@ -304,7 +325,19 @@ export const task = (
304
325
  new Error("Usage: agency task status <id> <status>"),
305
326
  )
306
327
  }
307
- const record = yield* tasks.setStatus(id, status, cwd)
328
+ const record = yield* tasks.setStatus(
329
+ id,
330
+ status,
331
+ cwd,
332
+ options.noPullRequest
333
+ ? {
334
+ summary: options.summary ?? "",
335
+ ...(options.evidenceUrl
336
+ ? { evidenceUrl: options.evidenceUrl }
337
+ : {}),
338
+ }
339
+ : undefined,
340
+ )
308
341
  const { content: _, ...output } = record
309
342
  log(
310
343
  options.json
@@ -423,7 +456,7 @@ Subcommands:
423
456
  create <id> Create a task without prompting
424
457
  list List tasks
425
458
  show <id> Show a task
426
- status <id> <status> Set open, working, or dropped
459
+ status <id> <status> Set open, working, dropped, or explicit non-PR done
427
460
  update <id> Update task metadata
428
461
  rename <id> <new-id> Rename a task and update graph references
429
462
  move <id> Move a task with --epic or --no-epic
@@ -433,6 +466,11 @@ Subcommands:
433
466
  Mutation option:
434
467
  --if-revision <hash> Require the target's current revision
435
468
 
469
+ Non-PR completion options (status done only):
470
+ --no-pull-request Complete without a pull request
471
+ --summary <text> Required durable outcome summary
472
+ --evidence-url <url> Optional supporting record
473
+
436
474
  Create options:
437
475
  --ticket-url <url> External ticket URL (optional)
438
476
  --description <text> Short description of the task
@@ -442,7 +480,11 @@ Create options:
442
480
  Read-only repository reference; repeatable
443
481
  --branch <name> Working branch (default: task/<id>)
444
482
  --base <name> Base branch (default: main)
445
- --multi-phase Create a task container for phases
483
+ --multi-phase Create a task container for phases
484
+ --review <alias> Create a pinned read-only review task
485
+ --pull-request <url-or-number>
486
+ Review a GitHub pull request from the alias origin
487
+ --ref <remote-ref> Review a branch or other fetchable origin ref
446
488
  --work Start work on the new task after creating it
447
489
  --auto Pass --auto to work; requires --work
448
490
 
@@ -24,6 +24,7 @@ const singlePhaseWorkspace: ExecutionWorkspace = {
24
24
  phasePath: null,
25
25
  codePath: "/workbase/tasks/example/code",
26
26
  writablePath: "/workbase/tasks/example/code/agency",
27
+ reviewPath: null,
27
28
  repo: "agency",
28
29
  repos: [],
29
30
  dryRun: false,
@@ -37,6 +38,7 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
37
38
  phasePath: "/workbase/tasks/example/phases/implementation/PHASE.md",
38
39
  codePath: "/workbase/tasks/example/phases/implementation/code",
39
40
  writablePath: "/workbase/tasks/example/phases/implementation/code/agency",
41
+ reviewPath: null,
40
42
  repo: "agency",
41
43
  repos: [],
42
44
  dryRun: false,
@@ -262,7 +262,7 @@ export const work = (
262
262
  ? `${action} the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
263
263
  : `${action} the task. Read ${workspace.taskPath}.`
264
264
  launchPath = dirname(workspace.phasePath ?? workspace.taskPath)
265
- writablePath = workspace.writablePath
265
+ writablePath = workspace.writablePath ?? undefined
266
266
  }
267
267
 
268
268
  const explicitlyRequested = Boolean(
@@ -468,7 +468,7 @@ export const workPrepare = (options: WorkOptions = {}) =>
468
468
  log(JSON.stringify(workspace, null, 2))
469
469
  } else {
470
470
  log(
471
- `${workspace.dryRun ? "Workspace plan" : "Workspace ready"}: ${workspace.writablePath}`,
471
+ `${workspace.dryRun ? "Workspace plan" : "Workspace ready"}: ${workspace.writablePath ?? workspace.reviewPath}`,
472
472
  )
473
473
  }
474
474
  })
@@ -3,6 +3,8 @@ import {
3
3
  EpicFrontmatter,
4
4
  PhaseFrontmatter,
5
5
  PullRequestRecord,
6
+ ClaimRecord,
7
+ ReviewRecord,
6
8
  TaskFrontmatter,
7
9
  WorkStatus,
8
10
  } from "./workbase/schemas"
@@ -69,13 +71,24 @@ const DocumentHash = Schema.Struct({ sha256: Schema.String })
69
71
  export const GraphEpicData = Schema.extend(EpicFrontmatter, DocumentHash)
70
72
  export const GraphTaskData = Schema.extend(TaskFrontmatter, DocumentHash)
71
73
  export const GraphPhaseData = Schema.extend(PhaseFrontmatter, DocumentHash)
72
- export const GraphExecutionData = Schema.extend(
73
- PhaseFrontmatter,
74
+ export const GraphExecutionData = Schema.Union(
75
+ Schema.extend(
76
+ PhaseFrontmatter,
77
+ Schema.Struct({
78
+ taskId: Schema.String,
79
+ phaseId: Schema.optional(Schema.String),
80
+ ticketUrl: Schema.optional(Schema.NullOr(Schema.String)),
81
+ epic: Schema.optional(Schema.String),
82
+ }),
83
+ ),
74
84
  Schema.Struct({
75
85
  taskId: Schema.String,
76
- phaseId: Schema.optional(Schema.String),
77
- ticketUrl: Schema.optional(Schema.NullOr(Schema.String)),
86
+ ticketUrl: Schema.NullOr(Schema.String),
87
+ description: Schema.optional(Schema.String),
78
88
  epic: Schema.optional(Schema.String),
89
+ review: ReviewRecord,
90
+ status: WorkStatus,
91
+ claim: Schema.optional(ClaimRecord),
79
92
  }),
80
93
  )
81
94
 
@@ -98,15 +111,24 @@ export const GraphRepositoryGit = Schema.Struct({
98
111
  head: Schema.NullOr(Schema.String),
99
112
  branch: Schema.NullOr(Schema.String),
100
113
  })
101
- export const GraphExecutionGit = Schema.Struct({
102
- branch: Schema.String,
103
- base: Schema.String,
104
- branchCommit: Schema.NullOr(Schema.String),
105
- baseCommit: Schema.NullOr(Schema.String),
106
- checkoutCommit: Schema.NullOr(Schema.String),
107
- checkoutBranch: Schema.NullOr(Schema.String),
108
- dirty: Schema.NullOr(Schema.Boolean),
109
- })
114
+ export const GraphExecutionGit = Schema.Union(
115
+ Schema.Struct({
116
+ branch: Schema.String,
117
+ base: Schema.String,
118
+ branchCommit: Schema.NullOr(Schema.String),
119
+ baseCommit: Schema.NullOr(Schema.String),
120
+ checkoutCommit: Schema.NullOr(Schema.String),
121
+ checkoutBranch: Schema.NullOr(Schema.String),
122
+ dirty: Schema.NullOr(Schema.Boolean),
123
+ }),
124
+ Schema.Struct({
125
+ pinnedCommit: Schema.String,
126
+ sourceCommit: Schema.NullOr(Schema.String),
127
+ checkoutCommit: Schema.NullOr(Schema.String),
128
+ checkoutBranch: Schema.NullOr(Schema.String),
129
+ dirty: Schema.NullOr(Schema.Boolean),
130
+ }),
131
+ )
110
132
  export const GraphPr = Schema.Union(
111
133
  Schema.Struct({ url: Schema.Null, state: Schema.Literal("none") }),
112
134
  Schema.Struct({ url: Schema.String, state: Schema.Literal("unavailable") }),
package/src/protocol.ts CHANGED
@@ -100,6 +100,7 @@ const errorMetadata: Readonly<Record<string, ErrorMetadata>> = {
100
100
  ArchiveError: { code: "ARCHIVE_ERROR", retryable: false },
101
101
  WorktreeError: { code: "WORKTREE_ERROR", retryable: false },
102
102
  PullRequestError: { code: "PULL_REQUEST_ERROR", retryable: false },
103
+ ReviewError: { code: "REVIEW_ERROR", retryable: false },
103
104
  ContextError: {
104
105
  code: "CONTEXT_ERROR",
105
106
  retryable: false,