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.
- package/README.md +27 -20
- package/dist/cli.js +5 -5
- package/dist/resource-loader.js +13 -3
- package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +23 -27
- 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-idempotency.ts +3 -2
- package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
- package/dist/resources/extensions/gsd/auto-post-unit.ts +32 -37
- package/dist/resources/extensions/gsd/auto-prompts.ts +84 -78
- package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/dist/resources/extensions/gsd/auto-start.ts +16 -12
- package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
- package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
- package/dist/resources/extensions/gsd/auto-worktree.ts +5 -4
- package/dist/resources/extensions/gsd/auto.ts +82 -60
- package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
- 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/error-utils.ts +6 -0
- package/dist/resources/extensions/gsd/export.ts +2 -1
- package/dist/resources/extensions/gsd/git-service.ts +12 -2
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +18 -5
- package/dist/resources/extensions/gsd/key-manager.ts +2 -1
- package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/dist/resources/extensions/gsd/metrics.ts +3 -3
- package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
- package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- 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 +61 -8
- package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
- package/dist/resources/extensions/gsd/session-lock.ts +12 -1
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- 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/undo.ts +5 -7
- package/dist/resources/extensions/gsd/unit-id.ts +14 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
- 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 +23 -27
- 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-idempotency.ts +3 -2
- package/src/resources/extensions/gsd/auto-observability.ts +2 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +32 -37
- package/src/resources/extensions/gsd/auto-prompts.ts +84 -78
- package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
- package/src/resources/extensions/gsd/auto-start.ts +16 -12
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
- package/src/resources/extensions/gsd/auto-timers.ts +3 -2
- package/src/resources/extensions/gsd/auto-verification.ts +6 -6
- package/src/resources/extensions/gsd/auto-worktree.ts +5 -4
- package/src/resources/extensions/gsd/auto.ts +82 -60
- package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
- package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
- 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/error-utils.ts +6 -0
- package/src/resources/extensions/gsd/export.ts +2 -1
- package/src/resources/extensions/gsd/git-service.ts +12 -2
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/guided-flow.ts +3 -2
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +18 -5
- package/src/resources/extensions/gsd/key-manager.ts +2 -1
- package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
- package/src/resources/extensions/gsd/metrics.ts +3 -3
- package/src/resources/extensions/gsd/migrate-external.ts +21 -4
- package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
- 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 +61 -8
- package/src/resources/extensions/gsd/repo-identity.ts +22 -1
- package/src/resources/extensions/gsd/session-lock.ts +12 -1
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- 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/undo.ts +5 -7
- package/src/resources/extensions/gsd/unit-id.ts +14 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/src/resources/extensions/gsd/worktree-command.ts +8 -7
|
@@ -105,6 +105,7 @@ import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.j
|
|
|
105
105
|
import { GSDError, GSD_ARTIFACT_MISSING } from "./errors.js";
|
|
106
106
|
import { join } from "node:path";
|
|
107
107
|
import { sep as pathSep } from "node:path";
|
|
108
|
+
import { parseUnitId } from "./unit-id.js";
|
|
108
109
|
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
|
|
109
110
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
110
111
|
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
@@ -118,7 +119,7 @@ import {
|
|
|
118
119
|
parseSliceBranch,
|
|
119
120
|
setActiveMilestoneId,
|
|
120
121
|
} from "./worktree.js";
|
|
121
|
-
import {
|
|
122
|
+
import { createGitService, type TaskCommitContext } from "./git-service.js";
|
|
122
123
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
123
124
|
import { formatGitError } from "./git-self-heal.js";
|
|
124
125
|
import {
|
|
@@ -189,6 +190,7 @@ import {
|
|
|
189
190
|
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
190
191
|
} from "./auto/session.js";
|
|
191
192
|
import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
|
|
193
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
192
194
|
|
|
193
195
|
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
|
|
194
196
|
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
|
|
@@ -204,8 +206,7 @@ import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerifi
|
|
|
204
206
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
207
|
const s = new AutoSession();
|
|
206
208
|
|
|
207
|
-
|
|
208
|
-
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
209
|
+
import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
|
|
209
210
|
|
|
210
211
|
export function shouldUseWorktreeIsolation(): boolean {
|
|
211
212
|
const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
@@ -429,7 +430,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
429
430
|
try {
|
|
430
431
|
await dispatchNextUnit(ctx, pi);
|
|
431
432
|
} catch (retryErr) {
|
|
432
|
-
const message =
|
|
433
|
+
const message = getErrorMessage(retryErr);
|
|
433
434
|
await stopAuto(ctx, pi, `Dispatch gap recovery failed: ${message}`);
|
|
434
435
|
return;
|
|
435
436
|
}
|
|
@@ -459,14 +460,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
459
460
|
// ── Auto-worktree: exit worktree and reset s.basePath on stop ──
|
|
460
461
|
if (s.currentMilestoneId && isInAutoWorktree(s.basePath)) {
|
|
461
462
|
try {
|
|
462
|
-
try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error:
|
|
463
|
+
try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: getErrorMessage(e) }); }
|
|
463
464
|
teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
|
|
464
465
|
s.basePath = s.originalBasePath;
|
|
465
|
-
s.gitService =
|
|
466
|
+
s.gitService = createGitService(s.basePath);
|
|
466
467
|
ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
|
|
467
468
|
} catch (err) {
|
|
468
469
|
ctx?.ui.notify(
|
|
469
|
-
`Auto-worktree teardown failed: ${
|
|
470
|
+
`Auto-worktree teardown failed: ${getErrorMessage(err)}`,
|
|
470
471
|
"warning",
|
|
471
472
|
);
|
|
472
473
|
}
|
|
@@ -477,7 +478,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
477
478
|
try {
|
|
478
479
|
const { closeDatabase } = await import("./gsd-db.js");
|
|
479
480
|
closeDatabase();
|
|
480
|
-
} catch (e) { debugLog("db-close-failed", { error:
|
|
481
|
+
} catch (e) { debugLog("db-close-failed", { error: getErrorMessage(e) }); }
|
|
481
482
|
}
|
|
482
483
|
|
|
483
484
|
if (s.originalBasePath) {
|
|
@@ -497,7 +498,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
|
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
if (s.basePath) {
|
|
500
|
-
try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error:
|
|
501
|
+
try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: getErrorMessage(e) }); }
|
|
501
502
|
}
|
|
502
503
|
|
|
503
504
|
if (isDebugEnabled()) {
|
|
@@ -626,17 +627,17 @@ export async function startAuto(
|
|
|
626
627
|
if (existingWtPath) {
|
|
627
628
|
const wtPath = enterAutoWorktree(s.originalBasePath, s.currentMilestoneId);
|
|
628
629
|
s.basePath = wtPath;
|
|
629
|
-
s.gitService =
|
|
630
|
+
s.gitService = createGitService(s.basePath);
|
|
630
631
|
ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
|
|
631
632
|
} else {
|
|
632
633
|
const wtPath = createAutoWorktree(s.originalBasePath, s.currentMilestoneId);
|
|
633
634
|
s.basePath = wtPath;
|
|
634
|
-
s.gitService =
|
|
635
|
+
s.gitService = createGitService(s.basePath);
|
|
635
636
|
ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
|
|
636
637
|
}
|
|
637
638
|
} catch (err) {
|
|
638
639
|
ctx.ui.notify(
|
|
639
|
-
`Auto-worktree re-entry failed: ${
|
|
640
|
+
`Auto-worktree re-entry failed: ${getErrorMessage(err)}. Continuing at current path.`,
|
|
640
641
|
"warning",
|
|
641
642
|
);
|
|
642
643
|
}
|
|
@@ -648,13 +649,13 @@ export async function startAuto(
|
|
|
648
649
|
ctx.ui.setFooter(hideFooter);
|
|
649
650
|
ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
650
651
|
restoreHookState(s.basePath);
|
|
651
|
-
try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error:
|
|
652
|
+
try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: getErrorMessage(e) }); }
|
|
652
653
|
try {
|
|
653
654
|
const report = await runGSDDoctor(s.basePath, { fix: true });
|
|
654
655
|
if (report.fixesApplied.length > 0) {
|
|
655
656
|
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
|
|
656
657
|
}
|
|
657
|
-
} catch (e) { debugLog("resume-doctor-failed", { error:
|
|
658
|
+
} catch (e) { debugLog("resume-doctor-failed", { error: getErrorMessage(e) }); }
|
|
658
659
|
await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
|
|
659
660
|
invalidateAllCaches();
|
|
660
661
|
|
|
@@ -701,7 +702,7 @@ export async function startAuto(
|
|
|
701
702
|
}
|
|
702
703
|
} catch (err) {
|
|
703
704
|
ctx.ui.notify(
|
|
704
|
-
`Secrets check error: ${
|
|
705
|
+
`Secrets check error: ${getErrorMessage(err)}. Continuing without secrets.`,
|
|
705
706
|
"warning",
|
|
706
707
|
);
|
|
707
708
|
}
|
|
@@ -808,7 +809,7 @@ export async function handleAgentEnd(
|
|
|
808
809
|
try {
|
|
809
810
|
await dispatchNextUnit(ctx, pi);
|
|
810
811
|
} catch (dispatchErr) {
|
|
811
|
-
const message =
|
|
812
|
+
const message = getErrorMessage(dispatchErr);
|
|
812
813
|
ctx.ui.notify(
|
|
813
814
|
`Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
|
|
814
815
|
"error",
|
|
@@ -834,9 +835,12 @@ export async function handleAgentEnd(
|
|
|
834
835
|
// permanently stalled with no unit running and no watchdog set.
|
|
835
836
|
if (s.pendingAgentEndRetry) {
|
|
836
837
|
s.pendingAgentEndRetry = false;
|
|
838
|
+
// Clear gap watchdog from the previous cycle to prevent concurrent
|
|
839
|
+
// dispatch when the deferred handleAgentEnd calls dispatchNextUnit (#1272).
|
|
840
|
+
clearDispatchGapWatchdog();
|
|
837
841
|
setImmediate(() => {
|
|
838
842
|
handleAgentEnd(ctx, pi).catch((err) => {
|
|
839
|
-
const msg =
|
|
843
|
+
const msg = getErrorMessage(err);
|
|
840
844
|
ctx.ui.notify(`Deferred agent_end retry failed: ${msg}`, "error");
|
|
841
845
|
pauseAuto(ctx, pi).catch(() => {});
|
|
842
846
|
});
|
|
@@ -976,8 +980,12 @@ async function dispatchNextUnit(
|
|
|
976
980
|
return;
|
|
977
981
|
}
|
|
978
982
|
|
|
979
|
-
// Reentrancy guard
|
|
980
|
-
|
|
983
|
+
// Reentrancy guard — unconditional to prevent concurrent dispatch from
|
|
984
|
+
// gap watchdog or pendingAgentEndRetry during skip chains (#1272).
|
|
985
|
+
// Previously the guard was bypassed when skipDepth > 0, but the recursive
|
|
986
|
+
// skip chain's inner finally block resets s.dispatching = false before the
|
|
987
|
+
// outer call's finally runs, opening a window for concurrent entry.
|
|
988
|
+
if (s.dispatching) {
|
|
981
989
|
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
982
990
|
return;
|
|
983
991
|
}
|
|
@@ -1080,7 +1088,7 @@ async function dispatchNextUnit(
|
|
|
1080
1088
|
);
|
|
1081
1089
|
} catch (err) {
|
|
1082
1090
|
ctx.ui.notify(
|
|
1083
|
-
`Report generation failed: ${
|
|
1091
|
+
`Report generation failed: ${getErrorMessage(err)}`,
|
|
1084
1092
|
"warning",
|
|
1085
1093
|
);
|
|
1086
1094
|
}
|
|
@@ -1096,7 +1104,7 @@ async function dispatchNextUnit(
|
|
|
1096
1104
|
atomicWriteSync(file, JSON.stringify([]));
|
|
1097
1105
|
}
|
|
1098
1106
|
s.completedKeySet.clear();
|
|
1099
|
-
} catch (e) { debugLog("completed-keys-reset-failed", { error:
|
|
1107
|
+
} catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
|
|
1100
1108
|
|
|
1101
1109
|
// ── Worktree lifecycle on milestone transition (#616) ──
|
|
1102
1110
|
if (isInAutoWorktree(s.basePath) && s.originalBasePath && shouldUseWorktreeIsolation()) {
|
|
@@ -1115,7 +1123,7 @@ async function dispatchNextUnit(
|
|
|
1115
1123
|
}
|
|
1116
1124
|
} catch (err) {
|
|
1117
1125
|
ctx.ui.notify(
|
|
1118
|
-
`Milestone merge failed during transition: ${
|
|
1126
|
+
`Milestone merge failed during transition: ${getErrorMessage(err)}`,
|
|
1119
1127
|
"warning",
|
|
1120
1128
|
);
|
|
1121
1129
|
if (s.originalBasePath) {
|
|
@@ -1124,7 +1132,7 @@ async function dispatchNextUnit(
|
|
|
1124
1132
|
}
|
|
1125
1133
|
|
|
1126
1134
|
s.basePath = s.originalBasePath;
|
|
1127
|
-
s.gitService =
|
|
1135
|
+
s.gitService = createGitService(s.basePath);
|
|
1128
1136
|
invalidateAllCaches();
|
|
1129
1137
|
|
|
1130
1138
|
state = await deriveState(s.basePath);
|
|
@@ -1136,11 +1144,11 @@ async function dispatchNextUnit(
|
|
|
1136
1144
|
try {
|
|
1137
1145
|
const wtPath = createAutoWorktree(s.basePath, mid);
|
|
1138
1146
|
s.basePath = wtPath;
|
|
1139
|
-
s.gitService =
|
|
1147
|
+
s.gitService = createGitService(s.basePath);
|
|
1140
1148
|
ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
|
|
1141
1149
|
} catch (err) {
|
|
1142
1150
|
ctx.ui.notify(
|
|
1143
|
-
`Auto-worktree creation for ${mid} failed: ${
|
|
1151
|
+
`Auto-worktree creation for ${mid} failed: ${getErrorMessage(err)}. Continuing in project root.`,
|
|
1144
1152
|
"warning",
|
|
1145
1153
|
);
|
|
1146
1154
|
}
|
|
@@ -1176,7 +1184,7 @@ async function dispatchNextUnit(
|
|
|
1176
1184
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1177
1185
|
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1178
1186
|
s.basePath = s.originalBasePath;
|
|
1179
|
-
s.gitService =
|
|
1187
|
+
s.gitService = createGitService(s.basePath);
|
|
1180
1188
|
ctx.ui.notify(
|
|
1181
1189
|
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1182
1190
|
"info",
|
|
@@ -1184,7 +1192,7 @@ async function dispatchNextUnit(
|
|
|
1184
1192
|
}
|
|
1185
1193
|
} catch (err) {
|
|
1186
1194
|
ctx.ui.notify(
|
|
1187
|
-
`Milestone merge failed: ${
|
|
1195
|
+
`Milestone merge failed: ${getErrorMessage(err)}`,
|
|
1188
1196
|
"warning",
|
|
1189
1197
|
);
|
|
1190
1198
|
if (s.originalBasePath) {
|
|
@@ -1201,7 +1209,7 @@ async function dispatchNextUnit(
|
|
|
1201
1209
|
if (roadmapPath) {
|
|
1202
1210
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1203
1211
|
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1204
|
-
s.gitService =
|
|
1212
|
+
s.gitService = createGitService(s.basePath);
|
|
1205
1213
|
ctx.ui.notify(
|
|
1206
1214
|
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1207
1215
|
"info",
|
|
@@ -1210,7 +1218,7 @@ async function dispatchNextUnit(
|
|
|
1210
1218
|
}
|
|
1211
1219
|
} catch (err) {
|
|
1212
1220
|
ctx.ui.notify(
|
|
1213
|
-
`Milestone merge failed (branch mode): ${
|
|
1221
|
+
`Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
|
|
1214
1222
|
"warning",
|
|
1215
1223
|
);
|
|
1216
1224
|
}
|
|
@@ -1270,7 +1278,7 @@ async function dispatchNextUnit(
|
|
|
1270
1278
|
atomicWriteSync(file, JSON.stringify([]));
|
|
1271
1279
|
}
|
|
1272
1280
|
s.completedKeySet.clear();
|
|
1273
|
-
} catch (e) { debugLog("completed-keys-reset-failed", { error:
|
|
1281
|
+
} catch (e) { debugLog("completed-keys-reset-failed", { error: getErrorMessage(e) }); }
|
|
1274
1282
|
// ── Milestone merge ──
|
|
1275
1283
|
if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
|
|
1276
1284
|
try {
|
|
@@ -1279,14 +1287,14 @@ async function dispatchNextUnit(
|
|
|
1279
1287
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1280
1288
|
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
|
1281
1289
|
s.basePath = s.originalBasePath;
|
|
1282
|
-
s.gitService =
|
|
1290
|
+
s.gitService = createGitService(s.basePath);
|
|
1283
1291
|
ctx.ui.notify(
|
|
1284
1292
|
`Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1285
1293
|
"info",
|
|
1286
1294
|
);
|
|
1287
1295
|
} catch (err) {
|
|
1288
1296
|
ctx.ui.notify(
|
|
1289
|
-
`Milestone merge failed: ${
|
|
1297
|
+
`Milestone merge failed: ${getErrorMessage(err)}`,
|
|
1290
1298
|
"warning",
|
|
1291
1299
|
);
|
|
1292
1300
|
if (s.originalBasePath) {
|
|
@@ -1303,7 +1311,7 @@ async function dispatchNextUnit(
|
|
|
1303
1311
|
if (roadmapPath) {
|
|
1304
1312
|
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1305
1313
|
const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
|
|
1306
|
-
s.gitService =
|
|
1314
|
+
s.gitService = createGitService(s.basePath);
|
|
1307
1315
|
ctx.ui.notify(
|
|
1308
1316
|
`Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1309
1317
|
"info",
|
|
@@ -1312,7 +1320,7 @@ async function dispatchNextUnit(
|
|
|
1312
1320
|
}
|
|
1313
1321
|
} catch (err) {
|
|
1314
1322
|
ctx.ui.notify(
|
|
1315
|
-
`Milestone merge failed (branch mode): ${
|
|
1323
|
+
`Milestone merge failed (branch mode): ${getErrorMessage(err)}`,
|
|
1316
1324
|
"warning",
|
|
1317
1325
|
);
|
|
1318
1326
|
}
|
|
@@ -1411,7 +1419,7 @@ async function dispatchNextUnit(
|
|
|
1411
1419
|
}
|
|
1412
1420
|
} catch (err) {
|
|
1413
1421
|
ctx.ui.notify(
|
|
1414
|
-
`Secrets collection error: ${
|
|
1422
|
+
`Secrets collection error: ${getErrorMessage(err)}. Continuing with next task.`,
|
|
1415
1423
|
"warning",
|
|
1416
1424
|
);
|
|
1417
1425
|
}
|
|
@@ -1449,15 +1457,18 @@ async function dispatchNextUnit(
|
|
|
1449
1457
|
}
|
|
1450
1458
|
|
|
1451
1459
|
if (dispatchResult.action !== "dispatch") {
|
|
1452
|
-
|
|
1453
|
-
|
|
1460
|
+
// Defer re-dispatch to next microtask so s.dispatching is released first,
|
|
1461
|
+
// preventing reentrancy guard bypass during concurrent entry (#1272).
|
|
1462
|
+
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1463
|
+
ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1464
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1465
|
+
}));
|
|
1454
1466
|
return;
|
|
1455
1467
|
}
|
|
1456
1468
|
|
|
1457
1469
|
unitType = dispatchResult.unitType;
|
|
1458
1470
|
unitId = dispatchResult.unitId;
|
|
1459
1471
|
prompt = dispatchResult.prompt;
|
|
1460
|
-
let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1461
1472
|
|
|
1462
1473
|
// ── Pre-dispatch hooks ──
|
|
1463
1474
|
const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
@@ -1469,8 +1480,10 @@ async function dispatchNextUnit(
|
|
|
1469
1480
|
}
|
|
1470
1481
|
if (preDispatchResult.action === "skip") {
|
|
1471
1482
|
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
1472
|
-
|
|
1473
|
-
|
|
1483
|
+
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1484
|
+
ctx.ui.notify(`Deferred dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1485
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1486
|
+
}));
|
|
1474
1487
|
return;
|
|
1475
1488
|
}
|
|
1476
1489
|
if (preDispatchResult.action === "replace") {
|
|
@@ -1501,9 +1514,16 @@ async function dispatchNextUnit(
|
|
|
1501
1514
|
if (idempotencyResult.reason === "completed" || idempotencyResult.reason === "fallback-persisted" || idempotencyResult.reason === "phantom-loop-cleared" || idempotencyResult.reason === "evicted") {
|
|
1502
1515
|
if (!s.active) return;
|
|
1503
1516
|
s.skipDepth++;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1517
|
+
const skipDelay = idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150;
|
|
1518
|
+
// Defer re-dispatch so s.dispatching is released first (#1272).
|
|
1519
|
+
setTimeout(() => {
|
|
1520
|
+
dispatchNextUnit(ctx, pi).catch(err => {
|
|
1521
|
+
ctx.ui.notify(`Deferred skip-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1522
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1523
|
+
}).finally(() => {
|
|
1524
|
+
s.skipDepth = Math.max(0, s.skipDepth - 1);
|
|
1525
|
+
});
|
|
1526
|
+
}, skipDelay);
|
|
1507
1527
|
return;
|
|
1508
1528
|
}
|
|
1509
1529
|
} else if (idempotencyResult.action === "stop") {
|
|
@@ -1534,8 +1554,11 @@ async function dispatchNextUnit(
|
|
|
1534
1554
|
return;
|
|
1535
1555
|
}
|
|
1536
1556
|
if (stuckResult.action === "recovered" && stuckResult.dispatchAgain) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1557
|
+
// Defer re-dispatch so s.dispatching is released first (#1272).
|
|
1558
|
+
setImmediate(() => dispatchNextUnit(ctx, pi).catch(err => {
|
|
1559
|
+
ctx.ui.notify(`Deferred recovery-dispatch failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1560
|
+
pauseAuto(ctx, pi).catch(() => {});
|
|
1561
|
+
}));
|
|
1539
1562
|
return;
|
|
1540
1563
|
}
|
|
1541
1564
|
|
|
@@ -1607,7 +1630,7 @@ async function dispatchNextUnit(
|
|
|
1607
1630
|
);
|
|
1608
1631
|
result = await Promise.race([sessionPromise, timeoutPromise]);
|
|
1609
1632
|
} catch (sessionErr) {
|
|
1610
|
-
const msg =
|
|
1633
|
+
const msg = getErrorMessage(sessionErr);
|
|
1611
1634
|
ctx.ui.notify(`Session creation failed: ${msg}. Retrying via watchdog.`, "error");
|
|
1612
1635
|
throw new Error(`newSession() failed: ${msg}`);
|
|
1613
1636
|
}
|
|
@@ -1683,7 +1706,7 @@ async function dispatchNextUnit(
|
|
|
1683
1706
|
const { reorderForCaching } = await import("./prompt-ordering.js");
|
|
1684
1707
|
finalPrompt = reorderForCaching(finalPrompt);
|
|
1685
1708
|
} catch (reorderErr) {
|
|
1686
|
-
const msg =
|
|
1709
|
+
const msg = getErrorMessage(reorderErr);
|
|
1687
1710
|
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
1688
1711
|
}
|
|
1689
1712
|
|
|
@@ -1712,13 +1735,6 @@ async function dispatchNextUnit(
|
|
|
1712
1735
|
{ triggerTurn: true },
|
|
1713
1736
|
);
|
|
1714
1737
|
|
|
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
1738
|
} finally {
|
|
1723
1739
|
s.dispatching = false;
|
|
1724
1740
|
}
|
|
@@ -1733,8 +1749,7 @@ async function dispatchNextUnit(
|
|
|
1733
1749
|
function ensurePreconditions(
|
|
1734
1750
|
unitType: string, unitId: string, base: string, state: GSDState,
|
|
1735
1751
|
): void {
|
|
1736
|
-
const
|
|
1737
|
-
const mid = parts[0]!;
|
|
1752
|
+
const { milestone: mid } = parseUnitId(unitId);
|
|
1738
1753
|
|
|
1739
1754
|
const mDir = resolveMilestonePath(base, mid);
|
|
1740
1755
|
if (!mDir) {
|
|
@@ -1742,8 +1757,8 @@ function ensurePreconditions(
|
|
|
1742
1757
|
mkdirSync(join(newDir, "slices"), { recursive: true });
|
|
1743
1758
|
}
|
|
1744
1759
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1760
|
+
const sid = parseUnitId(unitId).slice;
|
|
1761
|
+
if (sid) {
|
|
1747
1762
|
|
|
1748
1763
|
const mDirResolved = resolveMilestonePath(base, mid);
|
|
1749
1764
|
if (mDirResolved) {
|
|
@@ -1788,6 +1803,15 @@ export {
|
|
|
1788
1803
|
export function _getUnitConsecutiveSkips(): Map<string, number> { return s.unitConsecutiveSkips; }
|
|
1789
1804
|
export function _resetUnitConsecutiveSkips(): void { s.unitConsecutiveSkips.clear(); }
|
|
1790
1805
|
|
|
1806
|
+
/**
|
|
1807
|
+
* Test-only: expose dispatching / skipDepth state for reentrancy guard tests.
|
|
1808
|
+
* Not part of the public API.
|
|
1809
|
+
*/
|
|
1810
|
+
export function _getDispatching(): boolean { return s.dispatching; }
|
|
1811
|
+
export function _setDispatching(v: boolean): void { s.dispatching = v; }
|
|
1812
|
+
export function _getSkipDepth(): number { return s.skipDepth; }
|
|
1813
|
+
export function _setSkipDepth(v: number): void { s.skipDepth = v; }
|
|
1814
|
+
|
|
1791
1815
|
/**
|
|
1792
1816
|
* Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
|
|
1793
1817
|
* Used for manual hook triggers via /gsd run-hook.
|
|
@@ -1874,8 +1898,6 @@ export async function dispatchHookUnit(
|
|
|
1874
1898
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
1875
1899
|
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
|
|
1876
1900
|
|
|
1877
|
-
console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
|
|
1878
|
-
console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
|
|
1879
1901
|
pi.sendMessage(
|
|
1880
1902
|
{ customType: "gsd-auto", content: hookPrompt, display: true },
|
|
1881
1903
|
{ triggerTurn: true },
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
8
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
8
9
|
|
|
9
10
|
export interface InspectData {
|
|
10
11
|
schemaVersion: number | null;
|
|
@@ -84,7 +85,7 @@ export async function handleInspect(ctx: ExtensionCommandContext): Promise<void>
|
|
|
84
85
|
|
|
85
86
|
ctx.ui.notify(formatInspectOutput(data), "info");
|
|
86
87
|
} catch (err) {
|
|
87
|
-
process.stderr.write(`gsd-db: /gsd inspect failed: ${
|
|
88
|
+
process.stderr.write(`gsd-db: /gsd inspect failed: ${getErrorMessage(err)}\n`);
|
|
88
89
|
ctx.ui.notify("Failed to inspect GSD database. Check stderr for details.", "error");
|
|
89
90
|
}
|
|
90
91
|
}
|
|
@@ -19,9 +19,9 @@ 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";
|
|
24
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
25
25
|
|
|
26
26
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
@@ -423,9 +423,8 @@ export async function handleStart(
|
|
|
423
423
|
|
|
424
424
|
// ─── Create git branch (unless isolation: none) ─────────────────────────
|
|
425
425
|
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
const skipBranch = gitPrefs.isolation === "none";
|
|
426
|
+
const git = createGitService(basePath);
|
|
427
|
+
const skipBranch = git.prefs.isolation === "none";
|
|
429
428
|
const slug = slugify(description || templateId);
|
|
430
429
|
const branchName = `gsd/${templateId}/${slug}`;
|
|
431
430
|
let branchCreated = false;
|
|
@@ -441,7 +440,7 @@ export async function handleStart(
|
|
|
441
440
|
branchCreated = true;
|
|
442
441
|
}
|
|
443
442
|
} catch (err) {
|
|
444
|
-
const message =
|
|
443
|
+
const message = getErrorMessage(err);
|
|
445
444
|
ctx.ui.notify(
|
|
446
445
|
`Could not create branch ${branchName}: ${message}. Working on current branch.`,
|
|
447
446
|
"warning",
|
|
@@ -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
|
}
|
|
@@ -6,6 +6,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { gsdRoot } from "./paths.js";
|
|
8
8
|
import { getAdaptiveTierAdjustment } from "./routing-history.js";
|
|
9
|
+
import { parseUnitId } from "./unit-id.js";
|
|
9
10
|
|
|
10
11
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
11
12
|
|
|
@@ -180,15 +181,14 @@ function analyzePlanComplexity(
|
|
|
180
181
|
basePath: string,
|
|
181
182
|
): TaskAnalysis | null {
|
|
182
183
|
// Check if this is a milestone-level plan (more complex) vs single slice
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
184
|
+
const { milestone: mid, slice: sid } = parseUnitId(unitId);
|
|
185
|
+
if (!sid) {
|
|
185
186
|
// Milestone-level planning is always at least standard
|
|
186
187
|
return { tier: "standard", reason: "milestone-level planning" };
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
// For slice planning, try to read the context/research to gauge complexity
|
|
190
191
|
// If research exists and is large, bump to heavy
|
|
191
|
-
const [mid, sid] = parts;
|
|
192
192
|
const researchPath = join(gsdRoot(basePath), mid, "slices", sid, "RESEARCH.md");
|
|
193
193
|
try {
|
|
194
194
|
if (existsSync(researchPath)) {
|
|
@@ -210,10 +210,8 @@ function analyzePlanComplexity(
|
|
|
210
210
|
*/
|
|
211
211
|
function extractTaskMetadata(unitId: string, basePath: string): TaskMetadata {
|
|
212
212
|
const meta: TaskMetadata = {};
|
|
213
|
-
const
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
const [mid, sid, tid] = parts;
|
|
213
|
+
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
214
|
+
if (!mid || !sid || !tid) return meta;
|
|
217
215
|
const taskPlanPath = join(gsdRoot(basePath), mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
|
|
218
216
|
|
|
219
217
|
try {
|
|
@@ -98,11 +98,24 @@ export function isLockProcessAlive(lock: LockData): boolean {
|
|
|
98
98
|
|
|
99
99
|
/** Format crash info for display or injection into a prompt. */
|
|
100
100
|
export function formatCrashInfo(lock: LockData): string {
|
|
101
|
-
|
|
101
|
+
const lines = [
|
|
102
102
|
`Previous auto-mode session was interrupted.`,
|
|
103
103
|
` Was executing: ${lock.unitType} (${lock.unitId})`,
|
|
104
104
|
` Started at: ${lock.unitStartedAt}`,
|
|
105
105
|
` Units completed before crash: ${lock.completedUnits}`,
|
|
106
106
|
` PID: ${lock.pid}`,
|
|
107
|
-
]
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// Add recovery guidance based on what was happening when it crashed
|
|
110
|
+
if (lock.unitType === "starting" && lock.unitId === "bootstrap" && lock.completedUnits === 0) {
|
|
111
|
+
lines.push(`No work was lost. Run /gsd auto to restart.`);
|
|
112
|
+
} else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
|
|
113
|
+
lines.push(`The ${lock.unitType} unit may be incomplete. Run /gsd auto to re-run it.`);
|
|
114
|
+
} else if (lock.unitType.includes("execute")) {
|
|
115
|
+
lines.push(`Task execution was interrupted. Run /gsd auto to resume — completed work is preserved.`);
|
|
116
|
+
} else if (lock.unitType.includes("complete")) {
|
|
117
|
+
lines.push(`Slice/milestone completion was interrupted. Run /gsd auto to finish.`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
108
121
|
}
|
|
@@ -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")));
|
|
@@ -5,6 +5,7 @@ import { readdirSync } from "node:fs";
|
|
|
5
5
|
import { resolveMilestoneFile, milestonesDir } from "./paths.js";
|
|
6
6
|
import { parseRoadmapSlices } from "./roadmap-slices.js";
|
|
7
7
|
import { findMilestoneIds } from "./guided-flow.js";
|
|
8
|
+
import { parseUnitId } from "./unit-id.js";
|
|
8
9
|
|
|
9
10
|
const SLICE_DISPATCH_TYPES = new Set([
|
|
10
11
|
"research-slice",
|
|
@@ -39,7 +40,7 @@ function readRoadmapFromDisk(base: string, milestoneId: string): string | null {
|
|
|
39
40
|
export function getPriorSliceCompletionBlocker(base: string, _mainBranch: string, unitType: string, unitId: string): string | null {
|
|
40
41
|
if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
+
const { milestone: targetMid, slice: targetSid } = parseUnitId(unitId);
|
|
43
44
|
if (!targetMid || !targetSid) return null;
|
|
44
45
|
|
|
45
46
|
// Use findMilestoneIds to respect custom queue order.
|