gsd-pi 2.31.2-dev.64d8832 → 2.31.2-dev.91f95cf

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 (33) 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 +4 -8
  5. package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
  6. package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
  7. package/dist/resources/extensions/gsd/auto-start.ts +4 -4
  8. package/dist/resources/extensions/gsd/auto.ts +11 -22
  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/run-uat.test.ts +56 -7
  17. package/package.json +1 -1
  18. package/src/resources/extensions/gsd/auto-constants.ts +6 -0
  19. package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
  20. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  21. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
  22. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
  23. package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
  24. package/src/resources/extensions/gsd/auto-start.ts +4 -4
  25. package/src/resources/extensions/gsd/auto.ts +11 -22
  26. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
  27. package/src/resources/extensions/gsd/git-service.ts +9 -0
  28. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  29. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  30. package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
  31. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
  32. package/src/resources/extensions/gsd/quick.ts +3 -5
  33. package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
@@ -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
 
@@ -138,17 +137,14 @@ const DISPATCH_RULES: DispatchRule[] = [
138
137
  match: async ({ state, mid, basePath, prefs }) => {
139
138
  const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
140
139
  if (!needsRunUat) return null;
141
- const { sliceId, uatType } = needsRunUat;
142
- const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
143
- const uatContent = await loadFile(uatFile);
140
+ const { sliceId } = needsRunUat;
144
141
  return {
145
142
  action: "dispatch",
146
143
  unitType: "run-uat",
147
144
  unitId: `${mid}/${sliceId}`,
148
145
  prompt: await buildRunUatPrompt(
149
- mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
146
+ mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), basePath,
150
147
  ),
151
- pauseAfterDispatch: uatType !== "artifact-driven",
152
148
  };
153
149
  },
154
150
  },
@@ -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(
@@ -540,8 +561,11 @@ export async function checkNeedsRunUat(
540
561
  if (resultContent) return null;
541
562
  }
542
563
 
543
- // 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.
544
567
  const uatType = extractUatType(uatContent) ?? "human-experience";
568
+ if (uatType !== "artifact-driven") return null;
545
569
 
546
570
  return { sliceId: sid, uatType };
547
571
  }
@@ -564,7 +588,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
564
588
  if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
565
589
  inlined.push(inlineTemplate("research", "Research"));
566
590
 
567
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
591
+ const inlinedContext = buildInlinedContextSection(inlined);
568
592
 
569
593
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
570
594
  return loadPrompt("research-milestone", {
@@ -592,17 +616,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
592
616
  const { inlinePriorMilestoneSummary } = await import("./files.js");
593
617
  const priorSummaryInline = await inlinePriorMilestoneSummary(mid, base);
594
618
  if (priorSummaryInline) inlined.push(priorSummaryInline);
595
- // Build source file paths for the planner to read on demand (reduces inlining)
596
- const sourcePaths: string[] = [];
597
- if (existsSync(resolveGsdRootFile(base, "PROJECT")))
598
- sourcePaths.push(`- **Project**: \`${relGsdRootFile("PROJECT")}\``);
599
- if (existsSync(resolveGsdRootFile(base, "REQUIREMENTS")))
600
- sourcePaths.push(`- **Requirements**: \`${relGsdRootFile("REQUIREMENTS")}\``);
601
- if (existsSync(resolveGsdRootFile(base, "DECISIONS")))
602
- sourcePaths.push(`- **Decisions**: \`${relGsdRootFile("DECISIONS")}\``);
603
- const sourceFilePaths = sourcePaths.length > 0
604
- ? sourcePaths.join("\n")
605
- : "_No project/requirements/decisions files found._";
619
+ const sourceFilePaths = buildSourceFileList(base, { includeProject: true });
606
620
 
607
621
  const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
608
622
  if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
@@ -618,7 +632,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
618
632
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
619
633
  }
620
634
 
621
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
635
+ const inlinedContext = buildInlinedContextSection(inlined);
622
636
 
623
637
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
624
638
  const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
@@ -667,7 +681,7 @@ export async function buildResearchSlicePrompt(
667
681
  const overridesInline = formatOverridesSection(activeOverrides);
668
682
  if (overridesInline) inlined.unshift(overridesInline);
669
683
 
670
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
684
+ const inlinedContext = buildInlinedContextSection(inlined);
671
685
 
672
686
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
673
687
  return loadPrompt("research-slice", {
@@ -697,15 +711,7 @@ export async function buildPlanSlicePrompt(
697
711
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
698
712
  const researchInline = await inlineFileOptional(researchPath, researchRel, "Slice Research");
699
713
  if (researchInline) inlined.push(researchInline);
700
- // Build source file paths for the planner to read on demand (reduces inlining)
701
- const sliceSourcePaths: string[] = [];
702
- if (existsSync(resolveGsdRootFile(base, "REQUIREMENTS")))
703
- sliceSourcePaths.push(`- **Requirements**: \`${relGsdRootFile("REQUIREMENTS")}\``);
704
- if (existsSync(resolveGsdRootFile(base, "DECISIONS")))
705
- sliceSourcePaths.push(`- **Decisions**: \`${relGsdRootFile("DECISIONS")}\``);
706
- const sliceSourceFilePaths = sliceSourcePaths.length > 0
707
- ? sliceSourcePaths.join("\n")
708
- : "_No requirements/decisions files found._";
714
+ const sliceSourceFilePaths = buildSourceFileList(base);
709
715
 
710
716
  const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
711
717
  if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
@@ -719,7 +725,7 @@ export async function buildPlanSlicePrompt(
719
725
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
720
726
  if (planOverridesInline) inlined.unshift(planOverridesInline);
721
727
 
722
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
728
+ const inlinedContext = buildInlinedContextSection(inlined);
723
729
 
724
730
  // Build executor context constraints from the budget engine
725
731
  const executorContextConstraints = formatExecutorConstraints();
@@ -894,7 +900,7 @@ export async function buildCompleteSlicePrompt(
894
900
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
895
901
  if (completeOverridesInline) inlined.unshift(completeOverridesInline);
896
902
 
897
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
903
+ const inlinedContext = buildInlinedContextSection(inlined);
898
904
 
899
905
  const sliceRel = relSlicePath(base, mid, sid);
900
906
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
@@ -953,7 +959,7 @@ export async function buildCompleteMilestonePrompt(
953
959
  if (contextInline) inlined.push(contextInline);
954
960
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
955
961
 
956
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
962
+ const inlinedContext = buildInlinedContextSection(inlined);
957
963
 
958
964
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
959
965
 
@@ -1024,7 +1030,7 @@ export async function buildValidateMilestonePrompt(
1024
1030
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
1025
1031
  if (contextInline) inlined.push(contextInline);
1026
1032
 
1027
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1033
+ const inlinedContext = buildInlinedContextSection(inlined);
1028
1034
 
1029
1035
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
1030
1036
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
@@ -1078,7 +1084,7 @@ export async function buildReplanSlicePrompt(
1078
1084
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
1079
1085
  if (replanOverridesInline) inlined.unshift(replanOverridesInline);
1080
1086
 
1081
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1087
+ const inlinedContext = buildInlinedContextSection(inlined);
1082
1088
 
1083
1089
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
1084
1090
 
@@ -1111,7 +1117,7 @@ export async function buildReplanSlicePrompt(
1111
1117
  }
1112
1118
 
1113
1119
  export async function buildRunUatPrompt(
1114
- mid: string, sliceId: string, uatPath: string, uatContent: string, base: string,
1120
+ mid: string, sliceId: string, uatPath: string, base: string,
1115
1121
  ): Promise<string> {
1116
1122
  const inlined: string[] = [];
1117
1123
  inlined.push(await inlineFile(resolveSliceFile(base, mid, sliceId, "UAT"), uatPath, `${sliceId} UAT`));
@@ -1126,10 +1132,9 @@ export async function buildRunUatPrompt(
1126
1132
  const projectInline = await inlineProjectFromDb(base);
1127
1133
  if (projectInline) inlined.push(projectInline);
1128
1134
 
1129
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1135
+ const inlinedContext = buildInlinedContextSection(inlined);
1130
1136
 
1131
1137
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1132
- const uatType = extractUatType(uatContent) ?? "human-experience";
1133
1138
 
1134
1139
  return loadPrompt("run-uat", {
1135
1140
  workingDirectory: base,
@@ -1137,7 +1142,6 @@ export async function buildRunUatPrompt(
1137
1142
  sliceId,
1138
1143
  uatPath,
1139
1144
  uatResultPath,
1140
- uatType,
1141
1145
  inlinedContext,
1142
1146
  });
1143
1147
  }
@@ -1165,7 +1169,7 @@ export async function buildReassessRoadmapPrompt(
1165
1169
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1166
1170
  if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
1167
1171
 
1168
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1172
+ const inlinedContext = buildInlinedContextSection(inlined);
1169
1173
 
1170
1174
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1171
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);
@@ -118,7 +118,7 @@ import {
118
118
  parseSliceBranch,
119
119
  setActiveMilestoneId,
120
120
  } from "./worktree.js";
121
- import { GitServiceImpl, type TaskCommitContext } from "./git-service.js";
121
+ import { createGitService, type TaskCommitContext } from "./git-service.js";
122
122
  import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
123
123
  import { formatGitError } from "./git-self-heal.js";
124
124
  import {
@@ -204,8 +204,7 @@ import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerifi
204
204
  // ─────────────────────────────────────────────────────────────────────────────
205
205
  const s = new AutoSession();
206
206
 
207
- /** Throttle STATE.md rebuilds at most once per 30 seconds */
208
- const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
207
+ import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
209
208
 
210
209
  export function shouldUseWorktreeIsolation(): boolean {
211
210
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
@@ -462,7 +461,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
462
461
  try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: e instanceof Error ? e.message : String(e) }); }
463
462
  teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
464
463
  s.basePath = s.originalBasePath;
465
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
464
+ s.gitService = createGitService(s.basePath);
466
465
  ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
467
466
  } catch (err) {
468
467
  ctx?.ui.notify(
@@ -626,12 +625,12 @@ export async function startAuto(
626
625
  if (existingWtPath) {
627
626
  const wtPath = enterAutoWorktree(s.originalBasePath, s.currentMilestoneId);
628
627
  s.basePath = wtPath;
629
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
628
+ s.gitService = createGitService(s.basePath);
630
629
  ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
631
630
  } else {
632
631
  const wtPath = createAutoWorktree(s.originalBasePath, s.currentMilestoneId);
633
632
  s.basePath = wtPath;
634
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
633
+ s.gitService = createGitService(s.basePath);
635
634
  ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
636
635
  }
637
636
  } catch (err) {
@@ -1124,7 +1123,7 @@ async function dispatchNextUnit(
1124
1123
  }
1125
1124
 
1126
1125
  s.basePath = s.originalBasePath;
1127
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1126
+ s.gitService = createGitService(s.basePath);
1128
1127
  invalidateAllCaches();
1129
1128
 
1130
1129
  state = await deriveState(s.basePath);
@@ -1136,7 +1135,7 @@ async function dispatchNextUnit(
1136
1135
  try {
1137
1136
  const wtPath = createAutoWorktree(s.basePath, mid);
1138
1137
  s.basePath = wtPath;
1139
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1138
+ s.gitService = createGitService(s.basePath);
1140
1139
  ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
1141
1140
  } catch (err) {
1142
1141
  ctx.ui.notify(
@@ -1176,7 +1175,7 @@ async function dispatchNextUnit(
1176
1175
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1177
1176
  const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1178
1177
  s.basePath = s.originalBasePath;
1179
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1178
+ s.gitService = createGitService(s.basePath);
1180
1179
  ctx.ui.notify(
1181
1180
  `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1182
1181
  "info",
@@ -1201,7 +1200,7 @@ async function dispatchNextUnit(
1201
1200
  if (roadmapPath) {
1202
1201
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1203
1202
  const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1204
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1203
+ s.gitService = createGitService(s.basePath);
1205
1204
  ctx.ui.notify(
1206
1205
  `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1207
1206
  "info",
@@ -1279,7 +1278,7 @@ async function dispatchNextUnit(
1279
1278
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1280
1279
  const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1281
1280
  s.basePath = s.originalBasePath;
1282
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1281
+ s.gitService = createGitService(s.basePath);
1283
1282
  ctx.ui.notify(
1284
1283
  `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1285
1284
  "info",
@@ -1303,7 +1302,7 @@ async function dispatchNextUnit(
1303
1302
  if (roadmapPath) {
1304
1303
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
1305
1304
  const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1306
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1305
+ s.gitService = createGitService(s.basePath);
1307
1306
  ctx.ui.notify(
1308
1307
  `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1309
1308
  "info",
@@ -1457,7 +1456,6 @@ async function dispatchNextUnit(
1457
1456
  unitType = dispatchResult.unitType;
1458
1457
  unitId = dispatchResult.unitId;
1459
1458
  prompt = dispatchResult.prompt;
1460
- let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
1461
1459
 
1462
1460
  // ── Pre-dispatch hooks ──
1463
1461
  const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
@@ -1712,13 +1710,6 @@ async function dispatchNextUnit(
1712
1710
  { triggerTurn: true },
1713
1711
  );
1714
1712
 
1715
- if (pauseAfterUatDispatch) {
1716
- ctx.ui.notify(
1717
- "UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
1718
- "info",
1719
- );
1720
- await pauseAuto(ctx, pi);
1721
- }
1722
1713
  } finally {
1723
1714
  s.dispatching = false;
1724
1715
  }
@@ -1874,8 +1865,6 @@ export async function dispatchHookUnit(
1874
1865
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1875
1866
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
1876
1867
 
1877
- console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
1878
- console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
1879
1868
  pi.sendMessage(
1880
1869
  { customType: "gsd-auto", content: hookPrompt, display: true },
1881
1870
  { triggerTurn: true },
@@ -19,8 +19,7 @@ import {
19
19
  } from "./workflow-templates.js";
20
20
  import { loadPrompt } from "./prompt-loader.js";
21
21
  import { gsdRoot } from "./paths.js";
22
- import { GitServiceImpl, runGit } from "./git-service.js";
23
- import { loadEffectiveGSDPreferences } from "./preferences.js";
22
+ import { createGitService, runGit } from "./git-service.js";
24
23
  import { isAutoActive, isAutoPaused } from "./auto.js";
25
24
 
26
25
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -423,9 +422,8 @@ export async function handleStart(
423
422
 
424
423
  // ─── Create git branch (unless isolation: none) ─────────────────────────
425
424
 
426
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
427
- const git = new GitServiceImpl(basePath, gitPrefs);
428
- const skipBranch = gitPrefs.isolation === "none";
425
+ const git = createGitService(basePath);
426
+ const skipBranch = git.prefs.isolation === "none";
429
427
  const slug = slugify(description || templateId);
430
428
  const branchName = `gsd/${templateId}/${slug}`;
431
429
  let branchCreated = false;
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { gsdRoot } from "./paths.js";
15
15
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
16
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
16
17
 
17
18
  import {
18
19
  detectWorktreeName,
@@ -541,6 +542,14 @@ export class GitServiceImpl {
541
542
 
542
543
  }
543
544
 
545
+ // ─── Factory ───────────────────────────────────────────────────────────────
546
+
547
+ /** Create a GitServiceImpl with the current effective git preferences. */
548
+ export function createGitService(basePath: string): GitServiceImpl {
549
+ const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
550
+ return new GitServiceImpl(basePath, gitPrefs);
551
+ }
552
+
544
553
  // ─── Commit Type Inference ─────────────────────────────────────────────────
545
554
 
546
555
  /**
@@ -23,13 +23,6 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
23
23
  import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
24
24
  import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
25
25
 
26
- // ─── Commit Instruction Helper (local copy — avoids circular dep) ───────────
27
-
28
- /** Build commit instruction for queue prompts. .gsd/ is managed externally and always gitignored. */
29
- function buildDocsCommitInstruction(_message: string): string {
30
- return "Do not commit planning artifacts — .gsd/ is managed externally.";
31
- }
32
-
33
26
  // ─── Queue Entry Point ──────────────────────────────────────────────────────
34
27
 
35
28
  /**
@@ -207,7 +200,7 @@ export async function showQueueAdd(
207
200
  preamble,
208
201
  existingMilestonesContext: existingContext,
209
202
  inlinedTemplates: queueInlinedTemplates,
210
- commitInstruction: buildDocsCommitInstruction("docs: queue <milestone list>"),
203
+ commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
211
204
  });
212
205
 
213
206
  pi.sendMessage(
@@ -86,6 +86,14 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
86
86
  "context_selection",
87
87
  ]);
88
88
 
89
+ /** Canonical list of all dispatch unit types. */
90
+ export const KNOWN_UNIT_TYPES = [
91
+ "research-milestone", "plan-milestone", "research-slice", "plan-slice",
92
+ "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
93
+ "run-uat", "complete-milestone",
94
+ ] as const;
95
+ export type UnitType = (typeof KNOWN_UNIT_TYPES)[number];
96
+
89
97
  export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
90
98
 
91
99
  export interface GSDSkillRule {