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.
- package/README.md +43 -6
- package/dist/cli.js +4 -2
- package/dist/headless.d.ts +3 -0
- package/dist/headless.js +136 -8
- package/dist/help-text.js +3 -0
- package/dist/loader.js +33 -4
- package/dist/resources/extensions/bg-shell/index.ts +19 -2
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/gsd/auto/session.ts +224 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/dist/resources/extensions/gsd/auto.ts +977 -1551
- package/dist/resources/extensions/gsd/commands.ts +3 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/export-html.ts +1001 -0
- package/dist/resources/extensions/gsd/export.ts +49 -1
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
- package/dist/resources/extensions/gsd/index.ts +54 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/preferences.ts +62 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/reports.ts +510 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/types.ts +38 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/dist/resources/extensions/shared/format-utils.ts +85 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/subagent/index.ts +46 -1
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/scripts/link-workspace-packages.cjs +22 -6
- package/src/resources/extensions/bg-shell/index.ts +19 -2
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/gsd/auto/session.ts +224 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/src/resources/extensions/gsd/auto.ts +977 -1551
- package/src/resources/extensions/gsd/commands.ts +3 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/export-html.ts +1001 -0
- package/src/resources/extensions/gsd/export.ts +49 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -5
- package/src/resources/extensions/gsd/index.ts +54 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/src/resources/extensions/gsd/observability-validator.ts +21 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/preferences.ts +62 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/reports.ts +510 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/types.ts +38 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/src/resources/extensions/gsd/verification-gate.ts +567 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/src/resources/extensions/shared/format-utils.ts +85 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/subagent/index.ts +46 -1
- 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
|
-
|
|
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
|
+
});
|