gsd-pi 2.15.1 → 2.16.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 (99) hide show
  1. package/README.md +1 -0
  2. package/dist/resources/extensions/async-jobs/async-bash-tool.ts +2 -1
  3. package/dist/resources/extensions/async-jobs/await-tool.ts +5 -3
  4. package/dist/resources/extensions/async-jobs/cancel-job-tool.ts +2 -1
  5. package/dist/resources/extensions/async-jobs/index.ts +3 -3
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +3 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +31 -1
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +85 -2
  9. package/dist/resources/extensions/gsd/auto-recovery.ts +47 -28
  10. package/dist/resources/extensions/gsd/auto-supervisor.ts +2 -7
  11. package/dist/resources/extensions/gsd/auto-worktree.ts +53 -106
  12. package/dist/resources/extensions/gsd/auto.ts +28 -12
  13. package/dist/resources/extensions/gsd/commands.ts +93 -27
  14. package/dist/resources/extensions/gsd/doctor.ts +12 -22
  15. package/dist/resources/extensions/gsd/files.ts +154 -2
  16. package/dist/resources/extensions/gsd/git-self-heal.ts +4 -4
  17. package/dist/resources/extensions/gsd/git-service.ts +23 -23
  18. package/dist/resources/extensions/gsd/gitignore.ts +2 -5
  19. package/dist/resources/extensions/gsd/guided-flow.ts +11 -11
  20. package/dist/resources/extensions/gsd/index.ts +10 -3
  21. package/dist/resources/extensions/gsd/native-git-bridge.ts +843 -12
  22. package/dist/resources/extensions/gsd/native-parser-bridge.ts +128 -1
  23. package/dist/resources/extensions/gsd/paths.ts +89 -0
  24. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  25. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -0
  26. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +32 -0
  27. package/dist/resources/extensions/gsd/prompts/system.md +1 -0
  28. package/dist/resources/extensions/gsd/provider-error-pause.ts +1 -1
  29. package/dist/resources/extensions/gsd/session-forensics.ts +29 -12
  30. package/dist/resources/extensions/gsd/state.ts +1 -38
  31. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +2 -2
  32. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  33. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -1
  34. package/dist/resources/extensions/gsd/tests/overrides.test.ts +131 -0
  35. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +5 -5
  36. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +7 -6
  37. package/dist/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +1 -1
  38. package/dist/resources/extensions/gsd/undo.ts +24 -18
  39. package/dist/resources/extensions/gsd/worktree-command.ts +2 -2
  40. package/dist/resources/extensions/gsd/worktree-manager.ts +81 -134
  41. package/package.json +3 -2
  42. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +1 -1
  43. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/model-resolver.js +2 -2
  46. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/modes/interactive/components/custom-editor.js +8 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/components/custom-editor.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  52. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  53. package/packages/pi-coding-agent/src/core/model-resolver.ts +2 -2
  54. package/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts +8 -1
  55. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  56. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
  57. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  58. package/packages/pi-tui/dist/components/editor.js +4 -2
  59. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  60. package/packages/pi-tui/src/components/editor.ts +5 -3
  61. package/src/resources/extensions/async-jobs/async-bash-tool.ts +2 -1
  62. package/src/resources/extensions/async-jobs/await-tool.ts +5 -3
  63. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +2 -1
  64. package/src/resources/extensions/async-jobs/index.ts +3 -3
  65. package/src/resources/extensions/gsd/auto-dashboard.ts +3 -0
  66. package/src/resources/extensions/gsd/auto-dispatch.ts +31 -1
  67. package/src/resources/extensions/gsd/auto-prompts.ts +85 -2
  68. package/src/resources/extensions/gsd/auto-recovery.ts +47 -28
  69. package/src/resources/extensions/gsd/auto-supervisor.ts +2 -7
  70. package/src/resources/extensions/gsd/auto-worktree.ts +53 -106
  71. package/src/resources/extensions/gsd/auto.ts +28 -12
  72. package/src/resources/extensions/gsd/commands.ts +93 -27
  73. package/src/resources/extensions/gsd/doctor.ts +12 -22
  74. package/src/resources/extensions/gsd/files.ts +154 -2
  75. package/src/resources/extensions/gsd/git-self-heal.ts +4 -4
  76. package/src/resources/extensions/gsd/git-service.ts +23 -23
  77. package/src/resources/extensions/gsd/gitignore.ts +2 -5
  78. package/src/resources/extensions/gsd/guided-flow.ts +11 -11
  79. package/src/resources/extensions/gsd/index.ts +10 -3
  80. package/src/resources/extensions/gsd/native-git-bridge.ts +843 -12
  81. package/src/resources/extensions/gsd/native-parser-bridge.ts +128 -1
  82. package/src/resources/extensions/gsd/paths.ts +89 -0
  83. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  84. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -0
  85. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +32 -0
  86. package/src/resources/extensions/gsd/prompts/system.md +1 -0
  87. package/src/resources/extensions/gsd/provider-error-pause.ts +1 -1
  88. package/src/resources/extensions/gsd/session-forensics.ts +29 -12
  89. package/src/resources/extensions/gsd/state.ts +1 -38
  90. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +2 -2
  91. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
  92. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -1
  93. package/src/resources/extensions/gsd/tests/overrides.test.ts +131 -0
  94. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +5 -5
  95. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +7 -6
  96. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +1 -1
  97. package/src/resources/extensions/gsd/undo.ts +24 -18
  98. package/src/resources/extensions/gsd/worktree-command.ts +2 -2
  99. package/src/resources/extensions/gsd/worktree-manager.ts +81 -134
package/README.md CHANGED
@@ -213,6 +213,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
213
213
  | `/gsd next` | Explicit step mode (same as bare `/gsd`) |
214
214
  | `/gsd auto` | Autonomous mode — researches, plans, executes, commits, repeats |
215
215
  | `/gsd stop` | Stop auto mode gracefully |
216
+ | `/gsd steer` | Hard-steer plan documents during execution |
216
217
  | `/gsd discuss` | Discuss architecture and decisions (works alongside auto mode) |
217
218
  | `/gsd status` | Progress dashboard |
218
219
  | `/gsd queue` | Queue future milestones (safe during auto mode) |
@@ -71,7 +71,7 @@ export function createAsyncBashTool(
71
71
  "Check /jobs to see all running and recent background jobs.",
72
72
  ],
73
73
  parameters: schema,
74
- async execute(_toolCallId, params) {
74
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
75
75
  const manager = getManager();
76
76
  const cwd = getCwd();
77
77
  const { command, timeout, label } = params;
@@ -91,6 +91,7 @@ export function createAsyncBashTool(
91
91
  "Use `await_job` to get results when ready, or `cancel_job` to stop.",
92
92
  ].join("\n"),
93
93
  }],
94
+ details: undefined,
94
95
  };
95
96
  },
96
97
  };
@@ -24,7 +24,7 @@ export function createAwaitTool(getManager: () => AsyncJobManager): ToolDefiniti
24
24
  description:
25
25
  "Wait for background jobs to complete. Provide specific job IDs or omit to wait for the next job that finishes. Returns results of completed jobs.",
26
26
  parameters: schema,
27
- async execute(_toolCallId, params) {
27
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
28
28
  const manager = getManager();
29
29
  const { jobs: jobIds } = params;
30
30
 
@@ -43,6 +43,7 @@ export function createAwaitTool(getManager: () => AsyncJobManager): ToolDefiniti
43
43
  if (notFound.length > 0 && watched.length === 0) {
44
44
  return {
45
45
  content: [{ type: "text", text: `No jobs found: ${notFound.join(", ")}` }],
46
+ details: undefined,
46
47
  };
47
48
  }
48
49
  } else {
@@ -50,6 +51,7 @@ export function createAwaitTool(getManager: () => AsyncJobManager): ToolDefiniti
50
51
  if (watched.length === 0) {
51
52
  return {
52
53
  content: [{ type: "text", text: "No running background jobs." }],
54
+ details: undefined,
53
55
  };
54
56
  }
55
57
  }
@@ -59,7 +61,7 @@ export function createAwaitTool(getManager: () => AsyncJobManager): ToolDefiniti
59
61
  if (running.length === 0) {
60
62
  const result = formatResults(watched);
61
63
  manager.acknowledgeDeliveries(watched.map((j) => j.id));
62
- return { content: [{ type: "text", text: result }] };
64
+ return { content: [{ type: "text", text: result }], details: undefined };
63
65
  }
64
66
 
65
67
  // Wait for at least one to complete
@@ -75,7 +77,7 @@ export function createAwaitTool(getManager: () => AsyncJobManager): ToolDefiniti
75
77
  result += `\n\n**Still running:** ${stillRunning.map((j) => `${j.id} (${j.label})`).join(", ")}`;
76
78
  }
77
79
 
78
- return { content: [{ type: "text", text: result }] };
80
+ return { content: [{ type: "text", text: result }], details: undefined };
79
81
  },
80
82
  };
81
83
  }
@@ -16,7 +16,7 @@ export function createCancelJobTool(getManager: () => AsyncJobManager): ToolDefi
16
16
  label: "Cancel Background Job",
17
17
  description: "Cancel a running background job by its ID.",
18
18
  parameters: schema,
19
- async execute(_toolCallId, params) {
19
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
20
20
  const manager = getManager();
21
21
  const result = manager.cancel(params.job_id);
22
22
 
@@ -28,6 +28,7 @@ export function createCancelJobTool(getManager: () => AsyncJobManager): ToolDefi
28
28
 
29
29
  return {
30
30
  content: [{ type: "text", text: messages[result] ?? `Unknown result: ${result}` }],
31
+ details: undefined,
31
32
  };
32
33
  },
33
34
  };
@@ -62,7 +62,7 @@ export default function AsyncJobs(pi: ExtensionAPI) {
62
62
  "",
63
63
  truncatedOutput,
64
64
  ].join("\n"),
65
- display: `Background job ${job.id} ${job.status}`,
65
+ display: true,
66
66
  },
67
67
  { deliverAs: "followUp", triggerTurn: true },
68
68
  );
@@ -92,7 +92,7 @@ export default function AsyncJobs(pi: ExtensionAPI) {
92
92
  pi.sendMessage({
93
93
  customType: "async_jobs_list",
94
94
  content: "No async job manager active.",
95
- display: "No jobs",
95
+ display: true,
96
96
  });
97
97
  return;
98
98
  }
@@ -126,7 +126,7 @@ export default function AsyncJobs(pi: ExtensionAPI) {
126
126
  pi.sendMessage({
127
127
  customType: "async_jobs_list",
128
128
  content: lines.join("\n"),
129
- display: `${running.length} running, ${completed.length} recent`,
129
+ display: true,
130
130
  });
131
131
  },
132
132
  });
@@ -49,6 +49,7 @@ export function unitVerb(unitType: string): string {
49
49
  case "execute-task": return "executing";
50
50
  case "complete-slice": return "completing";
51
51
  case "replan-slice": return "replanning";
52
+ case "rewrite-docs": return "rewriting";
52
53
  case "reassess-roadmap": return "reassessing";
53
54
  case "run-uat": return "running UAT";
54
55
  default: return unitType;
@@ -65,6 +66,7 @@ export function unitPhaseLabel(unitType: string): string {
65
66
  case "execute-task": return "EXECUTE";
66
67
  case "complete-slice": return "COMPLETE";
67
68
  case "replan-slice": return "REPLAN";
69
+ case "rewrite-docs": return "REWRITE";
68
70
  case "reassess-roadmap": return "REASSESS";
69
71
  case "run-uat": return "UAT";
70
72
  default: return unitType.toUpperCase();
@@ -88,6 +90,7 @@ function peekNext(unitType: string, state: GSDState): string {
88
90
  case "execute-task": return `continue ${sid}`;
89
91
  case "complete-slice": return "reassess roadmap";
90
92
  case "replan-slice": return `re-execute ${sid}`;
93
+ case "rewrite-docs": return "continue execution";
91
94
  case "reassess-roadmap": return "advance to next slice";
92
95
  case "run-uat": return "reassess roadmap";
93
96
  default: return "";
@@ -12,7 +12,7 @@
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
14
  import type { UatType } from "./files.js";
15
- import { loadFile, extractUatType } from "./files.js";
15
+ import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
16
16
  import {
17
17
  resolveMilestoneFile, resolveSliceFile,
18
18
  relSliceFile,
@@ -28,6 +28,7 @@ import {
28
28
  buildReplanSlicePrompt,
29
29
  buildRunUatPrompt,
30
30
  buildReassessRoadmapPrompt,
31
+ buildRewriteDocsPrompt,
31
32
  checkNeedsReassessment,
32
33
  checkNeedsRunUat,
33
34
  } from "./auto-prompts.js";
@@ -54,9 +55,38 @@ interface DispatchRule {
54
55
  match: (ctx: DispatchContext) => Promise<DispatchAction | null>;
55
56
  }
56
57
 
58
+ // ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
59
+
60
+ const MAX_REWRITE_ATTEMPTS = 3;
61
+ let rewriteAttemptCount = 0;
62
+ export function resetRewriteCircuitBreaker(): void {
63
+ rewriteAttemptCount = 0;
64
+ }
65
+
57
66
  // ─── Rules ────────────────────────────────────────────────────────────────
58
67
 
59
68
  const DISPATCH_RULES: DispatchRule[] = [
69
+ {
70
+ name: "rewrite-docs (override gate)",
71
+ match: async ({ mid, midTitle, state, basePath }) => {
72
+ const pendingOverrides = await loadActiveOverrides(basePath);
73
+ if (pendingOverrides.length === 0) return null;
74
+ if (rewriteAttemptCount >= MAX_REWRITE_ATTEMPTS) {
75
+ const { resolveAllOverrides } = await import("./files.js");
76
+ await resolveAllOverrides(basePath);
77
+ rewriteAttemptCount = 0;
78
+ return null;
79
+ }
80
+ rewriteAttemptCount++;
81
+ const unitId = state.activeSlice ? `${mid}/${state.activeSlice.id}` : mid;
82
+ return {
83
+ action: "dispatch",
84
+ unitType: "rewrite-docs",
85
+ unitId,
86
+ prompt: await buildRewriteDocsPrompt(mid, midTitle, state.activeSlice, basePath, pendingOverrides),
87
+ };
88
+ },
89
+ },
60
90
  {
61
91
  name: "summarizing → complete-slice",
62
92
  match: async ({ state, mid, midTitle, basePath }) => {
@@ -6,8 +6,8 @@
6
6
  * utility.
7
7
  */
8
8
 
9
- import { loadFile, parseContinue, parseRoadmap, parseSummary, extractUatType } from "./files.js";
10
- import type { UatType } from "./files.js";
9
+ import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, loadActiveOverrides, formatOverridesSection } from "./files.js";
10
+ import type { Override, UatType } from "./files.js";
11
11
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
12
12
  import {
13
13
  resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
@@ -457,6 +457,9 @@ export async function buildResearchSlicePrompt(
457
457
  inlined.push(inlineTemplate("research", "Research"));
458
458
 
459
459
  const depContent = await inlineDependencySummaries(mid, sid, base);
460
+ const activeOverrides = await loadActiveOverrides(base);
461
+ const overridesInline = formatOverridesSection(activeOverrides);
462
+ if (overridesInline) inlined.unshift(overridesInline);
460
463
 
461
464
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
462
465
 
@@ -495,6 +498,9 @@ export async function buildPlanSlicePrompt(
495
498
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
496
499
 
497
500
  const depContent = await inlineDependencySummaries(mid, sid, base);
501
+ const planActiveOverrides = await loadActiveOverrides(base);
502
+ const planOverridesInline = formatOverridesSection(planActiveOverrides);
503
+ if (planOverridesInline) inlined.unshift(planOverridesInline);
498
504
 
499
505
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
500
506
 
@@ -562,7 +568,11 @@ export async function buildExecuteTaskPrompt(
562
568
 
563
569
  const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
564
570
 
571
+ const activeOverrides = await loadActiveOverrides(base);
572
+ const overridesSection = formatOverridesSection(activeOverrides);
573
+
565
574
  return loadPrompt("execute-task", {
575
+ overridesSection,
566
576
  workingDirectory: base,
567
577
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
568
578
  planPath: relSliceFile(base, mid, sid, "PLAN"),
@@ -609,6 +619,9 @@ export async function buildCompleteSlicePrompt(
609
619
  }
610
620
  inlined.push(inlineTemplate("slice-summary", "Slice Summary"));
611
621
  inlined.push(inlineTemplate("uat", "UAT"));
622
+ const completeActiveOverrides = await loadActiveOverrides(base);
623
+ const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
624
+ if (completeOverridesInline) inlined.unshift(completeOverridesInline);
612
625
 
613
626
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
614
627
 
@@ -712,6 +725,9 @@ export async function buildReplanSlicePrompt(
712
725
  // Inline decisions
713
726
  const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
714
727
  if (decisionsInline) inlined.push(decisionsInline);
728
+ const replanActiveOverrides = await loadActiveOverrides(base);
729
+ const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
730
+ if (replanOverridesInline) inlined.unshift(replanOverridesInline);
715
731
 
716
732
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
717
733
 
@@ -795,3 +811,70 @@ export async function buildReassessRoadmapPrompt(
795
811
  inlinedContext,
796
812
  });
797
813
  }
814
+
815
+ export async function buildRewriteDocsPrompt(
816
+ mid: string, midTitle: string,
817
+ activeSlice: { id: string; title: string } | null,
818
+ base: string,
819
+ overrides: Override[],
820
+ ): Promise<string> {
821
+ const sid = activeSlice?.id;
822
+ const sTitle = activeSlice?.title ?? "";
823
+ const docList: string[] = [];
824
+
825
+ if (sid) {
826
+ const slicePlanPath = resolveSliceFile(base, mid, sid, "PLAN");
827
+ const slicePlanRel = relSliceFile(base, mid, sid, "PLAN");
828
+ if (slicePlanPath) {
829
+ docList.push(`- Slice plan: \`${slicePlanRel}\``);
830
+ const tDir = resolveTasksDir(base, mid, sid);
831
+ if (tDir) {
832
+ const planContent = await loadFile(slicePlanPath);
833
+ if (planContent) {
834
+ const plan = parsePlan(planContent);
835
+ for (const task of plan.tasks) {
836
+ if (!task.done) {
837
+ const taskPlanPath = resolveTaskFile(base, mid, sid, task.id, "PLAN");
838
+ if (taskPlanPath) {
839
+ const taskRelPath = `${relSlicePath(base, mid, sid)}/tasks/${task.id}-PLAN.md`;
840
+ docList.push(`- Task plan: \`${taskRelPath}\``);
841
+ }
842
+ }
843
+ }
844
+ }
845
+ }
846
+ }
847
+ }
848
+
849
+ const decisionsPath = resolveGsdRootFile(base, "DECISIONS");
850
+ if (existsSync(decisionsPath)) docList.push(`- Decisions: \`${relGsdRootFile("DECISIONS")}\``);
851
+ const requirementsPath = resolveGsdRootFile(base, "REQUIREMENTS");
852
+ if (existsSync(requirementsPath)) docList.push(`- Requirements: \`${relGsdRootFile("REQUIREMENTS")}\``);
853
+ const projectPath = resolveGsdRootFile(base, "PROJECT");
854
+ if (existsSync(projectPath)) docList.push(`- Project: \`${relGsdRootFile("PROJECT")}\``);
855
+ const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
856
+ const contextRel = relMilestoneFile(base, mid, "CONTEXT");
857
+ if (contextPath) docList.push(`- Milestone context (reference only): \`${contextRel}\``);
858
+ const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
859
+ const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
860
+ if (roadmapPath) docList.push(`- Roadmap: \`${roadmapRel}\``);
861
+
862
+ const overrideContent = overrides.map((o, i) => [
863
+ `### Override ${i + 1}`,
864
+ `**Change:** ${o.change}`,
865
+ `**Issued:** ${o.timestamp}`,
866
+ `**During:** ${o.appliedAt}`,
867
+ ].join("\n")).join("\n\n");
868
+
869
+ const documentList = docList.length > 0 ? docList.join("\n") : "- No active plan documents found.";
870
+
871
+ return loadPrompt("rewrite-docs", {
872
+ milestoneId: mid,
873
+ milestoneTitle: midTitle,
874
+ sliceId: sid ?? "none",
875
+ sliceTitle: sTitle,
876
+ overrideContent,
877
+ documentList,
878
+ overridesPath: relGsdRootFile("OVERRIDES"),
879
+ });
880
+ }
@@ -11,7 +11,14 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent";
11
11
  import {
12
12
  clearUnitRuntimeRecord,
13
13
  } from "./unit-runtime.js";
14
- import { runGit } from "./git-service.js";
14
+ import {
15
+ nativeConflictFiles,
16
+ nativeCommit,
17
+ nativeCheckoutTheirs,
18
+ nativeAddPaths,
19
+ nativeMergeAbort,
20
+ nativeResetHard,
21
+ } from "./native-git-bridge.js";
15
22
  import {
16
23
  resolveMilestonePath,
17
24
  resolveSlicePath,
@@ -26,6 +33,7 @@ import {
26
33
  buildTaskFileName,
27
34
  resolveMilestoneFile,
28
35
  clearPathCache,
36
+ resolveGsdRootFile,
29
37
  } from "./paths.js";
30
38
  import { parseRoadmap } from "./files.js";
31
39
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
@@ -78,6 +86,8 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
78
86
  const dir = resolveMilestonePath(base, mid);
79
87
  return dir ? join(dir, buildMilestoneFileName(mid, "SUMMARY")) : null;
80
88
  }
89
+ case "rewrite-docs":
90
+ return null;
81
91
  default:
82
92
  return null;
83
93
  }
@@ -93,13 +103,20 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
93
103
  * skipped writing the UAT file (see #176).
94
104
  */
95
105
  export function verifyExpectedArtifact(unitType: string, unitId: string, base: string): boolean {
96
- // Clear stale directory listing cache so artifact checks see fresh disk state (#431)
97
- clearPathCache();
98
-
99
106
  // Hook units have no standard artifact — always pass. Their lifecycle
100
107
  // is managed by the hook engine, not the artifact verification system.
101
108
  if (unitType.startsWith("hook/")) return true;
102
109
 
110
+ // Clear stale directory listing cache so artifact checks see fresh disk state (#431).
111
+ // Moved after hook check to avoid unnecessary cache clears for hook units.
112
+ clearPathCache();
113
+
114
+ if (unitType === "rewrite-docs") {
115
+ const overridesPath = resolveGsdRootFile(base, "OVERRIDES");
116
+ if (!existsSync(overridesPath)) return true;
117
+ const content = readFileSync(overridesPath, "utf-8");
118
+ return !content.includes("**Scope:** active");
119
+ }
103
120
 
104
121
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
105
122
  // Unit types with no verifiable artifact always pass (e.g. replan-slice).
@@ -206,6 +223,8 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
206
223
  return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid!, "ROADMAP")} + summary + UAT written`;
207
224
  case "replan-slice":
208
225
  return `${relSliceFile(base, mid!, sid!, "REPLAN")} + updated ${relSliceFile(base, mid!, sid!, "PLAN")}`;
226
+ case "rewrite-docs":
227
+ return "Active overrides resolved in .gsd/OVERRIDES.md + plan documents updated";
209
228
  case "reassess-roadmap":
210
229
  return `${relSliceFile(base, mid!, sid!, "ASSESSMENT")} (roadmap reassessment)`;
211
230
  case "run-uat":
@@ -284,7 +303,8 @@ export function persistCompletedKey(base: string, key: string): void {
284
303
  keys = JSON.parse(readFileSync(file, "utf-8"));
285
304
  }
286
305
  } catch (e) { /* corrupt file — start fresh */ void e; }
287
- if (!keys.includes(key)) {
306
+ const keySet = new Set(keys);
307
+ if (!keySet.has(key)) {
288
308
  keys.push(key);
289
309
  // Atomic write: tmp file + rename prevents partial writes on crash
290
310
  const tmpFile = file + ".tmp";
@@ -298,12 +318,15 @@ export function removePersistedKey(base: string, key: string): void {
298
318
  const file = completedKeysPath(base);
299
319
  try {
300
320
  if (existsSync(file)) {
301
- let keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
302
- keys = keys.filter(k => k !== key);
303
- // Atomic write: tmp file + rename prevents partial writes on crash
304
- const tmpFile = file + ".tmp";
305
- writeFileSync(tmpFile, JSON.stringify(keys), "utf-8");
306
- renameSync(tmpFile, file);
321
+ const keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
322
+ const filtered = keys.filter(k => k !== key);
323
+ // Only write if the key was actually present
324
+ if (filtered.length !== keys.length) {
325
+ // Atomic write: tmp file + rename prevents partial writes on crash
326
+ const tmpFile = file + ".tmp";
327
+ writeFileSync(tmpFile, JSON.stringify(filtered), "utf-8");
328
+ renameSync(tmpFile, file);
329
+ }
307
330
  }
308
331
  } catch (e) { /* non-fatal: removePersistedKey failure */ void e; }
309
332
  }
@@ -335,11 +358,11 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
335
358
  const hasSquashMsg = existsSync(squashMsgPath);
336
359
  if (!hasMergeHead && !hasSquashMsg) return false;
337
360
 
338
- const unmerged = runGit(basePath, ["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
339
- if (!unmerged || !unmerged.trim()) {
361
+ const conflictedFiles = nativeConflictFiles(basePath);
362
+ if (conflictedFiles.length === 0) {
340
363
  // All conflicts resolved — finalize the merge/squash commit
341
364
  try {
342
- runGit(basePath, ["commit", "--no-edit"], { allowFailure: false });
365
+ nativeCommit(basePath, ""); // --no-edit equivalent: use empty message placeholder
343
366
  const mode = hasMergeHead ? "merge" : "squash commit";
344
367
  ctx.ui.notify(`Finalized leftover ${mode} from prior session.`, "info");
345
368
  } catch {
@@ -347,25 +370,21 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
347
370
  }
348
371
  } else {
349
372
  // Still conflicted — try auto-resolving .gsd/ state file conflicts (#530)
350
- const conflictedFiles = unmerged.trim().split("\n").filter(Boolean);
351
373
  const gsdConflicts = conflictedFiles.filter(f => f.startsWith(".gsd/"));
352
374
  const codeConflicts = conflictedFiles.filter(f => !f.startsWith(".gsd/"));
353
375
 
354
376
  if (gsdConflicts.length > 0 && codeConflicts.length === 0) {
355
377
  // All conflicts are in .gsd/ state files — auto-resolve by accepting theirs
356
378
  let resolved = true;
357
- for (const gsdFile of gsdConflicts) {
358
- try {
359
- runGit(basePath, ["checkout", "--theirs", "--", gsdFile], { allowFailure: false });
360
- runGit(basePath, ["add", "--", gsdFile], { allowFailure: false });
361
- } catch {
362
- resolved = false;
363
- break;
364
- }
379
+ try {
380
+ nativeCheckoutTheirs(basePath, gsdConflicts);
381
+ nativeAddPaths(basePath, gsdConflicts);
382
+ } catch {
383
+ resolved = false;
365
384
  }
366
385
  if (resolved) {
367
386
  try {
368
- runGit(basePath, ["commit", "--no-edit"], { allowFailure: false });
387
+ nativeCommit(basePath, "chore: auto-resolve .gsd/ state file conflicts");
369
388
  ctx.ui.notify(
370
389
  `Auto-resolved ${gsdConflicts.length} .gsd/ state file conflict(s) from prior merge.`,
371
390
  "info",
@@ -376,11 +395,11 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
376
395
  }
377
396
  if (!resolved) {
378
397
  if (hasMergeHead) {
379
- runGit(basePath, ["merge", "--abort"], { allowFailure: true });
398
+ try { nativeMergeAbort(basePath); } catch { /* best-effort */ }
380
399
  } else if (hasSquashMsg) {
381
400
  try { unlinkSync(squashMsgPath); } catch { /* best-effort */ }
382
401
  }
383
- runGit(basePath, ["reset", "--hard", "HEAD"], { allowFailure: true });
402
+ try { nativeResetHard(basePath); } catch { /* best-effort */ }
384
403
  ctx.ui.notify(
385
404
  "Detected leftover merge state — auto-resolve failed, cleaned up. Re-deriving state.",
386
405
  "warning",
@@ -389,11 +408,11 @@ export function reconcileMergeState(basePath: string, ctx: ExtensionContext): bo
389
408
  } else {
390
409
  // Code conflicts present — abort and reset
391
410
  if (hasMergeHead) {
392
- runGit(basePath, ["merge", "--abort"], { allowFailure: true });
411
+ try { nativeMergeAbort(basePath); } catch { /* best-effort */ }
393
412
  } else if (hasSquashMsg) {
394
413
  try { unlinkSync(squashMsgPath); } catch { /* best-effort */ }
395
414
  }
396
- runGit(basePath, ["reset", "--hard", "HEAD"], { allowFailure: true });
415
+ try { nativeResetHard(basePath); } catch { /* best-effort */ }
397
416
  ctx.ui.notify(
398
417
  "Detected leftover merge state with unresolved conflicts — cleaned up. Re-deriving state.",
399
418
  "warning",
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { clearLock } from "./crash-recovery.js";
8
- import { execSync } from "node:child_process";
8
+ import { nativeHasChanges } from "./native-git-bridge.js";
9
9
 
10
10
  // ─── SIGTERM Handling ─────────────────────────────────────────────────────────
11
11
 
@@ -47,12 +47,7 @@ export function deregisterSigtermHandler(handler: (() => void) | null): void {
47
47
  */
48
48
  export function detectWorkingTreeActivity(cwd: string): boolean {
49
49
  try {
50
- const out = execSync("git status --porcelain", {
51
- cwd,
52
- stdio: ["pipe", "pipe", "pipe"],
53
- timeout: 5000,
54
- });
55
- return out.toString().trim().length > 0;
50
+ return nativeHasChanges(cwd);
56
51
  } catch {
57
52
  return false;
58
53
  }