gsd-pi 2.26.0 → 2.27.0

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 (171) hide show
  1. package/README.md +43 -6
  2. package/dist/cli.js +4 -2
  3. package/dist/headless.d.ts +3 -0
  4. package/dist/headless.js +136 -8
  5. package/dist/help-text.js +3 -0
  6. package/dist/loader.js +33 -4
  7. package/dist/resources/extensions/bg-shell/index.ts +19 -2
  8. package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
  9. package/dist/resources/extensions/bg-shell/types.ts +21 -1
  10. package/dist/resources/extensions/gsd/auto/session.ts +224 -0
  11. package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
  12. package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
  13. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
  15. package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
  16. package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
  17. package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
  18. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  19. package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  20. package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  21. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  22. package/dist/resources/extensions/gsd/auto.ts +977 -1551
  23. package/dist/resources/extensions/gsd/commands.ts +3 -3
  24. package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  25. package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
  26. package/dist/resources/extensions/gsd/export-html.ts +1001 -0
  27. package/dist/resources/extensions/gsd/export.ts +49 -1
  28. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  29. package/dist/resources/extensions/gsd/gitignore.ts +4 -1
  30. package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
  31. package/dist/resources/extensions/gsd/index.ts +54 -1
  32. package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
  33. package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  35. package/dist/resources/extensions/gsd/preferences.ts +62 -1
  36. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
  37. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  38. package/dist/resources/extensions/gsd/reports.ts +510 -0
  39. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  40. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  41. package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  42. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  43. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  44. package/dist/resources/extensions/gsd/state.ts +30 -0
  45. package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
  46. package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  47. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  48. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  49. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  51. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  52. package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  54. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  55. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  57. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  58. package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  59. package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  60. package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  61. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  62. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  63. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  64. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  65. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  66. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  67. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  68. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  69. package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  70. package/dist/resources/extensions/gsd/types.ts +38 -0
  71. package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
  72. package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
  73. package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
  74. package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  75. package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
  76. package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
  77. package/dist/resources/extensions/shared/format-utils.ts +85 -0
  78. package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  79. package/dist/resources/extensions/subagent/index.ts +46 -1
  80. package/dist/resources/extensions/subagent/isolation.ts +9 -6
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
  83. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  84. package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
  85. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
  87. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
  90. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  91. package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
  92. package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
  93. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  94. package/packages/pi-tui/dist/components/editor.js +1 -1
  95. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  96. package/packages/pi-tui/src/components/editor.ts +3 -1
  97. package/scripts/link-workspace-packages.cjs +22 -6
  98. package/src/resources/extensions/bg-shell/index.ts +19 -2
  99. package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
  100. package/src/resources/extensions/bg-shell/types.ts +21 -1
  101. package/src/resources/extensions/gsd/auto/session.ts +224 -0
  102. package/src/resources/extensions/gsd/auto-budget.ts +32 -0
  103. package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
  104. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  105. package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
  106. package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
  107. package/src/resources/extensions/gsd/auto-observability.ts +74 -0
  108. package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
  109. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  110. package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  111. package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  112. package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  113. package/src/resources/extensions/gsd/auto.ts +977 -1551
  114. package/src/resources/extensions/gsd/commands.ts +3 -3
  115. package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  116. package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
  117. package/src/resources/extensions/gsd/export-html.ts +1001 -0
  118. package/src/resources/extensions/gsd/export.ts +49 -1
  119. package/src/resources/extensions/gsd/git-service.ts +6 -0
  120. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  121. package/src/resources/extensions/gsd/guided-flow.ts +24 -5
  122. package/src/resources/extensions/gsd/index.ts +54 -1
  123. package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
  124. package/src/resources/extensions/gsd/observability-validator.ts +21 -0
  125. package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  126. package/src/resources/extensions/gsd/preferences.ts +62 -1
  127. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
  128. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  129. package/src/resources/extensions/gsd/reports.ts +510 -0
  130. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  131. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  132. package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  133. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  134. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  135. package/src/resources/extensions/gsd/state.ts +30 -0
  136. package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
  137. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  138. package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  139. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  140. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  141. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  142. package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  143. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  144. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  146. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  147. package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  148. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  149. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  150. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  151. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  152. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  153. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  154. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  155. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  156. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  157. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  158. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  159. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  160. package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  161. package/src/resources/extensions/gsd/types.ts +38 -0
  162. package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
  163. package/src/resources/extensions/gsd/verification-gate.ts +567 -0
  164. package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
  165. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  166. package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
  167. package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
  168. package/src/resources/extensions/shared/format-utils.ts +85 -0
  169. package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  170. package/src/resources/extensions/subagent/index.ts +46 -1
  171. package/src/resources/extensions/subagent/isolation.ts +9 -6
@@ -103,6 +103,7 @@ async function main(): Promise<void> {
103
103
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
104
104
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
105
105
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
106
+ writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
106
107
  writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT);
107
108
 
108
109
  // Derive state from files only (no DB)
@@ -166,6 +167,7 @@ async function main(): Promise<void> {
166
167
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
167
168
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
168
169
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
170
+ writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
169
171
 
170
172
  // No DB open — isDbAvailable() is false
171
173
  assertTrue(!isDbAvailable(), 'fallback: DB is not available');
@@ -189,6 +191,7 @@ async function main(): Promise<void> {
189
191
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
190
192
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
191
193
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
194
+ writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
192
195
 
193
196
  // Open DB but insert nothing — empty artifacts table
194
197
  openDatabase(':memory:');
@@ -219,6 +222,7 @@ async function main(): Promise<void> {
219
222
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
220
223
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
221
224
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
225
+ writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
222
226
  writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT);
223
227
 
224
228
  // Open DB but only insert the roadmap — plan and requirements missing from DB
@@ -348,6 +352,7 @@ async function main(): Promise<void> {
348
352
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
349
353
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
350
354
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
355
+ writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
351
356
 
352
357
  openDatabase(':memory:');
353
358
  insertArtifactRow('milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT, {
@@ -45,6 +45,7 @@ function writeContext(base: string, mid: string, frontmatter: string): void {
45
45
  function writeSlicePlan(base: string, mid: string, sid: string, content: string): void {
46
46
  const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
47
47
  mkdirSync(join(dir, 'tasks'), { recursive: true });
48
+ writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n");
48
49
  writeFileSync(join(dir, `${sid}-PLAN.md`), content);
49
50
  }
50
51
 
@@ -45,6 +45,7 @@ function writeRoadmap(base: string, mid: string, content: string): void {
45
45
  function writePlan(base: string, mid: string, sid: string, content: string): void {
46
46
  const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
47
47
  mkdirSync(join(dir, 'tasks'), { recursive: true });
48
+ writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n");
48
49
  writeFileSync(join(dir, `${sid}-PLAN.md`), content);
49
50
  }
50
51
 
@@ -22,8 +22,17 @@ function writeRoadmap(base: string, mid: string, content: string): void {
22
22
 
23
23
  function writePlan(base: string, mid: string, sid: string, content: string): void {
24
24
  const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
25
- mkdirSync(join(dir, 'tasks'), { recursive: true });
25
+ const tasksDir = join(dir, 'tasks');
26
+ mkdirSync(tasksDir, { recursive: true });
26
27
  writeFileSync(join(dir, `${sid}-PLAN.md`), content);
28
+ // Create stub task plan files for any tasks in the plan content (#909)
29
+ // so deriveState doesn't fall back to planning phase.
30
+ const taskMatches = content.matchAll(/\*\*(T\d+):/g);
31
+ for (const m of taskMatches) {
32
+ const tid = m[1];
33
+ const planPath = join(tasksDir, `${tid}-PLAN.md`);
34
+ writeFileSync(planPath, `# ${tid} Plan\n\nTask plan stub for testing.\n`);
35
+ }
27
36
  }
28
37
 
29
38
  function writeContinue(base: string, mid: string, sid: string, content: string): void {
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Regression test for issue #909.
3
+ *
4
+ * When S##-PLAN.md exists (causing deriveState → phase:'executing') but the
5
+ * individual task plan files (tasks/T01-PLAN.md, etc.) are absent, the dispatch
6
+ * table must recover by re-running plan-slice — NOT hard-stop.
7
+ *
8
+ * Prior behaviour: action:"stop" → infinite loop on restart.
9
+ * Fixed behaviour: action:"dispatch" unitType:"plan-slice".
10
+ */
11
+
12
+ import test from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { tmpdir } from "node:os";
17
+ import { resolveDispatch } from "../auto-dispatch.ts";
18
+ import type { DispatchContext } from "../auto-dispatch.ts";
19
+ import type { GSDState } from "../types.ts";
20
+
21
+ function makeState(overrides: Partial<GSDState> = {}): GSDState {
22
+ return {
23
+ activeMilestone: { id: "M002", title: "Test Milestone" },
24
+ activeSlice: { id: "S03", title: "Third Slice" },
25
+ activeTask: { id: "T01", title: "First Task" },
26
+ phase: "executing",
27
+ recentDecisions: [],
28
+ blockers: [],
29
+ nextAction: "",
30
+ registry: [],
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function makeContext(basePath: string, stateOverrides?: Partial<GSDState>): DispatchContext {
36
+ return {
37
+ basePath,
38
+ mid: "M002",
39
+ midTitle: "Test Milestone",
40
+ state: makeState(stateOverrides),
41
+ prefs: undefined,
42
+ };
43
+ }
44
+
45
+ // ─── Scaffold helpers ──────────────────────────────────────────────────────
46
+
47
+ function scaffoldSlicePlan(basePath: string, mid: string, sid: string): void {
48
+ const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid);
49
+ mkdirSync(dir, { recursive: true });
50
+ writeFileSync(join(dir, `${sid}-PLAN.md`), [
51
+ `# ${sid}: Third Slice`,
52
+ "",
53
+ "## Tasks",
54
+ "- [ ] **T01: Do something** `est:1h`",
55
+ "- [ ] **T02: Do another thing** `est:30m`",
56
+ "",
57
+ ].join("\n"));
58
+ }
59
+
60
+ function scaffoldTaskPlan(basePath: string, mid: string, sid: string, tid: string): void {
61
+ const dir = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks");
62
+ mkdirSync(dir, { recursive: true });
63
+ writeFileSync(join(dir, `${tid}-PLAN.md`), [
64
+ `# ${tid}: Do something`,
65
+ "",
66
+ "## Steps",
67
+ "- [ ] Step 1",
68
+ "",
69
+ ].join("\n"));
70
+ }
71
+
72
+ // ─── Tests ─────────────────────────────────────────────────────────────────
73
+
74
+ test("dispatch: missing task plan triggers plan-slice (not stop) — issue #909", async () => {
75
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-909-"));
76
+ try {
77
+ // Slice plan exists with tasks, but tasks/ directory is empty
78
+ scaffoldSlicePlan(tmp, "M002", "S03");
79
+
80
+ const ctx = makeContext(tmp);
81
+ const result = await resolveDispatch(ctx);
82
+
83
+ assert.equal(result.action, "dispatch", "should dispatch, not stop");
84
+ assert.ok(result.action === "dispatch" && result.unitType === "plan-slice",
85
+ `unitType should be plan-slice, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
86
+ assert.ok(result.action === "dispatch" && result.unitId === "M002/S03",
87
+ `unitId should be M002/S03, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
88
+ } finally {
89
+ rmSync(tmp, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ test("dispatch: present task plan proceeds to execute-task normally", async () => {
94
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-909-ok-"));
95
+ try {
96
+ scaffoldSlicePlan(tmp, "M002", "S03");
97
+ scaffoldTaskPlan(tmp, "M002", "S03", "T01");
98
+
99
+ const ctx = makeContext(tmp);
100
+ const result = await resolveDispatch(ctx);
101
+
102
+ assert.equal(result.action, "dispatch");
103
+ assert.ok(result.action === "dispatch" && result.unitType === "execute-task",
104
+ `unitType should be execute-task, got: ${result.action === "dispatch" ? result.unitType : "(stop)"}`);
105
+ assert.ok(result.action === "dispatch" && result.unitId === "M002/S03/T01",
106
+ `unitId should be M002/S03/T01, got: ${result.action === "dispatch" ? result.unitId : "(stop)"}`);
107
+ } finally {
108
+ rmSync(tmp, { recursive: true, force: true });
109
+ }
110
+ });
111
+
112
+ test("dispatch: plan-slice recovery loop — second call after plan-slice still recovers cleanly", async () => {
113
+ // Simulate: plan-slice ran but T01-PLAN.md is still missing (e.g. agent crashed mid-write).
114
+ // Dispatch should still re-dispatch plan-slice, not hard-stop.
115
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-909-loop-"));
116
+ try {
117
+ scaffoldSlicePlan(tmp, "M002", "S03");
118
+
119
+ const ctx = makeContext(tmp);
120
+ const r1 = await resolveDispatch(ctx);
121
+ assert.equal(r1.action, "dispatch");
122
+ assert.ok(r1.action === "dispatch" && r1.unitType === "plan-slice");
123
+
124
+ // Still no task plan written — dispatch again
125
+ const r2 = await resolveDispatch(ctx);
126
+ assert.equal(r2.action, "dispatch");
127
+ assert.ok(r2.action === "dispatch" && r2.unitType === "plan-slice",
128
+ "should keep dispatching plan-slice until task plans appear");
129
+ } finally {
130
+ rmSync(tmp, { recursive: true, force: true });
131
+ }
132
+ });
@@ -193,6 +193,20 @@ async function main(): Promise<void> {
193
193
  assertEq(result.issues.length, 0, "no issues on clean state");
194
194
  }
195
195
 
196
+ console.log("\n=== health gate: missing STATE.md does NOT block dispatch (#889) ===");
197
+ {
198
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
199
+ cleanups.push(dir);
200
+ // Create milestones dir but no STATE.md — mimics fresh worktree
201
+ mkdirSync(join(dir, ".gsd", "milestones", "M001"), { recursive: true });
202
+ writeFileSync(join(dir, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# Roadmap\n");
203
+
204
+ const result = await preDispatchHealthGate(dir);
205
+ assertTrue(result.proceed, "gate must NOT block when STATE.md is missing (deadlock #889)");
206
+ assertEq(result.issues.length, 0, "missing STATE.md is not a blocking issue");
207
+ assertTrue(result.fixesApplied.some((f: string) => f.includes("STATE.md")), "reports STATE.md status as info");
208
+ }
209
+
196
210
  console.log("\n=== health gate: stale crash lock auto-cleared ===");
197
211
  {
198
212
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
@@ -42,6 +42,7 @@ function writeRoadmap(base: string, mid: string, content: string): void {
42
42
  function writePlan(base: string, mid: string, sid: string, content: string): void {
43
43
  const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
44
44
  mkdirSync(join(dir, 'tasks'), { recursive: true });
45
+ writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n");
45
46
  writeFileSync(join(dir, `${sid}-PLAN.md`), content);
46
47
  }
47
48
 
@@ -134,7 +134,7 @@ test("auto.ts milestone transition block contains worktree lifecycle", () => {
134
134
  "auto.ts should contain the worktree lifecycle comment marker",
135
135
  );
136
136
  assert.ok(
137
- autoSrc.includes("mergeMilestoneToMain") && autoSrc.includes("mid !== currentMilestoneId"),
137
+ autoSrc.includes("mergeMilestoneToMain") && autoSrc.includes("mid !== s.currentMilestoneId"),
138
138
  "auto.ts should call mergeMilestoneToMain during milestone transition",
139
139
  );
140
140
  assert.ok(
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Unit tests for the nativeHasChanges() fallback cache (10s TTL).
3
+ *
4
+ * Verifies:
5
+ * 1. Cached result is returned within the TTL window
6
+ * 2. Cache invalidates after TTL expires
7
+ * 3. Cache invalidates when basePath changes
8
+ */
9
+
10
+ import test from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { nativeHasChanges, _resetHasChangesCache } from '../native-git-bridge.ts';
13
+
14
+ // We can't easily mock gitExec or Date.now inside the module, so we test
15
+ // the observable caching behaviour by calling the real function against
16
+ // the current repo (which is a valid git checkout).
17
+
18
+ const REPO_ROOT = process.cwd();
19
+
20
+ test('nativeHasChanges: returns a boolean for the current repo', () => {
21
+ _resetHasChangesCache();
22
+ const result = nativeHasChanges(REPO_ROOT);
23
+ assert.strictEqual(typeof result, 'boolean', 'should return a boolean');
24
+ });
25
+
26
+ test('nativeHasChanges: second call within TTL returns same result (cache hit)', () => {
27
+ _resetHasChangesCache();
28
+ const first = nativeHasChanges(REPO_ROOT);
29
+ const second = nativeHasChanges(REPO_ROOT);
30
+ assert.strictEqual(first, second, 'cached result should match first call');
31
+ });
32
+
33
+ test('nativeHasChanges: different basePath invalidates cache', () => {
34
+ _resetHasChangesCache();
35
+
36
+ // Prime cache with REPO_ROOT
37
+ const first = nativeHasChanges(REPO_ROOT);
38
+
39
+ // Call with a different path — should NOT return the stale cached value
40
+ // (it will compute fresh). We just verify it doesn't throw and returns boolean.
41
+ const other = nativeHasChanges('/tmp');
42
+ assert.strictEqual(typeof other, 'boolean', 'should return boolean for different path');
43
+
44
+ // After switching path, calling with REPO_ROOT again should recompute
45
+ const third = nativeHasChanges(REPO_ROOT);
46
+ assert.strictEqual(typeof third, 'boolean', 'should return boolean after path switch');
47
+ });
48
+
49
+ test('nativeHasChanges: cache expires after TTL', () => {
50
+ _resetHasChangesCache();
51
+
52
+ // Prime the cache
53
+ nativeHasChanges(REPO_ROOT);
54
+
55
+ // Manually expire the cache by resetting it (simulates TTL expiry)
56
+ _resetHasChangesCache();
57
+
58
+ // This call should recompute (not use stale data)
59
+ const result = nativeHasChanges(REPO_ROOT);
60
+ assert.strictEqual(typeof result, 'boolean', 'should recompute after cache reset');
61
+ });
@@ -6,7 +6,7 @@ import assert from "node:assert/strict";
6
6
  // just test that `resolveModelWithFallbacksForUnit` returns the correct format since
7
7
  // the fallback rotation logic itself was verified manually.
8
8
 
9
- import { getNextFallbackModel } from "../preferences.ts";
9
+ import { getNextFallbackModel, isTransientNetworkError } from "../preferences.ts";
10
10
 
11
11
  test("getNextFallbackModel selects next fallback if current is a fallback", () => {
12
12
  const modelConfig = { primary: "model-a", fallbacks: ["model-b", "model-c"] };
@@ -52,3 +52,53 @@ test("getNextFallbackModel returns primary if current model is undefined", () =>
52
52
 
53
53
  assert.equal(nextModelId, "model-a", "should default to primary if current is undefined");
54
54
  });
55
+
56
+ // ── isTransientNetworkError tests ────────────────────────────────────────────
57
+
58
+ test("isTransientNetworkError detects ECONNRESET", () => {
59
+ assert.ok(isTransientNetworkError("fetch failed: ECONNRESET"));
60
+ });
61
+
62
+ test("isTransientNetworkError detects ETIMEDOUT", () => {
63
+ assert.ok(isTransientNetworkError("ETIMEDOUT: request timed out"));
64
+ });
65
+
66
+ test("isTransientNetworkError detects generic network error", () => {
67
+ assert.ok(isTransientNetworkError("network error"));
68
+ });
69
+
70
+ test("isTransientNetworkError detects socket hang up", () => {
71
+ assert.ok(isTransientNetworkError("socket hang up"));
72
+ });
73
+
74
+ test("isTransientNetworkError detects fetch failed", () => {
75
+ assert.ok(isTransientNetworkError("fetch failed"));
76
+ });
77
+
78
+ test("isTransientNetworkError detects connection reset", () => {
79
+ assert.ok(isTransientNetworkError("connection was reset by peer"));
80
+ });
81
+
82
+ test("isTransientNetworkError detects DNS errors", () => {
83
+ assert.ok(isTransientNetworkError("dns resolution failed"));
84
+ });
85
+
86
+ test("isTransientNetworkError rejects auth errors", () => {
87
+ assert.ok(!isTransientNetworkError("unauthorized: invalid API key"));
88
+ });
89
+
90
+ test("isTransientNetworkError rejects quota errors", () => {
91
+ assert.ok(!isTransientNetworkError("quota exceeded"));
92
+ });
93
+
94
+ test("isTransientNetworkError rejects billing errors", () => {
95
+ assert.ok(!isTransientNetworkError("billing issue: network payment required"));
96
+ });
97
+
98
+ test("isTransientNetworkError rejects empty string", () => {
99
+ assert.ok(!isTransientNetworkError(""));
100
+ });
101
+
102
+ test("isTransientNetworkError rejects non-network errors", () => {
103
+ assert.ok(!isTransientNetworkError("model not found"));
104
+ });