gsd-pi 2.24.0 → 2.25.0

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 (113) hide show
  1. package/README.md +2 -1
  2. package/dist/models-resolver.d.ts +0 -11
  3. package/dist/models-resolver.js +0 -15
  4. package/dist/resource-loader.d.ts +0 -1
  5. package/dist/resource-loader.js +0 -9
  6. package/dist/resources/GSD-WORKFLOW.md +12 -9
  7. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  8. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  9. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  10. package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
  11. package/dist/resources/extensions/gsd/auto-worktree.ts +119 -1
  12. package/dist/resources/extensions/gsd/auto.ts +184 -36
  13. package/dist/resources/extensions/gsd/cache.ts +3 -1
  14. package/dist/resources/extensions/gsd/doctor.ts +2 -0
  15. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  16. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +34 -12
  18. package/dist/resources/extensions/gsd/index.ts +14 -1
  19. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  20. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  21. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  22. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  24. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
  25. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  29. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  31. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  32. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  33. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  34. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  35. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  36. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  37. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  38. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  39. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  40. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  41. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  42. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  43. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  44. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  45. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  46. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  47. package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
  48. package/package.json +1 -1
  49. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  50. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  51. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  52. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  53. package/packages/pi-ai/dist/providers/anthropic.js +39 -0
  54. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  55. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  56. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  57. package/packages/pi-ai/dist/types.d.ts +23 -1
  58. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/types.js.map +1 -1
  60. package/packages/pi-ai/src/providers/anthropic.ts +38 -1
  61. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  62. package/packages/pi-ai/src/types.ts +19 -1
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  67. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  71. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
  72. package/src/resources/GSD-WORKFLOW.md +12 -9
  73. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  74. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  75. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  76. package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
  77. package/src/resources/extensions/gsd/auto-worktree.ts +119 -1
  78. package/src/resources/extensions/gsd/auto.ts +184 -36
  79. package/src/resources/extensions/gsd/cache.ts +3 -1
  80. package/src/resources/extensions/gsd/doctor.ts +2 -0
  81. package/src/resources/extensions/gsd/git-service.ts +74 -14
  82. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  83. package/src/resources/extensions/gsd/guided-flow.ts +34 -12
  84. package/src/resources/extensions/gsd/index.ts +14 -1
  85. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  86. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  87. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  88. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  89. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  90. package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
  91. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  92. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  93. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  94. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  95. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  96. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  97. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  98. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  99. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  100. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  102. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  103. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  104. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  105. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  106. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  107. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  108. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  109. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  110. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  111. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  112. package/src/resources/extensions/gsd/worktree.ts +9 -2
  113. package/src/resources/extensions/search-the-web/native-search.ts +15 -5
@@ -903,6 +903,24 @@ export class ToolExecutionComponent extends Container {
903
903
  text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
904
904
  }
905
905
  }
906
+ } else if (this.toolName === "web_search") {
907
+ // Server-side Anthropic web search
908
+ text = theme.fg("toolTitle", theme.bold("web search"));
909
+
910
+ if (this.result) {
911
+ const output = this.getTextOutput().trim();
912
+ if (output) {
913
+ const lines = output.split("\n");
914
+ const maxLines = this.expanded ? lines.length : 10;
915
+ const displayLines = lines.slice(0, maxLines);
916
+ const remaining = lines.length - maxLines;
917
+
918
+ text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
919
+ if (remaining > 0) {
920
+ text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
921
+ }
922
+ }
923
+ }
906
924
  } else {
907
925
  // Generic tool (shouldn't reach here for custom tools)
908
926
  text = theme.fg("toolTitle", theme.bold(this.toolName));
@@ -1165,6 +1165,34 @@ export class InteractiveMode {
1165
1165
  return registeredTool?.definition;
1166
1166
  }
1167
1167
 
1168
+ /**
1169
+ * Format web search result content for display in the TUI.
1170
+ */
1171
+ private formatWebSearchResult(content: unknown): string {
1172
+ if (!content) return "Web search completed";
1173
+
1174
+ // Error result
1175
+ if (typeof content === "object" && "type" in (content as any) && (content as any).type === "web_search_tool_result_error") {
1176
+ const error = content as any;
1177
+ return `Search error: ${error.error_code || "unknown"}`;
1178
+ }
1179
+
1180
+ // Array of search results
1181
+ if (Array.isArray(content)) {
1182
+ const results = content.filter((r: any) => r.type === "web_search_result");
1183
+ if (results.length === 0) return "No results found";
1184
+ return results
1185
+ .map((r: any) => {
1186
+ const title = r.title || "Untitled";
1187
+ const url = r.url || "";
1188
+ return `${title}\n ${url}`;
1189
+ })
1190
+ .join("\n");
1191
+ }
1192
+
1193
+ return "Web search completed";
1194
+ }
1195
+
1168
1196
  /**
1169
1197
  * Set up keyboard shortcuts registered by extensions.
1170
1198
  */
@@ -2201,6 +2229,35 @@ export class InteractiveMode {
2201
2229
  component.updateArgs(content.arguments);
2202
2230
  }
2203
2231
  }
2232
+ } else if (content.type === "serverToolUse") {
2233
+ // Server-side tool (e.g., native web search) — show as pending tool execution
2234
+ if (!this.pendingTools.has(content.id)) {
2235
+ const component = new ToolExecutionComponent(
2236
+ content.name,
2237
+ content.input ?? {},
2238
+ {
2239
+ showImages: this.settingsManager.getShowImages(),
2240
+ },
2241
+ undefined,
2242
+ this.ui,
2243
+ );
2244
+ component.setExpanded(this.toolOutputExpanded);
2245
+ this.chatContainer.addChild(component);
2246
+ this.pendingTools.set(content.id, component);
2247
+ }
2248
+ } else if (content.type === "webSearchResult") {
2249
+ // Server-side tool result — resolve the pending server tool execution
2250
+ const component = this.pendingTools.get(content.toolUseId);
2251
+ if (component) {
2252
+ const searchContent = content.content;
2253
+ const isError = searchContent && typeof searchContent === "object" && "type" in (searchContent as any) && (searchContent as any).type === "web_search_tool_result_error";
2254
+ const resultText = this.formatWebSearchResult(searchContent);
2255
+ component.updateResult({
2256
+ content: [{ type: "text", text: resultText }],
2257
+ isError: !!isError,
2258
+ });
2259
+ this.pendingTools.delete(content.toolUseId);
2260
+ }
2204
2261
  }
2205
2262
  }
2206
2263
  this.ui.requestRender();
@@ -2594,6 +2651,33 @@ export class InteractiveMode {
2594
2651
  } else {
2595
2652
  this.pendingTools.set(content.id, component);
2596
2653
  }
2654
+ } else if (content.type === "serverToolUse") {
2655
+ // Server-side tool (e.g., native web search)
2656
+ const component = new ToolExecutionComponent(
2657
+ content.name,
2658
+ content.input ?? {},
2659
+ { showImages: this.settingsManager.getShowImages() },
2660
+ undefined,
2661
+ this.ui,
2662
+ );
2663
+ component.setExpanded(this.toolOutputExpanded);
2664
+ this.chatContainer.addChild(component);
2665
+ // Find matching webSearchResult in this message's content
2666
+ const resultBlock = message.content.find(
2667
+ (c) => c.type === "webSearchResult" && c.toolUseId === content.id,
2668
+ );
2669
+ if (resultBlock && resultBlock.type === "webSearchResult") {
2670
+ const searchContent = resultBlock.content;
2671
+ const isError = searchContent && typeof searchContent === "object" && "type" in (searchContent as any) && (searchContent as any).type === "web_search_tool_result_error";
2672
+ const resultText = this.formatWebSearchResult(searchContent);
2673
+ component.updateResult({
2674
+ content: [{ type: "text", text: resultText }],
2675
+ isError: !!isError,
2676
+ });
2677
+ } else {
2678
+ // No result yet (aborted stream?) — show as pending
2679
+ this.pendingTools.set(content.id, component);
2680
+ }
2597
2681
  }
2598
2682
  }
2599
2683
  } else if (message.role === "toolResult") {
@@ -565,25 +565,28 @@ One commit per slice. Individually revertable. Reads like a changelog.
565
565
 
566
566
  ```
567
567
  gsd/M001/S01:
568
- test(S01): round-trip tests passing
568
+ test(S01/T03): round-trip tests passing
569
569
  feat(S01/T03): file writer with round-trip fidelity
570
- chore(S01/T03): auto-commit after task
571
570
  feat(S01/T02): markdown parser for plan files
572
- chore(S01/T02): auto-commit after task
573
571
  feat(S01/T01): core types and interfaces
574
- chore(S01/T01): auto-commit after task
572
+ docs(S01): add slice plan
575
573
  ```
576
574
 
577
575
  ### Commit Conventions
578
576
 
579
577
  | When | Format | Example |
580
578
  |------|--------|---------|
581
- | Auto-commit (dirty state) | `chore(S01/T02): auto-commit after task` | Automatic save of work in progress |
582
- | After task verified | `feat(S01/T02): <what was built>` | The real work |
583
- | Plan/docs committed | `docs(S01): add slice plan` | Bundled with first task |
584
- | Slice squash to main | `type(M001/S01): <slice title>` | Type inferred from title (`feat`, `fix`, `docs`, etc.) |
579
+ | Task completed | `{type}(S01/T02): <one-liner from summary>` | Type inferred from title (`feat`, `fix`, `test`, etc.) |
580
+ | Plan/docs committed | `docs(S01): add slice plan` | Planning artifacts |
581
+ | Slice squash to main | `type(M001/S01): <slice title>` | Type inferred from title |
582
+ | State rebuild | `chore(S01/T02): auto-commit after state-rebuild` | Bookkeeping only |
585
583
 
586
- Commit types: `feat`, `fix`, `test`, `refactor`, `docs`, `chore`
584
+ The system reads the task summary after execution and builds a meaningful commit message:
585
+ - **Subject**: `{type}({sliceId}/{taskId}): {one-liner}` — the one-liner from the summary frontmatter
586
+ - **Type**: Inferred from the task title and one-liner (`feat`, `fix`, `test`, `refactor`, `docs`, `perf`, `chore`)
587
+ - **Body**: Key files from the summary frontmatter (up to 8 files listed)
588
+
589
+ Commit types: `feat`, `fix`, `test`, `refactor`, `docs`, `perf`, `chore`
587
590
 
588
591
  ### Squash Merge Message
589
592
 
@@ -328,12 +328,9 @@ export class BgManagerOverlay {
328
328
  return this.box(inner, width);
329
329
  }
330
330
 
331
- private renderOutput(width: number): string[] {
331
+ private processStatusHeader(p: typeof this.viewingProcess, activeTab: "output" | "events"): { statusIcon: string; headerLine: string } {
332
332
  const th = this.theme;
333
- const p = this.viewingProcess;
334
- if (!p) return [""];
335
- const inner: string[] = [];
336
-
333
+ if (!p) return { statusIcon: "", headerLine: "" };
337
334
  const statusIcon = p.alive
338
335
  ? (p.status === "ready" ? th.fg("success", "●")
339
336
  : p.status === "error" ? th.fg("error", "●")
@@ -343,9 +340,21 @@ export class BgManagerOverlay {
343
340
  const uptime = th.fg("dim", formatUptime(Date.now() - p.startedAt));
344
341
  const typeTag = th.fg("dim", `[${p.processType}]`);
345
342
  const portInfo = p.ports.length > 0 ? th.fg("dim", ` :${p.ports.join(",")}`) : "";
346
- const tabIndicator = th.fg("accent", "[Output]") + " " + th.fg("dim", "Events");
343
+ const tabIndicator = activeTab === "output"
344
+ ? th.fg("accent", "[Output]") + " " + th.fg("dim", "Events")
345
+ : th.fg("dim", "Output") + " " + th.fg("accent", "[Events]");
346
+ const headerLine = `${statusIcon} ${name} ${typeTag} ${uptime}${portInfo} ${tabIndicator}`;
347
+ return { statusIcon, headerLine };
348
+ }
349
+
350
+ private renderOutput(width: number): string[] {
351
+ const th = this.theme;
352
+ const p = this.viewingProcess;
353
+ if (!p) return [""];
354
+ const inner: string[] = [];
347
355
 
348
- inner.push(`${statusIcon} ${name} ${typeTag} ${uptime}${portInfo} ${tabIndicator}`);
356
+ const { headerLine } = this.processStatusHeader(p, "output");
357
+ inner.push(headerLine);
349
358
  inner.push("");
350
359
 
351
360
  // Unified buffer is already chronologically interleaved
@@ -384,16 +393,8 @@ export class BgManagerOverlay {
384
393
  if (!p) return [""];
385
394
  const inner: string[] = [];
386
395
 
387
- const statusIcon = p.alive
388
- ? (p.status === "ready" ? th.fg("success", "●")
389
- : p.status === "error" ? th.fg("error", "●")
390
- : th.fg("warning", "●"))
391
- : th.fg("dim", "○");
392
- const name = th.fg("muted", p.label);
393
- const uptime = th.fg("dim", formatUptime(Date.now() - p.startedAt));
394
- const tabIndicator = th.fg("dim", "Output") + " " + th.fg("accent", "[Events]");
395
-
396
- inner.push(`${statusIcon} ${name} ${uptime} ${tabIndicator}`);
396
+ const { headerLine } = this.processStatusHeader(p, "events");
397
+ inner.push(headerLine);
397
398
  inner.push("");
398
399
 
399
400
  if (p.events.length === 0) {
@@ -369,32 +369,14 @@ async function applySecrets(
369
369
  }
370
370
  }
371
371
 
372
- if (destination === "vercel" && opts.exec) {
372
+ if ((destination === "vercel" || destination === "convex") && opts.exec) {
373
373
  const env = opts.environment ?? "development";
374
374
  for (const { key, value } of provided) {
375
+ const cmd = destination === "vercel"
376
+ ? `printf %s ${shellEscapeSingle(value)} | vercel env add ${key} ${env}`
377
+ : `npx convex env set ${key} ${shellEscapeSingle(value)}`;
375
378
  try {
376
- const result = await opts.exec("sh", [
377
- "-c",
378
- `printf %s ${shellEscapeSingle(value)} | vercel env add ${key} ${env}`,
379
- ]);
380
- if (result.code !== 0) {
381
- errors.push(`${key}: ${result.stderr.slice(0, 200)}`);
382
- } else {
383
- applied.push(key);
384
- }
385
- } catch (err: any) {
386
- errors.push(`${key}: ${err.message}`);
387
- }
388
- }
389
- }
390
-
391
- if (destination === "convex" && opts.exec) {
392
- for (const { key, value } of provided) {
393
- try {
394
- const result = await opts.exec("sh", [
395
- "-c",
396
- `npx convex env set ${key} ${shellEscapeSingle(value)}`,
397
- ]);
379
+ const result = await opts.exec("sh", ["-c", cmd]);
398
380
  if (result.code !== 0) {
399
381
  errors.push(`${key}: ${result.stderr.slice(0, 200)}`);
400
382
  } else {
@@ -103,10 +103,10 @@ export function saveActivityLog(
103
103
  basePath: string,
104
104
  unitType: string,
105
105
  unitId: string,
106
- ): void {
106
+ ): string | null {
107
107
  try {
108
108
  const entries = ctx.sessionManager.getEntries();
109
- if (!entries || entries.length === 0) return;
109
+ if (!entries || entries.length === 0) return null;
110
110
 
111
111
  const activityDir = join(gsdRoot(basePath), "activity");
112
112
  mkdirSync(activityDir, { recursive: true });
@@ -116,7 +116,7 @@ export function saveActivityLog(
116
116
  const unitKey = `${unitType}\0${safeUnitId}`;
117
117
  // Use lightweight fingerprint instead of serializing all entries (#611)
118
118
  const key = snapshotKey(unitType, safeUnitId, entries);
119
- if (state.lastSnapshotKeyByUnit.get(unitKey) === key) return;
119
+ if (state.lastSnapshotKeyByUnit.get(unitKey) === key) return null;
120
120
 
121
121
  const filePath = nextActivityFilePath(activityDir, state, unitType, safeUnitId);
122
122
  // Stream entries to disk line-by-line instead of building one massive string (#611).
@@ -131,9 +131,11 @@ export function saveActivityLog(
131
131
  }
132
132
  state.nextSeq += 1;
133
133
  state.lastSnapshotKeyByUnit.set(unitKey, key);
134
+ return filePath;
134
135
  } catch (e) {
135
136
  // Don't let logging failures break auto-mode
136
137
  void e;
138
+ return null;
137
139
  }
138
140
  }
139
141
 
@@ -637,6 +637,12 @@ export async function buildPlanSlicePrompt(
637
637
  const executorContextConstraints = formatExecutorConstraints();
638
638
 
639
639
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
640
+ const prefs = loadEffectiveGSDPreferences();
641
+ const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
642
+ const commitInstruction = commitDocsEnabled
643
+ ? `Commit: \`docs(${sid}): add slice plan\``
644
+ : "Do not commit — planning docs are not tracked in git for this project.";
645
+
640
646
  return loadPrompt("plan-slice", {
641
647
  workingDirectory: base,
642
648
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
@@ -647,6 +653,7 @@ export async function buildPlanSlicePrompt(
647
653
  inlinedContext,
648
654
  dependencySummaries: depContent,
649
655
  executorContextConstraints,
656
+ commitInstruction,
650
657
  });
651
658
  }
652
659
 
@@ -1071,6 +1078,12 @@ export async function buildReassessRoadmapPrompt(
1071
1078
  // Non-fatal — captures module may not be available
1072
1079
  }
1073
1080
 
1081
+ const reassessPrefs = loadEffectiveGSDPreferences();
1082
+ const reassessCommitDocsEnabled = reassessPrefs?.preferences?.git?.commit_docs !== false;
1083
+ const reassessCommitInstruction = reassessCommitDocsEnabled
1084
+ ? `Commit: \`docs(${mid}): reassess roadmap after ${completedSliceId}\``
1085
+ : "Do not commit — planning docs are not tracked in git for this project.";
1086
+
1074
1087
  return loadPrompt("reassess-roadmap", {
1075
1088
  workingDirectory: base,
1076
1089
  milestoneId: mid,
@@ -1081,6 +1094,7 @@ export async function buildReassessRoadmapPrompt(
1081
1094
  assessmentPath,
1082
1095
  inlinedContext,
1083
1096
  deferredCaptures,
1097
+ commitInstruction: reassessCommitInstruction,
1084
1098
  });
1085
1099
  }
1086
1100
 
@@ -6,7 +6,7 @@
6
6
  * manages create, enter, detect, and teardown for auto-mode worktrees.
7
7
  */
8
8
 
9
- import { existsSync, cpSync, readFileSync, realpathSync, utimesSync } from "node:fs";
9
+ import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, utimesSync } from "node:fs";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
11
  import { copyWorktreeDb, reconcileWorktreeDb, isDbAvailable } from "./gsd-db.js";
12
12
  import { execSync, execFileSync } from "node:child_process";
@@ -134,6 +134,112 @@ export function autoWorktreeBranch(milestoneId: string): string {
134
134
  * Atomic: chdir + originalBase update happen in the same try block
135
135
  * to prevent split-brain.
136
136
  */
137
+
138
+ /**
139
+ * Forward-merge plan checkbox state from the project root into a freshly
140
+ * re-attached worktree (#778).
141
+ *
142
+ * When auto-mode stops via crash (not graceful stop), the milestone branch
143
+ * HEAD may be behind the filesystem state at the project root because
144
+ * syncStateToProjectRoot() runs after every task completion but the final
145
+ * git commit may not have happened before the crash. On restart the worktree
146
+ * is re-attached to the branch HEAD, which has [ ] for the crashed task,
147
+ * causing verifyExpectedArtifact() to fail and triggering an infinite
148
+ * dispatch/skip loop.
149
+ *
150
+ * Fix: after re-attaching, read every *.md plan file in the milestone
151
+ * directory at the project root and apply any [x] checkbox states that are
152
+ * ahead of the worktree version (forward-only: never downgrade [x] → [ ]).
153
+ *
154
+ * This is safe because syncStateToProjectRoot() is the authoritative source
155
+ * of post-task state at the project root — it writes the same [x] the LLM
156
+ * produced, then the auto-commit follows. If the commit never happened, the
157
+ * filesystem copy is still valid and correct.
158
+ */
159
+ function reconcilePlanCheckboxes(projectRoot: string, wtPath: string, milestoneId: string): void {
160
+ const srcMilestone = join(projectRoot, ".gsd", "milestones", milestoneId);
161
+ const dstMilestone = join(wtPath, ".gsd", "milestones", milestoneId);
162
+ if (!existsSync(srcMilestone) || !existsSync(dstMilestone)) return;
163
+
164
+ // Walk all markdown files in the milestone directory (plans, summaries, etc.)
165
+ function walkMd(dir: string): string[] {
166
+ const results: string[] = [];
167
+ try {
168
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
169
+ const full = join(dir, entry.name);
170
+ if (entry.isDirectory()) {
171
+ results.push(...walkMd(full));
172
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
173
+ results.push(full);
174
+ }
175
+ }
176
+ } catch { /* non-fatal */ }
177
+ return results;
178
+ }
179
+
180
+ for (const srcFile of walkMd(srcMilestone)) {
181
+ const rel = srcFile.slice(srcMilestone.length);
182
+ const dstFile = dstMilestone + rel;
183
+ if (!existsSync(dstFile)) continue; // only reconcile existing files
184
+
185
+ let srcContent: string;
186
+ let dstContent: string;
187
+ try {
188
+ srcContent = readFileSync(srcFile, "utf-8");
189
+ dstContent = readFileSync(dstFile, "utf-8");
190
+ } catch { continue; }
191
+
192
+ if (srcContent === dstContent) continue;
193
+
194
+ // Extract all checked task IDs from the source (project root)
195
+ // Pattern: - [x] **T<id>: or - [x] **S<id>: (case-insensitive x)
196
+ const checkedRe = /^- \[[xX]\] \*\*([TS]\d+):/gm;
197
+ const srcChecked = new Set<string>();
198
+ for (const m of srcContent.matchAll(checkedRe)) srcChecked.add(m[1]);
199
+
200
+ if (srcChecked.size === 0) continue;
201
+
202
+ // Forward-apply: replace [ ] → [x] for any IDs that are checked in src
203
+ let updated = dstContent;
204
+ let changed = false;
205
+ for (const id of srcChecked) {
206
+ const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
207
+ const uncheckedRe = new RegExp(`^(- )\\[ \\]( \\*\\*${escapedId}:)`, "gm");
208
+ if (uncheckedRe.test(updated)) {
209
+ updated = updated.replace(
210
+ new RegExp(`^(- )\\[ \\]( \\*\\*${escapedId}:)`, "gm"),
211
+ "$1[x]$2",
212
+ );
213
+ changed = true;
214
+ }
215
+ }
216
+
217
+ if (changed) {
218
+ try {
219
+ writeFileSync(dstFile, updated, "utf-8");
220
+ } catch { /* non-fatal */ }
221
+ }
222
+ }
223
+
224
+ // Also forward-merge completed-units.json (set-union)
225
+ const srcKeys = join(projectRoot, ".gsd", "completed-units.json");
226
+ const dstKeys = join(wtPath, ".gsd", "completed-units.json");
227
+ if (existsSync(srcKeys)) {
228
+ try {
229
+ const src: string[] = JSON.parse(readFileSync(srcKeys, "utf-8"));
230
+ let dst: string[] = [];
231
+ if (existsSync(dstKeys)) {
232
+ try { dst = JSON.parse(readFileSync(dstKeys, "utf-8")); } catch { /* ignore corrupt */ }
233
+ }
234
+ const merged = [...new Set([...dst, ...src])];
235
+ if (merged.length > dst.length) {
236
+ mkdirSync(join(wtPath, ".gsd"), { recursive: true });
237
+ writeFileSync(dstKeys, JSON.stringify(merged), "utf-8");
238
+ }
239
+ } catch { /* non-fatal */ }
240
+ }
241
+ }
242
+
137
243
  export function createAutoWorktree(basePath: string, milestoneId: string): string {
138
244
  const branch = autoWorktreeBranch(milestoneId);
139
245
 
@@ -166,6 +272,18 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
166
272
  // not always fully synced.
167
273
  if (!branchExists) {
168
274
  copyPlanningArtifacts(basePath, info.path);
275
+ } else {
276
+ // Re-attaching to an existing branch: forward-merge any plan checkpoint
277
+ // state from the project root into the worktree (#778).
278
+ //
279
+ // If auto-mode stopped via crash, the milestone branch HEAD may lag behind
280
+ // the project root filesystem because syncStateToProjectRoot() ran after
281
+ // task completion but the auto-commit never fired. On restart the worktree
282
+ // is re-created from the branch HEAD (which has [ ] for the crashed task),
283
+ // causing verifyExpectedArtifact() to return false → stale-key eviction →
284
+ // infinite dispatch/skip loop. Reconciling here ensures the worktree sees
285
+ // the same [x] state that syncStateToProjectRoot() wrote to the root.
286
+ reconcilePlanCheckboxes(basePath, info.path, milestoneId);
169
287
  }
170
288
 
171
289
  // Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets