gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445

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 (155) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +7 -2
  3. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
  5. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
  7. package/dist/resources/extensions/gsd/auto.js +62 -1
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  9. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
  10. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  11. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  12. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  13. package/dist/resources/extensions/gsd/gsd-db.js +194 -0
  14. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  15. package/dist/resources/extensions/gsd/guided-flow.js +117 -25
  16. package/dist/resources/extensions/gsd/metrics.js +287 -1
  17. package/dist/resources/extensions/gsd/paths.js +79 -8
  18. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  20. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  23. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  24. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  25. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  26. package/dist/resources/extensions/gsd/workspace.js +59 -0
  27. package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
  28. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  29. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  30. package/dist/web/standalone/.next/BUILD_ID +1 -1
  31. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  32. package/dist/web/standalone/.next/build-manifest.json +2 -2
  33. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  34. package/dist/web/standalone/.next/required-server-files.json +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.html +1 -1
  52. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/README.md +2 -11
  66. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  67. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  68. package/packages/mcp-server/dist/remote-questions.js +28 -0
  69. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  70. package/packages/mcp-server/dist/server.d.ts +28 -0
  71. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  72. package/packages/mcp-server/dist/server.js +94 -4
  73. package/packages/mcp-server/dist/server.js.map +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  76. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  77. package/packages/mcp-server/src/remote-questions.ts +35 -0
  78. package/packages/mcp-server/src/server.ts +129 -6
  79. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  81. package/src/resources/extensions/gsd/auto/phases.ts +8 -2
  82. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  83. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
  84. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
  85. package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
  86. package/src/resources/extensions/gsd/auto.ts +79 -1
  87. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  88. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
  89. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  90. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  91. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  92. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  93. package/src/resources/extensions/gsd/gsd-db.ts +184 -0
  94. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  95. package/src/resources/extensions/gsd/guided-flow.ts +154 -25
  96. package/src/resources/extensions/gsd/metrics.ts +321 -1
  97. package/src/resources/extensions/gsd/paths.ts +67 -8
  98. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  99. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  100. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  101. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  102. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  103. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  104. package/src/resources/extensions/gsd/templates/project.md +10 -0
  105. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  106. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  107. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  108. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  109. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  110. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  111. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  112. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  113. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  114. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  115. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  116. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  117. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  118. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  119. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  120. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  121. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
  123. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  124. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  125. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  126. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  127. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  128. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  129. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  130. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  131. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  132. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  133. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  134. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  135. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
  136. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
  137. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  138. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
  139. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  140. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
  141. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  142. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  143. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  144. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  145. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  146. package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
  147. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  148. package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
  149. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  150. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  151. package/src/resources/extensions/gsd/workspace.ts +95 -0
  152. package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
  153. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  154. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
  155. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_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,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
+ });
@@ -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
+ });