@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
@@ -413,6 +413,92 @@ export function registerDbTools(pi: ExtensionAPI): void {
413
413
  pi.registerTool(summarySaveTool);
414
414
  registerAlias(pi, summarySaveTool, "gsd_save_summary", "gsd_summary_save");
415
415
 
416
+ // ─── gsd_uat_result_save ─────────────────────────────────────────────────
417
+
418
+ const uatResultSaveExecute = async (_toolCallId: string, params: any, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) => {
419
+ const { executeUatResultSave } = await loadWorkflowExecutors();
420
+ return executeUatResultSave(params, resolveWorkflowToolBasePath(_ctx, params));
421
+ };
422
+
423
+ const uatEvidenceRef = Type.Object({
424
+ kind: StringEnum(["gsd_uat_exec", "gsd_exec", "screenshot", "log", "url", "browser"], { description: "Evidence kind" }),
425
+ ref: Type.String({ description: "Evidence ID, approved .gsd path, or URL" }),
426
+ note: Type.Optional(Type.String({ description: "Short evidence note" })),
427
+ });
428
+
429
+ const uatCheck = Type.Object({
430
+ id: Type.String({ description: "Stable check ID from the UAT spec" }),
431
+ description: Type.String({ description: "Check description" }),
432
+ mode: StringEnum(["artifact", "runtime", "browser", "human-follow-up"], { description: "Evidence mode" }),
433
+ result: StringEnum(["PASS", "FAIL", "NEEDS-HUMAN"], { description: "Check result" }),
434
+ evidence: Type.Optional(Type.Array(uatEvidenceRef, { description: "Objective evidence references" })),
435
+ notes: Type.Optional(Type.String({ description: "Observed result, failure notes, or human instruction" })),
436
+ nonAutomatable: Type.Optional(Type.Boolean({ description: "True when the check is explicitly non-automatable" })),
437
+ });
438
+
439
+ const toolPresentationBlock = Type.Object({
440
+ surface: StringEnum(["provider-tools", "claude-code-sdk", "mcp", "hybrid"], { description: "Tool presentation surface" }),
441
+ model: Type.Optional(Type.Object({
442
+ provider: Type.Optional(Type.String()),
443
+ api: Type.Optional(Type.String()),
444
+ id: Type.Optional(Type.String()),
445
+ })),
446
+ presentedTools: Type.Array(Type.String(), { description: "Tool names actually presented to the model" }),
447
+ blockedTools: Type.Array(Type.Object({
448
+ name: Type.String(),
449
+ reason: Type.String(),
450
+ }), { description: "Tool names blocked from the model with reasons" }),
451
+ aliases: Type.Optional(Type.Array(Type.Object({
452
+ requested: Type.String(),
453
+ canonical: Type.String(),
454
+ }))),
455
+ fallbackToolsUsed: Type.Optional(Type.Array(Type.String())),
456
+ toolPresentationPlanId: Type.Optional(Type.String()),
457
+ notes: Type.Optional(Type.String()),
458
+ });
459
+
460
+ const uatResultSaveTool = {
461
+ name: "gsd_uat_result_save",
462
+ label: "Save UAT Result",
463
+ description:
464
+ "Save a structured UAT result for a slice. Validates evidence, writes the ASSESSMENT artifact, " +
465
+ "records attempt history, and saves the aggregate UAT gate result.",
466
+ promptSnippet: "Save structured UAT checks, evidence, verdict, and tool-presentation proof",
467
+ promptGuidelines: [
468
+ "Call gsd_uat_result_save once after all UAT checks have been executed.",
469
+ "Every PASS or FAIL check must cite objective evidence, preferably a gsd_uat_exec evidence ID.",
470
+ "Include the presented and blocked tool set in presentation so tool timing is auditable.",
471
+ "Do not use raw gsd_summary_save as a substitute for UAT results.",
472
+ ],
473
+ parameters: Type.Object({
474
+ milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
475
+ sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
476
+ uatType: StringEnum(["artifact-driven", "browser-executable", "runtime-executable", "live-runtime", "mixed", "human-experience"], { description: "Declared UAT mode" }),
477
+ verdict: StringEnum(["PASS", "FAIL", "PARTIAL"], { description: "Overall UAT verdict" }),
478
+ checks: Type.Array(uatCheck, { description: "Structured check results" }),
479
+ presentation: toolPresentationBlock,
480
+ notes: Type.Optional(Type.String({ description: "Overall verdict rationale" })),
481
+ attempt: Type.Optional(Type.String({ description: "Attempt number or auto" })),
482
+ previousAttemptId: Type.Optional(Type.String({ description: "Prior attempt ID, when retrying" })),
483
+ }),
484
+ execute: uatResultSaveExecute,
485
+ renderCall(args: any, theme: any) {
486
+ let text = theme.fg("toolTitle", theme.bold("uat_result_save "));
487
+ text += theme.fg("accent", `${args.milestoneId ?? "?"}/${args.sliceId ?? "?"}`);
488
+ if (args.verdict) text += theme.fg("dim", ` → ${args.verdict}`);
489
+ return new Text(text, 0, 0);
490
+ },
491
+ renderResult(result: any, _options: any, theme: any) {
492
+ const d = readDetails(result);
493
+ if (result.isError || d?.error) {
494
+ return new Text(theme.fg("error", formatToolErrorText(result, d)), 0, 0);
495
+ }
496
+ return new Text(theme.fg("success", `UAT ${d?.sliceId ?? ""}: ${d?.verdict ?? "saved"}`), 0, 0);
497
+ },
498
+ };
499
+
500
+ pi.registerTool(uatResultSaveTool);
501
+
416
502
  // ─── gsd_milestone_generate_id (formerly gsd_generate_milestone_id) ────
417
503
 
418
504
  const milestoneGenerateIdExecute = async (_toolCallId: string, _params: any, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) => {
@@ -25,6 +25,57 @@ async function loadContextModePreferences(baseDir: string) {
25
25
  }
26
26
 
27
27
  export function registerExecTools(pi: ExtensionAPI): void {
28
+ pi.registerTool({
29
+ name: "gsd_uat_exec",
30
+ label: "UAT Exec",
31
+ description:
32
+ "Run a UAT-scoped bash/node/python check with milestone/slice/check metadata. " +
33
+ "Uses the same capped .gsd/exec evidence store as gsd_exec, but rejects commands that mutate dependencies, git state, credentials, or destructive files.",
34
+ promptSnippet: "Run one UAT check and save typed evidence under .gsd/exec",
35
+ promptGuidelines: [
36
+ "Use gsd_uat_exec for each automated UAT check.",
37
+ "Every PASS/FAIL check saved by gsd_uat_result_save must reference objective evidence from this tool or another approved GSD evidence path.",
38
+ "Do not install packages, mutate git state, edit source files, or dump credentials during UAT.",
39
+ ],
40
+ parameters: Type.Object({
41
+ milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
42
+ sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
43
+ checkId: Type.String({ description: "Stable check ID from the UAT spec (e.g. UAT-01)" }),
44
+ intent: Type.String({
45
+ description:
46
+ "UAT command intent. Use one canonical value: uat-artifact-check, uat-runtime-check, " +
47
+ "uat-browser-check, uat-service-start, or uat-log-inspection. Short aliases such as artifact, " +
48
+ "runtime, browser, service-start, and log-inspection are accepted.",
49
+ }),
50
+ runtime: Type.Optional(
51
+ Type.String({
52
+ description:
53
+ "Optional interpreter. Defaults to bash. Supported: bash, node, python; sh/shell, js/nodejs, and py/python3 aliases are accepted.",
54
+ }),
55
+ ),
56
+ script: Type.Optional(Type.String({ description: "Script body. Keep output small (log the finding, not the data)." })),
57
+ command: Type.Optional(Type.String({ description: "Alias for script; defaults to bash when runtime is omitted." })),
58
+ cmd: Type.Optional(Type.String({ description: "Short alias for script." })),
59
+ code: Type.Optional(Type.String({ description: "Alias for script, useful for node/python snippets." })),
60
+ expected: Type.Optional(Type.String({ description: "Expected outcome for this UAT check." })),
61
+ timeout_ms: Type.Optional(
62
+ Type.Number({
63
+ description: "Per-invocation timeout (ms). Capped at 600000. Default from preferences.",
64
+ minimum: 1_000,
65
+ maximum: 600_000,
66
+ }),
67
+ ),
68
+ }),
69
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
70
+ const { executeUatExec } = await import("../tools/exec-tool.js");
71
+ const baseDir = resolveCtxCwd(_ctx);
72
+ return executeUatExec(params as Parameters<typeof executeUatExec>[0], {
73
+ baseDir,
74
+ preferences: await loadContextModePreferences(baseDir),
75
+ });
76
+ },
77
+ });
78
+
28
79
  pi.registerTool({
29
80
  name: "gsd_exec",
30
81
  label: "Exec (Sandboxed)",
@@ -36,7 +36,9 @@ import { resolveSkillManifest } from "../skill-manifest.js";
36
36
  import { applyUnitSkillVisibility, unitHasSkillManifest } from "../skill-scope.js";
37
37
  import { getGuidedUnitContext } from "../guided-unit-context.js";
38
38
  import { registerPlanMilestoneSchemaRecovery } from "./plan-milestone-schema-recovery.js";
39
- import { AUTO_UNIT_SCOPED_TOOLS, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
39
+ import { AUTO_UNIT_SCOPED_TOOLS, RUN_UAT_BROWSER_TOOL_NAMES, isWorkflowAliasTool } from "../auto-unit-tool-scope.js";
40
+ import { filterToolsForProvider } from "../model-router.js";
41
+ import { RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
40
42
 
41
43
  let approvalQuestionAbortInFlight = false;
42
44
 
@@ -123,6 +125,7 @@ export const MINIMAL_GSD_TOOL_NAMES = [
123
125
  "gsd_resume",
124
126
  "gsd_milestone_status",
125
127
  "gsd_checkpoint_db",
128
+ "gsd_plan_milestone",
126
129
  "memory_query",
127
130
  "capture_thought",
128
131
  ] as const;
@@ -226,6 +229,9 @@ export function buildMinimalAutoGsdToolSet(
226
229
  unitType: string | undefined,
227
230
  registeredToolNames: readonly string[] = activeToolNames,
228
231
  ): string[] {
232
+ if (unitType === "run-uat") {
233
+ return buildRunUatGsdToolSet(activeToolNames, registeredToolNames);
234
+ }
229
235
  const unitTools = unitType ? AUTO_UNIT_SCOPED_TOOLS[unitType] ?? [] : [];
230
236
  const autoBaseTools = new Set<string>(MINIMAL_AUTO_BASE_TOOL_NAMES);
231
237
  const availableBaseTools = registeredToolNames.filter((name) => autoBaseTools.has(name));
@@ -240,6 +246,17 @@ export function buildMinimalAutoGsdToolSet(
240
246
  return withPreservedShimTools([...new Set([...preserved, ...scoped])]);
241
247
  }
242
248
 
249
+ export function buildRunUatGsdToolSet(
250
+ activeToolNames: readonly string[],
251
+ registeredToolNames: readonly string[] = activeToolNames,
252
+ ): string[] {
253
+ const scoped = resolveScopedToolNames(
254
+ [...activeToolNames, ...registeredToolNames],
255
+ [...RUN_UAT_WORKFLOW_TOOL_NAMES, "subagent", ...RUN_UAT_BROWSER_TOOL_NAMES],
256
+ );
257
+ return [...new Set(scoped)];
258
+ }
259
+
243
260
  export function buildMinimalGsdWorkflowToolSet(
244
261
  activeToolNames: readonly string[],
245
262
  registeredToolNames: readonly string[] = activeToolNames,
@@ -1022,9 +1039,8 @@ export function registerHooks(
1022
1039
  if (result.block) return result;
1023
1040
  });
1024
1041
 
1025
- // ── Safety harness: evidence collection + destructive command warnings ──
1042
+ // ── Safety harness: evidence collection + destructive command blocking ──
1026
1043
  pi.on("tool_call", async (event, ctx) => {
1027
- if (!isAutoActive()) return;
1028
1044
  markToolStart(event.toolCallId, event.toolName);
1029
1045
  safetyRecordToolCall(event.toolCallId, event.toolName, event.input as Record<string, unknown>);
1030
1046
 
@@ -1041,17 +1057,28 @@ export function registerHooks(
1041
1057
  }
1042
1058
  }
1043
1059
 
1044
- // Destructive command classification (warn only, never block)
1060
+ // Destructive command classification + hard gate in all modes.
1045
1061
  if (isToolCallEventType("bash", event)) {
1046
1062
  const classification = classifyCommand(event.input.command);
1047
1063
  if (classification.destructive) {
1064
+ const reason = [
1065
+ "HARD BLOCK: destructive Bash command requires explicit human confirmation.",
1066
+ `Detected: ${classification.labels.join(", ")}`,
1067
+ "Run this via ask_user_questions, wait for the user's response,",
1068
+ "then issue the command only when confirmed in the current turn.",
1069
+ ].join(" ");
1048
1070
  safetyLogWarning("safety", `destructive command: ${classification.labels.join(", ")}`, {
1049
1071
  command: String(event.input.command).slice(0, 200),
1050
1072
  });
1051
- ctx.ui.notify(
1052
- `Destructive command detected: ${classification.labels.join(", ")}`,
1053
- "warning",
1054
- );
1073
+ if (ctx) {
1074
+ await maybePauseAutoForApprovalGate(
1075
+ ctx,
1076
+ pi,
1077
+ isAutoActive(),
1078
+ "Depth confirmation is waiting for your answer — pausing auto-mode.",
1079
+ );
1080
+ }
1081
+ return { block: true, reason };
1055
1082
  }
1056
1083
  }
1057
1084
  });
@@ -1320,19 +1347,27 @@ export function registerHooks(
1320
1347
  const fullToolsRequested = isFullGsdToolSurfaceRequested();
1321
1348
  const dropAliases = !fullToolsRequested;
1322
1349
  const dropBrowser = !fullToolsRequested && !isBrowserToolSurfaceRequested();
1323
- const providerCompatible = compatible.filter(
1324
- (name) => !(dropAliases && isWorkflowAliasTool(name)) && !(dropBrowser && isBrowserTool(name)),
1350
+ const aliasFilteredCompatible = compatible.filter(
1351
+ (name) => !(dropAliases && isWorkflowAliasTool(name)),
1352
+ );
1353
+ const providerCompatible = aliasFilteredCompatible.filter(
1354
+ (name) => !(dropBrowser && isBrowserTool(name)),
1325
1355
  );
1326
1356
  const surfaceReduced = providerCompatible.length !== compatible.length;
1327
1357
  if (fullToolsRequested) {
1328
1358
  return surfaceReduced ? { toolNames: providerCompatible } : undefined;
1329
1359
  }
1330
1360
  const registeredToolNames = resolveRegisteredToolNames(pi, event.activeToolNames);
1361
+ const compatibleRegisteredToolNames = filterToolsForProvider(
1362
+ registeredToolNames,
1363
+ event.selectedModelApi,
1364
+ event.selectedModelProvider,
1365
+ ).compatible.filter((name) => !(dropAliases && isWorkflowAliasTool(name)));
1331
1366
  const guidedUnit = getGuidedUnitContext();
1332
1367
  const requestScoped = buildRequestScopedGsdToolSet(
1333
- providerCompatible,
1368
+ guidedUnit?.unitType === "run-uat" ? aliasFilteredCompatible : providerCompatible,
1334
1369
  event.requestCustomMessages,
1335
- registeredToolNames,
1370
+ guidedUnit?.unitType === "run-uat" ? compatibleRegisteredToolNames : registeredToolNames,
1336
1371
  guidedUnit?.unitType,
1337
1372
  );
1338
1373
  if (requestScoped) {
@@ -1342,9 +1377,11 @@ export function registerHooks(
1342
1377
  if (dash.active && dash.currentUnit) {
1343
1378
  return {
1344
1379
  toolNames: buildMinimalAutoGsdToolSet(
1345
- providerCompatible,
1380
+ dash.currentUnit.type === "run-uat" ? aliasFilteredCompatible : providerCompatible,
1346
1381
  dash.currentUnit.type,
1347
- resolveRegisteredToolNames(pi, event.activeToolNames),
1382
+ dash.currentUnit.type === "run-uat"
1383
+ ? compatibleRegisteredToolNames
1384
+ : resolveRegisteredToolNames(pi, event.activeToolNames),
1348
1385
  ),
1349
1386
  };
1350
1387
  }
@@ -679,6 +679,7 @@ const PLANNING_SUBAGENT_TOOLS = new Set(["subagent", "task"]);
679
679
  * manifests still declare per-unit subsets via ToolsPolicy.allowedSubagents.
680
680
  */
681
681
  const PLANNING_DISPATCH_AGENT_REGISTRY = {
682
+ mnemo: { readOnlySpecialist: true },
682
683
  scout: { readOnlySpecialist: true },
683
684
  planner: { readOnlySpecialist: true },
684
685
  reviewer: { readOnlySpecialist: true },
@@ -692,7 +693,7 @@ export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set<string>(
692
693
  .map(([agentId]) => agentId),
693
694
  );
694
695
 
695
- let warnedMissingPlanningDispatchAgentClasses = false;
696
+ let warnedMissingControlledDispatchAgentClasses = false;
696
697
 
697
698
  function isReadOnlySpecialist(agentId: string): boolean {
698
699
  const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId as keyof typeof PLANNING_DISPATCH_AGENT_REGISTRY];
@@ -703,11 +704,20 @@ function allowedPlanningDispatchAgentsList(): string {
703
704
  return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
704
705
  }
705
706
 
706
- function warnMissingPlanningDispatchAgentClasses(unitType: string, mode: string, toolName: string): void {
707
- if (warnedMissingPlanningDispatchAgentClasses) return;
708
- warnedMissingPlanningDispatchAgentClasses = true;
707
+ function allowsControlledSubagentDispatch(
708
+ policy: ToolsPolicy,
709
+ ): policy is ToolsPolicy & { readonly allowedSubagents: readonly string[] } {
710
+ return (
711
+ (policy.mode === "planning-dispatch" || policy.mode === "verification") &&
712
+ Array.isArray((policy as { readonly allowedSubagents?: unknown }).allowedSubagents)
713
+ );
714
+ }
715
+
716
+ function warnMissingControlledDispatchAgentClasses(unitType: string, mode: string, toolName: string): void {
717
+ if (warnedMissingControlledDispatchAgentClasses) return;
718
+ warnedMissingControlledDispatchAgentClasses = true;
709
719
  // TODO(#5060): Remove this migration shim once all subagent/task callers are verified to forward agent identities.
710
- const message = `[write-gate] planning-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
720
+ const message = `[write-gate] controlled-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
711
721
  `on unit "${unitType}" without agentClasses - stale caller; blocking dispatch.`;
712
722
  console.warn(message);
713
723
  logWarning("intercept", message, {
@@ -777,8 +787,9 @@ function blockReason(unitType: string, mode: string, what: string): string {
777
787
  * - "docs" → like "planning" but also allows writes to paths
778
788
  * matching `allowedPathGlobs` relative to basePath.
779
789
  * - "verification"
780
- * → allows Bash for project verification commands, but keeps
781
- * writes restricted to .gsd/ and blocks subagent dispatch.
790
+ * → allows Bash for project verification commands, keeps
791
+ * writes restricted to .gsd/, and permits subagent dispatch
792
+ * only when the manifest declares allowedSubagents.
782
793
  *
783
794
  * `pathOrCommand` is the file path for write/edit-shaped tools and the
784
795
  * shell command for bash. Other tools ignore this argument.
@@ -825,7 +836,7 @@ export function shouldBlockPlanningUnit(
825
836
  if (tool.startsWith("gsd_")) return { block: false };
826
837
 
827
838
  if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
828
- if (policy.mode === "planning-dispatch") {
839
+ if (allowsControlledSubagentDispatch(policy)) {
829
840
  const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
830
841
  const dispatchContract = compileSubagentPermissionContract(policy);
831
842
  const allowedSubagents = dispatchContract.allowedSubagents;
@@ -834,7 +845,7 @@ export function shouldBlockPlanningUnit(
834
845
  // agent identities yet. Block and warn so stale callers surface in telemetry
835
846
  // instead of silently bypassing the gate.
836
847
  if (agentClasses === undefined) {
837
- warnMissingPlanningDispatchAgentClasses(unitType, policy.mode, tool);
848
+ warnMissingControlledDispatchAgentClasses(unitType, policy.mode, tool);
838
849
  return {
839
850
  block: true,
840
851
  reason: blockReason(
@@ -857,7 +868,7 @@ export function shouldBlockPlanningUnit(
857
868
  reason: blockReason(
858
869
  unitType,
859
870
  policy.mode,
860
- `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from planning-dispatch units`,
871
+ `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from ${policy.mode} units`,
861
872
  ),
862
873
  };
863
874
  }
@@ -75,7 +75,7 @@ export function showHelp(ctx: ExtensionCommandContext, args = ""): void {
75
75
  " /gsd new-milestone Create milestone from headless context (used by gsd headless)",
76
76
  " /gsd new-project Bootstrap a new project (use --deep for staged project-level discovery)",
77
77
  " /gsd quick Execute a quick task without full planning overhead",
78
- " /gsd dispatch Dispatch a specific phase directly [research|plan|execute|complete|uat|replan]",
78
+ " /gsd dispatch Dispatch a specific phase directly [research|plan|execute|complete|validate|reassess|uat|replan]",
79
79
  " /gsd verdict <v> Override milestone validation verdict [pass|needs-attention|needs-remediation] [--milestone Mxxx] [--rationale \"...\"]",
80
80
  " /gsd parallel Parallel milestone orchestration [start|status|stop|pause|resume|merge|watch]",
81
81
  " /gsd workflow Custom workflow lifecycle [new|run|list|validate|pause|resume]",
@@ -23,6 +23,9 @@ import {
23
23
  import { loadFile, saveFile, splitFrontmatter, parseFrontmatterMap } from "./files.js";
24
24
  import { runClaudeImportFlow } from "./claude-import.js";
25
25
 
26
+ const DEFAULT_WIDGET_MODE = "small";
27
+ const WIDGET_MODE_OPTIONS = [DEFAULT_WIDGET_MODE, "full", "min", "off"] as const;
28
+
26
29
  /** Extract body content after frontmatter closing delimiter, or null if none. */
27
30
  function extractBodyAfterFrontmatter(content: string): string | null {
28
31
  const closingIdx = content.indexOf("\n---", content.indexOf("---"));
@@ -1558,7 +1561,7 @@ async function configureAdvanced(ctx: ExtensionCommandContext, prefs: Record<str
1558
1561
  prefs.min_request_interval_ms = minRequestInterval;
1559
1562
  }
1560
1563
 
1561
- const widget = await promptEnum(ctx, "Auto-mode widget display", prefs.widget_mode, ["full", "small", "min", "off"], "full");
1564
+ const widget = await promptEnum(ctx, "Auto-mode widget display", prefs.widget_mode, WIDGET_MODE_OPTIONS, DEFAULT_WIDGET_MODE);
1562
1565
  if (widget !== undefined) prefs.widget_mode = widget;
1563
1566
 
1564
1567
  const experimental = (prefs.experimental as Record<string, unknown> | undefined) ?? {};
@@ -238,7 +238,7 @@ export async function handleVerdict(
238
238
 
239
239
  if (effectiveVerdict === "needs-remediation") {
240
240
  ctx.ui.notify(
241
- "Follow up with gsd_reassess_roadmap to add remediation slices, then re-run /gsd auto.",
241
+ "Follow up with /gsd dispatch reassess to add remediation slices, then re-run /gsd auto.",
242
242
  "info",
243
243
  );
244
244
  }
@@ -23,6 +23,8 @@ import {
23
23
  resolveAutoSupervisorConfig,
24
24
  } from "./preferences.js";
25
25
 
26
+ const DEFAULT_WIDGET_MODE = "small";
27
+
26
28
  // ─── Data Collection ──────────────────────────────────────────────────────
27
29
 
28
30
  interface ConfigSection {
@@ -160,7 +162,7 @@ function collectConfigSections(): ConfigSection[] {
160
162
  if (prefs?.service_tier) toggleRows.push({ label: "service_tier", value: prefs.service_tier });
161
163
  if (prefs?.search_provider && prefs.search_provider !== "auto") toggleRows.push({ label: "search_provider", value: prefs.search_provider });
162
164
  if (prefs?.context_selection) toggleRows.push({ label: "context_selection", value: prefs.context_selection });
163
- if (prefs?.widget_mode && prefs.widget_mode !== "full") toggleRows.push({ label: "widget_mode", value: prefs.widget_mode });
165
+ if (prefs?.widget_mode && prefs.widget_mode !== DEFAULT_WIDGET_MODE) toggleRows.push({ label: "widget_mode", value: prefs.widget_mode });
164
166
  if (prefs?.experimental?.rtk) toggleRows.push({ label: "experimental.rtk", value: "on" });
165
167
  if (toggleRows.length > 0) sections.push({ title: "Toggles", rows: toggleRows });
166
168
 
@@ -47,9 +47,10 @@ export function resetRetryState(state: RetryState): void {
47
47
  const PERMANENT_RE = /auth|unauthorized|forbidden|invalid.*key|invalid.*api|billing|quota exceeded|account/i;
48
48
  // Include provider-specific quota-window phrasing like:
49
49
  // - "You've hit your limit"
50
+ // - "You've reached your limit"
50
51
  // - "usage limit" / "quota reached"
51
52
  // - "out of extra usage"
52
- const RATE_LIMIT_RE = /rate.?limit|too many requests|429|hit your limit|usage limit|out of extra usage|quota (?:reached|hit)|limit.*resets?/i;
53
+ const RATE_LIMIT_RE = /rate.?limit|too many requests|429|(?:hit|reached) your (?:\w+ )?limit|(?:usage|session|weekly|daily|monthly|quota) limit|out of extra usage|quota (?:reached|hit)|limit.*resets?/i;
53
54
  // OpenRouter affordability-style quota errors should be treated as transient
54
55
  // so core retry logic can lower maxTokens and continue in-session.
55
56
  const AFFORDABILITY_RE = /requires more credits|can only afford|insufficient credits|not enough credits|fewer max_tokens/i;
@@ -20,6 +20,8 @@ export interface ExecSandboxRequest {
20
20
  script: string;
21
21
  /** Optional purpose/label recorded in meta.json. */
22
22
  purpose?: string;
23
+ /** Optional structured metadata recorded in meta.json. */
24
+ metadata?: Record<string, unknown>;
23
25
  /** Per-invocation timeout in ms. Clamped to `clamp_timeout_ms`. */
24
26
  timeout_ms?: number;
25
27
  }
@@ -315,6 +317,7 @@ function writeMeta(
315
317
  id: result.id,
316
318
  runtime: result.runtime,
317
319
  purpose: request.purpose ?? null,
320
+ ...(request.metadata ? { metadata: request.metadata } : {}),
318
321
  script_chars: request.script.length,
319
322
  started_at: now.toISOString(),
320
323
  finished_at: new Date(now.getTime() + result.duration_ms).toISOString(),
@@ -328,6 +331,7 @@ function writeMeta(
328
331
  stderr_truncated: result.stderr_truncated,
329
332
  stdout_path: result.stdout_path,
330
333
  stderr_path: result.stderr_path,
334
+ ...(request.metadata ? { metadata: request.metadata } : {}),
331
335
  };
332
336
  writeFileSync(path, `${JSON.stringify(meta, null, 2)}\n`);
333
337
  }
@@ -423,7 +423,7 @@ export interface GSDPreferences {
423
423
  search_provider?: "brave" | "tavily" | "ollama" | "native" | "auto";
424
424
  /** Context selection mode for file inlining. "full" inlines entire files, "smart" uses semantic chunking. Default derived from token profile. */
425
425
  context_selection?: ContextSelectionMode;
426
- /** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */
426
+ /** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "small". */
427
427
  widget_mode?: "full" | "small" | "min" | "off";
428
428
  /** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
429
429
  reactive_execution?: ReactiveExecutionConfig;
@@ -37,7 +37,13 @@ You are the UAT runner. Execute every check defined in `{{uatPath}}` as deeply a
37
37
 
38
38
  Choose the lightest tool that proves the check honestly:
39
39
 
40
- - Run shell commands with `bash`
40
+ - Run automated checks with `gsd_uat_exec`
41
+ - Use `uat-artifact-check` as `intent` for static file, grep, structure, or artifact checks.
42
+ - Use `uat-runtime-check` as `intent` for executing tests, scripts, or runtime assertions.
43
+ - Use `uat-browser-check` as `intent` for browser interaction or screenshot-backed UI checks.
44
+ - Use `uat-service-start` as `intent` only when starting or connecting to an app/service.
45
+ - Use `uat-log-inspection` as `intent` for checking logs or captured output files.
46
+ - The result-table evidence mode is separate; do not use `artifact`, `runtime`, or `human-follow-up` as `intent`.
41
47
  - Run `grep` / `rg` checks against files
42
48
  - Run `node` / other script invocations
43
49
  - Read files and verify their contents
@@ -48,7 +54,7 @@ Choose the lightest tool that proves the check honestly:
48
54
  For each check, record:
49
55
  - The check description (from the UAT file)
50
56
  - The evidence mode used: `artifact`, `runtime`, or `human-follow-up`
51
- - The command or action taken
57
+ - The command or action taken, including the `gsd_uat_exec` evidence ID for automated checks
52
58
  - The actual result observed
53
59
  - `PASS`, `FAIL`, or `NEEDS-HUMAN`
54
60
 
@@ -57,7 +63,7 @@ After running all checks, compute the **overall verdict**:
57
63
  - `FAIL` — one or more automatable checks failed
58
64
  - `PARTIAL` — one or more automatable checks were skipped or returned inconclusive results (not the same as `NEEDS-HUMAN` — use PARTIAL only when the agent itself could not determine pass/fail for a check it was supposed to automate)
59
65
 
60
- Call `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `slice_id: {{sliceId}}`, `artifact_type: "ASSESSMENT"`, and the full UAT result markdown as `content` the tool computes the file path and persists to both DB and disk. The content should follow this format:
66
+ Call `gsd_summary_save` with `milestone_id: "{{milestoneId}}"`, `slice_id: "{{sliceId}}"`, `artifact_type: "ASSESSMENT"`, and the full UAT result markdown as `content`. The tool computes the assessment path, persists to DB/disk, and saves the aggregate UAT gate. The content should follow this logical shape:
61
67
 
62
68
  ```markdown
63
69
  ---
@@ -86,6 +92,6 @@ date: <ISO 8601 timestamp>
86
92
 
87
93
  ---
88
94
 
89
- **You MUST call `gsd_summary_save` with the UAT result content before finishing.**
95
+ **You MUST call `gsd_summary_save` with `artifact_type: "ASSESSMENT"` and the UAT result content before finishing. Do not write the assessment file directly.**
90
96
 
91
97
  When done, say: "UAT {{sliceId}} complete."
@@ -32,7 +32,7 @@ GSD ships with bundled skills. Installed skills are listed in `<available_skills
32
32
  - Never print, echo, log, or restate secrets or credentials. Report only key names and applied/skipped status.
33
33
  - Never ask the user to edit `.env` files or set secrets manually. Use `secure_env_collect`.
34
34
  - In enduring files, write current state only unless the file is explicitly historical.
35
- - **Never take outward-facing actions on GitHub or external services without explicit user confirmation.** This includes creating/closing issues, merging/approving/commenting on PRs, pushing remote branches, publishing packages, or any state change outside local filesystem. Read-only listing/viewing/diffing is fine. Present intent and get a clear "yes" first. **Non-bypassable:** no response, ambiguity, or `ask_user_questions` failure means re-ask; never rationalize past the block. Missing "yes" means "no."
35
+ - **Never take outward-facing actions on GitHub or external services without explicit user confirmation.** This includes creating/closing issues, merging/approving/commenting on PRs, pushing remote branches, publishing packages, terragrunt/aws/kubectl mutations, or any state change outside local filesystem. Read-only listing/viewing/diffing is fine. Present intent and get a clear "yes" first. **Non-bypassable:** no response, ambiguity, or `ask_user_questions` failure means re-ask; never rationalize past the block. Missing "yes" means "no."
36
36
 
37
37
  If a `GSD Skill Preferences` block appears below, treat it as durable guidance for skills to use, prefer, or avoid unless it conflicts with artifact rules, verification, or higher-priority instructions.
38
38
 
@@ -160,4 +160,6 @@ Fix root causes, not symptoms. If applying temporary mitigation, label it and pr
160
160
  - When debugging, stay curious. Problems are puzzles. Say what's interesting about the failure before reaching for fixes.
161
161
  - After completing a task, give a brief summary and 2-4 numbered next-step options; last option is always "Other". Omit the list for strict output formats.
162
162
 
163
+ If any next step is destructive/outward-facing, present it via `ask_user_questions` and wait for the user's answer before execution. Do not execute a next-step item from a prior plain-text numbered list without fresh confirmation.
164
+
163
165
  Good narration states a decision or finding: "Three handlers follow a middleware pattern - using that instead of a custom wrapper." Bad narration just announces the next call ("Reading the file now.") or emits compressed planner notes ("Need create plan artifact maybe read existing plans.").
@@ -24,6 +24,9 @@ const DESTRUCTIVE_PATTERNS: readonly DestructivePattern[] = [
24
24
  { pattern: /\btruncate\s+table\b/i, label: "SQL truncate" },
25
25
  { pattern: /\bchmod\s+777\b/, label: "world-writable permissions" },
26
26
  { pattern: /\bcurl\s.*\|\s*(bash|sh|zsh)\b/, label: "pipe to shell" },
27
+ { pattern: /\bterra(form|grunt)\s+(apply|destroy)/i, label: "IaC apply/destroy" },
28
+ { pattern: /\baws\s+\w+\s+(delete|create|put|remove|terminate)\b/i, label: "AWS mutation" },
29
+ { pattern: /\bkubectl\s+(delete|apply)\b/i, label: "kubectl mutation" },
27
30
  ];
28
31
 
29
32
  // ─── Public API ─────────────────────────────────────────────────────────────
@@ -50,6 +50,16 @@ function tokenizeSkillContext(...parts: Array<string | null | undefined>): Set<s
50
50
  return tokens;
51
51
  }
52
52
 
53
+ function tokenizeUnitType(unitType: string | undefined): Set<string> {
54
+ const tokens = new Set<string>();
55
+ const value = unitType?.trim().toLowerCase();
56
+ if (!value) return tokens;
57
+ tokens.add(value);
58
+ tokens.add(value.replace(/[-_]+/g, " "));
59
+ tokens.add(value.replace(/[-_\s]+/g, ""));
60
+ return tokens;
61
+ }
62
+
53
63
  function skillMatchesContext(skill: Skill, contextTokens: Set<string>): boolean {
54
64
  const haystacks = [
55
65
  skill.name.toLowerCase(),
@@ -79,17 +89,25 @@ function ruleMatchesContext(when: string, contextTokens: Set<string>): boolean {
79
89
  );
80
90
  }
81
91
 
92
+ function ruleMatchesUnitType(when: string, unitType: string | undefined): boolean {
93
+ if (!unitType) return false;
94
+ const whenTokens = tokenizeSkillContext(when);
95
+ const unitTokens = tokenizeUnitType(unitType);
96
+ return [...unitTokens].some(token => whenTokens.has(token));
97
+ }
98
+
82
99
  function resolveSkillRuleMatches(
83
100
  prefs: GSDPreferences | undefined,
84
101
  contextTokens: Set<string>,
85
102
  base: string,
103
+ unitType?: string,
86
104
  ): { include: string[]; avoid: string[] } {
87
105
  if (!prefs?.skill_rules?.length) return { include: [], avoid: [] };
88
106
 
89
107
  const include: string[] = [];
90
108
  const avoid: string[] = [];
91
109
  for (const rule of prefs.skill_rules) {
92
- if (!ruleMatchesContext(rule.when, contextTokens)) continue;
110
+ if (!ruleMatchesContext(rule.when, contextTokens) && !ruleMatchesUnitType(rule.when, unitType)) continue;
93
111
  include.push(...resolvePreferenceSkillNames([...(rule.use ?? []), ...(rule.prefer ?? [])], base));
94
112
  avoid.push(...resolvePreferenceSkillNames(rule.avoid ?? [], base));
95
113
  }
@@ -196,7 +214,7 @@ export function buildSkillActivationBlock(params: {
196
214
  matched.add(name);
197
215
  }
198
216
 
199
- const ruleMatches = resolveSkillRuleMatches(prefs, contextTokens, params.base);
217
+ const ruleMatches = resolveSkillRuleMatches(prefs, contextTokens, params.base, params.unitType);
200
218
  for (const name of ruleMatches.include) matched.add(name);
201
219
  for (const name of ruleMatches.avoid) avoided.add(name);
202
220