gsd-pi 2.31.2 → 2.32.0-dev.3d7932c

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 (138) hide show
  1. package/README.md +27 -20
  2. package/dist/cli.js +5 -5
  3. package/dist/resource-loader.js +13 -3
  4. package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.ts +23 -27
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
  8. package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
  9. package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.ts +32 -37
  11. package/dist/resources/extensions/gsd/auto-prompts.ts +84 -78
  12. package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
  13. package/dist/resources/extensions/gsd/auto-start.ts +16 -12
  14. package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  16. package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
  17. package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
  18. package/dist/resources/extensions/gsd/auto-worktree.ts +5 -4
  19. package/dist/resources/extensions/gsd/auto.ts +82 -60
  20. package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
  21. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
  22. package/dist/resources/extensions/gsd/commands.ts +19 -0
  23. package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
  24. package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
  25. package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  26. package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
  27. package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
  28. package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
  29. package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
  30. package/dist/resources/extensions/gsd/doctor.ts +6 -0
  31. package/dist/resources/extensions/gsd/error-utils.ts +6 -0
  32. package/dist/resources/extensions/gsd/export.ts +2 -1
  33. package/dist/resources/extensions/gsd/git-service.ts +12 -2
  34. package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  35. package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
  36. package/dist/resources/extensions/gsd/health-widget.ts +167 -0
  37. package/dist/resources/extensions/gsd/index.ts +18 -5
  38. package/dist/resources/extensions/gsd/key-manager.ts +2 -1
  39. package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  40. package/dist/resources/extensions/gsd/metrics.ts +3 -3
  41. package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
  42. package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
  43. package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
  44. package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
  45. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  46. package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  47. package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
  48. package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
  49. package/dist/resources/extensions/gsd/progress-score.ts +273 -0
  50. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
  51. package/dist/resources/extensions/gsd/quick.ts +61 -8
  52. package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
  53. package/dist/resources/extensions/gsd/session-lock.ts +12 -1
  54. package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  57. package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  58. package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  59. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  60. package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  61. package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  62. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  63. package/dist/resources/extensions/gsd/undo.ts +5 -7
  64. package/dist/resources/extensions/gsd/unit-id.ts +14 -0
  65. package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
  66. package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
  67. package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
  68. package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
  69. package/dist/worktree-cli.d.ts +42 -6
  70. package/dist/worktree-cli.js +88 -48
  71. package/package.json +1 -1
  72. package/packages/pi-coding-agent/package.json +1 -1
  73. package/pkg/package.json +1 -1
  74. package/src/resources/extensions/gsd/auto-constants.ts +6 -0
  75. package/src/resources/extensions/gsd/auto-dashboard.ts +23 -27
  76. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  77. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
  78. package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
  79. package/src/resources/extensions/gsd/auto-observability.ts +2 -4
  80. package/src/resources/extensions/gsd/auto-post-unit.ts +32 -37
  81. package/src/resources/extensions/gsd/auto-prompts.ts +84 -78
  82. package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
  83. package/src/resources/extensions/gsd/auto-start.ts +16 -12
  84. package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  85. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  86. package/src/resources/extensions/gsd/auto-timers.ts +3 -2
  87. package/src/resources/extensions/gsd/auto-verification.ts +6 -6
  88. package/src/resources/extensions/gsd/auto-worktree.ts +5 -4
  89. package/src/resources/extensions/gsd/auto.ts +82 -60
  90. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  91. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
  92. package/src/resources/extensions/gsd/commands.ts +19 -0
  93. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
  94. package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
  95. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  96. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
  97. package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
  98. package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
  99. package/src/resources/extensions/gsd/doctor-types.ts +14 -1
  100. package/src/resources/extensions/gsd/doctor.ts +6 -0
  101. package/src/resources/extensions/gsd/error-utils.ts +6 -0
  102. package/src/resources/extensions/gsd/export.ts +2 -1
  103. package/src/resources/extensions/gsd/git-service.ts +12 -2
  104. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  105. package/src/resources/extensions/gsd/guided-flow.ts +3 -2
  106. package/src/resources/extensions/gsd/health-widget.ts +167 -0
  107. package/src/resources/extensions/gsd/index.ts +18 -5
  108. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  109. package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  110. package/src/resources/extensions/gsd/metrics.ts +3 -3
  111. package/src/resources/extensions/gsd/migrate-external.ts +21 -4
  112. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  113. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
  114. package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
  115. package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  116. package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  117. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  118. package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
  119. package/src/resources/extensions/gsd/progress-score.ts +273 -0
  120. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
  121. package/src/resources/extensions/gsd/quick.ts +61 -8
  122. package/src/resources/extensions/gsd/repo-identity.ts +22 -1
  123. package/src/resources/extensions/gsd/session-lock.ts +12 -1
  124. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  125. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  126. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  127. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  128. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  129. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  130. package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  131. package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  132. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  133. package/src/resources/extensions/gsd/undo.ts +5 -7
  134. package/src/resources/extensions/gsd/unit-id.ts +14 -0
  135. package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
  136. package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
  137. package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
  138. package/src/resources/extensions/gsd/worktree-command.ts +8 -7
@@ -189,30 +189,52 @@ export async function inlineGsdRootFile(
189
189
  // ─── DB-Aware Inline Helpers ──────────────────────────────────────────────
190
190
 
191
191
  /**
192
- * Inline decisions with optional milestone scoping from the DB.
193
- * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
192
+ * Shared DB-fallback pattern: attempt a DB query via the context-store, format
193
+ * the result, and fall back to the filesystem file when the DB is unavailable
194
+ * or the query yields no results.
195
+ *
196
+ * @param base Project root for filesystem fallback
197
+ * @param label Section heading (e.g. "Decisions")
198
+ * @param filename Filesystem fallback file (e.g. "decisions.md")
199
+ * @param queryDb Async callback receiving the dynamically-imported
200
+ * context-store module. Returns formatted markdown or null.
194
201
  */
195
- export async function inlineDecisionsFromDb(
196
- base: string, milestoneId?: string, scope?: string, level?: InlineLevel,
202
+ async function inlineFromDbOrFile(
203
+ base: string,
204
+ label: string,
205
+ filename: string,
206
+ queryDb: (cs: typeof import("./context-store.js")) => string | null,
197
207
  ): Promise<string | null> {
198
- const inlineLevel = level ?? resolveInlineLevel();
199
208
  try {
200
209
  const { isDbAvailable } = await import("./gsd-db.js");
201
210
  if (isDbAvailable()) {
202
- const { queryDecisions, formatDecisionsForPrompt } = await import("./context-store.js");
203
- const decisions = queryDecisions({ milestoneId, scope });
204
- if (decisions.length > 0) {
205
- // Use compact format for non-full levels to save ~35% tokens
206
- const formatted = inlineLevel !== "full"
207
- ? formatDecisionsCompact(decisions)
208
- : formatDecisionsForPrompt(decisions);
209
- return `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${formatted}`;
211
+ const contextStore = await import("./context-store.js");
212
+ const content = queryDb(contextStore);
213
+ if (content) {
214
+ return `### ${label}\nSource: \`.gsd/${filename.toUpperCase().replace(/\.MD$/i, "")}.md\`\n\n${content}`;
210
215
  }
211
216
  }
212
217
  } catch {
213
218
  // DB not available — fall through to filesystem
214
219
  }
215
- return inlineGsdRootFile(base, "decisions.md", "Decisions");
220
+ return inlineGsdRootFile(base, filename, label);
221
+ }
222
+
223
+ /**
224
+ * Inline decisions with optional milestone scoping from the DB.
225
+ * Falls back to filesystem via inlineGsdRootFile when DB unavailable or empty.
226
+ */
227
+ export async function inlineDecisionsFromDb(
228
+ base: string, milestoneId?: string, scope?: string, level?: InlineLevel,
229
+ ): Promise<string | null> {
230
+ const inlineLevel = level ?? resolveInlineLevel();
231
+ return inlineFromDbOrFile(base, "Decisions", "decisions.md", (cs) => {
232
+ const decisions = cs.queryDecisions({ milestoneId, scope });
233
+ if (decisions.length === 0) return null;
234
+ return inlineLevel !== "full"
235
+ ? formatDecisionsCompact(decisions)
236
+ : cs.formatDecisionsForPrompt(decisions);
237
+ });
216
238
  }
217
239
 
218
240
  /**
@@ -223,23 +245,13 @@ export async function inlineRequirementsFromDb(
223
245
  base: string, sliceId?: string, level?: InlineLevel,
224
246
  ): Promise<string | null> {
225
247
  const inlineLevel = level ?? resolveInlineLevel();
226
- try {
227
- const { isDbAvailable } = await import("./gsd-db.js");
228
- if (isDbAvailable()) {
229
- const { queryRequirements, formatRequirementsForPrompt } = await import("./context-store.js");
230
- const requirements = queryRequirements({ sliceId });
231
- if (requirements.length > 0) {
232
- // Use compact format for non-full levels to save ~40% tokens
233
- const formatted = inlineLevel !== "full"
234
- ? formatRequirementsCompact(requirements)
235
- : formatRequirementsForPrompt(requirements);
236
- return `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${formatted}`;
237
- }
238
- }
239
- } catch {
240
- // DB not available — fall through to filesystem
241
- }
242
- return inlineGsdRootFile(base, "requirements.md", "Requirements");
248
+ return inlineFromDbOrFile(base, "Requirements", "requirements.md", (cs) => {
249
+ const requirements = cs.queryRequirements({ sliceId });
250
+ if (requirements.length === 0) return null;
251
+ return inlineLevel !== "full"
252
+ ? formatRequirementsCompact(requirements)
253
+ : cs.formatRequirementsForPrompt(requirements);
254
+ });
243
255
  }
244
256
 
245
257
  /**
@@ -249,19 +261,9 @@ export async function inlineRequirementsFromDb(
249
261
  export async function inlineProjectFromDb(
250
262
  base: string,
251
263
  ): Promise<string | null> {
252
- try {
253
- const { isDbAvailable } = await import("./gsd-db.js");
254
- if (isDbAvailable()) {
255
- const { queryProject } = await import("./context-store.js");
256
- const content = queryProject();
257
- if (content) {
258
- return `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${content}`;
259
- }
260
- }
261
- } catch {
262
- // DB not available — fall through to filesystem
263
- }
264
- return inlineGsdRootFile(base, "project.md", "Project");
264
+ return inlineFromDbOrFile(base, "Project", "project.md", (cs) => {
265
+ return cs.queryProject();
266
+ });
265
267
  }
266
268
 
267
269
  // ─── Skill Discovery ──────────────────────────────────────────────────────
@@ -324,6 +326,27 @@ function oneLine(text: string): string {
324
326
  return text.replace(/\s+/g, " ").trim();
325
327
  }
326
328
 
329
+ /** Build the standard inlined-context section used by all prompt builders. */
330
+ function buildInlinedContextSection(inlined: string[]): string {
331
+ return `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
332
+ }
333
+
334
+ /** Build the formatted list of available GSD source files for planners to read on demand. */
335
+ function buildSourceFileList(base: string, opts?: { includeProject?: boolean }): string {
336
+ const paths: string[] = [];
337
+ if (opts?.includeProject && existsSync(resolveGsdRootFile(base, "PROJECT")))
338
+ paths.push(`- **Project**: \`${relGsdRootFile("PROJECT")}\``);
339
+ if (existsSync(resolveGsdRootFile(base, "REQUIREMENTS")))
340
+ paths.push(`- **Requirements**: \`${relGsdRootFile("REQUIREMENTS")}\``);
341
+ if (existsSync(resolveGsdRootFile(base, "DECISIONS")))
342
+ paths.push(`- **Decisions**: \`${relGsdRootFile("DECISIONS")}\``);
343
+ if (paths.length === 0) {
344
+ const types = opts?.includeProject ? "project/requirements/decisions" : "requirements/decisions";
345
+ return `_No ${types} files found._`;
346
+ }
347
+ return paths.join("\n");
348
+ }
349
+
327
350
  // ─── Section Builders ──────────────────────────────────────────────────────
328
351
 
329
352
  export function buildResumeSection(
@@ -540,8 +563,11 @@ export async function checkNeedsRunUat(
540
563
  if (resultContent) return null;
541
564
  }
542
565
 
543
- // Classify UAT type; unknown type treat as human-experience (human review)
566
+ // Classify UAT type; skip non-artifact-driven types auto-mode can only
567
+ // execute mechanical checks. Non-artifact UATs are tracked in the dashboard
568
+ // but don't block auto-mode progression.
544
569
  const uatType = extractUatType(uatContent) ?? "human-experience";
570
+ if (uatType !== "artifact-driven") return null;
545
571
 
546
572
  return { sliceId: sid, uatType };
547
573
  }
@@ -564,7 +590,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
564
590
  if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
565
591
  inlined.push(inlineTemplate("research", "Research"));
566
592
 
567
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
593
+ const inlinedContext = buildInlinedContextSection(inlined);
568
594
 
569
595
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
570
596
  return loadPrompt("research-milestone", {
@@ -592,17 +618,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
592
618
  const { inlinePriorMilestoneSummary } = await import("./files.js");
593
619
  const priorSummaryInline = await inlinePriorMilestoneSummary(mid, base);
594
620
  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._";
621
+ const sourceFilePaths = buildSourceFileList(base, { includeProject: true });
606
622
 
607
623
  const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
608
624
  if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
@@ -618,7 +634,7 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
618
634
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
619
635
  }
620
636
 
621
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
637
+ const inlinedContext = buildInlinedContextSection(inlined);
622
638
 
623
639
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
624
640
  const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
@@ -667,7 +683,7 @@ export async function buildResearchSlicePrompt(
667
683
  const overridesInline = formatOverridesSection(activeOverrides);
668
684
  if (overridesInline) inlined.unshift(overridesInline);
669
685
 
670
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
686
+ const inlinedContext = buildInlinedContextSection(inlined);
671
687
 
672
688
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
673
689
  return loadPrompt("research-slice", {
@@ -697,15 +713,7 @@ export async function buildPlanSlicePrompt(
697
713
  inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
698
714
  const researchInline = await inlineFileOptional(researchPath, researchRel, "Slice Research");
699
715
  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._";
716
+ const sliceSourceFilePaths = buildSourceFileList(base);
709
717
 
710
718
  const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
711
719
  if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
@@ -719,7 +727,7 @@ export async function buildPlanSlicePrompt(
719
727
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
720
728
  if (planOverridesInline) inlined.unshift(planOverridesInline);
721
729
 
722
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
730
+ const inlinedContext = buildInlinedContextSection(inlined);
723
731
 
724
732
  // Build executor context constraints from the budget engine
725
733
  const executorContextConstraints = formatExecutorConstraints();
@@ -894,7 +902,7 @@ export async function buildCompleteSlicePrompt(
894
902
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
895
903
  if (completeOverridesInline) inlined.unshift(completeOverridesInline);
896
904
 
897
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
905
+ const inlinedContext = buildInlinedContextSection(inlined);
898
906
 
899
907
  const sliceRel = relSlicePath(base, mid, sid);
900
908
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
@@ -953,7 +961,7 @@ export async function buildCompleteMilestonePrompt(
953
961
  if (contextInline) inlined.push(contextInline);
954
962
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
955
963
 
956
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
964
+ const inlinedContext = buildInlinedContextSection(inlined);
957
965
 
958
966
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
959
967
 
@@ -1024,7 +1032,7 @@ export async function buildValidateMilestonePrompt(
1024
1032
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
1025
1033
  if (contextInline) inlined.push(contextInline);
1026
1034
 
1027
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1035
+ const inlinedContext = buildInlinedContextSection(inlined);
1028
1036
 
1029
1037
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
1030
1038
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
@@ -1078,7 +1086,7 @@ export async function buildReplanSlicePrompt(
1078
1086
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
1079
1087
  if (replanOverridesInline) inlined.unshift(replanOverridesInline);
1080
1088
 
1081
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1089
+ const inlinedContext = buildInlinedContextSection(inlined);
1082
1090
 
1083
1091
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
1084
1092
 
@@ -1111,7 +1119,7 @@ export async function buildReplanSlicePrompt(
1111
1119
  }
1112
1120
 
1113
1121
  export async function buildRunUatPrompt(
1114
- mid: string, sliceId: string, uatPath: string, uatContent: string, base: string,
1122
+ mid: string, sliceId: string, uatPath: string, base: string,
1115
1123
  ): Promise<string> {
1116
1124
  const inlined: string[] = [];
1117
1125
  inlined.push(await inlineFile(resolveSliceFile(base, mid, sliceId, "UAT"), uatPath, `${sliceId} UAT`));
@@ -1126,10 +1134,9 @@ export async function buildRunUatPrompt(
1126
1134
  const projectInline = await inlineProjectFromDb(base);
1127
1135
  if (projectInline) inlined.push(projectInline);
1128
1136
 
1129
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1137
+ const inlinedContext = buildInlinedContextSection(inlined);
1130
1138
 
1131
1139
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1132
- const uatType = extractUatType(uatContent) ?? "human-experience";
1133
1140
 
1134
1141
  return loadPrompt("run-uat", {
1135
1142
  workingDirectory: base,
@@ -1137,7 +1144,6 @@ export async function buildRunUatPrompt(
1137
1144
  sliceId,
1138
1145
  uatPath,
1139
1146
  uatResultPath,
1140
- uatType,
1141
1147
  inlinedContext,
1142
1148
  });
1143
1149
  }
@@ -1165,7 +1171,7 @@ export async function buildReassessRoadmapPrompt(
1165
1171
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1166
1172
  if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
1167
1173
 
1168
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1174
+ const inlinedContext = buildInlinedContextSection(inlined);
1169
1175
 
1170
1176
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1171
1177
 
@@ -42,6 +42,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "
42
42
  import { atomicWriteSync } from "./atomic-write.js";
43
43
  import { loadJsonFileOrNull } from "./json-persistence.js";
44
44
  import { dirname, join } from "node:path";
45
+ import { parseUnitId } from "./unit-id.js";
45
46
 
46
47
  // ─── Artifact Resolution & Verification ───────────────────────────────────────
47
48
 
@@ -49,9 +50,7 @@ import { dirname, join } from "node:path";
49
50
  * Resolve the expected artifact for a unit to an absolute path.
50
51
  */
51
52
  export function resolveExpectedArtifactPath(unitType: string, unitId: string, base: string): string | null {
52
- const parts = unitId.split("/");
53
- const mid = parts[0]!;
54
- const sid = parts[1];
53
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
55
54
  switch (unitType) {
56
55
  case "research-milestone": {
57
56
  const dir = resolveMilestonePath(base, mid);
@@ -78,7 +77,6 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
78
77
  return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
79
78
  }
80
79
  case "execute-task": {
81
- const tid = parts[2];
82
80
  const dir = resolveSlicePath(base, mid, sid!);
83
81
  return dir && tid ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) : null;
84
82
  }
@@ -167,10 +165,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
167
165
 
168
166
  // execute-task must also have its checkbox marked [x] in the slice plan
169
167
  if (unitType === "execute-task") {
170
- const parts = unitId.split("/");
171
- const mid = parts[0];
172
- const sid = parts[1];
173
- const tid = parts[2];
168
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
174
169
  if (mid && sid && tid) {
175
170
  const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
176
171
  if (planAbs && existsSync(planAbs)) {
@@ -187,9 +182,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
187
182
  // but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
188
183
  // to dispatch with a missing task plan (see issue #739).
189
184
  if (unitType === "plan-slice") {
190
- const parts = unitId.split("/");
191
- const mid = parts[0];
192
- const sid = parts[1];
185
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
193
186
  if (mid && sid) {
194
187
  try {
195
188
  const planContent = readFileSync(absPath, "utf-8");
@@ -213,9 +206,8 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
213
206
  // state machine keeps returning the same complete-slice unit (roadmap still shows
214
207
  // the slice incomplete), so dispatchNextUnit recurses forever.
215
208
  if (unitType === "complete-slice") {
216
- const parts = unitId.split("/");
217
- const mid = parts[0];
218
- const sid = parts[1];
209
+ const { milestone: mid, slice: sid } = parseUnitId(unitId);
210
+
219
211
  if (mid && sid) {
220
212
  const dir = resolveSlicePath(base, mid, sid);
221
213
  if (dir) {
@@ -268,9 +260,7 @@ export function writeBlockerPlaceholder(unitType: string, unitId: string, base:
268
260
  }
269
261
 
270
262
  export function diagnoseExpectedArtifact(unitType: string, unitId: string, base: string): string | null {
271
- const parts = unitId.split("/");
272
- const mid = parts[0];
273
- const sid = parts[1];
263
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
274
264
  switch (unitType) {
275
265
  case "research-milestone":
276
266
  return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
@@ -281,7 +271,6 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
281
271
  case "plan-slice":
282
272
  return `${relSliceFile(base, mid!, sid!, "PLAN")} (slice plan)`;
283
273
  case "execute-task": {
284
- const tid = parts[2];
285
274
  return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
286
275
  }
287
276
  case "complete-slice":
@@ -539,10 +528,7 @@ export async function selfHealRuntimeRecords(
539
528
  * These are shown when automatic reconciliation is not possible.
540
529
  */
541
530
  export function buildLoopRemediationSteps(unitType: string, unitId: string, base: string): string | null {
542
- const parts = unitId.split("/");
543
- const mid = parts[0];
544
- const sid = parts[1];
545
- const tid = parts[2];
531
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
546
532
  switch (unitType) {
547
533
  case "execute-task": {
548
534
  if (!mid || !sid || !tid) break;
@@ -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,
@@ -63,6 +63,8 @@ import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath } from "./debug-
63
63
  import type { AutoSession } from "./auto/session.js";
64
64
  import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
65
65
  import { join } from "node:path";
66
+ import { getErrorMessage } from "./error-utils.js";
67
+ import { parseUnitId } from "./unit-id.js";
66
68
 
67
69
  export interface BootstrapDeps {
68
70
  shouldUseWorktreeIsolation: () => boolean;
@@ -129,14 +131,16 @@ export async function bootstrapAutoSession(
129
131
  }
130
132
 
131
133
  // Initialize GitServiceImpl
132
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
134
+ s.gitService = createGitService(s.basePath);
133
135
 
134
- // Check for crash from previous session (use both old and new lock data)
136
+ // Check for crash from previous session (use both old and new lock data).
137
+ // Skip if the lock PID matches this process — acquireSessionLock() writes
138
+ // to the same auto.lock file before this check, so we'd always false-positive.
135
139
  const crashLock = readCrashLock(base);
136
- if (crashLock) {
140
+ if (crashLock && crashLock.pid !== process.pid) {
137
141
  // We already hold the session lock, so no concurrent session is running.
138
142
  // The crash lock is from a dead process — recover context from it.
139
- const recoveredMid = crashLock.unitId.split("/")[0];
143
+ const recoveredMid = parseUnitId(crashLock.unitId).milestone;
140
144
  const milestoneAlreadyComplete = recoveredMid
141
145
  ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
142
146
  : false;
@@ -199,11 +203,11 @@ export async function bootstrapAutoSession(
199
203
  if (!midMatch) continue;
200
204
  const mid = midMatch[1];
201
205
  if (resolveMilestoneFile(base, mid, "SUMMARY")) {
202
- try { unlinkSync(join(runtimeUnitsDir, file)); } catch (e) { debugLog("stale-unit-cleanup-failed", { file, error: e instanceof Error ? e.message : String(e) }); }
206
+ try { unlinkSync(join(runtimeUnitsDir, file)); } catch (e) { debugLog("stale-unit-cleanup-failed", { file, error: getErrorMessage(e) }); }
203
207
  }
204
208
  }
205
209
  }
206
- } catch (e) { debugLog("stale-unit-dir-cleanup-failed", { error: e instanceof Error ? e.message : String(e) }); }
210
+ } catch (e) { debugLog("stale-unit-dir-cleanup-failed", { error: getErrorMessage(e) }); }
207
211
 
208
212
  let state = await deriveState(base);
209
213
 
@@ -330,18 +334,18 @@ export async function bootstrapAutoSession(
330
334
  if (existingWtPath) {
331
335
  const wtPath = enterAutoWorktree(base, s.currentMilestoneId);
332
336
  s.basePath = wtPath;
333
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
337
+ s.gitService = createGitService(s.basePath);
334
338
  ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
335
339
  } else {
336
340
  const wtPath = createAutoWorktree(base, s.currentMilestoneId);
337
341
  s.basePath = wtPath;
338
- s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
342
+ s.gitService = createGitService(s.basePath);
339
343
  ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
340
344
  }
341
345
  registerSigtermHandler(s.originalBasePath);
342
346
  } catch (err) {
343
347
  ctx.ui.notify(
344
- `Auto-worktree setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
348
+ `Auto-worktree setup failed: ${getErrorMessage(err)}. Continuing in project root.`,
345
349
  "warning",
346
350
  );
347
351
  }
@@ -433,7 +437,7 @@ export async function bootstrapAutoSession(
433
437
  }
434
438
  } catch (err) {
435
439
  ctx.ui.notify(
436
- `Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`,
440
+ `Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`,
437
441
  "warning",
438
442
  );
439
443
  }
@@ -451,7 +455,7 @@ export async function bootstrapAutoSession(
451
455
  ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
452
456
  }
453
457
  }
454
- } catch (e) { debugLog("git-lock-cleanup-failed", { error: e instanceof Error ? e.message : String(e) }); }
458
+ } catch (e) { debugLog("git-lock-cleanup-failed", { error: getErrorMessage(e) }); }
455
459
 
456
460
  // Pre-flight: validate milestone queue
457
461
  try {
@@ -39,6 +39,7 @@ import {
39
39
  import type { AutoSession } from "./auto/session.js";
40
40
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
41
41
  import { join } from "node:path";
42
+ import { parseUnitId } from "./unit-id.js";
42
43
 
43
44
  export interface StuckContext {
44
45
  s: AutoSession;
@@ -99,7 +100,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
99
100
 
100
101
  // Final reconciliation pass for execute-task
101
102
  if (unitType === "execute-task") {
102
- const [mid, sid, tid] = unitId.split("/");
103
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
103
104
  if (mid && sid && tid) {
104
105
  const status = await inspectExecuteTaskDurability(basePath, unitId);
105
106
  if (status) {
@@ -168,7 +169,7 @@ export async function checkStuckAndRecover(sctx: StuckContext): Promise<StuckRes
168
169
  // Adaptive self-repair: each retry attempts a different remediation step.
169
170
  if (unitType === "execute-task") {
170
171
  const status = await inspectExecuteTaskDurability(basePath, unitId);
171
- const [mid, sid, tid] = unitId.split("/");
172
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
172
173
  if (status && mid && sid && tid) {
173
174
  if (status.summaryExists && !status.taskChecked) {
174
175
  const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
@@ -18,6 +18,7 @@ import {
18
18
  writeBlockerPlaceholder,
19
19
  } from "./auto-recovery.js";
20
20
  import { existsSync } from "node:fs";
21
+ import { parseUnitId } from "./unit-id.js";
21
22
 
22
23
  export interface RecoveryContext {
23
24
  basePath: string;
@@ -128,7 +129,7 @@ export async function recoverTimedOutUnit(
128
129
 
129
130
  // Retries exhausted — write missing durable artifacts and advance.
130
131
  const diagnostic = formatExecuteTaskRecoveryStatus(status);
131
- const [mid, sid, tid] = unitId.split("/");
132
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
132
133
  const skipped = mid && sid && tid
133
134
  ? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
134
135
  : false;
@@ -20,6 +20,7 @@ import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
20
20
  import { saveActivityLog } from "./activity-log.js";
21
21
  import { recoverTimedOutUnit, type RecoveryContext } from "./auto-timeout-recovery.js";
22
22
  import type { AutoSession } from "./auto/session.js";
23
+ import { getErrorMessage } from "./error-utils.js";
23
24
 
24
25
  export interface SupervisionContext {
25
26
  s: AutoSession;
@@ -127,7 +128,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
127
128
  );
128
129
  await pauseAuto(ctx, pi);
129
130
  } catch (err) {
130
- const message = err instanceof Error ? err.message : String(err);
131
+ const message = getErrorMessage(err);
131
132
  console.error(`[idle-watchdog] Unhandled error: ${message}`);
132
133
  try {
133
134
  ctx.ui.notify(`Idle watchdog error: ${message}`, "warning");
@@ -159,7 +160,7 @@ export function startUnitSupervision(sctx: SupervisionContext): void {
159
160
  );
160
161
  await pauseAuto(ctx, pi);
161
162
  } catch (err) {
162
- const message = err instanceof Error ? err.message : String(err);
163
+ const message = getErrorMessage(err);
163
164
  console.error(`[hard-timeout] Unhandled error: ${message}`);
164
165
  try {
165
166
  ctx.ui.notify(`Hard timeout error: ${message}`, "warning");
@@ -24,6 +24,8 @@ import { writeVerificationJSON } from "./verification-evidence.js";
24
24
  import { removePersistedKey } from "./auto-recovery.js";
25
25
  import type { AutoSession, PendingVerificationRetry } from "./auto/session.js";
26
26
  import { join } from "node:path";
27
+ import { getErrorMessage } from "./error-utils.js";
28
+ import { parseUnitId } from "./unit-id.js";
27
29
 
28
30
  export interface VerificationContext {
29
31
  s: AutoSession;
@@ -57,10 +59,9 @@ export async function runPostUnitVerification(
57
59
  const prefs = effectivePrefs?.preferences;
58
60
 
59
61
  // Read task plan verify field
60
- const parts = s.currentUnit.id.split("/");
62
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
61
63
  let taskPlanVerify: string | undefined;
62
- if (parts.length >= 3) {
63
- const [mid, sid, tid] = parts;
64
+ if (mid && sid && tid) {
64
65
  const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
65
66
  if (planFile) {
66
67
  const planContent = await loadFile(planFile);
@@ -152,9 +153,8 @@ export async function runPostUnitVerification(
152
153
 
153
154
  // Write verification evidence JSON
154
155
  const attempt = s.verificationRetryCount.get(s.currentUnit.id) ?? 0;
155
- if (parts.length >= 3) {
156
+ if (mid && sid && tid) {
156
157
  try {
157
- const [mid, sid, tid] = parts;
158
158
  const sDir = resolveSlicePath(s.basePath, mid, sid);
159
159
  if (sDir) {
160
160
  const tasksDir = join(sDir, "tasks");
@@ -204,7 +204,7 @@ export async function runPostUnitVerification(
204
204
  try {
205
205
  await dispatchNextUnit(ctx, pi);
206
206
  } catch (retryDispatchErr) {
207
- const msg = retryDispatchErr instanceof Error ? retryDispatchErr.message : String(retryDispatchErr);
207
+ const msg = getErrorMessage(retryDispatchErr);
208
208
  ctx.ui.notify(`Verification retry dispatch error: ${msg}`, "error");
209
209
  startDispatchGapWatchdog(ctx, pi);
210
210
  }
@@ -38,6 +38,7 @@ import {
38
38
  nativeBranchDelete,
39
39
  nativeBranchExists,
40
40
  } from "./native-git-bridge.js";
41
+ import { getErrorMessage } from "./error-utils.js";
41
42
 
42
43
  // ─── Module State ──────────────────────────────────────────────────────────
43
44
 
@@ -81,7 +82,7 @@ export function runWorktreePostCreateHook(sourceDir: string, worktreeDir: string
81
82
  });
82
83
  return null;
83
84
  } catch (err) {
84
- const msg = err instanceof Error ? err.message : String(err);
85
+ const msg = getErrorMessage(err);
85
86
  return `Worktree post-create hook failed: ${msg}`;
86
87
  }
87
88
  }
@@ -141,7 +142,7 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
141
142
  // Don't store originalBase -- caller can retry or clean up.
142
143
  throw new GSDError(
143
144
  GSD_IO_ERROR,
144
- `Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
145
+ `Auto-worktree created at ${info.path} but chdir failed: ${getErrorMessage(err)}`,
145
146
  );
146
147
  }
147
148
 
@@ -168,7 +169,7 @@ export function teardownAutoWorktree(
168
169
  } catch (err) {
169
170
  throw new GSDError(
170
171
  GSD_IO_ERROR,
171
- `Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
172
+ `Failed to chdir back to ${originalBasePath} during teardown: ${getErrorMessage(err)}`,
172
173
  );
173
174
  }
174
175
 
@@ -274,7 +275,7 @@ export function enterAutoWorktree(basePath: string, milestoneId: string): string
274
275
  } catch (err) {
275
276
  throw new GSDError(
276
277
  GSD_IO_ERROR,
277
- `Failed to enter auto-worktree at ${p}: ${err instanceof Error ? err.message : String(err)}`,
278
+ `Failed to enter auto-worktree at ${p}: ${getErrorMessage(err)}`,
278
279
  );
279
280
  }
280
281