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
package/README.md CHANGED
@@ -350,7 +350,7 @@ This is what makes GSD different. Run it, walk away, come back to built software
350
350
 
351
351
  Auto mode is a state machine driven by the GSD database at the project root. It derives the next unit of work from authoritative SQLite state, creates a fresh agent session, injects a focused prompt with all relevant context pre-inlined, and lets the LLM execute. When the LLM finishes, auto mode persists the result to the database, refreshes markdown projections such as `STATE.md`, and dispatches the next unit.
352
352
 
353
- The database is authoritative for milestones, slices, tasks, requirements, summaries, and completion status. Durable decisions and project knowledge are stored in the `memories` table: decisions are `architecture` memories, and KNOWLEDGE patterns/lessons are `pattern`/`gotcha` memories. Markdown under `.gsd/` is a rendered projection for review, prompts, and git-friendly history; it is not a runtime fallback unless you explicitly run a recovery/import command. In worktree mode, project-root DB state remains authoritative and worktree markdown projections are not synced back as state.
353
+ The database is authoritative for milestones, slices, tasks, requirements, summaries, and completion status. Durable decisions and project knowledge are stored in the `memories` table: decisions are `architecture` memories, and KNOWLEDGE patterns/lessons are `pattern`/`gotcha` memories. Markdown under `.gsd/` is a rendered projection for review, prompts, and git-friendly history; it is not a runtime fallback unless you explicitly run a recovery/import command. In worktree mode, artifact/projection writes are rendered under the active worktree-local `.gsd/`, while the project-root DB remains authoritative runtime state.
354
354
 
355
355
  `KNOWLEDGE.md` is hybrid: rules remain file-canonical, while patterns and lessons are stored in the `memories` table and rendered back into `KNOWLEDGE.md` on the next session-start projection. Existing pattern and lesson rows are backfilled into memories before projection, so newly captured patterns and lessons may appear in memory-backed prompt context before the file view refreshes.
356
356
 
@@ -1 +1 @@
1
- b9c1b29c0681f84f
1
+ 8102192ede112252
@@ -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
  *
@@ -789,11 +791,18 @@ export async function autoLoop(ctx, pi, s, deps, options) {
789
791
  unitId: iterData.unitId,
790
792
  });
791
793
  const finalizeReason = finalizeResult.action === "break" ? finalizeResult.reason : undefined;
794
+ const finalizeStatus = finalizeReason === "step-wizard"
795
+ ? "completed"
796
+ : finalizeResult.action === "next"
797
+ ? "completed"
798
+ : finalizeResult.action === "continue"
799
+ ? "retry"
800
+ : "stopped";
792
801
  journalReporter.emit("post-unit-finalize-end", {
793
802
  iteration,
794
803
  unitType: iterData.unitType,
795
804
  unitId: iterData.unitId,
796
- status: finalizeResult.action === "next" ? "completed" : finalizeResult.action === "continue" ? "retry" : "stopped",
805
+ status: finalizeStatus,
797
806
  action: finalizeResult.action,
798
807
  ...(finalizeReason ? { reason: finalizeReason } : {}),
799
808
  });
@@ -837,6 +846,10 @@ export async function autoLoop(ctx, pi, s, deps, options) {
837
846
  }) || dispatchSettled;
838
847
  completeIteration();
839
848
  finishTurn("completed");
849
+ if (finalizeDecision.action === "complete-and-break") {
850
+ s.preserveStepSurfaceAfterLoopExit = true;
851
+ break;
852
+ }
840
853
  }
841
854
  catch (loopErr) {
842
855
  // ── Blanket catch: absorb unexpected exceptions, apply graduated recovery ──
@@ -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
  *
@@ -26,6 +28,7 @@ export class AutoSession {
26
28
  active = false;
27
29
  paused = false;
28
30
  completionStopInProgress = false;
31
+ preserveStepSurfaceAfterLoopExit = false;
29
32
  stepMode = false;
30
33
  verbose = false;
31
34
  activeEngineId = null;
@@ -210,6 +213,7 @@ export class AutoSession {
210
213
  this.active = false;
211
214
  this.paused = false;
212
215
  this.completionStopInProgress = false;
216
+ this.preserveStepSurfaceAfterLoopExit = false;
213
217
  this.stepMode = false;
214
218
  this.verbose = false;
215
219
  this.activeEngineId = null;
@@ -66,6 +66,9 @@ export function decideEngineDispatch(input) {
66
66
  export function decideFinalizeResult(input) {
67
67
  if (input.action === "break") {
68
68
  const reason = input.reason ?? "unknown";
69
+ if (reason === "step-wizard") {
70
+ return { action: "complete-and-break" };
71
+ }
69
72
  return {
70
73
  action: "stop",
71
74
  failureClass: reason === "git-closeout-failure" ? "git" : "closeout",
@@ -1108,7 +1108,7 @@ export const DISPATCH_RULES = [
1108
1108
  if (verdict !== "pass") {
1109
1109
  return {
1110
1110
  action: "stop",
1111
- reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or update the verdict manually.`,
1111
+ 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.`,
1112
1112
  level: "warning",
1113
1113
  };
1114
1114
  }
@@ -28,7 +28,7 @@ import { regenerateIfMissing } from "./workflow-projections.js";
28
28
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
29
29
  import { createWorkspace, scopeMilestone } from "./workspace.js";
30
30
  import { normalizeWorktreePathForCompare } from "./worktree-root.js";
31
- import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
31
+ import { isDbAvailable, getDbPath, refreshOpenDatabaseFromDisk, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter, getVerificationEvidence } from "./gsd-db.js";
32
32
  import { renderPlanCheckboxes } from "./markdown-renderer.js";
33
33
  import { consumeSignal } from "./session-status-io.js";
34
34
  import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, resolveHookArtifactPath, } from "./post-unit-hooks.js";
@@ -294,14 +294,14 @@ export function detectRogueFileWrites(unitType, unitId, basePath) {
294
294
  * looping indefinitely (#2007).
295
295
  */
296
296
  export const MAX_ARTIFACT_VERIFICATION_RETRIES = 3;
297
- export const STEP_COMPLETE_FALLBACK_MESSAGE = "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
297
+ export const STEP_COMPLETE_FALLBACK_MESSAGE = "Step complete. Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).";
298
298
  export function buildStepCompleteMessage(nextState) {
299
299
  if (nextState.phase === "complete") {
300
300
  return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
301
301
  }
302
302
  const next = describeNextUnit(nextState);
303
303
  return `Step complete. Next: ${next.label}\n`
304
- + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
304
+ + `Run /clear if you want a clean view, then /gsd next to continue one step (or /gsd auto to run continuously).`;
305
305
  }
306
306
  /**
307
307
  * Decide whether step mode should stop at the step wizard after a unit finishes.
@@ -553,6 +553,13 @@ export async function postUnitPreVerification(pctx, opts) {
553
553
  if (!opts?.skipSettleDelay) {
554
554
  await new Promise(r => setTimeout(r, 100));
555
555
  }
556
+ const dbPath = getDbPath();
557
+ if (isDbAvailable() && dbPath && dbPath !== ":memory:") {
558
+ const refreshed = refreshOpenDatabaseFromDisk();
559
+ if (!refreshed) {
560
+ logWarning("db", "post-unit database refresh failed; derived state may be stale");
561
+ }
562
+ }
556
563
  // Turn-level git action (commit | snapshot | status-only)
557
564
  if (s.currentUnit) {
558
565
  const unit = s.currentUnit;
@@ -1458,8 +1465,8 @@ export async function postUnitPostVerification(pctx) {
1458
1465
  }
1459
1466
  }
1460
1467
  // Step mode → show wizard instead of dispatch.
1461
- // Without this notify(), /gsd in step mode finishes a unit and silently
1462
- // exits the loop, leaving the user with no hint to /clear and /gsd again.
1468
+ // Without this notify(), /gsd next finishes a unit and silently exits the
1469
+ // loop, leaving the user with no next-step command.
1463
1470
  if (s.stepMode) {
1464
1471
  let phaseAfterUnit = null;
1465
1472
  try {
@@ -689,6 +689,8 @@ export async function rerootCommandSession(cmdCtx, workspaceRoot) {
689
689
  }
690
690
  }
691
691
  export async function cleanupAfterLoopExit(ctx) {
692
+ const preserveStepSurface = s.preserveStepSurfaceAfterLoopExit;
693
+ const preservePausedSurface = s.paused;
692
694
  s.currentUnit = null;
693
695
  s.active = false;
694
696
  deactivateGSD();
@@ -712,19 +714,24 @@ export async function cleanupAfterLoopExit(ctx) {
712
714
  // A transient provider-error pause intentionally leaves the paused badge
713
715
  // visible so the user still has a resumable auto-mode signal on screen.
714
716
  if (!s.paused) {
715
- ctx.ui.setStatus("gsd-auto", undefined);
716
- ctx.ui.setWidget("gsd-progress", undefined);
717
- if (s.completionStopInProgress) {
718
- s.completionStopInProgress = false;
717
+ if (preserveStepSurface) {
718
+ s.preserveStepSurfaceAfterLoopExit = false;
719
+ }
720
+ else {
721
+ ctx.ui.setStatus("gsd-auto", undefined);
722
+ ctx.ui.setWidget("gsd-progress", undefined);
723
+ if (s.completionStopInProgress) {
724
+ s.completionStopInProgress = false;
725
+ }
726
+ initHealthWidget(ctx);
719
727
  }
720
- initHealthWidget(ctx);
721
728
  }
722
729
  // ADR-016 phase 3 (#5693): the stop-path basePath restore + chdir routes
723
730
  // through `Lifecycle.restoreToProjectRoot()`, the sole owner of both
724
731
  // `s.basePath` mutation and the paired `process.chdir` for auto-loop
725
732
  // transitions. The verb assigns `s.basePath` before any throwable work, so
726
733
  // a thrown error still leaves basePath restored.
727
- if (s.originalBasePath) {
734
+ if (s.originalBasePath && !preserveStepSurface && !preservePausedSurface) {
728
735
  try {
729
736
  buildLifecycle().restoreToProjectRoot();
730
737
  }
@@ -732,7 +739,7 @@ export async function cleanupAfterLoopExit(ctx) {
732
739
  logWarning("engine", `restore project root failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
733
740
  }
734
741
  }
735
- if (s.originalBasePath && s.cmdCtx) {
742
+ if (s.originalBasePath && s.cmdCtx && !preserveStepSurface && !preservePausedSurface) {
736
743
  const result = await rerootCommandSession(s.cmdCtx, s.originalBasePath);
737
744
  if (result.status === "cancelled") {
738
745
  logWarning("engine", "post-loop session re-root was cancelled", { file: "auto.ts", basePath: s.originalBasePath });
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { loadRegistry } from "../workflow-templates.js";
4
4
  import { gsdHome } from "../gsd-home.js";
5
5
  import { VISUAL_BRIEF_MODES } from "../../visual-brief/prompts.js";
6
- export const GSD_COMMAND_DESCRIPTION = "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";
6
+ export const GSD_COMMAND_DESCRIPTION = "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";
7
7
  export const TOP_LEVEL_SUBCOMMANDS = [
8
8
  { cmd: "help", desc: "Categorized command reference with descriptions" },
9
9
  { cmd: "next", desc: "Explicit step mode (same as /gsd)" },
@@ -21,6 +21,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
21
21
  { cmd: "changelog", desc: "Show categorized release notes" },
22
22
  { cmd: "triage", desc: "Manually trigger triage of pending captures" },
23
23
  { cmd: "dispatch", desc: "Dispatch a specific phase directly" },
24
+ { cmd: "verdict", desc: "Override the recorded milestone validation verdict (pass|needs-attention|needs-remediation)" },
24
25
  { cmd: "history", desc: "View execution history" },
25
26
  { cmd: "undo", desc: "Revert last completed unit" },
26
27
  { cmd: "undo-task", desc: "Reset a specific task's completion state (DB + markdown)" },
@@ -235,6 +236,11 @@ const NESTED_COMPLETIONS = {
235
236
  { cmd: "uat", desc: "Run user acceptance testing" },
236
237
  { cmd: "replan", desc: "Replan the current slice" },
237
238
  ],
239
+ verdict: [
240
+ { cmd: "pass", desc: "Override the milestone validation verdict to pass" },
241
+ { cmd: "needs-attention", desc: "Override the verdict to needs-attention (requires --rationale)" },
242
+ { cmd: "needs-remediation", desc: "Override the verdict to needs-remediation (requires --rationale)" },
243
+ ],
238
244
  rate: [
239
245
  { cmd: "over", desc: "Model was overqualified for this task" },
240
246
  { cmd: "ok", desc: "Model was appropriate for this task" },
@@ -65,6 +65,7 @@ export function showHelp(ctx, args = "") {
65
65
  " /gsd new-project Bootstrap a new project (use --deep for staged project-level discovery)",
66
66
  " /gsd quick Execute a quick task without full planning overhead",
67
67
  " /gsd dispatch Dispatch a specific phase directly [research|plan|execute|complete|uat|replan]",
68
+ " /gsd verdict <v> Override milestone validation verdict [pass|needs-attention|needs-remediation] [--milestone Mxxx] [--rationale \"...\"]",
68
69
  " /gsd parallel Parallel milestone orchestration [start|status|stop|pause|resume|merge|watch]",
69
70
  " /gsd workflow Custom workflow lifecycle [new|run|list|validate|pause|resume]",
70
71
  "",
@@ -183,6 +183,11 @@ Examples:
183
183
  await dispatchDirectPhase(ctx, pi, phase, projectRoot());
184
184
  return true;
185
185
  }
186
+ if (trimmed === "verdict" || trimmed.startsWith("verdict ")) {
187
+ const { handleVerdict } = await import("../../commands-verdict.js");
188
+ await handleVerdict(trimmed.replace(/^verdict\s*/, "").trim(), ctx, projectRoot());
189
+ return true;
190
+ }
186
191
  if (trimmed === "notifications" || trimmed.startsWith("notifications ")) {
187
192
  const { handleNotificationsCommand } = await import("./notifications-handler.js");
188
193
  await handleNotificationsCommand(trimmed.replace(/^notifications\s*/, "").trim(), ctx, pi);
@@ -0,0 +1,139 @@
1
+ import { loadFile } from "./files.js";
2
+ import { resolveMilestoneFile } from "./paths.js";
3
+ import { deriveState } from "./state.js";
4
+ import { executeValidateMilestone } from "./tools/workflow-tool-executors.js";
5
+ import { VALIDATION_VERDICTS, extractVerdict, isValidMilestoneVerdict, } from "./verdict-parser.js";
6
+ const USAGE = 'Usage: /gsd verdict <pass|needs-attention|needs-remediation> [--milestone Mxxx] [--rationale "..."]';
7
+ function tokenize(raw) {
8
+ const tokens = [];
9
+ const re = /"([^"]*)"|(\S+)/g;
10
+ let match;
11
+ while ((match = re.exec(raw)) !== null) {
12
+ tokens.push(match[1] ?? match[2]);
13
+ }
14
+ return tokens;
15
+ }
16
+ function parseArgs(raw) {
17
+ const tokens = tokenize(raw);
18
+ const out = {};
19
+ for (let i = 0; i < tokens.length; i++) {
20
+ const t = tokens[i];
21
+ if (t === "--milestone") {
22
+ const next = tokens[++i];
23
+ if (!next)
24
+ return { error: "--milestone requires a milestone ID" };
25
+ out.milestoneId = next;
26
+ }
27
+ else if (t === "--rationale") {
28
+ const next = tokens[++i];
29
+ if (next == null)
30
+ return { error: "--rationale requires a value" };
31
+ out.rationale = next;
32
+ }
33
+ else if (!out.verdict) {
34
+ if (!isValidMilestoneVerdict(t)) {
35
+ return {
36
+ error: `Invalid verdict "${t}". Must be one of: ${VALIDATION_VERDICTS.join(", ")}`,
37
+ };
38
+ }
39
+ out.verdict = t;
40
+ }
41
+ else {
42
+ return { error: `Unexpected argument: ${t}` };
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ function extractRemediationRound(content) {
48
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
49
+ if (!fm)
50
+ return 0;
51
+ const m = fm[1].match(/^remediation_round:\s*(\d+)/im);
52
+ return m ? Number.parseInt(m[1], 10) : 0;
53
+ }
54
+ function extractSection(content, heading) {
55
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56
+ // Match section bodies bounded by the next "## " heading or end-of-string.
57
+ // Leading "\n" prefix lets a single pattern handle first-line headings too.
58
+ // No /m flag — we want `$` to mean end-of-string, not end-of-line.
59
+ const re = new RegExp(`\\n## ${escaped}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
60
+ const m = ("\n" + content).match(re);
61
+ if (!m)
62
+ return undefined;
63
+ return m[1].replace(/\s+$/, "");
64
+ }
65
+ export function parseValidationFile(content) {
66
+ return {
67
+ verdict: extractVerdict(content),
68
+ remediationRound: extractRemediationRound(content),
69
+ successCriteriaChecklist: extractSection(content, "Success Criteria Checklist") ?? "",
70
+ sliceDeliveryAudit: extractSection(content, "Slice Delivery Audit") ?? "",
71
+ crossSliceIntegration: extractSection(content, "Cross-Slice Integration") ?? "",
72
+ requirementCoverage: extractSection(content, "Requirement Coverage") ?? "",
73
+ verificationClasses: extractSection(content, "Verification Class Compliance"),
74
+ verdictRationale: extractSection(content, "Verdict Rationale") ?? "",
75
+ remediationPlan: extractSection(content, "Remediation Plan"),
76
+ };
77
+ }
78
+ export async function handleVerdict(rawArgs, ctx, basePath) {
79
+ if (!rawArgs.trim()) {
80
+ ctx.ui.notify(USAGE, "warning");
81
+ return;
82
+ }
83
+ const parsed = parseArgs(rawArgs);
84
+ if ("error" in parsed) {
85
+ ctx.ui.notify(`${parsed.error}\n${USAGE}`, "warning");
86
+ return;
87
+ }
88
+ if (!parsed.verdict) {
89
+ ctx.ui.notify(USAGE, "warning");
90
+ return;
91
+ }
92
+ let milestoneId = parsed.milestoneId;
93
+ if (!milestoneId) {
94
+ const state = await deriveState(basePath);
95
+ if (!state.activeMilestone) {
96
+ ctx.ui.notify("No active milestone — pass --milestone Mxxx to target a specific milestone.", "warning");
97
+ return;
98
+ }
99
+ milestoneId = state.activeMilestone.id;
100
+ }
101
+ const validationPath = resolveMilestoneFile(basePath, milestoneId, "VALIDATION");
102
+ if (!validationPath) {
103
+ ctx.ui.notify(`No VALIDATION file found for ${milestoneId}. Run gsd_validate_milestone first to produce one.`, "warning");
104
+ return;
105
+ }
106
+ const existing = await loadFile(validationPath);
107
+ if (!existing) {
108
+ ctx.ui.notify(`Could not read VALIDATION file for ${milestoneId} (${validationPath}).`, "warning");
109
+ return;
110
+ }
111
+ const current = parseValidationFile(existing);
112
+ if (parsed.verdict !== "pass" && !parsed.rationale) {
113
+ ctx.ui.notify(`--rationale is required when overriding to ${parsed.verdict}.`, "warning");
114
+ return;
115
+ }
116
+ const verdictRationale = parsed.rationale ?? "Manually overridden via /gsd verdict";
117
+ const result = await executeValidateMilestone({
118
+ milestoneId,
119
+ verdict: parsed.verdict,
120
+ remediationRound: current.remediationRound,
121
+ successCriteriaChecklist: current.successCriteriaChecklist,
122
+ sliceDeliveryAudit: current.sliceDeliveryAudit,
123
+ crossSliceIntegration: current.crossSliceIntegration,
124
+ requirementCoverage: current.requirementCoverage,
125
+ verificationClasses: current.verificationClasses,
126
+ verdictRationale,
127
+ remediationPlan: current.remediationPlan,
128
+ }, basePath);
129
+ if (result.isError) {
130
+ const msg = result.content[0]?.type === "text" ? result.content[0].text : "Unknown error";
131
+ ctx.ui.notify(msg, "error");
132
+ return;
133
+ }
134
+ const prevVerdict = current.verdict ?? "unknown";
135
+ ctx.ui.notify(`Milestone ${milestoneId} verdict: ${prevVerdict} -> ${parsed.verdict}`, "success");
136
+ if (parsed.verdict === "needs-remediation") {
137
+ ctx.ui.notify("Follow up with gsd_reassess_roadmap to add remediation slices, then re-run /gsd auto.", "info");
138
+ }
139
+ }
@@ -14,7 +14,7 @@ import { isClosedStatus } from "./status-guards.js";
14
14
  import { join, relative } from "node:path";
15
15
  import { createRequire } from "node:module";
16
16
  import { getAllMilestones, getMilestone, getMilestoneSlices, getSliceTasks, getTask, getSlice, getArtifact, insertArtifact, getGateResults, } from "./gsd-db.js";
17
- import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, gsdRoot, buildTaskFileName, buildSliceFileName, } from "./paths.js";
17
+ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, gsdProjectionRoot, gsdRoot, buildTaskFileName, buildSliceFileName, } from "./paths.js";
18
18
  import { saveFile, clearParseCache } from "./files.js";
19
19
  import { invalidateStateCache } from "./state.js";
20
20
  import { clearPathCache } from "./paths.js";
@@ -24,7 +24,11 @@ import { clearPathCache } from "./paths.js";
24
24
  * E.g. "/project/.gsd/milestones/M001/M001-ROADMAP.md" → "milestones/M001/M001-ROADMAP.md"
25
25
  */
26
26
  function toArtifactPath(absPath, basePath) {
27
- const root = gsdRoot(basePath);
27
+ const projectionRoot = gsdProjectionRoot(basePath);
28
+ const projectionRel = relative(projectionRoot, absPath);
29
+ const root = projectionRel && !projectionRel.startsWith("..") && !projectionRel.startsWith("/")
30
+ ? projectionRoot
31
+ : gsdRoot(basePath);
28
32
  const rel = relative(root, absPath);
29
33
  // Normalize to forward slashes for consistent DB keys
30
34
  return rel.replace(/\\/g, "/");
@@ -305,10 +309,9 @@ export async function renderPlanFromDb(basePath, milestoneId, sliceId) {
305
309
  if (tasks.length === 0) {
306
310
  throw new Error(`no tasks found for ${milestoneId}/${sliceId}`);
307
311
  }
308
- const slicePath = resolveSlicePath(basePath, milestoneId, sliceId)
309
- ?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId);
310
- const absPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN")
311
- ?? join(slicePath, `${sliceId}-PLAN.md`);
312
+ const slicePath = join(gsdProjectionRoot(basePath), "milestones", milestoneId, "slices", sliceId);
313
+ mkdirSync(slicePath, { recursive: true });
314
+ const absPath = join(slicePath, `${sliceId}-PLAN.md`);
312
315
  const artifactPath = toArtifactPath(absPath, basePath);
313
316
  const sliceGates = getGateResults(milestoneId, sliceId, "slice");
314
317
  const content = renderSlicePlanMarkdown(slice, tasks, sliceGates);
@@ -329,8 +332,7 @@ export async function renderTaskPlanFromDb(basePath, milestoneId, sliceId, taskI
329
332
  if (!task) {
330
333
  throw new Error(`task ${milestoneId}/${sliceId}/${taskId} not found`);
331
334
  }
332
- const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId)
333
- ?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks");
335
+ const tasksDir = join(gsdProjectionRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks");
334
336
  mkdirSync(tasksDir, { recursive: true });
335
337
  const absPath = join(tasksDir, buildTaskFileName(taskId, "PLAN"));
336
338
  const artifactPath = toArtifactPath(absPath, basePath);
@@ -320,6 +320,10 @@ export function resolveGsdPathContract(workRoot, originalProjectRoot) {
320
320
  isWorktree,
321
321
  };
322
322
  }
323
+ export function gsdProjectionRoot(basePath) {
324
+ const contract = resolveGsdPathContract(basePath);
325
+ return normalizeRealPath(contract.worktreeGsd ?? contract.projectGsd);
326
+ }
323
327
  /**
324
328
  * Invalidate the gsdRoot cache.
325
329
  * Use ONLY at session-reset boundaries: workspace switch, process exit, or
@@ -460,7 +460,7 @@ async function handleAllSlicesDone(basePath, activeMilestone, registry, requirem
460
460
  recentDecisions: [],
461
461
  blockers: [
462
462
  `Milestone ${activeMilestone.id} validation verdict is needs-remediation but all slices are complete. ` +
463
- `Add remediation slices via gsd_reassess_roadmap or override the verdict manually.`,
463
+ `Add remediation slices via gsd_reassess_roadmap, or run \`/gsd verdict pass --rationale "..."\` to override.`,
464
464
  ],
465
465
  nextAction: `Resolve ${activeMilestone.id} remediation before proceeding.`,
466
466
  registry, requirements,
@@ -1132,7 +1132,7 @@ export async function _deriveStateImpl(basePath, opts) {
1132
1132
  recentDecisions: [],
1133
1133
  blockers: [
1134
1134
  `Milestone ${activeMilestone.id} validation verdict is needs-remediation but all slices are complete. ` +
1135
- `Add remediation slices via gsd_reassess_roadmap or override the verdict manually.`,
1135
+ `Add remediation slices via gsd_reassess_roadmap, or run \`/gsd verdict pass --rationale "..."\` to override.`,
1136
1136
  ],
1137
1137
  nextAction: `Resolve ${activeMilestone.id} remediation before proceeding.`,
1138
1138
  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,
@@ -12,7 +12,7 @@ import { appendEvent } from "../workflow-events.js";
12
12
  import { logWarning } from "../workflow-logger.js";
13
13
  import { validatePlanningPathScope } from "../planning-path-scope.js";
14
14
  import { checkFilePathConsistency, checkTaskOrdering } from "../pre-execution-checks.js";
15
- import { buildTaskFileName, gsdRoot, resolveTasksDir } from "../paths.js";
15
+ import { buildTaskFileName, gsdProjectionRoot } from "../paths.js";
16
16
  function validateTasks(value) {
17
17
  if (!Array.isArray(value) || value.length === 0) {
18
18
  throw new Error("tasks must be a non-empty array");
@@ -241,14 +241,12 @@ export async function handlePlanSlice(rawParams, basePath) {
241
241
  return { error: guardError };
242
242
  }
243
243
  try {
244
- const tasksDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
244
+ const tasksDir = join(gsdProjectionRoot(basePath), "milestones", params.milestoneId, "slices", params.sliceId, "tasks");
245
245
  for (const taskId of omittedTaskIds) {
246
- if (!tasksDir)
247
- continue;
248
246
  const taskPlanPath = join(tasksDir, buildTaskFileName(taskId, "PLAN"));
249
247
  if (existsSync(taskPlanPath))
250
248
  rmSync(taskPlanPath, { force: true });
251
- const artifactPath = relative(gsdRoot(basePath), taskPlanPath).replace(/\\/g, "/");
249
+ const artifactPath = relative(gsdProjectionRoot(basePath), taskPlanPath).replace(/\\/g, "/");
252
250
  deleteArtifactByPath(artifactPath);
253
251
  }
254
252
  const renderResult = await renderPlanFromDb(basePath, params.milestoneId, params.sliceId);
@@ -43,6 +43,7 @@ const MAX_BUFFER_BYTES = 512 * 1024;
43
43
  * Prevents CPU spinning when deltas arrive faster than regex evaluation (#468).
44
44
  */
45
45
  const JS_FALLBACK_CHECK_INTERVAL_MS = 50;
46
+ const JS_FALLBACK_THROTTLE_MIN_BUFFER_BYTES = 4 * 1024;
46
47
  const DEFAULT_SCOPE = {
47
48
  allowText: true,
48
49
  allowThinking: false,
@@ -308,7 +309,8 @@ export class TtsrManager {
308
309
  // streams — regex on a growing buffer is O(rules × buffer_size) (#468).
309
310
  const now = Date.now();
310
311
  const lastCheck = this.#lastJsCheckAt.get(bufferKey) ?? 0;
311
- if (now - lastCheck < JS_FALLBACK_CHECK_INTERVAL_MS) {
312
+ if (nextBuffer.length >= JS_FALLBACK_THROTTLE_MIN_BUFFER_BYTES &&
313
+ now - lastCheck < JS_FALLBACK_CHECK_INTERVAL_MS) {
312
314
  stopTimer({ bufferSize: nextBuffer.length, throttled: true });
313
315
  return [];
314
316
  }