@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd

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 (135) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
  3. package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
  4. package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
  5. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  6. package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  8. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  9. package/dist/resources/extensions/gsd/auto.js +10 -3
  10. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  11. package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
  12. package/dist/resources/extensions/gsd/gsd-db.js +52 -0
  13. package/dist/resources/extensions/gsd/migrate-external.js +13 -4
  14. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
  15. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  16. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  17. package/dist/resources/extensions/gsd/unit-registry.js +27 -2
  18. package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
  19. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  20. package/dist/web/standalone/.next/BUILD_ID +1 -1
  21. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  22. package/dist/web/standalone/.next/build-manifest.json +2 -2
  23. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  24. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.html +1 -1
  41. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  48. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  50. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  51. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  52. package/integrations/hermes/CONTEXT.md +47 -0
  53. package/integrations/hermes/README.md +21 -0
  54. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  55. package/integrations/hermes/docs/setup.md +4 -0
  56. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  57. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  58. package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
  59. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  60. package/integrations/hermes/plugin.yaml +4 -0
  61. package/integrations/hermes/tests/test_config.py +6 -0
  62. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  63. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  64. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  65. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  66. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  67. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  68. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  69. package/package.json +1 -1
  70. package/packages/cloud-mcp-gateway/package.json +2 -2
  71. package/packages/contracts/package.json +1 -1
  72. package/packages/daemon/dist/session-manager.js +1 -1
  73. package/packages/daemon/dist/session-manager.js.map +1 -1
  74. package/packages/daemon/package.json +4 -4
  75. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  76. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  77. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  78. package/packages/gsd-agent-core/package.json +5 -5
  79. package/packages/gsd-agent-modes/package.json +7 -7
  80. package/packages/mcp-server/dist/server.d.ts +18 -5
  81. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  82. package/packages/mcp-server/dist/server.js +46 -5
  83. package/packages/mcp-server/dist/server.js.map +1 -1
  84. package/packages/mcp-server/dist/session-manager.js +1 -1
  85. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  86. package/packages/mcp-server/package.json +4 -4
  87. package/packages/native/package.json +1 -1
  88. package/packages/pi-agent-core/package.json +1 -1
  89. package/packages/pi-ai/package.json +1 -1
  90. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  91. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  95. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  96. package/packages/pi-coding-agent/package.json +7 -7
  97. package/packages/pi-tui/package.json +2 -2
  98. package/packages/rpc-client/package.json +2 -2
  99. package/pkg/package.json +1 -1
  100. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
  101. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
  102. package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
  103. package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
  104. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  105. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  106. package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
  107. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  108. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  109. package/src/resources/extensions/gsd/auto.ts +9 -3
  110. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  111. package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
  112. package/src/resources/extensions/gsd/gsd-db.ts +72 -0
  113. package/src/resources/extensions/gsd/migrate-external.ts +14 -4
  114. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
  115. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  116. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  117. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  118. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  119. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  120. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
  121. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  123. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
  124. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
  125. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  126. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  127. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  128. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  129. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
  130. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  131. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  132. package/src/resources/extensions/gsd/unit-registry.ts +34 -2
  133. package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
  134. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
  135. /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
@@ -355,6 +355,23 @@ export function externalProjectsRoot(): string {
355
355
  return join(base, "projects");
356
356
  }
357
357
 
358
+ /**
359
+ * Check whether this project already has authoritative external state.
360
+ *
361
+ * Used by legacy `.gsd/` migration to avoid re-copying a re-materialized local
362
+ * directory over newer state when an already-migrated symlink was replaced.
363
+ */
364
+ export function externalStateAlreadyExistsForProject(basePath: string): boolean {
365
+ const base = process.env.GSD_STATE_DIR || gsdHome();
366
+ const computedPath = join(base, "projects", repoIdentity(basePath));
367
+ if (hasProjectState(computedPath)) return true;
368
+
369
+ const markerId = readGsdIdMarker(resolveGitRoot(basePath));
370
+ if (!markerId) return false;
371
+
372
+ return hasProjectState(join(base, "projects", markerId));
373
+ }
374
+
358
375
  // ─── Numbered Variant Cleanup ────────────────────────────────────────────────
359
376
 
360
377
  /**
@@ -65,11 +65,12 @@ export function captureRootDirtySnapshot(rootPath: string): RootDirtySnapshot {
65
65
  for (const line of status.split("\n")) {
66
66
  if (!line.trim()) continue;
67
67
  const code = line.slice(0, 2);
68
+ if (code !== "??") continue;
68
69
  const path = line.slice(3).replace(/^"|"$/g, "");
69
70
  if (!path || isRootRuntimePath(path)) continue;
70
71
  snapshot.set(path, {
71
72
  path,
72
- status: code.trim() || code,
73
+ status: code,
73
74
  fingerprint: fileFingerprint(rootPath, path),
74
75
  });
75
76
  }
@@ -87,7 +88,7 @@ export function detectRootWriteLeak(input: {
87
88
  const leaked: RootDirtyEntry[] = [];
88
89
  for (const entry of after.values()) {
89
90
  const prior = input.before?.get(entry.path);
90
- if (!prior || prior.status !== entry.status || prior.fingerprint !== entry.fingerprint) {
91
+ if (!prior || prior.fingerprint !== entry.fingerprint) {
91
92
  leaked.push(entry);
92
93
  }
93
94
  }
@@ -22,7 +22,7 @@ import {
22
22
  isSessionSwitchAbortGraceActive,
23
23
  } from "../auto/resolve.js";
24
24
  import { runUnit, shouldDeferUnitFailsafeTimeout } from "../auto/run-unit.js";
25
- import { scheduleAutoWakeup, _resetAutoWakeupsForTest } from "../auto/schedule-wakeup.js";
25
+ import { consumeAutoWakeup, scheduleAutoWakeup, _resetAutoWakeupsForTest } from "../auto/schedule-wakeup.js";
26
26
  import { writeUnitRuntimeRecord, readUnitRuntimeRecord } from "../unit-runtime.js";
27
27
  import { autoLoop as rawAutoLoop } from "../auto/loop.js";
28
28
  import { runPreDispatch } from "../auto/pre-dispatch.js";
@@ -67,6 +67,76 @@ async function drainMicrotasks(turns = 20): Promise<void> {
67
67
  }
68
68
  }
69
69
 
70
+ function guardedExitMilestoneForTest(
71
+ merge: (milestoneId: string, opts: { merge: boolean }) => {
72
+ ok: boolean;
73
+ merged?: boolean;
74
+ codeFilesChanged?: boolean;
75
+ reason?: string;
76
+ cause?: unknown;
77
+ },
78
+ ) {
79
+ return (
80
+ milestoneId: string,
81
+ opts: {
82
+ merge: boolean;
83
+ guardedMerge?: {
84
+ projectRoot: string;
85
+ preflightCleanRoot: (
86
+ basePath: string,
87
+ milestoneId: string,
88
+ notify: (message: string, level: "info" | "warning" | "error") => void,
89
+ ) => { blocked?: boolean; blockedReason?: string; stashPushed: boolean; stashMarker?: string };
90
+ postflightPopStash: (
91
+ basePath: string,
92
+ milestoneId: string,
93
+ stashMarker: string | undefined,
94
+ notify: (message: string, level: "info" | "warning" | "error") => void,
95
+ ) => { needsManualRecovery?: boolean };
96
+ };
97
+ },
98
+ ctx?: { notify: (message: string, level: "info" | "warning" | "error") => void },
99
+ ) => {
100
+ const notify = ctx?.notify ?? (() => {});
101
+ const guard = opts.guardedMerge;
102
+ if (opts.merge && guard) {
103
+ const preflight = guard.preflightCleanRoot(guard.projectRoot, milestoneId, notify);
104
+ if (preflight.blocked) {
105
+ return {
106
+ ok: false,
107
+ reason: preflight.blockedReason?.startsWith("unmerged-conflicts")
108
+ ? "preflight-unmerged-conflicts"
109
+ : "preflight-dirty-overlap",
110
+ };
111
+ }
112
+
113
+ const mergeResult = merge(milestoneId, { merge: true });
114
+ const postflight = preflight.stashPushed
115
+ ? guard.postflightPopStash(
116
+ guard.projectRoot,
117
+ milestoneId,
118
+ preflight.stashMarker,
119
+ notify,
120
+ )
121
+ : undefined;
122
+
123
+ if (!mergeResult.ok) {
124
+ return {
125
+ ok: false,
126
+ reason: mergeResult.reason === "merge-conflict" ? "merge-conflict" : "merge-failed",
127
+ cause: mergeResult.cause,
128
+ postflight,
129
+ };
130
+ }
131
+ if (postflight?.needsManualRecovery) {
132
+ return { ok: false, reason: "postflight-stash-restore-failed", postflight };
133
+ }
134
+ return mergeResult;
135
+ }
136
+ return merge(milestoneId, opts);
137
+ };
138
+ }
139
+
70
140
  async function waitForMicrotasks(
71
141
  condition: () => boolean,
72
142
  label: string,
@@ -482,6 +552,62 @@ test("runUnit honors ScheduleWakeup by continuing the same unit session", async
482
552
  assert.equal(pi.calls.length, 2);
483
553
  });
484
554
 
555
+ test("runUnit clears scheduled wakeups when the unit is cancelled", async () => {
556
+ _resetPendingResolve();
557
+ _resetAutoWakeupsForTest();
558
+
559
+ const ctx = {
560
+ ...makeMockCtx(),
561
+ ui: {
562
+ notify: () => {},
563
+ setStatus: () => {},
564
+ setWorkingMessage: () => {},
565
+ },
566
+ sessionManager: {
567
+ getEntries: () => [],
568
+ },
569
+ modelRegistry: {
570
+ getProviderAuthMode: () => undefined,
571
+ isProviderRequestReady: () => true,
572
+ },
573
+ } as any;
574
+ const pi = makeMockPi();
575
+ const s = makeMockSession();
576
+
577
+ const resultPromise = runUnit(
578
+ ctx,
579
+ pi,
580
+ s,
581
+ "execute-task",
582
+ "M001/S01/T02",
583
+ "submit external job",
584
+ );
585
+
586
+ await waitForMicrotasks(() => pi.calls.length === 1, "initial unit dispatch");
587
+ scheduleAutoWakeup({
588
+ basePath: s.basePath,
589
+ unitType: "execute-task",
590
+ unitId: "M001/S01/T02",
591
+ delayMs: 0,
592
+ prompt: "stale prompt from cancelled unit",
593
+ reason: "poll external job",
594
+ createdAt: Date.now(),
595
+ });
596
+ resolveAgentEndCancelled({
597
+ message: "Auto-mode paused",
598
+ category: "aborted",
599
+ isTransient: true,
600
+ });
601
+
602
+ const result = await resultPromise;
603
+ assert.equal(result.status, "cancelled");
604
+ assert.equal(
605
+ consumeAutoWakeup(s.basePath, "execute-task", "M001/S01/T02"),
606
+ null,
607
+ "cancelled units must not leave stale ScheduleWakeup prompts for later retries",
608
+ );
609
+ });
610
+
485
611
  test("runUnit suppresses the global working-message loader for auto dashboard runs", async () => {
486
612
  _resetPendingResolve();
487
613
  const workingMessages: unknown[] = [];
@@ -1335,11 +1461,11 @@ function makeMockDeps(
1335
1461
  GitServiceImpl: class {} as any,
1336
1462
  lifecycle: {
1337
1463
  enterMilestone: () => ({ ok: true, mode: "worktree", path: "/tmp/project" }),
1338
- exitMilestone: (_mid: string, opts: { merge: boolean }) => ({
1464
+ exitMilestone: guardedExitMilestoneForTest((_mid, opts) => ({
1339
1465
  ok: true,
1340
1466
  merged: opts.merge,
1341
1467
  codeFilesChanged: false,
1342
- }),
1468
+ })),
1343
1469
  } as any,
1344
1470
  worktreeProjection: new WorktreeStateProjection(),
1345
1471
  postUnitPreVerification: async () => {
@@ -1762,10 +1888,10 @@ test("autoLoop marks transition merge complete before postflight recovery stop",
1762
1888
  enterMilestone: () => {
1763
1889
  assert.fail("must not enter the next milestone after postflight recovery fails");
1764
1890
  },
1765
- exitMilestone: (_mid: string, opts: { merge: boolean }) => {
1891
+ exitMilestone: guardedExitMilestoneForTest((_mid, opts) => {
1766
1892
  if (opts.merge) mergeCalls += 1;
1767
1893
  return { ok: true, merged: opts.merge, codeFilesChanged: false };
1768
- },
1894
+ }),
1769
1895
  } as any,
1770
1896
  stopAuto: async (_ctx, _pi, reason) => {
1771
1897
  deps.callLog.push("stopAuto");
@@ -61,6 +61,7 @@ async function runSuccessfulFinalize(s: AutoSession) {
61
61
  },
62
62
  stopAuto: async () => {},
63
63
  pauseAuto: async () => {},
64
+ checkpointWorkflowDatabase() {},
64
65
  updateProgressWidget() {},
65
66
  postUnitPreVerification: async () => "continue",
66
67
  runPostUnitVerification: async () => "continue",
@@ -410,9 +411,13 @@ test("runFinalize stops before merge when an isolated unit leaks app files into
410
411
  const notifications: Array<{ message: string; level?: string }> = [];
411
412
  const stopCalls: Array<{ reason?: string; preserve?: boolean }> = [];
412
413
  const mergeCalls: string[] = [];
414
+ const checkpointCalls: string[] = [];
413
415
  const result = await runFinalizeWithDeps(
414
416
  s,
415
417
  {
418
+ checkpointWorkflowDatabase() {
419
+ checkpointCalls.push("checkpoint");
420
+ },
416
421
  stopAuto: async (_ctx: unknown, _pi: unknown, reason?: string, options?: { preserveCompletedMilestoneBranch?: boolean }) => {
417
422
  stopCalls.push({ reason, preserve: options?.preserveCompletedMilestoneBranch });
418
423
  },
@@ -438,16 +443,53 @@ test("runFinalize stops before merge when an isolated unit leaks app files into
438
443
 
439
444
  assert.equal(result.action, "break");
440
445
  assert.equal(result.reason, "root-write-leak");
446
+ assert.deepEqual(checkpointCalls, ["checkpoint"], "root-write leak should flush DB before stopAuto");
441
447
  assert.deepEqual(stopCalls, [{ reason: "Root-write leak during isolated auto-mode", preserve: true }]);
442
448
  assert.deepEqual(mergeCalls, [], "root-write leak must stop before merge preflight");
443
449
  const message = notifications.find((n) => n.level === "error")?.message ?? "";
444
450
  assert.match(message, /execute-task M001\/S01\/T01/);
445
451
  assert.match(message, /Project root:/);
446
452
  assert.match(message, /Expected worktree:/);
447
- assert.match(message, /index\.html/);
453
+ assert.doesNotMatch(message, /index\.html/);
448
454
  assert.match(message, /tests\/verify-s09\.sh/);
449
455
  });
450
456
 
457
+ test("runFinalize ignores tracked root artifact changes during isolated units", async (t) => {
458
+ const root = mkdtempSync(join(tmpdir(), "gsd-root-leak-tracked-"));
459
+ const worktree = join(root, ".gsd", "worktrees", "M001");
460
+ t.after(() => {
461
+ rmSync(root, { recursive: true, force: true });
462
+ });
463
+ initRepo(root);
464
+ writeFileSync(join(root, "openapi.json"), "{}\n");
465
+ runGit(root, ["add", "openapi.json"]);
466
+ runGit(root, ["commit", "-m", "chore: add generated spec"]);
467
+ mkdirSync(worktree, { recursive: true });
468
+
469
+ const s = new AutoSession();
470
+ s.basePath = worktree;
471
+ s.originalBasePath = root;
472
+ s.currentMilestoneId = "M001";
473
+ s.rootWriteBaseline = captureRootDirtySnapshot(root);
474
+ s.currentUnit = {
475
+ type: "execute-task",
476
+ id: "M001/S01/T01",
477
+ startedAt: Date.now(),
478
+ };
479
+
480
+ writeFileSync(join(root, "openapi.json"), "{\"generated\":true}\n");
481
+
482
+ const stopCalls: string[] = [];
483
+ const result = await runFinalizeWithDeps(s, {
484
+ stopAuto: async (_ctx: unknown, _pi: unknown, reason?: string) => {
485
+ stopCalls.push(reason ?? "");
486
+ },
487
+ });
488
+
489
+ assert.equal(result.action, "next");
490
+ assert.deepEqual(stopCalls, []);
491
+ });
492
+
451
493
  test("runFinalize allows root .gsd-only changes during isolated units", async (t) => {
452
494
  const root = mkdtempSync(join(tmpdir(), "gsd-root-leak-gsd-"));
453
495
  const worktree = join(root, ".gsd", "worktrees", "M001");
@@ -838,6 +838,60 @@ test("verifyExpectedArtifact plan-slice fails when a task plan file is missing (
838
838
  }
839
839
  });
840
840
 
841
+ test("verifyExpectedArtifact accepts flat-phase plan-slice with embedded tasks and no task plan files", () => {
842
+ const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
843
+ try {
844
+ const phaseDir = join(base, ".gsd", "phases", "01-test");
845
+ mkdirSync(join(phaseDir, "tasks"), { recursive: true });
846
+ writeFileSync(join(phaseDir, "01-01-PLAN.md"), [
847
+ "# S01: Test Slice",
848
+ "",
849
+ "<tasks>",
850
+ "- [ ] **T01**: Implement feature _(1h)_",
851
+ " - Files: `src/index.ts`",
852
+ " - Verify: pnpm test",
853
+ "- [ ] **T02**: Write tests _(1h)_",
854
+ "</tasks>",
855
+ ].join("\n"));
856
+
857
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
858
+ assert.equal(result, true, "flat-phase embedded tasks should not require tasks/T##-PLAN.md files");
859
+ } finally {
860
+ cleanup(base);
861
+ }
862
+ });
863
+
864
+ test("verifyExpectedArtifact plan-slice still verifies per-task files for legacy ## Tasks plan with a stray empty <tasks> block", () => {
865
+ const base = makeTmpBase();
866
+ try {
867
+ const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
868
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
869
+ // Legacy layout: real tasks live in a "## Tasks" section with separate
870
+ // per-task PLAN files, but the plan also carries an empty <tasks></tasks>
871
+ // block. The empty block must NOT flip the plan into flat-phase mode and
872
+ // skip the per-task artifact checks.
873
+ const planContent = [
874
+ "# S01: Test Slice",
875
+ "",
876
+ "<tasks>",
877
+ "</tasks>",
878
+ "",
879
+ "## Tasks",
880
+ "",
881
+ "- [ ] **T01: First task** `est:1h`",
882
+ "- [ ] **T02: Second task** `est:2h`",
883
+ ].join("\n");
884
+ writeFileSync(planPath, planContent);
885
+ // Only T01-PLAN.md exists; T02's artifact is missing.
886
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
887
+
888
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
889
+ assert.equal(result, false, "a stray <tasks> block must not skip per-task file verification for a legacy ## Tasks plan");
890
+ } finally {
891
+ cleanup(base);
892
+ }
893
+ });
894
+
841
895
  test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", () => {
842
896
  const base = makeTmpBase();
843
897
  try {