@opengsd/gsd-pi 1.1.1-dev.b2556262 → 1.2.0-dev.844675c9

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 (211) hide show
  1. package/dist/project-sessions.js +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +17 -9
  4. package/dist/resources/extensions/gsd/auto/contracts.js +8 -1
  5. package/dist/resources/extensions/gsd/auto/orchestrator.js +659 -57
  6. package/dist/resources/extensions/gsd/auto-prompts.js +110 -1
  7. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  8. package/dist/resources/extensions/gsd/auto-tool-tracking.js +5 -0
  9. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +29 -0
  10. package/dist/resources/extensions/gsd/auto-worktree.js +24 -17
  11. package/dist/resources/extensions/gsd/auto.js +62 -464
  12. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -1
  13. package/dist/resources/extensions/gsd/debug-logger.js +10 -0
  14. package/dist/resources/extensions/gsd/doctor-proactive.js +7 -2
  15. package/dist/resources/extensions/gsd/guided-flow.js +2 -2
  16. package/dist/resources/extensions/gsd/markdown-renderer.js +31 -32
  17. package/dist/resources/extensions/gsd/mcp-filter.js +6 -0
  18. package/dist/resources/extensions/gsd/native-git-bridge.js +45 -0
  19. package/dist/resources/extensions/gsd/prompts/discuss.md +6 -7
  20. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +3 -5
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
  23. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
  24. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  25. package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  26. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
  27. package/dist/resources/extensions/gsd/schemas/parsers.js +6 -1
  28. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +21 -1
  29. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +169 -20
  30. package/dist/resources/extensions/gsd/user-input-boundary.js +42 -4
  31. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  32. package/dist/web/standalone/.next/BUILD_ID +1 -1
  33. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  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/api/boot/route.js.nft.json +1 -1
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  54. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  55. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  56. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  57. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  58. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  59. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  60. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  61. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  62. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  65. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/index.html +1 -1
  88. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  95. package/dist/web/standalone/.next/server/chunks/5047.js +2 -0
  96. package/dist/web/standalone/.next/server/chunks/5124.js +1 -0
  97. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  98. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  100. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  101. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  102. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  103. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  104. package/dist/web/standalone/node_modules/postcss/lib/container.js +26 -18
  105. package/dist/web/standalone/node_modules/postcss/lib/css-syntax-error.js +47 -14
  106. package/dist/web/standalone/node_modules/postcss/lib/declaration.js +4 -4
  107. package/dist/web/standalone/node_modules/postcss/lib/fromJSON.js +3 -3
  108. package/dist/web/standalone/node_modules/postcss/lib/input.js +54 -29
  109. package/dist/web/standalone/node_modules/postcss/lib/lazy-result.js +47 -37
  110. package/dist/web/standalone/node_modules/postcss/lib/map-generator.js +26 -9
  111. package/dist/web/standalone/node_modules/postcss/lib/no-work-result.js +57 -55
  112. package/dist/web/standalone/node_modules/postcss/lib/node.js +99 -31
  113. package/dist/web/standalone/node_modules/postcss/lib/parse.js +1 -1
  114. package/dist/web/standalone/node_modules/postcss/lib/parser.js +10 -9
  115. package/dist/web/standalone/node_modules/postcss/lib/postcss.js +12 -12
  116. package/dist/web/standalone/node_modules/postcss/lib/previous-map.js +30 -11
  117. package/dist/web/standalone/node_modules/postcss/lib/processor.js +7 -7
  118. package/dist/web/standalone/node_modules/postcss/lib/result.js +5 -5
  119. package/dist/web/standalone/node_modules/postcss/lib/rule.js +6 -6
  120. package/dist/web/standalone/node_modules/postcss/lib/stringifier.js +69 -28
  121. package/dist/web/standalone/node_modules/postcss/lib/tokenize.js +6 -2
  122. package/dist/web/standalone/node_modules/postcss/package.json +48 -48
  123. package/package.json +16 -11
  124. package/packages/cloud-mcp-gateway/package.json +2 -2
  125. package/packages/contracts/package.json +1 -1
  126. package/packages/daemon/package.json +4 -4
  127. package/packages/gsd-agent-core/package.json +5 -5
  128. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +1 -1
  129. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  130. package/packages/gsd-agent-modes/package.json +7 -7
  131. package/packages/mcp-server/package.json +3 -3
  132. package/packages/native/package.json +1 -1
  133. package/packages/pi-agent-core/package.json +1 -1
  134. package/packages/pi-ai/dist/models.generated.d.ts +0 -34
  135. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  136. package/packages/pi-ai/dist/models.generated.js +12 -46
  137. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  138. package/packages/pi-ai/package.json +1 -1
  139. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/auth-storage.js +11 -3
  141. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  142. package/packages/pi-coding-agent/package.json +7 -7
  143. package/packages/pi-tui/package.json +2 -2
  144. package/packages/rpc-client/package.json +2 -2
  145. package/pkg/package.json +1 -1
  146. package/scripts/install/deps.js +10 -0
  147. package/scripts/link-workspace-packages.cjs +7 -40
  148. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +18 -8
  149. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +2 -2
  150. package/src/resources/extensions/gsd/auto/contracts.ts +8 -119
  151. package/src/resources/extensions/gsd/auto/orchestrator.ts +794 -58
  152. package/src/resources/extensions/gsd/auto-prompts.ts +114 -1
  153. package/src/resources/extensions/gsd/auto-runtime-state.ts +4 -0
  154. package/src/resources/extensions/gsd/auto-tool-tracking.ts +5 -0
  155. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +33 -0
  156. package/src/resources/extensions/gsd/auto-worktree.ts +24 -16
  157. package/src/resources/extensions/gsd/auto.ts +81 -500
  158. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  159. package/src/resources/extensions/gsd/debug-logger.ts +11 -0
  160. package/src/resources/extensions/gsd/doctor-proactive.ts +8 -2
  161. package/src/resources/extensions/gsd/guided-flow.ts +2 -2
  162. package/src/resources/extensions/gsd/markdown-renderer.ts +38 -19
  163. package/src/resources/extensions/gsd/mcp-filter.ts +7 -0
  164. package/src/resources/extensions/gsd/native-git-bridge.ts +48 -0
  165. package/src/resources/extensions/gsd/prompts/discuss.md +6 -7
  166. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
  167. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +3 -5
  168. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
  169. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
  170. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  171. package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  172. package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
  173. package/src/resources/extensions/gsd/schemas/parsers.ts +6 -1
  174. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +31 -10
  175. package/src/resources/extensions/gsd/tests/artifact-db-drift-memo.test.ts +66 -0
  176. package/src/resources/extensions/gsd/tests/auto-dispatch-baseline-harness.test.ts +53 -0
  177. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +590 -855
  178. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +38 -10
  179. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +15 -0
  180. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +64 -1
  181. package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +157 -0
  182. package/src/resources/extensions/gsd/tests/markdown-renderer-parse-cache.test.ts +75 -0
  183. package/src/resources/extensions/gsd/tests/native-merge-regular.test.ts +139 -0
  184. package/src/resources/extensions/gsd/tests/orchestrator-legacy-parity.test.ts +127 -0
  185. package/src/resources/extensions/gsd/tests/parse-project-milestone-bridge.test.ts +77 -0
  186. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +4 -2
  187. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +29 -2
  188. package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +65 -0
  189. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +19 -5
  190. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +38 -0
  191. package/src/resources/extensions/gsd/tests/user-input-boundary.test.ts +62 -0
  192. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +24 -0
  193. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -3
  194. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +183 -21
  195. package/src/resources/extensions/gsd/user-input-boundary.ts +37 -5
  196. package/dist/web/standalone/.next/server/chunks/678.js +0 -2
  197. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts +0 -21
  198. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts.map +0 -1
  199. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js +0 -213
  200. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js.map +0 -1
  201. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts +0 -28
  202. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts.map +0 -1
  203. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js +0 -249
  204. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js.map +0 -1
  205. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts +0 -19
  206. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts.map +0 -1
  207. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js +0 -797
  208. package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js.map +0 -1
  209. package/scripts/ensure-workspace-builds.cjs +0 -129
  210. /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → Qbr81pQ-pbQXP4bq2VXLv}/_buildManifest.js +0 -0
  211. /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → Qbr81pQ-pbQXP4bq2VXLv}/_ssgManifest.js +0 -0
@@ -0,0 +1,127 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: #442 Phase 3.14 — characterization battery proving the
3
+ // Orchestrator's dispatch DECISION matches what the legacy
4
+ // runPreDispatch/runDispatch path would produce, across representative
5
+ // resolveDispatch outcomes. This is the safety net that licenses deleting the
6
+ // dead legacy `else` branch in auto/loop.ts (loop never reaches it because
7
+ // ensureOrchestrationModule runs unconditionally before autoLoop, so
8
+ // s.orchestration is always set).
9
+ //
10
+ // Both paths delegate to the same resolveDispatch rule engine; the only thing
11
+ // that differs is how each TRANSLATES a resolveDispatch action into a loop
12
+ // outcome. We pin that translation equivalence here:
13
+ // resolveDispatch action | legacy runDispatch -> loop | orchestrator decision
14
+ // ------------------------|----------------------------|----------------------
15
+ // dispatch | IterationData(unit) | { unitType, unitId } (advanced)
16
+ // stop | break -> stopped/blocked | { kind: "blocked", action } | stopped
17
+ // skip | continue -> skipped | { kind: "skipped" }
18
+ // (no rule matches) | stop/no-unit | null -> stopped(no remaining)
19
+
20
+ import test from "node:test";
21
+ import assert from "node:assert/strict";
22
+
23
+ import { decideOrchestratorDispatch } from "../auto/orchestrator.ts";
24
+ import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
25
+ import { RuleRegistry, setRegistry, resetRegistry } from "../rule-registry.ts";
26
+ import type { UnifiedRule } from "../rule-types.ts";
27
+ import type { GSDState } from "../types.ts";
28
+
29
+ function makeState(): GSDState {
30
+ return {
31
+ activeMilestone: { id: "M001", title: "Milestone" },
32
+ activeSlice: null,
33
+ activeTask: null,
34
+ phase: "executing",
35
+ recentDecisions: [],
36
+ blockers: [],
37
+ nextAction: "Execute task",
38
+ registry: [],
39
+ requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 },
40
+ progress: { milestones: { done: 0, total: 1 } },
41
+ };
42
+ }
43
+
44
+ const fakeCtx = {
45
+ model: { provider: "anthropic", baseUrl: "https://api.anthropic.com", contextWindow: 200_000 },
46
+ modelRegistry: { getAll: () => [], getProviderAuthMode: (_p: string) => "apiKey" as const },
47
+ } as never;
48
+ const fakePi = { getActiveTools: () => ["read_file", "write_file"] } as never;
49
+ const BASE = "/tmp/orchestrator-legacy-parity";
50
+
51
+ function installRule(where: UnifiedRule["where"]): void {
52
+ setRegistry(new RuleRegistry([{
53
+ name: "parity-rule",
54
+ when: "dispatch",
55
+ evaluation: "first-match",
56
+ where,
57
+ then: (r: unknown) => r,
58
+ }]));
59
+ }
60
+
61
+ function directCtx(state: GSDState): DispatchContext {
62
+ return {
63
+ basePath: BASE,
64
+ mid: state.activeMilestone!.id,
65
+ midTitle: state.activeMilestone!.title,
66
+ state,
67
+ prefs: undefined,
68
+ structuredQuestionsAvailable: "true",
69
+ sessionContextWindow: 200_000,
70
+ sessionProvider: "anthropic",
71
+ modelRegistry: (fakeCtx as { modelRegistry: unknown }).modelRegistry as never,
72
+ };
73
+ }
74
+
75
+ test("#442 characterization: dispatch action → orchestrator picks the same unit the legacy path would", async (t) => {
76
+ t.after(() => resetRegistry());
77
+ const state = makeState();
78
+ installRule(async () => ({ action: "dispatch", unitType: "execute-task", unitId: "T07", prompt: "p" }));
79
+
80
+ const legacy = await resolveDispatch(directCtx(state));
81
+ const decision = await decideOrchestratorDispatch(fakeCtx, fakePi, BASE, undefined, { stateSnapshot: state });
82
+
83
+ assert.equal((legacy as { action: string }).action, "dispatch");
84
+ assert.ok(decision && "unitType" in decision, "orchestrator must produce a unit decision");
85
+ assert.equal(decision.unitType, (legacy as { unitType: string }).unitType);
86
+ assert.equal(decision.unitId, (legacy as { unitId: string }).unitId);
87
+ });
88
+
89
+ test("#442 characterization: stop action → orchestrator blocks with stop, matching legacy break", async (t) => {
90
+ t.after(() => resetRegistry());
91
+ const state = makeState();
92
+ installRule(async () => ({ action: "stop", reason: "milestone blocked" }));
93
+
94
+ const legacy = await resolveDispatch(directCtx(state));
95
+ const decision = await decideOrchestratorDispatch(fakeCtx, fakePi, BASE, undefined, { stateSnapshot: state });
96
+
97
+ assert.equal((legacy as { action: string }).action, "stop");
98
+ assert.ok(decision && "kind" in decision && decision.kind === "blocked", "stop must translate to a blocked decision");
99
+ assert.equal((decision as { action: string }).action, "stop");
100
+ });
101
+
102
+ test("#442 characterization: skip action → orchestrator skips, matching legacy continue", async (t) => {
103
+ t.after(() => resetRegistry());
104
+ const state = makeState();
105
+ installRule(async () => ({ action: "skip", reason: "nothing to do this pass" }));
106
+
107
+ const legacy = await resolveDispatch(directCtx(state));
108
+ const decision = await decideOrchestratorDispatch(fakeCtx, fakePi, BASE, undefined, { stateSnapshot: state });
109
+
110
+ assert.equal((legacy as { action: string }).action, "skip");
111
+ assert.ok(decision && "kind" in decision && decision.kind === "skipped", "skip must translate to a skipped decision");
112
+ });
113
+
114
+ test("#442 characterization: no matching rule → orchestrator yields no unit (legacy 'no remaining units')", async (t) => {
115
+ t.after(() => resetRegistry());
116
+ const state = makeState();
117
+ installRule(async () => null);
118
+
119
+ const legacy = await resolveDispatch(directCtx(state));
120
+ const decision = await decideOrchestratorDispatch(fakeCtx, fakePi, BASE, undefined, { stateSnapshot: state });
121
+
122
+ // resolveDispatch with no match yields no dispatch action; the orchestrator
123
+ // surfaces that as a null decision, which advance() turns into a "stopped:
124
+ // no remaining units" outcome — the same terminal the legacy path reaches.
125
+ assert.ok(legacy == null || (legacy as { action?: string }).action !== "dispatch");
126
+ assert.ok(decision == null || !("unitType" in decision), "no-match must not yield a unit dispatch");
127
+ });
@@ -0,0 +1,77 @@
1
+ // gsd-pi / parseProject MILESTONE_LINE_RE bridging regression
2
+ //
3
+ // Guards against a silent data-integrity bug: MILESTONE_LINE_RE used `\s+`
4
+ // around its separator, and `\s` matches newlines. A milestone line lacking a
5
+ // valid internal separator could therefore "bridge" onto the NEXT bullet's
6
+ // `- `, consuming it as the separator and swallowing the following well-formed
7
+ // milestone. The separator gaps are now horizontal-whitespace-only so a line
8
+ // can never span a newline.
9
+ import { describe, test } from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { parseProject } from "../schemas/parsers.ts";
13
+
14
+ function milestoneSection(...lines: string[]): string {
15
+ return ["# Project", "", "## Milestone Sequence", "", ...lines, ""].join("\n");
16
+ }
17
+
18
+ describe("parseProject milestone bridging", () => {
19
+ test("a malformed milestone line does not swallow the following well-formed one", () => {
20
+ // M001 uses an invalid " : " separator; M002 is canonical. Before the fix
21
+ // this returned a SINGLE match (M001 with oneLiner "[ ] M002: Baz — qux"),
22
+ // dropping M002 entirely.
23
+ const content = milestoneSection(
24
+ "- [ ] M001: Foo : bar",
25
+ "- [ ] M002: Baz — qux",
26
+ );
27
+
28
+ const { milestones } = parseProject(content);
29
+
30
+ // M002 must survive intact — it must NOT be consumed as M001's one-liner.
31
+ const m002 = milestones.find(m => m.id === "M002");
32
+ assert.ok(m002, "M002 must be registered, not swallowed by the malformed M001 line");
33
+ assert.equal(m002!.title, "Baz", "M002 title parsed cleanly");
34
+ assert.equal(m002!.oneLiner, "qux", "M002 one-liner parsed cleanly");
35
+
36
+ // No milestone may have bridged the M002 bullet into its own one-liner.
37
+ assert.ok(
38
+ !milestones.some(m => m.oneLiner.includes("M002")),
39
+ "no milestone may bridge across the newline and consume the M002 bullet",
40
+ );
41
+
42
+ // M001 has no valid separator, so it is skipped — consistent with the
43
+ // empty-parse hard-fail in execute-summary-save-empty-project.test.ts.
44
+ // The fixture therefore yields exactly the one well-formed milestone.
45
+ assert.deepEqual(milestones.map(m => m.id), ["M002"], "only the well-formed milestone parses");
46
+ });
47
+
48
+ test("two well-formed milestones on adjacent lines both parse", () => {
49
+ const content = milestoneSection(
50
+ "- [x] M001: Foo — bar",
51
+ "- [ ] M002: Baz — qux",
52
+ );
53
+
54
+ const { milestones } = parseProject(content);
55
+ assert.deepEqual(
56
+ milestones.map(m => ({ id: m.id, title: m.title, oneLiner: m.oneLiner, done: m.done })),
57
+ [
58
+ { id: "M001", title: "Foo", oneLiner: "bar", done: true },
59
+ { id: "M002", title: "Baz", oneLiner: "qux", done: false },
60
+ ],
61
+ "adjacent canonical milestone lines are parsed independently",
62
+ );
63
+ });
64
+
65
+ test("a trailing malformed line cannot bridge onto a following bullet of any kind", () => {
66
+ // The last milestone is malformed and is followed by a non-milestone list
67
+ // bullet. The malformed line must be skipped without consuming the bullet.
68
+ const content = milestoneSection(
69
+ "- [ ] M001: Foo — bar",
70
+ "- [ ] M002: Baz : qux",
71
+ "- some unrelated bullet",
72
+ );
73
+
74
+ const { milestones } = parseProject(content);
75
+ assert.deepEqual(milestones.map(m => m.id), ["M001"], "malformed M002 is skipped, not bridged onto the unrelated bullet");
76
+ });
77
+ });
@@ -76,8 +76,10 @@ test("plan-slice prompt: compact planning gates survive template substitution",
76
76
  assert.ok(result.includes("Bias toward \"roadmap is fine.\""), "roadmap reassessment brake should remain visible");
77
77
  assert.ok(result.includes("Self-audit before finishing"), "self-audit gate should remain visible");
78
78
  assert.ok(result.includes("Quality gates: non-trivial slices/tasks include specific Q3-Q7 coverage where applicable."));
79
- assert.ok(result.includes("C:\\Users\\Test\\.gsd\\agent\\extensions\\gsd\\templates\\plan.md"));
80
- assert.ok(result.includes("C:\\Users\\Test\\.gsd\\agent\\extensions\\gsd\\templates\\task-plan.md"));
79
+ assert.ok(result.includes("Use the inlined Output Template sections already present in this prompt."));
80
+ assert.ok(result.includes("Do not read template files from disk."));
81
+ assert.ok(!result.includes("C:\\Users\\Test\\.gsd\\agent\\extensions\\gsd\\templates\\plan.md"));
82
+ assert.ok(!result.includes("C:\\Users\\Test\\.gsd\\agent\\extensions\\gsd\\templates\\task-plan.md"));
81
83
  assert.ok(!result.includes("{{templatesDir}}/plan.md"));
82
84
  assert.ok(!result.includes("{{templatesDir}}/task-plan.md"));
83
85
  assert.ok(!result.includes("{{"));
@@ -564,6 +564,33 @@ test("validate-milestone prompt dispatches parallel reviewers", () => {
564
564
  assert.match(prompt, /assessment evidence/i);
565
565
  });
566
566
 
567
+ // ─── ADR-029: forward preloaded evidence to validate reviewers ────────
568
+ test("validate-milestone forwards preloaded evidence to reviewers and keeps full reads on-demand", () => {
569
+ const prompt = readPrompt("validate-milestone");
570
+ // Orchestrator is told to embed the preloaded evidence into reviewer tasks.
571
+ assert.match(prompt, /[Ee]mbed the relevant preloaded evidence/);
572
+ // Each reviewer is told to use the preloaded evidence, not re-read from disk.
573
+ const useCount = (prompt.match(/do not re-read them from disk/g) ?? []).length;
574
+ assert.ok(useCount >= 3, `expected all three reviewers to use preloaded evidence, found ${useCount}`);
575
+ // Full reads are explicitly the on-demand exception, not the routine path.
576
+ assert.match(prompt, /only if (its|the) preloaded (excerpt|evidence) is missing, truncated, or inconsistent/i);
577
+ });
578
+
579
+ // ─── ADR-029: research-milestone grounds instead of surveying ─────────
580
+ test("research-milestone prompt grounds in preloaded context instead of open-ended survey", () => {
581
+ const prompt = readPrompt("research-milestone");
582
+ // Grounded-research invariant present.
583
+ assert.match(prompt, /do not re-survey/i);
584
+ assert.match(prompt, /Codebase Snapshot and Project Classification/);
585
+ // The old open-ended survey license is gone (no "use `scout` to build a broad map").
586
+ assert.doesNotMatch(prompt, /use `scout` to build a broad map/);
587
+ // resolve_library/docs lookups remain permitted (bounded, external).
588
+ assert.match(prompt, /resolve_library/);
589
+ // Incremental save enables resume after interruption.
590
+ assert.match(prompt, /Save \*\*incrementally\*\*/);
591
+ assert.match(prompt, /Resume — Prior Partial Research/);
592
+ });
593
+
567
594
  // ─── Prompt migration: replan-slice → gsd_replan_slice ────────────────
568
595
 
569
596
  test("replan-slice prompt names gsd_replan_slice as the tool to use", () => {
@@ -700,8 +727,8 @@ test("guided-discuss prompts require 3-or-4 options plus Other-let-me-discuss in
700
727
  );
701
728
  assert.match(
702
729
  prompt,
703
- /grounded in (the |your |)investigation/i,
704
- `${name} must require options grounded in prior investigation`,
730
+ /grounded in (the |your |)(investigation|preloaded context)/i,
731
+ `${name} must require options grounded in the preloaded context / prior investigation`,
705
732
  );
706
733
  }
707
734
  });
@@ -130,3 +130,68 @@ test("buildResearchMilestonePrompt keeps broad project docs on-demand", async (t
130
130
  assert.match(prompt, /`\.gsd\/REQUIREMENTS\.md`/);
131
131
  assert.match(prompt, /`\.gsd\/DECISIONS\.md`/);
132
132
  });
133
+
134
+ // ─── ADR-029: preload-authoritative research grounding ────────────────
135
+
136
+ test("ADR-029: research-milestone inlines project classification + codebase snapshot", async (t) => {
137
+ const base = makeBase();
138
+ t.after(() => cleanup(base));
139
+ invalidateAllCaches();
140
+
141
+ seed(base, "M001");
142
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# M001 Context\n");
143
+ // Give the codebase scan something real to sample.
144
+ writeFileSync(join(base, "index.html"), "<!doctype html><html><body><h1>hi</h1></body></html>\n");
145
+ writeFileSync(join(base, "script.js"), "const x = 1;\nasync function go() { await x; }\n");
146
+
147
+ const prompt = await buildResearchMilestonePrompt("M001", "Research Test", base);
148
+
149
+ // Project-size signal (same block plan-milestone gets).
150
+ assert.match(prompt, /### Project Classification/);
151
+ assert.match(prompt, /Workflow sizing:/);
152
+ // Bounded codebase snapshot grounds research in current code reality.
153
+ assert.match(prompt, /### Codebase Snapshot \(current code reality\)/);
154
+ assert.match(prompt, /do NOT re-survey the tree/);
155
+ });
156
+
157
+ test("ADR-029: codebase snapshot is suppressed when discuss_preparation is false", async (t) => {
158
+ const base = makeBase();
159
+ t.after(() => cleanup(base));
160
+ invalidateAllCaches();
161
+
162
+ seed(base, "M001");
163
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# M001 Context\n");
164
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\ndiscuss_preparation: false\n---\n");
165
+ invalidateAllCaches();
166
+
167
+ const prompt = await buildResearchMilestonePrompt("M001", "Research Test", base);
168
+
169
+ // Snapshot opt-out honored; classification (unconditional) still present.
170
+ assert.doesNotMatch(prompt, /### Codebase Snapshot/);
171
+ assert.match(prompt, /### Project Classification/);
172
+ });
173
+
174
+ test("ADR-029: prior partial RESEARCH is inlined as a resume block; absent otherwise", async (t) => {
175
+ const base = makeBase();
176
+ t.after(() => cleanup(base));
177
+ invalidateAllCaches();
178
+
179
+ seed(base, "M001");
180
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# M001 Context\n");
181
+
182
+ // No prior research yet → no resume block. (Match the rendered block heading,
183
+ // not the bare phrase — step 8 of the template references the phrase by name.)
184
+ const before = await buildResearchMilestonePrompt("M001", "Research Test", base);
185
+ assert.doesNotMatch(before, /### Resume — Prior Partial Research \(continue, do not redo\)/);
186
+
187
+ // A partial RESEARCH draft from a prior (interrupted) attempt.
188
+ writeFileSync(
189
+ join(base, ".gsd", "milestones", "M001", "M001-RESEARCH.md"),
190
+ "# Research\n\n## Findings\n\nPartial finding: the app is a static site.\n",
191
+ );
192
+ invalidateAllCaches();
193
+
194
+ const after = await buildResearchMilestonePrompt("M001", "Research Test", base);
195
+ assert.match(after, /Resume — Prior Partial Research \(continue, do not redo\)/);
196
+ assert.match(after, /Partial finding: the app is a static site/);
197
+ });
@@ -3,7 +3,10 @@ import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
 
6
- import { _withDetachedAutoKeepaliveForTest } from "../auto.ts";
6
+ import {
7
+ _resolveEffectiveUnitIsolationModeForTest,
8
+ _withDetachedAutoKeepaliveForTest,
9
+ } from "../auto.ts";
7
10
  import {
8
11
  _scheduleAutoStartAfterIdleForTest,
9
12
  resolveGuidedExecuteLaunchMode,
@@ -337,17 +340,28 @@ test("resume path only hard-exits on blocked stop, not blocked pause (#6154)", (
337
340
  });
338
341
 
339
342
  test("prepareForUnit skips worktree safety when isolation is not worktree (#6154)", () => {
340
- const autoSrc = readGsdFile("auto.ts");
341
- const prepareForUnitIdx = autoSrc.indexOf("async prepareForUnit(unitType, unitId) {");
342
- const prepareForUnitBody = autoSrc.slice(prepareForUnitIdx, autoSrc.indexOf("async syncAfterUnit() {}", prepareForUnitIdx));
343
+ const orchSrc = readGsdFile("auto/orchestrator.ts");
344
+ const prepareForUnitIdx = orchSrc.indexOf("private async prepareWorktreeForUnit(");
345
+ const prepareForUnitBody = orchSrc.slice(prepareForUnitIdx, orchSrc.indexOf("private classifyAndRecover(", prepareForUnitIdx));
343
346
 
344
347
  assert.ok(prepareForUnitIdx > -1, "prepareForUnit should exist");
345
348
  assert.ok(
346
- prepareForUnitBody.includes('if (getIsolationMode(runtimeBasePath) !== "worktree")'),
349
+ prepareForUnitBody.includes("const isolationMode = this.getEffectiveUnitIsolationMode(this.runtimeBasePath);"),
350
+ "prepareForUnit should resolve the effective isolation mode once",
351
+ );
352
+ assert.ok(
353
+ prepareForUnitBody.includes('if (isolationMode !== "worktree")'),
347
354
  "prepareForUnit should bypass worktree safety validation outside worktree isolation mode",
348
355
  );
349
356
  });
350
357
 
358
+ test("effective unit isolation follows degraded branch fallback", () => {
359
+ assert.equal(_resolveEffectiveUnitIsolationModeForTest("worktree", true), "branch");
360
+ assert.equal(_resolveEffectiveUnitIsolationModeForTest("worktree", false), "worktree");
361
+ assert.equal(_resolveEffectiveUnitIsolationModeForTest("branch", true), "branch");
362
+ assert.equal(_resolveEffectiveUnitIsolationModeForTest("none", true), "none");
363
+ });
364
+
351
365
  test("discuss-to-auto handoff defaults to step mode unless explicitly disabled", () => {
352
366
  const guidedFlowSrc = readGsdFile("guided-flow.ts");
353
367
  const workflowSrc = readGsdFile("commands/handlers/workflow.ts");
@@ -24,7 +24,9 @@ test("buildMinimalGsdToolSet preserves non-GSD tools and replaces broad GSD surf
24
24
  "gsd_milestone_status",
25
25
  "gsd_checkpoint_db",
26
26
  "memory_query",
27
+ "gsd_memory_query",
27
28
  "capture_thought",
29
+ "gsd_capture_thought",
28
30
  "gsd_graph",
29
31
  ]);
30
32
 
@@ -575,6 +577,42 @@ test("scopeGsdWorkflowToolsForDispatch applies and restores per-unit skill visib
575
577
  assert.equal(calls.filter((call) => call.kind === "skills").length, 2);
576
578
  });
577
579
 
580
+ // ── Regression #534: auto-mode subprocess cannot call gsd_memory_query / gsd_capture_thought ──
581
+ // MCP-workflow subprocesses register the gsd_-prefixed variants, not the pi-native names.
582
+ // MINIMAL_GSD_TOOL_NAMES must include both so resolveScopedToolNames exposes them.
583
+
584
+ test("MINIMAL_GSD_TOOL_NAMES includes gsd_memory_query and gsd_capture_thought (regression #534)", () => {
585
+ assert.ok(
586
+ (MINIMAL_GSD_TOOL_NAMES as readonly string[]).includes("gsd_memory_query"),
587
+ "MINIMAL_GSD_TOOL_NAMES must include gsd_memory_query for MCP-workflow surface parity",
588
+ );
589
+ assert.ok(
590
+ (MINIMAL_GSD_TOOL_NAMES as readonly string[]).includes("gsd_capture_thought"),
591
+ "MINIMAL_GSD_TOOL_NAMES must include gsd_capture_thought for MCP-workflow surface parity",
592
+ );
593
+ });
594
+
595
+ test("buildMinimalAutoGsdToolSet resolves MCP-scoped gsd_memory_query and gsd_capture_thought when subprocess only registers gsd_-prefixed variants (regression #534)", () => {
596
+ // Simulate a subprocess that only exposes gsd_-prefixed MCP tool names,
597
+ // not the pi-native memory_query / capture_thought variants.
598
+ const result = buildMinimalAutoGsdToolSet([
599
+ "bash",
600
+ "read",
601
+ "mcp__gsd-workflow__gsd_exec",
602
+ "mcp__gsd-workflow__gsd_memory_query",
603
+ "mcp__gsd-workflow__gsd_capture_thought",
604
+ ], "execute-task");
605
+
606
+ assert.ok(
607
+ result.includes("mcp__gsd-workflow__gsd_memory_query"),
608
+ "mcp__gsd-workflow__gsd_memory_query must be included when only the gsd_-prefixed variant is available",
609
+ );
610
+ assert.ok(
611
+ result.includes("mcp__gsd-workflow__gsd_capture_thought"),
612
+ "mcp__gsd-workflow__gsd_capture_thought must be included when only the gsd_-prefixed variant is available",
613
+ );
614
+ });
615
+
578
616
  test("applyUnitSkillVisibility sets manifest or clears for wildcard", () => {
579
617
  const calls: Array<string[] | undefined> = [];
580
618
  applyUnitSkillVisibility({
@@ -2,6 +2,7 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
 
4
4
  import * as userInputBoundary from "../user-input-boundary.ts";
5
+ import { isAwaitingUserInput, shouldPauseForUserApprovalQuestion } from "../user-input-boundary.ts";
5
6
 
6
7
  test("lastAssistantText extracts the latest assistant text block content", () => {
7
8
  const lastAssistantText = (userInputBoundary as {
@@ -24,3 +25,64 @@ test("lastAssistantText extracts the latest assistant text block content", () =>
24
25
  );
25
26
  assert.equal(lastAssistantText?.(null), "");
26
27
  });
28
+
29
+ test("lastAssistantText includes thinking blocks so rate-limit notices are not dropped", () => {
30
+ const lastAssistantText = (userInputBoundary as {
31
+ lastAssistantText?: (messages: unknown[] | null | undefined) => string;
32
+ }).lastAssistantText;
33
+
34
+ assert.equal(typeof lastAssistantText, "function");
35
+ // Turn with only a thinking block (no text block) — must not return ""
36
+ const result = lastAssistantText?.([
37
+ {
38
+ role: "assistant",
39
+ content: [
40
+ { type: "thinking", thinking: "You've hit your limit · resets in 2h" },
41
+ ],
42
+ },
43
+ ]);
44
+ assert.ok(result?.includes("You've hit your limit"), `expected rate-limit text, got: ${JSON.stringify(result)}`);
45
+ });
46
+
47
+ test("isAwaitingUserInput does not trigger on thinking-block question marks", () => {
48
+ // A thinking block with a question mark must NOT pause auto-mode —
49
+ // it's internal reasoning, not a user-visible prompt.
50
+ const messages = [
51
+ {
52
+ role: "assistant",
53
+ content: [
54
+ { type: "thinking", thinking: "Should I skip research? Let me check the config." },
55
+ ],
56
+ },
57
+ ];
58
+ assert.equal(isAwaitingUserInput(messages), false);
59
+ assert.equal(shouldPauseForUserApprovalQuestion("discuss-project", messages), false);
60
+ });
61
+
62
+ test("isAwaitingUserInput does not trigger on thinking-block approval phrases", () => {
63
+ // A thinking block with approval phrases must NOT pause auto-mode.
64
+ const messages = [
65
+ {
66
+ role: "assistant",
67
+ content: [
68
+ { type: "thinking", thinking: "The user confirmed and approved the plan. Should I proceed?" },
69
+ ],
70
+ },
71
+ ];
72
+ assert.equal(isAwaitingUserInput(messages), false);
73
+ assert.equal(shouldPauseForUserApprovalQuestion("discuss-requirements", messages), false);
74
+ });
75
+
76
+ test("isAwaitingUserInput still triggers on text-block question marks when thinking is also present", () => {
77
+ // When thinking + text are both present and the text asks a question, it should still pause.
78
+ const messages = [
79
+ {
80
+ role: "assistant",
81
+ content: [
82
+ { type: "thinking", thinking: "Internal reasoning without questions." },
83
+ { type: "text", text: "Does this look correct?" },
84
+ ],
85
+ },
86
+ ];
87
+ assert.equal(isAwaitingUserInput(messages), true);
88
+ });
@@ -150,6 +150,30 @@ describe("Worktree Safety module", () => {
150
150
  assert.equal(result.kind, "safe");
151
151
  });
152
152
 
153
+ test("accepts project root for source-writing Unit when isolation mode is branch", () => {
154
+ const safety = createWorktreeSafetyModule({
155
+ existsSync: () => true,
156
+ lstatSync: () => ({ isFile: () => false }),
157
+ listRegisteredWorktrees: () => [{ path: projectRoot, branch: "milestone/M001" }],
158
+ getCurrentBranch: () => "milestone/M001",
159
+ });
160
+
161
+ const result = safety.validateUnitRoot({
162
+ unitType: "execute-task",
163
+ unitId: "M001/S01/T01",
164
+ writeScope: "source-writing",
165
+ projectRoot,
166
+ unitRoot: projectRoot,
167
+ milestoneId: "M001",
168
+ isolationMode: "branch",
169
+ expectedBranch: "milestone/M001",
170
+ });
171
+
172
+ assert.equal(result.ok, true);
173
+ assert.equal(result.kind, "safe");
174
+ assert.equal(result.branch, "milestone/M001");
175
+ });
176
+
153
177
  test("rejects non-project root for source-writing Unit when isolation mode is none", () => {
154
178
  const safety = createWorktreeSafetyModule({
155
179
  existsSync: () => true,
@@ -9,7 +9,7 @@ import test from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import { join, sep } from 'node:path';
11
11
 
12
- import { GSD_PHASE_SCOPE_DISPLAY_REASON } from '../auto-unit-tool-scope.ts';
12
+ import { GSD_PHASE_SCOPE_DISPLAY_REASON, GSD_SECTION_CLOSE_GATE_DISPLAY_REASON } from '../auto-unit-tool-scope.ts';
13
13
  import { ALLOWED_PLANNING_DISPATCH_AGENTS, shouldBlockPlanningUnit } from '../bootstrap/write-gate.ts';
14
14
  import { extractSubagentAgentClasses } from '../bootstrap/subagent-input.ts';
15
15
  import { isDeterministicPolicyError } from '../auto-tool-tracking.ts';
@@ -377,14 +377,26 @@ test('auto-unit scope: execute-task allows only its task completion lifecycle to
377
377
  );
378
378
  assert.strictEqual(allowed.block, false);
379
379
 
380
+ // execute-task closes gates from summary sections, so gsd_save_gate_result gets
381
+ // the softer deterministic redirect instead of the normal HARD BLOCK wall.
380
382
  const blocked = shouldBlockPlanningUnit('gsd_save_gate_result', '', BASE, 'execute-task', ALL);
381
383
  assert.strictEqual(blocked.block, true);
382
- assert.match(blocked.reason!, /HARD BLOCK/);
384
+ assert.doesNotMatch(blocked.reason!, /HARD BLOCK/);
383
385
  assert.match(blocked.reason!, /gsd_save_gate_result/);
384
- assert.strictEqual(blocked.displayReason, GSD_PHASE_SCOPE_DISPLAY_REASON);
386
+ assert.match(blocked.reason!, /summary sections/);
387
+ assert.strictEqual(blocked.displayReason, GSD_SECTION_CLOSE_GATE_DISPLAY_REASON);
385
388
  assert.strictEqual(isDeterministicPolicyError(blocked.reason!), true);
386
389
  });
387
390
 
391
+ test('auto-unit scope: section-close gate units get the calm gsd_save_gate_result redirect', () => {
392
+ for (const unit of ['execute-task', 'complete-slice', 'validate-milestone']) {
393
+ const r = shouldBlockPlanningUnit('gsd_save_gate_result', '', BASE, unit, ALL);
394
+ assert.strictEqual(r.block, true, `${unit} should still block the call`);
395
+ assert.doesNotMatch(r.reason!, /HARD BLOCK/, `${unit} should use the calm redirect`);
396
+ assert.strictEqual(isDeterministicPolicyError(r.reason!), true, `${unit} redirect must stay deterministic`);
397
+ }
398
+ });
399
+
388
400
  test('auto-unit scope: execute-task blocks sibling task completion', () => {
389
401
  const r = shouldBlockPlanningUnit(
390
402
  'gsd_complete_task',