gsd-pi 2.31.1 → 2.31.2-dev.2453512

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 (37) hide show
  1. package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
  3. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  4. package/dist/resources/extensions/gsd/auto-dispatch.ts +18 -22
  5. package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
  6. package/dist/resources/extensions/gsd/auto-prompts.ts +43 -46
  7. package/dist/resources/extensions/gsd/auto-start.ts +4 -4
  8. package/dist/resources/extensions/gsd/auto.ts +54 -33
  9. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
  10. package/dist/resources/extensions/gsd/git-service.ts +9 -0
  11. package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  12. package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
  13. package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
  14. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
  15. package/dist/resources/extensions/gsd/quick.ts +3 -5
  16. package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  17. package/dist/resources/extensions/gsd/tests/run-uat.test.ts +101 -2
  18. package/package.json +1 -1
  19. package/packages/pi-coding-agent/package.json +1 -1
  20. package/pkg/package.json +1 -1
  21. package/src/resources/extensions/gsd/auto-constants.ts +6 -0
  22. package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
  23. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  24. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -22
  25. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
  26. package/src/resources/extensions/gsd/auto-prompts.ts +43 -46
  27. package/src/resources/extensions/gsd/auto-start.ts +4 -4
  28. package/src/resources/extensions/gsd/auto.ts +54 -33
  29. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
  30. package/src/resources/extensions/gsd/git-service.ts +9 -0
  31. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  32. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  33. package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
  34. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
  35. package/src/resources/extensions/gsd/quick.ts +3 -5
  36. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  37. package/src/resources/extensions/gsd/tests/run-uat.test.ts +101 -2
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared constants for auto-mode modules (auto.ts, auto-post-unit.ts, etc.).
3
+ */
4
+
5
+ /** Throttle STATE.md rebuilds — at most once per 30 seconds. */
6
+ export const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
@@ -48,40 +48,34 @@ export interface AutoDashboardData {
48
48
 
49
49
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
50
50
 
51
+ /** Canonical verb and phase label for each known unit type. */
52
+ const UNIT_TYPE_INFO: Record<string, { verb: string; phaseLabel: string }> = {
53
+ "research-milestone": { verb: "researching", phaseLabel: "RESEARCH" },
54
+ "research-slice": { verb: "researching", phaseLabel: "RESEARCH" },
55
+ "plan-milestone": { verb: "planning", phaseLabel: "PLAN" },
56
+ "plan-slice": { verb: "planning", phaseLabel: "PLAN" },
57
+ "execute-task": { verb: "executing", phaseLabel: "EXECUTE" },
58
+ "complete-slice": { verb: "completing", phaseLabel: "COMPLETE" },
59
+ "replan-slice": { verb: "replanning", phaseLabel: "REPLAN" },
60
+ "rewrite-docs": { verb: "rewriting", phaseLabel: "REWRITE" },
61
+ "reassess-roadmap": { verb: "reassessing", phaseLabel: "REASSESS" },
62
+ "run-uat": { verb: "running UAT", phaseLabel: "UAT" },
63
+ };
64
+
51
65
  export function unitVerb(unitType: string): string {
52
66
  if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
53
- switch (unitType) {
54
- case "research-milestone":
55
- case "research-slice": return "researching";
56
- case "plan-milestone":
57
- case "plan-slice": return "planning";
58
- case "execute-task": return "executing";
59
- case "complete-slice": return "completing";
60
- case "replan-slice": return "replanning";
61
- case "rewrite-docs": return "rewriting";
62
- case "reassess-roadmap": return "reassessing";
63
- case "run-uat": return "running UAT";
64
- default: return unitType;
65
- }
67
+ return UNIT_TYPE_INFO[unitType]?.verb ?? unitType;
66
68
  }
67
69
 
68
70
  export function unitPhaseLabel(unitType: string): string {
69
71
  if (unitType.startsWith("hook/")) return "HOOK";
70
- switch (unitType) {
71
- case "research-milestone": return "RESEARCH";
72
- case "research-slice": return "RESEARCH";
73
- case "plan-milestone": return "PLAN";
74
- case "plan-slice": return "PLAN";
75
- case "execute-task": return "EXECUTE";
76
- case "complete-slice": return "COMPLETE";
77
- case "replan-slice": return "REPLAN";
78
- case "rewrite-docs": return "REWRITE";
79
- case "reassess-roadmap": return "REASSESS";
80
- case "run-uat": return "UAT";
81
- default: return unitType.toUpperCase();
82
- }
72
+ return UNIT_TYPE_INFO[unitType]?.phaseLabel ?? unitType.toUpperCase();
83
73
  }
84
74
 
75
+ /**
76
+ * Describe the expected next step after the current unit completes.
77
+ * Unit types here mirror the keys in UNIT_TYPE_INFO above.
78
+ */
85
79
  function peekNext(unitType: string, state: GSDState): string {
86
80
  // Show active hook info in progress display
87
81
  const activeHookState = getActiveHook();
@@ -182,15 +182,10 @@ export async function dispatchDirectPhase(
182
182
  ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
183
183
  return;
184
184
  }
185
- const uatContent = await loadFile(uatFile);
186
- if (!uatContent) {
187
- ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
188
- return;
189
- }
190
185
  const uatPath = relSliceFile(base, mid, sid, "UAT");
191
186
  unitType = "run-uat";
192
187
  unitId = `${mid}/${sid}`;
193
- prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
188
+ prompt = await buildRunUatPrompt(mid, sid, uatPath, base);
194
189
  break;
195
190
  }
196
191
 
@@ -11,8 +11,7 @@
11
11
 
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
- import type { UatType } from "./files.js";
15
- import { loadFile, extractUatType, loadActiveOverrides, parseRoadmap } from "./files.js";
14
+ import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
16
15
  import {
17
16
  resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile,
18
17
  relSliceFile, buildMilestoneFileName,
@@ -39,7 +38,7 @@ import {
39
38
  // ─── Types ────────────────────────────────────────────────────────────────
40
39
 
41
40
  export type DispatchAction =
42
- | { action: "dispatch"; unitType: string; unitId: string; prompt: string; pauseAfterDispatch?: boolean }
41
+ | { action: "dispatch"; unitType: string; unitId: string; prompt: string }
43
42
  | { action: "stop"; reason: string; level: "info" | "warning" | "error" }
44
43
  | { action: "skip" };
45
44
 
@@ -104,25 +103,6 @@ const DISPATCH_RULES: DispatchRule[] = [
104
103
  };
105
104
  },
106
105
  },
107
- {
108
- name: "run-uat (post-completion)",
109
- match: async ({ state, mid, basePath, prefs }) => {
110
- const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
111
- if (!needsRunUat) return null;
112
- const { sliceId, uatType } = needsRunUat;
113
- const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
114
- const uatContent = await loadFile(uatFile);
115
- return {
116
- action: "dispatch",
117
- unitType: "run-uat",
118
- unitId: `${mid}/${sliceId}`,
119
- prompt: await buildRunUatPrompt(
120
- mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
121
- ),
122
- pauseAfterDispatch: uatType !== "artifact-driven",
123
- };
124
- },
125
- },
126
106
  {
127
107
  name: "uat-verdict-gate (non-PASS blocks progression)",
128
108
  match: async ({ mid, basePath, prefs }) => {
@@ -152,6 +132,22 @@ const DISPATCH_RULES: DispatchRule[] = [
152
132
  return null;
153
133
  },
154
134
  },
135
+ {
136
+ name: "run-uat (post-completion)",
137
+ match: async ({ state, mid, basePath, prefs }) => {
138
+ const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
139
+ if (!needsRunUat) return null;
140
+ const { sliceId } = needsRunUat;
141
+ return {
142
+ action: "dispatch",
143
+ unitType: "run-uat",
144
+ unitId: `${mid}/${sliceId}`,
145
+ prompt: await buildRunUatPrompt(
146
+ mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), basePath,
147
+ ),
148
+ };
149
+ },
150
+ },
155
151
  {
156
152
  name: "reassess-roadmap (post-completion)",
157
153
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
@@ -60,9 +60,31 @@ import {
60
60
  hideFooter,
61
61
  } from "./auto-dashboard.js";
62
62
  import { join } from "node:path";
63
+ import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
63
64
 
64
- /** Throttle STATE.md rebuilds — at most once per 30 seconds */
65
- const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
65
+ /**
66
+ * Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
67
+ * and persist the initial runtime record. Returns `startedAt` for callers
68
+ * that need the timestamp.
69
+ */
70
+ function dispatchUnit(
71
+ s: AutoSession,
72
+ basePath: string,
73
+ unitType: string,
74
+ unitId: string,
75
+ ): number {
76
+ const startedAt = Date.now();
77
+ s.currentUnit = { type: unitType, id: unitId, startedAt };
78
+ writeUnitRuntimeRecord(basePath, unitType, unitId, startedAt, {
79
+ phase: "dispatched",
80
+ wrapupWarningSent: false,
81
+ timeoutAt: null,
82
+ lastProgressAt: startedAt,
83
+ progressCount: 0,
84
+ lastProgressKind: "dispatch",
85
+ });
86
+ return startedAt;
87
+ }
66
88
 
67
89
  export interface PostUnitContext {
68
90
  s: AutoSession;
@@ -364,19 +386,10 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
364
386
  if (s.currentUnit && !s.stepMode) {
365
387
  const hookUnit = checkPostUnitHooks(s.currentUnit.type, s.currentUnit.id, s.basePath);
366
388
  if (hookUnit) {
367
- const hookStartedAt = Date.now();
368
389
  if (s.currentUnit) {
369
390
  await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
370
391
  }
371
- s.currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
372
- writeUnitRuntimeRecord(s.basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
373
- phase: "dispatched",
374
- wrapupWarningSent: false,
375
- timeoutAt: null,
376
- lastProgressAt: hookStartedAt,
377
- progressCount: 0,
378
- lastProgressKind: "dispatch",
379
- });
392
+ dispatchUnit(s, s.basePath, hookUnit.unitType, hookUnit.unitId);
380
393
 
381
394
  const state = await deriveState(s.basePath);
382
395
  updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
@@ -498,16 +511,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
498
511
 
499
512
  const triageUnitType = "triage-captures";
500
513
  const triageUnitId = `${mid}/${sid}/triage`;
501
- const triageStartedAt = Date.now();
502
- s.currentUnit = { type: triageUnitType, id: triageUnitId, startedAt: triageStartedAt };
503
- writeUnitRuntimeRecord(s.basePath, triageUnitType, triageUnitId, triageStartedAt, {
504
- phase: "dispatched",
505
- wrapupWarningSent: false,
506
- timeoutAt: null,
507
- lastProgressAt: triageStartedAt,
508
- progressCount: 0,
509
- lastProgressKind: "dispatch",
510
- });
514
+ dispatchUnit(s, s.basePath, triageUnitType, triageUnitId);
511
515
  updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
512
516
 
513
517
  const result = await s.cmdCtx!.newSession();
@@ -568,16 +572,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
568
572
 
569
573
  const qtUnitType = "quick-task";
570
574
  const qtUnitId = `${s.currentMilestoneId}/${capture.id}`;
571
- const qtStartedAt = Date.now();
572
- s.currentUnit = { type: qtUnitType, id: qtUnitId, startedAt: qtStartedAt };
573
- writeUnitRuntimeRecord(s.basePath, qtUnitType, qtUnitId, qtStartedAt, {
574
- phase: "dispatched",
575
- wrapupWarningSent: false,
576
- timeoutAt: null,
577
- lastProgressAt: qtStartedAt,
578
- progressCount: 0,
579
- lastProgressKind: "dispatch",
580
- });
575
+ dispatchUnit(s, s.basePath, qtUnitType, qtUnitId);
581
576
  const state = await deriveState(s.basePath);
582
577
  updateProgressWidget(ctx, qtUnitType, qtUnitId, state);
583
578
 
@@ -324,6 +324,27 @@ function oneLine(text: string): string {
324
324
  return text.replace(/\s+/g, " ").trim();
325
325
  }
326
326
 
327
+ /** Build the standard inlined-context section used by all prompt builders. */
328
+ function buildInlinedContextSection(inlined: string[]): string {
329
+ return `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
330
+ }
331
+
332
+ /** Build the formatted list of available GSD source files for planners to read on demand. */
333
+ function buildSourceFileList(base: string, opts?: { includeProject?: boolean }): string {
334
+ const paths: string[] = [];
335
+ if (opts?.includeProject && existsSync(resolveGsdRootFile(base, "PROJECT")))
336
+ paths.push(`- **Project**: \`${relGsdRootFile("PROJECT")}\``);
337
+ if (existsSync(resolveGsdRootFile(base, "REQUIREMENTS")))
338
+ paths.push(`- **Requirements**: \`${relGsdRootFile("REQUIREMENTS")}\``);
339
+ if (existsSync(resolveGsdRootFile(base, "DECISIONS")))
340
+ paths.push(`- **Decisions**: \`${relGsdRootFile("DECISIONS")}\``);
341
+ if (paths.length === 0) {
342
+ const types = opts?.includeProject ? "project/requirements/decisions" : "requirements/decisions";
343
+ return `_No ${types} files found._`;
344
+ }
345
+ return paths.join("\n");
346
+ }
347
+
327
348
  // ─── Section Builders ──────────────────────────────────────────────────────
328
349
 
329
350
  export function buildResumeSection(
@@ -530,25 +551,21 @@ export async function checkNeedsRunUat(
530
551
  const uatContent = await loadFile(uatFile);
531
552
  if (!uatContent) return null;
532
553
 
533
- // If UAT result already exists with a PASS verdict, skip (idempotent).
534
- // Non-PASS verdicts (FAIL, surfaced-for-human-review) should block slice
535
- // progression return the slice for re-evaluation (#1231).
554
+ // If a UAT result already exists, the UAT unit has already run and must not
555
+ // be re-dispatched. PASS means progression can continue; any non-PASS verdict
556
+ // must be handled by the dispatch table's verdict gate, which stops progression
557
+ // with a human-action message instead of replaying the same run-uat unit.
536
558
  const uatResultFile = resolveSliceFile(base, mid, sid, "UAT-RESULT");
537
559
  if (uatResultFile) {
538
560
  const resultContent = await loadFile(uatResultFile);
539
- if (resultContent) {
540
- const verdictMatch = resultContent.match(/verdict:\s*([\w-]+)/i);
541
- const verdict = verdictMatch?.[1]?.toLowerCase();
542
- if (verdict === "pass" || verdict === "passed") return null; // PASS — skip
543
- // Non-PASS verdict exists — don't re-run UAT, but don't advance either.
544
- // Return null here since the UAT already ran; the dispatch table's
545
- // complete-slice rule should check the verdict before advancing.
546
- // For now, returning the slice signals it still needs attention.
547
- }
561
+ if (resultContent) return null;
548
562
  }
549
563
 
550
- // Classify UAT type; unknown type treat as human-experience (human review)
564
+ // Classify UAT type; skip non-artifact-driven types auto-mode can only
565
+ // execute mechanical checks. Non-artifact UATs are tracked in the dashboard
566
+ // but don't block auto-mode progression.
551
567
  const uatType = extractUatType(uatContent) ?? "human-experience";
568
+ if (uatType !== "artifact-driven") return null;
552
569
 
553
570
  return { sliceId: sid, uatType };
554
571
  }
@@ -571,7 +588,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
571
588
  if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
572
589
  inlined.push(inlineTemplate("research", "Research"));
573
590
 
574
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
591
+ const inlinedContext = buildInlinedContextSection(inlined);
575
592
 
576
593
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
577
594
  return loadPrompt("research-milestone", {
@@ -599,17 +616,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
599
616
  const { inlinePriorMilestoneSummary } = await import("./files.js");
600
617
  const priorSummaryInline = await inlinePriorMilestoneSummary(mid, base);
601
618
  if (priorSummaryInline) inlined.push(priorSummaryInline);
602
- // Build source file paths for the planner to read on demand (reduces inlining)
603
- const sourcePaths: string[] = [];
604
- if (existsSync(resolveGsdRootFile(base, "PROJECT")))
605
- sourcePaths.push(`- **Project**: \`${relGsdRootFile("PROJECT")}\``);
606
- if (existsSync(resolveGsdRootFile(base, "REQUIREMENTS")))
607
- sourcePaths.push(`- **Requirements**: \`${relGsdRootFile("REQUIREMENTS")}\``);
608
- if (existsSync(resolveGsdRootFile(base, "DECISIONS")))
609
- sourcePaths.push(`- **Decisions**: \`${relGsdRootFile("DECISIONS")}\``);
610
- const sourceFilePaths = sourcePaths.length > 0
611
- ? sourcePaths.join("\n")
612
- : "_No project/requirements/decisions files found._";
619
+ const sourceFilePaths = buildSourceFileList(base, { includeProject: true });
613
620
 
614
621
  const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
615
622
  if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
@@ -625,7 +632,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
625
632
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
626
633
  }
627
634
 
628
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
635
+ const inlinedContext = buildInlinedContextSection(inlined);
629
636
 
630
637
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
631
638
  const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
@@ -674,7 +681,7 @@ export async function buildResearchSlicePrompt(
674
681
  const overridesInline = formatOverridesSection(activeOverrides);
675
682
  if (overridesInline) inlined.unshift(overridesInline);
676
683
 
677
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
684
+ const inlinedContext = buildInlinedContextSection(inlined);
678
685
 
679
686
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
680
687
  return loadPrompt("research-slice", {
@@ -704,15 +711,7 @@ export async function buildPlanSlicePrompt(
704
711
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
705
712
  const researchInline = await inlineFileOptional(researchPath, researchRel, "Slice Research");
706
713
  if (researchInline) inlined.push(researchInline);
707
- // Build source file paths for the planner to read on demand (reduces inlining)
708
- const sliceSourcePaths: string[] = [];
709
- if (existsSync(resolveGsdRootFile(base, "REQUIREMENTS")))
710
- sliceSourcePaths.push(`- **Requirements**: \`${relGsdRootFile("REQUIREMENTS")}\``);
711
- if (existsSync(resolveGsdRootFile(base, "DECISIONS")))
712
- sliceSourcePaths.push(`- **Decisions**: \`${relGsdRootFile("DECISIONS")}\``);
713
- const sliceSourceFilePaths = sliceSourcePaths.length > 0
714
- ? sliceSourcePaths.join("\n")
715
- : "_No requirements/decisions files found._";
714
+ const sliceSourceFilePaths = buildSourceFileList(base);
716
715
 
717
716
  const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
718
717
  if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
@@ -726,7 +725,7 @@ export async function buildPlanSlicePrompt(
726
725
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
727
726
  if (planOverridesInline) inlined.unshift(planOverridesInline);
728
727
 
729
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
728
+ const inlinedContext = buildInlinedContextSection(inlined);
730
729
 
731
730
  // Build executor context constraints from the budget engine
732
731
  const executorContextConstraints = formatExecutorConstraints();
@@ -901,7 +900,7 @@ export async function buildCompleteSlicePrompt(
901
900
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
902
901
  if (completeOverridesInline) inlined.unshift(completeOverridesInline);
903
902
 
904
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
903
+ const inlinedContext = buildInlinedContextSection(inlined);
905
904
 
906
905
  const sliceRel = relSlicePath(base, mid, sid);
907
906
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
@@ -960,7 +959,7 @@ export async function buildCompleteMilestonePrompt(
960
959
  if (contextInline) inlined.push(contextInline);
961
960
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
962
961
 
963
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
962
+ const inlinedContext = buildInlinedContextSection(inlined);
964
963
 
965
964
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
966
965
 
@@ -1031,7 +1030,7 @@ export async function buildValidateMilestonePrompt(
1031
1030
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
1032
1031
  if (contextInline) inlined.push(contextInline);
1033
1032
 
1034
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1033
+ const inlinedContext = buildInlinedContextSection(inlined);
1035
1034
 
1036
1035
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
1037
1036
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
@@ -1085,7 +1084,7 @@ export async function buildReplanSlicePrompt(
1085
1084
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
1086
1085
  if (replanOverridesInline) inlined.unshift(replanOverridesInline);
1087
1086
 
1088
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1087
+ const inlinedContext = buildInlinedContextSection(inlined);
1089
1088
 
1090
1089
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
1091
1090
 
@@ -1118,7 +1117,7 @@ export async function buildReplanSlicePrompt(
1118
1117
  }
1119
1118
 
1120
1119
  export async function buildRunUatPrompt(
1121
- mid: string, sliceId: string, uatPath: string, uatContent: string, base: string,
1120
+ mid: string, sliceId: string, uatPath: string, base: string,
1122
1121
  ): Promise<string> {
1123
1122
  const inlined: string[] = [];
1124
1123
  inlined.push(await inlineFile(resolveSliceFile(base, mid, sliceId, "UAT"), uatPath, `${sliceId} UAT`));
@@ -1133,10 +1132,9 @@ export async function buildRunUatPrompt(
1133
1132
  const projectInline = await inlineProjectFromDb(base);
1134
1133
  if (projectInline) inlined.push(projectInline);
1135
1134
 
1136
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1135
+ const inlinedContext = buildInlinedContextSection(inlined);
1137
1136
 
1138
1137
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1139
- const uatType = extractUatType(uatContent) ?? "human-experience";
1140
1138
 
1141
1139
  return loadPrompt("run-uat", {
1142
1140
  workingDirectory: base,
@@ -1144,7 +1142,6 @@ export async function buildRunUatPrompt(
1144
1142
  sliceId,
1145
1143
  uatPath,
1146
1144
  uatResultPath,
1147
- uatType,
1148
1145
  inlinedContext,
1149
1146
  });
1150
1147
  }
@@ -1172,7 +1169,7 @@ export async function buildReassessRoadmapPrompt(
1172
1169
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1173
1170
  if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
1174
1171
 
1175
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1172
+ const inlinedContext = buildInlinedContextSection(inlined);
1176
1173
 
1177
1174
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1178
1175
 
@@ -38,7 +38,7 @@ import {
38
38
  import { selfHealRuntimeRecords } from "./auto-recovery.js";
39
39
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
40
40
  import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
41
- import { GitServiceImpl } from "./git-service.js";
41
+ import { createGitService } from "./git-service.js";
42
42
  import {
43
43
  captureIntegrationBranch,
44
44
  detectWorktreeName,
@@ -129,7 +129,7 @@ export async function bootstrapAutoSession(
129
129
  }
130
130
 
131
131
  // Initialize GitServiceImpl
132
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
132
+ s.gitService = createGitService(s.basePath);
133
133
 
134
134
  // Check for crash from previous session (use both old and new lock data)
135
135
  const crashLock = readCrashLock(base);
@@ -330,12 +330,12 @@ export async function bootstrapAutoSession(
330
330
  if (existingWtPath) {
331
331
  const wtPath = enterAutoWorktree(base, s.currentMilestoneId);
332
332
  s.basePath = wtPath;
333
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
333
+ s.gitService = createGitService(s.basePath);
334
334
  ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
335
335
  } else {
336
336
  const wtPath = createAutoWorktree(base, s.currentMilestoneId);
337
337
  s.basePath = wtPath;
338
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
338
+ s.gitService = createGitService(s.basePath);
339
339
  ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
340
340
  }
341
341
  registerSigtermHandler(s.originalBasePath);