@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.
@@ -374,6 +374,16 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
374
374
  message: `Task '${id}' has an active claim; release or finish it before changing execution metadata`,
375
375
  })
376
376
  }
377
+ if (
378
+ updates.pr &&
379
+ "completion" in record.data &&
380
+ record.data.completion
381
+ ) {
382
+ return yield* new GraphMutationError({
383
+ message:
384
+ "Reopen non-PR completed work before recording a pull request",
385
+ })
386
+ }
377
387
  if (
378
388
  executionChange &&
379
389
  (yield* fs.isDirectory(join(dirname(record.path), "code")))
@@ -504,6 +514,12 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
504
514
  message: `Phase '${id}' has an active claim; release or finish it before changing execution metadata`,
505
515
  })
506
516
  }
517
+ if (updates.pr && record.data.completion) {
518
+ return yield* new GraphMutationError({
519
+ message:
520
+ "Reopen non-PR completed work before recording a pull request",
521
+ })
522
+ }
507
523
  const data: PhaseData = yield* decode(
508
524
  PhaseFrontmatter,
509
525
  {
@@ -261,7 +261,9 @@ describe("IntegrationService", () => {
261
261
  expect(body).toContain("Only `done` satisfies a dependency")
262
262
  expect(body).toContain("Require explicit user intent")
263
263
  expect(body).toContain("changing repository")
264
- expect(body).toMatch(/archiving, restoring,\s+dropping, or/)
264
+ expect(body).toMatch(
265
+ /archiving, restoring,\s+dropping, reopening, or completing work without a pull request/,
266
+ )
265
267
  expect(body).toContain("Never invent entity IDs")
266
268
  expect(body).toContain("Preserve parent backlinks")
267
269
  expect(body).toContain("dirty-worktree, active-claim, revision")
@@ -281,6 +283,7 @@ describe("IntegrationService", () => {
281
283
  expect(body).toContain("pausing or handing off")
282
284
  expect(body).toContain("`agency finish`")
283
285
  expect(body).toContain("`agency sync --apply`")
286
+ expect(body).toContain("`--no-pull-request --summary <text>`")
284
287
  expect(body).toContain("`TASK.md` or `PHASE.md`")
285
288
  expect(body).toContain("PR state, current head, diff summary")
286
289
  expect(body).toContain("Run `agency validate` before reporting completion")
@@ -26,6 +26,10 @@ import {
26
26
  type TransactionStep,
27
27
  } from "./LifecycleTransaction"
28
28
  import { withWorktreeLocks } from "./WorktreeLock"
29
+ import {
30
+ buildNonPrCompletion,
31
+ type NonPrCompletionInput,
32
+ } from "../workbase/completion"
29
33
 
30
34
  class PhaseError extends Data.TaggedError("PhaseError")<{
31
35
  readonly message: string
@@ -221,6 +225,9 @@ export class PhaseService extends Effect.Service<PhaseService>()(
221
225
  pr: task.data.pr,
222
226
  status: task.data.status,
223
227
  ...(task.data.claim ? { claim: task.data.claim } : {}),
228
+ ...(task.data.completion
229
+ ? { completion: task.data.completion }
230
+ : {}),
224
231
  })
225
232
  const firstTitle = firstPhaseId!
226
233
  .split("-")
@@ -469,6 +476,7 @@ export class PhaseService extends Effect.Service<PhaseService>()(
469
476
  id: string,
470
477
  status: string,
471
478
  startPath: string = process.cwd(),
479
+ nonPrCompletion?: NonPrCompletionInput,
472
480
  ) =>
473
481
  Effect.gen(function* () {
474
482
  const fs = yield* FileSystemService
@@ -486,11 +494,41 @@ export class PhaseService extends Effect.Service<PhaseService>()(
486
494
  message: `Phase '${id}' has an active claim; use agency release or agency finish`,
487
495
  })
488
496
  }
489
- if (!canTransitionStatus(record.data.status, validStatus)) {
497
+ if (nonPrCompletion && validStatus !== "done") {
498
+ return yield* new PhaseError({
499
+ message: "Non-PR completion is valid only with a done status",
500
+ })
501
+ }
502
+ if (nonPrCompletion && record.data.pr !== null) {
503
+ return yield* new PhaseError({
504
+ message:
505
+ "Cannot complete without a pull request while an authoritative pull request is recorded",
506
+ })
507
+ }
508
+ const completionResult = nonPrCompletion
509
+ ? buildNonPrCompletion(nonPrCompletion, new Date())
510
+ : undefined
511
+ if (completionResult && "error" in completionResult) {
512
+ return yield* new PhaseError({ message: completionResult.error })
513
+ }
514
+ if (
515
+ completionResult &&
516
+ record.data.status !== "open" &&
517
+ record.data.status !== "working" &&
518
+ record.data.status !== "delegated"
519
+ ) {
520
+ return yield* new PhaseError({
521
+ message: `Cannot transition phase '${id}' from ${record.data.status} to done; reopen it first`,
522
+ })
523
+ }
524
+ if (
525
+ !canTransitionStatus(record.data.status, validStatus) &&
526
+ !completionResult
527
+ ) {
490
528
  if (validStatus === "done") {
491
529
  return yield* new PhaseError({
492
530
  message:
493
- "Work becomes done after its authoritative pull request is merged; run 'agency sync --apply'",
531
+ "Work becomes done after its authoritative pull request is merged; run 'agency sync --apply', or explicitly complete a non-PR outcome with '--no-pull-request --summary <text>'",
494
532
  })
495
533
  }
496
534
  return yield* new PhaseError({
@@ -498,7 +536,16 @@ export class PhaseService extends Effect.Service<PhaseService>()(
498
536
  })
499
537
  }
500
538
  const parsed = yield* parseFrontmatter(record.content, record.path)
501
- const data = { ...record.data, status: validStatus }
539
+ const { completion: _, ...withoutCompletion } = record.data
540
+ const data: PhaseData = completionResult
541
+ ? {
542
+ ...record.data,
543
+ status: "done",
544
+ completion: completionResult.value,
545
+ }
546
+ : validStatus === "open"
547
+ ? { ...withoutCompletion, status: validStatus }
548
+ : { ...record.data, status: validStatus }
502
549
  const content = formatMarkdownDocument(data, parsed.body)
503
550
  yield* fs.writeFile(record.path, content)
504
551
  return {
@@ -279,6 +279,26 @@ process.exit(${exitCode})
279
279
  expect(await createPullRequest("example", undefined, false, true)).toBe(url)
280
280
  })
281
281
 
282
+ test("requires reopening non-PR completed work even when forced", async () => {
283
+ await createTask()
284
+ await runTestEffect(
285
+ TaskService.pipe(
286
+ Effect.flatMap((service) =>
287
+ service.setStatus("example", "done", root, {
288
+ summary: "Investigation completed without changes.",
289
+ }),
290
+ ),
291
+ ),
292
+ )
293
+
294
+ await expect(
295
+ createPullRequest("example", undefined, false, true),
296
+ ).rejects.toThrow("Reopen non-PR completed work")
297
+ expect(
298
+ await Bun.file(join(root, "tasks/example/code/agency")).exists(),
299
+ ).toBe(false)
300
+ })
301
+
282
302
  test("updates only PHASE.md for a phase PR", async () => {
283
303
  await runTestEffect(
284
304
  TaskService.pipe(
@@ -58,6 +58,12 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
58
58
  message: `Task '${taskId}' requires a phase ID`,
59
59
  })
60
60
  : task
61
+ if ("completion" in target.data && target.data.completion) {
62
+ return yield* new PullRequestError({
63
+ message:
64
+ "Reopen non-PR completed work before recording a pull request",
65
+ })
66
+ }
61
67
  const parsed = yield* parseFrontmatter(target.content, target.path)
62
68
  yield* fs.writeFile(
63
69
  target.path,
@@ -99,6 +105,21 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
99
105
  const worktrees = yield* WorktreeService
100
106
  const readiness = yield* ReadinessService
101
107
  const workbase = yield* WorkbaseService
108
+ const task = yield* tasks.show(taskId, startPath)
109
+ const target =
110
+ "phases" in task.data
111
+ ? phaseId
112
+ ? yield* phases.show(taskId, phaseId, startPath)
113
+ : yield* new PullRequestError({
114
+ message: `Task '${taskId}' requires a phase ID`,
115
+ })
116
+ : task
117
+ if ("completion" in target.data && target.data.completion) {
118
+ return yield* new PullRequestError({
119
+ message:
120
+ "Reopen non-PR completed work before creating a pull request",
121
+ })
122
+ }
102
123
  yield* readiness.guard(
103
124
  "pr",
104
125
  taskId,
@@ -112,11 +133,11 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
112
133
  startPath,
113
134
  options,
114
135
  )
115
- const task = yield* tasks.show(taskId, workspace.root)
136
+ const workspaceTask = yield* tasks.show(taskId, workspace.root)
116
137
  const execution =
117
- "phases" in task.data
138
+ "phases" in workspaceTask.data
118
139
  ? (yield* phases.show(taskId, phaseId!, workspace.root)).data
119
- : task.data
140
+ : workspaceTask.data
120
141
  const { config } = yield* workbase.loadConfig(workspace.root)
121
142
  const remote = config.delivery?.remote ?? "origin"
122
143
 
@@ -8,6 +8,7 @@ import { PullRequestService } from "./PullRequestService"
8
8
  import { SyncService } from "./SyncService"
9
9
  import { TaskService } from "./TaskService"
10
10
  import { WorktreeService } from "./WorktreeService"
11
+ import { WorkbaseService } from "./WorkbaseService"
11
12
 
12
13
  const git = async (args: string[], cwd?: string) => {
13
14
  const process = Bun.spawn(["git", ...args], {
@@ -663,4 +664,69 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
663
664
  )
664
665
  expect(task.data).toMatchObject({ status: "open" })
665
666
  })
667
+
668
+ test("leaves non-PR completion unchanged when a matching PR is discoverable", async () => {
669
+ await runTestEffect(
670
+ TaskService.pipe(
671
+ Effect.flatMap((service) =>
672
+ service.create(
673
+ {
674
+ id: "non-pr",
675
+ ticketUrl: null,
676
+ repo: "agency",
677
+ branch: "feat/example",
678
+ base: "main",
679
+ },
680
+ root,
681
+ ),
682
+ ),
683
+ ),
684
+ )
685
+ await runTestEffect(
686
+ TaskService.pipe(
687
+ Effect.flatMap((service) =>
688
+ service.setStatus("non-pr", "done", root, {
689
+ summary: "Investigation completed without changes.",
690
+ }),
691
+ ),
692
+ ),
693
+ )
694
+
695
+ for (let attempt = 0; attempt < 2; attempt += 1) {
696
+ const applied = await runTestEffect(
697
+ SyncService.pipe(
698
+ Effect.flatMap((service) =>
699
+ service.reconcile({ cwd: root, apply: true }),
700
+ ),
701
+ ),
702
+ )
703
+ expect(
704
+ applied.changes.some(
705
+ (change) =>
706
+ change.kind === "record-pr" || change.kind === "mark-done",
707
+ ),
708
+ ).toBe(false)
709
+ }
710
+
711
+ const task = await runTestEffect(
712
+ TaskService.pipe(
713
+ Effect.flatMap((service) => service.show("non-pr", root)),
714
+ ),
715
+ )
716
+ expect(task.data).toMatchObject({
717
+ status: "done",
718
+ pr: null,
719
+ completion: {
720
+ mode: "non-pr",
721
+ summary: "Investigation completed without changes.",
722
+ },
723
+ })
724
+ expect(
725
+ await runTestEffect(
726
+ WorkbaseService.pipe(
727
+ Effect.flatMap((service) => service.validate(root)),
728
+ ),
729
+ ),
730
+ ).toMatchObject({ valid: true, issues: [] })
731
+ })
666
732
  })
@@ -580,6 +580,19 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
580
580
  })
581
581
  }
582
582
 
583
+ if (data.completion) {
584
+ executions.push({
585
+ target: record.key,
586
+ status: data.status,
587
+ branch: data.branch,
588
+ base: data.base,
589
+ claim: data.claim ?? null,
590
+ checkouts: checkoutStates,
591
+ pr: { url: null, state: "none" },
592
+ })
593
+ continue
594
+ }
595
+
583
596
  const existing = data.pr ? normalizePullRequestRecord(data.pr) : null
584
597
  let current: PullRequestRecord | null = existing
585
598
  let pr: Record<string, unknown> = existing ?? {
@@ -283,6 +283,66 @@ describe("task and phase services", () => {
283
283
  })
284
284
  })
285
285
 
286
+ test("preserves non-PR completion when converting a task to phases", async () => {
287
+ await runTestEffect(
288
+ TaskService.pipe(
289
+ Effect.flatMap((service) =>
290
+ service.create(
291
+ {
292
+ id: "completed",
293
+ ticketUrl: null,
294
+ repo: "agency",
295
+ branch: "task/completed",
296
+ base: "main",
297
+ },
298
+ root,
299
+ ),
300
+ ),
301
+ ),
302
+ )
303
+ await runTestEffect(
304
+ TaskService.pipe(
305
+ Effect.flatMap((service) =>
306
+ service.setStatus("completed", "done", root, {
307
+ summary: "Investigation completed without changes.",
308
+ }),
309
+ ),
310
+ ),
311
+ )
312
+ await runTestEffect(
313
+ PhaseService.pipe(
314
+ Effect.flatMap((service) =>
315
+ service.create(
316
+ {
317
+ taskId: "completed",
318
+ id: "follow-up",
319
+ firstPhase: "investigation",
320
+ repo: "agency",
321
+ branch: "task/completed-follow-up",
322
+ base: "main",
323
+ },
324
+ root,
325
+ ),
326
+ ),
327
+ ),
328
+ )
329
+
330
+ const firstPhase = await runTestEffect(
331
+ PhaseService.pipe(
332
+ Effect.flatMap((service) =>
333
+ service.show("completed", "investigation", root),
334
+ ),
335
+ ),
336
+ )
337
+ expect(firstPhase.data).toMatchObject({
338
+ status: "done",
339
+ completion: {
340
+ mode: "non-pr",
341
+ summary: "Investigation completed without changes.",
342
+ },
343
+ })
344
+ })
345
+
286
346
  test("updates status on execution units", async () => {
287
347
  const createdTask = await runTestEffect(
288
348
  TaskService.pipe(
@@ -327,6 +387,50 @@ describe("task and phase services", () => {
327
387
  ),
328
388
  ),
329
389
  ).rejects.toThrow("authoritative pull request is merged")
390
+ const completedTask = await runTestEffect(
391
+ TaskService.pipe(
392
+ Effect.flatMap((service) =>
393
+ service.setStatus("single-status", "done", root, {
394
+ summary: "Investigation completed without repository changes.",
395
+ evidenceUrl: "https://example.com/investigation",
396
+ }),
397
+ ),
398
+ ),
399
+ )
400
+ expect(completedTask.data).toMatchObject({
401
+ status: "done",
402
+ completion: {
403
+ mode: "non-pr",
404
+ summary: "Investigation completed without repository changes.",
405
+ evidenceUrl: "https://example.com/investigation",
406
+ },
407
+ })
408
+ expect(completedTask.data.completion?.completedAt).toMatch(
409
+ /^\d{4}-\d{2}-\d{2}T/,
410
+ )
411
+ await expect(
412
+ runTestEffect(
413
+ PullRequestService.pipe(
414
+ Effect.flatMap((service) =>
415
+ service.setUrl(
416
+ "single-status",
417
+ undefined,
418
+ "https://github.com/example/agency/pull/1",
419
+ root,
420
+ ),
421
+ ),
422
+ ),
423
+ ),
424
+ ).rejects.toThrow("Reopen non-PR completed work")
425
+ const reopenedTask = await runTestEffect(
426
+ TaskService.pipe(
427
+ Effect.flatMap((service) =>
428
+ service.setStatus("single-status", "open", root),
429
+ ),
430
+ ),
431
+ )
432
+ expect(reopenedTask.data.status).toBe("open")
433
+ expect("completion" in reopenedTask.data).toBe(false)
330
434
  const droppedTask = await runTestEffect(
331
435
  TaskService.pipe(
332
436
  Effect.flatMap((service) =>
@@ -335,6 +439,17 @@ describe("task and phase services", () => {
335
439
  ),
336
440
  )
337
441
  expect(droppedTask.data.status).toBe("dropped")
442
+ await expect(
443
+ runTestEffect(
444
+ TaskService.pipe(
445
+ Effect.flatMap((service) =>
446
+ service.setStatus("single-status", "done", root, {
447
+ summary: "Cannot bypass reopening.",
448
+ }),
449
+ ),
450
+ ),
451
+ ),
452
+ ).rejects.toThrow("reopen it first")
338
453
 
339
454
  await runTestEffect(
340
455
  TaskService.pipe(
@@ -407,6 +522,73 @@ describe("task and phase services", () => {
407
522
  ),
408
523
  ),
409
524
  ).rejects.toThrow("authoritative pull request is merged")
525
+ await expect(
526
+ runTestEffect(
527
+ PhaseService.pipe(
528
+ Effect.flatMap((service) =>
529
+ service.setStatus("multi-status", "implementation", "done", root, {
530
+ summary: " ",
531
+ }),
532
+ ),
533
+ ),
534
+ ),
535
+ ).rejects.toThrow("summary must not be empty")
536
+ const completedPhase = await runTestEffect(
537
+ PhaseService.pipe(
538
+ Effect.flatMap((service) =>
539
+ service.setStatus("multi-status", "implementation", "done", root, {
540
+ summary: "Operational work completed outside the repository.",
541
+ }),
542
+ ),
543
+ ),
544
+ )
545
+ expect(completedPhase.data).toMatchObject({
546
+ status: "done",
547
+ completion: {
548
+ mode: "non-pr",
549
+ summary: "Operational work completed outside the repository.",
550
+ },
551
+ })
552
+
553
+ await runTestEffect(
554
+ TaskService.pipe(
555
+ Effect.flatMap((service) =>
556
+ service.create(
557
+ {
558
+ id: "recorded-pr",
559
+ ticketUrl: null,
560
+ repo: "agency",
561
+ branch: "task/recorded-pr",
562
+ base: "main",
563
+ },
564
+ root,
565
+ ),
566
+ ),
567
+ ),
568
+ )
569
+ await runTestEffect(
570
+ PullRequestService.pipe(
571
+ Effect.flatMap((service) =>
572
+ service.setUrl(
573
+ "recorded-pr",
574
+ undefined,
575
+ "https://github.com/example/agency/pull/1",
576
+ root,
577
+ ),
578
+ ),
579
+ ),
580
+ )
581
+ await expect(
582
+ runTestEffect(
583
+ TaskService.pipe(
584
+ Effect.flatMap((service) =>
585
+ service.setStatus("recorded-pr", "done", root, {
586
+ summary: "Attempted bypass",
587
+ }),
588
+ ),
589
+ ),
590
+ ),
591
+ ).rejects.toThrow("authoritative pull request is recorded")
410
592
  await expect(
411
593
  runTestEffect(
412
594
  TaskService.pipe(
@@ -19,6 +19,10 @@ import {
19
19
  import { canTransitionStatus } from "../readiness"
20
20
  import { documentRevision } from "../workbase/document-revision"
21
21
  import { archivedTaskDirectory } from "../workbase/archive"
22
+ import {
23
+ buildNonPrCompletion,
24
+ type NonPrCompletionInput,
25
+ } from "../workbase/completion"
22
26
  import {
23
27
  documentWriteStep,
24
28
  runLifecycleTransaction,
@@ -247,6 +251,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
247
251
  id: string,
248
252
  status: string,
249
253
  startPath: string = process.cwd(),
254
+ nonPrCompletion?: NonPrCompletionInput,
250
255
  ) =>
251
256
  Effect.gen(function* () {
252
257
  const fs = yield* FileSystemService
@@ -269,11 +274,41 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
269
274
  message: `Task '${id}' has an active claim; use agency release or agency finish`,
270
275
  })
271
276
  }
272
- if (!canTransitionStatus(record.data.status, validStatus)) {
277
+ if (nonPrCompletion && validStatus !== "done") {
278
+ return yield* new TaskError({
279
+ message: "Non-PR completion is valid only with a done status",
280
+ })
281
+ }
282
+ if (nonPrCompletion && record.data.pr !== null) {
283
+ return yield* new TaskError({
284
+ message:
285
+ "Cannot complete without a pull request while an authoritative pull request is recorded",
286
+ })
287
+ }
288
+ const completionResult = nonPrCompletion
289
+ ? buildNonPrCompletion(nonPrCompletion, new Date())
290
+ : undefined
291
+ if (completionResult && "error" in completionResult) {
292
+ return yield* new TaskError({ message: completionResult.error })
293
+ }
294
+ if (
295
+ completionResult &&
296
+ record.data.status !== "open" &&
297
+ record.data.status !== "working" &&
298
+ record.data.status !== "delegated"
299
+ ) {
300
+ return yield* new TaskError({
301
+ message: `Cannot transition task '${id}' from ${record.data.status} to done; reopen it first`,
302
+ })
303
+ }
304
+ if (
305
+ !canTransitionStatus(record.data.status, validStatus) &&
306
+ !completionResult
307
+ ) {
273
308
  if (validStatus === "done") {
274
309
  return yield* new TaskError({
275
310
  message:
276
- "Work becomes done after its authoritative pull request is merged; run 'agency sync --apply'",
311
+ "Work becomes done after its authoritative pull request is merged; run 'agency sync --apply', or explicitly complete a non-PR outcome with '--no-pull-request --summary <text>'",
277
312
  })
278
313
  }
279
314
  return yield* new TaskError({
@@ -281,7 +316,16 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
281
316
  })
282
317
  }
283
318
  const parsed = yield* parseFrontmatter(record.content, record.path)
284
- const data = { ...record.data, status: validStatus }
319
+ const { completion: _, ...withoutCompletion } = record.data
320
+ const data: TaskData = completionResult
321
+ ? {
322
+ ...record.data,
323
+ status: "done",
324
+ completion: completionResult.value,
325
+ }
326
+ : validStatus === "open"
327
+ ? { ...withoutCompletion, status: validStatus }
328
+ : { ...record.data, status: validStatus }
285
329
  const content = formatMarkdownDocument(data, parsed.body)
286
330
  yield* fs.writeFile(record.path, content)
287
331
  return {
@@ -73,6 +73,64 @@ pr: null
73
73
  expect(report.issues).toEqual([])
74
74
  })
75
75
 
76
+ test("validates non-PR completion invariants without rejecting legacy done work", async () => {
77
+ await write(
78
+ root,
79
+ "agency.json",
80
+ JSON.stringify({
81
+ version: 2,
82
+ repositories: {
83
+ agency: { remote: "https://example.com/agency.git" },
84
+ },
85
+ }),
86
+ )
87
+ await write(
88
+ root,
89
+ "tasks/invalid/TASK.md",
90
+ `---
91
+ ticketUrl: null
92
+ repo: agency
93
+ branch: task/invalid
94
+ base: main
95
+ pr: https://github.com/example/agency/pull/1
96
+ status: working
97
+ completion:
98
+ mode: non-pr
99
+ completedAt: 2026-07-23T18:00:00.000Z
100
+ summary: Investigation completed.
101
+ ---
102
+ `,
103
+ )
104
+ await write(
105
+ root,
106
+ "tasks/legacy/TASK.md",
107
+ `---
108
+ ticketUrl: null
109
+ repo: agency
110
+ branch: task/legacy
111
+ base: main
112
+ pr: null
113
+ status: done
114
+ ---
115
+ `,
116
+ )
117
+
118
+ const report = await runTestEffect(
119
+ WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
120
+ )
121
+ expect(report.issues).toContainEqual({
122
+ path: "tasks/invalid/TASK.md",
123
+ message: "Non-PR completion requires status 'done'",
124
+ })
125
+ expect(report.issues).toContainEqual({
126
+ path: "tasks/invalid/TASK.md",
127
+ message: "Non-PR completion cannot have a recorded pull request",
128
+ })
129
+ expect(
130
+ report.issues.some((issue) => issue.path === "tasks/legacy/TASK.md"),
131
+ ).toBe(false)
132
+ })
133
+
76
134
  test("registers canonical workbase paths without duplicates", async () => {
77
135
  const workbaseRoot = join(root, "workbase")
78
136
  const nested = join(workbaseRoot, "nested")