gsd-pi 2.37.1-dev.d3ace49 → 2.38.0-dev.63ad7e5
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/dist/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/remote-questions-config.js +2 -2
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +7 -8
- package/dist/resources/extensions/gsd/auto-loop.js +68 -97
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -71
- package/dist/resources/extensions/gsd/auto-prompts.js +7 -31
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +22 -2
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
- package/dist/resources/extensions/gsd/doctor.js +184 -11
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +2 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/index.js +2 -1
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +0 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +1 -11
- package/dist/resources/extensions/gsd/preferences.js +5 -5
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +6 -8
- package/src/resources/extensions/gsd/auto-loop.ts +88 -133
- package/src/resources/extensions/gsd/auto-post-unit.ts +52 -42
- package/src/resources/extensions/gsd/auto-prompts.ts +7 -33
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +24 -2
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +177 -13
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +2 -2
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/index.ts +3 -1
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +0 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -11
- package/src/resources/extensions/gsd/preferences.ts +5 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
- package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -100,6 +100,99 @@ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => {
|
|
|
100
100
|
assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" });
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
describe("createGridLayout", () => {
|
|
104
|
+
// Create a mock CmuxClient that tracks createSplitFrom calls
|
|
105
|
+
function makeMockClient() {
|
|
106
|
+
let nextId = 1;
|
|
107
|
+
const calls: Array<{ source: string | undefined; direction: string }> = [];
|
|
108
|
+
|
|
109
|
+
const client = {
|
|
110
|
+
calls,
|
|
111
|
+
async createGridLayout(count: number) {
|
|
112
|
+
// Simulate the grid layout logic with a fake client
|
|
113
|
+
if (count <= 0) return [];
|
|
114
|
+
const surfaces: string[] = [];
|
|
115
|
+
|
|
116
|
+
const createSplitFrom = async (source: string | undefined, direction: string) => {
|
|
117
|
+
calls.push({ source, direction });
|
|
118
|
+
return `surface-${nextId++}`;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const rightCol = await createSplitFrom("gsd-surface", "right");
|
|
122
|
+
surfaces.push(rightCol);
|
|
123
|
+
if (count === 1) return surfaces;
|
|
124
|
+
|
|
125
|
+
const bottomRight = await createSplitFrom(rightCol, "down");
|
|
126
|
+
surfaces.push(bottomRight);
|
|
127
|
+
if (count === 2) return surfaces;
|
|
128
|
+
|
|
129
|
+
const bottomLeft = await createSplitFrom("gsd-surface", "down");
|
|
130
|
+
surfaces.push(bottomLeft);
|
|
131
|
+
if (count === 3) return surfaces;
|
|
132
|
+
|
|
133
|
+
let lastSurface = bottomRight;
|
|
134
|
+
for (let i = 3; i < count; i++) {
|
|
135
|
+
const next = await createSplitFrom(lastSurface, "down");
|
|
136
|
+
surfaces.push(next);
|
|
137
|
+
lastSurface = next;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return surfaces;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
return client;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("1 agent creates single right split", async () => {
|
|
147
|
+
const mock = makeMockClient();
|
|
148
|
+
const surfaces = await mock.createGridLayout(1);
|
|
149
|
+
assert.equal(surfaces.length, 1);
|
|
150
|
+
assert.deepEqual(mock.calls, [
|
|
151
|
+
{ source: "gsd-surface", direction: "right" },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("2 agents creates right column then splits it down", async () => {
|
|
156
|
+
const mock = makeMockClient();
|
|
157
|
+
const surfaces = await mock.createGridLayout(2);
|
|
158
|
+
assert.equal(surfaces.length, 2);
|
|
159
|
+
assert.deepEqual(mock.calls, [
|
|
160
|
+
{ source: "gsd-surface", direction: "right" },
|
|
161
|
+
{ source: "surface-1", direction: "down" },
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("3 agents creates 2x2 grid (gsd + 3 agent surfaces)", async () => {
|
|
166
|
+
const mock = makeMockClient();
|
|
167
|
+
const surfaces = await mock.createGridLayout(3);
|
|
168
|
+
assert.equal(surfaces.length, 3);
|
|
169
|
+
assert.deepEqual(mock.calls, [
|
|
170
|
+
{ source: "gsd-surface", direction: "right" },
|
|
171
|
+
{ source: "surface-1", direction: "down" },
|
|
172
|
+
{ source: "gsd-surface", direction: "down" },
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("4 agents creates 2x2 grid with extra split", async () => {
|
|
177
|
+
const mock = makeMockClient();
|
|
178
|
+
const surfaces = await mock.createGridLayout(4);
|
|
179
|
+
assert.equal(surfaces.length, 4);
|
|
180
|
+
assert.deepEqual(mock.calls, [
|
|
181
|
+
{ source: "gsd-surface", direction: "right" },
|
|
182
|
+
{ source: "surface-1", direction: "down" },
|
|
183
|
+
{ source: "gsd-surface", direction: "down" },
|
|
184
|
+
{ source: "surface-2", direction: "down" },
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("0 agents returns empty", async () => {
|
|
189
|
+
const mock = makeMockClient();
|
|
190
|
+
const surfaces = await mock.createGridLayout(0);
|
|
191
|
+
assert.equal(surfaces.length, 0);
|
|
192
|
+
assert.equal(mock.calls.length, 0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
103
196
|
describe("cmux extension discovery opt-out", () => {
|
|
104
197
|
test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => {
|
|
105
198
|
const cmuxDir = path.resolve(
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { runGSDDoctor } from "../doctor.js";
|
|
6
|
+
import { formatDoctorReportJson } from "../doctor-format.js";
|
|
7
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
8
|
+
|
|
9
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
10
|
+
|
|
11
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function makeBase(): { base: string; gsd: string; mDir: string } {
|
|
14
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-enh-"));
|
|
15
|
+
const gsd = join(base, ".gsd");
|
|
16
|
+
const mDir = join(gsd, "milestones", "M001");
|
|
17
|
+
mkdirSync(join(mDir, "slices"), { recursive: true });
|
|
18
|
+
return { base, gsd, mDir };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeRoadmap(mDir: string, content: string): void {
|
|
22
|
+
writeFileSync(join(mDir, "M001-ROADMAP.md"), content);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeSlice(mDir: string, sliceId: string, planContent: string): string {
|
|
26
|
+
const sDir = join(mDir, "slices", sliceId);
|
|
27
|
+
const tDir = join(sDir, "tasks");
|
|
28
|
+
mkdirSync(tDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(sDir, `${sliceId}-PLAN.md`), planContent);
|
|
30
|
+
return sDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function main(): Promise<void> {
|
|
34
|
+
// ── 1. Circular dependency detection ──────────────────────────────────────
|
|
35
|
+
console.log("\n=== circular dependency detection ===");
|
|
36
|
+
{
|
|
37
|
+
const { base, mDir } = makeBase();
|
|
38
|
+
writeRoadmap(mDir, `# M001: Circular Test\n\n## Slices\n- [ ] **S01: Slice A** \`risk:low\` \`depends:[S02]\`\n > After this: done\n- [ ] **S02: Slice B** \`risk:low\` \`depends:[S01]\`\n > After this: done\n`);
|
|
39
|
+
writeSlice(mDir, "S01", "# S01: Slice A\n\n**Goal:** A\n**Demo:** A\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
40
|
+
writeSlice(mDir, "S02", "# S02: Slice B\n\n**Goal:** B\n**Demo:** B\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
41
|
+
|
|
42
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
43
|
+
assertTrue(
|
|
44
|
+
result.issues.some(i => i.code === "circular_slice_dependency"),
|
|
45
|
+
"detects circular dependency S01 → S02 → S01",
|
|
46
|
+
);
|
|
47
|
+
rmSync(base, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 2. Duplicate task IDs ──────────────────────────────────────────────────
|
|
51
|
+
console.log("\n=== duplicate task IDs ===");
|
|
52
|
+
{
|
|
53
|
+
const { base, mDir } = makeBase();
|
|
54
|
+
writeRoadmap(mDir, `# M001: Dup Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
55
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: First** `est:10m`\n Task one.\n- [ ] **T01: Duplicate** `est:10m`\n Task dup.\n");
|
|
56
|
+
|
|
57
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
58
|
+
assertTrue(
|
|
59
|
+
result.issues.some(i => i.code === "duplicate_task_id"),
|
|
60
|
+
"detects duplicate task ID T01",
|
|
61
|
+
);
|
|
62
|
+
rmSync(base, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 3. Orphaned slice directory ──────────────────────────────────────────
|
|
66
|
+
console.log("\n=== orphaned slice directory ===");
|
|
67
|
+
{
|
|
68
|
+
const { base, mDir } = makeBase();
|
|
69
|
+
writeRoadmap(mDir, `# M001: Orphan Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
70
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
71
|
+
// Create an extra slice directory not in roadmap
|
|
72
|
+
mkdirSync(join(mDir, "slices", "S99"), { recursive: true });
|
|
73
|
+
|
|
74
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
75
|
+
assertTrue(
|
|
76
|
+
result.issues.some(i => i.code === "orphaned_slice_directory" && i.message.includes("S99")),
|
|
77
|
+
"detects orphaned slice directory S99",
|
|
78
|
+
);
|
|
79
|
+
rmSync(base, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── 4. Task file not in plan ───────────────────────────────────────────────
|
|
83
|
+
console.log("\n=== task file not in plan ===");
|
|
84
|
+
{
|
|
85
|
+
const { base, mDir } = makeBase();
|
|
86
|
+
writeRoadmap(mDir, `# M001: Extra Task Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
87
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
88
|
+
// T01 summary (matches plan)
|
|
89
|
+
writeFileSync(join(sDir, "tasks", "T01-SUMMARY.md"), "---\nstatus: done\n---\n# T01\nDone.\n");
|
|
90
|
+
// T99 summary (NOT in plan)
|
|
91
|
+
writeFileSync(join(sDir, "tasks", "T99-SUMMARY.md"), "---\nstatus: done\n---\n# T99\nExtra.\n");
|
|
92
|
+
|
|
93
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
94
|
+
assertTrue(
|
|
95
|
+
result.issues.some(i => i.code === "task_file_not_in_plan" && i.message.includes("T99")),
|
|
96
|
+
"detects task summary T99 not in plan",
|
|
97
|
+
);
|
|
98
|
+
rmSync(base, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 5. Stale REPLAN file ────────────────────────────────────────────────────
|
|
102
|
+
console.log("\n=== stale REPLAN detection ===");
|
|
103
|
+
{
|
|
104
|
+
const { base, mDir } = makeBase();
|
|
105
|
+
writeRoadmap(mDir, `# M001: Replan Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
106
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
107
|
+
writeFileSync(join(sDir, "tasks", "T01-SUMMARY.md"), "---\nstatus: done\ncompleted_at: 2026-01-01T00:00:00Z\n---\n# T01\nDone.\n");
|
|
108
|
+
// Add a REPLAN file even though all tasks are done
|
|
109
|
+
writeFileSync(join(sDir, "S01-REPLAN.md"), "# S01 REPLAN\nSomething changed.\n");
|
|
110
|
+
|
|
111
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
112
|
+
assertTrue(
|
|
113
|
+
result.issues.some(i => i.code === "stale_replan_file"),
|
|
114
|
+
"detects stale REPLAN when all tasks are done",
|
|
115
|
+
);
|
|
116
|
+
rmSync(base, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 6. Metrics ledger corrupt ───────────────────────────────────────────────
|
|
120
|
+
console.log("\n=== metrics ledger corrupt ===");
|
|
121
|
+
{
|
|
122
|
+
const { base, gsd, mDir } = makeBase();
|
|
123
|
+
writeRoadmap(mDir, `# M001: Metrics Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
124
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
125
|
+
// Write invalid metrics.json
|
|
126
|
+
writeFileSync(join(gsd, "metrics.json"), '{"version":2,"data":[]}');
|
|
127
|
+
|
|
128
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
129
|
+
assertTrue(
|
|
130
|
+
result.issues.some(i => i.code === "metrics_ledger_corrupt"),
|
|
131
|
+
"detects corrupt metrics ledger (version != 1)",
|
|
132
|
+
);
|
|
133
|
+
rmSync(base, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── 7. Large planning file ──────────────────────────────────────────────────
|
|
137
|
+
console.log("\n=== large planning file ===");
|
|
138
|
+
{
|
|
139
|
+
const { base, mDir } = makeBase();
|
|
140
|
+
writeRoadmap(mDir, `# M001: Large File Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
141
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
142
|
+
// Write a 101KB .md file
|
|
143
|
+
const bigContent = "# Big File\n" + "x".repeat(101 * 1024);
|
|
144
|
+
writeFileSync(join(sDir, "BIGFILE.md"), bigContent);
|
|
145
|
+
|
|
146
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
147
|
+
assertTrue(
|
|
148
|
+
result.issues.some(i => i.code === "large_planning_file"),
|
|
149
|
+
"detects large planning file over 100KB",
|
|
150
|
+
);
|
|
151
|
+
rmSync(base, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── 8. Future timestamp ─────────────────────────────────────────────────────
|
|
155
|
+
console.log("\n=== future timestamp ===");
|
|
156
|
+
{
|
|
157
|
+
const { base, mDir } = makeBase();
|
|
158
|
+
writeRoadmap(mDir, `# M001: Timestamp Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
159
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
160
|
+
// completed_at is 2 days in the future
|
|
161
|
+
const futureDate = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString();
|
|
162
|
+
writeFileSync(
|
|
163
|
+
join(sDir, "tasks", "T01-SUMMARY.md"),
|
|
164
|
+
`---\nstatus: done\ncompleted_at: ${futureDate}\n---\n# T01\nDone.\n`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
168
|
+
assertTrue(
|
|
169
|
+
result.issues.some(i => i.code === "future_timestamp"),
|
|
170
|
+
"detects future completed_at timestamp",
|
|
171
|
+
);
|
|
172
|
+
rmSync(base, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── 9. JSON output format ───────────────────────────────────────────────────
|
|
176
|
+
console.log("\n=== JSON output format ===");
|
|
177
|
+
{
|
|
178
|
+
const { base, mDir } = makeBase();
|
|
179
|
+
writeRoadmap(mDir, `# M001: JSON Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
180
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
181
|
+
|
|
182
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
183
|
+
const json = formatDoctorReportJson(result);
|
|
184
|
+
|
|
185
|
+
let parsed: unknown;
|
|
186
|
+
try {
|
|
187
|
+
parsed = JSON.parse(json);
|
|
188
|
+
} catch {
|
|
189
|
+
parsed = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
assertTrue(parsed !== null, "formatDoctorReportJson produces valid JSON");
|
|
193
|
+
assertTrue(typeof (parsed as Record<string, unknown>)?.ok === "boolean", "JSON has ok field");
|
|
194
|
+
assertTrue(Array.isArray((parsed as Record<string, unknown>)?.issues), "JSON has issues array");
|
|
195
|
+
assertTrue(Array.isArray((parsed as Record<string, unknown>)?.fixesApplied), "JSON has fixesApplied array");
|
|
196
|
+
assertTrue(typeof (parsed as Record<string, unknown>)?.generatedAt === "string", "JSON has generatedAt field");
|
|
197
|
+
assertTrue(typeof (parsed as Record<string, unknown>)?.summary === "object", "JSON has summary object");
|
|
198
|
+
|
|
199
|
+
rmSync(base, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── 10. Dry-run mode ────────────────────────────────────────────────────────
|
|
203
|
+
console.log("\n=== dry-run mode ===");
|
|
204
|
+
{
|
|
205
|
+
const { base, mDir } = makeBase();
|
|
206
|
+
writeRoadmap(mDir, `# M001: Dry Run Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
207
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
208
|
+
|
|
209
|
+
const result = await runGSDDoctor(base, { fix: true, dryRun: true });
|
|
210
|
+
// In dry-run mode, no actual files should be created
|
|
211
|
+
assertTrue(!existsSync(join(sDir, "S01-SUMMARY.md")), "dry-run does not create slice summary");
|
|
212
|
+
assertTrue(
|
|
213
|
+
result.fixesApplied.some(f => f.startsWith("[dry-run]")),
|
|
214
|
+
"dry-run mode reports would-fix entries",
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
rmSync(base, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 11. Per-check timing ─────────────────────────────────────────────────────
|
|
221
|
+
console.log("\n=== per-check timing ===");
|
|
222
|
+
{
|
|
223
|
+
const { base, mDir } = makeBase();
|
|
224
|
+
writeRoadmap(mDir, `# M001: Timing Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
225
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
226
|
+
|
|
227
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
228
|
+
assertTrue(result.timing !== undefined, "report includes timing");
|
|
229
|
+
assertTrue(typeof result.timing?.git === "number", "timing.git is a number");
|
|
230
|
+
assertTrue(typeof result.timing?.runtime === "number", "timing.runtime is a number");
|
|
231
|
+
assertTrue(typeof result.timing?.environment === "number", "timing.environment is a number");
|
|
232
|
+
assertTrue(typeof result.timing?.gsdState === "number", "timing.gsdState is a number");
|
|
233
|
+
|
|
234
|
+
rmSync(base, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── 12. Doctor history ───────────────────────────────────────────────────────
|
|
238
|
+
console.log("\n=== doctor history ===");
|
|
239
|
+
{
|
|
240
|
+
const { base, gsd, mDir } = makeBase();
|
|
241
|
+
writeRoadmap(mDir, `# M001: History Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
242
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
243
|
+
|
|
244
|
+
await runGSDDoctor(base, { fix: false });
|
|
245
|
+
|
|
246
|
+
const historyPath = join(gsd, "doctor-history.jsonl");
|
|
247
|
+
assertTrue(existsSync(historyPath), "doctor-history.jsonl is created after run");
|
|
248
|
+
|
|
249
|
+
const { readDoctorHistory } = await import("../doctor.js");
|
|
250
|
+
const history = await readDoctorHistory(base);
|
|
251
|
+
assertTrue(history.length >= 1, "history has at least one entry");
|
|
252
|
+
assertTrue(typeof history[0]?.ts === "string", "history entry has ts field");
|
|
253
|
+
assertTrue(typeof history[0]?.ok === "boolean", "history entry has ok field");
|
|
254
|
+
assertTrue(typeof history[0]?.errors === "number", "history entry has errors count");
|
|
255
|
+
assertTrue(Array.isArray(history[0]?.codes), "history entry has codes array");
|
|
256
|
+
|
|
257
|
+
rmSync(base, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
report();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main().catch(err => {
|
|
264
|
+
console.error(err);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
});
|
|
@@ -47,6 +47,18 @@ function withEnv(vars: Record<string, string | undefined>, fn: () => void): void
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function withCwd(nextCwd: string, fn: () => void): void {
|
|
51
|
+
const saved = process.cwd();
|
|
52
|
+
process.chdir(nextCwd);
|
|
53
|
+
try {
|
|
54
|
+
fn();
|
|
55
|
+
} finally {
|
|
56
|
+
process.chdir(saved);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PRESENT_TEST_VALUE = "configured";
|
|
61
|
+
|
|
50
62
|
// ─── formatProviderReport ─────────────────────────────────────────────────────
|
|
51
63
|
|
|
52
64
|
test("formatProviderReport returns fallback for empty results", () => {
|
|
@@ -312,7 +324,7 @@ test("runProviderChecks reports ok for Anthropic when GitHub Copilot env var is
|
|
|
312
324
|
withEnv({
|
|
313
325
|
ANTHROPIC_API_KEY: undefined,
|
|
314
326
|
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
315
|
-
COPILOT_GITHUB_TOKEN:
|
|
327
|
+
COPILOT_GITHUB_TOKEN: PRESENT_TEST_VALUE,
|
|
316
328
|
GH_TOKEN: undefined,
|
|
317
329
|
GITHUB_TOKEN: undefined,
|
|
318
330
|
HOME: tmpHome,
|
|
@@ -336,7 +348,7 @@ test("runProviderChecks reports ok for Anthropic via GITHUB_TOKEN cross-provider
|
|
|
336
348
|
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
337
349
|
COPILOT_GITHUB_TOKEN: undefined,
|
|
338
350
|
GH_TOKEN: undefined,
|
|
339
|
-
GITHUB_TOKEN:
|
|
351
|
+
GITHUB_TOKEN: PRESENT_TEST_VALUE,
|
|
340
352
|
HOME: tmpHome,
|
|
341
353
|
}, () => {
|
|
342
354
|
try {
|
|
@@ -354,7 +366,7 @@ test("runProviderChecks detects ANTHROPIC_OAUTH_TOKEN as valid Anthropic auth",
|
|
|
354
366
|
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-oauth-test-")));
|
|
355
367
|
withEnv({
|
|
356
368
|
ANTHROPIC_API_KEY: undefined,
|
|
357
|
-
ANTHROPIC_OAUTH_TOKEN:
|
|
369
|
+
ANTHROPIC_OAUTH_TOKEN: PRESENT_TEST_VALUE,
|
|
358
370
|
COPILOT_GITHUB_TOKEN: undefined,
|
|
359
371
|
GH_TOKEN: undefined,
|
|
360
372
|
GITHUB_TOKEN: undefined,
|
|
@@ -401,3 +413,74 @@ test("runProviderChecks reports ok via Copilot auth.json for Anthropic", () => {
|
|
|
401
413
|
rmSync(tmpHome, { recursive: true, force: true });
|
|
402
414
|
});
|
|
403
415
|
});
|
|
416
|
+
|
|
417
|
+
test("runProviderChecks uses provider-qualified anthropic-vertex model IDs", () => {
|
|
418
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-home-")));
|
|
419
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-repo-")));
|
|
420
|
+
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
|
421
|
+
writeFileSync(
|
|
422
|
+
join(repo, ".gsd", "preferences.md"),
|
|
423
|
+
[
|
|
424
|
+
"---",
|
|
425
|
+
"models:",
|
|
426
|
+
" execution: anthropic-vertex/claude-sonnet-4-6",
|
|
427
|
+
"---",
|
|
428
|
+
"",
|
|
429
|
+
].join("\n"),
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
withEnv({
|
|
433
|
+
HOME: tmpHome,
|
|
434
|
+
ANTHROPIC_API_KEY: undefined,
|
|
435
|
+
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
436
|
+
ANTHROPIC_VERTEX_PROJECT_ID: "vertex-project",
|
|
437
|
+
}, () => {
|
|
438
|
+
withCwd(repo, () => {
|
|
439
|
+
const results = runProviderChecks();
|
|
440
|
+
const vertex = results.find(r => r.name === "anthropic-vertex");
|
|
441
|
+
const anthropic = results.find(r => r.name === "anthropic");
|
|
442
|
+
assert.ok(vertex, "anthropic-vertex result should exist");
|
|
443
|
+
assert.equal(vertex!.status, "ok", "should accept ANTHROPIC_VERTEX_PROJECT_ID as configured");
|
|
444
|
+
assert.ok(!anthropic || !anthropic.required, "plain anthropic should not be required for anthropic-vertex config");
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
rmSync(repo, { recursive: true, force: true });
|
|
449
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("runProviderChecks uses object provider field for anthropic-vertex models", () => {
|
|
453
|
+
const tmpHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-home-")));
|
|
454
|
+
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-repo-")));
|
|
455
|
+
mkdirSync(join(repo, ".gsd"), { recursive: true });
|
|
456
|
+
writeFileSync(
|
|
457
|
+
join(repo, ".gsd", "preferences.md"),
|
|
458
|
+
[
|
|
459
|
+
"---",
|
|
460
|
+
"models:",
|
|
461
|
+
" execution:",
|
|
462
|
+
" model: claude-sonnet-4-6",
|
|
463
|
+
" provider: anthropic-vertex",
|
|
464
|
+
"---",
|
|
465
|
+
"",
|
|
466
|
+
].join("\n"),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
withEnv({
|
|
470
|
+
HOME: tmpHome,
|
|
471
|
+
ANTHROPIC_API_KEY: undefined,
|
|
472
|
+
ANTHROPIC_OAUTH_TOKEN: undefined,
|
|
473
|
+
ANTHROPIC_VERTEX_PROJECT_ID: undefined,
|
|
474
|
+
}, () => {
|
|
475
|
+
withCwd(repo, () => {
|
|
476
|
+
const results = runProviderChecks();
|
|
477
|
+
const vertex = results.find(r => r.name === "anthropic-vertex");
|
|
478
|
+
assert.ok(vertex, "anthropic-vertex result should exist");
|
|
479
|
+
assert.equal(vertex!.status, "error", "missing vertex config should be reported against anthropic-vertex");
|
|
480
|
+
assert.ok(vertex!.detail?.includes("ANTHROPIC_VERTEX_PROJECT_ID"), "should point to vertex setup");
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
rmSync(repo, { recursive: true, force: true });
|
|
485
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
486
|
+
});
|
|
@@ -208,30 +208,25 @@ test("git fields comprehensive validation", () => {
|
|
|
208
208
|
assert.equal(preferences.git?.isolation, "branch");
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
-
test("auto_visualize, auto_report,
|
|
211
|
+
test("auto_visualize, auto_report, context_selection validate correctly", () => {
|
|
212
212
|
const { preferences, errors } = validatePreferences({
|
|
213
213
|
auto_visualize: true,
|
|
214
214
|
auto_report: false,
|
|
215
|
-
compression_strategy: "compress",
|
|
216
215
|
context_selection: "smart",
|
|
217
216
|
});
|
|
218
217
|
assert.equal(errors.length, 0);
|
|
219
218
|
assert.equal(preferences.auto_visualize, true);
|
|
220
219
|
assert.equal(preferences.auto_report, false);
|
|
221
|
-
assert.equal(preferences.compression_strategy, "compress");
|
|
222
220
|
assert.equal(preferences.context_selection, "smart");
|
|
223
221
|
});
|
|
224
222
|
|
|
225
|
-
test("auto_visualize, auto_report,
|
|
223
|
+
test("auto_visualize, auto_report, context_selection reject invalid values", () => {
|
|
226
224
|
const { errors: e1 } = validatePreferences({ auto_visualize: "yes" as never });
|
|
227
225
|
assert.ok(e1.some(e => e.includes("auto_visualize")));
|
|
228
226
|
|
|
229
227
|
const { errors: e2 } = validatePreferences({ auto_report: 1 as never });
|
|
230
228
|
assert.ok(e2.some(e => e.includes("auto_report")));
|
|
231
229
|
|
|
232
|
-
const { errors: e3 } = validatePreferences({ compression_strategy: "shrink" as never });
|
|
233
|
-
assert.ok(e3.some(e => e.includes("compression_strategy")));
|
|
234
|
-
|
|
235
230
|
const { errors: e4 } = validatePreferences({ context_selection: "partial" as never });
|
|
236
231
|
assert.ok(e4.some(e => e.includes("context_selection")));
|
|
237
232
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
|
|
7
|
+
|
|
8
|
+
function readPrompt(name: string): string {
|
|
9
|
+
return readFileSync(join(promptsDir, `${name}.md`), "utf-8");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test("reactive-execute prompt keeps task summaries with subagents and avoids batch commits", () => {
|
|
13
|
+
const prompt = readPrompt("reactive-execute");
|
|
14
|
+
assert.match(prompt, /subagent-written summary as authoritative/i);
|
|
15
|
+
assert.match(prompt, /Do NOT create a batch commit/i);
|
|
16
|
+
assert.doesNotMatch(prompt, /\*\*Write task summaries\*\*/i);
|
|
17
|
+
assert.doesNotMatch(prompt, /\*\*Commit\*\* all changes/i);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence", () => {
|
|
21
|
+
const prompt = readPrompt("run-uat");
|
|
22
|
+
assert.match(prompt, /\*\*Detected UAT mode:\*\*\s*`\{\{uatType\}\}`/);
|
|
23
|
+
assert.match(prompt, /uatType:\s*\{\{uatType\}\}/);
|
|
24
|
+
assert.match(prompt, /live-runtime/);
|
|
25
|
+
assert.match(prompt, /browser\/runtime\/network/i);
|
|
26
|
+
assert.match(prompt, /NEEDS-HUMAN/);
|
|
27
|
+
assert.doesNotMatch(prompt, /uatType:\s*artifact-driven/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
|
|
31
|
+
const prompt = readPrompt("workflow-start");
|
|
32
|
+
assert.match(prompt, /Keep moving by default/i);
|
|
33
|
+
assert.match(prompt, /Decision gates, not ceremony/i);
|
|
34
|
+
assert.doesNotMatch(prompt, /confirm with the user before proceeding/i);
|
|
35
|
+
assert.doesNotMatch(prompt, /Gate between phases/i);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("discuss prompt allows implementation questions when they materially matter", () => {
|
|
39
|
+
const prompt = readPrompt("discuss");
|
|
40
|
+
assert.match(prompt, /Lead with experience, but ask implementation when it materially matters/i);
|
|
41
|
+
assert.match(prompt, /one gate, not two/i);
|
|
42
|
+
assert.doesNotMatch(prompt, /Questions must be about the experience, not the implementation/i);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("guided discussion prompts avoid wrap-up prompts after every round", () => {
|
|
46
|
+
const milestonePrompt = readPrompt("guided-discuss-milestone");
|
|
47
|
+
const slicePrompt = readPrompt("guided-discuss-slice");
|
|
48
|
+
assert.match(milestonePrompt, /Do \*\*not\*\* ask a meta "ready to wrap up\?" question after every round/i);
|
|
49
|
+
assert.match(slicePrompt, /Do \*\*not\*\* ask a meta "ready to wrap up\?" question after every round/i);
|
|
50
|
+
assert.doesNotMatch(milestonePrompt, /I think I have a solid picture of this milestone\. Ready to wrap up/i);
|
|
51
|
+
assert.doesNotMatch(slicePrompt, /I think I have a solid picture of this slice\. Ready to wrap up/i);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("guided-resume-task prompt preserves recovery state until work is superseded", () => {
|
|
55
|
+
const prompt = readPrompt("guided-resume-task");
|
|
56
|
+
assert.match(prompt, /Do \*\*not\*\* delete the continue file immediately/i);
|
|
57
|
+
assert.match(prompt, /successfully completed or you have written a newer summary\/continue artifact/i);
|
|
58
|
+
assert.doesNotMatch(prompt, /Delete the continue file after reading it/i);
|
|
59
|
+
});
|
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
-
import { externalGsdRoot, ensureGsdSymlink } from "../repo-identity.ts";
|
|
6
|
+
import { repoIdentity, externalGsdRoot, ensureGsdSymlink, validateProjectId } from "../repo-identity.ts";
|
|
7
7
|
import { createTestContext } from "./test-helpers.ts";
|
|
8
8
|
|
|
9
9
|
const { assertEq, assertTrue, report } = createTestContext();
|
|
@@ -57,6 +57,26 @@ async function main(): Promise<void> {
|
|
|
57
57
|
assertEq(preservedDirState, join(worktreePath, ".gsd"), "worktree .gsd directory is left in place for sync-based refresh");
|
|
58
58
|
assertTrue(lstatSync(join(worktreePath, ".gsd")).isDirectory(), "worktree .gsd directory remains a directory");
|
|
59
59
|
assertTrue(existsSync(join(worktreePath, ".gsd", "milestones", "stale.txt")), "existing worktree .gsd directory contents remain available for sync logic");
|
|
60
|
+
|
|
61
|
+
console.log("\n=== GSD_PROJECT_ID overrides computed repo hash ===");
|
|
62
|
+
process.env.GSD_PROJECT_ID = "my-project";
|
|
63
|
+
assertEq(repoIdentity(base), "my-project", "repoIdentity returns GSD_PROJECT_ID when set");
|
|
64
|
+
assertEq(externalGsdRoot(base), join(stateDir, "projects", "my-project"), "externalGsdRoot uses GSD_PROJECT_ID");
|
|
65
|
+
delete process.env.GSD_PROJECT_ID;
|
|
66
|
+
|
|
67
|
+
console.log("\n=== GSD_PROJECT_ID falls back to hash when unset ===");
|
|
68
|
+
const hashIdentity = repoIdentity(base);
|
|
69
|
+
assertTrue(/^[0-9a-f]{12}$/.test(hashIdentity), "repoIdentity returns 12-char hex hash when GSD_PROJECT_ID is unset");
|
|
70
|
+
|
|
71
|
+
console.log("\n=== validateProjectId rejects invalid values ===");
|
|
72
|
+
for (const invalid of ["has spaces", "path/traversal", "dot..dot", "back\\slash"]) {
|
|
73
|
+
assertTrue(!validateProjectId(invalid), `validateProjectId rejects invalid value: "${invalid}"`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log("\n=== validateProjectId accepts valid values ===");
|
|
77
|
+
for (const valid of ["my-project", "foo_bar", "abc123", "A-Z_0-9"]) {
|
|
78
|
+
assertTrue(validateProjectId(valid), `validateProjectId accepts valid value: "${valid}"`);
|
|
79
|
+
}
|
|
60
80
|
} finally {
|
|
61
81
|
delete process.env.GSD_STATE_DIR;
|
|
62
82
|
rmSync(base, { recursive: true, force: true });
|
|
@@ -210,7 +210,7 @@ async function main(): Promise<void> {
|
|
|
210
210
|
const sliceId = 'S01';
|
|
211
211
|
const uatPath = '.gsd/milestones/M001/slices/S01/S01-UAT.md';
|
|
212
212
|
const uatResultPath = '.gsd/milestones/M001/slices/S01/S01-UAT-RESULT.md';
|
|
213
|
-
const uatType = '
|
|
213
|
+
const uatType = 'live-runtime';
|
|
214
214
|
const inlinedContext = '<!-- no context -->';
|
|
215
215
|
|
|
216
216
|
let promptResult: string | undefined;
|
|
@@ -246,13 +246,21 @@ async function main(): Promise<void> {
|
|
|
246
246
|
promptResult?.includes(uatResultPath) ?? false,
|
|
247
247
|
`prompt contains uatResultPath value after substitution`,
|
|
248
248
|
);
|
|
249
|
+
assertTrue(
|
|
250
|
+
promptResult?.includes(`Detected UAT mode:** \`${uatType}\``) ?? false,
|
|
251
|
+
`prompt contains detected dynamic uatType value "${uatType}" after substitution`,
|
|
252
|
+
);
|
|
253
|
+
assertTrue(
|
|
254
|
+
promptResult?.includes(`uatType: ${uatType}`) ?? false,
|
|
255
|
+
`prompt contains dynamic uatType frontmatter value "${uatType}" after substitution`,
|
|
256
|
+
);
|
|
249
257
|
assertTrue(
|
|
250
258
|
!/\{\{[^}]+\}\}/.test(promptResult ?? ''),
|
|
251
259
|
'no unreplaced {{...}} tokens remain after variable substitution',
|
|
252
260
|
);
|
|
253
261
|
assertTrue(
|
|
254
|
-
/
|
|
255
|
-
'prompt contains
|
|
262
|
+
/browser|runtime|execute|run/i.test(promptResult ?? ''),
|
|
263
|
+
'prompt contains runtime execution language (browser/runtime/execute/run)',
|
|
256
264
|
);
|
|
257
265
|
assertTrue(
|
|
258
266
|
!/surfaced for human review/i.test(promptResult ?? ''),
|