gsd-pi 2.82.0-dev.3a3c6509d → 2.82.0-dev.4285182e8
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 +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +14 -1
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto/workflow-kernel.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +12 -5
- package/dist/resources/extensions/gsd/auto.js +14 -7
- package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-verdict.js +139 -0
- package/dist/resources/extensions/gsd/markdown-renderer.js +10 -8
- package/dist/resources/extensions/gsd/paths.js +4 -0
- package/dist/resources/extensions/gsd/state.js +2 -2
- package/dist/resources/extensions/gsd/templates/plan.md +1 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +6 -0
- package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -5
- package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/8359.65b24fac92188a6b.js +10 -0
- package/dist/web/standalone/.next/static/chunks/9441.ff70bb53f6835771.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-9a4db269f9ed63ad.js → webpack-855d616060cb6e59.js} +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +13 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +16 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -2
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +14 -1
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto/workflow-kernel.ts +5 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +13 -5
- package/src/resources/extensions/gsd/auto.ts +13 -7
- package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-verdict.ts +202 -0
- package/src/resources/extensions/gsd/markdown-renderer.ts +10 -8
- package/src/resources/extensions/gsd/paths.ts +5 -0
- package/src/resources/extensions/gsd/state.ts +2 -2
- package/src/resources/extensions/gsd/templates/plan.md +1 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +6 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +5 -2
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +84 -0
- package/src/resources/extensions/gsd/tests/quality-gates.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -4
- package/src/resources/extensions/ttsr/ttsr-manager.ts +5 -1
- package/dist/web/standalone/.next/static/chunks/8359.7eb3bb8f8ecf4c01.js +0 -10
- package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +0 -1
- /package/dist/web/standalone/.next/static/{O6femb9LLl3nlgsDaYwS- → 78uanrILNOKG-Jpi4itAE}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{O6femb9LLl3nlgsDaYwS- → 78uanrILNOKG-Jpi4itAE}/_ssgManifest.js +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Main auto-mode execution loop.
|
|
1
3
|
/**
|
|
2
4
|
* auto/loop.ts — Main auto-mode execution loop.
|
|
3
5
|
*
|
|
@@ -945,11 +947,18 @@ export async function autoLoop(
|
|
|
945
947
|
unitId: iterData.unitId,
|
|
946
948
|
});
|
|
947
949
|
const finalizeReason = finalizeResult.action === "break" ? finalizeResult.reason : undefined;
|
|
950
|
+
const finalizeStatus = finalizeReason === "step-wizard"
|
|
951
|
+
? "completed"
|
|
952
|
+
: finalizeResult.action === "next"
|
|
953
|
+
? "completed"
|
|
954
|
+
: finalizeResult.action === "continue"
|
|
955
|
+
? "retry"
|
|
956
|
+
: "stopped";
|
|
948
957
|
journalReporter.emit("post-unit-finalize-end", {
|
|
949
958
|
iteration,
|
|
950
959
|
unitType: iterData.unitType,
|
|
951
960
|
unitId: iterData.unitId,
|
|
952
|
-
status:
|
|
961
|
+
status: finalizeStatus,
|
|
953
962
|
action: finalizeResult.action,
|
|
954
963
|
...(finalizeReason ? { reason: finalizeReason } : {}),
|
|
955
964
|
});
|
|
@@ -996,6 +1005,10 @@ export async function autoLoop(
|
|
|
996
1005
|
}) || dispatchSettled;
|
|
997
1006
|
completeIteration();
|
|
998
1007
|
finishTurn("completed");
|
|
1008
|
+
if (finalizeDecision.action === "complete-and-break") {
|
|
1009
|
+
s.preserveStepSurfaceAfterLoopExit = true;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
999
1012
|
} catch (loopErr) {
|
|
1000
1013
|
// ── Blanket catch: absorb unexpected exceptions, apply graduated recovery ──
|
|
1001
1014
|
const msg = loopErr instanceof Error ? loopErr.message : String(loopErr);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Mutable auto-mode session state container.
|
|
1
3
|
/**
|
|
2
4
|
* AutoSession — encapsulates all mutable auto-mode state into a single instance.
|
|
3
5
|
*
|
|
@@ -89,6 +91,7 @@ export class AutoSession {
|
|
|
89
91
|
active = false;
|
|
90
92
|
paused = false;
|
|
91
93
|
completionStopInProgress = false;
|
|
94
|
+
preserveStepSurfaceAfterLoopExit = false;
|
|
92
95
|
stepMode = false;
|
|
93
96
|
verbose = false;
|
|
94
97
|
activeEngineId: string | null = null;
|
|
@@ -289,6 +292,7 @@ export class AutoSession {
|
|
|
289
292
|
this.active = false;
|
|
290
293
|
this.paused = false;
|
|
291
294
|
this.completionStopInProgress = false;
|
|
295
|
+
this.preserveStepSurfaceAfterLoopExit = false;
|
|
292
296
|
this.stepMode = false;
|
|
293
297
|
this.verbose = false;
|
|
294
298
|
this.activeEngineId = null;
|
|
@@ -48,7 +48,8 @@ export type FinalizeDecision =
|
|
|
48
48
|
action: "retry";
|
|
49
49
|
ledgerErrorSummary: "finalize-retry";
|
|
50
50
|
}
|
|
51
|
-
| { action: "complete" }
|
|
51
|
+
| { action: "complete" }
|
|
52
|
+
| { action: "complete-and-break" };
|
|
52
53
|
|
|
53
54
|
export type EngineReconcileInput =
|
|
54
55
|
| { outcome: "milestone-complete" }
|
|
@@ -278,6 +279,9 @@ export function decideEngineDispatch(input: EngineDispatchInput): EngineDispatch
|
|
|
278
279
|
export function decideFinalizeResult(input: FinalizeInput): FinalizeDecision {
|
|
279
280
|
if (input.action === "break") {
|
|
280
281
|
const reason = input.reason ?? "unknown";
|
|
282
|
+
if (reason === "step-wizard") {
|
|
283
|
+
return { action: "complete-and-break" };
|
|
284
|
+
}
|
|
281
285
|
return {
|
|
282
286
|
action: "stop",
|
|
283
287
|
failureClass: reason === "git-closeout-failure" ? "git" : "closeout",
|
|
@@ -1340,7 +1340,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
1340
1340
|
if (verdict !== "pass") {
|
|
1341
1341
|
return {
|
|
1342
1342
|
action: "stop",
|
|
1343
|
-
reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or
|
|
1343
|
+
reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or run \`/gsd verdict pass --rationale "..."\` to override.`,
|
|
1344
1344
|
level: "warning",
|
|
1345
1345
|
};
|
|
1346
1346
|
}
|
|
@@ -47,7 +47,7 @@ import { regenerateIfMissing } from "./workflow-projections.js";
|
|
|
47
47
|
import { WorktreeStateProjection } from "./worktree-state-projection.js";
|
|
48
48
|
import { createWorkspace, scopeMilestone } from "./workspace.js";
|
|
49
49
|
import { normalizeWorktreePathForCompare } from "./worktree-root.js";
|
|
50
|
-
import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
|
|
50
|
+
import { isDbAvailable, getDbPath, refreshOpenDatabaseFromDisk, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
|
|
51
51
|
import { renderPlanCheckboxes } from "./markdown-renderer.js";
|
|
52
52
|
import { consumeSignal } from "./session-status-io.js";
|
|
53
53
|
import {
|
|
@@ -371,7 +371,7 @@ export function detectRogueFileWrites(
|
|
|
371
371
|
export const MAX_ARTIFACT_VERIFICATION_RETRIES = 3;
|
|
372
372
|
|
|
373
373
|
export const STEP_COMPLETE_FALLBACK_MESSAGE =
|
|
374
|
-
"Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
|
|
374
|
+
"Step complete. Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).";
|
|
375
375
|
|
|
376
376
|
export function buildStepCompleteMessage(nextState: import("./types.js").GSDState): string {
|
|
377
377
|
if (nextState.phase === "complete") {
|
|
@@ -379,7 +379,7 @@ export function buildStepCompleteMessage(nextState: import("./types.js").GSDStat
|
|
|
379
379
|
}
|
|
380
380
|
const next = describeNextUnit(nextState);
|
|
381
381
|
return `Step complete. Next: ${next.label}\n`
|
|
382
|
-
+ `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
|
|
382
|
+
+ `Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).`;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
/**
|
|
@@ -686,6 +686,14 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
686
686
|
await new Promise(r => setTimeout(r, 100));
|
|
687
687
|
}
|
|
688
688
|
|
|
689
|
+
const dbPath = getDbPath();
|
|
690
|
+
if (isDbAvailable() && dbPath && dbPath !== ":memory:") {
|
|
691
|
+
const refreshed = refreshOpenDatabaseFromDisk();
|
|
692
|
+
if (!refreshed) {
|
|
693
|
+
logWarning("db", "post-unit database refresh failed; derived state may be stale");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
689
697
|
// Turn-level git action (commit | snapshot | status-only)
|
|
690
698
|
if (s.currentUnit) {
|
|
691
699
|
const unit = s.currentUnit;
|
|
@@ -1731,8 +1739,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
|
|
|
1731
1739
|
}
|
|
1732
1740
|
|
|
1733
1741
|
// Step mode → show wizard instead of dispatch.
|
|
1734
|
-
// Without this notify(), /gsd
|
|
1735
|
-
//
|
|
1742
|
+
// Without this notify(), /gsd next finishes a unit and silently exits the
|
|
1743
|
+
// loop, leaving the user with no next-step command.
|
|
1736
1744
|
if (s.stepMode) {
|
|
1737
1745
|
let phaseAfterUnit: string | null = null;
|
|
1738
1746
|
try {
|
|
@@ -1029,6 +1029,8 @@ export async function rerootCommandSession(
|
|
|
1029
1029
|
}
|
|
1030
1030
|
|
|
1031
1031
|
export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void> {
|
|
1032
|
+
const preserveStepSurface = s.preserveStepSurfaceAfterLoopExit;
|
|
1033
|
+
const preservePausedSurface = s.paused;
|
|
1032
1034
|
s.currentUnit = null;
|
|
1033
1035
|
s.active = false;
|
|
1034
1036
|
deactivateGSD();
|
|
@@ -1051,12 +1053,16 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
|
|
|
1051
1053
|
// A transient provider-error pause intentionally leaves the paused badge
|
|
1052
1054
|
// visible so the user still has a resumable auto-mode signal on screen.
|
|
1053
1055
|
if (!s.paused) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1056
|
+
if (preserveStepSurface) {
|
|
1057
|
+
s.preserveStepSurfaceAfterLoopExit = false;
|
|
1058
|
+
} else {
|
|
1059
|
+
ctx.ui.setStatus("gsd-auto", undefined);
|
|
1060
|
+
ctx.ui.setWidget("gsd-progress", undefined);
|
|
1061
|
+
if (s.completionStopInProgress) {
|
|
1062
|
+
s.completionStopInProgress = false;
|
|
1063
|
+
}
|
|
1064
|
+
initHealthWidget(ctx);
|
|
1058
1065
|
}
|
|
1059
|
-
initHealthWidget(ctx);
|
|
1060
1066
|
}
|
|
1061
1067
|
|
|
1062
1068
|
// ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
|
|
@@ -1064,7 +1070,7 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
|
|
|
1064
1070
|
// `s.basePath` mutation and the paired `process.chdir` for auto-loop
|
|
1065
1071
|
// transitions. The verb assigns `s.basePath` before any throwable work, so
|
|
1066
1072
|
// a thrown error still leaves basePath restored.
|
|
1067
|
-
if (s.originalBasePath) {
|
|
1073
|
+
if (s.originalBasePath && !preserveStepSurface && !preservePausedSurface) {
|
|
1068
1074
|
try {
|
|
1069
1075
|
buildLifecycle().restoreToProjectRoot();
|
|
1070
1076
|
} catch (err) {
|
|
@@ -1076,7 +1082,7 @@ export async function cleanupAfterLoopExit(ctx: ExtensionContext): Promise<void>
|
|
|
1076
1082
|
}
|
|
1077
1083
|
}
|
|
1078
1084
|
|
|
1079
|
-
if (s.originalBasePath && s.cmdCtx) {
|
|
1085
|
+
if (s.originalBasePath && s.cmdCtx && !preserveStepSurface && !preservePausedSurface) {
|
|
1080
1086
|
const result = await rerootCommandSession(s.cmdCtx, s.originalBasePath);
|
|
1081
1087
|
if (result.status === "cancelled") {
|
|
1082
1088
|
logWarning("engine", "post-loop session re-root was cancelled", { file: "auto.ts", basePath: s.originalBasePath });
|
|
@@ -14,7 +14,7 @@ export interface GsdCommandDefinition {
|
|
|
14
14
|
type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
|
|
15
15
|
|
|
16
16
|
export const GSD_COMMAND_DESCRIPTION =
|
|
17
|
-
"GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|brief|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|new-project|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language|worktree|eval-review";
|
|
17
|
+
"GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|brief|queue|quick|discuss|capture|triage|dispatch|verdict|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|debug|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|new-project|parallel|cmux|park|unpark|init|setup|onboarding|inspect|extensions|update|fast|mcp|rethink|workflow|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|language|worktree|eval-review";
|
|
18
18
|
|
|
19
19
|
export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
|
|
20
20
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -33,6 +33,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
|
|
|
33
33
|
{ cmd: "changelog", desc: "Show categorized release notes" },
|
|
34
34
|
{ cmd: "triage", desc: "Manually trigger triage of pending captures" },
|
|
35
35
|
{ cmd: "dispatch", desc: "Dispatch a specific phase directly" },
|
|
36
|
+
{ cmd: "verdict", desc: "Override the recorded milestone validation verdict (pass|needs-attention|needs-remediation)" },
|
|
36
37
|
{ cmd: "history", desc: "View execution history" },
|
|
37
38
|
{ cmd: "undo", desc: "Revert last completed unit" },
|
|
38
39
|
{ cmd: "undo-task", desc: "Reset a specific task's completion state (DB + markdown)" },
|
|
@@ -248,6 +249,11 @@ const NESTED_COMPLETIONS: CompletionMap = {
|
|
|
248
249
|
{ cmd: "uat", desc: "Run user acceptance testing" },
|
|
249
250
|
{ cmd: "replan", desc: "Replan the current slice" },
|
|
250
251
|
],
|
|
252
|
+
verdict: [
|
|
253
|
+
{ cmd: "pass", desc: "Override the milestone validation verdict to pass" },
|
|
254
|
+
{ cmd: "needs-attention", desc: "Override the verdict to needs-attention (requires --rationale)" },
|
|
255
|
+
{ cmd: "needs-remediation", desc: "Override the verdict to needs-remediation (requires --rationale)" },
|
|
256
|
+
],
|
|
251
257
|
rate: [
|
|
252
258
|
{ cmd: "over", desc: "Model was overqualified for this task" },
|
|
253
259
|
{ cmd: "ok", desc: "Model was appropriate for this task" },
|
|
@@ -71,6 +71,7 @@ export function showHelp(ctx: ExtensionCommandContext, args = ""): void {
|
|
|
71
71
|
" /gsd new-project Bootstrap a new project (use --deep for staged project-level discovery)",
|
|
72
72
|
" /gsd quick Execute a quick task without full planning overhead",
|
|
73
73
|
" /gsd dispatch Dispatch a specific phase directly [research|plan|execute|complete|uat|replan]",
|
|
74
|
+
" /gsd verdict <v> Override milestone validation verdict [pass|needs-attention|needs-remediation] [--milestone Mxxx] [--rationale \"...\"]",
|
|
74
75
|
" /gsd parallel Parallel milestone orchestration [start|status|stop|pause|resume|merge|watch]",
|
|
75
76
|
" /gsd workflow Custom workflow lifecycle [new|run|list|validate|pause|resume]",
|
|
76
77
|
"",
|
|
@@ -188,6 +188,11 @@ Examples:
|
|
|
188
188
|
await dispatchDirectPhase(ctx, pi, phase, projectRoot());
|
|
189
189
|
return true;
|
|
190
190
|
}
|
|
191
|
+
if (trimmed === "verdict" || trimmed.startsWith("verdict ")) {
|
|
192
|
+
const { handleVerdict } = await import("../../commands-verdict.js");
|
|
193
|
+
await handleVerdict(trimmed.replace(/^verdict\s*/, "").trim(), ctx, projectRoot());
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
191
196
|
if (trimmed === "notifications" || trimmed.startsWith("notifications ")) {
|
|
192
197
|
const { handleNotificationsCommand } = await import("./notifications-handler.js");
|
|
193
198
|
await handleNotificationsCommand(trimmed.replace(/^notifications\s*/, "").trim(), ctx, pi);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { loadFile } from "./files.js";
|
|
4
|
+
import { resolveMilestoneFile } from "./paths.js";
|
|
5
|
+
import { deriveState } from "./state.js";
|
|
6
|
+
import { executeValidateMilestone } from "./tools/workflow-tool-executors.js";
|
|
7
|
+
import {
|
|
8
|
+
VALIDATION_VERDICTS,
|
|
9
|
+
extractVerdict,
|
|
10
|
+
isValidMilestoneVerdict,
|
|
11
|
+
type ValidationVerdict,
|
|
12
|
+
} from "./verdict-parser.js";
|
|
13
|
+
|
|
14
|
+
const USAGE =
|
|
15
|
+
'Usage: /gsd verdict <pass|needs-attention|needs-remediation> [--milestone Mxxx] [--rationale "..."]';
|
|
16
|
+
|
|
17
|
+
interface ParsedArgs {
|
|
18
|
+
verdict?: ValidationVerdict;
|
|
19
|
+
milestoneId?: string;
|
|
20
|
+
rationale?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ParsedValidation {
|
|
24
|
+
verdict: string | undefined;
|
|
25
|
+
remediationRound: number;
|
|
26
|
+
successCriteriaChecklist: string;
|
|
27
|
+
sliceDeliveryAudit: string;
|
|
28
|
+
crossSliceIntegration: string;
|
|
29
|
+
requirementCoverage: string;
|
|
30
|
+
verificationClasses?: string;
|
|
31
|
+
verdictRationale: string;
|
|
32
|
+
remediationPlan?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function tokenize(raw: string): string[] {
|
|
36
|
+
const tokens: string[] = [];
|
|
37
|
+
const re = /"([^"]*)"|(\S+)/g;
|
|
38
|
+
let match: RegExpExecArray | null;
|
|
39
|
+
while ((match = re.exec(raw)) !== null) {
|
|
40
|
+
tokens.push(match[1] ?? match[2]);
|
|
41
|
+
}
|
|
42
|
+
return tokens;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArgs(raw: string): ParsedArgs | { error: string } {
|
|
46
|
+
const tokens = tokenize(raw);
|
|
47
|
+
const out: ParsedArgs = {};
|
|
48
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
49
|
+
const t = tokens[i];
|
|
50
|
+
if (t === "--milestone") {
|
|
51
|
+
const next = tokens[++i];
|
|
52
|
+
if (!next) return { error: "--milestone requires a milestone ID" };
|
|
53
|
+
out.milestoneId = next;
|
|
54
|
+
} else if (t === "--rationale") {
|
|
55
|
+
const next = tokens[++i];
|
|
56
|
+
if (next == null) return { error: "--rationale requires a value" };
|
|
57
|
+
out.rationale = next;
|
|
58
|
+
} else if (!out.verdict) {
|
|
59
|
+
if (!isValidMilestoneVerdict(t)) {
|
|
60
|
+
return {
|
|
61
|
+
error: `Invalid verdict "${t}". Must be one of: ${VALIDATION_VERDICTS.join(", ")}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
out.verdict = t;
|
|
65
|
+
} else {
|
|
66
|
+
return { error: `Unexpected argument: ${t}` };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractRemediationRound(content: string): number {
|
|
73
|
+
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
74
|
+
if (!fm) return 0;
|
|
75
|
+
const m = fm[1].match(/^remediation_round:\s*(\d+)/im);
|
|
76
|
+
return m ? Number.parseInt(m[1], 10) : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractSection(content: string, heading: string): string | undefined {
|
|
80
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
81
|
+
// Match section bodies bounded by the next "## " heading or end-of-string.
|
|
82
|
+
// Leading "\n" prefix lets a single pattern handle first-line headings too.
|
|
83
|
+
// No /m flag — we want `$` to mean end-of-string, not end-of-line.
|
|
84
|
+
const re = new RegExp(`\\n## ${escaped}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
85
|
+
const m = ("\n" + content).match(re);
|
|
86
|
+
if (!m) return undefined;
|
|
87
|
+
return m[1].replace(/\s+$/, "");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseValidationFile(content: string): ParsedValidation {
|
|
91
|
+
return {
|
|
92
|
+
verdict: extractVerdict(content),
|
|
93
|
+
remediationRound: extractRemediationRound(content),
|
|
94
|
+
successCriteriaChecklist: extractSection(content, "Success Criteria Checklist") ?? "",
|
|
95
|
+
sliceDeliveryAudit: extractSection(content, "Slice Delivery Audit") ?? "",
|
|
96
|
+
crossSliceIntegration: extractSection(content, "Cross-Slice Integration") ?? "",
|
|
97
|
+
requirementCoverage: extractSection(content, "Requirement Coverage") ?? "",
|
|
98
|
+
verificationClasses: extractSection(content, "Verification Class Compliance"),
|
|
99
|
+
verdictRationale: extractSection(content, "Verdict Rationale") ?? "",
|
|
100
|
+
remediationPlan: extractSection(content, "Remediation Plan"),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function handleVerdict(
|
|
105
|
+
rawArgs: string,
|
|
106
|
+
ctx: ExtensionCommandContext,
|
|
107
|
+
basePath: string,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
if (!rawArgs.trim()) {
|
|
110
|
+
ctx.ui.notify(USAGE, "warning");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const parsed = parseArgs(rawArgs);
|
|
115
|
+
if ("error" in parsed) {
|
|
116
|
+
ctx.ui.notify(`${parsed.error}\n${USAGE}`, "warning");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!parsed.verdict) {
|
|
120
|
+
ctx.ui.notify(USAGE, "warning");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let milestoneId = parsed.milestoneId;
|
|
125
|
+
if (!milestoneId) {
|
|
126
|
+
const state = await deriveState(basePath);
|
|
127
|
+
if (!state.activeMilestone) {
|
|
128
|
+
ctx.ui.notify(
|
|
129
|
+
"No active milestone — pass --milestone Mxxx to target a specific milestone.",
|
|
130
|
+
"warning",
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
milestoneId = state.activeMilestone.id;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const validationPath = resolveMilestoneFile(basePath, milestoneId, "VALIDATION");
|
|
138
|
+
if (!validationPath) {
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
`No VALIDATION file found for ${milestoneId}. Run gsd_validate_milestone first to produce one.`,
|
|
141
|
+
"warning",
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const existing = await loadFile(validationPath);
|
|
146
|
+
if (!existing) {
|
|
147
|
+
ctx.ui.notify(
|
|
148
|
+
`Could not read VALIDATION file for ${milestoneId} (${validationPath}).`,
|
|
149
|
+
"warning",
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const current = parseValidationFile(existing);
|
|
155
|
+
|
|
156
|
+
if (parsed.verdict !== "pass" && !parsed.rationale) {
|
|
157
|
+
ctx.ui.notify(
|
|
158
|
+
`--rationale is required when overriding to ${parsed.verdict}.`,
|
|
159
|
+
"warning",
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const verdictRationale =
|
|
165
|
+
parsed.rationale ?? "Manually overridden via /gsd verdict";
|
|
166
|
+
|
|
167
|
+
const result = await executeValidateMilestone(
|
|
168
|
+
{
|
|
169
|
+
milestoneId,
|
|
170
|
+
verdict: parsed.verdict,
|
|
171
|
+
remediationRound: current.remediationRound,
|
|
172
|
+
successCriteriaChecklist: current.successCriteriaChecklist,
|
|
173
|
+
sliceDeliveryAudit: current.sliceDeliveryAudit,
|
|
174
|
+
crossSliceIntegration: current.crossSliceIntegration,
|
|
175
|
+
requirementCoverage: current.requirementCoverage,
|
|
176
|
+
verificationClasses: current.verificationClasses,
|
|
177
|
+
verdictRationale,
|
|
178
|
+
remediationPlan: current.remediationPlan,
|
|
179
|
+
},
|
|
180
|
+
basePath,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (result.isError) {
|
|
184
|
+
const msg =
|
|
185
|
+
result.content[0]?.type === "text" ? result.content[0].text : "Unknown error";
|
|
186
|
+
ctx.ui.notify(msg, "error");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const prevVerdict = current.verdict ?? "unknown";
|
|
191
|
+
ctx.ui.notify(
|
|
192
|
+
`Milestone ${milestoneId} verdict: ${prevVerdict} -> ${parsed.verdict}`,
|
|
193
|
+
"success",
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (parsed.verdict === "needs-remediation") {
|
|
197
|
+
ctx.ui.notify(
|
|
198
|
+
"Follow up with gsd_reassess_roadmap to add remediation slices, then re-run /gsd auto.",
|
|
199
|
+
"info",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
resolveMilestoneFile,
|
|
33
33
|
resolveSliceFile,
|
|
34
34
|
resolveSlicePath,
|
|
35
|
-
|
|
35
|
+
gsdProjectionRoot,
|
|
36
36
|
gsdRoot,
|
|
37
37
|
buildTaskFileName,
|
|
38
38
|
buildSliceFileName,
|
|
@@ -48,7 +48,11 @@ import { clearPathCache } from "./paths.js";
|
|
|
48
48
|
* E.g. "/project/.gsd/milestones/M001/M001-ROADMAP.md" → "milestones/M001/M001-ROADMAP.md"
|
|
49
49
|
*/
|
|
50
50
|
function toArtifactPath(absPath: string, basePath: string): string {
|
|
51
|
-
const
|
|
51
|
+
const projectionRoot = gsdProjectionRoot(basePath);
|
|
52
|
+
const projectionRel = relative(projectionRoot, absPath);
|
|
53
|
+
const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
|
|
54
|
+
? projectionRoot
|
|
55
|
+
: gsdRoot(basePath);
|
|
52
56
|
const rel = relative(root, absPath);
|
|
53
57
|
// Normalize to forward slashes for consistent DB keys
|
|
54
58
|
return rel.replace(/\\/g, "/");
|
|
@@ -374,10 +378,9 @@ export async function renderPlanFromDb(
|
|
|
374
378
|
throw new Error(`no tasks found for ${milestoneId}/${sliceId}`);
|
|
375
379
|
}
|
|
376
380
|
|
|
377
|
-
const slicePath =
|
|
378
|
-
|
|
379
|
-
const absPath =
|
|
380
|
-
?? join(slicePath, `${sliceId}-PLAN.md`);
|
|
381
|
+
const slicePath = join(gsdProjectionRoot(basePath), "milestones", milestoneId, "slices", sliceId);
|
|
382
|
+
mkdirSync(slicePath, { recursive: true });
|
|
383
|
+
const absPath = join(slicePath, `${sliceId}-PLAN.md`);
|
|
381
384
|
const artifactPath = toArtifactPath(absPath, basePath);
|
|
382
385
|
const sliceGates = getGateResults(milestoneId, sliceId, "slice");
|
|
383
386
|
const content = renderSlicePlanMarkdown(slice, tasks, sliceGates);
|
|
@@ -408,8 +411,7 @@ export async function renderTaskPlanFromDb(
|
|
|
408
411
|
throw new Error(`task ${milestoneId}/${sliceId}/${taskId} not found`);
|
|
409
412
|
}
|
|
410
413
|
|
|
411
|
-
const tasksDir =
|
|
412
|
-
?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks");
|
|
414
|
+
const tasksDir = join(gsdProjectionRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks");
|
|
413
415
|
mkdirSync(tasksDir, { recursive: true });
|
|
414
416
|
const absPath = join(tasksDir, buildTaskFileName(taskId, "PLAN"));
|
|
415
417
|
const artifactPath = toArtifactPath(absPath, basePath);
|
|
@@ -357,6 +357,11 @@ export function resolveGsdPathContract(
|
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
export function gsdProjectionRoot(basePath: string): string {
|
|
361
|
+
const contract = resolveGsdPathContract(basePath);
|
|
362
|
+
return normalizeRealPath(contract.worktreeGsd ?? contract.projectGsd);
|
|
363
|
+
}
|
|
364
|
+
|
|
360
365
|
/**
|
|
361
366
|
* Invalidate the gsdRoot cache.
|
|
362
367
|
* Use ONLY at session-reset boundaries: workspace switch, process exit, or
|
|
@@ -591,7 +591,7 @@ async function handleAllSlicesDone(
|
|
|
591
591
|
recentDecisions: [],
|
|
592
592
|
blockers: [
|
|
593
593
|
`Milestone ${activeMilestone.id} validation verdict is needs-remediation but all slices are complete. ` +
|
|
594
|
-
`Add remediation slices via gsd_reassess_roadmap or
|
|
594
|
+
`Add remediation slices via gsd_reassess_roadmap, or run \`/gsd verdict pass --rationale "..."\` to override.`,
|
|
595
595
|
],
|
|
596
596
|
nextAction: `Resolve ${activeMilestone.id} remediation before proceeding.`,
|
|
597
597
|
registry, requirements,
|
|
@@ -1314,7 +1314,7 @@ export async function _deriveStateImpl(
|
|
|
1314
1314
|
recentDecisions: [],
|
|
1315
1315
|
blockers: [
|
|
1316
1316
|
`Milestone ${activeMilestone.id} validation verdict is needs-remediation but all slices are complete. ` +
|
|
1317
|
-
`Add remediation slices via gsd_reassess_roadmap or
|
|
1317
|
+
`Add remediation slices via gsd_reassess_roadmap, or run \`/gsd verdict pass --rationale "..."\` to override.`,
|
|
1318
1318
|
],
|
|
1319
1319
|
nextAction: `Resolve ${activeMilestone.id} remediation before proceeding.`,
|
|
1320
1320
|
registry,
|
|
@@ -132,6 +132,7 @@
|
|
|
132
132
|
Verify field rules:
|
|
133
133
|
- MUST be a mechanically executable command: `npm test`, `grep -q "pattern" file`, `test -f path`
|
|
134
134
|
- MUST NOT use shell pipes, redirects, semicolons, backticks, command substitution, or output trimming
|
|
135
|
+
- MUST NOT use inline `node -e` assertions for verification; put assertions in a real test file and run it with `node --test` or a package test script
|
|
135
136
|
- For content/document tasks: verify file existence, section count, YAML validity, or word count
|
|
136
137
|
NOT exact phrasing, specific formulas, or "zero TBD" aspirational criteria
|
|
137
138
|
- If no command can verify the output, write: "Manual review — file exists and is non-empty"
|
|
@@ -57,6 +57,12 @@ skills_used:
|
|
|
57
57
|
- {{howToVerifyThisTaskIsActuallyDone}}
|
|
58
58
|
- {{commandToRun_OR_behaviorToCheck}}
|
|
59
59
|
|
|
60
|
+
## Verify Rules
|
|
61
|
+
|
|
62
|
+
- Use a real executable check, not prose.
|
|
63
|
+
- If the check needs file-content assertions, write a `node:test` file and run it with `node --test` or a package test script.
|
|
64
|
+
- Do not use inline `node -e` assertions for verification.
|
|
65
|
+
|
|
60
66
|
## Observability Impact
|
|
61
67
|
|
|
62
68
|
<!-- OMIT THIS SECTION ENTIRELY for simple tasks that don't touch runtime boundaries,
|
|
@@ -43,6 +43,52 @@ test("cleanupAfterLoopExit preserves paused auto badge after provider pause", as
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
test("cleanupAfterLoopExit preserves paused worktree session and visible failure output", async (t) => {
|
|
47
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-paused-session-preserve-"));
|
|
48
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
49
|
+
const previousCwd = process.cwd();
|
|
50
|
+
const newSessionWorkspaces: string[] = [];
|
|
51
|
+
let restoreCalls = 0;
|
|
52
|
+
|
|
53
|
+
t.mock.method(WorktreeLifecycle.prototype, "restoreToProjectRoot", function () {
|
|
54
|
+
restoreCalls += 1;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
mkdirSync(worktree, { recursive: true });
|
|
58
|
+
process.chdir(worktree);
|
|
59
|
+
autoSession.reset();
|
|
60
|
+
autoSession.active = true;
|
|
61
|
+
autoSession.paused = true;
|
|
62
|
+
autoSession.basePath = worktree;
|
|
63
|
+
autoSession.originalBasePath = base;
|
|
64
|
+
autoSession.cmdCtx = {
|
|
65
|
+
newSession: async ({ workspaceRoot }: { workspaceRoot: string }) => {
|
|
66
|
+
newSessionWorkspaces.push(workspaceRoot);
|
|
67
|
+
return { cancelled: false };
|
|
68
|
+
},
|
|
69
|
+
} as any;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await cleanupAfterLoopExit({
|
|
73
|
+
ui: {
|
|
74
|
+
setStatus: () => {},
|
|
75
|
+
setWidget: () => {},
|
|
76
|
+
notify: () => {},
|
|
77
|
+
},
|
|
78
|
+
} as any);
|
|
79
|
+
|
|
80
|
+
assert.equal(restoreCalls, 0, "paused cleanup must not restore out of the active worktree");
|
|
81
|
+
assert.deepEqual(newSessionWorkspaces, [], "paused cleanup must not start a blank rerooted session");
|
|
82
|
+
assert.equal(autoSession.basePath, worktree);
|
|
83
|
+
assert.equal(realpathSync(process.cwd()), realpathSync(worktree));
|
|
84
|
+
assert.equal(autoSession.paused, true);
|
|
85
|
+
} finally {
|
|
86
|
+
autoSession.reset();
|
|
87
|
+
process.chdir(previousCwd);
|
|
88
|
+
rmSync(base, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
46
92
|
test("cleanupAfterLoopExit clears status and progress widget without replacing outcome surface", async () => {
|
|
47
93
|
const statusCalls: unknown[] = [];
|
|
48
94
|
const widgetCalls: unknown[] = [];
|
|
@@ -145,6 +191,70 @@ test("pauseAuto preserves artifact retry counts across pause/resume", async () =
|
|
|
145
191
|
}
|
|
146
192
|
});
|
|
147
193
|
|
|
194
|
+
test("cleanupAfterLoopExit preserves step-mode surface and worktree session after completed step", async (t) => {
|
|
195
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-step-surface-"));
|
|
196
|
+
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
197
|
+
const previousCwd = process.cwd();
|
|
198
|
+
const statusCalls: unknown[] = [];
|
|
199
|
+
const widgetCalls: unknown[] = [];
|
|
200
|
+
const newSessionWorkspaces: string[] = [];
|
|
201
|
+
let restoreCalls = 0;
|
|
202
|
+
|
|
203
|
+
t.mock.method(WorktreeLifecycle.prototype, "restoreToProjectRoot", function () {
|
|
204
|
+
restoreCalls += 1;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
mkdirSync(worktree, { recursive: true });
|
|
208
|
+
process.chdir(worktree);
|
|
209
|
+
autoSession.reset();
|
|
210
|
+
autoSession.active = true;
|
|
211
|
+
autoSession.paused = false;
|
|
212
|
+
autoSession.stepMode = true;
|
|
213
|
+
autoSession.preserveStepSurfaceAfterLoopExit = true;
|
|
214
|
+
autoSession.basePath = worktree;
|
|
215
|
+
autoSession.originalBasePath = base;
|
|
216
|
+
autoSession.cmdCtx = {
|
|
217
|
+
newSession: async ({ workspaceRoot }: { workspaceRoot: string }) => {
|
|
218
|
+
newSessionWorkspaces.push(workspaceRoot);
|
|
219
|
+
return { cancelled: false };
|
|
220
|
+
},
|
|
221
|
+
} as any;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await cleanupAfterLoopExit({
|
|
225
|
+
hasUI: true,
|
|
226
|
+
ui: {
|
|
227
|
+
setStatus: (...args: unknown[]) => statusCalls.push(args),
|
|
228
|
+
setWidget: (...args: unknown[]) => widgetCalls.push(args),
|
|
229
|
+
setHeader: () => {},
|
|
230
|
+
notify: () => {},
|
|
231
|
+
},
|
|
232
|
+
} as any);
|
|
233
|
+
|
|
234
|
+
assert.deepEqual(statusCalls, [], "step-mode cleanup must leave the NEXT badge visible");
|
|
235
|
+
assert.equal(
|
|
236
|
+
widgetCalls.some((args) => Array.isArray(args) && args[0] === "gsd-progress" && args[1] === undefined),
|
|
237
|
+
false,
|
|
238
|
+
"step-mode cleanup must not clear the completed step progress surface",
|
|
239
|
+
);
|
|
240
|
+
assert.equal(
|
|
241
|
+
widgetCalls.some((args) => Array.isArray(args) && args[0] === "gsd-health"),
|
|
242
|
+
false,
|
|
243
|
+
"step-mode cleanup must not replace the progress surface with idle health",
|
|
244
|
+
);
|
|
245
|
+
assert.deepEqual(newSessionWorkspaces, [], "step-mode cleanup must not re-root the visible command session");
|
|
246
|
+
assert.equal(restoreCalls, 0, "step-mode cleanup must not restore out of the active worktree");
|
|
247
|
+
assert.equal(autoSession.active, false);
|
|
248
|
+
assert.equal(autoSession.preserveStepSurfaceAfterLoopExit, false);
|
|
249
|
+
assert.equal(autoSession.basePath, worktree);
|
|
250
|
+
assert.equal(realpathSync(process.cwd()), realpathSync(worktree));
|
|
251
|
+
} finally {
|
|
252
|
+
autoSession.reset();
|
|
253
|
+
process.chdir(previousCwd);
|
|
254
|
+
rmSync(base, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
148
258
|
test("cleanupAfterLoopExit restores project root through lifecycle and preserves chdir", async (t) => {
|
|
149
259
|
const base = mkdtempSync(join(tmpdir(), "gsd-cleanup-lifecycle-"));
|
|
150
260
|
const worktree = join(base, ".gsd", "worktrees", "M001");
|