@markjaquith/agency 2.49.0 → 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.
package/README.md CHANGED
@@ -589,6 +589,26 @@ agency task create <id> --multi-phase
589
589
  [--ticket-url <url>] [--description <text>] [--epic <id>] [--json]
590
590
  ```
591
591
 
592
+ Create a pinned, read-only review task from the selected alias's origin:
593
+
594
+ ```text
595
+ agency task create <id> --review <alias> --pull-request <url-or-number>
596
+ agency task create <id> --review <alias> --ref <remote-ref>
597
+ agency review refresh <id> [--if-revision <hash>] [--json]
598
+ ```
599
+
600
+ Review creation fetches the source and records its exact 40-character commit.
601
+ GitHub pull requests use the base alias's `refs/pull/<number>/head`, including
602
+ fork pull requests exposed through that ref. Review workspaces contain one
603
+ detached checkout and no writable branch. Source movement is observed separately
604
+ from the pin and applied only by `review refresh`; sync, doctor, work, cleanup,
605
+ and archive never move the pin implicitly. Dirty or structurally unexpected
606
+ review checkouts block refresh and cleanup. Review tasks support normal status
607
+ and claim lifecycle, but reject phase conversion and delivery PR operations.
608
+ Each active or archived review task owns one internal task-scoped pin ref. A
609
+ refresh advances that ref transactionally; failed creation removes it. Archiving
610
+ retains the pin so a deleted source can still be restored and inspected.
611
+
592
612
  ### Noninteractive Use
593
613
 
594
614
  Agency never prompts when `--no-input` is set or stdin/stderr are not TTYs.
@@ -771,7 +791,8 @@ their built-in presets. Launches are interactive and promptless by default; use
771
791
  `--auto` to send Agency's generated context prompt.
772
792
 
773
793
  `agency work prepare` resolves an execution unit and creates or reuses its
774
- writable and reference worktrees without launching an agent or changing status.
794
+ writable and reference worktrees, or its single pinned review checkout, without
795
+ launching an agent or changing status.
775
796
  Its JSON result includes document and checkout paths, resolved commits, actions,
776
797
  and Git operations. Use `--dry-run` to report planned fetch, branch, and worktree
777
798
  changes without applying them.
package/cli.ts CHANGED
@@ -43,6 +43,8 @@ import { SyncService } from "./src/services/SyncService"
43
43
  import { ReadinessService } from "./src/services/ReadinessService"
44
44
  import { GraphMutationService } from "./src/services/GraphMutationService"
45
45
  import { DoctorService } from "./src/services/DoctorService"
46
+ import { ReviewService } from "./src/services/ReviewService"
47
+ import { review, help as reviewHelp } from "./src/commands/review"
46
48
  import {
47
49
  claimCommand,
48
50
  claimHelp,
@@ -75,6 +77,7 @@ const CliLayer = Layer.mergeAll(
75
77
  ReadinessService.Default,
76
78
  GraphMutationService.Default,
77
79
  DoctorService.Default,
80
+ ReviewService.Default,
78
81
  )
79
82
 
80
83
  /**
@@ -449,6 +452,9 @@ const commands: Record<string, Command> = {
449
452
  clearDescription: options["clear-description"],
450
453
  epic: options.epic,
451
454
  repo: options.repo?.[0],
455
+ review: options.review,
456
+ pullRequest: options["pull-request"],
457
+ ref: options.ref,
452
458
  references: options.reference,
453
459
  branch: options.branch,
454
460
  base: options.base,
@@ -477,6 +483,22 @@ const commands: Record<string, Command> = {
477
483
  )
478
484
  },
479
485
  },
486
+ review: {
487
+ run: async (args: string[], options: Record<string, any>) => {
488
+ if (options.help) return console.log(reviewHelp)
489
+ await runCommand(
490
+ review({
491
+ subcommand: args[0],
492
+ taskId: args[1],
493
+ ifRevision: options["if-revision"],
494
+ json: options.json,
495
+ silent: options.silent,
496
+ verbose: options.verbose,
497
+ cwd: options.cwd,
498
+ }),
499
+ )
500
+ },
501
+ },
480
502
  work: {
481
503
  run: async (args: string[], options: Record<string, any>) => {
482
504
  if (options.help) {
@@ -683,6 +705,7 @@ Commands:
683
705
  worktree <subcommand> Inspect and maintain managed worktrees
684
706
  next List or select ready execution units
685
707
  pr create Create a pull request for an execution unit
708
+ review refresh Explicitly refresh a pinned review task
686
709
  repo <subcommand> Manage workbase repositories
687
710
  status Show status for the current workbase
688
711
  doctor Diagnose workbase health and integrations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.49.0",
3
+ "version": "2.50.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -176,6 +176,7 @@
176
176
  { "$ref": "#/$defs/epicData" },
177
177
  { "$ref": "#/$defs/singleTaskData" },
178
178
  { "$ref": "#/$defs/multiTaskData" },
179
+ { "$ref": "#/$defs/reviewTaskData" },
179
180
  { "$ref": "#/$defs/phaseData" },
180
181
  { "$ref": "#/$defs/executionData" },
181
182
  { "$ref": "#/$defs/repositoryData" }
@@ -220,7 +221,8 @@
220
221
  "data": {
221
222
  "oneOf": [
222
223
  { "$ref": "#/$defs/singleTaskData" },
223
- { "$ref": "#/$defs/multiTaskData" }
224
+ { "$ref": "#/$defs/multiTaskData" },
225
+ { "$ref": "#/$defs/reviewTaskData" }
224
226
  ]
225
227
  },
226
228
  "workspace": { "$ref": "#/$defs/documentWorkspace" },
@@ -358,34 +360,72 @@
358
360
  "sha256": { "type": "string" }
359
361
  }
360
362
  },
361
- "phaseData": {
363
+ "reviewSource": {
364
+ "oneOf": [
365
+ {
366
+ "type": "object",
367
+ "additionalProperties": false,
368
+ "required": [
369
+ "kind",
370
+ "provider",
371
+ "repository",
372
+ "identifier",
373
+ "url",
374
+ "fetchRef"
375
+ ],
376
+ "properties": {
377
+ "kind": { "const": "pull-request" },
378
+ "provider": { "const": "github" },
379
+ "repository": { "type": "string" },
380
+ "identifier": { "type": "string", "pattern": "^\\d+$" },
381
+ "url": {
382
+ "type": "string",
383
+ "pattern": "^https://github\\.com/[^/]+/[^/]+/pull/\\d+$"
384
+ },
385
+ "fetchRef": { "type": "string", "pattern": "^refs/pull/\\d+/head$" }
386
+ }
387
+ },
388
+ {
389
+ "type": "object",
390
+ "additionalProperties": false,
391
+ "required": ["kind", "ref"],
392
+ "properties": {
393
+ "kind": { "const": "branch" },
394
+ "ref": { "type": "string", "pattern": "^refs/heads/" }
395
+ }
396
+ }
397
+ ]
398
+ },
399
+ "reviewRecord": {
362
400
  "type": "object",
363
401
  "additionalProperties": false,
364
- "required": ["repo", "branch", "base", "pr", "status", "sha256"],
402
+ "required": ["repo", "source", "commit", "refreshedAt"],
365
403
  "properties": {
366
- "description": { "type": "string" },
367
404
  "repo": { "type": "string" },
368
- "repos": {
369
- "type": "array",
370
- "items": { "$ref": "#/$defs/repositoryReference" }
371
- },
372
- "branch": { "type": "string" },
373
- "base": { "type": "string" },
374
- "pr": { "type": ["string", "null"] },
405
+ "source": { "$ref": "#/$defs/reviewSource" },
406
+ "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
407
+ "refreshedAt": { "type": "string", "format": "date-time" }
408
+ }
409
+ },
410
+ "reviewTaskData": {
411
+ "type": "object",
412
+ "additionalProperties": false,
413
+ "required": ["ticketUrl", "review", "status", "sha256"],
414
+ "properties": {
415
+ "ticketUrl": { "type": ["string", "null"] },
416
+ "description": { "type": "string" },
417
+ "epic": { "type": "string" },
418
+ "review": { "$ref": "#/$defs/reviewRecord" },
375
419
  "status": { "$ref": "#/$defs/status" },
376
420
  "claim": { "$ref": "#/$defs/claim" },
377
421
  "sha256": { "type": "string" }
378
422
  }
379
423
  },
380
- "executionData": {
424
+ "phaseData": {
381
425
  "type": "object",
382
426
  "additionalProperties": false,
383
- "required": ["taskId", "repo", "branch", "base", "pr", "status"],
427
+ "required": ["repo", "branch", "base", "pr", "status", "sha256"],
384
428
  "properties": {
385
- "taskId": { "type": "string" },
386
- "phaseId": { "type": "string" },
387
- "ticketUrl": { "type": ["string", "null"] },
388
- "epic": { "type": "string" },
389
429
  "description": { "type": "string" },
390
430
  "repo": { "type": "string" },
391
431
  "repos": {
@@ -396,9 +436,50 @@
396
436
  "base": { "type": "string" },
397
437
  "pr": { "type": ["string", "null"] },
398
438
  "status": { "$ref": "#/$defs/status" },
399
- "claim": { "$ref": "#/$defs/claim" }
439
+ "claim": { "$ref": "#/$defs/claim" },
440
+ "sha256": { "type": "string" }
400
441
  }
401
442
  },
443
+ "executionData": {
444
+ "oneOf": [
445
+ {
446
+ "type": "object",
447
+ "additionalProperties": false,
448
+ "required": ["taskId", "repo", "branch", "base", "pr", "status"],
449
+ "properties": {
450
+ "taskId": { "type": "string" },
451
+ "phaseId": { "type": "string" },
452
+ "ticketUrl": { "type": ["string", "null"] },
453
+ "epic": { "type": "string" },
454
+ "description": { "type": "string" },
455
+ "repo": { "type": "string" },
456
+ "repos": {
457
+ "type": "array",
458
+ "items": { "$ref": "#/$defs/repositoryReference" }
459
+ },
460
+ "branch": { "type": "string" },
461
+ "base": { "type": "string" },
462
+ "pr": { "type": ["string", "null"] },
463
+ "status": { "$ref": "#/$defs/status" },
464
+ "claim": { "$ref": "#/$defs/claim" }
465
+ }
466
+ },
467
+ {
468
+ "type": "object",
469
+ "additionalProperties": false,
470
+ "required": ["taskId", "ticketUrl", "review", "status"],
471
+ "properties": {
472
+ "taskId": { "type": "string" },
473
+ "ticketUrl": { "type": ["string", "null"] },
474
+ "description": { "type": "string" },
475
+ "epic": { "type": "string" },
476
+ "review": { "$ref": "#/$defs/reviewRecord" },
477
+ "status": { "$ref": "#/$defs/status" },
478
+ "claim": { "$ref": "#/$defs/claim" }
479
+ }
480
+ }
481
+ ]
482
+ },
402
483
  "claim": {
403
484
  "type": "object",
404
485
  "additionalProperties": false,
@@ -469,26 +550,48 @@
469
550
  }
470
551
  },
471
552
  "executionGit": {
472
- "type": "object",
473
- "additionalProperties": false,
474
- "required": [
475
- "branch",
476
- "base",
477
- "branchCommit",
478
- "baseCommit",
479
- "checkoutCommit",
480
- "checkoutBranch",
481
- "dirty"
482
- ],
483
- "properties": {
484
- "branch": { "type": "string" },
485
- "base": { "type": "string" },
486
- "branchCommit": { "type": ["string", "null"] },
487
- "baseCommit": { "type": ["string", "null"] },
488
- "checkoutCommit": { "type": ["string", "null"] },
489
- "checkoutBranch": { "type": ["string", "null"] },
490
- "dirty": { "type": ["boolean", "null"] }
491
- }
553
+ "oneOf": [
554
+ {
555
+ "type": "object",
556
+ "additionalProperties": false,
557
+ "required": [
558
+ "branch",
559
+ "base",
560
+ "branchCommit",
561
+ "baseCommit",
562
+ "checkoutCommit",
563
+ "checkoutBranch",
564
+ "dirty"
565
+ ],
566
+ "properties": {
567
+ "branch": { "type": "string" },
568
+ "base": { "type": "string" },
569
+ "branchCommit": { "type": ["string", "null"] },
570
+ "baseCommit": { "type": ["string", "null"] },
571
+ "checkoutCommit": { "type": ["string", "null"] },
572
+ "checkoutBranch": { "type": ["string", "null"] },
573
+ "dirty": { "type": ["boolean", "null"] }
574
+ }
575
+ },
576
+ {
577
+ "type": "object",
578
+ "additionalProperties": false,
579
+ "required": [
580
+ "pinnedCommit",
581
+ "sourceCommit",
582
+ "checkoutCommit",
583
+ "checkoutBranch",
584
+ "dirty"
585
+ ],
586
+ "properties": {
587
+ "pinnedCommit": { "type": "string" },
588
+ "sourceCommit": { "type": ["string", "null"] },
589
+ "checkoutCommit": { "type": ["string", "null"] },
590
+ "checkoutBranch": { "type": ["string", "null"] },
591
+ "dirty": { "type": ["boolean", "null"] }
592
+ }
593
+ }
594
+ ]
492
595
  },
493
596
  "pr": {
494
597
  "oneOf": [
@@ -7,6 +7,68 @@ const expectUsageError = (args: string[], usage: string) => {
7
7
  }
8
8
 
9
9
  describe("strict CLI parsing", () => {
10
+ test("accepts review creation and refresh while enforcing one source", () => {
11
+ expect(
12
+ parseCli([
13
+ "task",
14
+ "create",
15
+ "review-it",
16
+ "--review",
17
+ "agency",
18
+ "--pull-request",
19
+ "42",
20
+ ]).values,
21
+ ).toMatchObject({ review: "agency", "pull-request": "42" })
22
+ expect(() =>
23
+ parseCli([
24
+ "task",
25
+ "create",
26
+ "review-it",
27
+ "--review",
28
+ "agency",
29
+ "--ref",
30
+ "feature/review",
31
+ ]),
32
+ ).not.toThrow()
33
+ expect(() =>
34
+ parseCli([
35
+ "review",
36
+ "refresh",
37
+ "review-it",
38
+ "--if-revision",
39
+ "a".repeat(64),
40
+ ]),
41
+ ).not.toThrow()
42
+ expect(() =>
43
+ parseCli([
44
+ "task",
45
+ "create",
46
+ "review-it",
47
+ "--review",
48
+ "agency",
49
+ "--pull-request",
50
+ "42",
51
+ "--ref",
52
+ "feature/review",
53
+ ]),
54
+ ).toThrow("exactly one")
55
+ for (const extra of [
56
+ ["--ref", "feature/review", "--repo", "agency"],
57
+ ["--ref", "feature/review", "--multi-phase"],
58
+ ]) {
59
+ expect(() =>
60
+ parseCli([
61
+ "task",
62
+ "create",
63
+ "review-it",
64
+ "--review",
65
+ "agency",
66
+ ...extra,
67
+ ]),
68
+ ).toThrow("cannot be combined")
69
+ }
70
+ })
71
+
10
72
  test("accepts every documented orchestration recipe command", () => {
11
73
  for (const args of orchestrationRecipes) {
12
74
  expect(() => parseCli(args)).not.toThrow()
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 = {
@@ -393,7 +396,7 @@ const commands = {
393
396
  },
394
397
  create: {
395
398
  usage:
396
- "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]",
397
400
  minArgs: 1,
398
401
  maxArgs: 1,
399
402
  options: [
@@ -405,6 +408,9 @@ const commands = {
405
408
  "branch",
406
409
  "base",
407
410
  "multi-phase",
411
+ "review",
412
+ "pull-request",
413
+ "ref",
408
414
  "json",
409
415
  ],
410
416
  repeatable: ["reference"],
@@ -760,6 +766,22 @@ const commands = {
760
766
  },
761
767
  },
762
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
+ },
763
785
  worktree: {
764
786
  usage: "agency worktree <list|inspect|prepare|remove|rebuild|repair>",
765
787
  options: {
@@ -1161,6 +1183,38 @@ function validateTaskCreate(
1161
1183
  spec: LeafCommand,
1162
1184
  requireRepo: boolean,
1163
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
+ }
1164
1218
  if (values["multi-phase"]) {
1165
1219
  for (const option of ["repo", "reference", "branch", "base"] as const) {
1166
1220
  if (values[option] !== undefined) {
@@ -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[]
@@ -85,6 +89,7 @@ export const task = (
85
89
  const repositories = yield* RepositoryService
86
90
  const workbase = yield* WorkbaseService
87
91
  const mutations = yield* GraphMutationService
92
+ const reviews = yield* ReviewService
88
93
  const { log } = createLoggers(options)
89
94
  const cwd = options.cwd ?? process.cwd()
90
95
 
@@ -211,11 +216,18 @@ export const task = (
211
216
  return yield* Effect.fail(new Error("Task ID is required"))
212
217
  }
213
218
  const multiPhase = options.multiPhase ?? false
214
- if (!multiPhase && !options.repo) {
219
+ if (!multiPhase && !options.repo && !options.review) {
215
220
  return yield* Effect.fail(
216
221
  new Error("Writable repository is required for task create"),
217
222
  )
218
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
219
231
  const record = yield* tasks.create(
220
232
  {
221
233
  id,
@@ -223,10 +235,16 @@ export const task = (
223
235
  description: options.description?.trim() || undefined,
224
236
  epic: options.epic,
225
237
  multiPhase,
238
+ review,
226
239
  repo: options.repo,
227
- repos: parseRepositoryReferences(options.references),
228
- branch: multiPhase ? undefined : (options.branch ?? `task/${id}`),
229
- 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"),
230
248
  },
231
249
  cwd,
232
250
  )
@@ -462,7 +480,11 @@ Create options:
462
480
  Read-only repository reference; repeatable
463
481
  --branch <name> Working branch (default: task/<id>)
464
482
  --base <name> Base branch (default: main)
465
- --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
466
488
  --work Start work on the new task after creating it
467
489
  --auto Pass --auto to work; requires --work
468
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
  })