gsd-pi 2.7.1 → 2.8.1

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 (133) hide show
  1. package/README.md +12 -5
  2. package/dist/loader.js +0 -0
  3. package/dist/modes/interactive/theme/dark.json +85 -0
  4. package/dist/modes/interactive/theme/light.json +84 -0
  5. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  6. package/dist/modes/interactive/theme/theme.d.ts +78 -0
  7. package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  8. package/dist/modes/interactive/theme/theme.js +949 -0
  9. package/dist/modes/interactive/theme/theme.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +30 -20
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  30. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  32. package/node_modules/@gsd/pi-coding-agent/dist/index.js +3 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  34. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  35. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  36. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  37. package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
  38. package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  39. package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
  40. package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
  41. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +33 -20
  42. package/node_modules/@gsd/pi-coding-agent/src/index.ts +3 -0
  43. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  44. package/node_modules/cliui/CHANGELOG.md +121 -0
  45. package/node_modules/color-convert/CHANGELOG.md +54 -0
  46. package/node_modules/esprima/ChangeLog +235 -0
  47. package/node_modules/mz/HISTORY.md +66 -0
  48. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  49. package/node_modules/source-map/CHANGELOG.md +301 -0
  50. package/node_modules/thenify/History.md +11 -0
  51. package/node_modules/thenify-all/History.md +11 -0
  52. package/node_modules/y18n/CHANGELOG.md +100 -0
  53. package/node_modules/yargs/CHANGELOG.md +88 -0
  54. package/node_modules/yargs-parser/CHANGELOG.md +263 -0
  55. package/package.json +10 -2
  56. package/packages/pi-coding-agent/dist/config.d.ts +2 -0
  57. package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
  58. package/packages/pi-coding-agent/dist/config.js +4 -0
  59. package/packages/pi-coding-agent/dist/config.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  61. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  63. package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  65. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  66. package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
  67. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  68. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  69. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
  71. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  73. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/tools/bash.js +30 -20
  75. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/index.d.ts +2 -0
  77. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/index.js +3 -0
  79. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  82. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/packages/pi-coding-agent/src/config.ts +5 -0
  84. package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  85. package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
  86. package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
  87. package/packages/pi-coding-agent/src/core/tools/bash.ts +33 -20
  88. package/packages/pi-coding-agent/src/index.ts +3 -0
  89. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  90. package/src/resources/extensions/browser-tools/capture.ts +165 -0
  91. package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
  92. package/src/resources/extensions/browser-tools/index.ts +47 -4985
  93. package/src/resources/extensions/browser-tools/lifecycle.ts +270 -0
  94. package/src/resources/extensions/browser-tools/package.json +5 -1
  95. package/src/resources/extensions/browser-tools/refs.ts +264 -0
  96. package/src/resources/extensions/browser-tools/settle.ts +197 -0
  97. package/src/resources/extensions/browser-tools/state.ts +408 -0
  98. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
  99. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
  100. package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
  101. package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
  102. package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
  103. package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
  104. package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
  105. package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
  106. package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
  107. package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
  108. package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
  109. package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
  110. package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
  111. package/src/resources/extensions/browser-tools/utils.ts +660 -0
  112. package/src/resources/extensions/gsd/auto.ts +74 -13
  113. package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
  114. package/src/resources/extensions/gsd/exit-command.ts +18 -0
  115. package/src/resources/extensions/gsd/files.ts +9 -40
  116. package/src/resources/extensions/gsd/git-service.ts +65 -17
  117. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  118. package/src/resources/extensions/gsd/guided-flow.ts +39 -2
  119. package/src/resources/extensions/gsd/index.ts +111 -16
  120. package/src/resources/extensions/gsd/preferences.ts +8 -0
  121. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  122. package/src/resources/extensions/gsd/prompts/discuss.md +25 -0
  123. package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
  124. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
  125. package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
  126. package/src/resources/extensions/gsd/tests/git-service.test.ts +64 -39
  127. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
  128. package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
  129. package/src/resources/extensions/shared/interview-ui.ts +1 -1
  130. package/src/resources/extensions/ttsr/index.ts +163 -0
  131. package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
  132. package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
  133. package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
@@ -20,18 +20,20 @@
20
20
 
21
21
  import type {
22
22
  ExtensionAPI,
23
+ ExtensionCommandContext,
23
24
  ExtensionContext,
24
25
  } from "@gsd/pi-coding-agent";
25
- import { createBashTool, createWriteTool, createReadTool, createEditTool } from "@gsd/pi-coding-agent";
26
+ import { createBashTool, createWriteTool, createReadTool, createEditTool, isToolCallEventType } from "@gsd/pi-coding-agent";
26
27
 
27
28
  import { registerGSDCommand } from "./commands.js";
29
+ import { registerExitCommand } from "./exit-command.js";
28
30
  import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
29
31
  import { saveFile, formatContinue, loadFile, parseContinue, parseSummary } from "./files.js";
30
32
  import { loadPrompt } from "./prompt-loader.js";
31
33
  import { deriveState } from "./state.js";
32
34
  import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData } from "./auto.js";
33
35
  import { saveActivityLog } from "./activity-log.js";
34
- import { checkAutoStartAfterDiscuss } from "./guided-flow.js";
36
+ import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js";
35
37
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
36
38
  import {
37
39
  loadEffectiveGSDPreferences,
@@ -42,7 +44,7 @@ import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "./skill-disc
42
44
  import {
43
45
  resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
44
46
  relSliceFile, relSlicePath, relTaskFile,
45
- buildSliceFileName, gsdRoot,
47
+ buildSliceFileName, buildMilestoneFileName, gsdRoot, resolveMilestonePath,
46
48
  } from "./paths.js";
47
49
  import { Key } from "@gsd/pi-tui";
48
50
  import { join } from "node:path";
@@ -50,6 +52,32 @@ import { existsSync } from "node:fs";
50
52
  import { shortcutDesc } from "../shared/terminal.js";
51
53
  import { Text } from "@gsd/pi-tui";
52
54
 
55
+ // ── Depth verification state ──────────────────────────────────────────────
56
+ let depthVerificationDone = false;
57
+
58
+ export function isDepthVerified(): boolean {
59
+ return depthVerificationDone;
60
+ }
61
+
62
+ // ── Write-gate: block CONTEXT.md writes during discussion without depth verification ──
63
+ const MILESTONE_CONTEXT_RE = /M\d+-CONTEXT\.md$/;
64
+
65
+ export function shouldBlockContextWrite(
66
+ toolName: string,
67
+ inputPath: string,
68
+ milestoneId: string | null,
69
+ depthVerified: boolean,
70
+ ): { block: boolean; reason?: string } {
71
+ if (toolName !== "write") return { block: false };
72
+ if (!milestoneId) return { block: false };
73
+ if (!MILESTONE_CONTEXT_RE.test(inputPath)) return { block: false };
74
+ if (depthVerified) return { block: false };
75
+ return {
76
+ block: true,
77
+ reason: `Blocked: Cannot write to milestone CONTEXT.md during discussion phase without depth verification. Call ask_user_questions with question id "depth_verification" first to confirm discussion depth before writing context.`,
78
+ };
79
+ }
80
+
53
81
  // ── ASCII logo ────────────────────────────────────────────────────────────
54
82
  const GSD_LOGO_LINES = [
55
83
  " ██████╗ ███████╗██████╗ ",
@@ -63,22 +91,12 @@ const GSD_LOGO_LINES = [
63
91
  export default function (pi: ExtensionAPI) {
64
92
  registerGSDCommand(pi);
65
93
  registerWorktreeCommand(pi);
66
-
67
- // ── /exit — graceful exit (cleanup auto-mode, save state) ──────────────
68
- pi.registerCommand("exit", {
69
- description: "Exit GSD gracefully (saves auto-mode state)",
70
- handler: async (_ctx) => {
71
- // Gracefully stop auto-mode if running (saves activity log, clears locks)
72
- const { stopAuto } = await import("./auto.js");
73
- await stopAuto(_ctx, pi);
74
- process.exit(0);
75
- },
76
- });
94
+ registerExitCommand(pi);
77
95
 
78
96
  // ── /kill — immediate exit (bypass cleanup) ─────────────────────────────
79
97
  pi.registerCommand("kill", {
80
98
  description: "Exit GSD immediately (no cleanup)",
81
- handler: async (_ctx) => {
99
+ handler: async (_args: string, _ctx: ExtensionCommandContext) => {
82
100
  process.exit(0);
83
101
  },
84
102
  });
@@ -299,7 +317,10 @@ export default function (pi: ExtensionAPI) {
299
317
  // ── agent_end: auto-mode advancement or auto-start after discuss ───────────
300
318
  pi.on("agent_end", async (event, ctx: ExtensionContext) => {
301
319
  // If discuss phase just finished, start auto-mode
302
- if (checkAutoStartAfterDiscuss()) return;
320
+ if (checkAutoStartAfterDiscuss()) {
321
+ depthVerificationDone = false;
322
+ return;
323
+ }
303
324
 
304
325
  // If auto-mode is already running, advance to next unit
305
326
  if (!isAutoActive()) return;
@@ -373,6 +394,80 @@ export default function (pi: ExtensionAPI) {
373
394
  saveActivityLog(ctx, dash.basePath, dash.currentUnit.type, dash.currentUnit.id);
374
395
  }
375
396
  });
397
+
398
+ // ── tool_call: block CONTEXT.md writes during discussion without depth verification ──
399
+ pi.on("tool_call", async (event) => {
400
+ if (!isToolCallEventType("write", event)) return;
401
+ const result = shouldBlockContextWrite(
402
+ event.toolName,
403
+ event.input.path,
404
+ getDiscussionMilestoneId(),
405
+ isDepthVerified(),
406
+ );
407
+ if (result.block) return result;
408
+ });
409
+
410
+ // ── tool_result: persist discussion exchanges & detect depth gate ──────
411
+ pi.on("tool_result", async (event) => {
412
+ if (event.toolName !== "ask_user_questions") return;
413
+
414
+ const milestoneId = getDiscussionMilestoneId();
415
+ if (!milestoneId) return;
416
+
417
+ const details = event.details as any;
418
+ if (details?.cancelled || !details?.response) return;
419
+
420
+ // ── Depth gate detection ──────────────────────────────────────────
421
+ const questions: any[] = (event.input as any)?.questions ?? [];
422
+ for (const q of questions) {
423
+ if (typeof q.id === "string" && q.id.includes("depth_verification")) {
424
+ depthVerificationDone = true;
425
+ break;
426
+ }
427
+ }
428
+
429
+ // ── Persist exchange to DISCUSSION.md ──────────────────────────────
430
+ const basePath = process.cwd();
431
+ const milestoneDir = resolveMilestonePath(basePath, milestoneId);
432
+ if (!milestoneDir) return;
433
+
434
+ const fileName = buildMilestoneFileName(milestoneId, "DISCUSSION");
435
+ const discussionPath = join(milestoneDir, fileName);
436
+ const timestamp = new Date().toISOString();
437
+
438
+ // Format exchange as markdown
439
+ const lines: string[] = [`## Exchange — ${timestamp}`, ""];
440
+
441
+ for (const q of questions) {
442
+ lines.push(`### ${q.header ?? "Question"}`);
443
+ lines.push("");
444
+ lines.push(q.question ?? "");
445
+ if (Array.isArray(q.options)) {
446
+ lines.push("");
447
+ for (const opt of q.options) {
448
+ lines.push(`- **${opt.label}** — ${opt.description ?? ""}`);
449
+ }
450
+ }
451
+
452
+ // Append user response for this question
453
+ const answer = details.response?.answers?.[q.id];
454
+ if (answer) {
455
+ lines.push("");
456
+ const selected = Array.isArray(answer.selected) ? answer.selected.join(", ") : answer.selected;
457
+ lines.push(`**Selected:** ${selected}`);
458
+ if (answer.notes) {
459
+ lines.push(`**Notes:** ${answer.notes}`);
460
+ }
461
+ }
462
+ lines.push("");
463
+ }
464
+
465
+ lines.push("---", "");
466
+
467
+ const newBlock = lines.join("\n");
468
+ const existing = await loadFile(discussionPath) ?? `# ${milestoneId} Discussion Log\n\n`;
469
+ await saveFile(discussionPath, existing + newBlock);
470
+ });
376
471
  }
377
472
 
378
473
  async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
@@ -702,6 +702,14 @@ function validatePreferences(preferences: GSDPreferences): {
702
702
  errors.push(`git.commit_type must be one of: feat, fix, refactor, docs, test, chore, perf, ci, build, style`);
703
703
  }
704
704
  }
705
+ if (g.merge_strategy !== undefined) {
706
+ const validStrategies = new Set(["squash", "merge"]);
707
+ if (typeof g.merge_strategy === "string" && validStrategies.has(g.merge_strategy)) {
708
+ git.merge_strategy = g.merge_strategy as "squash" | "merge";
709
+ } else {
710
+ errors.push("git.merge_strategy must be one of: squash, merge");
711
+ }
712
+ }
705
713
  if (g.main_branch !== undefined) {
706
714
  if (typeof g.main_branch === "string" && g.main_branch.trim() !== "" && VALID_BRANCH_NAME.test(g.main_branch)) {
707
715
  git.main_branch = g.main_branch;
@@ -17,13 +17,13 @@ Then:
17
17
  4. If the slice plan includes observability/diagnostic surfaces, confirm they work. Skip this for simple slices that don't have observability sections.
18
18
  5. If `.gsd/REQUIREMENTS.md` exists, update it based on what this slice actually proved. Move requirements between Active, Validated, Deferred, Blocked, or Out of Scope only when the evidence from execution supports that change.
19
19
  6. Write `{{sliceSummaryAbsPath}}` (compress all task summaries).
20
- 7. Write `{{sliceUatAbsPath}}`.
20
+ 7. Write `{{sliceUatAbsPath}}` — a concrete UAT script with real test cases derived from the slice plan and task summaries. Include preconditions, numbered steps with expected outcomes, and edge cases. This must NOT be a placeholder or generic template — tailor every test case to what this slice actually built.
21
21
  8. Review task summaries for `key_decisions`. Append any significant decisions to `.gsd/DECISIONS.md` if missing.
22
22
  9. Mark {{sliceId}} done in `{{roadmapPath}}` (change `[ ]` to `[x]`)
23
23
  10. Do not commit or squash-merge manually — the system auto-commits your changes and handles the merge after this unit succeeds.
24
24
  11. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed.
25
25
  12. Update `.gsd/STATE.md`
26
26
 
27
- **You MUST mark {{sliceId}} as `[x]` in `{{roadmapPath}}` AND write `{{sliceSummaryAbsPath}}` before finishing.**
27
+ **You MUST do ALL THREE before finishing: (1) write `{{sliceSummaryAbsPath}}`, (2) write `{{sliceUatAbsPath}}`, (3) mark {{sliceId}} as `[x]` in `{{roadmapPath}}`. The unit will not be marked complete if any of these files are missing.**
28
28
 
29
29
  When done, say: "Slice {{sliceId}} complete."
@@ -52,6 +52,16 @@ You are a thinking partner, not an interviewer.
52
52
 
53
53
  **Freeform rule:** When the user selects "Other" or clearly wants to explain something freely, stop using `ask_user_questions` and switch to plain text follow-ups. Let them talk. Resume structured questions when appropriate.
54
54
 
55
+ **Depth-signal awareness.** When a user writes extensively about something — long notes, detailed explanations, specific examples — that's a signal. Probe that area deeper. Don't spread attention evenly across all topics when the user is clearly investing energy in one.
56
+
57
+ **Enrichment fusion.** Weave the user's specific language, terminology, and framing into your subsequent questions. If they said "craft feel," your next question references "craft feel" — don't paraphrase it into "user experience quality." Their precision is signal, not noise.
58
+
59
+ **Position-first framing.** Have opinions. State your read of a tradeoff with rationale before asking what they think. "I'd lean toward X because Y — does that match your thinking, or am I missing context?" is better than "what do you think about X vs Y?" You're a thinking partner, not a neutral interviewer.
60
+
61
+ **Negative constraints.** Ask what would disappoint them. What they explicitly don't want. What the product should never feel like. Negative constraints are sharper than positive wishes — "never feel sluggish" defines the performance bar more precisely than "should be fast."
62
+
63
+ **Observation ≠ Conclusion.** Technical facts you discover in the codebase during investigation are context, not decisions. Present them as context and let the user decide what they mean for direction. "The current auth uses JWT with 24h expiry" is an observation. Whether to keep that pattern is the user's call.
64
+
55
65
  **Anti-patterns — never do these:**
56
66
  - **Checklist walking** — going through a predetermined list of topics regardless of what the user said
57
67
  - **Canned questions** — asking generic questions that could apply to any project
@@ -73,10 +83,22 @@ Do NOT offer to proceed until ALL of the following are satisfied. Track these in
73
83
  - [ ] **The biggest technical unknowns / risks** — what could fail, what hasn't been proven
74
84
  - [ ] **What external systems/services this touches** — APIs, databases, third-party services, hardware
75
85
 
86
+ Before offering to proceed, demonstrate absorption: reference specific things the user emphasized, specific terminology they used, specific nuance they sharpened — and show how those shaped your understanding. Synthesize, don't recite. "Your emphasis on X led me to prioritize Y over Z" is good. "You said X, you said Y, you said Z" is not. The user should feel heard in the specifics, not just acknowledged in the abstract.
87
+
76
88
  **Questioning depth should match scope.** Simple, well-defined work needs fewer rounds — maybe 1-2. Large, ambiguous visions need more — maybe 4+. Don't pad rounds to hit a number. Stop when the depth checklist is satisfied and you genuinely understand the work.
77
89
 
78
90
  Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
79
91
 
92
+ ## Depth Verification
93
+
94
+ Before moving to the wrap-up gate, present a structured depth summary to the user via `ask_user_questions`. This is a checkpoint — show what you captured across the depth checklist dimensions, using the user's own terminology and framing.
95
+
96
+ The question should summarize: what you understood them to be building, what shaped your understanding most (their emphasis, constraints, concerns), and any areas where you're least confident in your understanding. Frame it as: "Before we move to planning, here's what I captured — did I get the depth right?"
97
+
98
+ **Convention:** The question ID must contain `depth_verification` (e.g., `depth_verification_summary`). This naming convention enables downstream mechanical detection of this step.
99
+
100
+ Offer two options: "Yes, you got it (Recommended)" and "Not quite — let me clarify." If they clarify, absorb the correction and re-verify.
101
+
80
102
  ## Wrap-up Gate
81
103
 
82
104
  Only after the depth checklist is fully satisfied and you genuinely understand the work, offer to proceed.
@@ -166,6 +188,9 @@ Once the user is satisfied, in a single pass:
166
188
  1. `mkdir -p .gsd/milestones/{{milestoneId}}/slices`
167
189
  2. Write or update `.gsd/PROJECT.md` — read the template at `~/.gsd/agent/extensions/gsd/templates/project.md` first. Describe what the project is, its current state, and list the milestone sequence.
168
190
  3. Write or update `.gsd/REQUIREMENTS.md` — read the template at `~/.gsd/agent/extensions/gsd/templates/requirements.md` first. Confirm requirement states, ownership, and traceability before roadmap creation.
191
+ **Depth-Preservation Guidance for context.md:**
192
+ When writing context.md, preserve the user's exact terminology, emphasis, and specific framing from the discussion. Do not paraphrase user nuance into generic summaries. If the user said "craft feel," write "craft feel" — not "high-quality user experience." If they emphasized a specific constraint or negative requirement, carry that emphasis through verbatim. The context file is downstream agents' only window into this conversation — flattening specifics into generics loses the signal that shaped every decision.
193
+
169
194
  4. Write `{{contextAbsPath}}` — read the template at `~/.gsd/agent/extensions/gsd/templates/context.md` first. Preserve key risks, unknowns, existing codebase constraints, integration points, and relevant requirements surfaced during discussion.
170
195
  5. Write `{{roadmapAbsPath}}` — read the template at `~/.gsd/agent/extensions/gsd/templates/roadmap.md` first. Decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice that proves the assembled system works end-to-end in a real environment.
171
196
  6. Seed `.gsd/DECISIONS.md` — read the template at `~/.gsd/agent/extensions/gsd/templates/decisions.md` first. Append rows for any architectural or pattern decisions made during discussion.
@@ -0,0 +1,50 @@
1
+ import type { RoadmapSliceEntry, RiskLevel } from "./types.ts";
2
+
3
+ function extractSlicesSection(content: string): string {
4
+ const headingMatch = /^## Slices\s*$/m.exec(content);
5
+ if (!headingMatch || headingMatch.index == null) return "";
6
+
7
+ const start = headingMatch.index + headingMatch[0].length;
8
+ const rest = content.slice(start).replace(/^\r?\n/, "");
9
+ const nextHeading = /^##\s+/m.exec(rest);
10
+ return (nextHeading ? rest.slice(0, nextHeading.index) : rest).trimEnd();
11
+ }
12
+
13
+ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
14
+ const slicesSection = extractSlicesSection(content);
15
+ const slices: RoadmapSliceEntry[] = [];
16
+ if (!slicesSection) return slices;
17
+
18
+ const checkboxItems = slicesSection.split("\n");
19
+ let currentSlice: RoadmapSliceEntry | null = null;
20
+
21
+ for (const line of checkboxItems) {
22
+ const cbMatch = line.match(/^\s*-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
23
+ if (cbMatch) {
24
+ if (currentSlice) slices.push(currentSlice);
25
+
26
+ const done = cbMatch[1].toLowerCase() === "x";
27
+ const id = cbMatch[2]!;
28
+ const title = cbMatch[3]!;
29
+ const rest = cbMatch[4] ?? "";
30
+
31
+ const riskMatch = rest.match(/`risk:(\w+)`/);
32
+ const risk = (riskMatch ? riskMatch[1] : "low") as RiskLevel;
33
+
34
+ const depsMatch = rest.match(/`depends:\[([^\]]*)\]`/);
35
+ const depends = depsMatch && depsMatch[1]!.trim()
36
+ ? depsMatch[1]!.split(",").map(s => s.trim())
37
+ : [];
38
+
39
+ currentSlice = { id, title, risk, depends, done, demo: "" };
40
+ continue;
41
+ }
42
+
43
+ if (currentSlice && line.trim().startsWith(">")) {
44
+ currentSlice.demo = line.trim().replace(/^>\s*/, "").replace(/^After this:\s*/i, "");
45
+ }
46
+ }
47
+
48
+ if (currentSlice) slices.push(currentSlice);
49
+ return slices;
50
+ }
@@ -0,0 +1,102 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { getPriorSliceCompletionBlocker } from "../dispatch-guard.ts";
6
+
7
+ let passed = 0;
8
+ let failed = 0;
9
+
10
+ function assertEq<T>(actual: T, expected: T, message: string): void {
11
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
12
+ passed += 1;
13
+ return;
14
+ }
15
+ failed += 1;
16
+ console.error(`FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
17
+ }
18
+
19
+ function run(command: string, cwd: string): void {
20
+ execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"] });
21
+ }
22
+
23
+ const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
24
+ try {
25
+ mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
26
+ mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
27
+
28
+ writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
29
+ "# M002: Previous",
30
+ "",
31
+ "## Slices",
32
+ "- [x] **S01: Done** `risk:low` `depends:[]`",
33
+ "- [ ] **S02: Pending** `risk:low` `depends:[S01]`",
34
+ "",
35
+ ].join("\n"));
36
+
37
+ writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
38
+ "# M003: Current",
39
+ "",
40
+ "## Slices",
41
+ "- [ ] **S01: First** `risk:low` `depends:[]`",
42
+ "- [ ] **S02: Second** `risk:low` `depends:[S01]`",
43
+ "",
44
+ ].join("\n"));
45
+
46
+ run("git init -b main", repo);
47
+ run("git config user.email test@example.com", repo);
48
+ run("git config user.name Test", repo);
49
+ run("git add .", repo);
50
+ run("git commit -m init", repo);
51
+
52
+ assertEq(
53
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M003/S01"),
54
+ "Cannot dispatch plan-slice M003/S01: earlier slice M002/S02 is not complete on main.",
55
+ "blocks first slice of next milestone when prior milestone is incomplete on main",
56
+ );
57
+
58
+ writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
59
+ "# M002: Previous",
60
+ "",
61
+ "## Slices",
62
+ "- [x] **S01: Done** `risk:low` `depends:[]`",
63
+ "- [x] **S02: Done** `risk:low` `depends:[S01]`",
64
+ "",
65
+ ].join("\n"));
66
+ run("git add .", repo);
67
+ run("git commit -m complete-m002", repo);
68
+
69
+ assertEq(
70
+ getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
71
+ "Cannot dispatch execute-task M003/S02/T01: earlier slice M003/S01 is not complete on main.",
72
+ "blocks later slice in same milestone when an earlier slice is incomplete on main",
73
+ );
74
+
75
+ writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
76
+ "# M003: Current",
77
+ "",
78
+ "## Slices",
79
+ "- [x] **S01: First** `risk:low` `depends:[]`",
80
+ "- [ ] **S02: Second** `risk:low` `depends:[S01]`",
81
+ "",
82
+ ].join("\n"));
83
+ run("git add .", repo);
84
+ run("git commit -m complete-m003-s01", repo);
85
+
86
+ assertEq(
87
+ getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
88
+ null,
89
+ "allows dispatch when all earlier slices are complete on main",
90
+ );
91
+
92
+ assertEq(
93
+ getPriorSliceCompletionBlocker(repo, "main", "plan-milestone", "M003"),
94
+ null,
95
+ "does not affect non-slice dispatch types",
96
+ );
97
+ } finally {
98
+ rmSync(repo, { recursive: true, force: true });
99
+ }
100
+
101
+ console.log(`Passed: ${passed}, Failed: ${failed}`);
102
+ if (failed > 0) process.exit(1);
@@ -0,0 +1,50 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { registerExitCommand } from "../exit-command.ts";
5
+
6
+ test("/exit requests graceful shutdown instead of process.exit", async () => {
7
+ const commands = new Map<
8
+ string,
9
+ {
10
+ description?: string;
11
+ handler: (args: string, ctx: { shutdown: () => Promise<void> }) => Promise<void>;
12
+ }
13
+ >();
14
+
15
+ const pi = {
16
+ registerCommand(name: string, options: any) {
17
+ commands.set(name, options);
18
+ },
19
+ };
20
+
21
+ let stopAutoCalls = 0;
22
+ registerExitCommand(pi as any, {
23
+ async stopAuto() {
24
+ stopAutoCalls += 1;
25
+ },
26
+ });
27
+
28
+ const exit = commands.get("exit");
29
+ assert.ok(exit, "registerExitCommand should register /exit");
30
+ assert.equal(exit.description, "Exit GSD gracefully");
31
+
32
+ let shutdownCalls = 0;
33
+ const originalExit = process.exit;
34
+ process.exit = ((code?: number) => {
35
+ throw new Error(`process.exit should not be called: ${code ?? "undefined"}`);
36
+ }) as typeof process.exit;
37
+
38
+ try {
39
+ await exit.handler("", {
40
+ async shutdown() {
41
+ shutdownCalls += 1;
42
+ },
43
+ });
44
+ } finally {
45
+ process.exit = originalExit;
46
+ }
47
+
48
+ assert.equal(stopAutoCalls, 1, "handler should stop auto-mode exactly once before shutdown");
49
+ assert.equal(shutdownCalls, 1, "handler should request graceful shutdown exactly once");
50
+ });
@@ -210,8 +210,8 @@ async function main(): Promise<void> {
210
210
 
211
211
  assertEq(
212
212
  RUNTIME_EXCLUSION_PATHS.length,
213
- 6,
214
- "exactly 6 runtime exclusion paths"
213
+ 7,
214
+ "exactly 7 runtime exclusion paths"
215
215
  );
216
216
 
217
217
  const expectedPaths = [
@@ -220,6 +220,7 @@ async function main(): Promise<void> {
220
220
  ".gsd/worktrees/",
221
221
  ".gsd/auto.lock",
222
222
  ".gsd/metrics.json",
223
+ ".gsd/completed-units.json",
223
224
  ".gsd/STATE.md",
224
225
  ];
225
226
 
@@ -347,52 +348,76 @@ async function main(): Promise<void> {
347
348
  rmSync(repo, { recursive: true, force: true });
348
349
  }
349
350
 
350
- // ─── GitServiceImpl: smart staging fallback ────────────────────────────
351
+ // ─── GitServiceImpl: smart staging excludes tracked runtime files ──────
351
352
 
352
- console.log("\n=== GitServiceImpl: smart staging fallback ===");
353
+ console.log("\n=== GitServiceImpl: smart staging excludes tracked runtime files ===");
353
354
 
354
355
  {
355
- // We can't easily make the pathspec fail in a real repo, but we can test
356
- // the fallback behavior by verifying that if smart staging somehow fails,
357
- // everything gets staged. We do this by checking that a commit with both
358
- // runtime and real files works when pathspec would fail.
356
+ // Reproduces the real bug: .gsd/ runtime files that are already tracked
357
+ // (in the git index) must be excluded from staging even when .gsd/ is
358
+ // in .gitignore. The old pathspec-exclude approach failed silently in
359
+ // this case and fell back to `git add -A`, staging everything.
359
360
  //
360
- // To force the fallback: temporarily override RUNTIME_EXCLUSION_PATHS
361
- // with an invalid pathspec. Since we can't modify a readonly array,
362
- // we'll test the actual fallback by creating a custom subclass.
361
+ // The fix has three layers:
362
+ // 1. Auto-cleanup: git rm --cached removes tracked runtime files from index
363
+ // 2. Stage-then-unstage: git add -A + git reset HEAD replaces pathspec excludes
364
+ // 3. Pre-checkout discard: git checkout -- .gsd/ clears dirty runtime files
363
365
 
364
366
  const repo = initTempRepo();
367
+ const svc = new GitServiceImpl(repo);
365
368
 
366
- // Create a subclass that overrides smartStage to simulate failure + fallback
367
- class FallbackTestService extends GitServiceImpl {
368
- fallbackUsed = false;
369
- smartStageWithBadPathspec(): void {
370
- // Simulate: try bad pathspec, catch, fallback
371
- try {
372
- runGit(this.basePath, ["add", "-A", "--", ".", ":(exclude)__NONEXISTENT_PATHSPEC_SYNTAX_ERROR__["]);
373
- // If the above doesn't throw, git accepted it (some versions do).
374
- // That's fine — the point is testing the fallback path.
375
- throw new Error("force fallback for test");
376
- } catch {
377
- console.error("GitService: smart staging failed, falling back to git add -A");
378
- this.fallbackUsed = true;
379
- runGit(this.basePath, ["add", "-A"]);
380
- }
381
- }
382
- }
383
-
384
- const svc = new FallbackTestService(repo);
369
+ // Simulate a repo where .gsd/ files were previously force-added
370
+ createFile(repo, ".gsd/metrics.json", '{"version":1}');
371
+ createFile(repo, ".gsd/completed-units.json", '["unit1"]');
372
+ createFile(repo, ".gsd/activity/log.jsonl", '{"ts":1}');
385
373
  createFile(repo, "src/real.ts", "real code");
386
- createFile(repo, ".gsd/activity/log.jsonl", "log");
374
+ // Force-add .gsd/ files to simulate historical tracking
375
+ runGit(repo, ["add", "-f", ".gsd/metrics.json", ".gsd/completed-units.json", ".gsd/activity/log.jsonl", "src/real.ts"]);
376
+ runGit(repo, ["commit", "-F", "-"], { input: "init with tracked runtime files" });
377
+
378
+ // Add .gitignore with .gsd/ (matches real-world setup from ensureGitignore)
379
+ createFile(repo, ".gitignore", ".gsd/\n");
380
+ runGit(repo, ["add", ".gitignore"]);
381
+ runGit(repo, ["commit", "-F", "-"], { input: "add gitignore" });
382
+
383
+ // Verify runtime files are tracked (precondition)
384
+ const tracked = run("git ls-files .gsd/", repo);
385
+ assert(tracked.includes("metrics.json"), "precondition: metrics.json tracked");
386
+ assert(tracked.includes("completed-units.json"), "precondition: completed-units.json tracked");
387
+ assert(tracked.includes("activity/log.jsonl"), "precondition: activity log tracked");
388
+
389
+ // Now modify both runtime and real files
390
+ createFile(repo, ".gsd/metrics.json", '{"version":2}');
391
+ createFile(repo, ".gsd/completed-units.json", '["unit1","unit2"]');
392
+ createFile(repo, ".gsd/activity/log.jsonl", '{"ts":2}');
393
+ createFile(repo, "src/real.ts", "updated code");
394
+
395
+ // autoCommit should commit real.ts. The first call also runs auto-cleanup
396
+ // which removes runtime files from the index via a dedicated commit.
397
+ const msg = svc.autoCommit("execute-task", "M001/S01/T01");
398
+ assert(msg !== null, "autoCommit produces a commit");
387
399
 
388
- // Call the fallback path manually
389
- svc.smartStageWithBadPathspec();
390
-
391
- // Check that everything was staged (fallback stages all)
392
- const staged = run("git diff --cached --name-only", repo);
393
- assert(staged.includes("src/real.ts"), "fallback stages real files");
394
- assert(staged.includes(".gsd/activity/log.jsonl"), "fallback stages runtime files too (no exclusion)");
395
- assert(svc.fallbackUsed, "fallback path was actually used");
400
+ const show = run("git show --stat HEAD", repo);
401
+ assert(show.includes("src/real.ts"), "real files are committed");
402
+
403
+ // After the commit, runtime files must no longer be in the git index.
404
+ // They remain on disk but are untracked (protected by .gitignore).
405
+ const trackedAfter = run("git ls-files .gsd/", repo);
406
+ assertEq(trackedAfter, "", "no .gsd/ runtime files remain in the index");
407
+
408
+ // Verify a second autoCommit with changed runtime files does NOT stage them
409
+ createFile(repo, ".gsd/metrics.json", '{"version":3}');
410
+ createFile(repo, ".gsd/completed-units.json", '["unit1","unit2","unit3"]');
411
+ createFile(repo, "src/real.ts", "third version");
412
+
413
+ const msg2 = svc.autoCommit("execute-task", "M001/S01/T02");
414
+ assert(msg2 !== null, "second autoCommit produces a commit");
415
+
416
+ const show2 = run("git show --stat HEAD", repo);
417
+ assert(show2.includes("src/real.ts"), "real files committed in second commit");
418
+ assert(!show2.includes("metrics"), "metrics.json not in second commit");
419
+ assert(!show2.includes("completed-units"), "completed-units.json not in second commit");
420
+ assert(!show2.includes("activity"), "activity not in second commit");
396
421
 
397
422
  rmSync(repo, { recursive: true, force: true });
398
423
  }