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
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parallel-merge.test.ts — Tests for parallel merge reconciliation (G5).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - determineMergeOrder: sequential vs by-completion ordering, filtering
|
|
6
|
+
* - formatMergeResults: success, conflict, empty, mixed output formatting
|
|
7
|
+
* - mergeCompletedMilestone: clean merge with session cleanup, missing roadmap,
|
|
8
|
+
* conflict detection with structured error
|
|
9
|
+
* - mergeAllCompleted: stop-on-first-conflict, sequential execution order
|
|
10
|
+
*
|
|
11
|
+
* Pure-function tests need no git. Integration tests use temp repos with real
|
|
12
|
+
* git operations (same pattern as auto-worktree-milestone-merge.test.ts).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import {
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
existsSync,
|
|
23
|
+
realpathSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { execSync } from "node:child_process";
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
determineMergeOrder,
|
|
31
|
+
mergeCompletedMilestone,
|
|
32
|
+
mergeAllCompleted,
|
|
33
|
+
formatMergeResults,
|
|
34
|
+
type MergeResult,
|
|
35
|
+
} from "../parallel-merge.ts";
|
|
36
|
+
import type { WorkerInfo } from "../parallel-orchestrator.ts";
|
|
37
|
+
import {
|
|
38
|
+
writeSessionStatus,
|
|
39
|
+
readSessionStatus,
|
|
40
|
+
} from "../session-status-io.ts";
|
|
41
|
+
|
|
42
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function run(cmd: string, cwd: string): string {
|
|
45
|
+
return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createTempRepo(): string {
|
|
49
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "parallel-merge-test-")));
|
|
50
|
+
run("git init -b main", dir);
|
|
51
|
+
run("git config user.email test@test.com", dir);
|
|
52
|
+
run("git config user.name Test", dir);
|
|
53
|
+
writeFileSync(join(dir, "README.md"), "# test\n");
|
|
54
|
+
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
55
|
+
writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
|
|
56
|
+
run("git add .", dir);
|
|
57
|
+
run("git commit -m init", dir);
|
|
58
|
+
return dir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeWorker(overrides: Partial<WorkerInfo> = {}): WorkerInfo {
|
|
62
|
+
return {
|
|
63
|
+
milestoneId: "M001",
|
|
64
|
+
title: "Test milestone",
|
|
65
|
+
pid: process.pid,
|
|
66
|
+
process: null,
|
|
67
|
+
worktreePath: "/tmp/test",
|
|
68
|
+
startedAt: Date.now(),
|
|
69
|
+
state: "stopped",
|
|
70
|
+
completedUnits: 3,
|
|
71
|
+
cost: 1.5,
|
|
72
|
+
...overrides,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function cleanup(dir: string): void {
|
|
77
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Set up a milestone roadmap file in .gsd/milestones/<MID>/ */
|
|
81
|
+
function setupRoadmap(repo: string, mid: string, title: string, slices: string[]): void {
|
|
82
|
+
const dir = join(repo, ".gsd", "milestones", mid);
|
|
83
|
+
mkdirSync(dir, { recursive: true });
|
|
84
|
+
const sliceLines = slices.map(s => `- [x] **${s}**`).join("\n");
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(dir, `${mid}-ROADMAP.md`),
|
|
87
|
+
`# ${mid}: ${title}\n\n## Slices\n${sliceLines}\n`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Create a milestone branch with file changes, then return to main. */
|
|
92
|
+
function createMilestoneBranch(
|
|
93
|
+
repo: string,
|
|
94
|
+
mid: string,
|
|
95
|
+
files: Array<{ name: string; content: string }>,
|
|
96
|
+
): void {
|
|
97
|
+
run(`git checkout -b milestone/${mid}`, repo);
|
|
98
|
+
for (const f of files) {
|
|
99
|
+
const dir = join(repo, ...f.name.split("/").slice(0, -1));
|
|
100
|
+
if (dir !== repo) mkdirSync(dir, { recursive: true });
|
|
101
|
+
writeFileSync(join(repo, f.name), f.content);
|
|
102
|
+
}
|
|
103
|
+
run("git add .", repo);
|
|
104
|
+
run(`git commit -m "feat(${mid}): add files"`, repo);
|
|
105
|
+
run("git checkout main", repo);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
109
|
+
// determineMergeOrder — Pure function tests
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
111
|
+
|
|
112
|
+
test("determineMergeOrder — sequential sorts by milestone ID", () => {
|
|
113
|
+
const workers = [
|
|
114
|
+
makeWorker({ milestoneId: "M003", startedAt: 100 }),
|
|
115
|
+
makeWorker({ milestoneId: "M001", startedAt: 300 }),
|
|
116
|
+
makeWorker({ milestoneId: "M002", startedAt: 200 }),
|
|
117
|
+
];
|
|
118
|
+
const order = determineMergeOrder(workers, "sequential");
|
|
119
|
+
assert.deepEqual(order, ["M001", "M002", "M003"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("determineMergeOrder — by-completion sorts by startedAt (earliest first)", () => {
|
|
123
|
+
const workers = [
|
|
124
|
+
makeWorker({ milestoneId: "M003", startedAt: 100 }),
|
|
125
|
+
makeWorker({ milestoneId: "M001", startedAt: 300 }),
|
|
126
|
+
makeWorker({ milestoneId: "M002", startedAt: 200 }),
|
|
127
|
+
];
|
|
128
|
+
const order = determineMergeOrder(workers, "by-completion");
|
|
129
|
+
assert.deepEqual(order, ["M003", "M002", "M001"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("determineMergeOrder — only includes stopped workers with completedUnits > 0", () => {
|
|
133
|
+
const workers = [
|
|
134
|
+
makeWorker({ milestoneId: "M001", state: "stopped", completedUnits: 3 }),
|
|
135
|
+
makeWorker({ milestoneId: "M002", state: "running", completedUnits: 2 }),
|
|
136
|
+
makeWorker({ milestoneId: "M003", state: "stopped", completedUnits: 0 }),
|
|
137
|
+
makeWorker({ milestoneId: "M004", state: "error", completedUnits: 5 }),
|
|
138
|
+
makeWorker({ milestoneId: "M005", state: "paused", completedUnits: 1 }),
|
|
139
|
+
];
|
|
140
|
+
const order = determineMergeOrder(workers, "sequential");
|
|
141
|
+
assert.deepEqual(order, ["M001"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("determineMergeOrder — empty workers returns empty array", () => {
|
|
145
|
+
assert.deepEqual(determineMergeOrder([], "sequential"), []);
|
|
146
|
+
assert.deepEqual(determineMergeOrder([], "by-completion"), []);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("determineMergeOrder — defaults to sequential when order not specified", () => {
|
|
150
|
+
const workers = [
|
|
151
|
+
makeWorker({ milestoneId: "M002" }),
|
|
152
|
+
makeWorker({ milestoneId: "M001" }),
|
|
153
|
+
];
|
|
154
|
+
const order = determineMergeOrder(workers);
|
|
155
|
+
assert.deepEqual(order, ["M001", "M002"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
// formatMergeResults — Pure function tests
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
test("formatMergeResults — empty results", () => {
|
|
163
|
+
const output = formatMergeResults([]);
|
|
164
|
+
assert.ok(output.includes("No completed milestones"));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("formatMergeResults — successful merge", () => {
|
|
168
|
+
const results: MergeResult[] = [
|
|
169
|
+
{ milestoneId: "M001", success: true, commitMessage: "feat(M001): Auth", pushed: true },
|
|
170
|
+
];
|
|
171
|
+
const output = formatMergeResults(results);
|
|
172
|
+
assert.ok(output.includes("M001"));
|
|
173
|
+
assert.ok(output.includes("merged successfully"));
|
|
174
|
+
assert.ok(output.includes("(pushed)"));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("formatMergeResults — successful merge without push", () => {
|
|
178
|
+
const results: MergeResult[] = [
|
|
179
|
+
{ milestoneId: "M001", success: true, commitMessage: "feat(M001): Auth", pushed: false },
|
|
180
|
+
];
|
|
181
|
+
const output = formatMergeResults(results);
|
|
182
|
+
assert.ok(output.includes("merged successfully"));
|
|
183
|
+
assert.ok(!output.includes("(pushed)"));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("formatMergeResults — conflict with file list", () => {
|
|
187
|
+
const results: MergeResult[] = [
|
|
188
|
+
{
|
|
189
|
+
milestoneId: "M002",
|
|
190
|
+
success: false,
|
|
191
|
+
error: "Merge conflict: 2 conflicting file(s)",
|
|
192
|
+
conflictFiles: ["src/app.ts", "src/main.ts"],
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
const output = formatMergeResults(results);
|
|
196
|
+
assert.ok(output.includes("CONFLICT"));
|
|
197
|
+
assert.ok(output.includes("src/app.ts"));
|
|
198
|
+
assert.ok(output.includes("src/main.ts"));
|
|
199
|
+
assert.ok(output.includes("Resolve conflicts manually"));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("formatMergeResults — generic failure without conflict files", () => {
|
|
203
|
+
const results: MergeResult[] = [
|
|
204
|
+
{ milestoneId: "M003", success: false, error: "No roadmap found for M003" },
|
|
205
|
+
];
|
|
206
|
+
const output = formatMergeResults(results);
|
|
207
|
+
assert.ok(output.includes("M003"));
|
|
208
|
+
assert.ok(output.includes("failed"));
|
|
209
|
+
assert.ok(output.includes("No roadmap found"));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("formatMergeResults — mixed results", () => {
|
|
213
|
+
const results: MergeResult[] = [
|
|
214
|
+
{ milestoneId: "M001", success: true, commitMessage: "feat(M001): OK", pushed: false },
|
|
215
|
+
{ milestoneId: "M002", success: false, error: "conflict", conflictFiles: ["a.ts"] },
|
|
216
|
+
];
|
|
217
|
+
const output = formatMergeResults(results);
|
|
218
|
+
assert.ok(output.includes("M001"));
|
|
219
|
+
assert.ok(output.includes("merged successfully"));
|
|
220
|
+
assert.ok(output.includes("M002"));
|
|
221
|
+
assert.ok(output.includes("CONFLICT"));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
225
|
+
// mergeCompletedMilestone — Integration tests (real git)
|
|
226
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
227
|
+
|
|
228
|
+
test("mergeCompletedMilestone — missing roadmap returns error result", async () => {
|
|
229
|
+
const base = join(tmpdir(), `parallel-merge-noroadmap-${Date.now()}`);
|
|
230
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
231
|
+
try {
|
|
232
|
+
const result = await mergeCompletedMilestone(base, "M999");
|
|
233
|
+
assert.equal(result.success, false);
|
|
234
|
+
assert.ok(result.error?.includes("No roadmap found") || result.error?.includes("Could not read"));
|
|
235
|
+
assert.equal(result.milestoneId, "M999");
|
|
236
|
+
} finally {
|
|
237
|
+
cleanup(base);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("mergeCompletedMilestone — clean merge, session status cleaned up", async () => {
|
|
242
|
+
const savedCwd = process.cwd();
|
|
243
|
+
const repo = createTempRepo();
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Create milestone branch with a new file
|
|
247
|
+
createMilestoneBranch(repo, "M010", [
|
|
248
|
+
{ name: "auth.ts", content: "export const auth = true;\n" },
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
// Set up roadmap
|
|
252
|
+
setupRoadmap(repo, "M010", "Auth System", ["S01: JWT module"]);
|
|
253
|
+
|
|
254
|
+
// Write session status to verify cleanup
|
|
255
|
+
writeSessionStatus(repo, {
|
|
256
|
+
milestoneId: "M010",
|
|
257
|
+
pid: process.pid,
|
|
258
|
+
state: "stopped",
|
|
259
|
+
currentUnit: null,
|
|
260
|
+
completedUnits: 3,
|
|
261
|
+
cost: 1.5,
|
|
262
|
+
lastHeartbeat: Date.now(),
|
|
263
|
+
startedAt: Date.now() - 60000,
|
|
264
|
+
worktreePath: join(repo, ".gsd", "worktrees", "M010"),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Verify session status exists before merge
|
|
268
|
+
const statusBefore = readSessionStatus(repo, "M010");
|
|
269
|
+
assert.ok(statusBefore, "session status should exist before merge");
|
|
270
|
+
|
|
271
|
+
// Merge from project root
|
|
272
|
+
process.chdir(repo);
|
|
273
|
+
const result = await mergeCompletedMilestone(repo, "M010");
|
|
274
|
+
|
|
275
|
+
assert.equal(result.success, true, `merge should succeed: ${result.error}`);
|
|
276
|
+
assert.ok(result.commitMessage, "should have commit message");
|
|
277
|
+
assert.equal(result.milestoneId, "M010");
|
|
278
|
+
|
|
279
|
+
// Verify file merged to main
|
|
280
|
+
assert.ok(existsSync(join(repo, "auth.ts")), "auth.ts should be on main");
|
|
281
|
+
|
|
282
|
+
// Verify commit on main
|
|
283
|
+
const log = run("git log --oneline main", repo);
|
|
284
|
+
assert.ok(log.includes("M010"), "commit message should reference M010");
|
|
285
|
+
|
|
286
|
+
// Verify session status cleaned up
|
|
287
|
+
const statusAfter = readSessionStatus(repo, "M010");
|
|
288
|
+
assert.equal(statusAfter, null, "session status should be cleaned up after merge");
|
|
289
|
+
|
|
290
|
+
// Verify milestone branch deleted
|
|
291
|
+
const branches = run("git branch", repo);
|
|
292
|
+
assert.ok(!branches.includes("milestone/M010"), "milestone branch should be deleted");
|
|
293
|
+
} finally {
|
|
294
|
+
process.chdir(savedCwd);
|
|
295
|
+
cleanup(repo);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("mergeCompletedMilestone — conflict returns structured error with file list", async () => {
|
|
300
|
+
const savedCwd = process.cwd();
|
|
301
|
+
const repo = createTempRepo();
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
// Create milestone branch that modifies README.md
|
|
305
|
+
run("git checkout -b milestone/M020", repo);
|
|
306
|
+
writeFileSync(join(repo, "README.md"), "# M020 version\n");
|
|
307
|
+
run("git add .", repo);
|
|
308
|
+
run('git commit -m "M020 changes README"', repo);
|
|
309
|
+
run("git checkout main", repo);
|
|
310
|
+
|
|
311
|
+
// Modify README.md on main to create conflict
|
|
312
|
+
writeFileSync(join(repo, "README.md"), "# main version (diverged)\n");
|
|
313
|
+
run("git add .", repo);
|
|
314
|
+
run('git commit -m "main changes README"', repo);
|
|
315
|
+
|
|
316
|
+
// Set up roadmap
|
|
317
|
+
setupRoadmap(repo, "M020", "Conflict Test", ["S01: Conflict scenario"]);
|
|
318
|
+
|
|
319
|
+
process.chdir(repo);
|
|
320
|
+
const result = await mergeCompletedMilestone(repo, "M020");
|
|
321
|
+
|
|
322
|
+
assert.equal(result.success, false, "merge should fail with conflict");
|
|
323
|
+
assert.equal(result.milestoneId, "M020");
|
|
324
|
+
assert.ok(result.conflictFiles, "should have conflictFiles");
|
|
325
|
+
assert.ok(result.conflictFiles!.length > 0, "should have at least one conflict file");
|
|
326
|
+
assert.ok(result.conflictFiles!.includes("README.md"), "README.md should be in conflicts");
|
|
327
|
+
assert.ok(result.error?.includes("conflict"), "error message should mention conflict");
|
|
328
|
+
} finally {
|
|
329
|
+
process.chdir(savedCwd);
|
|
330
|
+
// Reset git state before cleanup (repo may be in conflicted state)
|
|
331
|
+
try { run("git reset --hard HEAD", repo); } catch { /* */ }
|
|
332
|
+
cleanup(repo);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
337
|
+
// mergeAllCompleted — Integration tests
|
|
338
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
339
|
+
|
|
340
|
+
test("mergeAllCompleted — merges in sequential order", async () => {
|
|
341
|
+
const savedCwd = process.cwd();
|
|
342
|
+
const repo = createTempRepo();
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// M001: adds auth.ts
|
|
346
|
+
createMilestoneBranch(repo, "M001", [
|
|
347
|
+
{ name: "auth.ts", content: "export const auth = true;\n" },
|
|
348
|
+
]);
|
|
349
|
+
// M002: adds dashboard.ts
|
|
350
|
+
createMilestoneBranch(repo, "M002", [
|
|
351
|
+
{ name: "dashboard.ts", content: "export const dash = true;\n" },
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
setupRoadmap(repo, "M001", "Auth", ["S01: Auth module"]);
|
|
355
|
+
setupRoadmap(repo, "M002", "Dashboard", ["S01: Dashboard module"]);
|
|
356
|
+
|
|
357
|
+
const workers = [
|
|
358
|
+
makeWorker({ milestoneId: "M002", startedAt: 100 }),
|
|
359
|
+
makeWorker({ milestoneId: "M001", startedAt: 200 }),
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
process.chdir(repo);
|
|
363
|
+
const results = await mergeAllCompleted(repo, workers, "sequential");
|
|
364
|
+
|
|
365
|
+
// Both should succeed
|
|
366
|
+
assert.equal(results.length, 2, "should have two results");
|
|
367
|
+
assert.equal(results[0]!.milestoneId, "M001", "M001 merged first (sequential)");
|
|
368
|
+
assert.equal(results[0]!.success, true, "M001 should succeed");
|
|
369
|
+
assert.equal(results[1]!.milestoneId, "M002", "M002 merged second");
|
|
370
|
+
assert.equal(results[1]!.success, true, "M002 should succeed");
|
|
371
|
+
|
|
372
|
+
// Both files on main
|
|
373
|
+
assert.ok(existsSync(join(repo, "auth.ts")), "auth.ts on main");
|
|
374
|
+
assert.ok(existsSync(join(repo, "dashboard.ts")), "dashboard.ts on main");
|
|
375
|
+
} finally {
|
|
376
|
+
process.chdir(savedCwd);
|
|
377
|
+
cleanup(repo);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("mergeAllCompleted — stops on first conflict, skips later milestones", async () => {
|
|
382
|
+
const savedCwd = process.cwd();
|
|
383
|
+
const repo = createTempRepo();
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
// M001: modifies README.md (will conflict with main)
|
|
387
|
+
run("git checkout -b milestone/M001", repo);
|
|
388
|
+
writeFileSync(join(repo, "README.md"), "# M001 version\n");
|
|
389
|
+
run("git add .", repo);
|
|
390
|
+
run('git commit -m "M001 changes README"', repo);
|
|
391
|
+
run("git checkout main", repo);
|
|
392
|
+
|
|
393
|
+
// M002: adds a new file (would NOT conflict)
|
|
394
|
+
createMilestoneBranch(repo, "M002", [
|
|
395
|
+
{ name: "feature.ts", content: "export const feature = true;\n" },
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
// Modify README.md on main to create conflict with M001
|
|
399
|
+
writeFileSync(join(repo, "README.md"), "# main diverged version\n");
|
|
400
|
+
run("git add .", repo);
|
|
401
|
+
run('git commit -m "main diverges README"', repo);
|
|
402
|
+
|
|
403
|
+
setupRoadmap(repo, "M001", "Conflict milestone", ["S01: Conflict test"]);
|
|
404
|
+
setupRoadmap(repo, "M002", "Clean milestone", ["S01: Clean test"]);
|
|
405
|
+
|
|
406
|
+
const workers = [
|
|
407
|
+
makeWorker({ milestoneId: "M001" }),
|
|
408
|
+
makeWorker({ milestoneId: "M002" }),
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
process.chdir(repo);
|
|
412
|
+
const results = await mergeAllCompleted(repo, workers, "sequential");
|
|
413
|
+
|
|
414
|
+
// Only M001 attempted (conflict stops the queue)
|
|
415
|
+
assert.equal(results.length, 1, "should only have one result — stopped after conflict");
|
|
416
|
+
assert.equal(results[0]!.milestoneId, "M001");
|
|
417
|
+
assert.equal(results[0]!.success, false, "M001 should fail");
|
|
418
|
+
assert.ok(results[0]!.conflictFiles && results[0]!.conflictFiles.length > 0, "should have conflict files");
|
|
419
|
+
|
|
420
|
+
// M002 was NOT attempted
|
|
421
|
+
assert.ok(!results.some(r => r.milestoneId === "M002"), "M002 should not be attempted");
|
|
422
|
+
|
|
423
|
+
// feature.ts should NOT be on main (M002 never merged)
|
|
424
|
+
assert.ok(!existsSync(join(repo, "feature.ts")), "feature.ts should not be on main");
|
|
425
|
+
} finally {
|
|
426
|
+
process.chdir(savedCwd);
|
|
427
|
+
try { run("git reset --hard HEAD", repo); } catch { /* */ }
|
|
428
|
+
cleanup(repo);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("mergeAllCompleted — by-completion order respects startedAt", async () => {
|
|
433
|
+
const savedCwd = process.cwd();
|
|
434
|
+
const repo = createTempRepo();
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
// M001: adds auth.ts (started later)
|
|
438
|
+
createMilestoneBranch(repo, "M001", [
|
|
439
|
+
{ name: "auth.ts", content: "export const auth = true;\n" },
|
|
440
|
+
]);
|
|
441
|
+
// M002: adds feature.ts (started earlier)
|
|
442
|
+
createMilestoneBranch(repo, "M002", [
|
|
443
|
+
{ name: "feature.ts", content: "export const feature = true;\n" },
|
|
444
|
+
]);
|
|
445
|
+
|
|
446
|
+
setupRoadmap(repo, "M001", "Auth", ["S01: Auth module"]);
|
|
447
|
+
setupRoadmap(repo, "M002", "Feature", ["S01: Feature module"]);
|
|
448
|
+
|
|
449
|
+
const workers = [
|
|
450
|
+
makeWorker({ milestoneId: "M001", startedAt: 2000 }),
|
|
451
|
+
makeWorker({ milestoneId: "M002", startedAt: 1000 }),
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
process.chdir(repo);
|
|
455
|
+
const results = await mergeAllCompleted(repo, workers, "by-completion");
|
|
456
|
+
|
|
457
|
+
// M002 should be merged first (earlier startedAt)
|
|
458
|
+
assert.equal(results.length, 2);
|
|
459
|
+
assert.equal(results[0]!.milestoneId, "M002", "M002 merged first (earlier startedAt)");
|
|
460
|
+
assert.equal(results[1]!.milestoneId, "M001", "M001 merged second");
|
|
461
|
+
} finally {
|
|
462
|
+
process.chdir(savedCwd);
|
|
463
|
+
cleanup(repo);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
getWorkerStatuses,
|
|
36
36
|
startParallel,
|
|
37
37
|
stopParallel,
|
|
38
|
+
shutdownParallel,
|
|
38
39
|
pauseWorker,
|
|
39
40
|
resumeWorker,
|
|
40
41
|
getAggregateCost,
|
|
@@ -301,7 +302,9 @@ describe("parallel-orchestrator: lifecycle", () => {
|
|
|
301
302
|
const status = readSessionStatus(base, "M001");
|
|
302
303
|
assert.ok(status);
|
|
303
304
|
assert.equal(status.milestoneId, "M001");
|
|
304
|
-
|
|
305
|
+
// State is "running" if spawn succeeds, "error" if binary not found (CI)
|
|
306
|
+
assert.ok(status.state === "running" || status.state === "error",
|
|
307
|
+
`expected running or error, got ${status.state}`);
|
|
305
308
|
});
|
|
306
309
|
|
|
307
310
|
it("stopParallel stops all workers", async () => {
|
|
@@ -319,24 +322,50 @@ describe("parallel-orchestrator: lifecycle", () => {
|
|
|
319
322
|
const m1 = workers.find(w => w.milestoneId === "M001");
|
|
320
323
|
const m2 = workers.find(w => w.milestoneId === "M002");
|
|
321
324
|
assert.equal(m1?.state, "stopped");
|
|
322
|
-
|
|
325
|
+
// M002 is "running" if spawn succeeded, "error" if binary not found (CI)
|
|
326
|
+
assert.ok(m2?.state === "running" || m2?.state === "error",
|
|
327
|
+
`expected running or error, got ${m2?.state}`);
|
|
323
328
|
assert.equal(isParallelActive(), true);
|
|
324
329
|
});
|
|
325
330
|
|
|
326
331
|
it("pauseWorker and resumeWorker toggle worker state", async () => {
|
|
327
332
|
await startParallel(base, ["M001"], undefined);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
333
|
+
const initial = getWorkerStatuses()[0].state;
|
|
334
|
+
// Only test pause/resume if worker is in a pausable state
|
|
335
|
+
if (initial === "running") {
|
|
336
|
+
pauseWorker(base, "M001");
|
|
337
|
+
assert.equal(getWorkerStatuses()[0].state, "paused");
|
|
338
|
+
resumeWorker(base, "M001");
|
|
339
|
+
assert.equal(getWorkerStatuses()[0].state, "running");
|
|
340
|
+
} else {
|
|
341
|
+
// Spawn failed (CI) — pause/resume are no-ops on error state
|
|
342
|
+
pauseWorker(base, "M001");
|
|
343
|
+
assert.equal(getWorkerStatuses()[0].state, initial);
|
|
344
|
+
}
|
|
332
345
|
});
|
|
333
346
|
|
|
334
347
|
it("pauseWorker sends pause signal", async () => {
|
|
335
348
|
await startParallel(base, ["M001"], undefined);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
349
|
+
const w = getWorkerStatuses()[0];
|
|
350
|
+
if (w.state === "running") {
|
|
351
|
+
pauseWorker(base, "M001");
|
|
352
|
+
const signal = consumeSignal(base, "M001");
|
|
353
|
+
assert.ok(signal);
|
|
354
|
+
assert.equal(signal.signal, "pause");
|
|
355
|
+
} else {
|
|
356
|
+
// Spawn failed — pauseWorker is a no-op, signal not written
|
|
357
|
+
pauseWorker(base, "M001");
|
|
358
|
+
const signal = consumeSignal(base, "M001");
|
|
359
|
+
assert.equal(signal, null);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("shutdownParallel deactivates the orchestrator state", async () => {
|
|
364
|
+
await startParallel(base, ["M001"], undefined);
|
|
365
|
+
assert.equal(isParallelActive(), true);
|
|
366
|
+
await shutdownParallel(base);
|
|
367
|
+
assert.equal(isParallelActive(), false);
|
|
368
|
+
assert.equal(getOrchestratorState(), null);
|
|
340
369
|
});
|
|
341
370
|
});
|
|
342
371
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createTestContext } from './test-helpers.ts';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const worktreePromptsDir = join(__dirname, "..", "prompts");
|
|
8
|
+
|
|
9
|
+
const { assertTrue, report } = createTestContext();
|
|
10
|
+
|
|
11
|
+
function loadPromptFromWorktree(name: string, vars: Record<string, string> = {}): string {
|
|
12
|
+
const path = join(worktreePromptsDir, `${name}.md`);
|
|
13
|
+
let content = readFileSync(path, "utf-8");
|
|
14
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
15
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
16
|
+
}
|
|
17
|
+
return content.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BASE_VARS = {
|
|
21
|
+
workingDirectory: "/tmp/test-project",
|
|
22
|
+
milestoneId: "M001",
|
|
23
|
+
sliceId: "S01",
|
|
24
|
+
sliceTitle: "Test Slice",
|
|
25
|
+
slicePath: ".gsd/milestones/M001/slices/S01",
|
|
26
|
+
roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
|
|
27
|
+
researchPath: ".gsd/milestones/M001/slices/S01/S01-RESEARCH.md",
|
|
28
|
+
outputPath: "/tmp/test-project/.gsd/milestones/M001/slices/S01/S01-PLAN.md",
|
|
29
|
+
inlinedContext: "--- test inlined context ---",
|
|
30
|
+
dependencySummaries: "",
|
|
31
|
+
executorContextConstraints: "",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function main(): Promise<void> {
|
|
35
|
+
|
|
36
|
+
// ─── commit_docs=true (default): commit step is present ─────────────────
|
|
37
|
+
console.log("\n=== plan-slice prompt: commit_docs default (true) ===");
|
|
38
|
+
{
|
|
39
|
+
const commitInstruction = `Commit: \`docs(S01): add slice plan\``;
|
|
40
|
+
const result = loadPromptFromWorktree("plan-slice", { ...BASE_VARS, commitInstruction });
|
|
41
|
+
|
|
42
|
+
assertTrue(result.includes("docs(S01): add slice plan"), "commit step present when commit_docs is not false");
|
|
43
|
+
assertTrue(result.includes("Update `.gsd/STATE.md`"), "STATE.md update step present");
|
|
44
|
+
assertTrue(!result.includes("{{commitInstruction}}"), "no unresolved placeholder");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── commit_docs=false: no commit step, only STATE.md update ────────────
|
|
48
|
+
console.log("\n=== plan-slice prompt: commit_docs=false ===");
|
|
49
|
+
{
|
|
50
|
+
const commitInstruction = "Do not commit — planning docs are not tracked in git for this project.";
|
|
51
|
+
const result = loadPromptFromWorktree("plan-slice", { ...BASE_VARS, commitInstruction });
|
|
52
|
+
|
|
53
|
+
assertTrue(!result.includes("docs(S01): add slice plan"), "commit step absent when commit_docs=false");
|
|
54
|
+
assertTrue(result.includes("Do not commit"), "no-commit instruction present");
|
|
55
|
+
assertTrue(result.includes("Update `.gsd/STATE.md`"), "STATE.md update step still present");
|
|
56
|
+
assertTrue(!result.includes("{{commitInstruction}}"), "no unresolved placeholder");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── all base variables are substituted ─────────────────────────────────
|
|
60
|
+
console.log("\n=== plan-slice prompt: all variables substituted ===");
|
|
61
|
+
{
|
|
62
|
+
const commitInstruction = `Commit: \`docs(S01): add slice plan\``;
|
|
63
|
+
const result = loadPromptFromWorktree("plan-slice", { ...BASE_VARS, commitInstruction });
|
|
64
|
+
|
|
65
|
+
assertTrue(!result.includes("{{"), "no unresolved placeholders remain");
|
|
66
|
+
assertTrue(result.includes("M001"), "milestoneId substituted");
|
|
67
|
+
assertTrue(result.includes("S01"), "sliceId substituted");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().then(report);
|
|
@@ -40,6 +40,7 @@ function writeRoadmap(base: string, mid: string, content: string): void {
|
|
|
40
40
|
function writePlan(base: string, mid: string, sid: string, content: string): void {
|
|
41
41
|
const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
|
|
42
42
|
mkdirSync(join(dir, 'tasks'), { recursive: true });
|
|
43
|
+
writeFileSync(join(dir, "tasks", "T01-PLAN.md"), "# T01 Plan\n");
|
|
43
44
|
writeFileSync(join(dir, `${sid}-PLAN.md`), content);
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -493,4 +494,45 @@ console.log('\n=== doctor: no blocker → no blocker_discovered_no_replan issue
|
|
|
493
494
|
rmSync(base, { recursive: true, force: true });
|
|
494
495
|
}
|
|
495
496
|
|
|
497
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
498
|
+
// Artifact Resolution: resolveExpectedArtifactPath for replan-slice (#858)
|
|
499
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
500
|
+
|
|
501
|
+
import { resolveExpectedArtifactPath, verifyExpectedArtifact } from '../auto-recovery.ts';
|
|
502
|
+
|
|
503
|
+
console.log('\n=== artifact: resolveExpectedArtifactPath returns REPLAN.md path for replan-slice ===');
|
|
504
|
+
{
|
|
505
|
+
const base = createFixtureBase();
|
|
506
|
+
writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
|
|
507
|
+
writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
|
|
508
|
+
|
|
509
|
+
const path = resolveExpectedArtifactPath('replan-slice', 'M001/S01', base);
|
|
510
|
+
assertTrue(path !== null, 'resolveExpectedArtifactPath returns non-null for replan-slice');
|
|
511
|
+
assertTrue(path!.endsWith('S01-REPLAN.md'), 'path ends with S01-REPLAN.md');
|
|
512
|
+
rmSync(base, { recursive: true, force: true });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log('\n=== artifact: verifyExpectedArtifact fails when REPLAN.md missing (#858) ===');
|
|
516
|
+
{
|
|
517
|
+
const base = createFixtureBase();
|
|
518
|
+
writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
|
|
519
|
+
writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
|
|
520
|
+
|
|
521
|
+
const result = verifyExpectedArtifact('replan-slice', 'M001/S01', base);
|
|
522
|
+
assertEq(result, false, 'verifyExpectedArtifact returns false when REPLAN.md is missing');
|
|
523
|
+
rmSync(base, { recursive: true, force: true });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log('\n=== artifact: verifyExpectedArtifact passes when REPLAN.md exists (#858) ===');
|
|
527
|
+
{
|
|
528
|
+
const base = createFixtureBase();
|
|
529
|
+
writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
|
|
530
|
+
writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
|
|
531
|
+
writeReplanFile(base, 'M001', 'S01', '# Replan\n\nBlocker addressed.');
|
|
532
|
+
|
|
533
|
+
const result = verifyExpectedArtifact('replan-slice', 'M001/S01', base);
|
|
534
|
+
assertEq(result, true, 'verifyExpectedArtifact returns true when REPLAN.md exists');
|
|
535
|
+
rmSync(base, { recursive: true, force: true });
|
|
536
|
+
}
|
|
537
|
+
|
|
496
538
|
report();
|