@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
@@ -9,6 +9,7 @@ import { existsSync, readFileSync } from "node:fs";
9
9
  import {
10
10
  getMilestone,
11
11
  getMilestoneSlices,
12
+ getSliceTasks,
12
13
  isDbAvailable,
13
14
  } from "../../gsd-db.js";
14
15
  import { renderRoadmapFromDb } from "../../markdown-renderer.js";
@@ -30,6 +31,19 @@ function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
30
31
  return true;
31
32
  }
32
33
 
34
+ function getSlicesReadyForDivergenceCheck(
35
+ milestoneId: string,
36
+ dbSlices: ReturnType<typeof getMilestoneSlices>,
37
+ ): Set<string> {
38
+ const ready = new Set<string>();
39
+ for (const slice of dbSlices) {
40
+ if (isClosedStatus(slice.status) || getSliceTasks(milestoneId, slice.id).length > 0) {
41
+ ready.add(slice.id);
42
+ }
43
+ }
44
+ return ready;
45
+ }
46
+
33
47
  function milestoneHasDivergence(
34
48
  basePath: string,
35
49
  milestoneId: string,
@@ -46,6 +60,10 @@ function milestoneHasDivergence(
46
60
 
47
61
  const dbSlices = getMilestoneSlices(milestoneId);
48
62
  const dbSliceMap = new Map(dbSlices.map((s) => [s.id, s]));
63
+ const readySliceIds = getSlicesReadyForDivergenceCheck(milestoneId, dbSlices);
64
+ if (dbSlices.length > 0 && readySliceIds.size === 0) {
65
+ return false;
66
+ }
49
67
  const roadmapSliceIds = new Set<string>();
50
68
 
51
69
  for (let i = 0; i < roadmap.slices.length; i++) {
@@ -54,11 +72,13 @@ function milestoneHasDivergence(
54
72
  const expectedSequence = i + 1;
55
73
  const dbSlice = dbSliceMap.get(roadmapSlice.id);
56
74
  if (!dbSlice) return true; // Roadmap has a slice the DB doesn't.
75
+ if (!readySliceIds.has(dbSlice.id)) continue;
57
76
  if (dbSlice.sequence !== expectedSequence) return true;
58
77
  if (!arraysEqual(dbSlice.depends, roadmapSlice.depends)) return true;
59
78
  if (isClosedStatus(dbSlice.status) !== roadmapSlice.done) return true;
60
79
  }
61
80
  for (const dbSlice of dbSlices) {
81
+ if (!readySliceIds.has(dbSlice.id)) continue;
62
82
  if (!roadmapSliceIds.has(dbSlice.id)) return true;
63
83
  }
64
84
  return false;
@@ -6,6 +6,7 @@ import {
6
6
  deriveState as defaultDeriveState,
7
7
  invalidateStateCache as defaultInvalidate,
8
8
  } from "../state.js";
9
+ import { clearParseCache as defaultClearParseCache } from "../files.js";
9
10
  import type { GSDState } from "../types.js";
10
11
 
11
12
  import {
@@ -37,6 +38,7 @@ const MAX_PASSES = 2;
37
38
  const defaultDeps: ReconciliationDeps = {
38
39
  invalidateStateCache: defaultInvalidate,
39
40
  deriveState: defaultDeriveState,
41
+ clearParseCache: defaultClearParseCache,
40
42
  };
41
43
 
42
44
  /**
@@ -58,6 +60,7 @@ export async function reconcileBeforeDispatch(
58
60
  deps: ReconciliationDeps = defaultDeps,
59
61
  ): Promise<ReconciliationResult> {
60
62
  const registry = deps.registry ?? DRIFT_REGISTRY;
63
+ const clearParseCache = deps.clearParseCache ?? defaultClearParseCache;
61
64
  const repaired: DriftRecord[] = [];
62
65
 
63
66
  for (let pass = 0; pass < MAX_PASSES; pass++) {
@@ -103,6 +106,9 @@ export async function reconcileBeforeDispatch(
103
106
  }
104
107
  }
105
108
 
109
+ if (repairedThisPass) {
110
+ clearParseCache();
111
+ }
106
112
  if (blockers.length > 0) {
107
113
  let blockerState = stateSnapshot;
108
114
  if (repairedThisPass) {
@@ -101,6 +101,7 @@ export interface ReconciliationDeps {
101
101
  basePath: string,
102
102
  opts?: DeriveStateOptions,
103
103
  ) => Promise<GSDState>;
104
+ clearParseCache?: () => void;
104
105
  /**
105
106
  * Override of the drift handler catalog. Defaults to DRIFT_REGISTRY. Each
106
107
  * handler is parameterized over its own DriftRecord variant; the union of
@@ -83,7 +83,7 @@ function formatNeedsRemediationBlocker(milestoneId: string): string {
83
83
  return [
84
84
  `Milestone ${milestoneId} is blocked because milestone validation returned needs-remediation, but all slices are complete.`,
85
85
  `Fix options:`,
86
- `1. Add remediation slices with \`gsd_reassess_roadmap\`, then run \`/gsd auto\``,
86
+ `1. Run \`/gsd dispatch reassess\` to add remediation slices, then run \`/gsd auto\``,
87
87
  `2. If the finding is acceptable, override it: \`/gsd verdict pass --rationale "why this is okay"\``,
88
88
  `3. If this should wait, defer it explicitly: \`/gsd park ${milestoneId}\``,
89
89
  ].join("\n");
@@ -16,11 +16,13 @@ import {
16
16
  buildPhaseHandoffOutcome,
17
17
  updateProgressWidget,
18
18
  setAutoOutcomeWidget,
19
+ setAutoActiveStatus,
19
20
  setCompletionProgressWidget,
20
21
  getRoadmapSlicesSync,
21
22
  clearSliceProgressCache,
22
23
  getWidgetMode,
23
24
  cycleWidgetMode,
25
+ setWidgetMode,
24
26
  _resetWidgetModeForTests,
25
27
  _resetLastCommitCacheForTests,
26
28
  _refreshLastCommitForTests,
@@ -78,6 +80,26 @@ test("unitVerb handles hook types", () => {
78
80
  assert.equal(unitVerb("hook/"), "hook: ");
79
81
  });
80
82
 
83
+ test("setAutoActiveStatus clears stale outcome surfaces", () => {
84
+ const statusCalls: Array<[string, string]> = [];
85
+ const widgetCalls: Array<[string, unknown]> = [];
86
+
87
+ setAutoActiveStatus({
88
+ hasUI: true,
89
+ ui: {
90
+ setStatus: (key: string, value: string) => {
91
+ statusCalls.push([key, value]);
92
+ },
93
+ setWidget: (key: string, value: unknown) => {
94
+ widgetCalls.push([key, value]);
95
+ },
96
+ },
97
+ } as any, "next");
98
+
99
+ assert.deepEqual(statusCalls, [["gsd-auto", "next"]]);
100
+ assert.deepEqual(widgetCalls, [["gsd-outcome", undefined]]);
101
+ });
102
+
81
103
  // ─── unitPhaseLabel ───────────────────────────────────────────────────────
82
104
 
83
105
  test("unitPhaseLabel maps known types to labels", () => {
@@ -571,14 +593,21 @@ test("updateProgressWidget refreshes slice progress cache immediately", (t) => {
571
593
  test("updateProgressWidget full mode keeps footer-owned signals out of auto deck", (t) => {
572
594
  const dir = makeTempDir("command-deck");
573
595
  mkdirSync(join(dir, ".gsd"), { recursive: true });
596
+ const projectPrefsPath = join(dir, ".gsd", "preferences.md");
597
+ const globalPrefsPath = join(dir, ".gsd", "global-preferences.md");
598
+ writeFileSync(projectPrefsPath, "---\nversion: 1\n---\n", "utf-8");
574
599
  let widget: { render(width: number): string[]; dispose?: () => void } | null = null;
575
600
 
576
601
  t.after(() => {
577
602
  widget?.dispose?.();
603
+ _resetWidgetModeForTests();
578
604
  clearSliceProgressCache();
579
605
  cleanup(dir);
580
606
  });
581
607
 
608
+ _resetWidgetModeForTests();
609
+ setWidgetMode("full", projectPrefsPath, globalPrefsPath);
610
+
582
611
  updateProgressWidget(
583
612
  {
584
613
  hasUI: true,
@@ -812,3 +841,25 @@ test("widget mode respects project preference precedence and persists there", (t
812
841
  assert.match(projectPrefs, /widget_mode:\s*min/);
813
842
  assert.match(globalPrefs, /widget_mode:\s*off/);
814
843
  });
844
+
845
+ test("widget mode defaults to small when preferences do not set it", (t) => {
846
+ const homeDir = makeTempDir("home-no-widget-pref");
847
+ const projectDir = makeTempDir("project-no-widget-pref");
848
+ const globalPrefsPath = join(homeDir, ".gsd", "preferences.md");
849
+ const projectPrefsPath = join(projectDir, ".gsd", "preferences.md");
850
+
851
+ mkdirSync(join(homeDir, ".gsd"), { recursive: true });
852
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
853
+ writeFileSync(globalPrefsPath, "---\nversion: 1\n---\n", "utf-8");
854
+ writeFileSync(projectPrefsPath, "---\nversion: 1\n---\n", "utf-8");
855
+
856
+ t.after(() => {
857
+ cleanup(homeDir);
858
+ cleanup(projectDir);
859
+ _resetWidgetModeForTests();
860
+ });
861
+
862
+ _resetWidgetModeForTests();
863
+
864
+ assert.equal(getWidgetMode(projectPrefsPath, globalPrefsPath), "small");
865
+ });
@@ -531,8 +531,13 @@ test("bootstrap honors explicit solo milestone lock when recovering stranded tar
531
531
  assert.equal(ready, true);
532
532
  assert.deepEqual(adoptCalls, [{ milestoneId: "M002", mode: "worktree" }]);
533
533
  assert.equal(s.currentMilestoneId, "M002");
534
- assert.match(messages, /Recovering stranded work for M002/);
534
+ assert.match(messages, /Resuming saved milestone work for M002/);
535
535
  assert.doesNotMatch(messages, /blocks auto-mode before M001/);
536
+ assert.doesNotMatch(messages, /Stranded work for in-progress milestone M002/);
537
+ assert.ok(
538
+ notifications.some((entry) => entry.level === "info" && entry.message.includes("Resuming saved milestone work for M002")),
539
+ "active recovery should be presented as an info-level resume",
540
+ );
536
541
  } finally {
537
542
  if (previousLock === undefined) delete process.env.GSD_MILESTONE_LOCK;
538
543
  else process.env.GSD_MILESTONE_LOCK = previousLock;
@@ -624,7 +629,11 @@ test("bootstrap adopts stranded active branch even when isolation is none", asyn
624
629
  assert.equal(s.strandedRecoveryIsolationMode, "branch");
625
630
  assert.match(
626
631
  notifications.map((entry) => entry.message).join("\n"),
627
- /Recovering stranded work for M001/,
632
+ /Resuming saved milestone work for M001/,
633
+ );
634
+ assert.ok(
635
+ notifications.every((entry) => entry.level !== "warning" || !entry.message.includes("Stranded work for in-progress milestone M001")),
636
+ "adopting the active milestone should not emit a scary stranded-work warning",
628
637
  );
629
638
  } finally {
630
639
  try {
@@ -713,7 +722,11 @@ test("bootstrap adopts stranded active branch before deep project setup", async
713
722
  assert.equal(s.strandedRecoveryIsolationMode, "branch");
714
723
  assert.match(
715
724
  notifications.map((entry) => entry.message).join("\n"),
716
- /Recovering stranded work for M001/,
725
+ /Resuming saved milestone work for M001/,
726
+ );
727
+ assert.ok(
728
+ notifications.every((entry) => entry.level !== "warning" || !entry.message.includes("Stranded work for in-progress milestone M001")),
729
+ "adopting the active milestone should not emit a scary stranded-work warning",
717
730
  );
718
731
  } finally {
719
732
  try {
@@ -40,10 +40,12 @@ function makeMockCtx(base: string): {
40
40
  calls: NotifyCall[];
41
41
  widgets: Array<[string, unknown]>;
42
42
  statuses: Array<[string, string | undefined]>;
43
+ newSessions: Array<{ workspaceRoot?: string }>;
43
44
  } {
44
45
  const calls: NotifyCall[] = [];
45
46
  const widgets: Array<[string, unknown]> = [];
46
47
  const statuses: Array<[string, string | undefined]> = [];
48
+ const newSessions: Array<{ workspaceRoot?: string }> = [];
47
49
  return {
48
50
  ctx: {
49
51
  cwd: base,
@@ -58,10 +60,15 @@ function makeMockCtx(base: string): {
58
60
  statuses.push([key, value]);
59
61
  },
60
62
  },
63
+ newSession: async (options?: { workspaceRoot?: string }) => {
64
+ newSessions.push(options ?? {});
65
+ return { cancelled: false };
66
+ },
61
67
  },
62
68
  calls,
63
69
  widgets,
64
70
  statuses,
71
+ newSessions,
65
72
  };
66
73
  }
67
74
 
@@ -77,7 +84,10 @@ function makeMockPi(): { pi: any; messages: SentMessage[] } {
77
84
  };
78
85
  }
79
86
 
80
- function seedValidationBlockedMilestone(base: string): void {
87
+ function seedValidationBlockedMilestone(
88
+ base: string,
89
+ status: "needs-attention" | "needs-remediation" = "needs-attention",
90
+ ): void {
81
91
  openDatabase(join(base, ".gsd", "gsd.db"));
82
92
  insertMilestone({ id: "M006", title: "Mark All Complete", status: "active" });
83
93
  insertSlice({
@@ -91,9 +101,9 @@ function seedValidationBlockedMilestone(base: string): void {
91
101
  insertAssessment({
92
102
  path: "milestones/M006/M006-VALIDATION.md",
93
103
  milestoneId: "M006",
94
- status: "needs-attention",
104
+ status,
95
105
  scope: "milestone-validation",
96
- fullContent: "verdict: needs-attention",
106
+ fullContent: `verdict: ${status}`,
97
107
  });
98
108
  invalidateStateCache();
99
109
  }
@@ -155,6 +165,31 @@ test("dispatcher blocks workflow-advancing aliases while validation is blocked",
155
165
  }
156
166
  });
157
167
 
168
+ test("dispatcher allows reassess dispatch while validation needs remediation", async () => {
169
+ const base = makeBase();
170
+ try {
171
+ seedValidationBlockedMilestone(base, "needs-remediation");
172
+ const { ctx, calls, newSessions } = makeMockCtx(base);
173
+ const { pi, messages } = makeMockPi();
174
+
175
+ await handleGSDCommand("dispatch reassess", ctx, pi);
176
+
177
+ assert.equal(messages.length, 1);
178
+ assert.equal(messages[0].customType, "gsd-dispatch");
179
+ assert.equal(messages[0].display, false);
180
+ assert.match(messages[0].content, /UNIT: Reassess Roadmap/);
181
+ assert.ok(
182
+ calls.some((call) => call.kind === "info" && /Dispatching reassess-roadmap for M006\/S01/.test(call.message)),
183
+ `expected reassess dispatch notification, got: ${JSON.stringify(calls)}`,
184
+ );
185
+ assert.deepEqual(newSessions, [{ workspaceRoot: base }]);
186
+ } finally {
187
+ closeDatabase();
188
+ invalidateStateCache();
189
+ cleanup(base);
190
+ }
191
+ });
192
+
158
193
  test("dispatcher still allows recovery commands while validation is blocked", async () => {
159
194
  const base = makeBase();
160
195
  try {
@@ -399,8 +399,12 @@ test("handleVerdict needs-remediation override with --rationale rewrites verdict
399
399
  assert.match(rewritten, /found missing slice/);
400
400
 
401
401
  assert.ok(
402
- calls.some((c) => /gsd_reassess_roadmap/.test(c.message)),
403
- "needs-remediation override should suggest gsd_reassess_roadmap follow-up",
402
+ calls.some((c) => /\/gsd dispatch reassess/.test(c.message)),
403
+ "needs-remediation override should suggest the reassess dispatch follow-up",
404
+ );
405
+ assert.ok(
406
+ calls.every((c) => !/gsd_reassess_roadmap/.test(c.message)),
407
+ "needs-remediation override should not expose the internal tool name",
404
408
  );
405
409
  } finally {
406
410
  closeDatabase();
@@ -868,6 +868,14 @@ describe('derive-state-db', async () => {
868
868
  dbState.blockers.some(b => b.includes('needs-remediation') && b.includes('M001')),
869
869
  'remediation-stuck-db: blocker message mentions milestone and verdict',
870
870
  );
871
+ assert.ok(
872
+ dbState.blockers.some(b => b.includes('/gsd dispatch reassess')),
873
+ 'remediation-stuck-db: blocker message points users to the reassess command',
874
+ );
875
+ assert.ok(
876
+ dbState.blockers.every(b => !b.includes('gsd_reassess_roadmap')),
877
+ 'remediation-stuck-db: blocker message does not expose the internal tool name',
878
+ );
871
879
 
872
880
  closeDatabase();
873
881
  } finally {
@@ -555,6 +555,14 @@ describe('derive-state-helpers', () => {
555
555
  state.blockers.some(b => b.includes('needs-remediation') && b.includes('M001')),
556
556
  'remediation-stuck: blocker message mentions milestone and verdict',
557
557
  );
558
+ assert.ok(
559
+ state.blockers.some(b => b.includes('/gsd dispatch reassess')),
560
+ 'remediation-stuck: blocker message points users to the reassess command',
561
+ );
562
+ assert.ok(
563
+ state.blockers.every(b => !b.includes('gsd_reassess_roadmap')),
564
+ 'remediation-stuck: blocker message does not expose the internal tool name',
565
+ );
558
566
  } finally {
559
567
  closeDatabase();
560
568
  cleanup(base);
@@ -53,6 +53,24 @@ test('runExecSandbox: captures stdout, persists artifacts, returns digest', asyn
53
53
  }
54
54
  });
55
55
 
56
+ test('runExecSandbox: persists optional request metadata', async () => {
57
+ const base = freshBase();
58
+ try {
59
+ const result = await runExecSandbox(
60
+ {
61
+ runtime: 'bash',
62
+ script: 'echo metadata-ok',
63
+ metadata: { kind: 'uat_exec', intent: 'uat-artifact-check' },
64
+ },
65
+ baseOpts(base),
66
+ );
67
+ const meta = JSON.parse(readFileSync(result.meta_path, 'utf-8')) as Record<string, unknown>;
68
+ assert.deepEqual(meta.metadata, { kind: 'uat_exec', intent: 'uat-artifact-check' });
69
+ } finally {
70
+ cleanup(base);
71
+ }
72
+ });
73
+
56
74
  test('runExecSandbox: enforces stdout cap and marks truncation', async () => {
57
75
  const base = freshBase();
58
76
  try {
@@ -0,0 +1,69 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { registerExecTools } from "../bootstrap/exec-tools.ts";
5
+ import { executeUatExec } from "../tools/exec-tool.ts";
6
+ import type { ExecSandboxRequest, ExecSandboxResult } from "../exec-sandbox.ts";
7
+
8
+ function makeExecResult(request: ExecSandboxRequest): ExecSandboxResult {
9
+ return {
10
+ id: "exec-1",
11
+ runtime: request.runtime,
12
+ exit_code: 0,
13
+ signal: null,
14
+ timed_out: false,
15
+ duration_ms: 1,
16
+ stdout_bytes: 12,
17
+ stderr_bytes: 0,
18
+ stdout_truncated: false,
19
+ stderr_truncated: false,
20
+ stdout_path: ".gsd/exec/exec-1.stdout",
21
+ stderr_path: ".gsd/exec/exec-1.stderr",
22
+ meta_path: ".gsd/exec/exec-1.meta.json",
23
+ digest: "check passed",
24
+ };
25
+ }
26
+
27
+ test("executeUatExec accepts evidence-mode aliases for intent", async () => {
28
+ const requests: ExecSandboxRequest[] = [];
29
+ const result = await executeUatExec(
30
+ {
31
+ milestoneId: "M001",
32
+ sliceId: "S01",
33
+ checkId: "UAT-PRE",
34
+ intent: "artifact",
35
+ runtime: "bash",
36
+ script: "printf ok",
37
+ },
38
+ {
39
+ baseDir: "/tmp/gsd-uat-exec-test",
40
+ preferences: null,
41
+ run: async (request) => {
42
+ requests.push(request);
43
+ return makeExecResult(request);
44
+ },
45
+ },
46
+ );
47
+
48
+ assert.equal(result.isError, false);
49
+ assert.equal(result.details?.operation, "gsd_uat_exec");
50
+ assert.equal(result.details?.intent, "uat-artifact-check");
51
+ assert.equal(requests[0]?.metadata?.intent, "uat-artifact-check");
52
+ });
53
+
54
+ test("registerExecTools exposes gsd_uat_exec intent as recoverable string schema", () => {
55
+ const tools: Array<{ name: string; parameters: any }> = [];
56
+ registerExecTools({
57
+ registerTool: (tool: { name: string; parameters: any }) => {
58
+ tools.push(tool);
59
+ },
60
+ } as any);
61
+
62
+ const tool = tools.find((registeredTool) => registeredTool.name === "gsd_uat_exec");
63
+ assert.ok(tool, "gsd_uat_exec should be registered");
64
+ const intentSchema = tool.parameters.properties.intent;
65
+ assert.equal(intentSchema.type, "string");
66
+ assert.equal("anyOf" in intentSchema, false);
67
+ assert.match(intentSchema.description, /uat-artifact-check/);
68
+ assert.match(intentSchema.description, /artifact/);
69
+ });
@@ -22,23 +22,31 @@ import { tmpdir } from "node:os";
22
22
 
23
23
  import { loadSkills } from "@gsd/pi-coding-agent";
24
24
  import {
25
- buildResearchSlicePrompt,
25
+ buildCompleteSlicePrompt,
26
26
  buildParallelResearchSlicesPrompt,
27
+ buildResearchSlicePrompt,
27
28
  } from "../auto-prompts.ts";
28
29
 
29
30
  const SKILL_NAME = "testskill";
31
+ const COMPLETE_SLICE_SKILL_NAME = "complete-slice-policies";
30
32
  const SKILL_ACTIVATION_SUBSTRING = `Call Skill({ skill: '${SKILL_NAME}' })`;
33
+ const COMPLETE_SLICE_SKILL_ACTIVATION_SUBSTRING = `Call Skill({ skill: '${COMPLETE_SLICE_SKILL_NAME}' })`;
31
34
 
32
35
  const tmpDirs: string[] = [];
33
36
  let savedCwd: string | undefined;
34
37
 
35
- function setupProjectWithSkill(): string {
38
+ function setupProjectWithSkill(options: {
39
+ skillName?: string;
40
+ preferencesLines?: string[];
41
+ } = {}): string {
42
+ const skillName = options.skillName ?? SKILL_NAME;
36
43
  const base = mkdtempSync(join(tmpdir(), "gsd-worker-skill-int-"));
37
44
  tmpDirs.push(base);
38
45
 
39
46
  // Milestone roadmap — buildResearchSlicePrompt inlines the roadmap excerpt.
40
47
  const milestoneDir = join(base, ".gsd", "milestones", "M001");
41
- mkdirSync(join(milestoneDir, "slices", "S01"), { recursive: true });
48
+ const sliceOneDir = join(milestoneDir, "slices", "S01");
49
+ mkdirSync(join(sliceOneDir, "tasks"), { recursive: true });
42
50
  mkdirSync(join(milestoneDir, "slices", "S02"), { recursive: true });
43
51
  writeFileSync(
44
52
  join(milestoneDir, "M001-ROADMAP.md"),
@@ -55,27 +63,41 @@ function setupProjectWithSkill(): string {
55
63
  ].join("\n"),
56
64
  "utf-8",
57
65
  );
66
+ writeFileSync(
67
+ join(sliceOneDir, "S01-PLAN.md"),
68
+ [
69
+ "# S01: Alpha",
70
+ "",
71
+ "**Goal:** Verify worker x skill prompt plumbing.",
72
+ "**Demo:** Rendered prompts include the skill activation block.",
73
+ "",
74
+ "## Tasks",
75
+ "- [x] **T01: Task** `est:10m`",
76
+ "",
77
+ ].join("\n"),
78
+ "utf-8",
79
+ );
58
80
 
59
81
  // Project preferences — buildSkillActivationBlock picks these up via
60
82
  // loadEffectiveGSDPreferences(), which reads from `${cwd}/.gsd/PREFERENCES.md`.
61
83
  writeFileSync(
62
84
  join(base, ".gsd", "PREFERENCES.md"),
63
- ["---", `always_use_skills:`, ` - ${SKILL_NAME}`, "---", ""].join("\n"),
85
+ ["---", ...(options.preferencesLines ?? [`always_use_skills:`, ` - ${skillName}`]), "---", ""].join("\n"),
64
86
  "utf-8",
65
87
  );
66
88
 
67
89
  // Project-scoped skill — resolveSkillReference scans `${cwd}/.agents/skills/`.
68
- const skillDir = join(base, ".agents", "skills", SKILL_NAME);
90
+ const skillDir = join(base, ".agents", "skills", skillName);
69
91
  mkdirSync(skillDir, { recursive: true });
70
92
  writeFileSync(
71
93
  join(skillDir, "SKILL.md"),
72
94
  [
73
95
  "---",
74
- `name: ${SKILL_NAME}`,
96
+ `name: ${skillName}`,
75
97
  `description: Integration-test skill for worker × skill prompt plumbing.`,
76
98
  "---",
77
99
  "",
78
- `# ${SKILL_NAME}`,
100
+ `# ${skillName}`,
79
101
  "",
80
102
  "Test skill body.",
81
103
  ].join("\n"),
@@ -122,6 +144,31 @@ test("worker prompt (buildResearchSlicePrompt) includes <skill_activation> from
122
144
  );
123
145
  });
124
146
 
147
+ test("complete-slice prompt includes <skill_activation> from unit-specific skill_rules", async () => {
148
+ const base = setupProjectWithSkill({
149
+ skillName: COMPLETE_SLICE_SKILL_NAME,
150
+ preferencesLines: [
151
+ "skill_rules:",
152
+ " - when: complete-slice",
153
+ " use:",
154
+ ` - ${COMPLETE_SLICE_SKILL_NAME}`,
155
+ ],
156
+ });
157
+ savedCwd = process.cwd();
158
+ process.chdir(base);
159
+
160
+ const prompt = await buildCompleteSlicePrompt("M001", "Test Milestone", "S01", "Alpha", base, "minimal");
161
+
162
+ assert.ok(
163
+ prompt.includes("<skill_activation>"),
164
+ "complete-slice prompt should contain a <skill_activation> block",
165
+ );
166
+ assert.ok(
167
+ prompt.includes(COMPLETE_SLICE_SKILL_ACTIVATION_SUBSTRING),
168
+ `complete-slice prompt should reference the skill-rule skill '${COMPLETE_SLICE_SKILL_NAME}'`,
169
+ );
170
+ });
171
+
125
172
  test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skill_activation> into each embedded per-slice section", async () => {
126
173
  const base = setupProjectWithSkill();
127
174
  savedCwd = process.cwd();
@@ -32,6 +32,16 @@ test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence"
32
32
  assert.doesNotMatch(prompt, /uatType:\s*artifact-driven/);
33
33
  });
34
34
 
35
+ test("run-uat prompt lists canonical gsd_uat_exec intent values", () => {
36
+ const prompt = readPrompt("run-uat");
37
+ assert.match(prompt, /`uat-artifact-check`/);
38
+ assert.match(prompt, /`uat-runtime-check`/);
39
+ assert.match(prompt, /`uat-browser-check`/);
40
+ assert.match(prompt, /`uat-service-start`/);
41
+ assert.match(prompt, /`uat-log-inspection`/);
42
+ assert.match(prompt, /do not use `artifact`, `runtime`, or `human-follow-up` as `intent`/i);
43
+ });
44
+
35
45
  test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
36
46
  const prompt = readPrompt("workflow-start");
37
47
  assert.match(prompt, /Keep moving by default/i);
@@ -17,7 +17,10 @@ import {
17
17
  shouldDeferTransientErrorToCoreRetry,
18
18
  suppressTerminalDeletedWorktreeMessageEnd,
19
19
  } from "../bootstrap/agent-end-recovery.ts";
20
- import { _buildCancelledUnitStopReason } from "../auto/phases.ts";
20
+ import {
21
+ _buildCancelledUnitStopReason,
22
+ _classifyZeroToolProviderMessageForTest,
23
+ } from "../auto/phases.ts";
21
24
  import { autoSession } from "../auto-runtime-state.ts";
22
25
  import { getNextFallbackModel } from "../preferences.ts";
23
26
  // Zero-import module — imported by path rather than through the package
@@ -53,6 +56,20 @@ test("classifyError treats usage-limit phrasing as transient rate-limit (#4373)"
53
56
  assert.equal(result.kind, "rate-limit");
54
57
  });
55
58
 
59
+ test("zero-tool provider classifier treats Claude session-limit wording as transient rate-limit (#371)", () => {
60
+ const result = _classifyZeroToolProviderMessageForTest("Claude Code session limit reached. Limit resets at 5 PM.");
61
+ assert.ok(result, "session-limit wording should be recognized");
62
+ assert.ok(isTransient(result));
63
+ assert.equal(result.kind, "rate-limit");
64
+ });
65
+
66
+ test("zero-tool provider classifier treats weekly limit wording as transient rate-limit (#371)", () => {
67
+ const result = _classifyZeroToolProviderMessageForTest("You've reached your weekly limit. Try again later.");
68
+ assert.ok(result, "weekly limit wording should be recognized");
69
+ assert.ok(isTransient(result));
70
+ assert.equal(result.kind, "rate-limit");
71
+ });
72
+
56
73
  test("classifyError treats extra-usage phrasing as transient rate-limit (#4397)", () => {
57
74
  const result = classifyError("You are out of extra usage. Please wait before retrying.");
58
75
  assert.ok(isTransient(result));
@@ -17,6 +17,10 @@ import { validatePreferences } from "../preferences-validation.ts";
17
17
  import type { ReactiveExecutionState } from "../types.ts";
18
18
  import { parseUnitId } from "../unit-id.ts";
19
19
  import { resolveDispatch } from "../auto-dispatch.ts";
20
+ import {
21
+ _getPlannedKeyFilesForTest,
22
+ _parseReactiveBatchTaskIdsForTest,
23
+ } from "../auto-post-unit.ts";
20
24
 
21
25
  // ─── Preference Validation ────────────────────────────────────────────────
22
26
 
@@ -71,6 +75,38 @@ test("reactive_execution validation warns on unknown keys", () => {
71
75
  assert.ok(result.warnings.some((w) => w.includes("unknown_thing")));
72
76
  });
73
77
 
78
+ test("reactive batch unit ids are parsed and deduped for commit context", () => {
79
+ assert.deepEqual(
80
+ _parseReactiveBatchTaskIdsForTest("M001/S01/reactive+T01,t02,T01"),
81
+ ["T01", "T02"],
82
+ );
83
+ assert.deepEqual(_parseReactiveBatchTaskIdsForTest("M001/S01/T01"), []);
84
+ });
85
+
86
+ test("reactive commit context key files include planned output, files, and key_files once", () => {
87
+ const result = _getPlannedKeyFilesForTest([
88
+ {
89
+ expected_output: ["src/new.ts", "src/shared.ts"],
90
+ files: ["src/input.ts", "src/shared.ts"],
91
+ key_files: ["src/key.ts"],
92
+ },
93
+ {
94
+ expected_output: ["src/new.ts"],
95
+ files: ["src/other.ts"],
96
+ key_files: ["src/key.ts", "src/final.ts"],
97
+ },
98
+ ]);
99
+
100
+ assert.deepEqual(result, [
101
+ "src/new.ts",
102
+ "src/shared.ts",
103
+ "src/input.ts",
104
+ "src/key.ts",
105
+ "src/other.ts",
106
+ "src/final.ts",
107
+ ]);
108
+ });
109
+
74
110
  // ─── Dispatch Rule Matching Logic ─────────────────────────────────────────
75
111
 
76
112
  test("reactive dispatch requires enabled config and multiple ready tasks", async () => {