@opengsd/gsd-pi 1.1.1-dev.3ea310e → 1.1.1-dev.74e8dd1

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/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +4 -3
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +15 -4
  4. package/dist/resources/extensions/gsd/auto-post-unit.js +111 -5
  5. package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
  6. package/dist/resources/extensions/gsd/auto-start.js +41 -12
  7. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +2 -1
  8. package/dist/resources/extensions/gsd/auto.js +3 -3
  9. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +79 -0
  10. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +43 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +30 -9
  12. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +16 -10
  13. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
  14. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +3 -1
  15. package/dist/resources/extensions/gsd/commands-verdict.js +1 -1
  16. package/dist/resources/extensions/gsd/config-overlay.js +2 -1
  17. package/dist/resources/extensions/gsd/error-classifier.js +2 -1
  18. package/dist/resources/extensions/gsd/exec-sandbox.js +2 -0
  19. package/dist/resources/extensions/gsd/prompts/run-uat.md +10 -4
  20. package/dist/resources/extensions/gsd/prompts/system.md +3 -1
  21. package/dist/resources/extensions/gsd/safety/destructive-guard.js +3 -0
  22. package/dist/resources/extensions/gsd/skill-activation.js +20 -3
  23. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +18 -1
  24. package/dist/resources/extensions/gsd/state-reconciliation/index.js +6 -0
  25. package/dist/resources/extensions/gsd/state.js +1 -1
  26. package/dist/resources/extensions/gsd/tools/exec-tool.js +109 -0
  27. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +366 -3
  28. package/dist/resources/extensions/gsd/unit-context-manifest.js +8 -3
  29. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -0
  30. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +1 -1
  31. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -1
  32. package/dist/web/standalone/.next/BUILD_ID +1 -1
  33. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  34. package/dist/web/standalone/.next/build-manifest.json +2 -2
  35. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  36. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.html +1 -1
  53. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  60. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  61. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  63. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  64. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/package.json +2 -2
  66. package/packages/cloud-mcp-gateway/package.json +2 -2
  67. package/packages/contracts/dist/workflow.d.ts +14 -0
  68. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  69. package/packages/contracts/dist/workflow.js +16 -0
  70. package/packages/contracts/dist/workflow.js.map +1 -1
  71. package/packages/contracts/package.json +1 -1
  72. package/packages/daemon/package.json +4 -4
  73. package/packages/gsd-agent-core/package.json +5 -5
  74. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  75. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  76. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
  77. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  78. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
  79. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  80. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +69 -31
  81. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  82. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -1
  83. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -1
  85. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  86. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  87. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +1 -0
  88. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  89. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
  90. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +5 -0
  91. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
  92. package/packages/gsd-agent-modes/package.json +7 -7
  93. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/workflow-tools.js +82 -0
  95. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  96. package/packages/mcp-server/package.json +3 -3
  97. package/packages/native/package.json +1 -1
  98. package/packages/pi-agent-core/package.json +1 -1
  99. package/packages/pi-ai/dist/image-models.generated.d.ts +15 -0
  100. package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  101. package/packages/pi-ai/dist/image-models.generated.js +15 -0
  102. package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
  103. package/packages/pi-ai/dist/models.generated.d.ts +35 -1
  104. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/models.generated.js +53 -19
  106. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  107. package/packages/pi-ai/package.json +1 -1
  108. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  109. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  111. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  112. package/packages/pi-coding-agent/package.json +7 -7
  113. package/packages/pi-tui/dist/terminal.d.ts +1 -0
  114. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  115. package/packages/pi-tui/dist/terminal.js +8 -4
  116. package/packages/pi-tui/dist/terminal.js.map +1 -1
  117. package/packages/pi-tui/package.json +1 -1
  118. package/packages/rpc-client/package.json +2 -2
  119. package/pkg/package.json +1 -1
  120. package/src/resources/extensions/gsd/auto/phases.ts +5 -3
  121. package/src/resources/extensions/gsd/auto-dashboard.ts +16 -4
  122. package/src/resources/extensions/gsd/auto-post-unit.ts +136 -5
  123. package/src/resources/extensions/gsd/auto-prompts.ts +9 -0
  124. package/src/resources/extensions/gsd/auto-start.ts +54 -14
  125. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +2 -1
  126. package/src/resources/extensions/gsd/auto.ts +3 -2
  127. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +86 -0
  128. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +51 -0
  129. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +51 -14
  130. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +21 -10
  131. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
  132. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -1
  133. package/src/resources/extensions/gsd/commands-verdict.ts +1 -1
  134. package/src/resources/extensions/gsd/config-overlay.ts +3 -1
  135. package/src/resources/extensions/gsd/error-classifier.ts +2 -1
  136. package/src/resources/extensions/gsd/exec-sandbox.ts +4 -0
  137. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  138. package/src/resources/extensions/gsd/prompts/run-uat.md +10 -4
  139. package/src/resources/extensions/gsd/prompts/system.md +3 -1
  140. package/src/resources/extensions/gsd/safety/destructive-guard.ts +3 -0
  141. package/src/resources/extensions/gsd/skill-activation.ts +20 -2
  142. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +20 -0
  143. package/src/resources/extensions/gsd/state-reconciliation/index.ts +6 -0
  144. package/src/resources/extensions/gsd/state-reconciliation/types.ts +1 -0
  145. package/src/resources/extensions/gsd/state.ts +1 -1
  146. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +51 -0
  147. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +16 -3
  148. package/src/resources/extensions/gsd/tests/commands-dispatcher-validation-block.test.ts +38 -3
  149. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +6 -2
  150. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +8 -0
  151. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +8 -0
  152. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +18 -0
  153. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +69 -0
  154. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +54 -7
  155. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +10 -0
  156. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
  157. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +36 -0
  158. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +35 -0
  159. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +1 -1
  160. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +55 -0
  161. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  162. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +84 -10
  163. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +12 -2
  164. package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +29 -6
  165. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +29 -6
  166. package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +21 -0
  167. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +2 -2
  168. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +83 -0
  169. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +25 -0
  170. package/src/resources/extensions/gsd/tools/exec-tool.ts +130 -0
  171. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +440 -2
  172. package/src/resources/extensions/gsd/unit-context-manifest.ts +14 -5
  173. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -0
  174. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +1 -1
  175. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -1
  176. /package/dist/web/standalone/.next/static/{xACmObbrDjwLriepRgaa9 → eRWf-RI9bzbrwEurm_3uI}/_buildManifest.js +0 -0
  177. /package/dist/web/standalone/.next/static/{xACmObbrDjwLriepRgaa9 → eRWf-RI9bzbrwEurm_3uI}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- 3a34fcd8ba090855
1
+ 5f6f7839b5420a75
@@ -51,13 +51,13 @@ import { resolveManifest } from "../unit-context-manifest.js";
51
51
  import { createWorktreeSafetyModule } from "../worktree-safety.js";
52
52
  import { isSuspiciousGhostCompletion } from "../auto-unit-closeout.js";
53
53
  import { decideVerificationRetry, verificationRetryKey } from "./verification-retry-policy.js";
54
- import { buildPhaseHandoffOutcome, setAutoOutcomeWidget } from "../auto-dashboard.js";
54
+ import { buildPhaseHandoffOutcome, setAutoActiveStatus, setAutoOutcomeWidget } from "../auto-dashboard.js";
55
55
  import { getConsecutiveDispatchBlocker } from "../dispatch-guard.js";
56
56
  import { captureRootDirtySnapshot, detectRootWriteLeak, formatRootWriteLeakMessage, } from "../root-write-leak-guard.js";
57
57
  import { classifyError, isTransient } from "../error-classifier.js";
58
58
  export const STUCK_WINDOW_SIZE = 6;
59
59
  const STUCK_RECOVERY_ATTEMPTS_KEY = "stuck_recovery_attempts";
60
- const ZERO_TOOL_PROVIDER_ERROR_PREFIX_RE = /^(?:api error(?::|$|\s*\()|provider error(?::|$|\s*\()|request failed\b|(?:http\s*)?(?:429|500|502|503)\b|\b(?:econnreset|etimedout|econnrefused|epipe)\b|socket hang up\b|fetch failed\b|(?:network|connection|server) error(?::|$)|connection (?:reset|refused)(?::|$|\s+by\b)|dns\b.*(?:fail|error|timeout)|unexpected eof\b|stream idle timeout\b|partial response received\b|stream_exhausted\b|terminated(?::|$)|(?:connection|stream|request)\b.{0,40}\bterminated\b|other side closed\b|rate.?limit(?:ed| exceeded| reached| error)|too many requests\b|you(?:'ve| have) hit your limit\b|usage limit\b|out of extra usage\b|service.?unavailable\b|internal(?: server)? error(?::|$)|internal(?:[_-]server)?[_-]error\b|server[_-]error\b|(?:provider|server|api|model|codex|claude|openai|anthropic|gemini)\b.{0,80}\boverloaded\b|overloaded\b.{0,80}\b(?:provider|server|api|model)\b|context (?:window|length) exceed|context window exceed)/i;
60
+ const ZERO_TOOL_PROVIDER_ERROR_PREFIX_RE = /^(?:api error(?::|$|\s*\()|provider error(?::|$|\s*\()|request failed\b|(?:http\s*)?(?:429|500|502|503)\b|\b(?:econnreset|etimedout|econnrefused|epipe)\b|socket hang up\b|fetch failed\b|(?:network|connection|server) error(?::|$)|connection (?:reset|refused)(?::|$|\s+by\b)|dns\b.*(?:fail|error|timeout)|unexpected eof\b|stream idle timeout\b|partial response received\b|stream_exhausted\b|terminated(?::|$)|(?:connection|stream|request)\b.{0,40}\bterminated\b|other side closed\b|rate.?limit(?:ed| exceeded| reached| error)|too many requests\b|you(?:'ve| have) (?:hit|reached) your (?:\w+ )?limit\b|.*\b(?:usage|session|weekly|daily|monthly|quota) limit\b|limit\b.{0,40}\bresets?\b|out of extra usage\b|service.?unavailable\b|internal(?: server)? error(?::|$)|internal(?:[_-]server)?[_-]error\b|server[_-]error\b|(?:provider|server|api|model|codex|claude|openai|anthropic|gemini)\b.{0,80}\boverloaded\b|overloaded\b.{0,80}\b(?:provider|server|api|model)\b|context (?:window|length) exceed|context window exceed)/i;
61
61
  const ZERO_TOOL_PROVIDER_ERROR_SIGNAL_RE = /(?:\b(?:http|status(?: code)?|code|error:)\s*(?:429|500|502|503)\b|\b(?:api|provider) error\s*[:(]?\s*(?:429|500|502|503)\b|\b(?:typeerror|error):\s*(?:fetch failed\b|socket hang up\b|terminated(?::|$)|connection (?:reset|refused)(?::|$|\s+by\b)|(?:network|connection|server) error(?::|$)|stream idle timeout\b|partial response received\b|unexpected eof\b)|\b(?:server_error|api_error|stream_exhausted(?:_without_result)?)\b|\b(?:econnreset|etimedout|econnrefused|epipe)\b|context (?:window|length) exceed|context window exceed)/i;
62
62
  function classifyZeroToolProviderMessage(message) {
63
63
  const firstLine = message.trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
@@ -67,6 +67,7 @@ function classifyZeroToolProviderMessage(message) {
67
67
  return null;
68
68
  return classifyError(firstLine);
69
69
  }
70
+ export const _classifyZeroToolProviderMessageForTest = classifyZeroToolProviderMessage;
70
71
  export function resolveDispatchRecoveryAttempts(unitRecoveryCount, unitType, unitId) {
71
72
  return (unitRecoveryCount.get(`${unitType}/${unitId}`) ?? 0) > 0
72
73
  ? 0
@@ -1582,7 +1583,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1582
1583
  const dispatchKey = `${unitType}/${unitId}`;
1583
1584
  const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
1584
1585
  // Status bar (widget + preconditions deferred until after model selection — see #2899)
1585
- ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1586
+ setAutoActiveStatus(ctx, s.stepMode ? "next" : "auto");
1586
1587
  if (mid)
1587
1588
  deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
1588
1589
  // ── Safety harness: reset evidence + create checkpoint ──
@@ -369,8 +369,9 @@ export const hideFooter = (_tui, theme, footerData) => ({
369
369
  invalidate() { },
370
370
  dispose() { },
371
371
  });
372
+ export const DEFAULT_WIDGET_MODE = "small";
372
373
  const WIDGET_MODES = ["full", "small", "min", "off"];
373
- let widgetMode = "full";
374
+ let widgetMode = DEFAULT_WIDGET_MODE;
374
375
  let widgetModeInitialized = false;
375
376
  let widgetModePreferencePath = null;
376
377
  function safeReadTextFile(path) {
@@ -473,10 +474,19 @@ export function getWidgetMode(projectPath, globalPath) {
473
474
  }
474
475
  /** Test-only reset for widget mode caching. */
475
476
  export function _resetWidgetModeForTests() {
476
- widgetMode = "full";
477
+ widgetMode = DEFAULT_WIDGET_MODE;
477
478
  widgetModeInitialized = false;
478
479
  widgetModePreferencePath = null;
479
480
  }
481
+ function clearAutoOutcomeWidget(ctx) {
482
+ if (!ctx.hasUI)
483
+ return;
484
+ ctx.ui.setWidget("gsd-outcome", undefined);
485
+ }
486
+ export function setAutoActiveStatus(ctx, status) {
487
+ ctx.ui.setStatus("gsd-auto", status);
488
+ clearAutoOutcomeWidget(ctx);
489
+ }
480
490
  export function updateProgressWidget(ctx, unitType, unitId, state, accessors, tierBadge) {
481
491
  if (!ctx.hasUI)
482
492
  return;
@@ -495,7 +505,7 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
495
505
  ctx.ui.setStatus("gsd-step", undefined);
496
506
  }
497
507
  if (!accessors.isSessionSwitching()) {
498
- ctx.ui.setWidget("gsd-outcome", undefined);
508
+ clearAutoOutcomeWidget(ctx);
499
509
  }
500
510
  const verb = unitVerb(unitType);
501
511
  const phaseLabel = unitPhaseLabel(unitType);
@@ -548,6 +558,7 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
548
558
  logWarning("dashboard", `DB status update failed: ${err instanceof Error ? err.message : String(err)}`);
549
559
  }
550
560
  }, 15_000);
561
+ progressRefreshTimer.unref?.();
551
562
  return {
552
563
  render(width) {
553
564
  if (cachedLines && cachedWidth === width)
@@ -789,7 +800,7 @@ export function setCompletionProgressWidget(ctx, snapshot) {
789
800
  if (!ctx.hasUI)
790
801
  return;
791
802
  const widgetKey = "gsd-progress";
792
- ctx.ui.setWidget("gsd-outcome", undefined);
803
+ clearAutoOutcomeWidget(ctx);
793
804
  if (typeof ctx.ui?.setHeader === "function") {
794
805
  ctx.ui.setHeader(() => ({
795
806
  render() { return []; },
@@ -323,6 +323,41 @@ function stripKnownIdPrefix(value, id) {
323
323
  return raw.slice(id.length + 1).trim() || undefined;
324
324
  return raw;
325
325
  }
326
+ function parseReactiveBatchTaskIds(unitId) {
327
+ const { task: batchPart } = parseUnitId(unitId);
328
+ if (!batchPart?.startsWith("reactive+"))
329
+ return [];
330
+ const rawIds = batchPart
331
+ .slice("reactive+".length)
332
+ .split(",")
333
+ .map((taskId) => taskId.trim().toUpperCase())
334
+ .filter(Boolean);
335
+ const unique = new Set();
336
+ for (const taskId of rawIds) {
337
+ unique.add(taskId);
338
+ }
339
+ return [...unique];
340
+ }
341
+ function dedupePaths(values) {
342
+ const seen = new Set();
343
+ const result = [];
344
+ for (const value of values) {
345
+ if (!seen.has(value)) {
346
+ seen.add(value);
347
+ result.push(value);
348
+ }
349
+ }
350
+ return result;
351
+ }
352
+ function getPlannedKeyFiles(tasks) {
353
+ return dedupePaths(tasks.flatMap((taskRow) => [
354
+ ...(taskRow.expected_output ?? []),
355
+ ...(taskRow.files ?? []),
356
+ ...(taskRow.key_files ?? []),
357
+ ]));
358
+ }
359
+ export const _parseReactiveBatchTaskIdsForTest = parseReactiveBatchTaskIds;
360
+ export const _getPlannedKeyFilesForTest = getPlannedKeyFiles;
326
361
  function resolveVerificationFailureMarkerPath(unitType, unitId, basePath) {
327
362
  const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
328
363
  switch (unitType) {
@@ -402,6 +437,34 @@ async function buildTaskCommitContextForUnit(basePath, unitId) {
402
437
  issueNumber: ghIssueNumber,
403
438
  };
404
439
  }
440
+ async function buildReactiveTaskCommitContext(_basePath, unitId) {
441
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
442
+ if (!mid || !sid || !isDbAvailable())
443
+ return undefined;
444
+ const batchTaskIds = parseReactiveBatchTaskIds(unitId);
445
+ if (batchTaskIds.length === 0)
446
+ return undefined;
447
+ const milestone = getMilestone(mid);
448
+ const slice = getSlice(mid, sid);
449
+ const taskRows = batchTaskIds
450
+ .map((tid) => getTask(mid, sid, tid))
451
+ .filter((taskRow) => taskRow !== null);
452
+ const keyFiles = getPlannedKeyFiles(taskRows);
453
+ if (taskRows.length === 0 || keyFiles.length === 0)
454
+ return undefined;
455
+ const taskLabel = taskRows.map((row) => row.id).join(",");
456
+ return {
457
+ taskId: `${sid}/${taskLabel}`,
458
+ taskDisplayId: "reactive-batch",
459
+ taskTitle: `Reactive batch: ${taskLabel}`,
460
+ milestoneId: mid,
461
+ milestoneTitle: stripKnownIdPrefix(milestone?.title, mid),
462
+ sliceId: sid,
463
+ sliceTitle: stripKnownIdPrefix(slice?.title, sid),
464
+ oneLiner: `Reactive execute for ${taskLabel}`,
465
+ keyFiles,
466
+ };
467
+ }
405
468
  async function runPostUnitGitHubSyncIfNeeded(basePath, unit) {
406
469
  if (unit.type === "complete-milestone")
407
470
  return;
@@ -761,6 +824,9 @@ export async function autoCommitUnit(basePath, unitType, unitId, ctx) {
761
824
  if (unitType === "execute-task") {
762
825
  taskContext = await buildTaskCommitContextForUnit(basePath, unitId);
763
826
  }
827
+ else if (unitType === "reactive-execute") {
828
+ taskContext = await buildReactiveTaskCommitContext(basePath, unitId);
829
+ }
764
830
  _resetHasChangesCache();
765
831
  if (LIFECYCLE_ONLY_UNITS.has(unitType)) {
766
832
  return null;
@@ -812,6 +878,22 @@ async function runCloseoutGitAction(pctx, unit, opts) {
812
878
  targetRepositories = getTask(mid, sid, tid)?.target_repositories;
813
879
  }
814
880
  }
881
+ else if (turnAction === "commit" && unit.type === "reactive-execute") {
882
+ taskContext = await buildReactiveTaskCommitContext(s.basePath, unit.id);
883
+ const { milestone: mid, slice: sid } = parseUnitId(unit.id);
884
+ if (mid && sid && isDbAvailable()) {
885
+ const repositories = new Set();
886
+ for (const tid of parseReactiveBatchTaskIds(unit.id)) {
887
+ const taskRow = getTask(mid, sid, tid);
888
+ for (const repoId of taskRow?.target_repositories ?? []) {
889
+ repositories.add(repoId);
890
+ }
891
+ }
892
+ if (repositories.size > 0) {
893
+ targetRepositories = [...repositories];
894
+ }
895
+ }
896
+ }
815
897
  // Invalidate the nativeHasChanges cache before auto-commit (#1853).
816
898
  // The cache has a 10-second TTL and is keyed by basePath. A stale
817
899
  // `false` result causes autoCommit to skip staging entirely.
@@ -1206,12 +1288,19 @@ export async function postUnitPreVerification(pctx, opts) {
1206
1288
  if (safetyConfig.enabled) {
1207
1289
  const { milestone: sMid, slice: sSid, task: sTid } = parseUnitId(s.currentUnit.id);
1208
1290
  // File change validation (execute-task only, after unit execution)
1209
- if (safetyConfig.file_change_validation && s.currentUnit.type === "execute-task" && sMid && sSid && sTid && isDbAvailable()) {
1291
+ if (safetyConfig.file_change_validation && s.currentUnit.type === "execute-task" && sMid && sSid && sTid) {
1210
1292
  try {
1211
- const taskRow = getTask(sMid, sSid, sTid);
1212
- if (taskRow) {
1213
- const expectedOutput = taskRow.expected_output ?? [];
1214
- const plannedFiles = taskRow.files ?? [];
1293
+ const sliceTaskRows = isDbAvailable()
1294
+ ? getSliceTasks(sMid, sSid).filter((t) => isClosedStatus(t.status) || t.id === sTid)
1295
+ : [];
1296
+ if (sliceTaskRows.length > 0) {
1297
+ const expectedOutput = getPlannedKeyFiles(sliceTaskRows.map((taskRow) => ({
1298
+ expected_output: taskRow.expected_output,
1299
+ files: taskRow.files,
1300
+ })));
1301
+ const plannedFiles = getPlannedKeyFiles(sliceTaskRows.map((taskRow) => ({
1302
+ files: taskRow.files,
1303
+ })));
1215
1304
  const audit = validateFileChanges(s.basePath, expectedOutput, plannedFiles, safetyConfig.file_change_allowlist);
1216
1305
  if (audit && audit.violations.length > 0) {
1217
1306
  const warnings = audit.violations.filter(v => v.severity === "warning");
@@ -1223,6 +1312,23 @@ export async function postUnitPreVerification(pctx, opts) {
1223
1312
  }
1224
1313
  }
1225
1314
  }
1315
+ else {
1316
+ const taskRow = getTask(sMid, sSid, sTid);
1317
+ if (taskRow) {
1318
+ const expectedOutput = taskRow.expected_output ?? [];
1319
+ const plannedFiles = taskRow.files ?? [];
1320
+ const audit = validateFileChanges(s.basePath, expectedOutput, plannedFiles, safetyConfig.file_change_allowlist);
1321
+ if (audit && audit.violations.length > 0) {
1322
+ const warnings = audit.violations.filter(v => v.severity === "warning");
1323
+ for (const v of warnings) {
1324
+ logWarning("safety", `file-change: ${v.file} — ${v.reason}`);
1325
+ }
1326
+ if (warnings.length > 0) {
1327
+ ctx.ui.notify(`Safety: ${warnings.length} unexpected file change(s) outside task plan`, "warning");
1328
+ }
1329
+ }
1330
+ }
1331
+ }
1226
1332
  }
1227
1333
  catch (e) {
1228
1334
  debugLog("postUnit", { phase: "safety-file-change", error: String(e) });
@@ -2351,6 +2351,15 @@ export async function buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base,
2351
2351
  sliceSummaryPath,
2352
2352
  sliceUatPath,
2353
2353
  gatesToClose,
2354
+ skillActivation: buildSkillActivationBlock({
2355
+ base,
2356
+ milestoneId: mid,
2357
+ milestoneTitle: midTitle,
2358
+ sliceId: sid,
2359
+ sliceTitle: sTitle,
2360
+ extraContext: [inlinedContext],
2361
+ unitType: "complete-slice",
2362
+ }),
2354
2363
  });
2355
2364
  }
2356
2365
  export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
@@ -49,6 +49,7 @@ import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
49
49
  import { validateDirectory } from "./validate-directory.js";
50
50
  import { isCustomProvider, resolveDefaultSessionModel, resolveDynamicRoutingConfig, } from "./preferences-models.js";
51
51
  import { getSessionModelOverride } from "./session-model-override.js";
52
+ import { setAutoActiveStatus } from "./auto-dashboard.js";
52
53
  export function resolveIsolationNoneBranchCheckout(currentBranch, integrationBranch, isolationMode, isRepo) {
53
54
  if (!isRepo || isolationMode !== "none")
54
55
  return null;
@@ -164,6 +165,19 @@ export function resolveSurvivorRecoveryIsolationMode(isolationMode, phase) {
164
165
  function isBlockingStrandedWorkAction(action) {
165
166
  return action.kind === "in-progress-stranded-work" && action.blocksAuto;
166
167
  }
168
+ function strandedWorkEvidence(args) {
169
+ const evidence = [];
170
+ if (args.branch && args.commitsAhead > 0) {
171
+ evidence.push(`branch ${args.branch} has ${args.commitsAhead} commit(s) ahead of ${args.mainBranch}`);
172
+ }
173
+ if (args.dirtyWorktree) {
174
+ evidence.push("the worktree has uncommitted changes");
175
+ }
176
+ if (evidence.length === 0) {
177
+ evidence.push("physical git evidence exists");
178
+ }
179
+ return evidence;
180
+ }
167
181
  function detectWorktreeEvidence(basePath, milestoneId, hasChanges) {
168
182
  const wtDir = getWorktreeDir(basePath, milestoneId);
169
183
  const wtPath = getAutoWorktreePath(basePath, milestoneId);
@@ -183,16 +197,7 @@ function detectWorktreeEvidence(basePath, milestoneId, hasChanges) {
183
197
  };
184
198
  }
185
199
  function strandedWorkMessage(args) {
186
- const evidence = [];
187
- if (args.branch && args.commitsAhead > 0) {
188
- evidence.push(`branch ${args.branch} has ${args.commitsAhead} commit(s) ahead of ${args.mainBranch}`);
189
- }
190
- if (args.dirtyWorktree) {
191
- evidence.push("the worktree has uncommitted changes");
192
- }
193
- if (evidence.length === 0) {
194
- evidence.push("physical git evidence exists");
195
- }
200
+ const evidence = strandedWorkEvidence(args);
196
201
  const wtSuffix = args.worktreeDirExists
197
202
  ? ` Worktree directory at .gsd/worktrees/${args.milestoneId}/ holds live work.`
198
203
  : "";
@@ -203,6 +208,23 @@ function strandedWorkMessage(args) {
203
208
  wtSuffix +
204
209
  ` ${recovery} Park or discard explicitly if abandoning.`);
205
210
  }
211
+ function formatStrandedWorkRecoveryMessage(action) {
212
+ const recoveryMode = action.recoveryMode === "worktree"
213
+ ? "existing worktree"
214
+ : "milestone branch";
215
+ const evidence = strandedWorkEvidence({
216
+ branch: action.branch,
217
+ commitsAhead: action.commitsAhead ?? 0,
218
+ mainBranch: action.mainBranch ?? "main",
219
+ dirtyWorktree: action.dirtyWorktree ?? false,
220
+ });
221
+ const wtSuffix = action.worktreeDirExists
222
+ ? ` Worktree directory at .gsd/worktrees/${action.milestoneId}/ holds live work.`
223
+ : "";
224
+ return (`Resuming saved milestone work for ${action.milestoneId}: ${evidence.join("; ")}.` +
225
+ wtSuffix +
226
+ ` Adopting the ${recoveryMode} before dispatching new units. Park or discard explicitly if abandoning.`);
227
+ }
206
228
  function formatStrandedWorkBlockerMessage(action, activeMilestoneId) {
207
229
  const target = action.milestoneId;
208
230
  const mode = action.recoveryMode === "worktree" ? "existing worktree" : "milestone branch";
@@ -307,6 +329,7 @@ export function auditOrphanedMilestoneBranches(basePath, _isolationMode, gitDeps
307
329
  kind: "in-progress-stranded-work",
308
330
  milestoneId,
309
331
  branch,
332
+ mainBranch,
310
333
  commitsAhead,
311
334
  dirtyWorktree: worktreeEvidence.dirty,
312
335
  worktreeDirExists: worktreeEvidence.dirExists,
@@ -462,6 +485,7 @@ export function auditOrphanedMilestoneBranches(basePath, _isolationMode, gitDeps
462
485
  pushAction({
463
486
  kind: "in-progress-stranded-work",
464
487
  milestoneId: m.id,
488
+ mainBranch,
465
489
  commitsAhead: 0,
466
490
  dirtyWorktree: true,
467
491
  worktreeDirExists: worktreeEvidence.dirExists,
@@ -890,7 +914,12 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
890
914
  for (const msg of auditResult.recovered) {
891
915
  ctx.ui.notify(`Orphan audit: ${msg}`, "info");
892
916
  }
917
+ const deferredStrandedMessages = new Set(auditResult.actions
918
+ .filter(isBlockingStrandedWorkAction)
919
+ .map((action) => action.message));
893
920
  for (const msg of auditResult.warnings) {
921
+ if (deferredStrandedMessages.has(msg))
922
+ continue;
894
923
  const prefix = msg.startsWith("Stranded work") ? "" : "Orphan audit: ";
895
924
  ctx.ui.notify(`${prefix}${msg}`, "warning");
896
925
  }
@@ -958,7 +987,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
958
987
  return releaseLockAndReturn();
959
988
  }
960
989
  strandedRecoveryAction = blockingStrandedRecoveryAction;
961
- ctx.ui.notify(`Recovering stranded work for ${strandedRecoveryAction.milestoneId} before dispatching new units.`, "info");
990
+ ctx.ui.notify(formatStrandedWorkRecoveryMessage(strandedRecoveryAction), "info");
962
991
  }
963
992
  if (process.env.GSD_HEADLESS === "1" &&
964
993
  orphanAuditRecovered &&
@@ -1343,7 +1372,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
1343
1372
  if (resolveSkillDiscoveryMode(base) !== "off") {
1344
1373
  snapshotSkills();
1345
1374
  }
1346
- ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1375
+ setAutoActiveStatus(ctx, s.stepMode ? "next" : "auto");
1347
1376
  ctx.ui.setWidget("gsd-health", undefined);
1348
1377
  const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
1349
1378
  const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
@@ -1,4 +1,5 @@
1
1
  import { parseUnitId } from "./unit-id.js";
2
+ import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "./tool-presentation-plan.js";
2
3
  export const RUN_UAT_BROWSER_TOOL_NAMES = [
3
4
  "browser_navigate",
4
5
  "browser_click",
@@ -42,7 +43,7 @@ export const AUTO_UNIT_SCOPED_TOOLS = {
42
43
  "execute-task": ["gsd_task_complete", "gsd_decision_save"],
43
44
  "execute-task-simple": ["gsd_task_complete", "gsd_decision_save"],
44
45
  "reactive-execute": ["gsd_task_complete", "gsd_decision_save"],
45
- "run-uat": ["gsd_summary_save", ...RUN_UAT_BROWSER_TOOL_NAMES],
46
+ "run-uat": [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES],
46
47
  "gate-evaluate": ["gsd_save_gate_result"],
47
48
  "rewrite-docs": ["gsd_summary_save", "gsd_decision_save"],
48
49
  "workflow-preferences": ["gsd_summary_save"],
@@ -64,7 +64,7 @@ import { initRegistry, convertDispatchRules } from "./rule-registry.js";
64
64
  import { emitJournalEvent as _emitJournalEvent } from "./journal.js";
65
65
  import { isClosedStatus } from "./status-guards.js";
66
66
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
67
- import { updateProgressWidget as _updateProgressWidget, setCompletionProgressWidget, setAutoOutcomeWidget, updateSliceProgressCache, clearSliceProgressCache, unitVerb, } from "./auto-dashboard.js";
67
+ import { updateProgressWidget as _updateProgressWidget, setCompletionProgressWidget, setAutoOutcomeWidget, setAutoActiveStatus, updateSliceProgressCache, clearSliceProgressCache, unitVerb, } from "./auto-dashboard.js";
68
68
  import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
69
69
  import { isDbAvailable, getMilestone, getMilestoneSlices, getSlice, getTask, refreshOpenDatabaseFromDisk, } from "./gsd-db.js";
70
70
  import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
@@ -2414,7 +2414,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
2414
2414
  const loopDeps = buildLoopDeps(pi);
2415
2415
  ensureOrchestrationModule(ctx, pi, s.basePath || base);
2416
2416
  registerSigtermHandler(lockBase());
2417
- ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
2417
+ setAutoActiveStatus(ctx, s.stepMode ? "next" : "auto");
2418
2418
  ctx.ui.setWidget("gsd-health", undefined);
2419
2419
  ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
2420
2420
  restoreHookState(s.basePath);
@@ -2659,7 +2659,7 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
2659
2659
  resetHookState();
2660
2660
  await pauseAuto(ctx, pi);
2661
2661
  }, hookHardTimeoutMs);
2662
- ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
2662
+ setAutoActiveStatus(ctx, s.stepMode ? "next" : "auto");
2663
2663
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
2664
2664
  debugLog("dispatchHookUnit", {
2665
2665
  phase: "send-message",
@@ -396,6 +396,85 @@ export function registerDbTools(pi) {
396
396
  };
397
397
  pi.registerTool(summarySaveTool);
398
398
  registerAlias(pi, summarySaveTool, "gsd_save_summary", "gsd_summary_save");
399
+ // ─── gsd_uat_result_save ─────────────────────────────────────────────────
400
+ const uatResultSaveExecute = async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
401
+ const { executeUatResultSave } = await loadWorkflowExecutors();
402
+ return executeUatResultSave(params, resolveWorkflowToolBasePath(_ctx, params));
403
+ };
404
+ const uatEvidenceRef = Type.Object({
405
+ kind: StringEnum(["gsd_uat_exec", "gsd_exec", "screenshot", "log", "url", "browser"], { description: "Evidence kind" }),
406
+ ref: Type.String({ description: "Evidence ID, approved .gsd path, or URL" }),
407
+ note: Type.Optional(Type.String({ description: "Short evidence note" })),
408
+ });
409
+ const uatCheck = Type.Object({
410
+ id: Type.String({ description: "Stable check ID from the UAT spec" }),
411
+ description: Type.String({ description: "Check description" }),
412
+ mode: StringEnum(["artifact", "runtime", "browser", "human-follow-up"], { description: "Evidence mode" }),
413
+ result: StringEnum(["PASS", "FAIL", "NEEDS-HUMAN"], { description: "Check result" }),
414
+ evidence: Type.Optional(Type.Array(uatEvidenceRef, { description: "Objective evidence references" })),
415
+ notes: Type.Optional(Type.String({ description: "Observed result, failure notes, or human instruction" })),
416
+ nonAutomatable: Type.Optional(Type.Boolean({ description: "True when the check is explicitly non-automatable" })),
417
+ });
418
+ const toolPresentationBlock = Type.Object({
419
+ surface: StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" }),
420
+ model: Type.Optional(Type.Object({
421
+ provider: Type.Optional(Type.String()),
422
+ api: Type.Optional(Type.String()),
423
+ id: Type.Optional(Type.String()),
424
+ })),
425
+ presentedTools: Type.Array(Type.String(), { description: "Tool names actually presented to the model" }),
426
+ blockedTools: Type.Array(Type.Object({
427
+ name: Type.String(),
428
+ reason: Type.String(),
429
+ }), { description: "Tool names blocked from the model with reasons" }),
430
+ aliases: Type.Optional(Type.Array(Type.Object({
431
+ requested: Type.String(),
432
+ canonical: Type.String(),
433
+ }))),
434
+ fallbackToolsUsed: Type.Optional(Type.Array(Type.String())),
435
+ toolPresentationPlanId: Type.Optional(Type.String()),
436
+ notes: Type.Optional(Type.String()),
437
+ });
438
+ const uatResultSaveTool = {
439
+ name: "gsd_uat_result_save",
440
+ label: "Save UAT Result",
441
+ description: "Save a structured UAT result for a slice. Validates evidence, writes the ASSESSMENT artifact, " +
442
+ "records attempt history, and saves the aggregate UAT gate result.",
443
+ promptSnippet: "Save structured UAT checks, evidence, verdict, and tool-presentation proof",
444
+ promptGuidelines: [
445
+ "Call gsd_uat_result_save once after all UAT checks have been executed.",
446
+ "Every PASS or FAIL check must cite objective evidence, preferably a gsd_uat_exec evidence ID.",
447
+ "Include the presented and blocked tool set in presentation so tool timing is auditable.",
448
+ "Do not use raw gsd_summary_save as a substitute for UAT results.",
449
+ ],
450
+ parameters: Type.Object({
451
+ milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
452
+ sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
453
+ uatType: StringEnum(["artifact-driven", "browser-executable", "runtime-executable", "live-runtime", "mixed", "human-experience"], { description: "Declared UAT mode" }),
454
+ verdict: StringEnum(["PASS", "FAIL", "PARTIAL"], { description: "Overall UAT verdict" }),
455
+ checks: Type.Array(uatCheck, { description: "Structured check results" }),
456
+ presentation: toolPresentationBlock,
457
+ notes: Type.Optional(Type.String({ description: "Overall verdict rationale" })),
458
+ attempt: Type.Optional(Type.String({ description: "Attempt number or auto" })),
459
+ previousAttemptId: Type.Optional(Type.String({ description: "Prior attempt ID, when retrying" })),
460
+ }),
461
+ execute: uatResultSaveExecute,
462
+ renderCall(args, theme) {
463
+ let text = theme.fg("toolTitle", theme.bold("uat_result_save "));
464
+ text += theme.fg("accent", `${args.milestoneId ?? "?"}/${args.sliceId ?? "?"}`);
465
+ if (args.verdict)
466
+ text += theme.fg("dim", ` → ${args.verdict}`);
467
+ return new Text(text, 0, 0);
468
+ },
469
+ renderResult(result, _options, theme) {
470
+ const d = readDetails(result);
471
+ if (result.isError || d?.error) {
472
+ return new Text(theme.fg("error", formatToolErrorText(result, d)), 0, 0);
473
+ }
474
+ return new Text(theme.fg("success", `UAT ${d?.sliceId ?? ""}: ${d?.verdict ?? "saved"}`), 0, 0);
475
+ },
476
+ };
477
+ pi.registerTool(uatResultSaveTool);
399
478
  // ─── gsd_milestone_generate_id (formerly gsd_generate_milestone_id) ────
400
479
  const milestoneGenerateIdExecute = async (_toolCallId, _params, _signal, _onUpdate, _ctx) => {
401
480
  try {
@@ -20,6 +20,49 @@ async function loadContextModePreferences(baseDir) {
20
20
  }
21
21
  }
22
22
  export function registerExecTools(pi) {
23
+ pi.registerTool({
24
+ name: "gsd_uat_exec",
25
+ label: "UAT Exec",
26
+ description: "Run a UAT-scoped bash/node/python check with milestone/slice/check metadata. " +
27
+ "Uses the same capped .gsd/exec evidence store as gsd_exec, but rejects commands that mutate dependencies, git state, credentials, or destructive files.",
28
+ promptSnippet: "Run one UAT check and save typed evidence under .gsd/exec",
29
+ promptGuidelines: [
30
+ "Use gsd_uat_exec for each automated UAT check.",
31
+ "Every PASS/FAIL check saved by gsd_uat_result_save must reference objective evidence from this tool or another approved GSD evidence path.",
32
+ "Do not install packages, mutate git state, edit source files, or dump credentials during UAT.",
33
+ ],
34
+ parameters: Type.Object({
35
+ milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
36
+ sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
37
+ checkId: Type.String({ description: "Stable check ID from the UAT spec (e.g. UAT-01)" }),
38
+ intent: Type.String({
39
+ description: "UAT command intent. Use one canonical value: uat-artifact-check, uat-runtime-check, " +
40
+ "uat-browser-check, uat-service-start, or uat-log-inspection. Short aliases such as artifact, " +
41
+ "runtime, browser, service-start, and log-inspection are accepted.",
42
+ }),
43
+ runtime: Type.Optional(Type.String({
44
+ description: "Optional interpreter. Defaults to bash. Supported: bash, node, python; sh/shell, js/nodejs, and py/python3 aliases are accepted.",
45
+ })),
46
+ script: Type.Optional(Type.String({ description: "Script body. Keep output small (log the finding, not the data)." })),
47
+ command: Type.Optional(Type.String({ description: "Alias for script; defaults to bash when runtime is omitted." })),
48
+ cmd: Type.Optional(Type.String({ description: "Short alias for script." })),
49
+ code: Type.Optional(Type.String({ description: "Alias for script, useful for node/python snippets." })),
50
+ expected: Type.Optional(Type.String({ description: "Expected outcome for this UAT check." })),
51
+ timeout_ms: Type.Optional(Type.Number({
52
+ description: "Per-invocation timeout (ms). Capped at 600000. Default from preferences.",
53
+ minimum: 1_000,
54
+ maximum: 600_000,
55
+ })),
56
+ }),
57
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
58
+ const { executeUatExec } = await import("../tools/exec-tool.js");
59
+ const baseDir = resolveCtxCwd(_ctx);
60
+ return executeUatExec(params, {
61
+ baseDir,
62
+ preferences: await loadContextModePreferences(baseDir),
63
+ });
64
+ },
65
+ });
23
66
  pi.registerTool({
24
67
  name: "gsd_exec",
25
68
  label: "Exec (Sandboxed)",