gsd-pi 2.29.0-dev.2ccf3fb → 2.29.0-dev.4c155ee

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/headless.js +4 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.ts +31 -0
  3. package/dist/resources/extensions/gsd/auto-dispatch.ts +32 -3
  4. package/dist/resources/extensions/gsd/auto-post-unit.ts +39 -10
  5. package/dist/resources/extensions/gsd/auto-prompts.ts +40 -17
  6. package/dist/resources/extensions/gsd/auto-recovery.ts +2 -1
  7. package/dist/resources/extensions/gsd/auto-start.ts +18 -32
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +21 -182
  9. package/dist/resources/extensions/gsd/auto.ts +2 -9
  10. package/dist/resources/extensions/gsd/captures.ts +4 -10
  11. package/dist/resources/extensions/gsd/commands-handlers.ts +2 -1
  12. package/dist/resources/extensions/gsd/commands.ts +2 -1
  13. package/dist/resources/extensions/gsd/detection.ts +2 -1
  14. package/dist/resources/extensions/gsd/doctor-checks.ts +49 -1
  15. package/dist/resources/extensions/gsd/doctor-types.ts +3 -1
  16. package/dist/resources/extensions/gsd/forensics.ts +2 -2
  17. package/dist/resources/extensions/gsd/git-service.ts +3 -2
  18. package/dist/resources/extensions/gsd/gitignore.ts +9 -63
  19. package/dist/resources/extensions/gsd/gsd-db.ts +1 -165
  20. package/dist/resources/extensions/gsd/guided-flow.ts +8 -5
  21. package/dist/resources/extensions/gsd/index.ts +3 -3
  22. package/dist/resources/extensions/gsd/md-importer.ts +3 -2
  23. package/dist/resources/extensions/gsd/mechanical-completion.ts +430 -0
  24. package/dist/resources/extensions/gsd/migrate/command.ts +3 -2
  25. package/dist/resources/extensions/gsd/migrate/writer.ts +2 -1
  26. package/dist/resources/extensions/gsd/migrate-external.ts +123 -0
  27. package/dist/resources/extensions/gsd/paths.ts +24 -2
  28. package/dist/resources/extensions/gsd/post-unit-hooks.ts +6 -5
  29. package/dist/resources/extensions/gsd/preferences-models.ts +7 -1
  30. package/dist/resources/extensions/gsd/preferences-validation.ts +2 -1
  31. package/dist/resources/extensions/gsd/preferences.ts +10 -5
  32. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
  33. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
  34. package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -1
  35. package/dist/resources/extensions/gsd/repo-identity.ts +148 -0
  36. package/dist/resources/extensions/gsd/resource-version.ts +99 -0
  37. package/dist/resources/extensions/gsd/session-forensics.ts +4 -3
  38. package/dist/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
  39. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
  40. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
  41. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
  42. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
  43. package/dist/resources/extensions/gsd/tests/git-service.test.ts +10 -37
  44. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
  45. package/dist/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
  46. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
  47. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
  48. package/dist/resources/extensions/gsd/triage-resolution.ts +2 -1
  49. package/dist/resources/extensions/gsd/types.ts +2 -0
  50. package/dist/resources/extensions/gsd/worktree-command.ts +1 -11
  51. package/dist/resources/extensions/gsd/worktree-manager.ts +3 -2
  52. package/dist/resources/extensions/gsd/worktree.ts +42 -5
  53. package/dist/resources/skills/react-best-practices/SKILL.md +1 -1
  54. package/package.json +1 -1
  55. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/core/lsp/client.js +3 -0
  57. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  58. package/packages/pi-coding-agent/src/core/lsp/client.ts +3 -0
  59. package/src/resources/extensions/gsd/auto-dashboard.ts +31 -0
  60. package/src/resources/extensions/gsd/auto-dispatch.ts +32 -3
  61. package/src/resources/extensions/gsd/auto-post-unit.ts +39 -10
  62. package/src/resources/extensions/gsd/auto-prompts.ts +40 -17
  63. package/src/resources/extensions/gsd/auto-recovery.ts +2 -1
  64. package/src/resources/extensions/gsd/auto-start.ts +18 -32
  65. package/src/resources/extensions/gsd/auto-worktree.ts +21 -182
  66. package/src/resources/extensions/gsd/auto.ts +2 -9
  67. package/src/resources/extensions/gsd/captures.ts +4 -10
  68. package/src/resources/extensions/gsd/commands-handlers.ts +2 -1
  69. package/src/resources/extensions/gsd/commands.ts +2 -1
  70. package/src/resources/extensions/gsd/detection.ts +2 -1
  71. package/src/resources/extensions/gsd/doctor-checks.ts +49 -1
  72. package/src/resources/extensions/gsd/doctor-types.ts +3 -1
  73. package/src/resources/extensions/gsd/forensics.ts +2 -2
  74. package/src/resources/extensions/gsd/git-service.ts +3 -2
  75. package/src/resources/extensions/gsd/gitignore.ts +9 -63
  76. package/src/resources/extensions/gsd/gsd-db.ts +1 -165
  77. package/src/resources/extensions/gsd/guided-flow.ts +8 -5
  78. package/src/resources/extensions/gsd/index.ts +3 -3
  79. package/src/resources/extensions/gsd/md-importer.ts +3 -2
  80. package/src/resources/extensions/gsd/mechanical-completion.ts +430 -0
  81. package/src/resources/extensions/gsd/migrate/command.ts +3 -2
  82. package/src/resources/extensions/gsd/migrate/writer.ts +2 -1
  83. package/src/resources/extensions/gsd/migrate-external.ts +123 -0
  84. package/src/resources/extensions/gsd/paths.ts +24 -2
  85. package/src/resources/extensions/gsd/post-unit-hooks.ts +6 -5
  86. package/src/resources/extensions/gsd/preferences-models.ts +7 -1
  87. package/src/resources/extensions/gsd/preferences-validation.ts +2 -1
  88. package/src/resources/extensions/gsd/preferences.ts +10 -5
  89. package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
  90. package/src/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
  91. package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -1
  92. package/src/resources/extensions/gsd/repo-identity.ts +148 -0
  93. package/src/resources/extensions/gsd/resource-version.ts +99 -0
  94. package/src/resources/extensions/gsd/session-forensics.ts +4 -3
  95. package/src/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
  96. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
  97. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
  98. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
  99. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
  100. package/src/resources/extensions/gsd/tests/git-service.test.ts +10 -37
  101. package/src/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
  102. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
  103. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
  104. package/src/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
  105. package/src/resources/extensions/gsd/triage-resolution.ts +2 -1
  106. package/src/resources/extensions/gsd/types.ts +2 -0
  107. package/src/resources/extensions/gsd/worktree-command.ts +1 -11
  108. package/src/resources/extensions/gsd/worktree-manager.ts +3 -2
  109. package/src/resources/extensions/gsd/worktree.ts +42 -5
  110. package/src/resources/skills/react-best-practices/SKILL.md +1 -1
  111. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
  112. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
  113. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
  114. package/src/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
  115. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
  116. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
@@ -1167,53 +1167,26 @@ async function main(): Promise<void> {
1167
1167
  rmSync(repo, { recursive: true, force: true });
1168
1168
  }
1169
1169
 
1170
- // ─── ensureGitignore: commit_docs false adds blanket .gsd/ ──────────
1170
+ // ─── ensureGitignore: always adds .gsd to gitignore ──────────────────
1171
1171
 
1172
- console.log("\n=== ensureGitignore: commit_docs false ===");
1172
+ console.log("\n=== ensureGitignore: adds .gsd entry ===");
1173
1173
 
1174
1174
  {
1175
1175
  const { ensureGitignore } = await import("../gitignore.ts");
1176
- const repo = mkdtempSync(join(tmpdir(), "gsd-gitignore-commit-docs-"));
1176
+ const repo = mkdtempSync(join(tmpdir(), "gsd-gitignore-external-state-"));
1177
1177
 
1178
- // When commit_docs is false, should add blanket .gsd/ to gitignore
1179
- const modified = ensureGitignore(repo, { commitDocs: false });
1180
- assertTrue(modified, "commit_docs=false: gitignore was modified");
1178
+ // Should add .gsd to gitignore (external state dir is a symlink)
1179
+ const modified = ensureGitignore(repo);
1180
+ assertTrue(modified, "ensureGitignore: gitignore was modified");
1181
1181
 
1182
1182
  const { readFileSync } = await import("node:fs");
1183
1183
  const content = readFileSync(join(repo, ".gitignore"), "utf-8");
1184
- assertTrue(content.includes(".gsd/"), "commit_docs=false: .gitignore contains blanket .gsd/");
1185
- assertTrue(content.includes("commit_docs: false"), "commit_docs=false: .gitignore contains explanatory comment");
1186
-
1187
- // Should NOT contain individual runtime patterns (those are subsumed by blanket .gsd/)
1188
- // But it's OK if it does — the blanket .gsd/ covers everything
1184
+ const lines = content.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("#"));
1185
+ assertTrue(lines.includes(".gsd"), "ensureGitignore: .gitignore contains .gsd");
1189
1186
 
1190
1187
  // Idempotent — calling again doesn't add duplicates
1191
- const modified2 = ensureGitignore(repo, { commitDocs: false });
1192
- assertTrue(!modified2, "commit_docs=false: second call is idempotent");
1193
-
1194
- rmSync(repo, { recursive: true, force: true });
1195
- }
1196
-
1197
- // ─── ensureGitignore: commit_docs true removes blanket .gsd/ ────────
1198
-
1199
- console.log("\n=== ensureGitignore: commit_docs true self-heals ===");
1200
-
1201
- {
1202
- const { ensureGitignore } = await import("../gitignore.ts");
1203
- const repo = mkdtempSync(join(tmpdir(), "gsd-gitignore-selfheal-"));
1204
-
1205
- // Start with a gitignore that has a blanket .gsd/ (e.g., user switched setting)
1206
- writeFileSync(join(repo, ".gitignore"), ".gsd/\n");
1207
-
1208
- const modified = ensureGitignore(repo, { commitDocs: true });
1209
- assertTrue(modified, "commit_docs=true: gitignore was modified");
1210
-
1211
- const { readFileSync } = await import("node:fs");
1212
- const content = readFileSync(join(repo, ".gitignore"), "utf-8");
1213
- // Blanket .gsd/ should be removed
1214
- const lines = content.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("#"));
1215
- assertTrue(!lines.includes(".gsd/"), "commit_docs=true: blanket .gsd/ was removed");
1216
- assertTrue(!lines.includes(".gsd"), "commit_docs=true: blanket .gsd was removed");
1188
+ const modified2 = ensureGitignore(repo);
1189
+ assertTrue(!modified2, "ensureGitignore: second call is idempotent");
1217
1190
 
1218
1191
  rmSync(repo, { recursive: true, force: true });
1219
1192
  }
@@ -10,7 +10,7 @@
10
10
 
11
11
  import test from 'node:test';
12
12
  import assert from 'node:assert/strict';
13
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
13
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, realpathSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
  import { tmpdir } from 'node:os';
16
16
  import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
@@ -27,7 +27,7 @@ test('knowledge: KNOWLEDGE key exists in GSD_ROOT_FILES', () => {
27
27
  // ─── resolveGsdRootFile resolves KNOWLEDGE.md ───────────────────────────────
28
28
 
29
29
  test('knowledge: resolveGsdRootFile returns canonical path when KNOWLEDGE.md exists', () => {
30
- const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
30
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
31
31
  const gsdDir = join(tmp, '.gsd');
32
32
  mkdirSync(gsdDir, { recursive: true });
33
33
  writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), '# Project Knowledge\n');
@@ -39,7 +39,7 @@ test('knowledge: resolveGsdRootFile returns canonical path when KNOWLEDGE.md exi
39
39
  });
40
40
 
41
41
  test('knowledge: resolveGsdRootFile resolves when legacy knowledge.md exists', () => {
42
- const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
42
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
43
43
  const gsdDir = join(tmp, '.gsd');
44
44
  mkdirSync(gsdDir, { recursive: true });
45
45
  writeFileSync(join(gsdDir, 'knowledge.md'), '# Project Knowledge\n');
@@ -58,7 +58,7 @@ test('knowledge: resolveGsdRootFile resolves when legacy knowledge.md exists', (
58
58
  });
59
59
 
60
60
  test('knowledge: resolveGsdRootFile returns canonical path when file does not exist', () => {
61
- const tmp = mkdtempSync(join(tmpdir(), 'gsd-knowledge-'));
61
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
62
62
  const gsdDir = join(tmp, '.gsd');
63
63
  mkdirSync(gsdDir, { recursive: true });
64
64
 
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Mechanical Completion — unit tests (ADR-003).
3
+ *
4
+ * Tests deterministic slice/milestone completion using fixture data.
5
+ * Uses node:test + node:assert for consistency with token-profile.test.ts.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { randomBytes } from "node:crypto";
14
+
15
+ // ─── Fixture Helpers ──────────────────────────────────────────────────────────
16
+
17
+ function createTmpBase(): string {
18
+ const base = join(tmpdir(), `gsd-mech-test-${randomBytes(4).toString("hex")}`);
19
+ mkdirSync(base, { recursive: true });
20
+ return base;
21
+ }
22
+
23
+ function scaffold(base: string, mid: string, sid: string, taskSummaries: Array<{ tid: string; content: string }>) {
24
+ const gsdRoot = join(base, ".gsd");
25
+ const mDir = join(gsdRoot, "milestones", mid);
26
+ const sDir = join(mDir, "slices", sid);
27
+ const tDir = join(sDir, "tasks");
28
+ mkdirSync(tDir, { recursive: true });
29
+
30
+ for (const { tid, content } of taskSummaries) {
31
+ writeFileSync(join(tDir, `${tid}-SUMMARY.md`), content, "utf-8");
32
+ }
33
+
34
+ return { gsdRoot, mDir, sDir, tDir };
35
+ }
36
+
37
+ function makeTaskSummary(tid: string, opts: {
38
+ oneLiner?: string;
39
+ provides?: string[];
40
+ key_files?: string[];
41
+ key_decisions?: string[];
42
+ verification_result?: string;
43
+ }): string {
44
+ const lines: string[] = [
45
+ "---",
46
+ `id: ${tid}`,
47
+ `parent: S01`,
48
+ `milestone: M001`,
49
+ ];
50
+ if (opts.provides?.length) lines.push(`provides:\n${opts.provides.map(p => ` - ${p}`).join("\n")}`);
51
+ if (opts.key_files?.length) lines.push(`key_files:\n${opts.key_files.map(f => ` - ${f}`).join("\n")}`);
52
+ if (opts.key_decisions?.length) lines.push(`key_decisions:\n${opts.key_decisions.map(d => ` - ${d}`).join("\n")}`);
53
+ lines.push(`verification_result: ${opts.verification_result ?? "passed"}`);
54
+ lines.push("---");
55
+ lines.push("");
56
+ lines.push(`# ${tid}: Test Task`);
57
+ lines.push("");
58
+ if (opts.oneLiner) lines.push(`**${opts.oneLiner}**`);
59
+ lines.push("");
60
+ lines.push("## What Happened");
61
+ lines.push("");
62
+ lines.push(`Implemented the feature described in ${tid}. This was a significant change that modified multiple files across the codebase to support the new functionality.`);
63
+ lines.push("");
64
+ return lines.join("\n");
65
+ }
66
+
67
+ // ─── Source-level structural tests ────────────────────────────────────────────
68
+
69
+ const mechanicalSrc = readFileSync(
70
+ join(import.meta.dirname!, "..", "mechanical-completion.ts"),
71
+ "utf-8",
72
+ );
73
+
74
+ test("mechanical-completion: exports mechanicalSliceCompletion", () => {
75
+ assert.ok(
76
+ mechanicalSrc.includes("export async function mechanicalSliceCompletion"),
77
+ "should export mechanicalSliceCompletion",
78
+ );
79
+ });
80
+
81
+ test("mechanical-completion: exports aggregateMilestoneVerification", () => {
82
+ assert.ok(
83
+ mechanicalSrc.includes("export async function aggregateMilestoneVerification"),
84
+ "should export aggregateMilestoneVerification",
85
+ );
86
+ });
87
+
88
+ test("mechanical-completion: exports generateMilestoneSummary", () => {
89
+ assert.ok(
90
+ mechanicalSrc.includes("export async function generateMilestoneSummary"),
91
+ "should export generateMilestoneSummary",
92
+ );
93
+ });
94
+
95
+ test("mechanical-completion: exports appendNewDecisions", () => {
96
+ assert.ok(
97
+ mechanicalSrc.includes("export async function appendNewDecisions"),
98
+ "should export appendNewDecisions",
99
+ );
100
+ });
101
+
102
+ test("mechanical-completion: uses atomicWriteSync for file writes", () => {
103
+ assert.ok(
104
+ mechanicalSrc.includes("atomicWriteSync"),
105
+ "should use atomicWriteSync for safe file writes",
106
+ );
107
+ });
108
+
109
+ test("mechanical-completion: quality gate checks summary length for multi-task slices", () => {
110
+ assert.ok(
111
+ mechanicalSrc.includes("totalContent.length < 200"),
112
+ "should have quality gate for summary content length",
113
+ );
114
+ });
115
+
116
+ test("mechanical-completion: marks slice [x] in roadmap", () => {
117
+ assert.ok(
118
+ mechanicalSrc.includes("markSliceInRoadmap"),
119
+ "should mark slice done in roadmap",
120
+ );
121
+ });
122
+
123
+ test("mechanical-completion: aggregates VERIFY.json files for milestone validation", () => {
124
+ assert.ok(
125
+ mechanicalSrc.includes("resolveTaskJsonFiles") && mechanicalSrc.includes("VERIFY"),
126
+ "should read VERIFY.json files for milestone validation",
127
+ );
128
+ });
129
+
130
+ test("mechanical-completion: deduplicates decisions against existing DECISIONS.md", () => {
131
+ assert.ok(
132
+ mechanicalSrc.includes("existing.includes(d.trim())"),
133
+ "should deduplicate decisions against existing content",
134
+ );
135
+ });
136
+
137
+ test("mechanical-completion: produces VALIDATION.md with verdict frontmatter", () => {
138
+ assert.ok(
139
+ mechanicalSrc.includes("verdict:") && mechanicalSrc.includes("remediation_round: 0"),
140
+ "VALIDATION.md should have verdict and remediation_round frontmatter",
141
+ );
142
+ });
143
+
144
+ // ─── Integration tests with fixture data ──────────────────────────────────────
145
+
146
+ test("mechanical: slice completion with 2 task summaries produces SUMMARY.md", async () => {
147
+ const base = createTmpBase();
148
+ try {
149
+ const mid = "M001";
150
+ const sid = "S01";
151
+
152
+ // Scaffold task summaries
153
+ scaffold(base, mid, sid, [
154
+ {
155
+ tid: "T01",
156
+ content: makeTaskSummary("T01", {
157
+ oneLiner: "Set up project structure",
158
+ provides: ["project-scaffold"],
159
+ key_files: ["src/index.ts", "package.json"],
160
+ verification_result: "passed",
161
+ }),
162
+ },
163
+ {
164
+ tid: "T02",
165
+ content: makeTaskSummary("T02", {
166
+ oneLiner: "Add core API endpoints",
167
+ provides: ["api-endpoints"],
168
+ key_files: ["src/api.ts"],
169
+ key_decisions: ["Used Express over Fastify"],
170
+ verification_result: "passed",
171
+ }),
172
+ },
173
+ ]);
174
+
175
+ // Write a roadmap with the slice unchecked
176
+ const roadmapPath = join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`);
177
+ writeFileSync(roadmapPath, `# Roadmap\n\n- [ ] **${sid}: First Slice**\n`, "utf-8");
178
+
179
+ // Write a slice plan with Verification section
180
+ const planPath = join(base, ".gsd", "milestones", mid, "slices", sid, `${sid}-PLAN.md`);
181
+ writeFileSync(planPath, `# Plan\n\n## Verification\n\n- Run \`npm test\`\n- Check output\n`, "utf-8");
182
+
183
+ // Dynamic import to get the actual module
184
+ const { mechanicalSliceCompletion } = await import("../mechanical-completion.js");
185
+ const ok = await mechanicalSliceCompletion(base, mid, sid);
186
+
187
+ assert.ok(ok, "should return true for valid slice completion");
188
+
189
+ // Check SUMMARY.md was written
190
+ const summaryPath = join(base, ".gsd", "milestones", mid, "slices", sid, `${sid}-SUMMARY.md`);
191
+ assert.ok(existsSync(summaryPath), "SUMMARY.md should exist");
192
+
193
+ const summaryContent = readFileSync(summaryPath, "utf-8");
194
+ assert.ok(summaryContent.includes("T01"), "summary should reference T01");
195
+ assert.ok(summaryContent.includes("T02"), "summary should reference T02");
196
+ assert.ok(summaryContent.includes("verification_result: passed"), "should have passed verification");
197
+
198
+ // Check roadmap was updated
199
+ const updatedRoadmap = readFileSync(roadmapPath, "utf-8");
200
+ assert.ok(updatedRoadmap.includes("[x]"), "roadmap should have [x] checkbox");
201
+
202
+ // Check UAT was written
203
+ const uatPath = join(base, ".gsd", "milestones", mid, "slices", sid, `${sid}-UAT.md`);
204
+ assert.ok(existsSync(uatPath), "UAT.md should exist");
205
+ const uatContent = readFileSync(uatPath, "utf-8");
206
+ assert.ok(uatContent.includes("npm test"), "UAT should contain verification content");
207
+ } finally {
208
+ rmSync(base, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ test("mechanical: returns false for empty task summaries", async () => {
213
+ const base = createTmpBase();
214
+ try {
215
+ const mid = "M001";
216
+ const sid = "S01";
217
+ scaffold(base, mid, sid, []);
218
+
219
+ const { mechanicalSliceCompletion } = await import("../mechanical-completion.js");
220
+ const ok = await mechanicalSliceCompletion(base, mid, sid);
221
+ assert.ok(!ok, "should return false when no summaries exist");
222
+ } finally {
223
+ rmSync(base, { recursive: true, force: true });
224
+ }
225
+ });
226
+
227
+ test("mechanical: returns false for insufficient summary content in multi-task slice", async () => {
228
+ const base = createTmpBase();
229
+ try {
230
+ const mid = "M001";
231
+ const sid = "S01";
232
+
233
+ // Two tasks but with very short content (under 200 chars)
234
+ scaffold(base, mid, sid, [
235
+ { tid: "T01", content: "---\nid: T01\nparent: S01\nmilestone: M001\n---\n\n# T01: A\n\n**Short**\n" },
236
+ { tid: "T02", content: "---\nid: T02\nparent: S01\nmilestone: M001\n---\n\n# T02: B\n\n**Brief**\n" },
237
+ ]);
238
+
239
+ const { mechanicalSliceCompletion } = await import("../mechanical-completion.js");
240
+ const ok = await mechanicalSliceCompletion(base, mid, sid);
241
+ assert.ok(!ok, "should return false when summaries are too short");
242
+ } finally {
243
+ rmSync(base, { recursive: true, force: true });
244
+ }
245
+ });
246
+
247
+ test("mechanical: milestone verification aggregates VERIFY.json files", async () => {
248
+ const base = createTmpBase();
249
+ try {
250
+ const mid = "M001";
251
+ const sid = "S01";
252
+ const { tDir } = scaffold(base, mid, sid, []);
253
+
254
+ // Write VERIFY.json files
255
+ const evidence = {
256
+ schemaVersion: 1,
257
+ taskId: "T01",
258
+ unitId: "M001/S01/T01",
259
+ timestamp: Date.now(),
260
+ passed: true,
261
+ discoverySource: "plan",
262
+ checks: [
263
+ { command: "npm test", exitCode: 0, durationMs: 1500, verdict: "pass", blocking: true },
264
+ ],
265
+ };
266
+ writeFileSync(join(tDir, "T01-VERIFY.json"), JSON.stringify(evidence), "utf-8");
267
+
268
+ const evidence2 = { ...evidence, taskId: "T02", passed: false, checks: [
269
+ { command: "npm test", exitCode: 1, durationMs: 500, verdict: "fail", blocking: true },
270
+ ]};
271
+ writeFileSync(join(tDir, "T02-VERIFY.json"), JSON.stringify(evidence2), "utf-8");
272
+
273
+ const { aggregateMilestoneVerification } = await import("../mechanical-completion.js");
274
+ const result = await aggregateMilestoneVerification(base, mid);
275
+
276
+ assert.equal(result.verdict, "mixed", "should be mixed when some pass and some fail");
277
+ assert.equal(result.checks.length, 2, "should have 2 checks");
278
+
279
+ // Check VALIDATION.md was written
280
+ const validationPath = join(base, ".gsd", "milestones", mid, `${mid}-VALIDATION.md`);
281
+ assert.ok(existsSync(validationPath), "VALIDATION.md should exist");
282
+ const validationContent = readFileSync(validationPath, "utf-8");
283
+ assert.ok(validationContent.includes("verdict: mixed"), "should have mixed verdict in frontmatter");
284
+ } finally {
285
+ rmSync(base, { recursive: true, force: true });
286
+ }
287
+ });
288
+
289
+ test("mechanical: milestone summary aggregates slice summaries", async () => {
290
+ const base = createTmpBase();
291
+ try {
292
+ const mid = "M001";
293
+
294
+ // Create two slices with summaries
295
+ for (const sid of ["S01", "S02"]) {
296
+ const sDir = join(base, ".gsd", "milestones", mid, "slices", sid);
297
+ mkdirSync(sDir, { recursive: true });
298
+ writeFileSync(
299
+ join(sDir, `${sid}-SUMMARY.md`),
300
+ `---\nid: ${sid}\nprovides:\n - feature-${sid.toLowerCase()}\nkey_files:\n - src/${sid.toLowerCase()}.ts\n---\n\n# ${sid}: Slice\n\n**${sid} implemented**\n`,
301
+ "utf-8",
302
+ );
303
+ }
304
+
305
+ const { generateMilestoneSummary } = await import("../mechanical-completion.js");
306
+ const content = await generateMilestoneSummary(base, mid);
307
+
308
+ assert.ok(content.includes("S01"), "should reference S01");
309
+ assert.ok(content.includes("S02"), "should reference S02");
310
+ assert.ok(content.includes("feature-s01"), "should aggregate provides");
311
+ assert.ok(content.includes("feature-s02"), "should aggregate provides");
312
+
313
+ const summaryPath = join(base, ".gsd", "milestones", mid, `${mid}-SUMMARY.md`);
314
+ assert.ok(existsSync(summaryPath), "M##-SUMMARY.md should exist");
315
+ } finally {
316
+ rmSync(base, { recursive: true, force: true });
317
+ }
318
+ });
319
+
320
+ test("mechanical: decision deduplication skips existing decisions", async () => {
321
+ const base = createTmpBase();
322
+ try {
323
+ const gsdRoot = join(base, ".gsd");
324
+ mkdirSync(gsdRoot, { recursive: true });
325
+
326
+ // Write existing decisions
327
+ const decisionsPath = join(gsdRoot, "DECISIONS.md");
328
+ writeFileSync(decisionsPath, "# Decisions\n\n- Used TypeScript for type safety\n", "utf-8");
329
+
330
+ const { appendNewDecisions } = await import("../mechanical-completion.js");
331
+
332
+ // Call with one existing and one new decision
333
+ const mockSummaries = [
334
+ {
335
+ frontmatter: {
336
+ id: "T01", parent: "S01", milestone: "M001",
337
+ provides: [], requires: [], affects: [],
338
+ key_files: [], key_decisions: ["Used TypeScript for type safety", "Chose Express over Koa"],
339
+ patterns_established: [], drill_down_paths: [], observability_surfaces: [],
340
+ duration: "", verification_result: "passed", completed_at: "", blocker_discovered: false,
341
+ },
342
+ title: "T01", oneLiner: "", whatHappened: "", deviations: "", filesModified: [],
343
+ },
344
+ ];
345
+
346
+ await appendNewDecisions(base, mockSummaries as any);
347
+
348
+ const updated = readFileSync(decisionsPath, "utf-8");
349
+ assert.ok(updated.includes("Chose Express over Koa"), "should append new decision");
350
+ // The existing decision should not be duplicated
351
+ const matches = updated.match(/Used TypeScript for type safety/g);
352
+ assert.equal(matches?.length, 1, "should not duplicate existing decision");
353
+ } finally {
354
+ rmSync(base, { recursive: true, force: true });
355
+ }
356
+ });
@@ -25,6 +25,7 @@ const BASE_VARS = {
25
25
  outputPath: "/tmp/test-project/.gsd/milestones/M001/slices/S01/S01-PLAN.md",
26
26
  inlinedContext: "--- test inlined context ---",
27
27
  dependencySummaries: "", executorContextConstraints: "",
28
+ sourceFilePaths: "- **Requirements**: `.gsd/REQUIREMENTS.md`",
28
29
  };
29
30
 
30
31
  test("plan-slice prompt: commit step present when commit_docs=true", () => {
@@ -53,6 +53,7 @@ test("types: PhaseSkipPreferences interface exported", () => {
53
53
  assert.ok(typesSrc.includes("skip_research"), "should include skip_research");
54
54
  assert.ok(typesSrc.includes("skip_reassess"), "should include skip_reassess");
55
55
  assert.ok(typesSrc.includes("skip_slice_research"), "should include skip_slice_research");
56
+ assert.ok(typesSrc.includes("reassess_after_slice"), "should include reassess_after_slice");
56
57
  });
57
58
 
58
59
  // ═══════════════════════════════════════════════════════════════════════════
@@ -113,24 +114,21 @@ test("profile: budget profile sets phase skips to true", () => {
113
114
  assert.ok(budgetBlock.includes("skip_slice_research: true"), "budget should skip slice research");
114
115
  });
115
116
 
116
- test("profile: balanced profile skips only slice research", () => {
117
+ test("profile: balanced profile skips research, reassess, and slice research (ADR-003)", () => {
117
118
  const balancedIdx = preferencesSrc.indexOf('case "balanced":');
118
119
  const qualityIdx = preferencesSrc.indexOf('case "quality":');
119
120
  const balancedBlock = preferencesSrc.slice(balancedIdx, qualityIdx);
120
121
  assert.ok(balancedBlock.includes("skip_slice_research: true"), "balanced should skip slice research");
121
- assert.ok(!balancedBlock.includes("skip_research: true"), "balanced should NOT skip milestone research");
122
- assert.ok(!balancedBlock.includes("skip_reassess: true"), "balanced should NOT skip reassess");
122
+ assert.ok(balancedBlock.includes("skip_research: true"), "balanced should skip milestone research");
123
+ assert.ok(balancedBlock.includes("skip_reassess: true"), "balanced should skip reassess");
123
124
  });
124
125
 
125
- test("profile: quality profile has empty phases (no skips)", () => {
126
+ test("profile: quality profile skips research, slice research, and reassess (ADR-003)", () => {
126
127
  const qualityIdx = preferencesSrc.indexOf('case "quality":');
127
- const qualityEnd = preferencesSrc.indexOf("}", qualityIdx + 50);
128
- // Look for the return block after case "quality":
129
- const qualityReturn = preferencesSrc.slice(qualityIdx, qualityIdx + 200);
130
- assert.ok(
131
- qualityReturn.includes("phases: {}"),
132
- "quality should have empty phases object (no skips)",
133
- );
128
+ const qualityBlock = preferencesSrc.slice(qualityIdx, qualityIdx + 300);
129
+ assert.ok(qualityBlock.includes("skip_research: true"), "quality should skip research");
130
+ assert.ok(qualityBlock.includes("skip_slice_research: true"), "quality should skip slice research");
131
+ assert.ok(qualityBlock.includes("skip_reassess: true"), "quality should skip reassess");
134
132
  });
135
133
 
136
134
  // ═══════════════════════════════════════════════════════════════════════════
@@ -253,10 +251,10 @@ test("dispatch: research-slice rule has skip guards", () => {
253
251
  );
254
252
  });
255
253
 
256
- test("dispatch: reassess-roadmap rule has skip_reassess guard", () => {
254
+ test("dispatch: reassess-roadmap rule has reassess_after_slice opt-in guard (ADR-003)", () => {
257
255
  assert.ok(
258
- dispatchSrc.includes("skip_reassess") && dispatchSrc.includes("reassess-roadmap"),
259
- "reassess-roadmap dispatch rule should check phases.skip_reassess",
256
+ dispatchSrc.includes("reassess_after_slice") && dispatchSrc.includes("reassess-roadmap"),
257
+ "reassess-roadmap dispatch rule should check phases.reassess_after_slice",
260
258
  );
261
259
  });
262
260
 
@@ -265,6 +263,6 @@ test("dispatch: phase skip guards return null (not stop)", () => {
265
263
  const researchGuard = dispatchSrc.match(/skip_research\).*?return null/s);
266
264
  assert.ok(researchGuard, "skip_research guard should return null (fall-through)");
267
265
 
268
- const reassessGuard = dispatchSrc.match(/skip_reassess\).*?return null/s);
269
- assert.ok(reassessGuard, "skip_reassess guard should return null (fall-through)");
266
+ const reassessGuard = dispatchSrc.match(/reassess_after_slice\).*?return null/s);
267
+ assert.ok(reassessGuard, "reassess_after_slice guard should return null (fall-through)");
270
268
  });
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
+ import { gsdRoot } from "./paths.js";
15
16
  import type { Classification, CaptureEntry } from "./captures.js";
16
17
  import {
17
18
  loadPendingCaptures,
@@ -36,7 +37,7 @@ export function executeInject(
36
37
  ): string | null {
37
38
  try {
38
39
  // Resolve the plan file path
39
- const planPath = join(basePath, ".gsd", "milestones", mid, "slices", sid, `${sid}-PLAN.md`);
40
+ const planPath = join(gsdRoot(basePath), "milestones", mid, "slices", sid, `${sid}-PLAN.md`);
40
41
  if (!existsSync(planPath)) return null;
41
42
 
42
43
  const content = readFileSync(planPath, "utf-8");
@@ -304,6 +304,8 @@ export interface PhaseSkipPreferences {
304
304
  skip_reassess?: boolean;
305
305
  skip_slice_research?: boolean;
306
306
  skip_milestone_validation?: boolean;
307
+ /** When true, reassess-roadmap fires after each slice completion. Opt-in. */
308
+ reassess_after_slice?: boolean;
307
309
  /** When true, auto-mode pauses before each slice for discussion (#789). */
308
310
  require_slice_discussion?: boolean;
309
311
  }
@@ -50,7 +50,7 @@ export function getWorktreeOriginalCwd(): string | null {
50
50
  export function getActiveWorktreeName(): string | null {
51
51
  if (!originalCwd) return null;
52
52
  const cwd = process.cwd();
53
- const wtDir = join(originalCwd, ".gsd", "worktrees");
53
+ const wtDir = join(gsdRoot(originalCwd), "worktrees");
54
54
  if (!cwd.startsWith(wtDir)) return null;
55
55
  const rel = cwd.slice(wtDir.length + 1);
56
56
  const name = rel.split("/")[0] ?? rel.split("\\")[0];
@@ -633,16 +633,6 @@ async function handleMerge(
633
633
  const commitType = inferCommitType(name);
634
634
  const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
635
635
 
636
- // Reconcile worktree DB into main DB before squash merge
637
- const wtDbPath = join(worktreePath(basePath, name), ".gsd", "gsd.db");
638
- const mainDbPath = join(basePath, ".gsd", "gsd.db");
639
- if (existsSync(wtDbPath) && existsSync(mainDbPath)) {
640
- try {
641
- const { reconcileWorktreeDb } = await import("./gsd-db.js");
642
- reconcileWorktreeDb(mainDbPath, wtDbPath);
643
- } catch { /* non-fatal */ }
644
- }
645
-
646
636
  try {
647
637
  mergeWorktreeToMain(basePath, name, commitMessage);
648
638
  ctx.ui.notify(
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
19
19
  import { join, resolve, sep } from "node:path";
20
+ import { gsdRoot } from "./paths.js";
20
21
  import { GSDError, GSD_PARSE_ERROR, GSD_STALE_STATE, GSD_LOCK_HELD, GSD_GIT_ERROR, GSD_MERGE_CONFLICT } from "./errors.js";
21
22
  import {
22
23
  nativeBranchDelete,
@@ -100,7 +101,7 @@ export function resolveGitDir(basePath: string): string {
100
101
  }
101
102
 
102
103
  export function worktreesDir(basePath: string): string {
103
- return join(basePath, ".gsd", "worktrees");
104
+ return join(gsdRoot(basePath), "worktrees");
104
105
  }
105
106
 
106
107
  export function worktreePath(basePath: string, name: string): string {
@@ -193,7 +194,7 @@ export function listWorktrees(basePath: string): WorktreeInfo[] {
193
194
  const seenRoots = new Set<string>();
194
195
  const worktreeRoots = baseVariants
195
196
  .map(baseVariant => {
196
- const path = join(baseVariant, ".gsd", "worktrees");
197
+ const path = join(gsdRoot(baseVariant), "worktrees");
197
198
  return {
198
199
  normalized: normalizePathForComparison(path),
199
200
  };