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
@@ -68,6 +68,7 @@ import {
68
68
  mergeSliceToMain,
69
69
  } from "./worktree.ts";
70
70
  import { GitServiceImpl } from "./git-service.ts";
71
+ import { getPriorSliceCompletionBlocker } from "./dispatch-guard.ts";
71
72
  import type { GitPreferences } from "./git-service.ts";
72
73
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
73
74
  import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
@@ -1254,7 +1255,14 @@ async function dispatchNextUnit(
1254
1255
  }
1255
1256
  }
1256
1257
 
1257
- await emitObservabilityWarnings(ctx, unitType, unitId);
1258
+ const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
1259
+ if (priorSliceBlocker) {
1260
+ await stopAuto(ctx, pi);
1261
+ ctx.ui.notify(priorSliceBlocker, "error");
1262
+ return;
1263
+ }
1264
+
1265
+ const observabilityIssues = await collectObservabilityWarnings(ctx, unitType, unitId);
1258
1266
 
1259
1267
  // Idempotency: skip units already completed in a prior session.
1260
1268
  const idempotencyKey = `${unitType}/${unitId}`;
@@ -1394,6 +1402,13 @@ async function dispatchNextUnit(
1394
1402
  }
1395
1403
  }
1396
1404
 
1405
+ // Inject observability repair instructions so the agent fixes gaps before
1406
+ // proceeding with the unit (see #174).
1407
+ const repairBlock = buildObservabilityRepairBlock(observabilityIssues);
1408
+ if (repairBlock) {
1409
+ finalPrompt = `${finalPrompt}${repairBlock}`;
1410
+ }
1411
+
1397
1412
  // Switch model if preferences specify one for this unit type
1398
1413
  // Try primary model, then fallbacks in order if setting fails
1399
1414
  const modelConfig = resolveModelWithFallbacksForUnit(unitType);
@@ -2357,17 +2372,17 @@ function ensurePreconditions(
2357
2372
 
2358
2373
  // ─── Diagnostics ──────────────────────────────────────────────────────────────
2359
2374
 
2360
- async function emitObservabilityWarnings(
2375
+ async function collectObservabilityWarnings(
2361
2376
  ctx: ExtensionContext,
2362
2377
  unitType: string,
2363
2378
  unitId: string,
2364
- ): Promise<void> {
2379
+ ): Promise<import("./observability-validator.ts").ValidationIssue[]> {
2365
2380
  const parts = unitId.split("/");
2366
2381
  const mid = parts[0];
2367
2382
  const sid = parts[1];
2368
2383
  const tid = parts[2];
2369
2384
 
2370
- if (!mid || !sid) return;
2385
+ if (!mid || !sid) return [];
2371
2386
 
2372
2387
  let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
2373
2388
 
@@ -2379,12 +2394,38 @@ async function emitObservabilityWarnings(
2379
2394
  issues = await validateCompleteBoundary(basePath, mid, sid);
2380
2395
  }
2381
2396
 
2382
- if (issues.length === 0) return;
2397
+ if (issues.length > 0) {
2398
+ ctx.ui.notify(
2399
+ `Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
2400
+ "warning",
2401
+ );
2402
+ }
2403
+
2404
+ return issues;
2405
+ }
2383
2406
 
2384
- ctx.ui.notify(
2385
- `Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
2386
- "warning",
2387
- );
2407
+ function buildObservabilityRepairBlock(issues: import("./observability-validator.ts").ValidationIssue[]): string {
2408
+ if (issues.length === 0) return "";
2409
+ const items = issues.map(issue => {
2410
+ const fileName = issue.file.split("/").pop() || issue.file;
2411
+ let line = `- **${fileName}**: ${issue.message}`;
2412
+ if (issue.suggestion) line += ` → ${issue.suggestion}`;
2413
+ return line;
2414
+ });
2415
+ return [
2416
+ "",
2417
+ "---",
2418
+ "",
2419
+ "## Pre-flight: Observability gaps to fix FIRST",
2420
+ "",
2421
+ "The following issues were detected in plan/summary files for this unit.",
2422
+ "**Read each flagged file, apply the fix described, then proceed with the unit.**",
2423
+ "",
2424
+ ...items,
2425
+ "",
2426
+ "---",
2427
+ "",
2428
+ ].join("\n");
2388
2429
  }
2389
2430
 
2390
2431
  async function recoverTimedOutUnit(
@@ -2736,14 +2777,34 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
2736
2777
  }
2737
2778
 
2738
2779
  /**
2739
- * Check whether the expected artifact for a unit exists on disk.
2740
- * Returns true if the artifact file exists, or if the unit type has no
2780
+ * Check whether the expected artifact(s) for a unit exist on disk.
2781
+ * Returns true if all required artifacts exist, or if the unit type has no
2741
2782
  * single verifiable artifact (e.g., replan-slice).
2783
+ *
2784
+ * complete-slice requires both SUMMARY and UAT files — verifying only
2785
+ * the summary allowed the unit to be marked complete when the LLM
2786
+ * skipped writing the UAT file (see #176).
2742
2787
  */
2743
2788
  function verifyExpectedArtifact(unitType: string, unitId: string, base: string): boolean {
2744
2789
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
2745
2790
  if (!absPath) return true;
2746
- return existsSync(absPath);
2791
+ if (!existsSync(absPath)) return false;
2792
+
2793
+ // complete-slice must also produce a UAT file
2794
+ if (unitType === "complete-slice") {
2795
+ const parts = unitId.split("/");
2796
+ const mid = parts[0];
2797
+ const sid = parts[1];
2798
+ if (mid && sid) {
2799
+ const dir = resolveSlicePath(base, mid, sid);
2800
+ if (dir) {
2801
+ const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
2802
+ if (!existsSync(uatPath)) return false;
2803
+ }
2804
+ }
2805
+ }
2806
+
2807
+ return true;
2747
2808
  }
2748
2809
 
2749
2810
  /**
@@ -2787,7 +2848,7 @@ function diagnoseExpectedArtifact(unitType: string, unitId: string, base: string
2787
2848
  return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
2788
2849
  }
2789
2850
  case "complete-slice":
2790
- return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid!, "ROADMAP")} + summary written`;
2851
+ return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid!, "ROADMAP")} + summary + UAT written`;
2791
2852
  case "replan-slice":
2792
2853
  return `${relSliceFile(base, mid!, sid!, "REPLAN")} + updated ${relSliceFile(base, mid!, sid!, "PLAN")}`;
2793
2854
  case "reassess-roadmap":
@@ -0,0 +1,65 @@
1
+ import { execSync } from "node:child_process";
2
+ import { relMilestoneFile } from "./paths.js";
3
+ import { parseRoadmapSlices } from "./roadmap-slices.ts";
4
+
5
+ const SLICE_DISPATCH_TYPES = new Set([
6
+ "research-slice",
7
+ "plan-slice",
8
+ "replan-slice",
9
+ "execute-task",
10
+ "complete-slice",
11
+ ]);
12
+
13
+ function readTrackedFileFromBranch(base: string, branch: string, relPath: string): string | null {
14
+ try {
15
+ return execSync(`git show ${branch}:${relPath}`, {
16
+ cwd: base,
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ encoding: "utf-8",
19
+ }).trim();
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function milestoneIdFromNumber(num: number): string {
26
+ return `M${String(num).padStart(3, "0")}`;
27
+ }
28
+
29
+ export function getPriorSliceCompletionBlocker(base: string, mainBranch: string, unitType: string, unitId: string): string | null {
30
+ if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
31
+
32
+ const [targetMid, targetSid] = unitId.split("/");
33
+ if (!targetMid || !targetSid) return null;
34
+
35
+ const targetMidNumber = Number.parseInt(targetMid.slice(1), 10);
36
+ if (!Number.isFinite(targetMidNumber)) return null;
37
+
38
+ for (let milestoneNumber = 1; milestoneNumber <= targetMidNumber; milestoneNumber += 1) {
39
+ const mid = milestoneIdFromNumber(milestoneNumber);
40
+ const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
41
+ if (!roadmapRel) continue;
42
+
43
+ const roadmapContent = readTrackedFileFromBranch(base, mainBranch, roadmapRel);
44
+ if (!roadmapContent) continue;
45
+
46
+ const slices = parseRoadmapSlices(roadmapContent);
47
+ if (mid !== targetMid) {
48
+ const incomplete = slices.find(slice => !slice.done);
49
+ if (incomplete) {
50
+ return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${mid}/${incomplete.id} is not complete on ${mainBranch}.`;
51
+ }
52
+ continue;
53
+ }
54
+
55
+ const targetIndex = slices.findIndex(slice => slice.id === targetSid);
56
+ if (targetIndex === -1) return null;
57
+
58
+ const incomplete = slices.slice(0, targetIndex).find(slice => !slice.done);
59
+ if (incomplete) {
60
+ return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${targetMid}/${incomplete.id} is not complete on ${mainBranch}.`;
61
+ }
62
+ }
63
+
64
+ return null;
65
+ }
@@ -0,0 +1,18 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
+
3
+ type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI) => Promise<void>;
4
+
5
+ export function registerExitCommand(
6
+ pi: ExtensionAPI,
7
+ deps: { stopAuto?: StopAutoFn } = {},
8
+ ): void {
9
+ pi.registerCommand("exit", {
10
+ description: "Exit GSD gracefully",
11
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
12
+ // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
13
+ const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
14
+ await stopAuto(ctx, pi);
15
+ ctx.shutdown();
16
+ },
17
+ });
18
+ }
@@ -1,14 +1,14 @@
1
- // GSD Extension File Parsing and I/O
1
+ // GSD Extension - File Parsing and I/O
2
2
  // Parsers for roadmap, plan, summary, and continue files.
3
3
  // Used by state derivation and the status widget.
4
- // Pure functions, zero Pi dependencies uses only Node built-ins.
4
+ // Pure functions, zero Pi dependencies - uses only Node built-ins.
5
5
 
6
6
  import { promises as fs, readdirSync } from 'node:fs';
7
7
  import { dirname, resolve } from 'node:path';
8
8
  import { milestonesDir, resolveMilestoneFile, relMilestoneFile } from './paths.js';
9
9
 
10
10
  import type {
11
- Roadmap, RoadmapSliceEntry, BoundaryMapEntry, RiskLevel,
11
+ Roadmap, BoundaryMapEntry,
12
12
  SlicePlan, TaskPlanEntry,
13
13
  Summary, SummaryFrontmatter, SummaryRequires, FileModified,
14
14
  Continue, ContinueFrontmatter, ContinueStatus,
@@ -18,6 +18,7 @@ import type {
18
18
  } from './types.ts';
19
19
 
20
20
  import { checkExistingEnvKeys } from '../get-secrets-from-user.ts';
21
+ import { parseRoadmapSlices } from './roadmap-slices.ts';
21
22
 
22
23
  // ─── Helpers ───────────────────────────────────────────────────────────────
23
24
 
@@ -199,40 +200,8 @@ export function parseRoadmap(content: string): Roadmap {
199
200
  })();
200
201
  const successCriteria = scSection ? parseBullets(scSection) : [];
201
202
 
202
- // Slices
203
- const slicesSection = extractSection(content, 'Slices');
204
- const slices: RoadmapSliceEntry[] = [];
205
-
206
- if (slicesSection) {
207
- const checkboxItems = slicesSection.split('\n');
208
- let currentSlice: RoadmapSliceEntry | null = null;
209
-
210
- for (const line of checkboxItems) {
211
- const cbMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
212
- if (cbMatch) {
213
- if (currentSlice) slices.push(currentSlice);
214
-
215
- const done = cbMatch[1].toLowerCase() === 'x';
216
- const id = cbMatch[2];
217
- const sliceTitle = cbMatch[3];
218
- const rest = cbMatch[4];
219
-
220
- const riskMatch = rest.match(/`risk:(\w+)`/);
221
- const risk = (riskMatch ? riskMatch[1] : 'low') as RiskLevel;
222
-
223
- const depsMatch = rest.match(/`depends:\[([^\]]*)\]`/);
224
- const depends = depsMatch && depsMatch[1].trim()
225
- ? depsMatch[1].split(',').map(s => s.trim())
226
- : [];
227
-
228
- currentSlice = { id, title: sliceTitle, risk, depends, done, demo: '' };
229
- } else if (currentSlice && line.trim().startsWith('>')) {
230
- const demoText = line.trim().replace(/^>\s*/, '').replace(/^After this:\s*/i, '');
231
- currentSlice.demo = demoText;
232
- }
233
- }
234
- if (currentSlice) slices.push(currentSlice);
235
- }
203
+ // Slices
204
+ const slices = parseRoadmapSlices(content);
236
205
 
237
206
  // Boundary map
238
207
  const boundaryMap: BoundaryMapEntry[] = [];
@@ -657,7 +626,7 @@ export function parseTaskPlanMustHaves(content: string): Array<{ text: string; c
657
626
  checked: cbMatch[1].toLowerCase() === 'x',
658
627
  };
659
628
  }
660
- // No checkbox treat as unchecked with full line as text
629
+ // No checkbox - treat as unchecked with full line as text
661
630
  return { text: line.trim(), checked: false };
662
631
  });
663
632
  }
@@ -732,7 +701,7 @@ export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' |
732
701
  /**
733
702
  * Extract the UAT type from a UAT file's raw content.
734
703
  *
735
- * UAT files have no YAML frontmatter pass raw file content directly.
704
+ * UAT files have no YAML frontmatter - pass raw file content directly.
736
705
  * Classification is leading-keyword-only: e.g. `mixed (artifact-driven + live-runtime)` → `'mixed'`.
737
706
  *
738
707
  * Returns `undefined` when:
@@ -811,7 +780,7 @@ export async function inlinePriorMilestoneSummary(mid: string, base: string): Pr
811
780
  * with the current environment (.env + process.env).
812
781
  *
813
782
  * Returns `null` when no manifest file exists (path resolution failure or
814
- * file not on disk) callers can distinguish "no manifest" from "empty manifest".
783
+ * file not on disk) - callers can distinguish "no manifest" from "empty manifest".
815
784
  */
816
785
  export async function getManifestStatus(
817
786
  base: string, milestoneId: string,
@@ -27,6 +27,7 @@ export interface GitPreferences {
27
27
  pre_merge_check?: boolean | string;
28
28
  commit_type?: string;
29
29
  main_branch?: string;
30
+ merge_strategy?: "squash" | "merge";
30
31
  }
31
32
 
32
33
  export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
@@ -55,7 +56,7 @@ export interface PreMergeCheckResult {
55
56
  * GSD runtime paths that should be excluded from smart staging.
56
57
  * These are transient/generated artifacts that should never be committed.
57
58
  * Matches the union of SKIP_PATHS + SKIP_EXACT in worktree-manager.ts
58
- * and the first 6 entries in gitignore.ts BASELINE_PATTERNS.
59
+ * and the first 7 entries in gitignore.ts BASELINE_PATTERNS.
59
60
  */
60
61
  export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
61
62
  ".gsd/activity/",
@@ -63,6 +64,7 @@ export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
63
64
  ".gsd/worktrees/",
64
65
  ".gsd/auto.lock",
65
66
  ".gsd/metrics.json",
67
+ ".gsd/completed-units.json",
66
68
  ".gsd/STATE.md",
67
69
  ];
68
70
 
@@ -130,16 +132,42 @@ export class GitServiceImpl {
130
132
  */
131
133
  private smartStage(extraExclusions: readonly string[] = []): void {
132
134
  const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
133
- const excludes = allExclusions.map(p => `':(exclude)${p}'`);
134
- const args = ["add", "-A", "--", ".", ...excludes];
135
- try {
136
- this.git(args);
137
- } catch {
138
- console.error("GitService: smart staging failed, falling back to git add -A");
139
- this.git(["add", "-A"]);
135
+
136
+ // One-time cleanup: if runtime files are already tracked in the index
137
+ // (from older versions where the fallback bug staged them), untrack them
138
+ // in a dedicated commit. This must happen as a separate commit because
139
+ // the git reset HEAD step below would otherwise undo the rm --cached.
140
+ if (!this._runtimeFilesCleanedUp) {
141
+ let cleaned = false;
142
+ for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
143
+ const result = this.git(["rm", "--cached", "-r", "--ignore-unmatch", exclusion], { allowFailure: true });
144
+ if (result && result.includes("rm '")) cleaned = true;
145
+ }
146
+ if (cleaned) {
147
+ this.git(["commit", "-F", "-"], { input: "chore: untrack .gsd/ runtime files from git index" });
148
+ }
149
+ this._runtimeFilesCleanedUp = true;
150
+ }
151
+
152
+ // Stage everything, then unstage excluded paths.
153
+ //
154
+ // Previous approach used pathspec excludes (:(exclude)...) with git add -A,
155
+ // but that fails when .gsd/ is in .gitignore — git exits non-zero before
156
+ // evaluating the excludes. The catch fallback ran plain `git add -A`,
157
+ // staging all tracked runtime files unconditionally and defeating the
158
+ // exclusion list entirely.
159
+ //
160
+ // git reset HEAD silently succeeds when the path isn't staged, so no
161
+ // error handling is needed per-path.
162
+ this.git(["add", "-A"]);
163
+ for (const exclusion of allExclusions) {
164
+ this.git(["reset", "HEAD", "--", exclusion], { allowFailure: true });
140
165
  }
141
166
  }
142
167
 
168
+ /** Tracks whether runtime file cleanup has run this session. */
169
+ private _runtimeFilesCleanedUp = false;
170
+
143
171
  /**
144
172
  * Stage files (smart staging) and commit.
145
173
  * Returns the commit message string on success, or null if nothing to commit.
@@ -312,6 +340,12 @@ export class GitServiceImpl {
312
340
  // Exclude .gsd/ to prevent merge conflicts when both branches modify planning artifacts.
313
341
  this.autoCommit("pre-switch", current, [".gsd/"]);
314
342
 
343
+ // Discard uncommitted .gsd/ changes so checkout doesn't fail.
344
+ // These are runtime files (metrics, completed-units, STATE) that were
345
+ // intentionally excluded from the commit above. If they remain dirty,
346
+ // git checkout refuses when the target branch has different versions.
347
+ this.git(["checkout", "--", ".gsd/"], { allowFailure: true });
348
+
315
349
  this.git(["checkout", branch]);
316
350
  return created;
317
351
  }
@@ -327,6 +361,9 @@ export class GitServiceImpl {
327
361
  // Exclude .gsd/ to prevent merge conflicts when both branches modify planning artifacts.
328
362
  this.autoCommit("pre-switch", current, [".gsd/"]);
329
363
 
364
+ // Discard uncommitted .gsd/ changes so checkout doesn't fail.
365
+ this.git(["checkout", "--", ".gsd/"], { allowFailure: true });
366
+
330
367
  this.git(["checkout", mainBranch]);
331
368
  }
332
369
 
@@ -487,25 +524,36 @@ export class GitServiceImpl {
487
524
  commitType, milestoneId, sliceId, sliceTitle, mainBranch, branch,
488
525
  );
489
526
 
490
- // Squash merge abort cleanly on conflict so the working tree is never
491
- // left in a half-merged state (see: merge-bug-fix).
527
+ // Pull latest main before merging to avoid conflicts from remote changes
528
+ this.git(["pull", "--rebase", "origin", mainBranch], { allowFailure: true });
529
+
530
+ // Merge slice branch — strategy is configurable via git.merge_strategy
531
+ // preference. Default: "squash" (preserves existing behavior).
532
+ // "merge" uses --no-ff which is more resilient to conflicts from
533
+ // long-lived branches or frequently-changing .gsd/* artifacts.
534
+ const strategy = this.prefs.merge_strategy ?? "squash";
535
+ const mergeArgs = strategy === "merge"
536
+ ? ["merge", "--no-ff", "-m", message, branch]
537
+ : ["merge", "--squash", branch];
538
+
492
539
  try {
493
- this.git(["merge", "--squash", branch]);
540
+ this.git(mergeArgs);
494
541
  } catch (mergeError) {
495
- // git merge --squash exits non-zero on conflict. The working tree now
496
- // has conflict markers and a dirty index. Reset to restore a clean state.
542
+ // Merge exits non-zero on conflict. Reset to restore a clean state.
497
543
  this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
498
544
  const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
499
545
  throw new Error(
500
- `Squash-merge of "${branch}" into "${mainBranch}" failed with conflicts. ` +
546
+ `${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts. ` +
501
547
  `Working tree has been reset to a clean state. ` +
502
- `Resolve manually: git checkout ${mainBranch} && git merge --squash ${branch}\n` +
548
+ `Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
503
549
  `Original error: ${msg}`,
504
550
  );
505
551
  }
506
552
 
507
- // Commit with rich message via stdin pipe
508
- this.git(["commit", "-F", "-"], { input: message });
553
+ // Squash merge needs a separate commit; --no-ff merge already committed
554
+ if (strategy === "squash") {
555
+ this.git(["commit", "-F", "-"], { input: message });
556
+ }
509
557
 
510
558
  // Delete the merged branch
511
559
  this.git(["branch", "-D", branch]);
@@ -20,6 +20,7 @@ const BASELINE_PATTERNS = [
20
20
  ".gsd/worktrees/",
21
21
  ".gsd/auto.lock",
22
22
  ".gsd/metrics.json",
23
+ ".gsd/completed-units.json",
23
24
  ".gsd/STATE.md",
24
25
 
25
26
  // ── OS junk ──
@@ -14,15 +14,16 @@ import { deriveState } from "./state.js";
14
14
  import { startAuto } from "./auto.js";
15
15
  import { readCrashLock, clearLock, formatCrashInfo } from "./crash-recovery.js";
16
16
  import {
17
- gsdRoot, milestonesDir, resolveMilestoneFile,
17
+ gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath,
18
18
  resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile,
19
19
  relMilestoneFile, relSliceFile, relSlicePath,
20
20
  } from "./paths.js";
21
21
  import { join } from "node:path";
22
- import { readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
22
+ import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
23
23
  import { execSync, execFileSync } from "node:child_process";
24
24
  import { ensureGitignore, ensurePreferences } from "./gitignore.js";
25
25
  import { loadEffectiveGSDPreferences } from "./preferences.js";
26
+ import { showConfirm } from "../shared/confirm-ui.js";
26
27
 
27
28
  // ─── Auto-start after discuss ─────────────────────────────────────────────────
28
29
 
@@ -35,6 +36,11 @@ let pendingAutoStart: {
35
36
  step?: boolean; // preserve step mode through discuss → auto transition
36
37
  } | null = null;
37
38
 
39
+ /** Returns the milestoneId being discussed, or null if no discussion is active */
40
+ export function getDiscussionMilestoneId(): string | null {
41
+ return pendingAutoStart?.milestoneId ?? null;
42
+ }
43
+
38
44
  /** Called from agent_end to check if auto-mode should start after discuss */
39
45
  export function checkAutoStartAfterDiscuss(): boolean {
40
46
  if (!pendingAutoStart) return false;
@@ -601,6 +607,16 @@ export async function showSmartEntry(
601
607
  label: "Discuss first",
602
608
  description: "Capture decisions on gray areas before planning.",
603
609
  }] : []),
610
+ {
611
+ id: "skip_milestone",
612
+ label: "Skip — create new milestone",
613
+ description: "Leave this milestone on disk and start a fresh one.",
614
+ },
615
+ {
616
+ id: "discard_milestone",
617
+ label: "Discard this milestone",
618
+ description: "Delete the milestone directory and start over.",
619
+ },
604
620
  ];
605
621
 
606
622
  const choice = await showNextAction(ctx as any, {
@@ -619,6 +635,27 @@ export async function showSmartEntry(
619
635
  dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
620
636
  milestoneId, milestoneTitle,
621
637
  }));
638
+ } else if (choice === "skip_milestone") {
639
+ const milestoneIds = findMilestoneIds(basePath);
640
+ const nextId = `M${String(milestoneIds.length + 1).padStart(3, "0")}`;
641
+ pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
642
+ dispatchWorkflow(pi, buildDiscussPrompt(nextId,
643
+ `New milestone ${nextId}.`,
644
+ basePath
645
+ ));
646
+ } else if (choice === "discard_milestone") {
647
+ const mDir = resolveMilestonePath(basePath, milestoneId);
648
+ if (!mDir) return;
649
+ const confirmed = await showConfirm(ctx as any, {
650
+ title: "Discard milestone?",
651
+ message: `This will permanently delete ${milestoneId} and all its contents.`,
652
+ confirmLabel: "Discard",
653
+ declineLabel: "Cancel",
654
+ });
655
+ if (confirmed) {
656
+ rmSync(mDir, { recursive: true, force: true });
657
+ return showSmartEntry(ctx, pi, basePath, options);
658
+ }
622
659
  }
623
660
  } else {
624
661
  // Roadmap exists — either blocked or ready for auto