gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27

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 (212) hide show
  1. package/README.md +5 -7
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +6 -1
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
  6. package/dist/resources/extensions/gsd/auto/loop.js +235 -36
  7. package/dist/resources/extensions/gsd/auto/phases.js +14 -7
  8. package/dist/resources/extensions/gsd/auto/session.js +36 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
  11. package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
  12. package/dist/resources/extensions/gsd/auto.js +139 -49
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  16. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  17. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  18. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  19. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  20. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  22. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  23. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  24. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  25. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  26. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  27. package/dist/resources/extensions/gsd/doctor.js +12 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +355 -3
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  30. package/dist/resources/extensions/gsd/guided-flow.js +116 -26
  31. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  32. package/dist/resources/extensions/gsd/metrics.js +287 -1
  33. package/dist/resources/extensions/gsd/paths.js +79 -8
  34. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  38. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  40. package/dist/resources/extensions/gsd/state.js +21 -6
  41. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  42. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  43. package/dist/resources/extensions/gsd/workspace.js +59 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
  45. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  78. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  79. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/dist/web/standalone/server.js +1 -1
  81. package/package.json +1 -1
  82. package/packages/mcp-server/README.md +2 -11
  83. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  85. package/packages/mcp-server/dist/remote-questions.js +28 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  87. package/packages/mcp-server/dist/server.d.ts +28 -0
  88. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/server.js +94 -4
  90. package/packages/mcp-server/dist/server.js.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  92. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  93. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  94. package/packages/mcp-server/src/remote-questions.ts +35 -0
  95. package/packages/mcp-server/src/server.ts +129 -6
  96. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  97. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  98. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  99. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  100. package/src/resources/extensions/gsd/auto/phases.ts +15 -7
  101. package/src/resources/extensions/gsd/auto/session.ts +40 -0
  102. package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
  104. package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
  105. package/src/resources/extensions/gsd/auto.ts +166 -43
  106. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
  108. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  109. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  110. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  111. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  112. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  113. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  114. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  115. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  116. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  117. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  118. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  119. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  120. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  121. package/src/resources/extensions/gsd/doctor.ts +10 -2
  122. package/src/resources/extensions/gsd/gsd-db.ts +354 -3
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  124. package/src/resources/extensions/gsd/guided-flow.ts +152 -26
  125. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  126. package/src/resources/extensions/gsd/metrics.ts +321 -1
  127. package/src/resources/extensions/gsd/paths.ts +67 -8
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  129. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  130. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  131. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  134. package/src/resources/extensions/gsd/state.ts +44 -6
  135. package/src/resources/extensions/gsd/templates/project.md +10 -0
  136. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  137. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  138. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  139. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  140. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  141. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  142. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  143. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  144. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  145. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  146. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  147. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  148. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  149. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  150. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  151. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  152. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  153. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  154. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  155. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  156. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  157. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  158. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  159. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  160. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  161. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  162. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  163. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
  164. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  165. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  166. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  167. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  168. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  169. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  170. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  171. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  172. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  173. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  174. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  175. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  176. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  177. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  178. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  179. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  180. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  181. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  182. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  183. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  184. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
  185. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  186. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  187. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  188. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  189. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  190. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  192. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  194. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  196. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  197. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  198. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  199. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  200. package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
  201. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  202. package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
  203. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  204. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  205. package/src/resources/extensions/gsd/workspace.ts +95 -0
  206. package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
  207. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  208. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  209. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  210. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  211. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -0,0 +1,246 @@
1
+ // GSD-2 + Gate 1b recovery bound corrections — regression tests for the two bugs
2
+ // found in peer review of the H1 fix (commit f0e1d42a2):
3
+ // 1. Escalation message must describe /gsd (counter reset) AND /gsd-debug (diagnose).
4
+ // 2. planBlockedRecoveryCount must NOT increment when pi.sendMessage throws.
5
+
6
+ import { describe, test, beforeEach, afterEach } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+
12
+ import {
13
+ checkAutoStartAfterDiscuss,
14
+ setPendingAutoStart,
15
+ clearPendingAutoStart,
16
+ _getPendingAutoStart,
17
+ } from "../guided-flow.ts";
18
+ import { drainLogs } from "../workflow-logger.ts";
19
+ import {
20
+ openDatabase,
21
+ closeDatabase,
22
+ insertMilestone,
23
+ } from "../gsd-db.ts";
24
+
25
+ // ─── Harness ───────────────────────────────────────────────────────────────
26
+
27
+ interface MockCapture {
28
+ notifies: Array<{ msg: string; level: string }>;
29
+ messages: Array<{ payload: any; options: any }>;
30
+ }
31
+
32
+ function mkCapture(): MockCapture {
33
+ return { notifies: [], messages: [] };
34
+ }
35
+
36
+ function mkCtx(cap: MockCapture): any {
37
+ return {
38
+ ui: {
39
+ notify: (msg: string, level: string) => {
40
+ cap.notifies.push({ msg, level });
41
+ },
42
+ },
43
+ };
44
+ }
45
+
46
+ /** Returns a pi stub whose sendMessage throws on the first call, succeeds after. */
47
+ function mkPiThrowOnce(cap: MockCapture): any {
48
+ let callCount = 0;
49
+ return {
50
+ sendMessage: (payload: any, options: any) => {
51
+ callCount += 1;
52
+ if (callCount === 1) {
53
+ throw new Error("transient network error");
54
+ }
55
+ cap.messages.push({ payload, options });
56
+ },
57
+ setActiveTools: () => undefined,
58
+ getActiveTools: () => [],
59
+ };
60
+ }
61
+
62
+ function mkPi(cap: MockCapture): any {
63
+ return {
64
+ sendMessage: (payload: any, options: any) => {
65
+ cap.messages.push({ payload, options });
66
+ },
67
+ setActiveTools: () => undefined,
68
+ getActiveTools: () => [],
69
+ };
70
+ }
71
+
72
+ function mkBase(): string {
73
+ const base = mkdtempSync(join(tmpdir(), "gsd-gate1b-corrections-"));
74
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
75
+ writeFileSync(
76
+ join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
77
+ "# M001: Corrections Test\n\nContext written by discuss phase.\n",
78
+ );
79
+ return base;
80
+ }
81
+
82
+ // ─── Tests ─────────────────────────────────────────────────────────────────
83
+
84
+ describe("Gate 1b recovery bound corrections", () => {
85
+ let base: string;
86
+ let cap: MockCapture;
87
+
88
+ beforeEach(() => {
89
+ clearPendingAutoStart();
90
+ drainLogs();
91
+ });
92
+
93
+ afterEach(() => {
94
+ closeDatabase();
95
+ clearPendingAutoStart();
96
+ if (base) {
97
+ rmSync(base, { recursive: true, force: true });
98
+ }
99
+ });
100
+
101
+ // ── Fix 1: escalation message ──────────────────────────────────────────
102
+
103
+ test("escalation message describes /gsd for reset AND /gsd-debug for diagnosis", () => {
104
+ base = mkBase();
105
+ openDatabase(":memory:");
106
+ insertMilestone({ id: "M001", title: "Corrections Test", status: "queued" });
107
+
108
+ cap = mkCapture();
109
+ setPendingAutoStart(base, {
110
+ basePath: base,
111
+ milestoneId: "M001",
112
+ ctx: mkCtx(cap),
113
+ pi: mkPi(cap),
114
+ });
115
+
116
+ // Exhaust the recovery budget (MAX = 3)
117
+ checkAutoStartAfterDiscuss(); // count → 1
118
+ checkAutoStartAfterDiscuss(); // count → 2
119
+ checkAutoStartAfterDiscuss(); // count → 3
120
+
121
+ cap.notifies = [];
122
+ drainLogs();
123
+
124
+ // This call hits the cap and must escalate
125
+ const result = checkAutoStartAfterDiscuss();
126
+ assert.equal(result, false, "escalation call must return false");
127
+
128
+ const errorNotify = cap.notifies.find((n) => n.level === "error");
129
+ assert.ok(errorNotify, "escalation must emit a notify with level 'error'");
130
+
131
+ // Must mention /gsd with reset semantics
132
+ assert.match(
133
+ errorNotify.msg,
134
+ /\/gsd\b/,
135
+ "escalation message must reference /gsd (the command that resets the counter)",
136
+ );
137
+ assert.match(
138
+ errorNotify.msg,
139
+ /reset/i,
140
+ "escalation message must use the word 'reset' so users know /gsd resets the counter",
141
+ );
142
+
143
+ // Must also mention /gsd-debug
144
+ assert.match(
145
+ errorNotify.msg,
146
+ /\/gsd-debug/i,
147
+ "escalation message must also reference /gsd-debug for diagnosis",
148
+ );
149
+
150
+ // Must NOT suggest /gsd-debug alone as the sole remediation
151
+ assert.doesNotMatch(
152
+ errorNotify.msg,
153
+ /^[^/]*\/gsd-debug[^/]*$/,
154
+ "escalation message must not mention /gsd-debug as the only option",
155
+ );
156
+ });
157
+
158
+ // ── Fix 2: counter ordering ────────────────────────────────────────────
159
+
160
+ test("counter stays at 0 when sendMessage throws on the first call", () => {
161
+ base = mkBase();
162
+ openDatabase(":memory:");
163
+ insertMilestone({ id: "M001", title: "Corrections Test", status: "queued" });
164
+
165
+ cap = mkCapture();
166
+ setPendingAutoStart(base, {
167
+ basePath: base,
168
+ milestoneId: "M001",
169
+ ctx: mkCtx(cap),
170
+ pi: mkPiThrowOnce(cap),
171
+ });
172
+
173
+ // First call: sendMessage throws — counter must NOT increment
174
+ const result = checkAutoStartAfterDiscuss();
175
+ assert.equal(result, false, "must return false even when sendMessage throws");
176
+
177
+ const entry = _getPendingAutoStart(base);
178
+ assert.ok(entry, "entry must still exist after a failed sendMessage");
179
+ assert.equal(
180
+ entry.planBlockedRecoveryCount,
181
+ 0,
182
+ "counter must remain 0 when sendMessage throws — no budget burned by transient failure",
183
+ );
184
+ });
185
+
186
+ test("counter increments to 1 on the second call when first sendMessage threw", () => {
187
+ base = mkBase();
188
+ openDatabase(":memory:");
189
+ insertMilestone({ id: "M001", title: "Corrections Test", status: "queued" });
190
+
191
+ cap = mkCapture();
192
+ setPendingAutoStart(base, {
193
+ basePath: base,
194
+ milestoneId: "M001",
195
+ ctx: mkCtx(cap),
196
+ pi: mkPiThrowOnce(cap),
197
+ });
198
+
199
+ checkAutoStartAfterDiscuss(); // sendMessage throws → count stays 0
200
+
201
+ const entryAfterThrow = _getPendingAutoStart(base);
202
+ assert.equal(entryAfterThrow!.planBlockedRecoveryCount, 0, "count is 0 after throw");
203
+
204
+ checkAutoStartAfterDiscuss(); // sendMessage succeeds → count becomes 1
205
+ assert.equal(cap.messages.length, 1, "second call must produce one successful sendMessage");
206
+
207
+ const entryAfterSuccess = _getPendingAutoStart(base);
208
+ assert.equal(
209
+ entryAfterSuccess!.planBlockedRecoveryCount,
210
+ 1,
211
+ "counter must be 1 after first successful dispatch",
212
+ );
213
+ });
214
+
215
+ test("3 successful sendMessage calls exhaust the budget; 4th emits escalation notify", () => {
216
+ base = mkBase();
217
+ openDatabase(":memory:");
218
+ insertMilestone({ id: "M001", title: "Corrections Test", status: "queued" });
219
+
220
+ cap = mkCapture();
221
+ setPendingAutoStart(base, {
222
+ basePath: base,
223
+ milestoneId: "M001",
224
+ ctx: mkCtx(cap),
225
+ pi: mkPi(cap),
226
+ });
227
+
228
+ // Three successful recoveries
229
+ checkAutoStartAfterDiscuss(); // count → 1
230
+ checkAutoStartAfterDiscuss(); // count → 2
231
+ checkAutoStartAfterDiscuss(); // count → 3
232
+
233
+ const entry = _getPendingAutoStart(base);
234
+ assert.equal(entry!.planBlockedRecoveryCount, 3, "counter must be 3 after three successes");
235
+ assert.equal(cap.messages.length, 3, "three sendMessage calls must have occurred");
236
+
237
+ // Fourth call hits the cap
238
+ cap.notifies = [];
239
+ cap.messages = [];
240
+ const resultAtCap = checkAutoStartAfterDiscuss();
241
+ assert.equal(resultAtCap, false, "4th call must return false");
242
+ assert.equal(cap.messages.length, 0, "4th call must NOT call sendMessage");
243
+ const errorNotify = cap.notifies.find((n) => n.level === "error");
244
+ assert.ok(errorNotify, "4th call must emit escalation notify with level 'error'");
245
+ });
246
+ });
@@ -0,0 +1,218 @@
1
+ // GSD-2 + Gate 1b recovery counter bound — regression tests for H1 fix (#5012)
2
+ //
3
+ // Verifies that checkAutoStartAfterDiscuss stops emitting plan-blocked recovery
4
+ // hints (with triggerTurn:true) after MAX_PLAN_BLOCKED_RECOVERIES attempts and
5
+ // instead escalates to the user via ctx.ui.notify("error"), breaking the loop.
6
+
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ import {
14
+ checkAutoStartAfterDiscuss,
15
+ setPendingAutoStart,
16
+ clearPendingAutoStart,
17
+ _getPendingAutoStart,
18
+ } from "../guided-flow.ts";
19
+ import { drainLogs } from "../workflow-logger.ts";
20
+ import {
21
+ openDatabase,
22
+ closeDatabase,
23
+ insertMilestone,
24
+ } from "../gsd-db.ts";
25
+
26
+ // ─── Harness ───────────────────────────────────────────────────────────────
27
+
28
+ interface MockCapture {
29
+ notifies: Array<{ msg: string; level: string }>;
30
+ messages: Array<{ payload: any; options: any }>;
31
+ }
32
+
33
+ function mkCapture(): MockCapture {
34
+ return { notifies: [], messages: [] };
35
+ }
36
+
37
+ function mkCtx(cap: MockCapture): any {
38
+ return {
39
+ ui: {
40
+ notify: (msg: string, level: string) => {
41
+ cap.notifies.push({ msg, level });
42
+ },
43
+ },
44
+ };
45
+ }
46
+
47
+ function mkPi(cap: MockCapture): any {
48
+ return {
49
+ sendMessage: (payload: any, options: any) => {
50
+ cap.messages.push({ payload, options });
51
+ },
52
+ setActiveTools: () => undefined,
53
+ getActiveTools: () => [],
54
+ };
55
+ }
56
+
57
+ function mkBase(): string {
58
+ const base = mkdtempSync(join(tmpdir(), "gsd-gate1b-bound-"));
59
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
60
+ writeFileSync(
61
+ join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
62
+ "# M001: Bound Test\n\nContext written by discuss phase.\n",
63
+ );
64
+ return base;
65
+ }
66
+
67
+ // ─── Tests ─────────────────────────────────────────────────────────────────
68
+
69
+ describe("Gate 1b recovery bound (H1)", () => {
70
+ let base: string;
71
+ let cap: MockCapture;
72
+
73
+ beforeEach(() => {
74
+ clearPendingAutoStart();
75
+ drainLogs();
76
+ });
77
+
78
+ afterEach(() => {
79
+ closeDatabase();
80
+ clearPendingAutoStart();
81
+ if (base) {
82
+ rmSync(base, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ test("first N-1 invocations increment counter and emit recovery with triggerTurn:true", () => {
87
+ base = mkBase();
88
+ openDatabase(":memory:");
89
+ insertMilestone({ id: "M001", title: "Bound Test", status: "queued" });
90
+
91
+ cap = mkCapture();
92
+ setPendingAutoStart(base, {
93
+ basePath: base,
94
+ milestoneId: "M001",
95
+ ctx: mkCtx(cap),
96
+ pi: mkPi(cap),
97
+ });
98
+
99
+ // MAX_PLAN_BLOCKED_RECOVERIES = 3; first two calls should emit recovery
100
+ const resultOne = checkAutoStartAfterDiscuss();
101
+ assert.equal(resultOne, false, "call 1: must return false");
102
+ assert.equal(cap.messages.length, 1, "call 1: exactly one sendMessage");
103
+ assert.equal(cap.messages[0].options.triggerTurn, true, "call 1: triggerTurn must be true");
104
+ assert.equal(cap.messages[0].payload.customType, "gsd-plan-milestone-blocked-recovery");
105
+
106
+ const entryAfterOne = _getPendingAutoStart(base);
107
+ assert.ok(entryAfterOne, "entry must still exist after call 1");
108
+ assert.equal(entryAfterOne.planBlockedRecoveryCount, 1, "counter must be 1 after call 1");
109
+
110
+ const resultTwo = checkAutoStartAfterDiscuss();
111
+ assert.equal(resultTwo, false, "call 2: must return false");
112
+ assert.equal(cap.messages.length, 2, "call 2: second sendMessage emitted");
113
+ assert.equal(cap.messages[1].options.triggerTurn, true, "call 2: triggerTurn must be true");
114
+
115
+ const entryAfterTwo = _getPendingAutoStart(base);
116
+ assert.ok(entryAfterTwo, "entry must still exist after call 2");
117
+ assert.equal(entryAfterTwo.planBlockedRecoveryCount, 2, "counter must be 2 after call 2");
118
+ });
119
+
120
+ test("Nth invocation (at MAX_PLAN_BLOCKED_RECOVERIES) escalates via notify(error) without sendMessage(triggerTurn)", () => {
121
+ base = mkBase();
122
+ openDatabase(":memory:");
123
+ insertMilestone({ id: "M001", title: "Bound Test", status: "queued" });
124
+
125
+ cap = mkCapture();
126
+ setPendingAutoStart(base, {
127
+ basePath: base,
128
+ milestoneId: "M001",
129
+ ctx: mkCtx(cap),
130
+ pi: mkPi(cap),
131
+ });
132
+
133
+ // Exhaust the recovery budget (MAX = 3): call 3 times to reach the limit
134
+ checkAutoStartAfterDiscuss(); // count → 1
135
+ checkAutoStartAfterDiscuss(); // count → 2
136
+ checkAutoStartAfterDiscuss(); // count → 3
137
+
138
+ // At count = 3 the counter equals MAX so the next call must escalate
139
+ cap.messages = [];
140
+ cap.notifies = [];
141
+ drainLogs();
142
+
143
+ const resultAtLimit = checkAutoStartAfterDiscuss();
144
+ assert.equal(resultAtLimit, false, "at-limit call: must return false");
145
+
146
+ // Must NOT trigger a new LLM turn
147
+ assert.equal(
148
+ cap.messages.length,
149
+ 0,
150
+ "at-limit call: sendMessage must NOT be called (loop must stop)",
151
+ );
152
+ const triggerMessages = cap.messages.filter((m) => m.options?.triggerTurn);
153
+ assert.equal(triggerMessages.length, 0, "no triggerTurn message after limit");
154
+
155
+ // Must escalate to user via notify("error")
156
+ const errorNotify = cap.notifies.find((n) => n.level === "error");
157
+ assert.ok(errorNotify, "at-limit call: ctx.ui.notify('error') must be called");
158
+ assert.match(
159
+ errorNotify.msg,
160
+ /gsd-debug/i,
161
+ "error notification must direct user to run /gsd-debug",
162
+ );
163
+ assert.match(
164
+ errorNotify.msg,
165
+ /M001/,
166
+ "error notification must include the milestone ID",
167
+ );
168
+
169
+ // Confirm the log records the escalation
170
+ const logs = drainLogs();
171
+ const escalationLog = logs.find(
172
+ (e) => e.component === "guided" && /Gate 1b/.test(e.message) && /escalat/.test(e.message),
173
+ );
174
+ assert.ok(escalationLog, "escalation must be logged via logWarning");
175
+ });
176
+
177
+ test("after clearPendingAutoStart + setPendingAutoStart the counter is reset to 0", () => {
178
+ base = mkBase();
179
+ openDatabase(":memory:");
180
+ insertMilestone({ id: "M001", title: "Bound Test", status: "queued" });
181
+
182
+ cap = mkCapture();
183
+ setPendingAutoStart(base, {
184
+ basePath: base,
185
+ milestoneId: "M001",
186
+ ctx: mkCtx(cap),
187
+ pi: mkPi(cap),
188
+ });
189
+
190
+ // Advance counter to 2
191
+ checkAutoStartAfterDiscuss();
192
+ checkAutoStartAfterDiscuss();
193
+
194
+ const entryBefore = _getPendingAutoStart(base);
195
+ assert.ok(entryBefore, "entry must exist");
196
+ assert.equal(entryBefore.planBlockedRecoveryCount, 2, "counter must be 2 before reset");
197
+
198
+ // Simulate user retry: clear then re-set
199
+ clearPendingAutoStart(base);
200
+ cap = mkCapture();
201
+ setPendingAutoStart(base, {
202
+ basePath: base,
203
+ milestoneId: "M001",
204
+ ctx: mkCtx(cap),
205
+ pi: mkPi(cap),
206
+ });
207
+
208
+ const entryAfter = _getPendingAutoStart(base);
209
+ assert.ok(entryAfter, "entry must exist after re-set");
210
+ assert.equal(entryAfter.planBlockedRecoveryCount, 0, "counter must be 0 after re-set (fresh entry)");
211
+
212
+ // Verify first call after reset emits recovery, not escalation
213
+ const result = checkAutoStartAfterDiscuss();
214
+ assert.equal(result, false, "first call after reset must return false");
215
+ assert.equal(cap.messages.length, 1, "recovery hint must be emitted after reset");
216
+ assert.equal(cap.messages[0].options.triggerTurn, true, "triggerTurn must be true after reset");
217
+ });
218
+ });
@@ -0,0 +1,117 @@
1
+ // GSD-2 + gsd-db failed-open restore: previous workspace connection survives a failed openDatabaseByWorkspace
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ import {
10
+ openDatabase,
11
+ openDatabaseByWorkspace,
12
+ closeDatabase,
13
+ isDbAvailable,
14
+ getDbPath,
15
+ _getDbCache,
16
+ } from "../gsd-db.ts";
17
+ import { createWorkspace } from "../workspace.ts";
18
+ import type { GsdWorkspace } from "../workspace.ts";
19
+
20
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
21
+
22
+ function makeProjectDir(base: string): string {
23
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
24
+ return base;
25
+ }
26
+
27
+ // ─── Tests ───────────────────────────────────────────────────────────────────
28
+
29
+ describe("openDatabaseByWorkspace: restores previous connection on failure", () => {
30
+ let tmpA: string;
31
+ let tmpB: string;
32
+
33
+ beforeEach(() => {
34
+ tmpA = mkdtempSync(join(tmpdir(), "gsd-db-restore-a-"));
35
+ tmpB = mkdtempSync(join(tmpdir(), "gsd-db-restore-b-"));
36
+ makeProjectDir(tmpA);
37
+ makeProjectDir(tmpB);
38
+ });
39
+
40
+ afterEach(() => {
41
+ closeDatabase();
42
+ rmSync(tmpA, { recursive: true, force: true });
43
+ rmSync(tmpB, { recursive: true, force: true });
44
+ });
45
+
46
+ test("previous workspace connection stays active after failed switch to non-existent path", () => {
47
+ // Open workspace A successfully
48
+ const wsA = createWorkspace(tmpA);
49
+ const openedA = openDatabaseByWorkspace(wsA);
50
+ assert.ok(openedA, "opening workspace A should succeed");
51
+ assert.ok(isDbAvailable(), "DB should be available after opening A");
52
+
53
+ const pathAfterA = getDbPath();
54
+ assert.ok(pathAfterA, "should have a DB path after opening A");
55
+
56
+ // Attempt to open a workspace pointing to a completely non-existent directory
57
+ // ("/does-not-exist-gsd-test" cannot be created), which will cause openDatabase to throw.
58
+ const fakeWs = {
59
+ identityKey: "fake-key-that-does-not-exist",
60
+ projectRoot: "/does-not-exist-gsd-test-ws-restore",
61
+ worktreeRoot: null,
62
+ mode: "project" as const,
63
+ contract: {
64
+ projectRoot: "/does-not-exist-gsd-test-ws-restore",
65
+ workRoot: "/does-not-exist-gsd-test-ws-restore",
66
+ projectGsd: "/does-not-exist-gsd-test-ws-restore/.gsd",
67
+ projectDb: "/does-not-exist-gsd-test-ws-restore/.gsd/does-not-exist.db",
68
+ worktreeGsd: null,
69
+ isWorktree: false,
70
+ },
71
+ lockRoot: "/does-not-exist-gsd-test-ws-restore",
72
+ } satisfies GsdWorkspace;
73
+
74
+ // This should throw because the path is invalid
75
+ assert.throws(
76
+ () => openDatabaseByWorkspace(fakeWs),
77
+ (err: Error) => err instanceof Error,
78
+ );
79
+
80
+ // After the failure, the previous workspace A connection must be restored
81
+ assert.ok(isDbAvailable(), "DB must still be available (workspace A connection restored)");
82
+ const pathAfterFailure = getDbPath();
83
+ assert.equal(pathAfterFailure, pathAfterA, "DB path must match workspace A's path after failed switch");
84
+ });
85
+
86
+ test("cache still contains workspace A entry after failed switch", () => {
87
+ const wsA = createWorkspace(tmpA);
88
+ openDatabaseByWorkspace(wsA);
89
+
90
+ const fakeWs = {
91
+ identityKey: "fake-key-cache-test",
92
+ projectRoot: "/does-not-exist-gsd-cache-test",
93
+ worktreeRoot: null,
94
+ mode: "project" as const,
95
+ contract: {
96
+ projectRoot: "/does-not-exist-gsd-cache-test",
97
+ workRoot: "/does-not-exist-gsd-cache-test",
98
+ projectGsd: "/does-not-exist-gsd-cache-test/.gsd",
99
+ projectDb: "/does-not-exist-gsd-cache-test/.gsd/no.db",
100
+ worktreeGsd: null,
101
+ isWorktree: false,
102
+ },
103
+ lockRoot: "/does-not-exist-gsd-cache-test",
104
+ } satisfies GsdWorkspace;
105
+
106
+ assert.throws(() => openDatabaseByWorkspace(fakeWs));
107
+
108
+ const cache = _getDbCache();
109
+ assert.ok(cache.has(wsA.identityKey), "cache must retain workspace A's entry after failed switch");
110
+
111
+ // Workspace A's connection is back as the active connection; switching back
112
+ // should succeed from cache (cache hit path) without re-opening.
113
+ const reopened = openDatabaseByWorkspace(wsA);
114
+ assert.ok(reopened, "switching back to workspace A should succeed from cache");
115
+ assert.ok(isDbAvailable(), "DB should be available after re-activating A");
116
+ });
117
+ });