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,151 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ annotateBackgroundable,
5
+ getDelegationVerdict,
6
+ getVerdictByUnitType,
7
+ isBackgroundable,
8
+ listBackgroundableTools,
9
+ } from "../delegation-policy.js";
10
+
11
+ // Pin the GOOD set: changes here must come with explicit re-evaluation.
12
+ const EXPECTED_BACKGROUNDABLE = [
13
+ "gsd_execute",
14
+ "gsd_plan_slice",
15
+ "gsd_reassess_roadmap",
16
+ "gsd_validate_milestone",
17
+ ];
18
+
19
+ test("isBackgroundable returns true for the four GOOD-verdict tools", () => {
20
+ for (const name of EXPECTED_BACKGROUNDABLE) {
21
+ assert.equal(isBackgroundable(name), true, `${name} should be backgroundable`);
22
+ }
23
+ });
24
+
25
+ test("isBackgroundable returns false for RISKY-verdict tools", () => {
26
+ for (const name of ["gsd_doctor", "gsd_plan_milestone", "gsd_replan_slice"]) {
27
+ assert.equal(isBackgroundable(name), false, `${name} should not be backgroundable`);
28
+ }
29
+ });
30
+
31
+ test("isBackgroundable returns false for NO-verdict tools", () => {
32
+ assert.equal(isBackgroundable("gsd_plan_task"), false);
33
+ });
34
+
35
+ test("isBackgroundable defaults to false for unknown tools (default-deny)", () => {
36
+ assert.equal(isBackgroundable("gsd_nonexistent_tool"), false);
37
+ assert.equal(isBackgroundable(""), false);
38
+ });
39
+
40
+ test("listBackgroundableTools returns exactly the four GOOD tools, sorted", () => {
41
+ assert.deepEqual(listBackgroundableTools(), EXPECTED_BACKGROUNDABLE);
42
+ });
43
+
44
+ test("getDelegationVerdict resolves alias names to canonical entries", () => {
45
+ for (const [alias, canonical] of [
46
+ ["gsd_milestone_validate", "gsd_validate_milestone"],
47
+ ["gsd_roadmap_reassess", "gsd_reassess_roadmap"],
48
+ ["gsd_slice_replan", "gsd_replan_slice"],
49
+ ["gsd_task_plan", "gsd_plan_task"],
50
+ ] as const) {
51
+ const entry = getDelegationVerdict(alias);
52
+ assert.ok(entry, `alias ${alias} should resolve`);
53
+ assert.equal(entry.toolName, canonical, `${alias} should resolve to ${canonical}`);
54
+ }
55
+ });
56
+
57
+ test("plan_slice carries the slice-lock + await constraints", () => {
58
+ const entry = getDelegationVerdict("gsd_plan_slice");
59
+ assert.ok(entry);
60
+ assert.ok(entry.constraints && entry.constraints.length >= 3);
61
+ assert.ok(
62
+ entry.constraints!.some((c) => /lock the slice/i.test(c)),
63
+ "plan_slice must carry the slice-lock constraint",
64
+ );
65
+ assert.ok(
66
+ entry.constraints!.some((c) => /await background completion/i.test(c)),
67
+ "plan_slice must require await before downstream reads",
68
+ );
69
+ });
70
+
71
+ test("doctor carries fix-mode safety constraints", () => {
72
+ const entry = getDelegationVerdict("gsd_doctor");
73
+ assert.ok(entry);
74
+ assert.equal(entry.verdict, "risky");
75
+ assert.ok(
76
+ entry.constraints && entry.constraints.some((c) => /fix=false/.test(c)),
77
+ "doctor must restrict background runs to fix=false",
78
+ );
79
+ });
80
+
81
+ test("getVerdictByUnitType maps dispatcher unit types back to the policy", () => {
82
+ assert.equal(getVerdictByUnitType("plan-slice")?.toolName, "gsd_plan_slice");
83
+ assert.equal(getVerdictByUnitType("validate-milestone")?.toolName, "gsd_validate_milestone");
84
+ assert.equal(getVerdictByUnitType("reassess-roadmap")?.toolName, "gsd_reassess_roadmap");
85
+ assert.equal(getVerdictByUnitType("plan-milestone")?.toolName, "gsd_plan_milestone");
86
+ assert.equal(getVerdictByUnitType("replan-slice")?.toolName, "gsd_replan_slice");
87
+ assert.equal(getVerdictByUnitType("nonexistent-unit"), null);
88
+ });
89
+
90
+ test("every entry carries a non-empty rationale so the verdict is auditable", () => {
91
+ for (const name of [...EXPECTED_BACKGROUNDABLE, "gsd_doctor", "gsd_plan_milestone", "gsd_replan_slice", "gsd_plan_task"]) {
92
+ const entry = getDelegationVerdict(name);
93
+ assert.ok(entry, `${name} should be in the policy`);
94
+ assert.ok(entry.rationale.length > 20, `${name} rationale must be substantive`);
95
+ }
96
+ });
97
+
98
+ // ─── annotateBackgroundable contract pins ────────────────────────────────
99
+
100
+ test("annotateBackgroundable recomputes the verdict on every call (no internal cache)", () => {
101
+ // The annotator mutates in place. Repeated calls on the same object with
102
+ // different unit types must always reflect the latest unitType — never a
103
+ // stale cached value. This pins the contract documented in the JSDoc so a
104
+ // future "optimization" that adds memoization keyed on object identity
105
+ // breaks the suite instead of silently leaking a stale flag.
106
+ const action: { action: "dispatch"; unitType: string; backgroundable?: boolean } = {
107
+ action: "dispatch",
108
+ unitType: "plan-slice",
109
+ };
110
+ annotateBackgroundable(action);
111
+ assert.equal(action.backgroundable, true, "plan-slice should annotate true");
112
+
113
+ action.unitType = "plan-milestone";
114
+ annotateBackgroundable(action);
115
+ assert.equal(action.backgroundable, false, "plan-milestone (risky) should re-annotate false");
116
+
117
+ action.unitType = "validate-milestone";
118
+ annotateBackgroundable(action);
119
+ assert.equal(action.backgroundable, true, "validate-milestone should re-annotate true");
120
+
121
+ action.unitType = "complete-slice";
122
+ annotateBackgroundable(action);
123
+ assert.equal(action.backgroundable, false, "uncovered unit type should re-annotate false (default-deny)");
124
+ });
125
+
126
+ test("annotateBackgroundable passes stop/skip actions through unchanged", () => {
127
+ const stop = { action: "stop" as const, reason: "x", level: "info" as const };
128
+ const skip = { action: "skip" as const };
129
+ assert.equal(annotateBackgroundable(stop), stop);
130
+ assert.equal(annotateBackgroundable(skip), skip);
131
+ assert.equal((stop as Record<string, unknown>).backgroundable, undefined);
132
+ assert.equal((skip as Record<string, unknown>).backgroundable, undefined);
133
+ });
134
+
135
+ // ─── F4 latent gap pin: silent default-deny on unit types invoking GOOD tools ──
136
+
137
+ test("execute-task / reactive-execute / execute-task-simple intentionally default-deny despite gsd_execute being GOOD", () => {
138
+ // gsd_execute carries a GOOD verdict but no `unitType`, by design — the
139
+ // unit-level orchestrations wrap prompt and harness work whose safety is
140
+ // a separate analysis. Lifting these out of default-deny must be an
141
+ // explicit, audited change. This test pins the current behavior; if the
142
+ // policy entry gains a unitType mapping (or a unitTypes array), update
143
+ // both the entry and this test together.
144
+ for (const unitType of ["execute-task", "execute-task-simple", "reactive-execute"]) {
145
+ assert.equal(
146
+ getVerdictByUnitType(unitType),
147
+ null,
148
+ `${unitType} must remain unmapped until per-unit analysis is recorded`,
149
+ );
150
+ }
151
+ });
@@ -0,0 +1,173 @@
1
+ // gsd-2 + Stuck-detector retry coupling regression (Phase B / codex MEDIUM B3)
2
+ //
3
+ // Rule 2b previously tripped on 3 same-unit appearances regardless of
4
+ // retry budget. With unit_dispatches.attempt_n + next_run_at driving in-DB
5
+ // backoff, a unit that fails 3× under retry would trip the stuck-detector
6
+ // before its retry budget exhausted. This test verifies suppression while
7
+ // the retry window is open and re-engagement once the window passes or
8
+ // budget exhausts.
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import {
17
+ openDatabase,
18
+ closeDatabase,
19
+ insertMilestone,
20
+ _getAdapter,
21
+ } from "../gsd-db.ts";
22
+ import { registerAutoWorker } from "../db/auto-workers.ts";
23
+ import { claimMilestoneLease } from "../db/milestone-leases.ts";
24
+ import {
25
+ recordDispatchClaim,
26
+ markFailed,
27
+ getLatestForUnit,
28
+ } from "../db/unit-dispatches.ts";
29
+ import { detectStuck } from "../auto/detect-stuck.ts";
30
+ import type { WindowEntry } from "../auto/types.ts";
31
+
32
+ function makeBase(): string {
33
+ const base = mkdtempSync(join(tmpdir(), "gsd-detect-stuck-retry-"));
34
+ mkdirSync(join(base, ".gsd"), { recursive: true });
35
+ return base;
36
+ }
37
+
38
+ function cleanup(base: string): void {
39
+ try { closeDatabase(); } catch { /* noop */ }
40
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
41
+ }
42
+
43
+ function windowOf(...keys: string[]): WindowEntry[] {
44
+ return keys.map((key) => ({ key }));
45
+ }
46
+
47
+ test("rule 2b trips with no DB context (legacy behavior preserved)", () => {
48
+ // No DB open — getLatestForUnit returns null, suppression cannot fire,
49
+ // pre-Phase-B behavior is intact.
50
+ const result = detectStuck(
51
+ windowOf(
52
+ "plan-slice:M001/S01",
53
+ "other-unit",
54
+ "plan-slice:M001/S01",
55
+ "third-unit",
56
+ "plan-slice:M001/S01",
57
+ ),
58
+ );
59
+ assert.ok(result, "stuck signal returned");
60
+ assert.equal(result!.stuck, true);
61
+ });
62
+
63
+ test("rule 2b SUPPRESSED while retry budget remains and next_run_at is in the future", (t) => {
64
+ const base = makeBase();
65
+ t.after(() => cleanup(base));
66
+ openDatabase(join(base, ".gsd", "gsd.db"));
67
+ insertMilestone({ id: "M001", title: "T", status: "active" });
68
+ const w = registerAutoWorker({ projectRootRealpath: base });
69
+ const lease = claimMilestoneLease(w, "M001");
70
+ assert.equal(lease.ok, true);
71
+ if (!lease.ok) return;
72
+
73
+ // Record a failed dispatch with attempt_n=1, max_attempts=3, retry_after
74
+ // pushing next_run_at into the future.
75
+ const claim = recordDispatchClaim({
76
+ traceId: "t1", workerId: w, milestoneLeaseToken: lease.token,
77
+ milestoneId: "M001", unitType: "plan-slice", unitId: "plan-slice:M001/S01",
78
+ attemptN: 1, maxAttempts: 3,
79
+ });
80
+ assert.equal(claim.ok, true);
81
+ if (!claim.ok) return;
82
+ markFailed(claim.dispatchId, { errorSummary: "transient", retryAfterMs: 60_000 });
83
+
84
+ const latest = getLatestForUnit("plan-slice:M001/S01")!;
85
+ assert.equal(latest.attempt_n, 1);
86
+ assert.ok(latest.next_run_at);
87
+
88
+ const result = detectStuck(
89
+ windowOf(
90
+ "plan-slice:M001/S01",
91
+ "other-unit",
92
+ "plan-slice:M001/S01",
93
+ "third-unit",
94
+ "plan-slice:M001/S01",
95
+ ),
96
+ );
97
+ assert.equal(result, null, "rule 2b suppressed while retry window is active");
98
+ });
99
+
100
+ test("rule 2b RE-ENGAGES once attempt_n reaches max_attempts", (t) => {
101
+ const base = makeBase();
102
+ t.after(() => cleanup(base));
103
+ openDatabase(join(base, ".gsd", "gsd.db"));
104
+ insertMilestone({ id: "M001", title: "T", status: "active" });
105
+ const w = registerAutoWorker({ projectRootRealpath: base });
106
+ const lease = claimMilestoneLease(w, "M001");
107
+ assert.equal(lease.ok, true);
108
+ if (!lease.ok) return;
109
+
110
+ // Burn through attempts up to the cap — last attempt = max_attempts.
111
+ for (let attempt = 1; attempt <= 3; attempt++) {
112
+ const claim = recordDispatchClaim({
113
+ traceId: `t${attempt}`, workerId: w, milestoneLeaseToken: lease.token,
114
+ milestoneId: "M001", unitType: "plan-slice", unitId: "plan-slice:M001/S01",
115
+ attemptN: attempt, maxAttempts: 3,
116
+ });
117
+ assert.equal(claim.ok, true);
118
+ if (!claim.ok) return;
119
+ markFailed(claim.dispatchId, { errorSummary: "transient", retryAfterMs: 60_000 });
120
+ }
121
+
122
+ const latest = getLatestForUnit("plan-slice:M001/S01")!;
123
+ assert.equal(latest.attempt_n, 3);
124
+ assert.equal(latest.max_attempts, 3);
125
+
126
+ const result = detectStuck(
127
+ windowOf(
128
+ "plan-slice:M001/S01",
129
+ "other-unit",
130
+ "plan-slice:M001/S01",
131
+ "third-unit",
132
+ "plan-slice:M001/S01",
133
+ ),
134
+ );
135
+ assert.ok(result, "stuck signal returned once retry budget is exhausted");
136
+ });
137
+
138
+ test("rule 2b RE-ENGAGES once next_run_at is in the past", (t) => {
139
+ const base = makeBase();
140
+ t.after(() => cleanup(base));
141
+ openDatabase(join(base, ".gsd", "gsd.db"));
142
+ insertMilestone({ id: "M001", title: "T", status: "active" });
143
+ const w = registerAutoWorker({ projectRootRealpath: base });
144
+ const lease = claimMilestoneLease(w, "M001");
145
+ assert.equal(lease.ok, true);
146
+ if (!lease.ok) return;
147
+
148
+ const claim = recordDispatchClaim({
149
+ traceId: "t", workerId: w, milestoneLeaseToken: lease.token,
150
+ milestoneId: "M001", unitType: "plan-slice", unitId: "plan-slice:M001/S01",
151
+ attemptN: 1, maxAttempts: 3,
152
+ });
153
+ assert.equal(claim.ok, true);
154
+ if (!claim.ok) return;
155
+ markFailed(claim.dispatchId, { errorSummary: "transient", retryAfterMs: 60_000 });
156
+
157
+ // Force next_run_at into the past — retry window has already lapsed.
158
+ const db = _getAdapter()!;
159
+ db.prepare(
160
+ `UPDATE unit_dispatches SET next_run_at = '1970-01-01T00:00:00.000Z' WHERE id = :id`,
161
+ ).run({ ":id": claim.dispatchId });
162
+
163
+ const result = detectStuck(
164
+ windowOf(
165
+ "plan-slice:M001/S01",
166
+ "other-unit",
167
+ "plan-slice:M001/S01",
168
+ "third-unit",
169
+ "plan-slice:M001/S01",
170
+ ),
171
+ );
172
+ assert.ok(result, "stuck re-engages once retry window has passed");
173
+ });
@@ -0,0 +1,55 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ annotateBackgroundable,
5
+ type AnnotatableDispatchAction,
6
+ } from "../delegation-policy.js";
7
+
8
+ function dispatchAction(unitType: string): AnnotatableDispatchAction {
9
+ return {
10
+ action: "dispatch",
11
+ unitType,
12
+ unitId: `M001/${unitType}`,
13
+ prompt: "(test prompt)",
14
+ };
15
+ }
16
+
17
+ test("annotateBackgroundable marks plan-slice as backgroundable", () => {
18
+ const annotated = annotateBackgroundable(dispatchAction("plan-slice"));
19
+ assert.equal(annotated.action, "dispatch");
20
+ if (annotated.action !== "dispatch") return;
21
+ assert.equal(annotated.backgroundable, true);
22
+ assert.equal(annotated.unitType, "plan-slice");
23
+ });
24
+
25
+ test("annotateBackgroundable marks validate-milestone and reassess-roadmap as backgroundable", () => {
26
+ for (const unitType of ["validate-milestone", "reassess-roadmap"]) {
27
+ const annotated = annotateBackgroundable(dispatchAction(unitType));
28
+ assert.equal(annotated.action, "dispatch");
29
+ if (annotated.action !== "dispatch") continue;
30
+ assert.equal(annotated.backgroundable, true, `${unitType} should be backgroundable`);
31
+ }
32
+ });
33
+
34
+ test("annotateBackgroundable marks plan-milestone and replan-slice as NOT backgroundable", () => {
35
+ for (const unitType of ["plan-milestone", "replan-slice"]) {
36
+ const annotated = annotateBackgroundable(dispatchAction(unitType));
37
+ assert.equal(annotated.action, "dispatch");
38
+ if (annotated.action !== "dispatch") continue;
39
+ assert.equal(annotated.backgroundable, false, `${unitType} should not be backgroundable`);
40
+ }
41
+ });
42
+
43
+ test("annotateBackgroundable defaults unknown unit types to false (default-deny)", () => {
44
+ const annotated = annotateBackgroundable(dispatchAction("execute-task"));
45
+ assert.equal(annotated.action, "dispatch");
46
+ if (annotated.action !== "dispatch") return;
47
+ assert.equal(annotated.backgroundable, false);
48
+ });
49
+
50
+ test("annotateBackgroundable leaves stop and skip actions untouched", () => {
51
+ const stop: AnnotatableDispatchAction = { action: "stop", reason: "test", level: "info" };
52
+ const skip: AnnotatableDispatchAction = { action: "skip" };
53
+ assert.deepEqual(annotateBackgroundable(stop), stop);
54
+ assert.deepEqual(annotateBackgroundable(skip), skip);
55
+ });
@@ -133,29 +133,9 @@ assert(
133
133
  "stale CONTEXT-DRAFT.md should be deleted in both-files case",
134
134
  );
135
135
 
136
- // ─── Static: guided-flow.ts has cleanup code ───────────────────────────
137
-
138
- console.log("=== Static: cleanup code in guided-flow.ts ===");
139
-
140
- const { readFileSync } = await import("node:fs");
141
- const guidedFlowSource = readFileSync(
142
- join(import.meta.dirname, "..", "guided-flow.ts"),
143
- "utf-8",
144
- );
145
-
146
- const checkFnIdx = guidedFlowSource.indexOf("checkAutoStartAfterDiscuss");
147
- const checkFnEnd = guidedFlowSource.indexOf("\nexport ", checkFnIdx + 1);
148
- const checkFnChunk = guidedFlowSource.slice(checkFnIdx, checkFnEnd > checkFnIdx ? checkFnEnd : checkFnIdx + 5000);
149
-
150
- assert(
151
- checkFnChunk.includes("CONTEXT-DRAFT"),
152
- "checkAutoStartAfterDiscuss should reference CONTEXT-DRAFT for cleanup",
153
- );
154
-
155
- assert(
156
- checkFnChunk.includes("unlinkSync"),
157
- "checkAutoStartAfterDiscuss should use unlinkSync to delete the draft",
158
- );
136
+ // Note: source-grep assertions removed per CONTRIBUTING.md (no asserting against
137
+ // readFileSync of source). The behavioral scenarios above already exercise the
138
+ // CONTEXT-DRAFT cleanup path end-to-end via the actual filesystem state.
159
139
 
160
140
  // ─── Cleanup ──────────────────────────────────────────────────────────
161
141
 
@@ -0,0 +1,193 @@
1
+ /**
2
+ * GSD-2 / guided-flow — regression tests for Gate 1b orphan discrimination
3
+ *
4
+ * Gate 1b in checkAutoStartAfterDiscuss discriminates between two "queued" states:
5
+ * (a) plan-blocked: discuss completed (CONTEXT.md on disk), but gsd_plan_milestone
6
+ * was hard-blocked by the depth-verification gate. DB row stuck at "queued".
7
+ * → emit recovery hint directing the LLM to retry gsd_plan_milestone.
8
+ * (b) discuss-incomplete: discuss did not finish, no CONTEXT.md, DB row "queued".
9
+ * → silent block (no recovery hint).
10
+ */
11
+
12
+ import { describe, test, beforeEach, afterEach } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { tmpdir } from "node:os";
17
+
18
+ import {
19
+ checkAutoStartAfterDiscuss,
20
+ setPendingAutoStart,
21
+ clearPendingAutoStart,
22
+ } from "../guided-flow.ts";
23
+ import { drainLogs } from "../workflow-logger.ts";
24
+ import {
25
+ openDatabase,
26
+ closeDatabase,
27
+ insertMilestone,
28
+ } from "../gsd-db.ts";
29
+
30
+ // ─── Harness ───────────────────────────────────────────────────────────────
31
+
32
+ interface MockCapture {
33
+ notifies: Array<{ msg: string; level: string }>;
34
+ messages: Array<{ payload: any; options: any }>;
35
+ }
36
+
37
+ function mkCapture(): MockCapture {
38
+ return { notifies: [], messages: [] };
39
+ }
40
+
41
+ function mkCtx(cap: MockCapture): any {
42
+ return {
43
+ ui: {
44
+ notify: (msg: string, level: string) => {
45
+ cap.notifies.push({ msg, level });
46
+ },
47
+ },
48
+ };
49
+ }
50
+
51
+ function mkPi(cap: MockCapture): any {
52
+ return {
53
+ sendMessage: (payload: any, options: any) => {
54
+ cap.messages.push({ payload, options });
55
+ },
56
+ setActiveTools: () => undefined,
57
+ getActiveTools: () => [],
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Create a minimal temp tree with a .gsd/milestones/M001 directory.
63
+ */
64
+ function mkBase(): string {
65
+ const base = mkdtempSync(join(tmpdir(), "gsd-gate1b-"));
66
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
67
+ return base;
68
+ }
69
+
70
+ // ─── Tests ─────────────────────────────────────────────────────────────────
71
+
72
+ describe("Gate 1b orphan discrimination in checkAutoStartAfterDiscuss", () => {
73
+ let base: string;
74
+ let cap: MockCapture;
75
+
76
+ beforeEach(() => {
77
+ clearPendingAutoStart();
78
+ drainLogs(); // discard noise from prior tests
79
+ });
80
+
81
+ afterEach(() => {
82
+ closeDatabase();
83
+ clearPendingAutoStart();
84
+ if (base) {
85
+ rmSync(base, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ test("plan-blocked: CONTEXT.md present + DB row queued → returns false + recovery hint emitted", () => {
90
+ base = mkBase();
91
+ openDatabase(":memory:");
92
+
93
+ // DB row exists with status "queued" (plan_milestone was blocked)
94
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "queued" });
95
+
96
+ // CONTEXT.md on disk (discuss phase completed)
97
+ writeFileSync(
98
+ join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
99
+ "# M001: Test Milestone\n\nContext written by discuss phase.\n",
100
+ );
101
+
102
+ cap = mkCapture();
103
+ setPendingAutoStart(base, {
104
+ basePath: base,
105
+ milestoneId: "M001",
106
+ ctx: mkCtx(cap),
107
+ pi: mkPi(cap),
108
+ });
109
+
110
+ const result = checkAutoStartAfterDiscuss();
111
+
112
+ // Must return false — auto-start should not proceed
113
+ assert.equal(result, false, "checkAutoStartAfterDiscuss must return false (plan still blocked)");
114
+
115
+ // Recovery hint must be sent to the LLM
116
+ assert.equal(
117
+ cap.messages.length,
118
+ 1,
119
+ "exactly one sendMessage call expected for the recovery hint",
120
+ );
121
+ assert.equal(
122
+ cap.messages[0].payload.customType,
123
+ "gsd-plan-milestone-blocked-recovery",
124
+ "recovery message must have customType gsd-plan-milestone-blocked-recovery",
125
+ );
126
+ assert.equal(
127
+ cap.messages[0].options.triggerTurn,
128
+ true,
129
+ "recovery message must set triggerTurn: true",
130
+ );
131
+ assert.match(
132
+ cap.messages[0].payload.content,
133
+ /gsd_plan_milestone/,
134
+ "recovery message content must mention gsd_plan_milestone",
135
+ );
136
+
137
+ // User must be notified via ctx.ui.notify
138
+ assert.ok(
139
+ cap.notifies.some((n) => n.level === "warning" && /queued/.test(n.msg)),
140
+ "user must be notified with a warning about the queued state",
141
+ );
142
+
143
+ // logWarning must have recorded the Gate 1b event
144
+ const logs = drainLogs();
145
+ const gate1bLog = logs.find(
146
+ (e) => e.component === "guided" && /Gate 1b/.test(e.message),
147
+ );
148
+ assert.ok(gate1bLog, "Gate 1b warning must be logged via logWarning");
149
+ });
150
+
151
+ test("discuss-incomplete: no CONTEXT.md + DB row queued → returns false silently (no recovery hint)", () => {
152
+ base = mkBase();
153
+ openDatabase(":memory:");
154
+
155
+ // DB row exists with status "queued", but NO CONTEXT.md on disk
156
+ insertMilestone({ id: "M001", title: "Test Milestone", status: "queued" });
157
+
158
+ // No CONTEXT.md written — discuss phase is incomplete
159
+ cap = mkCapture();
160
+ setPendingAutoStart(base, {
161
+ basePath: base,
162
+ milestoneId: "M001",
163
+ ctx: mkCtx(cap),
164
+ pi: mkPi(cap),
165
+ });
166
+
167
+ drainLogs(); // clear any noise before the call
168
+
169
+ const result = checkAutoStartAfterDiscuss();
170
+
171
+ // Must return false — silent block
172
+ assert.equal(result, false, "checkAutoStartAfterDiscuss must return false when discuss is incomplete");
173
+
174
+ // No recovery hint — Gate 1 blocks before Gate 1b is reached
175
+ assert.equal(
176
+ cap.messages.length,
177
+ 0,
178
+ "no sendMessage calls expected when CONTEXT.md is absent",
179
+ );
180
+ assert.equal(
181
+ cap.notifies.length,
182
+ 0,
183
+ "no user notifications expected for discuss-incomplete case",
184
+ );
185
+
186
+ // No Gate 1b log entry
187
+ const logs = drainLogs();
188
+ const gate1bLog = logs.find(
189
+ (e) => e.component === "guided" && /Gate 1b/.test(e.message),
190
+ );
191
+ assert.equal(gate1bLog, undefined, "Gate 1b must not log when CONTEXT.md is absent");
192
+ });
193
+ });