gsd-pi 2.80.0-dev.c5c38454b → 2.80.0-dev.f55d16d13

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 (77) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/GSD-WORKFLOW.md +2 -2
  3. package/dist/resources/extensions/gsd/auto/phases.js +37 -30
  4. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
  5. package/dist/resources/extensions/gsd/auto-prompts.js +111 -1
  6. package/dist/resources/extensions/gsd/auto.js +9 -1
  7. package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
  8. package/dist/resources/extensions/gsd/detection.js +106 -0
  9. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  10. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  11. package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
  12. package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
  13. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  14. package/dist/web/standalone/.next/BUILD_ID +1 -1
  15. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  16. package/dist/web/standalone/.next/build-manifest.json +2 -2
  17. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  18. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.html +1 -1
  35. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  42. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  44. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  45. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  46. package/package.json +1 -1
  47. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +30 -0
  48. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +2 -0
  51. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  52. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +36 -0
  53. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +2 -0
  54. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  55. package/src/resources/GSD-WORKFLOW.md +2 -2
  56. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  57. package/src/resources/extensions/gsd/auto/phases.ts +42 -28
  58. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
  59. package/src/resources/extensions/gsd/auto-prompts.ts +116 -1
  60. package/src/resources/extensions/gsd/auto.ts +12 -1
  61. package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
  62. package/src/resources/extensions/gsd/detection.ts +128 -0
  63. package/src/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  64. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  65. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
  66. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -1
  67. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
  68. package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
  69. package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
  70. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
  71. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +46 -2
  72. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
  73. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
  74. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
  75. package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
  76. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → mPZbi5BH9dwokaPZlrYuQ}/_buildManifest.js +0 -0
  77. /package/dist/web/standalone/.next/static/{TCSim36ZpcPu2WgeoC45g → mPZbi5BH9dwokaPZlrYuQ}/_ssgManifest.js +0 -0
@@ -16,15 +16,14 @@ Start with what the excerpts give you. Read full files when the section heads si
16
16
 
17
17
  **On-demand Read ordering:** Complete all slice SUMMARY Reads you need for cross-slice synthesis, the Decision Re-evaluation table, and LEARNINGS **before** calling `gsd_complete_milestone` (step 12). Once that tool runs, the milestone is marked complete in the DB, so it must be the final persistent milestone-closeout write.
18
18
 
19
- ### Delegate Review Work
19
+ ### Closeout Review Mode
20
20
 
21
- Use `subagent` for review work needing fresh context, before drafting LEARNINGS:
21
+ The inlined context includes a validation status block.
22
22
 
23
- - Cross-slice integrations or new public APIs -> **reviewer** with milestone diff and roadmap.
24
- - Auth, network, parsing, file IO, shell exec, or crypto -> **security** audit.
25
- - Significant tests added or changed -> **tester** coverage check against success criteria.
23
+ - If it says a passing validation artifact is present, treat that artifact as authoritative for success criteria, requirement coverage, verification classes, and cross-slice integration. Do not delegate fresh reviewer/security/tester audits unless the validation artifact is internally inconsistent with the inlined summaries.
24
+ - If validation is missing, stale, non-pass, or internally inconsistent, use `subagent` for review work needing fresh context before drafting LEARNINGS: cross-slice integrations or new public APIs -> **reviewer**; auth, network, parsing, file IO, shell exec, or crypto -> **security**; significant tests added or changed -> **tester**.
26
25
 
27
- Subagents report only; they do not write user source. Fold findings into Decision Re-evaluation and LEARNINGS before completion.
26
+ Subagents report only; they do not write user source. Fold any findings into Decision Re-evaluation and LEARNINGS before completion.
28
27
 
29
28
  {{inlinedContext}}
30
29
 
@@ -33,8 +32,8 @@ Subagents report only; they do not write user source. Fold findings into Decisio
33
32
  1. Use the **Milestone Summary** output template from the inlined context above
34
33
  2. {{skillActivation}}
35
34
  3. **Verify code changes exist.** Compare milestone work against the integration branch (`main`, `master`, or recorded branch), using merge-base as older revision and `HEAD` as newer. If the diff lists non-`.gsd/` files, pass. If `HEAD` equals the integration branch/merge-base, treat it as a self-diff retry: inspect milestone-scoped commit evidence (`GSD-Unit: {{milestoneId}}` or production `GSD-Task: Sxx/Tyy` trailers touching `.gsd/milestones/{{milestoneId}}/`) and verify those commits touched non-`.gsd/` files. Record **verification failure** only when neither source shows implementation files.
36
- 4. Verify every **success criterion** from `{{roadmapPath}}` with evidence from summaries, tests, or observable behavior. Record unmet criteria as **verification failure**.
37
- 5. Verify **definition of done**: all slices `[x]`, summaries exist, and integrations work. Record unmet items as **verification failure**.
35
+ 4. Verify every **success criterion** from `{{roadmapPath}}`. If passing validation is present, summarize the validation evidence instead of re-auditing it; otherwise verify with evidence from summaries, tests, or observable behavior. Record unmet criteria as **verification failure**.
36
+ 5. Verify **definition of done**: all slices `[x]`, summaries exist, and integrations work. If passing validation is present, trust its integration/verification verdict unless inconsistent with current artifacts. Record unmet items as **verification failure**.
38
37
  6. If the roadmap includes a **Horizontal Checklist**, verify each item and note unchecked items in the summary.
39
38
  7. Fill the **Decision Re-evaluation** table: compare each key `.gsd/DECISIONS.md` decision from this milestone with what shipped, and flag decisions to revisit.
40
39
  8. Validate **requirement status transitions**. For each changed requirement, confirm evidence supports the new status. Requirements may move between Active, Validated, Deferred, Blocked, or Out of Scope only with proof.
@@ -48,7 +48,7 @@ Narrate decomposition reasoning in complete sentences: grouping, risk order, ver
48
48
  Then:
49
49
  1. Use the **Roadmap** output template from the inlined context above
50
50
  2. {{skillActivation}}
51
- 3. Create only as many demoable vertical slices as the work genuinely needs.
51
+ 3. Create only as many demoable vertical slices as the work genuinely needs. Use 1-10 slices, sized to the work; tiny/single-file/static work should usually be one slice.
52
52
  4. Order by risk, high-risk first.
53
53
  5. Call `gsd_plan_milestone` to persist milestone fields, slice rows, and **Horizontal Checklist** through the DB-backed path. Fill checklist concerns considered during planning: requirements, decisions, shutdown, revenue, auth, shared resources, reconnection. Omit for trivial milestones. Do **not** write `{{outputPath}}`, `ROADMAP.md`, or other planning artifacts manually; the tool owns rendering and persistence.
54
54
  6. If planning produced structural decisions (slice ordering, technology choices, scope exclusions), call `gsd_decision_save` for each; the tool assigns IDs and regenerates `.gsd/DECISIONS.md`.
@@ -78,6 +78,8 @@ Apply these when decomposing and ordering slices:
78
78
  - Ship features, not proofs; use clearly marked realistic stubs only when necessary.
79
79
  - **Dependency format is comma-separated, never range syntax.** Write `depends:[S01,S02,S03]`, not `depends:[S01-S03]`.
80
80
  - Roadmap ambition must match the milestone; right-size decomposition.
81
+ - Missing ecosystem markers are not a reason to over-plan. If Project Classification says `untyped-existing`, treat the listed content files as the project surface and use generic file-level workflow guidance.
82
+ - For `untyped-existing` projects with 1-2 content files, prefer exactly one slice unless the request clearly spans multiple independent user-visible capabilities. For 3-5 content files, prefer 1-2 slices.
81
83
 
82
84
  ## Progressive Planning (ADR-011)
83
85
 
@@ -50,6 +50,15 @@ export interface FileEditEvidence {
50
50
 
51
51
  export type EvidenceEntry = BashEvidence | FileWriteEvidence | FileEditEvidence;
52
52
 
53
+ const EXECUTION_TOOL_NAMES = new Set([
54
+ "bash",
55
+ "Bash",
56
+ "gsd_exec",
57
+ "gsd_exec_search",
58
+ "mcp__gsd-workflow__gsd_exec",
59
+ "mcp__gsd-workflow__gsd_exec_search",
60
+ ]);
61
+
53
62
  // ─── Module State ───────────────────────────────────────────────────────────
54
63
 
55
64
  let unitEvidence: EvidenceEntry[] = [];
@@ -188,11 +197,11 @@ export function clearEvidenceFromDisk(
188
197
  * Exit codes and output are filled in by recordToolResult after execution.
189
198
  */
190
199
  export function recordToolCall(toolCallId: string, toolName: string, input: Record<string, unknown>): void {
191
- if (toolName === "bash" || toolName === "Bash") {
200
+ if (EXECUTION_TOOL_NAMES.has(toolName)) {
192
201
  unitEvidence.push({
193
202
  kind: "bash",
194
203
  toolCallId,
195
- command: String(input.command ?? ""),
204
+ command: String(input.command ?? input.cmd ?? input.query ?? ""),
196
205
  exitCode: -1,
197
206
  outputSnippet: "",
198
207
  timestamp: Date.now(),
@@ -2556,7 +2556,7 @@ test("autoLoop warns but proceeds for greenfield project (no project files) (#18
2556
2556
  "should not stop with health check failure for greenfield project",
2557
2557
  );
2558
2558
  const greenfieldWarning = notifications.find(
2559
- (n) => n.includes("no recognized project files") && n.includes("greenfield"),
2559
+ (n) => n.includes("no project content yet") && n.includes("greenfield"),
2560
2560
  );
2561
2561
  assert.ok(
2562
2562
  greenfieldWarning,
@@ -131,7 +131,7 @@ test("postflightPopStash — restores stashed changes and emits info notificatio
131
131
  run('git commit -m "simulate merge"', repo);
132
132
 
133
133
  const postNotifications: Array<{ msg: string; level: string }> = [];
134
- postflightPopStash(repo, "M004", (msg, level) => {
134
+ postflightPopStash(repo, "M004", preflight.stashMarker, (msg, level) => {
135
135
  postNotifications.push({ msg, level });
136
136
  });
137
137
 
@@ -171,7 +171,7 @@ test("preflight + merge + postflight round-trip preserves uncommitted changes",
171
171
  run('git commit -m "feat: add feature"', repo);
172
172
 
173
173
  // Postflight: pop stash
174
- postflightPopStash(repo, "M005", () => {});
174
+ postflightPopStash(repo, "M005", preflight.stashMarker, () => {});
175
175
 
176
176
  // README.md must still have our local content
177
177
  const restored = readFileSync(join(repo, "README.md"), "utf-8");
@@ -184,3 +184,89 @@ test("preflight + merge + postflight round-trip preserves uncommitted changes",
184
184
  try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
185
185
  }
186
186
  });
187
+
188
+ test("postflightPopStash conflict warning names the exact stash ref", () => {
189
+ const repo = createTempRepo();
190
+ try {
191
+ writeFileSync(join(repo, "README.md"), "# local work\n");
192
+ const preflight = preflightCleanRoot(repo, "M005C", () => {});
193
+ assert.equal(preflight.stashPushed, true, "must have stashed");
194
+
195
+ writeFileSync(join(repo, "README.md"), "# merged work\n");
196
+ run("git add README.md", repo);
197
+ run('git commit -m "simulate conflicting merge"', repo);
198
+
199
+ const notifications: Array<{ msg: string; level: string }> = [];
200
+ postflightPopStash(repo, "M005C", preflight.stashMarker, (msg, level) => {
201
+ notifications.push({ msg, level });
202
+ });
203
+
204
+ const warning = notifications.find((n) => n.level === "warning")?.msg ?? "";
205
+ assert.match(warning, /git stash pop stash@\{\d+\}/);
206
+ assert.match(warning, /git stash apply stash@\{\d+\}/);
207
+ } finally {
208
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
209
+ }
210
+ });
211
+
212
+ test("postflightPopStash restores the matching GSD stash, not stash@{0}", () => {
213
+ const repo = createTempRepo();
214
+ try {
215
+ writeFileSync(join(repo, "README.md"), "# target stash\n");
216
+ const preflight = preflightCleanRoot(repo, "M006", () => {});
217
+ assert.equal(preflight.stashPushed, true, "must have stashed target change");
218
+
219
+ writeFileSync(join(repo, "other.txt"), "other stash\n");
220
+ run('git stash push --include-untracked -m "unrelated newer stash"', repo);
221
+
222
+ postflightPopStash(repo, "M006", preflight.stashMarker, () => {});
223
+
224
+ const content = readFileSync(join(repo, "README.md"), "utf-8");
225
+ assert.equal(content.replace(/\r\n/g, "\n"), "# target stash\n");
226
+ const stashList = run("git stash list", repo);
227
+ assert.ok(stashList.includes("unrelated newer stash"), "unrelated newer stash must remain");
228
+ assert.ok(!stashList.includes("gsd-preflight-stash [gsd-preflight-stash:M006"), "target stash should be consumed");
229
+ } finally {
230
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
231
+ }
232
+ });
233
+
234
+ test("postflightPopStash restores the exact preflight marker when another same-milestone stash exists", () => {
235
+ const repo = createTempRepo();
236
+ try {
237
+ writeFileSync(join(repo, "README.md"), "# target stash\n");
238
+ const preflight = preflightCleanRoot(repo, "M007", () => {});
239
+ assert.equal(preflight.stashPushed, true, "must have stashed target change");
240
+ assert.ok(preflight.stashMarker, "preflight must expose exact stash marker");
241
+
242
+ writeFileSync(join(repo, "same-milestone.txt"), "newer same milestone stash\n");
243
+ run('git stash push --include-untracked -m "gsd-preflight-stash [gsd-preflight-stash:M007:other]"', repo);
244
+
245
+ postflightPopStash(repo, "M007", preflight.stashMarker, () => {});
246
+
247
+ const content = readFileSync(join(repo, "README.md"), "utf-8");
248
+ assert.equal(content.replace(/\r\n/g, "\n"), "# target stash\n");
249
+ const stashList = run("git stash list", repo);
250
+ assert.ok(stashList.includes("gsd-preflight-stash:M007:other"), "newer same-milestone stash must remain");
251
+ assert.ok(!stashList.includes(preflight.stashMarker), "exact target stash should be consumed");
252
+ } finally {
253
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
254
+ }
255
+ });
256
+
257
+ test("postflightPopStash falls back to milestone marker prefix when exact marker is unavailable", () => {
258
+ const repo = createTempRepo();
259
+ try {
260
+ writeFileSync(join(repo, "README.md"), "# fallback stash\n");
261
+ run('git stash push --include-untracked -m "gsd-preflight-stash [gsd-preflight-stash:M008:fallback]"', repo);
262
+
263
+ postflightPopStash(repo, "M008", undefined, () => {});
264
+
265
+ const content = readFileSync(join(repo, "README.md"), "utf-8");
266
+ assert.equal(content.replace(/\r\n/g, "\n"), "# fallback stash\n");
267
+ const stashList = run("git stash list", repo);
268
+ assert.ok(!stashList.includes("gsd-preflight-stash:M008:fallback"), "fallback stash should be consumed");
269
+ } finally {
270
+ try { rmSync(repo, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* ignore */ }
271
+ }
272
+ });
@@ -11,12 +11,14 @@
11
11
  import test from "node:test";
12
12
  import assert from "node:assert/strict";
13
13
  import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
14
+ import { execFileSync } from "node:child_process";
14
15
  import { join } from "node:path";
15
16
  import { tmpdir } from "node:os";
16
17
  import {
17
18
  detectProjectState,
18
19
  detectV1Planning,
19
20
  detectProjectSignals,
21
+ classifyProject,
20
22
  scanProjectFiles,
21
23
  } from "../detection.ts";
22
24
 
@@ -37,6 +39,18 @@ function cleanup(dir: string): void {
37
39
  }
38
40
  }
39
41
 
42
+ function git(dir: string, args: string[]): void {
43
+ execFileSync("git", args, { cwd: dir, stdio: "ignore" });
44
+ }
45
+
46
+ function makeGitRepo(prefix: string): string {
47
+ const dir = makeTempDir(prefix);
48
+ git(dir, ["init"]);
49
+ git(dir, ["config", "user.email", "test@example.com"]);
50
+ git(dir, ["config", "user.name", "Test User"]);
51
+ return dir;
52
+ }
53
+
40
54
  // ─── detectProjectState ─────────────────────────────────────────────────────────
41
55
 
42
56
  test("detectProjectState: empty directory returns state=none", (t) => {
@@ -49,6 +63,132 @@ test("detectProjectState: empty directory returns state=none", (t) => {
49
63
  assert.equal(result.v2, undefined);
50
64
  });
51
65
 
66
+ test("classifyProject: no git repo is invalid", (t) => {
67
+ const dir = makeTempDir("classify-invalid");
68
+ t.after(() => cleanup(dir));
69
+
70
+ const classification = classifyProject(dir);
71
+ assert.equal(classification.kind, "invalid-repo");
72
+ });
73
+
74
+ test("classifyProject: empty git repo is greenfield", (t) => {
75
+ const dir = makeGitRepo("classify-greenfield");
76
+ t.after(() => cleanup(dir));
77
+
78
+ const classification = classifyProject(dir);
79
+ assert.equal(classification.kind, "greenfield");
80
+ });
81
+
82
+ test("classifyProject: nested empty git repo does not inherit ancestor markers", (t) => {
83
+ const parent = makeGitRepo("classify-parent-marker");
84
+ t.after(() => cleanup(parent));
85
+
86
+ writeFileSync(join(parent, "package.json"), JSON.stringify({ name: "parent" }), "utf-8");
87
+ git(parent, ["add", "package.json"]);
88
+ git(parent, ["commit", "-m", "add parent marker"]);
89
+ const child = join(parent, "nested");
90
+ mkdirSync(child, { recursive: true });
91
+ git(child, ["init"]);
92
+ git(child, ["config", "user.email", "test@example.com"]);
93
+ git(child, ["config", "user.name", "Test User"]);
94
+
95
+ const classification = classifyProject(child);
96
+ assert.equal(classification.kind, "greenfield");
97
+ });
98
+
99
+ test("classifyProject: tracked static HTML is existing untyped content", (t) => {
100
+ const dir = makeGitRepo("classify-index");
101
+ t.after(() => cleanup(dir));
102
+
103
+ writeFileSync(join(dir, "index.html"), "<main></main>\n", "utf-8");
104
+ git(dir, ["add", "index.html"]);
105
+ git(dir, ["commit", "-m", "add static page"]);
106
+
107
+ const classification = classifyProject(dir);
108
+ assert.equal(classification.kind, "untyped-existing");
109
+ assert.deepEqual(classification.contentFiles, ["index.html"]);
110
+ });
111
+
112
+ test("classifyProject: README-only repo is existing untyped content", (t) => {
113
+ const dir = makeGitRepo("classify-readme");
114
+ t.after(() => cleanup(dir));
115
+
116
+ writeFileSync(join(dir, "README.md"), "# docs\n", "utf-8");
117
+ git(dir, ["add", "README.md"]);
118
+ git(dir, ["commit", "-m", "add docs"]);
119
+
120
+ const classification = classifyProject(dir);
121
+ assert.equal(classification.kind, "untyped-existing");
122
+ });
123
+
124
+ test("classifyProject: src-only content is untyped existing, not typed marker", (t) => {
125
+ const dir = makeGitRepo("classify-src-only");
126
+ t.after(() => cleanup(dir));
127
+
128
+ mkdirSync(join(dir, "src"), { recursive: true });
129
+ writeFileSync(join(dir, "src", "index.txt"), "content\n", "utf-8");
130
+ git(dir, ["add", "src/index.txt"]);
131
+ git(dir, ["commit", "-m", "add source content"]);
132
+
133
+ const classification = classifyProject(dir);
134
+ assert.equal(classification.kind, "untyped-existing");
135
+ assert.deepEqual(classification.contentFiles, ["src/index.txt"]);
136
+ });
137
+
138
+ test("classifyProject: nested untracked files count as project content", (t) => {
139
+ const dir = makeGitRepo("classify-untracked-nested");
140
+ t.after(() => cleanup(dir));
141
+
142
+ mkdirSync(join(dir, "docs"), { recursive: true });
143
+ writeFileSync(join(dir, "docs", "index.html"), "<main></main>\n", "utf-8");
144
+
145
+ const classification = classifyProject(dir);
146
+ assert.equal(classification.kind, "untyped-existing");
147
+ assert.deepEqual(classification.untrackedFiles, ["docs/index.html"]);
148
+ });
149
+
150
+ test("classifyProject: known markers produce typed existing project", (t) => {
151
+ const dir = makeGitRepo("classify-typed");
152
+ t.after(() => cleanup(dir));
153
+
154
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "typed" }), "utf-8");
155
+ git(dir, ["add", "package.json"]);
156
+ git(dir, ["commit", "-m", "add package"]);
157
+
158
+ const classification = classifyProject(dir);
159
+ assert.equal(classification.kind, "typed-existing");
160
+ assert.ok(classification.markers.includes("package.json"));
161
+ });
162
+
163
+ test("classifyProject: ignored build/cache-only files do not count as content", (t) => {
164
+ const dir = makeGitRepo("classify-ignored");
165
+ t.after(() => cleanup(dir));
166
+
167
+ writeFileSync(join(dir, ".gitignore"), "dist/\n.cache/\n", "utf-8");
168
+ git(dir, ["add", ".gitignore"]);
169
+ git(dir, ["commit", "-m", "ignore generated files"]);
170
+ mkdirSync(join(dir, "dist"), { recursive: true });
171
+ writeFileSync(join(dir, "dist", "bundle.js"), "generated\n", "utf-8");
172
+ mkdirSync(join(dir, ".cache"), { recursive: true });
173
+ writeFileSync(join(dir, ".cache", "x"), "cache\n", "utf-8");
174
+
175
+ const classification = classifyProject(dir);
176
+ assert.equal(classification.kind, "greenfield");
177
+ });
178
+
179
+ test("classifyProject: generated framework/cache dirs do not count as content", (t) => {
180
+ const dir = makeGitRepo("classify-generated-dirs");
181
+ t.after(() => cleanup(dir));
182
+
183
+ mkdirSync(join(dir, ".next", "server"), { recursive: true });
184
+ writeFileSync(join(dir, ".next", "server", "page.js"), "generated\n", "utf-8");
185
+ mkdirSync(join(dir, ".venv", "lib"), { recursive: true });
186
+ writeFileSync(join(dir, ".venv", "lib", "site.py"), "generated\n", "utf-8");
187
+
188
+ const classification = classifyProject(dir);
189
+ assert.equal(classification.kind, "greenfield");
190
+ });
191
+
52
192
  test("detectProjectState: directory with .gsd/milestones/M001 returns v2-gsd", (t) => {
53
193
  const dir = makeTempDir("v2-gsd");
54
194
  t.after(() => cleanup(dir));
@@ -0,0 +1,192 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { execFileSync } from "node:child_process";
4
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ import { buildCompleteMilestonePrompt, buildPlanMilestonePrompt } from "../auto-prompts.ts";
9
+
10
+ function git(cwd: string, args: string[]): string {
11
+ return execFileSync("git", args, {
12
+ cwd,
13
+ stdio: ["ignore", "pipe", "pipe"],
14
+ encoding: "utf-8",
15
+ env: { ...process.env, GIT_AUTHOR_NAME: "Test User", GIT_AUTHOR_EMAIL: "test@example.com", GIT_COMMITTER_NAME: "Test User", GIT_COMMITTER_EMAIL: "test@example.com" },
16
+ }).trim();
17
+ }
18
+
19
+ function makeRepo(files: Record<string, string>): string {
20
+ const base = mkdtempSync(join(tmpdir(), "gsd-right-size-"));
21
+ git(base, ["init", "-b", "main"]);
22
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
23
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# Context\n\nTest milestone.");
24
+ for (const [path, content] of Object.entries(files)) {
25
+ const abs = join(base, path);
26
+ mkdirSync(join(abs, ".."), { recursive: true });
27
+ writeFileSync(abs, content);
28
+ }
29
+ git(base, ["add", "."]);
30
+ git(base, ["commit", "-m", "init"]);
31
+ return base;
32
+ }
33
+
34
+ function writeCompleteMilestoneFiles(base: string, validation: string): void {
35
+ const dir = join(base, ".gsd", "milestones", "M001");
36
+ mkdirSync(join(dir, "slices", "S01"), { recursive: true });
37
+ writeFileSync(join(dir, "M001-ROADMAP.md"), "# M001\n\n## Slices\n- [x] **S01: One** `risk:low` `depends:[]`\n > Done\n");
38
+ writeFileSync(join(dir, "M001-VALIDATION.md"), validation);
39
+ writeFileSync(join(dir, "slices", "S01", "S01-SUMMARY.md"), "# S01 Summary\n\n**Verification:** passed\n");
40
+ }
41
+
42
+ function validationMetadata(): string {
43
+ return [
44
+ "validation_metadata:",
45
+ " covered_artifacts:",
46
+ " - `.gsd/milestones/M001/M001-VALIDATION.md`",
47
+ " - `.gsd/milestones/M001/M001-ROADMAP.md`",
48
+ " - `.gsd/milestones/M001/slices/S01/S01-SUMMARY.md`",
49
+ ].join("\n");
50
+ }
51
+
52
+ test("plan-milestone prompt includes tiny untyped project classification and one-slice guidance", async () => {
53
+ const base = makeRepo({ "index.html": "<!doctype html>\n<title>Test</title>\n" });
54
+ try {
55
+ const prompt = await buildPlanMilestonePrompt("M001", "Polish static page", base, "minimal");
56
+ assert.match(prompt, /\*\*Kind:\*\* untyped-existing/);
57
+ assert.match(prompt, /\*\*Content files:\*\* 1/);
58
+ assert.match(prompt, /`index\.html`/);
59
+ assert.match(prompt, /Prefer exactly one slice/);
60
+ } finally {
61
+ rmSync(base, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test("plan-milestone prompt includes small untyped project 1-2 slice guidance", async () => {
66
+ const base = makeRepo({
67
+ "index.html": "html",
68
+ "README.md": "readme",
69
+ "styles.css": "body {}",
70
+ });
71
+ try {
72
+ const prompt = await buildPlanMilestonePrompt("M001", "Polish static files", base, "minimal");
73
+ assert.match(prompt, /\*\*Kind:\*\* untyped-existing/);
74
+ assert.match(prompt, /\*\*Content files:\*\* 3/);
75
+ assert.match(prompt, /Prefer 1-2 slices/);
76
+ } finally {
77
+ rmSync(base, { recursive: true, force: true });
78
+ }
79
+ });
80
+
81
+ test("plan-milestone prompt keeps normal guidance for typed projects", async () => {
82
+ const base = makeRepo({
83
+ "package.json": "{\"scripts\":{\"test\":\"node --test\"}}\n",
84
+ "src/index.js": "console.log('ok');\n",
85
+ });
86
+ try {
87
+ const prompt = await buildPlanMilestonePrompt("M001", "Update app", base, "minimal");
88
+ assert.match(prompt, /\*\*Kind:\*\* typed-existing/);
89
+ assert.match(prompt, /Use normal ecosystem-aware planning guidance/);
90
+ assert.doesNotMatch(prompt, /Prefer exactly one slice/);
91
+ } finally {
92
+ rmSync(base, { recursive: true, force: true });
93
+ }
94
+ });
95
+
96
+ test("workflow docs no longer contain blanket 4-10 slice guidance", () => {
97
+ const docs = readFileSync(join(process.cwd(), "src", "resources", "GSD-WORKFLOW.md"), "utf-8");
98
+ assert.doesNotMatch(docs, /4-10 slices/);
99
+ assert.match(docs, /1-10 slices/);
100
+ assert.match(docs, /single-file/);
101
+ });
102
+
103
+ test("prompt templates carry right-sized planning and closeout mode guidance", () => {
104
+ const planTemplate = readFileSync(join(process.cwd(), "src", "resources", "extensions", "gsd", "prompts", "plan-milestone.md"), "utf-8");
105
+ const completeTemplate = readFileSync(join(process.cwd(), "src", "resources", "extensions", "gsd", "prompts", "complete-milestone.md"), "utf-8");
106
+
107
+ assert.match(planTemplate, /Use 1-10 slices, sized to the work/);
108
+ assert.match(planTemplate, /tiny\/single-file\/static work should usually be one slice/);
109
+ assert.match(planTemplate, /untyped-existing/);
110
+ assert.match(completeTemplate, /Closeout Review Mode/);
111
+ assert.match(completeTemplate, /passing validation artifact is present/);
112
+ assert.doesNotMatch(completeTemplate, /^### Delegate Review Work/m);
113
+ });
114
+
115
+ test("complete-milestone prompt trusts passing validation artifact", async () => {
116
+ const base = makeRepo({ "index.html": "<!doctype html>\n<title>Test</title>\n" });
117
+ try {
118
+ writeCompleteMilestoneFiles(base, `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\n${validationMetadata()}\n\nAll checks passed.`);
119
+ const prompt = await buildCompleteMilestonePrompt("M001", "Polish static page", base, "minimal");
120
+ assert.match(prompt, /Passing Validation Artifact/);
121
+ assert.match(prompt, /Treat it as authoritative/);
122
+ assert.match(prompt, /Do not delegate fresh reviewer\/security\/tester audits/);
123
+ assert.match(prompt, /All checks passed/);
124
+ } finally {
125
+ rmSync(base, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ test("complete-milestone prompt trusts centralized markdown body pass verdict", async () => {
130
+ const base = makeRepo({ "index.html": "<!doctype html>\n<title>Test</title>\n" });
131
+ try {
132
+ writeCompleteMilestoneFiles(base, `# Validation\n\n**Verdict:** PASS\n\n${validationMetadata()}\n\nAll checks passed.`);
133
+ const prompt = await buildCompleteMilestonePrompt("M001", "Polish static page", base, "minimal");
134
+ assert.match(prompt, /Passing Validation Artifact/);
135
+ assert.match(prompt, /Treat it as authoritative/);
136
+ assert.match(prompt, /Do not delegate fresh reviewer\/security\/tester audits/);
137
+ } finally {
138
+ rmSync(base, { recursive: true, force: true });
139
+ }
140
+ });
141
+
142
+ test("complete-milestone prompt does not trust stale pass validation without metadata", async () => {
143
+ const base = makeRepo({ "index.html": "<!doctype html>\n<title>Test</title>\n" });
144
+ try {
145
+ writeCompleteMilestoneFiles(base, "---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nAll checks passed.");
146
+ const prompt = await buildCompleteMilestonePrompt("M001", "Polish static page", base, "minimal");
147
+ assert.match(prompt, /Validation Requires Attention/);
148
+ assert.match(prompt, /missing freshness metadata/);
149
+ assert.doesNotMatch(prompt, /Passing Validation Artifact/);
150
+ } finally {
151
+ rmSync(base, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ test("complete-milestone prompt does not trust pass validation missing current summary coverage", async () => {
156
+ const base = makeRepo({ "index.html": "<!doctype html>\n<title>Test</title>\n" });
157
+ try {
158
+ writeCompleteMilestoneFiles(base, [
159
+ "---",
160
+ "verdict: pass",
161
+ "remediation_round: 0",
162
+ "---",
163
+ "",
164
+ "# Validation",
165
+ "validation_metadata:",
166
+ " covered_artifacts:",
167
+ " - `.gsd/milestones/M001/M001-VALIDATION.md`",
168
+ " - `.gsd/milestones/M001/M001-ROADMAP.md`",
169
+ "",
170
+ "All checks passed.",
171
+ ].join("\n"));
172
+ const prompt = await buildCompleteMilestonePrompt("M001", "Polish static page", base, "minimal");
173
+ assert.match(prompt, /Validation Requires Attention/);
174
+ assert.match(prompt, /does not cover current milestone artifacts/);
175
+ assert.doesNotMatch(prompt, /Passing Validation Artifact/);
176
+ } finally {
177
+ rmSync(base, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ test("complete-milestone prompt keeps deeper review path without passing validation", async () => {
182
+ const base = makeRepo({ "index.html": "<!doctype html>\n<title>Test</title>\n" });
183
+ try {
184
+ writeCompleteMilestoneFiles(base, "---\nverdict: needs-attention\nremediation_round: 0\n---\n\n# Validation\nFix gaps.");
185
+ const prompt = await buildCompleteMilestonePrompt("M001", "Polish static page", base, "minimal");
186
+ assert.match(prompt, /Validation Requires Attention/);
187
+ assert.match(prompt, /verdict `needs-attention`/);
188
+ assert.match(prompt, /Use `subagent` for review work needing fresh context/i);
189
+ } finally {
190
+ rmSync(base, { recursive: true, force: true });
191
+ }
192
+ });
@@ -144,6 +144,18 @@ test("safety-harness-bug2-race: bash evidence survives mid-unit reset between to
144
144
  assert.ok(bash[0].outputSnippet.includes("found"), "output snippet captured");
145
145
  });
146
146
 
147
+ test("safety-harness: gsd_exec counts as execution evidence", () => {
148
+ resetEvidence();
149
+
150
+ recordToolCall("tc-exec-1", "gsd_exec", { command: "grep -n render index.html" });
151
+ recordToolResult("tc-exec-1", "gsd_exec", "Command exited with code 0\n1:render\n", false);
152
+
153
+ const bash = getEvidence().filter((e): e is BashEvidence => e.kind === "bash");
154
+ assert.equal(bash.length, 1, "gsd_exec must be tracked as execution evidence");
155
+ assert.equal(bash[0].command, "grep -n render index.html");
156
+ assert.equal(bash[0].exitCode, 0);
157
+ });
158
+
147
159
  // ─── Bug 3: git diff HEAD~1 scope check ─────────────────────────────────────
148
160
 
149
161
  test("safety-harness-bug3: validateFileChanges works on initial commit (no HEAD~1)", (t) => {
@@ -237,3 +249,20 @@ test("safety-harness-bug3: validateFileChanges works on merge commit", (t) => {
237
249
  // Must produce a valid result without throwing
238
250
  assert.ok(audit !== null, "audit must be produced for merge commit repo");
239
251
  });
252
+
253
+ test("safety-harness: planned changed file avoids unexpected-file warning", (t) => {
254
+ const base = mkdtempSync(join(tmpdir(), "gsd-planned-file-"));
255
+ t.after(() => rmSync(base, { recursive: true, force: true }));
256
+
257
+ execFileSync("git", ["init"], { cwd: base });
258
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: base });
259
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: base });
260
+ writeFileSync(join(base, "index.html"), "<main></main>\n");
261
+ execFileSync("git", ["add", "index.html"], { cwd: base });
262
+ execFileSync("git", ["commit", "-m", "add static app"], { cwd: base });
263
+
264
+ const audit = validateFileChanges(base, [], ["index.html"]);
265
+ assert.ok(audit !== null, "audit must be produced");
266
+ assert.deepEqual(audit!.unexpectedFiles, [], "planned index.html must not be unexpected");
267
+ assert.deepEqual(audit!.missingFiles, [], "planned index.html must not be missing");
268
+ });