gsd-pi 2.31.2 → 2.32.0-dev.1e39869
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.
- package/dist/cli.js +5 -5
- package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/dist/resources/extensions/gsd/auto-start.ts +8 -6
- package/dist/resources/extensions/gsd/auto.ts +54 -33
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
- package/dist/resources/extensions/gsd/doctor.ts +6 -0
- package/dist/resources/extensions/gsd/git-service.ts +9 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +6 -0
- package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
- package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/dist/resources/extensions/gsd/progress-score.ts +273 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/dist/resources/extensions/gsd/quick.ts +3 -5
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/dist/worktree-cli.d.ts +42 -6
- package/dist/worktree-cli.js +88 -48
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-constants.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/src/resources/extensions/gsd/auto-start.ts +8 -6
- package/src/resources/extensions/gsd/auto.ts +54 -33
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/src/resources/extensions/gsd/doctor-types.ts +14 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -0
- package/src/resources/extensions/gsd/git-service.ts +9 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/src/resources/extensions/gsd/progress-score.ts +273 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/src/resources/extensions/gsd/quick.ts +3 -5
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
|
@@ -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;
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 {
|
|
41
|
+
import { createGitService } from "./git-service.js";
|
|
42
42
|
import {
|
|
43
43
|
captureIntegrationBranch,
|
|
44
44
|
detectWorktreeName,
|
|
@@ -129,11 +129,13 @@ export async function bootstrapAutoSession(
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// Initialize GitServiceImpl
|
|
132
|
-
s.gitService =
|
|
132
|
+
s.gitService = createGitService(s.basePath);
|
|
133
133
|
|
|
134
|
-
// Check for crash from previous session (use both old and new lock data)
|
|
134
|
+
// Check for crash from previous session (use both old and new lock data).
|
|
135
|
+
// Skip if the lock PID matches this process — acquireSessionLock() writes
|
|
136
|
+
// to the same auto.lock file before this check, so we'd always false-positive.
|
|
135
137
|
const crashLock = readCrashLock(base);
|
|
136
|
-
if (crashLock) {
|
|
138
|
+
if (crashLock && crashLock.pid !== process.pid) {
|
|
137
139
|
// We already hold the session lock, so no concurrent session is running.
|
|
138
140
|
// The crash lock is from a dead process — recover context from it.
|
|
139
141
|
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
@@ -330,12 +332,12 @@ export async function bootstrapAutoSession(
|
|
|
330
332
|
if (existingWtPath) {
|
|
331
333
|
const wtPath = enterAutoWorktree(base, s.currentMilestoneId);
|
|
332
334
|
s.basePath = wtPath;
|
|
333
|
-
s.gitService =
|
|
335
|
+
s.gitService = createGitService(s.basePath);
|
|
334
336
|
ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
|
|
335
337
|
} else {
|
|
336
338
|
const wtPath = createAutoWorktree(base, s.currentMilestoneId);
|
|
337
339
|
s.basePath = wtPath;
|
|
338
|
-
s.gitService =
|
|
340
|
+
s.gitService = createGitService(s.basePath);
|
|
339
341
|
ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
|
|
340
342
|
}
|
|
341
343
|
registerSigtermHandler(s.originalBasePath);
|
|
@@ -118,7 +118,7 @@ import {
|
|
|
118
118
|
parseSliceBranch,
|
|
119
119
|
setActiveMilestoneId,
|
|
120
120
|
} from "./worktree.js";
|
|
121
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
633
|
+
s.gitService = createGitService(s.basePath);
|
|
635
634
|
ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
|
|
636
635
|
}
|
|
637
636
|
} catch (err) {
|
|
@@ -834,6 +833,9 @@ export async function handleAgentEnd(
|
|
|
834
833
|
// permanently stalled with no unit running and no watchdog set.
|
|
835
834
|
if (s.pendingAgentEndRetry) {
|
|
836
835
|
s.pendingAgentEndRetry = false;
|
|
836
|
+
// Clear gap watchdog from the previous cycle to prevent concurrent
|
|
837
|
+
// dispatch when the deferred handleAgentEnd calls dispatchNextUnit (#1272).
|
|
838
|
+
clearDispatchGapWatchdog();
|
|
837
839
|
setImmediate(() => {
|
|
838
840
|
handleAgentEnd(ctx, pi).catch((err) => {
|
|
839
841
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -976,8 +978,12 @@ async function dispatchNextUnit(
|
|
|
976
978
|
return;
|
|
977
979
|
}
|
|
978
980
|
|
|
979
|
-
// Reentrancy guard
|
|
980
|
-
|
|
981
|
+
// Reentrancy guard — unconditional to prevent concurrent dispatch from
|
|
982
|
+
// gap watchdog or pendingAgentEndRetry during skip chains (#1272).
|
|
983
|
+
// Previously the guard was bypassed when skipDepth > 0, but the recursive
|
|
984
|
+
// skip chain's inner finally block resets s.dispatching = false before the
|
|
985
|
+
// outer call's finally runs, opening a window for concurrent entry.
|
|
986
|
+
if (s.dispatching) {
|
|
981
987
|
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
982
988
|
return;
|
|
983
989
|
}
|
|
@@ -1124,7 +1130,7 @@ async function dispatchNextUnit(
|
|
|
1124
1130
|
}
|
|
1125
1131
|
|
|
1126
1132
|
s.basePath = s.originalBasePath;
|
|
1127
|
-
s.gitService =
|
|
1133
|
+
s.gitService = createGitService(s.basePath);
|
|
1128
1134
|
invalidateAllCaches();
|
|
1129
1135
|
|
|
1130
1136
|
state = await deriveState(s.basePath);
|
|
@@ -1136,7 +1142,7 @@ async function dispatchNextUnit(
|
|
|
1136
1142
|
try {
|
|
1137
1143
|
const wtPath = createAutoWorktree(s.basePath, mid);
|
|
1138
1144
|
s.basePath = wtPath;
|
|
1139
|
-
s.gitService =
|
|
1145
|
+
s.gitService = createGitService(s.basePath);
|
|
1140
1146
|
ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
|
|
1141
1147
|
} catch (err) {
|
|
1142
1148
|
ctx.ui.notify(
|
|
@@ -1176,7 +1182,7 @@ async function dispatchNextUnit(
|
|
|
1176
1182
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1177
1183
|
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1178
1184
|
s.basePath = s.originalBasePath;
|
|
1179
|
-
s.gitService =
|
|
1185
|
+
s.gitService = createGitService(s.basePath);
|
|
1180
1186
|
ctx.ui.notify(
|
|
1181
1187
|
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1182
1188
|
"info",
|
|
@@ -1201,7 +1207,7 @@ async function dispatchNextUnit(
|
|
|
1201
1207
|
if (roadmapPath) {
|
|
1202
1208
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1203
1209
|
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1204
|
-
s.gitService =
|
|
1210
|
+
s.gitService = createGitService(s.basePath);
|
|
1205
1211
|
ctx.ui.notify(
|
|
1206
1212
|
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1207
1213
|
"info",
|
|
@@ -1279,7 +1285,7 @@ async function dispatchNextUnit(
|
|
|
1279
1285
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1280
1286
|
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1281
1287
|
s.basePath = s.originalBasePath;
|
|
1282
|
-
s.gitService =
|
|
1288
|
+
s.gitService = createGitService(s.basePath);
|
|
1283
1289
|
ctx.ui.notify(
|
|
1284
1290
|
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1285
1291
|
"info",
|
|
@@ -1303,7 +1309,7 @@ async function dispatchNextUnit(
|
|
|
1303
1309
|
if (roadmapPath) {
|
|
1304
1310
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1305
1311
|
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1306
|
-
s.gitService =
|
|
1312
|
+
s.gitService = createGitService(s.basePath);
|
|
1307
1313
|
ctx.ui.notify(
|
|
1308
1314
|
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1309
1315
|
"info",
|
|
@@ -1449,15 +1455,18 @@ async function dispatchNextUnit(
|
|
|
1449
1455
|
}
|
|
1450
1456
|
|
|
1451
1457
|
if (dispatchResult.action !== "dispatch") {
|
|
1452
|
-
|
|
1453
|
-
|
|
1458
|
+
// Defer re-dispatch to next microtask so s.dispatching is released first,
|
|
1459
|
+
// preventing reentrancy guard bypass during concurrent entry (#1272).
|
|
1460
|
+
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1461
|
+
ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1462
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1463
|
+
}));
|
|
1454
1464
|
return;
|
|
1455
1465
|
}
|
|
1456
1466
|
|
|
1457
1467
|
unitType = dispatchResult.unitType;
|
|
1458
1468
|
unitId = dispatchResult.unitId;
|
|
1459
1469
|
prompt = dispatchResult.prompt;
|
|
1460
|
-
let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1461
1470
|
|
|
1462
1471
|
// ── Pre-dispatch hooks ──
|
|
1463
1472
|
const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
@@ -1469,8 +1478,10 @@ async function dispatchNextUnit(
|
|
|
1469
1478
|
}
|
|
1470
1479
|
if (preDispatchResult.action === "skip") {
|
|
1471
1480
|
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
1472
|
-
|
|
1473
|
-
|
|
1481
|
+
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1482
|
+
ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1483
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1484
|
+
}));
|
|
1474
1485
|
return;
|
|
1475
1486
|
}
|
|
1476
1487
|
if (preDispatchResult.action === "replace") {
|
|
@@ -1501,9 +1512,16 @@ async function dispatchNextUnit(
|
|
|
1501
1512
|
if (idempotencyResult.reason === "completed" || idempotencyResult.reason === "fallback-persisted" || idempotencyResult.reason === "phantom-loop-cleared" || idempotencyResult.reason === "evicted") {
|
|
1502
1513
|
if (!s.active) return;
|
|
1503
1514
|
s.skipDepth++;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1515
|
+
const skipDelay = idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150;
|
|
1516
|
+
// Defer re-dispatch so s.dispatching is released first (#1272).
|
|
1517
|
+
setTimeout(() => {
|
|
1518
|
+
dispatchNextUnit(ctx, pi).catch(err => {
|
|
1519
|
+
ctx.ui.notify(`Deferred skip-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1520
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1521
|
+
}).finally(() => {
|
|
1522
|
+
s.skipDepth = Math.max(0, s.skipDepth - 1);
|
|
1523
|
+
});
|
|
1524
|
+
}, skipDelay);
|
|
1507
1525
|
return;
|
|
1508
1526
|
}
|
|
1509
1527
|
} else if (idempotencyResult.action === "stop") {
|
|
@@ -1534,8 +1552,11 @@ async function dispatchNextUnit(
|
|
|
1534
1552
|
return;
|
|
1535
1553
|
}
|
|
1536
1554
|
if (stuckResult.action === "recovered" && stuckResult.dispatchAgain) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1555
|
+
// Defer re-dispatch so s.dispatching is released first (#1272).
|
|
1556
|
+
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1557
|
+
ctx.ui.notify(`Deferred recovery-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1558
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1559
|
+
}));
|
|
1539
1560
|
return;
|
|
1540
1561
|
}
|
|
1541
1562
|
|
|
@@ -1712,13 +1733,6 @@ async function dispatchNextUnit(
|
|
|
1712
1733
|
{ triggerTurn: true },
|
|
1713
1734
|
);
|
|
1714
1735
|
|
|
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
1736
|
} finally {
|
|
1723
1737
|
s.dispatching = false;
|
|
1724
1738
|
}
|
|
@@ -1788,6 +1802,15 @@ export {
|
|
|
1788
1802
|
export function _getUnitConsecutiveSkips(): Map<string, number> { return s.unitConsecutiveSkips; }
|
|
1789
1803
|
export function _resetUnitConsecutiveSkips(): void { s.unitConsecutiveSkips.clear(); }
|
|
1790
1804
|
|
|
1805
|
+
/**
|
|
1806
|
+
* Test-only: expose dispatching / skipDepth state for reentrancy guard tests.
|
|
1807
|
+
* Not part of the public API.
|
|
1808
|
+
*/
|
|
1809
|
+
export function _getDispatching(): boolean { return s.dispatching; }
|
|
1810
|
+
export function _setDispatching(v: boolean): void { s.dispatching = v; }
|
|
1811
|
+
export function _getSkipDepth(): number { return s.skipDepth; }
|
|
1812
|
+
export function _setSkipDepth(v: number): void { s.skipDepth = v; }
|
|
1813
|
+
|
|
1791
1814
|
/**
|
|
1792
1815
|
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
1793
1816
|
* Used for manual hook triggers via /gsd run-hook.
|
|
@@ -1874,8 +1897,6 @@ export async function dispatchHookUnit(
|
|
|
1874
1897
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
1875
1898
|
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
|
|
1876
1899
|
|
|
1877
|
-
console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
|
|
1878
|
-
console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
|
|
1879
1900
|
pi.sendMessage(
|
|
1880
1901
|
{ customType: "gsd-auto", content: hookPrompt, display: true },
|
|
1881
1902
|
{ 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 {
|
|
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
|
|
427
|
-
const
|
|
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;
|
|
@@ -44,6 +44,8 @@ import { handleConfig } from "./commands-config.js";
|
|
|
44
44
|
import { handleInspect } from "./commands-inspect.js";
|
|
45
45
|
import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
|
|
46
46
|
import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
|
|
47
|
+
import { computeProgressScore, formatProgressLine } from "./progress-score.js";
|
|
48
|
+
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
47
49
|
import { handleLogs } from "./commands-logs.js";
|
|
48
50
|
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
49
51
|
|
|
@@ -1068,6 +1070,11 @@ async function handleSetup(args: string, ctx: ExtensionCommandContext): Promise<
|
|
|
1068
1070
|
function formatTextStatus(state: GSDState): string {
|
|
1069
1071
|
const lines: string[] = ["GSD Status\n"];
|
|
1070
1072
|
|
|
1073
|
+
// Progress score — traffic light (#1221)
|
|
1074
|
+
const progressScore = computeProgressScore();
|
|
1075
|
+
lines.push(formatProgressLine(progressScore));
|
|
1076
|
+
lines.push("");
|
|
1077
|
+
|
|
1071
1078
|
// Phase
|
|
1072
1079
|
lines.push(`Phase: ${state.phase}`);
|
|
1073
1080
|
|
|
@@ -1114,5 +1121,17 @@ function formatTextStatus(state: GSDState): string {
|
|
|
1114
1121
|
}
|
|
1115
1122
|
}
|
|
1116
1123
|
|
|
1124
|
+
// Environment health (#1221)
|
|
1125
|
+
const envResults = runEnvironmentChecks(projectRoot());
|
|
1126
|
+
const envIssues = envResults.filter(r => r.status !== "ok");
|
|
1127
|
+
if (envIssues.length > 0) {
|
|
1128
|
+
lines.push("");
|
|
1129
|
+
lines.push("Environment:");
|
|
1130
|
+
for (const r of envIssues) {
|
|
1131
|
+
const icon = r.status === "error" ? "✗" : "⚠";
|
|
1132
|
+
lines.push(` ${icon} ${r.message}`);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1117
1136
|
return lines.join("\n");
|
|
1118
1137
|
}
|
|
@@ -23,6 +23,8 @@ import { getActiveWorktreeName } from "./worktree-command.js";
|
|
|
23
23
|
import { getWorkerBatches, hasActiveWorkers, type WorkerEntry } from "../subagent/worker-registry.js";
|
|
24
24
|
import { formatDuration, padRight, joinColumns, centerLine, fitColumns, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js";
|
|
25
25
|
import { estimateTimeRemaining } from "./auto-dashboard.js";
|
|
26
|
+
import { computeProgressScore, formatProgressLine } from "./progress-score.js";
|
|
27
|
+
import { runEnvironmentChecks, type EnvironmentCheckResult } from "./doctor-environment.js";
|
|
26
28
|
|
|
27
29
|
function unitLabel(type: string): string {
|
|
28
30
|
switch (type) {
|
|
@@ -310,6 +312,15 @@ export class GSDDashboardOverlay {
|
|
|
310
312
|
elapsedParts = th.fg("dim", `since ${this.dashData.remoteSession!.startedAt.replace("T", " ").slice(0, 19)}`);
|
|
311
313
|
}
|
|
312
314
|
lines.push(row(joinColumns(`${title} ${status}${worktreeTag}`, elapsedParts, contentWidth)));
|
|
315
|
+
|
|
316
|
+
// Progress score — traffic light indicator (#1221)
|
|
317
|
+
if (this.dashData.active || this.dashData.paused) {
|
|
318
|
+
const progressScore = computeProgressScore();
|
|
319
|
+
const progressIcon = progressScore.level === "green" ? th.fg("success", "●")
|
|
320
|
+
: progressScore.level === "yellow" ? th.fg("warning", "●")
|
|
321
|
+
: th.fg("error", "●");
|
|
322
|
+
lines.push(row(`${progressIcon} ${th.fg("text", progressScore.summary)}`));
|
|
323
|
+
}
|
|
313
324
|
lines.push(blank());
|
|
314
325
|
|
|
315
326
|
if (this.dashData.currentUnit) {
|
|
@@ -579,6 +590,23 @@ export class GSDDashboardOverlay {
|
|
|
579
590
|
}
|
|
580
591
|
}
|
|
581
592
|
|
|
593
|
+
// Environment health section (#1221) — only show issues
|
|
594
|
+
const envResults = runEnvironmentChecks(this.dashData.basePath || process.cwd());
|
|
595
|
+
const envIssues = envResults.filter(r => r.status !== "ok");
|
|
596
|
+
if (envIssues.length > 0) {
|
|
597
|
+
lines.push(blank());
|
|
598
|
+
lines.push(hr());
|
|
599
|
+
lines.push(row(th.fg("text", th.bold("Environment"))));
|
|
600
|
+
lines.push(blank());
|
|
601
|
+
for (const r of envIssues) {
|
|
602
|
+
const icon = r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
|
|
603
|
+
lines.push(row(` ${icon} ${th.fg("text", r.message)}`));
|
|
604
|
+
if (r.detail) {
|
|
605
|
+
lines.push(row(th.fg("dim", ` ${r.detail}`)));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
582
610
|
lines.push(blank());
|
|
583
611
|
lines.push(hr());
|
|
584
612
|
lines.push(centered(th.fg("dim", "↑↓ scroll · g/G top/end · esc close")));
|