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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +14 -1
  4. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  5. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +3 -0
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-post-unit.js +12 -5
  8. package/dist/resources/extensions/gsd/auto.js +14 -7
  9. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  10. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  11. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  12. package/dist/resources/extensions/gsd/commands-verdict.js +139 -0
  13. package/dist/resources/extensions/gsd/markdown-renderer.js +10 -8
  14. package/dist/resources/extensions/gsd/paths.js +4 -0
  15. package/dist/resources/extensions/gsd/state.js +2 -2
  16. package/dist/resources/extensions/gsd/templates/plan.md +1 -0
  17. package/dist/resources/extensions/gsd/templates/task-plan.md +6 -0
  18. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -5
  19. package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -1
  20. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  23. package/dist/web/standalone/.next/build-manifest.json +3 -3
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.html +1 -1
  43. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  50. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  53. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  54. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  55. package/dist/web/standalone/.next/static/chunks/8359.65b24fac92188a6b.js +10 -0
  56. package/dist/web/standalone/.next/static/chunks/9441.ff70bb53f6835771.js +1 -0
  57. package/dist/web/standalone/.next/static/chunks/{webpack-9a4db269f9ed63ad.js → webpack-855d616060cb6e59.js} +1 -1
  58. package/package.json +1 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +13 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
  61. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  62. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  63. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +5 -2
  64. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  65. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +16 -1
  66. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -2
  67. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  68. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  69. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  70. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +5 -1
  71. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  72. package/src/resources/extensions/gsd/auto-post-unit.ts +13 -5
  73. package/src/resources/extensions/gsd/auto.ts +13 -7
  74. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  75. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  76. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  77. package/src/resources/extensions/gsd/commands-verdict.ts +202 -0
  78. package/src/resources/extensions/gsd/markdown-renderer.ts +10 -8
  79. package/src/resources/extensions/gsd/paths.ts +5 -0
  80. package/src/resources/extensions/gsd/state.ts +2 -2
  81. package/src/resources/extensions/gsd/templates/plan.md +1 -0
  82. package/src/resources/extensions/gsd/templates/task-plan.md +6 -0
  83. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +110 -0
  84. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +6 -5
  85. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +378 -0
  86. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +5 -2
  87. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +26 -1
  88. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +84 -0
  89. package/src/resources/extensions/gsd/tests/quality-gates.test.ts +6 -0
  90. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  91. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -4
  92. package/src/resources/extensions/ttsr/ttsr-manager.ts +5 -1
  93. package/dist/web/standalone/.next/static/chunks/8359.7eb3bb8f8ecf4c01.js +0 -10
  94. package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +0 -1
  95. /package/dist/web/standalone/.next/static/{O6femb9LLl3nlgsDaYwS- → 78uanrILNOKG-Jpi4itAE}/_buildManifest.js +0 -0
  96. /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: finalizeResult.action === "next" ? "completed" : finalizeResult.action === "continue" ? "retry" : "stopped",
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 update the verdict manually.`,
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 in step mode finishes a unit and silently
1735
- // exits the loop, leaving the user with no hint to /clear and /gsd again.
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
- ctx.ui.setStatus("gsd-auto", undefined);
1055
- ctx.ui.setWidget("gsd-progress", undefined);
1056
- if (s.completionStopInProgress) {
1057
- s.completionStopInProgress = false;
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
- resolveTasksDir,
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 root = gsdRoot(basePath);
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 = resolveSlicePath(basePath, milestoneId, sliceId)
378
- ?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId);
379
- const absPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN")
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 = resolveTasksDir(basePath, milestoneId, sliceId)
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 override the verdict manually.`,
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 override the verdict manually.`,
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");