@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.d2070876

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 (177) hide show
  1. package/dist/headless.js +2 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +24 -4
  4. package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
  5. package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
  8. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
  9. package/dist/resources/extensions/gsd/auto-start.js +29 -9
  10. package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
  11. package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
  12. package/dist/resources/extensions/gsd/auto.js +15 -5
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
  14. package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
  15. package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
  16. package/dist/resources/extensions/gsd/db/engine.js +5 -2
  17. package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
  18. package/dist/resources/extensions/gsd/error-classifier.js +12 -2
  19. package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
  20. package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
  21. package/dist/resources/extensions/gsd/gsd-db.js +75 -31
  22. package/dist/resources/extensions/gsd/guided-flow.js +2 -2
  23. package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
  24. package/dist/resources/extensions/gsd/paths.js +17 -7
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  26. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
  27. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
  28. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
  29. package/dist/resources/extensions/gsd/stop-notice.js +9 -0
  30. package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
  31. package/dist/resources/extensions/shared/interview-ui.js +43 -6
  32. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  61. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  62. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  63. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/package.json +2 -1
  69. package/packages/cloud-mcp-gateway/package.json +2 -2
  70. package/packages/contracts/package.json +1 -1
  71. package/packages/daemon/package.json +4 -4
  72. package/packages/gsd-agent-core/package.json +5 -5
  73. package/packages/gsd-agent-modes/package.json +7 -7
  74. package/packages/gsd-cloud/README.md +9 -3
  75. package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
  76. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
  77. package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
  78. package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
  79. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  80. package/packages/gsd-cloud/dist/cli.js +121 -15
  81. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  82. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  83. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  84. package/packages/gsd-cloud/dist/cloud-config.js +7 -1
  85. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  86. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
  87. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  88. package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
  89. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  90. package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
  91. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
  92. package/packages/gsd-cloud/dist/runtime-process.js +272 -0
  93. package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
  94. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
  95. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
  96. package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
  97. package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
  98. package/packages/gsd-cloud/package.json +2 -2
  99. package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
  100. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  101. package/packages/mcp-server/dist/cli-runner.js +119 -33
  102. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  103. package/packages/mcp-server/package.json +4 -4
  104. package/packages/native/package.json +1 -1
  105. package/packages/pi-agent-core/package.json +1 -1
  106. package/packages/pi-ai/dist/models.generated.d.ts +17 -0
  107. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.js +17 -0
  109. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
  112. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  113. package/packages/pi-ai/package.json +1 -1
  114. package/packages/pi-coding-agent/dist/core/skills.d.ts +5 -0
  115. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/skills.js +54 -0
  117. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  118. package/packages/pi-coding-agent/package.json +7 -7
  119. package/packages/pi-tui/package.json +2 -2
  120. package/packages/rpc-client/package.json +2 -2
  121. package/pkg/package.json +1 -1
  122. package/src/resources/extensions/gsd/auto/loop.ts +48 -4
  123. package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
  124. package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  126. package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
  127. package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
  128. package/src/resources/extensions/gsd/auto-start.ts +31 -8
  129. package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
  130. package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
  131. package/src/resources/extensions/gsd/auto.ts +20 -7
  132. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
  133. package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
  134. package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
  135. package/src/resources/extensions/gsd/db/engine.ts +5 -2
  136. package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
  137. package/src/resources/extensions/gsd/error-classifier.ts +13 -2
  138. package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
  139. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
  140. package/src/resources/extensions/gsd/gsd-db.ts +85 -33
  141. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  142. package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
  143. package/src/resources/extensions/gsd/paths.ts +17 -7
  144. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
  145. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
  146. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
  147. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
  148. package/src/resources/extensions/gsd/stop-notice.ts +11 -0
  149. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
  150. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
  151. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
  152. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
  153. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
  154. package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
  155. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
  156. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
  157. package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
  158. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
  159. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
  160. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
  161. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
  162. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
  163. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
  164. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
  165. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
  166. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  167. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
  168. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
  169. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
  170. package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
  171. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  172. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
  173. package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
  174. package/src/resources/extensions/shared/interview-ui.ts +47 -8
  175. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
  176. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_buildManifest.js +0 -0
  177. /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_ssgManifest.js +0 -0
@@ -8,9 +8,81 @@
8
8
  // renderAllFromDb pass re-projects; the write-time invalidation hook then
9
9
  // refreshes the marker entry, closing the loop.
10
10
  import { existsSync, readFileSync } from "node:fs";
11
- import { join } from "node:path";
11
+ import { basename, join } from "node:path";
12
12
  import { computeProjectionSha, readCompatMarker, writeCompatMarker, } from "../../compat/compat-marker.js";
13
+ import { _getAdapter } from "../../gsd-db.js";
13
14
  import { logWarning } from "../../workflow-logger.js";
15
+ function deepestProjectionScope(entities) {
16
+ let best = null;
17
+ for (const entity of entities) {
18
+ const parts = entity.split("/").filter(Boolean);
19
+ if (parts.length === 0)
20
+ continue;
21
+ if (!best || parts.length > best.length)
22
+ best = parts;
23
+ }
24
+ if (!best)
25
+ return null;
26
+ return {
27
+ milestoneId: best[0],
28
+ sliceId: best[1] ?? null,
29
+ taskId: best[2] ?? null,
30
+ };
31
+ }
32
+ function artifactTypeFromProjectionPath(projectionPath) {
33
+ const name = basename(projectionPath);
34
+ const match = name.match(/^(?:[A-Za-z]\d+|\d+(?:-\d+)?)-(.+)\.md$/);
35
+ return match?.[1]?.toUpperCase() ?? null;
36
+ }
37
+ function dbProjectionMatches(projectionPath, entry, actualSha) {
38
+ const adapter = _getAdapter();
39
+ if (!adapter)
40
+ return false;
41
+ try {
42
+ const rows = adapter
43
+ .prepare("SELECT full_content FROM artifacts WHERE path = :path")
44
+ .all({ ":path": projectionPath });
45
+ const scope = deepestProjectionScope(entry.entities);
46
+ const artifactType = artifactTypeFromProjectionPath(projectionPath);
47
+ if (scope?.taskId) {
48
+ rows.push(...adapter
49
+ .prepare(`SELECT full_content
50
+ FROM artifacts
51
+ WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid
52
+ AND (:type IS NULL OR artifact_type = :type)`)
53
+ .all({
54
+ ":mid": scope.milestoneId,
55
+ ":sid": scope.sliceId,
56
+ ":tid": scope.taskId,
57
+ ":type": artifactType,
58
+ }));
59
+ }
60
+ else if (scope?.sliceId) {
61
+ rows.push(...adapter
62
+ .prepare(`SELECT full_content
63
+ FROM artifacts
64
+ WHERE milestone_id = :mid AND slice_id = :sid AND task_id IS NULL
65
+ AND (:type IS NULL OR artifact_type = :type)`)
66
+ .all({
67
+ ":mid": scope.milestoneId,
68
+ ":sid": scope.sliceId,
69
+ ":type": artifactType,
70
+ }));
71
+ }
72
+ else if (scope) {
73
+ rows.push(...adapter
74
+ .prepare(`SELECT full_content
75
+ FROM artifacts
76
+ WHERE milestone_id = :mid AND slice_id IS NULL AND task_id IS NULL
77
+ AND (:type IS NULL OR artifact_type = :type)`)
78
+ .all({ ":mid": scope.milestoneId, ":type": artifactType }));
79
+ }
80
+ return rows.some((row) => computeProjectionSha(row.full_content) === actualSha);
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
14
86
  /**
15
87
  * Detect sha drift between the marker baseline and current file contents.
16
88
  *
@@ -21,11 +93,15 @@ import { logWarning } from "../../workflow-logger.js";
21
93
  * - Sha mismatch → one record per drifted file, scoped to its recorded entities.
22
94
  */
23
95
  function detectExternalMarkdownEdit(_state, ctx) {
24
- const marker = readCompatMarker(ctx.basePath);
96
+ const marker = readCompatMarker(ctx.basePath, {
97
+ healInvalidKeys: !ctx.dryRun,
98
+ quarantineInvalid: !ctx.dryRun,
99
+ });
25
100
  const entries = Object.entries(marker.projections);
26
101
  if (entries.length === 0)
27
102
  return [];
28
103
  const records = [];
104
+ let markerChanged = false;
29
105
  for (const [projectionPath, entry] of entries) {
30
106
  const abs = join(ctx.basePath, ".gsd", projectionPath);
31
107
  if (!existsSync(abs))
@@ -33,6 +109,21 @@ function detectExternalMarkdownEdit(_state, ctx) {
33
109
  const actual = computeProjectionSha(readFileSync(abs, "utf-8"));
34
110
  if (actual === entry.sha)
35
111
  continue;
112
+ if (dbProjectionMatches(projectionPath, entry, actual)) {
113
+ if (!ctx.dryRun) {
114
+ marker.projections[projectionPath] = {
115
+ sha: actual,
116
+ entities: entry.entities,
117
+ };
118
+ markerChanged = true;
119
+ logWarning("reconcile", `external-markdown-edit: refreshed stale compat marker for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`);
120
+ }
121
+ else {
122
+ logWarning("reconcile", `external-markdown-edit: stale compat marker for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`);
123
+ }
124
+ continue;
125
+ }
126
+ logWarning("reconcile", `external-markdown-edit drift for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`);
36
127
  records.push({
37
128
  kind: "external-markdown-edit",
38
129
  projectionPath,
@@ -41,6 +132,11 @@ function detectExternalMarkdownEdit(_state, ctx) {
41
132
  entities: entry.entities,
42
133
  });
43
134
  }
135
+ if (markerChanged) {
136
+ marker.lastWriter = "gsd-pi";
137
+ marker.lastProjectedAt = new Date().toISOString();
138
+ writeCompatMarker(ctx.basePath, marker);
139
+ }
44
140
  return records;
45
141
  }
46
142
  /**
@@ -55,7 +55,10 @@ function detectUnseededPlanningFiles(ctx, projections, passthrough) {
55
55
  return records;
56
56
  }
57
57
  async function detectExternalPlanningEdit(_state, ctx) {
58
- const marker = readCompatMarker(ctx.basePath);
58
+ const marker = readCompatMarker(ctx.basePath, {
59
+ healInvalidKeys: !ctx.dryRun,
60
+ quarantineInvalid: !ctx.dryRun,
61
+ });
59
62
  if (!marker.planning?.active) {
60
63
  // Not yet activated. Activation (layout parse + DB import) is owned by
61
64
  // capturePlanningCompatIfNeeded, called from reconcileBeforeDispatch when
@@ -9,7 +9,7 @@ import { renderRoadmapFromDb } from "../../markdown-renderer.js";
9
9
  import { findMilestoneIds } from "../../milestone-ids.js";
10
10
  import { parseRoadmap } from "../../parsers-legacy.js";
11
11
  import { resolveMilestoneFile } from "../../paths.js";
12
- import { isClosedStatus } from "../../status-guards.js";
12
+ import { isClosedStatus, isSkippedForDispatch } from "../../status-guards.js";
13
13
  function arraysEqual(a, b) {
14
14
  if (a.length !== b.length)
15
15
  return false;
@@ -76,7 +76,10 @@ export function detectRoadmapDivergenceDrift(_state, ctx) {
76
76
  for (const milestoneId of findMilestoneIds(ctx.basePath)) {
77
77
  // Skip milestones that don't yet have a DB row — that's the
78
78
  // unregistered-milestone drift handler's responsibility.
79
- if (!getMilestone(milestoneId))
79
+ const milestone = getMilestone(milestoneId);
80
+ if (!milestone)
81
+ continue;
82
+ if (isSkippedForDispatch(milestone.status))
80
83
  continue;
81
84
  if (milestoneHasDivergence(ctx.basePath, milestoneId)) {
82
85
  drifts.push({ kind: "roadmap-divergence", milestoneId });
@@ -22,6 +22,12 @@ export function formatStopNoticePrefix(reason) {
22
22
  const prefix = stopNoticeKind(reason) === "blocked" ? "Auto-mode blocked" : "Auto-mode stopped";
23
23
  return displayReason ? `${prefix} — ${displayReason}` : prefix;
24
24
  }
25
+ export function formatVerdictRecordedNotice(message) {
26
+ return `Verdict recorded: ${message}`;
27
+ }
28
+ export function formatVerdictRejectedNotice(message) {
29
+ return `Verdict rejected: ${message}`;
30
+ }
25
31
  // ─── Classification (headless host side) ────────────────────────────────
26
32
  // The canonical lowercase prefixes the headless event loop recognizes in
27
33
  // notify messages. Emitters above and ad-hoc emitters elsewhere must start
@@ -33,6 +39,8 @@ export const TERMINAL_NOTICE_PREFIXES = [
33
39
  "auto-mode complete",
34
40
  "no active milestone",
35
41
  "auto-mode idle",
42
+ "verdict recorded",
43
+ "verdict rejected",
36
44
  ];
37
45
  /** Manual-resolution notices emitted before auto-mode can formally pause/stop. */
38
46
  export function isManualResolutionNotice(message) {
@@ -65,6 +73,7 @@ export function isInteractiveMenuUnavailableNotice(message) {
65
73
  }
66
74
  export function isBlockedNoticeMessage(message) {
67
75
  return (message.includes("blocked:") ||
76
+ message.startsWith("verdict rejected") ||
68
77
  (isPauseNotice(message) && !isNonBlockingPauseNotice(message)) ||
69
78
  isManualResolutionNotice(message) ||
70
79
  isInteractiveMenuUnavailableNotice(message));
@@ -21,7 +21,7 @@
21
21
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSync, } from "node:fs";
22
22
  import { join } from "node:path";
23
23
  import { reconcileWorktreeDb } from "./gsd-db.js";
24
- import { resolveGsdPathContract } from "./paths.js";
24
+ import { dirIsContentBearingLegacyMilestone, resolveGsdPathContract } from "./paths.js";
25
25
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
26
26
  import { logError, logWarning } from "./workflow-logger.js";
27
27
  // ─── Private helpers ──────────────────────────────────────────────────────
@@ -129,6 +129,8 @@ function syncOtherMilestoneArtifacts(srcMilestonesDir, dstMilestonesDir, current
129
129
  if (milestoneEntry.name === currentMilestoneId)
130
130
  continue;
131
131
  const srcMilestoneDir = join(srcMilestonesDir, milestoneEntry.name);
132
+ if (!dirIsContentBearingLegacyMilestone(srcMilestoneDir))
133
+ continue;
132
134
  const dstMilestoneDir = join(dstMilestonesDir, milestoneEntry.name);
133
135
  // Additively project the entire milestone subtree (force:false), not just
134
136
  // top-level files. Prior completed milestones keep their per-slice and
@@ -219,25 +221,25 @@ export function _projectRootToWorktreeImpl(projectRoot, worktreePath_, milestone
219
221
  // Without this, worktree-local files (e.g. VALIDATION.md written
220
222
  // by validate-milestone) get clobbered by stale project root copies,
221
223
  // causing an infinite re-validation loop (#1886).
222
- safeCopyRecursive(join(prGsd, "milestones", milestoneId), join(wtGsd, "milestones", milestoneId), { force: false });
223
- // Additively project the full subtree of every OTHER milestone so
224
- // worktree-bound units can read prior-milestone context artifacts and so
225
- // completed milestones retain their per-slice/per-task SUMMARY.md and
226
- // UAT.md on the worktree filesystem (otherwise the stale-render detector
227
- // flags them as "complete in DB but missing on disk").
228
- syncOtherMilestoneArtifacts(join(prGsd, "milestones"), join(wtGsd, "milestones"), milestoneId);
229
- // Force-sync ASSESSMENT files that have a verdict from project root (#2821).
230
- // The additive-only copy above preserves worktree-local files, but
231
- // ASSESSMENT files are special: after run-uat writes a verdict and post-unit
232
- // syncs it to the project root, the worktree may retain a stale copy (e.g.
233
- // verdict:fail while the project root has verdict:pass from a retry). On
234
- // session resume the DB is rebuilt from disk, and if the stale ASSESSMENT
235
- // persists, checkNeedsRunUat finds no passing verdict → re-dispatches
236
- // run-uat indefinitely (stuck-loop ×9).
237
224
  const prMilestoneDir = join(prGsd, "milestones", milestoneId);
238
225
  const wtMilestoneDir = join(wtGsd, "milestones", milestoneId);
239
- forceOverwriteValidationWithVerdict(prMilestoneDir, wtMilestoneDir, milestoneId);
240
- forceOverwriteAssessmentsWithVerdict(prMilestoneDir, wtMilestoneDir);
226
+ if (dirIsContentBearingLegacyMilestone(prMilestoneDir)) {
227
+ safeCopyRecursive(prMilestoneDir, wtMilestoneDir, { force: false });
228
+ // Force-sync ASSESSMENT files that have a verdict from project root (#2821).
229
+ // The additive-only copy above preserves worktree-local files, but
230
+ // ASSESSMENT files are special: after run-uat writes a verdict and post-unit
231
+ // syncs it to the project root, the worktree may retain a stale copy (e.g.
232
+ // verdict:fail while the project root has verdict:pass from a retry). On
233
+ // session resume the DB is rebuilt from disk, and if the stale ASSESSMENT
234
+ // persists, checkNeedsRunUat finds no passing verdict → re-dispatches
235
+ // run-uat indefinitely (stuck-loop ×9).
236
+ forceOverwriteValidationWithVerdict(prMilestoneDir, wtMilestoneDir, milestoneId);
237
+ forceOverwriteAssessmentsWithVerdict(prMilestoneDir, wtMilestoneDir);
238
+ }
239
+ // Additively project the full subtree of every OTHER content-bearing legacy
240
+ // milestone so worktree-bound units can read prior-milestone context artifacts
241
+ // without recreating empty/meta-only milestones/ scaffolds in flat-phase projects.
242
+ syncOtherMilestoneArtifacts(join(prGsd, "milestones"), join(wtGsd, "milestones"), milestoneId);
241
243
  // Forward-sync completed-units.json from project root to worktree.
242
244
  // Project root is authoritative for completion state after crash recovery;
243
245
  // without this, the worktree re-dispatches already-completed units (#1886).
@@ -144,6 +144,10 @@ export async function showInterviewRound(questions, opts, ctx) {
144
144
  let previewViewport = 0;
145
145
  let previewTotal = 0;
146
146
  function resetPreviewScroll() { previewScroll = 0; }
147
+ let optionsScroll = 0;
148
+ let optionsViewport = 0;
149
+ let optionsTotal = 0;
150
+ function resetOptionsScroll() { optionsScroll = 0; }
147
151
  function finish(result) {
148
152
  if (completed)
149
153
  return;
@@ -203,6 +207,7 @@ export async function showInterviewRound(questions, opts, ctx) {
203
207
  saveEditorToState();
204
208
  currentIdx = newIdx;
205
209
  resetPreviewScroll();
210
+ resetOptionsScroll();
206
211
  loadStateToEditor();
207
212
  focusNotes = states[currentIdx].notesVisible && states[currentIdx].notes.length > 0;
208
213
  refresh();
@@ -376,32 +381,49 @@ export async function showInterviewRound(questions, opts, ctx) {
376
381
  return;
377
382
  }
378
383
  }
379
- // ── Preview scrolling ────────────────────────────────────────
384
+ // ── Side-by-side panel scrolling ─────────────────────────────
380
385
  if (matchesKey(data, Key.pageUp)) {
386
+ let didScroll = false;
387
+ if (optionsScroll > 0) {
388
+ optionsScroll = Math.max(0, optionsScroll - optionsViewport);
389
+ didScroll = true;
390
+ }
381
391
  if (previewScroll > 0) {
382
392
  previewScroll = Math.max(0, previewScroll - previewViewport);
383
- refresh();
393
+ didScroll = true;
384
394
  }
395
+ if (didScroll)
396
+ refresh();
385
397
  return;
386
398
  }
387
399
  if (matchesKey(data, Key.pageDown)) {
400
+ let didScroll = false;
401
+ const maxOptionsScroll = Math.max(0, optionsTotal - optionsViewport);
402
+ if (optionsScroll < maxOptionsScroll) {
403
+ optionsScroll = Math.min(maxOptionsScroll, optionsScroll + optionsViewport);
404
+ didScroll = true;
405
+ }
388
406
  const maxScroll = Math.max(0, previewTotal - previewViewport);
389
407
  if (previewScroll < maxScroll) {
390
408
  previewScroll = Math.min(maxScroll, previewScroll + previewViewport);
391
- refresh();
409
+ didScroll = true;
392
410
  }
411
+ if (didScroll)
412
+ refresh();
393
413
  return;
394
414
  }
395
415
  // ── Cursor navigation ────────────────────────────────────────
396
416
  if (matchesKey(data, Key.up)) {
397
417
  st.cursorIndex = (st.cursorIndex - 1 + optCount) % optCount;
398
418
  resetPreviewScroll();
419
+ resetOptionsScroll();
399
420
  refresh();
400
421
  return;
401
422
  }
402
423
  if (matchesKey(data, Key.down)) {
403
424
  st.cursorIndex = (st.cursorIndex + 1) % optCount;
404
425
  resetPreviewScroll();
426
+ resetOptionsScroll();
405
427
  refresh();
406
428
  return;
407
429
  }
@@ -637,6 +659,8 @@ export async function showInterviewRound(questions, opts, ctx) {
637
659
  // No scrollable preview unless the side-by-side path sets these below.
638
660
  previewTotal = 0;
639
661
  previewViewport = 0;
662
+ optionsTotal = 0;
663
+ optionsViewport = 0;
640
664
  if (useSideBySide) {
641
665
  // ── Preview path ──────────────────────────────────────
642
666
  const ui = makeUI(theme, contentWidth);
@@ -672,10 +696,21 @@ export async function showInterviewRound(questions, opts, ctx) {
672
696
  const previewWidth = Math.max(MIN_PREVIEW_WIDTH, Math.min(contentWidth - MIN_OPTIONS_WIDTH - DIVIDER_WIDTH, Math.floor(contentWidth * PREVIEW_RATIO)));
673
697
  const leftWidth = Math.max(MIN_OPTIONS_WIDTH, contentWidth - previewWidth - DIVIDER_WIDTH);
674
698
  const fullLeft = renderOptionsColumn(leftWidth);
675
- const leftLines = fullLeft.slice(0, maxBody);
699
+ optionsTotal = fullLeft.length;
700
+ optionsViewport = maxBody;
701
+ const maxOptionsScroll = Math.max(0, fullLeft.length - maxBody);
702
+ if (optionsScroll > maxOptionsScroll)
703
+ optionsScroll = maxOptionsScroll;
704
+ const leftLines = fullLeft.slice(optionsScroll, optionsScroll + maxBody);
676
705
  if (fullLeft.length > maxBody) {
677
- const n = fullLeft.length - maxBody + 1;
678
- leftLines[maxBody - 1] = scrollIndicator(`+${n} lines hidden`, leftWidth);
706
+ const hiddenAbove = optionsScroll;
707
+ const hiddenBelow = fullLeft.length - (optionsScroll + maxBody);
708
+ if (hiddenAbove > 0) {
709
+ leftLines[0] = scrollIndicator(`▲ ${hiddenAbove} more`, leftWidth);
710
+ }
711
+ if (hiddenBelow > 0) {
712
+ leftLines[leftLines.length - 1] = scrollIndicator(`▼ ${hiddenBelow} more · PgUp/PgDn`, leftWidth);
713
+ }
679
714
  }
680
715
  const preview = getCurrentPreview();
681
716
  const fullRight = preview ? renderPreviewColumn(preview, previewWidth) : [];
@@ -734,6 +769,8 @@ export async function showInterviewRound(questions, opts, ctx) {
734
769
  }
735
770
  if (previewTotal > previewViewport)
736
771
  hints.push("pgup/pgdn scroll preview");
772
+ if (optionsTotal > optionsViewport)
773
+ hints.push("pgup/pgdn scroll options");
737
774
  hints.push("esc to exit");
738
775
  const footer = ui.hints(hints)[0] ?? "";
739
776
  cachedLines = renderSharedDialogFrame(theme, title, lines, width, { footer });