@opengsd/gsd-pi 1.1.1-dev.9bb7453 → 1.1.1-dev.9f86580

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 (219) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +18 -2
  3. package/dist/resources/extensions/browser-tools/engine/selection.js +1 -1
  4. package/dist/resources/extensions/browser-tools/extension-manifest.json +1 -1
  5. package/dist/resources/extensions/browser-tools/index.js +29 -2
  6. package/dist/resources/extensions/browser-tools/web-app-detect.js +52 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +45 -3
  8. package/dist/resources/extensions/gsd/auto/session.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +21 -2
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +26 -0
  11. package/dist/resources/extensions/gsd/auto-prompts.js +4 -0
  12. package/dist/resources/extensions/gsd/auto-recovery.js +3 -4
  13. package/dist/resources/extensions/gsd/auto-timers.js +24 -10
  14. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +18 -66
  15. package/dist/resources/extensions/gsd/auto-worktree.js +18 -5
  16. package/dist/resources/extensions/gsd/auto.js +26 -4
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +16 -10
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +48 -29
  19. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  20. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +18 -29
  21. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +61 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -0
  23. package/dist/resources/extensions/gsd/commands-mcp-status.js +1 -1
  24. package/dist/resources/extensions/gsd/config-overlay.js +1 -0
  25. package/dist/resources/extensions/gsd/context-masker.js +129 -5
  26. package/dist/resources/extensions/gsd/guided-flow.js +93 -108
  27. package/dist/resources/extensions/gsd/milestone-closeout.js +3 -1
  28. package/dist/resources/extensions/gsd/pending-auto-start.js +0 -1
  29. package/dist/resources/extensions/gsd/planner-handoff.js +98 -0
  30. package/dist/resources/extensions/gsd/preferences-models.js +1 -0
  31. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/run-uat.md +5 -19
  33. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  34. package/dist/resources/extensions/gsd/recovery-classification.js +20 -0
  35. package/dist/resources/extensions/gsd/skill-manifest.js +12 -0
  36. package/dist/resources/extensions/gsd/tool-contract.js +6 -1
  37. package/dist/resources/extensions/gsd/tool-presentation-plan.js +47 -7
  38. package/dist/resources/extensions/gsd/tools/complete-slice.js +28 -1
  39. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +113 -8
  40. package/dist/resources/extensions/gsd/unit-tool-contracts.js +193 -0
  41. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -78
  42. package/dist/resources/extensions/gsd/worktree-manager.js +26 -0
  43. package/dist/resources/extensions/gsd/worktree-reentry.js +96 -0
  44. package/dist/resources/extensions/shared/gsd-browser-cli.js +6 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +5 -5
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +5 -5
  73. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +1 -1
  79. package/packages/cloud-mcp-gateway/package.json +2 -2
  80. package/packages/contracts/package.json +1 -1
  81. package/packages/daemon/package.json +4 -4
  82. package/packages/gsd-agent-core/package.json +5 -5
  83. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +5 -0
  85. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  86. package/packages/gsd-agent-modes/package.json +7 -7
  87. package/packages/mcp-server/package.json +3 -3
  88. package/packages/native/package.json +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -3
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/harness/agent-harness.d.ts.map +1 -1
  92. package/packages/pi-agent-core/dist/harness/agent-harness.js +3 -1
  93. package/packages/pi-agent-core/dist/harness/agent-harness.js.map +1 -1
  94. package/packages/pi-agent-core/dist/harness/types.d.ts +1 -0
  95. package/packages/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/harness/types.js.map +1 -1
  97. package/packages/pi-agent-core/dist/types.d.ts +3 -1
  98. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  99. package/packages/pi-agent-core/dist/types.js.map +1 -1
  100. package/packages/pi-agent-core/package.json +1 -1
  101. package/packages/pi-ai/dist/models.generated.d.ts +157 -18
  102. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  103. package/packages/pi-ai/dist/models.generated.js +159 -36
  104. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  105. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  106. package/packages/pi-ai/dist/providers/transform-messages.js +8 -1
  107. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  108. package/packages/pi-ai/package.json +1 -1
  109. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +3 -0
  110. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  113. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/tools/edit.js +3 -2
  116. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts +1 -0
  118. package/packages/pi-coding-agent/dist/core/tools/render-utils.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/render-utils.js +6 -0
  120. package/packages/pi-coding-agent/dist/core/tools/render-utils.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/write.js +3 -2
  123. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  124. package/packages/pi-coding-agent/package.json +7 -7
  125. package/packages/pi-tui/package.json +1 -1
  126. package/packages/rpc-client/package.json +2 -2
  127. package/pkg/package.json +1 -1
  128. package/scripts/install/handoff.js +16 -3
  129. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +21 -2
  130. package/src/resources/extensions/browser-tools/engine/selection.ts +1 -1
  131. package/src/resources/extensions/browser-tools/extension-manifest.json +1 -1
  132. package/src/resources/extensions/browser-tools/index.ts +36 -5
  133. package/src/resources/extensions/browser-tools/tests/browser-engine-selection.test.mjs +2 -2
  134. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +37 -0
  135. package/src/resources/extensions/browser-tools/tests/web-app-detect.test.mjs +68 -0
  136. package/src/resources/extensions/browser-tools/web-app-detect.ts +63 -0
  137. package/src/resources/extensions/gsd/auto/phases.ts +48 -6
  138. package/src/resources/extensions/gsd/auto/session.ts +2 -0
  139. package/src/resources/extensions/gsd/auto-dispatch.ts +48 -2
  140. package/src/resources/extensions/gsd/auto-model-selection.ts +26 -0
  141. package/src/resources/extensions/gsd/auto-prompts.ts +4 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  143. package/src/resources/extensions/gsd/auto-timers.ts +25 -9
  144. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +43 -74
  145. package/src/resources/extensions/gsd/auto-worktree.ts +23 -5
  146. package/src/resources/extensions/gsd/auto.ts +28 -4
  147. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +16 -10
  148. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +63 -29
  149. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  150. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +50 -54
  151. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +137 -0
  152. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -0
  153. package/src/resources/extensions/gsd/commands-mcp-status.ts +1 -1
  154. package/src/resources/extensions/gsd/config-overlay.ts +1 -0
  155. package/src/resources/extensions/gsd/context-masker.ts +152 -5
  156. package/src/resources/extensions/gsd/guided-flow.ts +128 -135
  157. package/src/resources/extensions/gsd/milestone-closeout.ts +3 -1
  158. package/src/resources/extensions/gsd/pending-auto-start.ts +0 -2
  159. package/src/resources/extensions/gsd/planner-handoff.ts +149 -0
  160. package/src/resources/extensions/gsd/preferences-models.ts +1 -0
  161. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  162. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/run-uat.md +5 -19
  164. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  165. package/src/resources/extensions/gsd/recovery-classification.ts +20 -0
  166. package/src/resources/extensions/gsd/skill-manifest.ts +12 -0
  167. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +99 -0
  168. package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +66 -4
  169. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +10 -2
  170. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +4 -1
  171. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +4 -0
  172. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +12 -2
  173. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +9 -0
  174. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +9 -15
  175. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +26 -16
  176. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +21 -0
  177. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +118 -0
  178. package/src/resources/extensions/gsd/tests/context-masker.test.ts +56 -1
  179. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +40 -1
  181. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +24 -0
  182. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +31 -79
  183. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +5 -3
  184. package/src/resources/extensions/gsd/tests/guided-flow-state-rebuild.test.ts +40 -4
  185. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +8 -0
  186. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +16 -0
  187. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +7 -1
  188. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +27 -0
  189. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -0
  190. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +7 -1
  191. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +63 -0
  193. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +10 -1
  194. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +9 -1
  195. package/src/resources/extensions/gsd/tests/planner-handoff.test.ts +100 -0
  196. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +147 -5
  197. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +55 -0
  198. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +44 -0
  199. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +4 -0
  200. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +56 -0
  201. package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +4 -3
  202. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -4
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +77 -10
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +409 -0
  205. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +102 -0
  206. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  207. package/src/resources/extensions/gsd/tool-contract.ts +7 -1
  208. package/src/resources/extensions/gsd/tool-presentation-plan.ts +82 -7
  209. package/src/resources/extensions/gsd/tools/complete-slice.ts +29 -1
  210. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +146 -9
  211. package/src/resources/extensions/gsd/unit-tool-contracts.ts +210 -0
  212. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -78
  213. package/src/resources/extensions/gsd/worktree-manager.ts +32 -0
  214. package/src/resources/extensions/gsd/worktree-reentry.ts +103 -0
  215. package/src/resources/extensions/shared/gsd-browser-cli.ts +6 -0
  216. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +0 -246
  217. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +0 -218
  218. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_buildManifest.js +0 -0
  219. /package/dist/web/standalone/.next/static/{jBtwT9v1u2lUA3UEOy_ZH → zzYMrKpPGfRQRxSFO32Jr}/_ssgManifest.js +0 -0
@@ -5,13 +5,36 @@
5
5
  * Reduces context bloat between compactions with zero LLM overhead.
6
6
  * Preserves message ordering, roles, and all assistant/user messages.
7
7
  *
8
- * Operates on the pi-ai Message[] format (post-convertToLlm, pre-provider):
8
+ * Operates on provider payloads after convertToLlm:
9
+ *
10
+ * pi-ai Message[] payloads:
9
11
  * - toolResult messages: { role: "toolResult", content: TextContent[] }
10
12
  * - bash results are already converted to: { role: "user", content: [{type:"text",text:"..."}] }
11
13
  * and start with "Ran `" from bashExecutionToText.
14
+ *
15
+ * OpenAI/Codex Responses payloads:
16
+ * - conversation items live in `input`, not `messages`
17
+ * - tool results are { type: "function_call_output", output: string | content[] }
18
+ * - bash results are user items with input_text content starting with "Ran `"
12
19
  */
13
20
  const MASK_PLACEHOLDER = "[result masked — within summarized history]";
14
21
  const MASK_CONTENT_BLOCK = [{ type: "text", text: MASK_PLACEHOLDER }];
22
+ const RESPONSES_MASK_CONTENT_BLOCK = [{ type: "input_text", text: MASK_PLACEHOLDER }];
23
+ const TRUNCATION_MARKER = "\n…[truncated]";
24
+ function isTextLikeBlock(block) {
25
+ return Boolean(block && typeof block === "object" && "text" in block);
26
+ }
27
+ function firstTextFromContent(content) {
28
+ if (typeof content === "string")
29
+ return content;
30
+ if (!Array.isArray(content))
31
+ return undefined;
32
+ const first = content.find(isTextLikeBlock);
33
+ return typeof first?.text === "string" ? first.text : undefined;
34
+ }
35
+ function isBashResultText(text) {
36
+ return typeof text === "string" && text.startsWith("Ran `");
37
+ }
15
38
  function findTurnBoundary(messages, keepRecentTurns) {
16
39
  let turnsSeen = 0;
17
40
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -35,11 +58,9 @@ function findTurnBoundary(messages, keepRecentTurns) {
35
58
  * The bashExecutionToText format always starts with "Ran `".
36
59
  */
37
60
  function isBashResultUserMessage(m) {
38
- if (m.role !== "user" || !Array.isArray(m.content))
61
+ if (m.role !== "user")
39
62
  return false;
40
- const first = m.content[0];
41
- return first && typeof first === "object" && "text" in first &&
42
- typeof first.text === "string" && first.text.startsWith("Ran `");
63
+ return isBashResultText(firstTextFromContent(m.content));
43
64
  }
44
65
  function isMaskableMessage(m) {
45
66
  // Tool result messages (role: "toolResult" in pi-ai format)
@@ -66,3 +87,106 @@ export function createObservationMask(keepRecentTurns = 8) {
66
87
  });
67
88
  };
68
89
  }
90
+ function isResponsesBashResultUserItem(item) {
91
+ if (item.role !== "user")
92
+ return false;
93
+ return isBashResultText(firstTextFromContent(item.content));
94
+ }
95
+ function findResponsesTurnBoundary(items, keepRecentTurns) {
96
+ let turnsSeen = 0;
97
+ for (let i = items.length - 1; i >= 0; i--) {
98
+ const item = items[i];
99
+ if (item.role === "user" && !isResponsesBashResultUserItem(item)) {
100
+ turnsSeen++;
101
+ if (turnsSeen >= keepRecentTurns)
102
+ return i;
103
+ }
104
+ }
105
+ return 0;
106
+ }
107
+ /**
108
+ * Observation masking for OpenAI/Codex Responses API payloads.
109
+ *
110
+ * Responses payloads store the conversation under `input` instead of
111
+ * `messages`, with tool results as `function_call_output` items. Keep this
112
+ * separate from createObservationMask so each payload shape stays explicit.
113
+ */
114
+ export function createResponsesInputObservationMask(keepRecentTurns = 8) {
115
+ return (items) => {
116
+ const boundary = findResponsesTurnBoundary(items, keepRecentTurns);
117
+ if (boundary === 0)
118
+ return items;
119
+ return items.map((item, i) => {
120
+ if (i >= boundary)
121
+ return item;
122
+ if (item.type === "function_call_output") {
123
+ return { ...item, output: MASK_PLACEHOLDER };
124
+ }
125
+ if (isResponsesBashResultUserItem(item)) {
126
+ return { ...item, content: RESPONSES_MASK_CONTENT_BLOCK };
127
+ }
128
+ return item;
129
+ });
130
+ };
131
+ }
132
+ function truncateText(text, maxChars) {
133
+ if (text.length <= maxChars)
134
+ return text;
135
+ return text.slice(0, maxChars) + TRUNCATION_MARKER;
136
+ }
137
+ function truncateTextBlocks(content, maxChars) {
138
+ if (typeof content === "string") {
139
+ return truncateText(content, maxChars);
140
+ }
141
+ if (!Array.isArray(content))
142
+ return content;
143
+ let remaining = maxChars;
144
+ let didTruncate = false;
145
+ const nextBlocks = [];
146
+ for (const block of content) {
147
+ if (!isTextLikeBlock(block) || typeof block.text !== "string") {
148
+ nextBlocks.push(block);
149
+ continue;
150
+ }
151
+ if (remaining <= 0) {
152
+ didTruncate = true;
153
+ continue;
154
+ }
155
+ const text = block.text;
156
+ if (text.length <= remaining) {
157
+ nextBlocks.push(block);
158
+ remaining -= text.length;
159
+ continue;
160
+ }
161
+ nextBlocks.push({ ...block, text: truncateText(text, remaining) });
162
+ remaining = 0;
163
+ didTruncate = true;
164
+ }
165
+ return didTruncate ? nextBlocks : content;
166
+ }
167
+ function normalizedMaxChars(maxChars) {
168
+ return Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : 800;
169
+ }
170
+ export function truncateContextResultMessages(messages, maxChars = 800) {
171
+ const limit = normalizedMaxChars(maxChars);
172
+ return messages.map((message) => {
173
+ if (!isMaskableMessage(message))
174
+ return message;
175
+ const content = truncateTextBlocks(message.content, limit);
176
+ return content === message.content ? message : { ...message, content };
177
+ });
178
+ }
179
+ export function truncateResponsesInputResultItems(items, maxChars = 800) {
180
+ const limit = normalizedMaxChars(maxChars);
181
+ return items.map((item) => {
182
+ if (item.type === "function_call_output") {
183
+ const output = truncateTextBlocks(item.output, limit);
184
+ return output === item.output ? item : { ...item, output };
185
+ }
186
+ if (isResponsesBashResultUserItem(item)) {
187
+ const content = truncateTextBlocks(item.content, limit);
188
+ return content === item.content ? item : { ...item, content };
189
+ }
190
+ return item;
191
+ });
192
+ }
@@ -178,6 +178,84 @@ export function _roadmapHasParseableSlicesForTest(roadmapContent) {
178
178
  return false;
179
179
  return parseRoadmapSlices(roadmapContent).length > 0;
180
180
  }
181
+ function hasExecutablePlanForHandoff(milestoneId, roadmapFile) {
182
+ if (isDbAvailable()) {
183
+ return getMilestoneSlices(milestoneId).length > 0;
184
+ }
185
+ if (!roadmapFile)
186
+ return false;
187
+ try {
188
+ return parseRoadmapSlices(readFileSync(roadmapFile, "utf-8")).length > 0;
189
+ }
190
+ catch (e) {
191
+ logWarning("guided", `failed to parse roadmap slices for ${milestoneId}: ${e.message}`);
192
+ return false;
193
+ }
194
+ }
195
+ function formatAcceptedDiscussHandoffMessage(milestoneId, contextFile, hasExecutablePlan) {
196
+ if (hasExecutablePlan)
197
+ return `Milestone ${milestoneId} ready.`;
198
+ if (contextFile)
199
+ return `Milestone ${milestoneId} context captured. Continuing the planning pipeline.`;
200
+ return `Milestone ${milestoneId} planning artifacts captured. Continuing the planning pipeline.`;
201
+ }
202
+ function manifestContainsMilestone(basePath, milestoneId) {
203
+ try {
204
+ const manifest = readManifest(basePath);
205
+ return (Array.isArray(manifest?.milestones) &&
206
+ manifest.milestones.some(m => m.id === milestoneId));
207
+ }
208
+ catch (e) {
209
+ logWarning("guided", `R3b: failed to read state manifest: ${e.message}`);
210
+ return false;
211
+ }
212
+ }
213
+ function notifyDbRowRecoveryFailed(entry) {
214
+ entry.ctx.ui.notify(`Milestone ${entry.milestoneId}: DB row recovery failed ${entry.r3bRecoveryCount} times. ` +
215
+ `Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`, "error");
216
+ }
217
+ function noteDbRowRecoveryMiss(entry) {
218
+ entry.r3bRecoveryCount += 1;
219
+ if (entry.r3bRecoveryCount >= MAX_DB_ROW_RECOVERIES) {
220
+ notifyDbRowRecoveryFailed(entry);
221
+ }
222
+ }
223
+ function ensureMilestoneRowForAcceptedHandoff(entry, contextFile) {
224
+ if (!isDbAvailable())
225
+ return true;
226
+ const { basePath, milestoneId } = entry;
227
+ const milestoneRow = getMilestone(milestoneId);
228
+ if (milestoneRow)
229
+ return true;
230
+ if (manifestContainsMilestone(basePath, milestoneId)) {
231
+ logWarning("guided", `R3b: getMilestone(${milestoneId}) returned null but manifest has the row — treating as stale read`);
232
+ return true;
233
+ }
234
+ if (!contextFile) {
235
+ entry.ctx.ui.notify(`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
236
+ `PROJECT.md may have failed to register milestones. ` +
237
+ `Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
238
+ `then re-run /gsd to recover.`, "error");
239
+ return false;
240
+ }
241
+ if (entry.r3bRecoveryCount >= MAX_DB_ROW_RECOVERIES) {
242
+ logWarning("guided", `R3b: milestone ${milestoneId} DB-row recovery limit reached ` +
243
+ `(${entry.r3bRecoveryCount}/${MAX_DB_ROW_RECOVERIES}); user already notified`);
244
+ return false;
245
+ }
246
+ logWarning("guided", `R3b: ${milestoneId} has CONTEXT.md but no DB row — inserting placeholder "queued" row ` +
247
+ `(attempt ${entry.r3bRecoveryCount + 1}/${MAX_DB_ROW_RECOVERIES})`);
248
+ try {
249
+ insertMilestone({ id: milestoneId, title: milestoneId, status: "queued" });
250
+ }
251
+ catch (e) {
252
+ logWarning("guided", `R3b: insertMilestone failed: ${e.message}`);
253
+ }
254
+ if (getMilestone(milestoneId))
255
+ return true;
256
+ noteDbRowRecoveryMiss(entry);
257
+ return false;
258
+ }
181
259
  // ─── Commit Instruction Helpers ──────────────────────────────────────────────
182
260
  /** Build commit instruction for planning prompts. .gsd/ is managed externally and always gitignored. */
183
261
  function buildDocsCommitInstruction(_message) {
@@ -186,10 +264,8 @@ function buildDocsCommitInstruction(_message) {
186
264
  // #4573: cap for how many times we nudge the LLM after a premature ready
187
265
  // phrase before giving up and asking the user to re-run /gsd.
188
266
  const MAX_READY_REJECTS = 2;
189
- // H1 (#5012): cap for Gate 1b plan-blocked recovery hints. After this many
190
- // consecutive recovery attempts the loop is stopped and the user is directed
191
- // to investigate manually.
192
- const MAX_PLAN_BLOCKED_RECOVERIES = 3;
267
+ // Cap failed in-flight DB row repair attempts before escalating to the user.
268
+ const MAX_DB_ROW_RECOVERIES = 3;
193
269
  // #4573: matches the canonical ready phrase the discuss prompt asks the LLM
194
270
  // to emit. Accepts any M-prefixed milestone ID (three digits + optional
195
271
  // suffix) with optional trailing punctuation.
@@ -420,56 +496,12 @@ export function checkAutoStartAfterDiscuss(lookupBasePath) {
420
496
  return false;
421
497
  }
422
498
  }
423
- // Gate 1b: Discriminate plan-blocked from discuss-incomplete when the DB row is queued.
424
- // If the DB is available and the row is still "queued" but CONTEXT.md already exists on
425
- // disk, the discuss phase completed but gsd_plan_milestone was hard-blocked by the
426
- // depth-verification gate. Emit a recovery hint so the next agent turn can retry
427
- // gsd_plan_milestone, then return false (keep blocking auto-start).
428
- // If CONTEXT.md does not exist (discuss-incomplete), Gate 1 already blocked above.
429
- if (isDbAvailable()) {
430
- const dbRow = getMilestone(milestoneId);
431
- if (dbRow?.status === "queued" && contextFile) {
432
- if (entry.planBlockedRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
433
- // H1: recovery loop cap reached — stop triggering new turns, escalate to user.
434
- logWarning("guided", `Gate 1b: milestone ${milestoneId} plan-blocked recovery limit reached ` +
435
- `(${entry.planBlockedRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`);
436
- ctx.ui.notify(`Milestone ${milestoneId} plan_milestone has been blocked ${entry.planBlockedRecoveryCount} times. ` +
437
- `Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`, "error");
438
- return false;
439
- }
440
- logWarning("guided", `Gate 1b: milestone ${milestoneId} queued with CONTEXT.md present — ` +
441
- `plan_milestone was blocked; emitting recovery hint ` +
442
- `(attempt ${entry.planBlockedRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`);
443
- ctx.ui.notify(`Milestone ${milestoneId}: context file exists but milestone is still queued. ` +
444
- `Retrying gsd_plan_milestone to complete the blocked planning step.`, "warning");
445
- try {
446
- pi.sendMessage({
447
- customType: "gsd-plan-milestone-blocked-recovery",
448
- content: `Milestone ${milestoneId} has ${contextFile} on disk but its DB row is still ` +
449
- `"queued". The gsd_plan_milestone tool was previously blocked by the ` +
450
- `depth-verification gate. Call gsd_plan_milestone now to complete the ` +
451
- `planning phase.`,
452
- display: false,
453
- }, { triggerTurn: true });
454
- // Increment only after a successful dispatch so transient sendMessage
455
- // failures do not consume recovery budget.
456
- entry.planBlockedRecoveryCount += 1;
457
- }
458
- catch (e) {
459
- logWarning("guided", `Gate 1b recovery sendMessage failed: ${e.message}`);
460
- }
461
- return false;
462
- }
463
- }
464
- // Gate 2: STATE.md must exist — written as the last step in the discuss
465
- // output phase. This prevents auto-start from firing during Phase 3
466
- // (sequential readiness gates for remaining milestones) in multi-milestone
467
- // discussions, where M001-CONTEXT.md exists but M002/M003 haven't been
468
- // processed yet.
469
- const stateFilePath = entry.scope.stateFile();
470
- if (!existsSync(stateFilePath))
471
- return false; // discussion not finalized yet
472
- // Gate 3: Multi-milestone completeness warning
499
+ // Gate 1b: accept the in-flight discuss handoff. A queued DB row with pinned
500
+ // CONTEXT.md is Discussion Complete, Planning Pending, not a plan-blocked
501
+ // failure. If the row is missing, only this pending handoff may repair it.
502
+ if (!ensureMilestoneRowForAcceptedHandoff(entry, contextFile))
503
+ return false;
504
+ // Gate 2: Multi-milestone completeness warning
473
505
  // Parse PROJECT.md for milestone sequence, warn if any are missing context.
474
506
  // Don't block — milestones can be intentionally queued without context.
475
507
  const projectFile = resolveGsdRootFile(basePath, "PROJECT");
@@ -495,7 +527,7 @@ export function checkAutoStartAfterDiscuss(lookupBasePath) {
495
527
  logWarning("guided", `PROJECT.md parsing failed: ${e.message}`);
496
528
  }
497
529
  }
498
- // Gate 4: Discussion manifest process verification (multi-milestone only)
530
+ // Gate 3: Discussion manifest process verification (multi-milestone only)
499
531
  // The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
500
532
  // When it exists, validate it before auto-starting. Project history alone is
501
533
  // not a reliable signal for the current discussion mode.
@@ -541,59 +573,9 @@ export function checkAutoStartAfterDiscuss(lookupBasePath) {
541
573
  logWarning("guided", `manifest unlink failed: ${e.message}`);
542
574
  }
543
575
  }
544
- // R3b: belt-and-suspenders for silent registration failure. The discuss flow
545
- // finished and STATE.md exists, but the milestone may never have landed in
546
- // the DB. Without this guard, the user sees "Milestone M001 ready." and then
547
- // /gsd reports "No Active Milestone".
548
- if (isDbAvailable()) {
549
- const milestoneRow = getMilestone(milestoneId);
550
- if (!milestoneRow) {
551
- let manifestHasMilestone = false;
552
- try {
553
- const manifest = readManifest(basePath);
554
- manifestHasMilestone = Array.isArray(manifest?.milestones) && manifest.milestones.some(m => m.id === milestoneId);
555
- }
556
- catch (e) {
557
- logWarning("guided", `R3b: failed to read state manifest: ${e.message}`);
558
- }
559
- if (manifestHasMilestone) {
560
- logWarning("guided", `R3b: getMilestone(${milestoneId}) returned null but manifest has the row — treating as stale read`);
561
- }
562
- else if (contextFile) {
563
- // R3b-recovery: CONTEXT.md is on disk but gsd_plan_milestone was never called
564
- // (likely blocked by the depth-verification gate re-firing on post-verification
565
- // text). Auto-register as "queued" so Gate 1b can pick it up and retry
566
- // gsd_plan_milestone on the next checkAutoStartAfterDiscuss call.
567
- if (entry.r3bRecoveryCount >= MAX_PLAN_BLOCKED_RECOVERIES) {
568
- logWarning("guided", `R3b: milestone ${milestoneId} DB-row recovery limit reached ` +
569
- `(${entry.r3bRecoveryCount}/${MAX_PLAN_BLOCKED_RECOVERIES}); escalating to user`);
570
- ctx.ui.notify(`Milestone ${milestoneId}: DB row recovery failed ${entry.r3bRecoveryCount} times. ` +
571
- `Re-run /gsd to reset the recovery counter, or run /gsd-debug to diagnose without resetting.`, "error");
572
- return false;
573
- }
574
- logWarning("guided", `R3b: ${milestoneId} has CONTEXT.md but no DB row — inserting placeholder "queued" row ` +
575
- `for Gate 1b recovery (attempt ${entry.r3bRecoveryCount + 1}/${MAX_PLAN_BLOCKED_RECOVERIES})`);
576
- try {
577
- insertMilestone({ id: milestoneId, title: milestoneId, status: "queued" });
578
- }
579
- catch (e) {
580
- logWarning("guided", `R3b: insertMilestone failed: ${e.message}`);
581
- }
582
- entry.r3bRecoveryCount += 1;
583
- ctx.ui.notify(`Milestone ${milestoneId}: context file exists but DB row was missing — recovering. Retrying gsd_plan_milestone.`, "warning");
584
- return false;
585
- }
586
- else {
587
- ctx.ui.notify(`Milestone ${milestoneId}: discuss artifacts on disk but no DB row exists. ` +
588
- `PROJECT.md may have failed to register milestones. ` +
589
- `Re-save PROJECT.md with canonical "- [ ] M001: Title — One-liner" lines, ` +
590
- `then re-run /gsd to recover.`, "error");
591
- return false;
592
- }
593
- }
594
- }
595
576
  deletePendingAutoStart(basePath);
596
- ctx.ui.notify(`Milestone ${milestoneId} ready.`, "success");
577
+ const hasExecutablePlan = hasExecutablePlanForHandoff(milestoneId, roadmapFile);
578
+ ctx.ui.notify(formatAcceptedDiscussHandoffMessage(milestoneId, contextFile, hasExecutablePlan), "success");
597
579
  if (entry.startAuto !== false) {
598
580
  scheduleAutoStartAfterIdle(ctx, pi, basePath, false, { step: step ?? true });
599
581
  }
@@ -883,7 +865,10 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType,
883
865
  ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
884
866
  : undefined,
885
867
  baseUrl: result.appliedModel?.baseUrl ?? ctx.model?.baseUrl,
886
- activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
868
+ // Guided flow starts the MCP workflow server as part of dispatch, so the
869
+ // parent session's activeTools doesn't include MCP tools yet. The MCP
870
+ // launch config check (detectWorkflowMcpLaunchConfig) is the right gate
871
+ // here — not whether MCP tools are pre-registered in the parent session.
887
872
  });
888
873
  if (compatibilityError) {
889
874
  ctx.ui.notify(compatibilityError, "error");
@@ -15,6 +15,7 @@ import { extractVerdict, isAcceptableUatVerdict } from "./verdict-parser.js";
15
15
  import { logWarning } from "./workflow-logger.js";
16
16
  import { hasImplementationArtifacts } from "./milestone-implementation-evidence.js";
17
17
  import { buildCompleteMilestonePrompt } from "./auto-prompts.js";
18
+ import { checkCloseoutConsistencyGate } from "./closeout-consistency-gate.js";
18
19
  import { commitPendingMilestoneCloseoutChanges, findMissingSummaries, isVerificationNotApplicable, readUatGateVerdict, } from "./auto-dispatch.js";
19
20
  const COMPLETE_MILESTONE_DB_SETTLE_MS = 1500;
20
21
  const COMPLETE_MILESTONE_DB_SETTLE_POLL_MS = 100;
@@ -28,7 +29,8 @@ export async function isMilestoneCloseoutSettled(mid, basePath) {
28
29
  if (isDbAvailable()) {
29
30
  const milestone = getMilestone(mid);
30
31
  if (milestone && isClosedStatus(milestone.status)) {
31
- if (verifyExpectedArtifact("complete-milestone", mid, basePath)) {
32
+ const closeoutGate = checkCloseoutConsistencyGate(mid, { refreshFromDisk: true });
33
+ if (closeoutGate.ok && verifyExpectedArtifact("complete-milestone", mid, basePath)) {
32
34
  return true;
33
35
  }
34
36
  }
@@ -23,7 +23,6 @@ export function setPendingAutoStart(basePath, entry) {
23
23
  const scope = scopeMilestone(ws, entry.milestoneId);
24
24
  pendingAutoStartMap.set(basePath, {
25
25
  createdAt: Date.now(),
26
- planBlockedRecoveryCount: 0,
27
26
  r3bRecoveryCount: 0,
28
27
  ...entry,
29
28
  scope,
@@ -0,0 +1,98 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Optional gsd-planner handoff after milestone planning.
3
+ import { spawn as spawnChild } from "node:child_process";
4
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { gsdRoot } from "./paths.js";
7
+ export const PLANNER_HANDOFF_RULE_NAME = "planning review handoff -> gsd-planner";
8
+ export const GSD_PLANNER_COMMAND = "gsd-planner";
9
+ function handoffDir(basePath) {
10
+ return join(gsdRoot(basePath), "runtime", "planner-handoffs");
11
+ }
12
+ function safeMilestoneFileSegment(milestoneId) {
13
+ return milestoneId.replace(/[^A-Za-z0-9._-]/g, "_") || "unknown";
14
+ }
15
+ function handoffMarkerPath(basePath, milestoneId) {
16
+ return join(handoffDir(basePath), `${safeMilestoneFileSegment(milestoneId)}.json`);
17
+ }
18
+ export function hasPlannerHandoffBeenOffered(basePath, milestoneId) {
19
+ return existsSync(handoffMarkerPath(basePath, milestoneId));
20
+ }
21
+ export function markPlannerHandoffOffered(basePath, milestoneId, source = "auto") {
22
+ mkdirSync(handoffDir(basePath), { recursive: true });
23
+ writeFileSync(handoffMarkerPath(basePath, milestoneId), JSON.stringify({
24
+ milestoneId,
25
+ source,
26
+ offeredAt: new Date().toISOString(),
27
+ }, null, 2) + "\n", "utf-8");
28
+ }
29
+ export function buildGsdPlannerSpawnPlan(input) {
30
+ const args = ["--project", input.basePath];
31
+ const milestoneId = input.milestoneId?.trim();
32
+ if (milestoneId)
33
+ args.push("--milestone", milestoneId);
34
+ args.push(...(input.extraArgs ?? []));
35
+ return {
36
+ command: GSD_PLANNER_COMMAND,
37
+ args,
38
+ cwd: input.basePath,
39
+ };
40
+ }
41
+ function quoteArg(arg) {
42
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : JSON.stringify(arg);
43
+ }
44
+ export function formatGsdPlannerCommand(plan) {
45
+ return [plan.command, ...plan.args].map(quoteArg).join(" ");
46
+ }
47
+ export async function launchGsdPlanner(input, deps = {}) {
48
+ const plan = buildGsdPlannerSpawnPlan(input);
49
+ const spawn = deps.spawn ?? spawnChild;
50
+ let child;
51
+ try {
52
+ child = spawn(plan.command, plan.args, {
53
+ cwd: plan.cwd,
54
+ detached: true,
55
+ stdio: "ignore",
56
+ windowsHide: true,
57
+ });
58
+ }
59
+ catch (err) {
60
+ return {
61
+ status: "failed",
62
+ plan,
63
+ error: err instanceof Error ? err : new Error(String(err)),
64
+ };
65
+ }
66
+ return new Promise((resolve) => {
67
+ let settled = false;
68
+ const settle = (result) => {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ resolve(result);
73
+ };
74
+ child.once("error", (err) => {
75
+ settle({
76
+ status: "failed",
77
+ plan,
78
+ error: err instanceof Error ? err : new Error(String(err)),
79
+ });
80
+ });
81
+ child.once("spawn", () => {
82
+ child.unref();
83
+ settle({ status: "launched", plan });
84
+ });
85
+ });
86
+ }
87
+ export function formatPlannerHandoffPauseReason(milestoneId) {
88
+ return [
89
+ `Milestone ${milestoneId} is planned. Review or customize the plan before implementation if needed.`,
90
+ `Run /gsd planner to launch ${GSD_PLANNER_COMMAND}, or run /gsd auto to continue without planner changes.`,
91
+ ].join(" ");
92
+ }
93
+ export function formatPlannerLaunchUnavailable(plan, error) {
94
+ return [
95
+ `Could not launch ${GSD_PLANNER_COMMAND}: ${error.message}`,
96
+ `Install ${GSD_PLANNER_COMMAND} or run it manually: ${formatGsdPlannerCommand(plan)}`,
97
+ ].join("\n");
98
+ }
@@ -339,6 +339,7 @@ export function resolveAutoSupervisorConfig() {
339
339
  soft_timeout_minutes: configured.soft_timeout_minutes ?? 20,
340
340
  idle_timeout_minutes: configured.idle_timeout_minutes ?? 10,
341
341
  hard_timeout_minutes: configured.hard_timeout_minutes ?? 30,
342
+ stalled_tool_timeout_minutes: configured.stalled_tool_timeout_minutes ?? 5,
342
343
  ...(configured.model ? { model: configured.model } : {}),
343
344
  };
344
345
  }
@@ -14,7 +14,7 @@ All relevant context is preloaded below. Start immediately without re-reading th
14
14
 
15
15
  ## Already Planned? Soft Brake
16
16
 
17
- If `{{outputPath}}` exists with at least one slice line (e.g. `- [ ] **S01:`) AND `gsd_query` reports slice rows for this milestone, a prior `gsd_plan_milestone` call already persisted the plan. Do **not** re-call it; its UPSERT could overwrite existing planning. Skip to the ready phrase.
17
+ If `{{outputPath}}` exists with at least one slice line (e.g. `- [ ] **S01:`) AND `gsd_milestone_status` reports slice rows for this milestone, a prior `gsd_plan_milestone` call already persisted the plan. Do **not** re-call it; its UPSERT could overwrite existing planning. Skip to the ready phrase.
18
18
 
19
19
  If only the file or only DB rows exist, the prior write was incomplete; plan normally so the tool reconciles both.
20
20
 
@@ -27,7 +27,7 @@ You are the UAT runner. Execute every check defined in `{{uatPath}}` as deeply a
27
27
  ### Automation rules by mode
28
28
 
29
29
  - `artifact-driven` — verify with shell commands, scripts, file reads, and artifact structure checks.
30
- - `browser-executable` — use gsd-browser tools to navigate to the target URL and verify expected behavior. Prefer `mcp__gsd-browser__browser_*` tools when namespaced, or direct `browser_*` tools when surfaced without a namespace. Capture screenshots as evidence. Record pass/fail with specific assertions.
30
+ - `browser-executable` — use browser tools to navigate to the target URL and verify expected behavior. Prefer direct `browser_*` tools when available. Capture screenshots as evidence. Record pass/fail with specific assertions.
31
31
  - `runtime-executable` — execute the specified command or script. Capture stdout/stderr as evidence. Record pass/fail based on exit code and output.
32
32
  - `live-runtime` — exercise the real runtime path. Start or connect to the app/service if needed, use browser/runtime/network checks, and verify observable behavior.
33
33
  - `mixed` — run all automatable artifact-driven and live-runtime checks. Separate any remaining human-only checks explicitly.
@@ -48,7 +48,7 @@ Choose the lightest tool that proves the check honestly:
48
48
  - Run `node` / other script invocations
49
49
  - Read files and verify their contents
50
50
  - Check that expected artifacts exist and have correct structure
51
- - For live/runtime/UI checks, exercise the real flow with gsd-browser when applicable and inspect runtime/network/console state
51
+ - For live/runtime/UI checks, exercise the real flow with browser tools when applicable and inspect runtime/network/console state
52
52
  - When a check cannot be honestly automated, gather the best objective evidence you can and mark it `NEEDS-HUMAN`
53
53
 
54
54
  For each check, record:
@@ -75,24 +75,10 @@ verdict: "PASS" | "FAIL" | "PARTIAL",
75
75
  notes: "<one sentence overall verdict rationale>",
76
76
  ```
77
77
 
78
- Use this exact `presentation` shape in the save call so the audit can verify the run-uat tool surface without retrying missing fields one by one:
78
+ Use this canonical `presentation` object in the save call so the audit can verify the run-uat tool surface without retrying missing fields one by one. Keep `toolPresentationPlanId` as `{{toolPresentationPlanId}}`. If browser tools were actually presented for this run, add those concrete browser tool names to `presentedTools`; otherwise reuse this object exactly:
79
79
 
80
- ```ts
81
- presentation: {
82
- surface: "mcp",
83
- presentedTools: [
84
- "gsd_uat_exec",
85
- "gsd_uat_result_save",
86
- "gsd_resume",
87
- "gsd_milestone_status",
88
- "gsd_journal_query",
89
- ],
90
- blockedTools: [
91
- { name: "gsd_exec", reason: "forbidden during run-uat" },
92
- { name: "gsd_summary_save", reason: "forbidden during run-uat" },
93
- { name: "gsd_save_gate_result", reason: "forbidden during run-uat" },
94
- ],
95
- }
80
+ ```json
81
+ {{canonicalPresentation}}
96
82
  ```
97
83
 
98
84
  Pass `checks` with this logical shape:
@@ -118,7 +118,7 @@ Templates are in `{{templatesDir}}`.
118
118
 
119
119
  **Secrets:** Use `secure_env_collect`. Never ask the user to edit `.env` files or paste secrets.
120
120
 
121
- **Browser verification:** Verify frontend work against a running app with gsd-browser by default. Use `browser_find`/`browser_snapshot_refs` for discovery, refs/selectors -> `browser_batch` for actions, `browser_assert` for verification, and `browser_diff` -> console/network logs -> full inspection as last resort. If tools are MCP-namespaced, prefer `mcp__gsd-browser__browser_*`. Retry only with a new hypothesis.
121
+ **Browser verification:** Verify frontend work against a running app with browser tools by default. Use `browser_find`/`browser_snapshot_refs` for discovery, refs/selectors -> `browser_batch` for actions, `browser_assert` for verification, and `browser_diff` -> console/network logs -> full inspection as last resort. If browser tools are MCP-namespaced, use that host-provided browser surface. Retry only with a new hypothesis.
122
122
 
123
123
  **Database:** Never query `.gsd/gsd.db` directly via `sqlite3`, `better-sqlite3`, or `node -e require('better-sqlite3')`; the engine owns a single-writer WAL connection. Use `gsd_milestone_status`, `gsd_journal_query`, or other `gsd_*` tools.
124
124
 
@@ -19,6 +19,14 @@ export function classifyFailure(input) {
19
19
  exitReason: "tool-schema",
20
20
  remediation: "Fix the Unit Tool Contract or tool schema before retrying.",
21
21
  };
22
+ case "tool-contract":
23
+ return {
24
+ failureKind,
25
+ action: "stop",
26
+ reason: `Tool Contract failure${unitSuffix(input)}: ${message}`,
27
+ exitReason: "tool-contract",
28
+ remediation: "Fix the Unit Tool Contract or prompt so the Unit is only asked to use tools owned by its phase.",
29
+ };
22
30
  case "deterministic-policy":
23
31
  return {
24
32
  failureKind,
@@ -27,6 +35,14 @@ export function classifyFailure(input) {
27
35
  exitReason: "deterministic-policy",
28
36
  remediation: "Resolve the policy blocker; retrying the same Unit will repeat the failure.",
29
37
  };
38
+ case "lifecycle-progression":
39
+ return {
40
+ failureKind,
41
+ action: "stop",
42
+ reason: `Lifecycle progression failure${unitSuffix(input)}: ${message}`,
43
+ exitReason: "lifecycle-progression",
44
+ remediation: "Route to the required owning Unit or restore the missing artifact before advancing lifecycle state.",
45
+ };
30
46
  case "stale-worker":
31
47
  return {
32
48
  failureKind,
@@ -83,6 +99,10 @@ export function classifyFailure(input) {
83
99
  }
84
100
  }
85
101
  function inferFailureKind(message) {
102
+ if (/tool contract|auto-unit tool scope|phase-boundary gate|not permitted.*own/i.test(message))
103
+ return "tool-contract";
104
+ if (/lifecycle progression|required artifact|missing .*assessment|missing .*closeout|cannot legally (?:advance|progress)/i.test(message))
105
+ return "lifecycle-progression";
86
106
  if (/schema|invalid.*tool|tool.*invalid|enum/i.test(message))
87
107
  return "tool-schema";
88
108
  if (/deterministic policy|policy rejection|write gate|blocked by policy/i.test(message))