gsd-pi 2.71.0-dev.e17e0ce → 2.72.0-dev.de4c4b3

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 (159) hide show
  1. package/README.md +34 -1
  2. package/dist/cli.js +17 -0
  3. package/dist/mcp-server.js +37 -14
  4. package/dist/resources/agents/debugger.md +58 -0
  5. package/dist/resources/agents/doc-writer.md +43 -0
  6. package/dist/resources/agents/git-ops.md +56 -0
  7. package/dist/resources/agents/javascript-pro.md +46 -271
  8. package/dist/resources/agents/planner.md +55 -0
  9. package/dist/resources/agents/refactorer.md +47 -0
  10. package/dist/resources/agents/reviewer.md +48 -0
  11. package/dist/resources/agents/security.md +59 -0
  12. package/dist/resources/agents/tester.md +50 -0
  13. package/dist/resources/agents/typescript-pro.md +41 -235
  14. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +103 -6
  15. package/dist/resources/extensions/gsd/auto/phases.js +4 -0
  16. package/dist/resources/extensions/gsd/auto-prompts.js +88 -33
  17. package/dist/resources/extensions/gsd/auto-start.js +24 -4
  18. package/dist/resources/extensions/gsd/auto.js +4 -0
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  20. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +2 -5
  21. package/dist/resources/extensions/gsd/doctor-providers.js +23 -0
  22. package/dist/resources/extensions/gsd/error-classifier.js +4 -1
  23. package/dist/resources/extensions/gsd/gate-registry.js +208 -0
  24. package/dist/resources/extensions/gsd/gsd-db.js +41 -0
  25. package/dist/resources/extensions/gsd/milestone-validation-gates.js +11 -12
  26. package/dist/resources/extensions/gsd/notification-overlay.js +26 -12
  27. package/dist/resources/extensions/gsd/notification-store.js +5 -4
  28. package/dist/resources/extensions/gsd/prompt-validation.js +126 -0
  29. package/dist/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  30. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  31. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  32. package/dist/resources/extensions/gsd/shortcut-defs.js +7 -1
  33. package/dist/resources/extensions/gsd/state.js +9 -2
  34. package/dist/resources/extensions/gsd/tools/complete-slice.js +52 -1
  35. package/dist/resources/extensions/gsd/tools/complete-task.js +51 -1
  36. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +4 -1
  37. package/dist/resources/extensions/ollama/index.js +13 -5
  38. package/dist/resources/extensions/shared/gsd-phase-state.js +35 -0
  39. package/dist/resources/extensions/subagent/agents.js +8 -0
  40. package/dist/resources/extensions/subagent/index.js +17 -0
  41. package/dist/startup-model-validation.d.ts +0 -1
  42. package/dist/startup-model-validation.js +6 -2
  43. package/dist/web/standalone/.next/BUILD_ID +1 -1
  44. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  45. package/dist/web/standalone/.next/build-manifest.json +2 -2
  46. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  71. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  73. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  74. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  75. package/package.json +1 -1
  76. package/packages/mcp-server/dist/server.d.ts +12 -1
  77. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  78. package/packages/mcp-server/dist/server.js +90 -42
  79. package/packages/mcp-server/dist/server.js.map +1 -1
  80. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  81. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  82. package/packages/mcp-server/src/server.ts +110 -38
  83. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  84. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts +8 -0
  85. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/model-resolver.test.js +75 -0
  87. package/packages/pi-coding-agent/dist/core/model-resolver.test.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +5 -0
  89. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/retry-handler.js +55 -1
  91. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +57 -0
  93. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +6 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -1
  100. package/packages/pi-coding-agent/package.json +1 -1
  101. package/packages/pi-coding-agent/src/core/model-resolver.test.ts +85 -0
  102. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +83 -0
  103. package/packages/pi-coding-agent/src/core/retry-handler.ts +60 -1
  104. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +15 -6
  105. package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +6 -1
  106. package/pkg/package.json +1 -1
  107. package/src/resources/agents/debugger.md +58 -0
  108. package/src/resources/agents/doc-writer.md +43 -0
  109. package/src/resources/agents/git-ops.md +56 -0
  110. package/src/resources/agents/javascript-pro.md +46 -271
  111. package/src/resources/agents/planner.md +55 -0
  112. package/src/resources/agents/refactorer.md +47 -0
  113. package/src/resources/agents/reviewer.md +48 -0
  114. package/src/resources/agents/security.md +59 -0
  115. package/src/resources/agents/tester.md +50 -0
  116. package/src/resources/agents/typescript-pro.md +41 -235
  117. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +109 -3
  118. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +133 -2
  119. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  120. package/src/resources/extensions/gsd/auto-prompts.ts +111 -33
  121. package/src/resources/extensions/gsd/auto-start.ts +31 -4
  122. package/src/resources/extensions/gsd/auto.ts +4 -0
  123. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  124. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +2 -5
  125. package/src/resources/extensions/gsd/doctor-providers.ts +24 -0
  126. package/src/resources/extensions/gsd/error-classifier.ts +4 -1
  127. package/src/resources/extensions/gsd/gate-registry.ts +251 -0
  128. package/src/resources/extensions/gsd/gsd-db.ts +51 -0
  129. package/src/resources/extensions/gsd/milestone-validation-gates.ts +11 -13
  130. package/src/resources/extensions/gsd/notification-overlay.ts +27 -11
  131. package/src/resources/extensions/gsd/notification-store.ts +5 -4
  132. package/src/resources/extensions/gsd/prompt-validation.ts +157 -0
  133. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  134. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  135. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  136. package/src/resources/extensions/gsd/shortcut-defs.ts +8 -1
  137. package/src/resources/extensions/gsd/state.ts +13 -2
  138. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +14 -0
  139. package/src/resources/extensions/gsd/tests/complete-slice-gate-closure.test.ts +167 -0
  140. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +36 -0
  141. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +16 -0
  142. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +27 -0
  143. package/src/resources/extensions/gsd/tests/gate-registry.test.ts +140 -0
  144. package/src/resources/extensions/gsd/tests/prompt-system-gate-coverage.test.ts +208 -0
  145. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +9 -0
  146. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +3 -2
  147. package/src/resources/extensions/gsd/tools/complete-slice.ts +63 -0
  148. package/src/resources/extensions/gsd/tools/complete-task.ts +63 -0
  149. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +4 -1
  150. package/src/resources/extensions/gsd/types.ts +26 -0
  151. package/src/resources/extensions/ollama/index.ts +13 -3
  152. package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +28 -0
  153. package/src/resources/extensions/shared/gsd-phase-state.ts +42 -0
  154. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +48 -0
  155. package/src/resources/extensions/subagent/agents.ts +10 -0
  156. package/src/resources/extensions/subagent/index.ts +18 -0
  157. package/src/resources/extensions/subagent/tests/agents-conflicts.test.ts +33 -0
  158. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → f-Gremw0nLxxFUySaHRPw}/_buildManifest.js +0 -0
  159. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → f-Gremw0nLxxFUySaHRPw}/_ssgManifest.js +0 -0
@@ -10,6 +10,7 @@ import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
10
10
  import { dirname } from "node:path";
11
11
  import type { Decision, Requirement, GateRow, GateId, GateScope, GateStatus, GateVerdict } from "./types.js";
12
12
  import { GSDError, GSD_STALE_STATE } from "./errors.js";
13
+ import { getGateIdsForTurn, type OwnerTurn } from "./gate-registry.js";
13
14
  import { logError, logWarning } from "./workflow-logger.js";
14
15
 
15
16
  const _require = createRequire(import.meta.url);
@@ -2302,3 +2303,53 @@ export function getPendingSliceGateCount(milestoneId: string, sliceId: string):
2302
2303
  ).get({ ":mid": milestoneId, ":sid": sliceId });
2303
2304
  return row ? (row["cnt"] as number) : 0;
2304
2305
  }
2306
+
2307
+ /**
2308
+ * Return pending gate rows owned by a specific workflow turn.
2309
+ *
2310
+ * Unlike `getPendingGates(..., scope)`, this filters by the registry's
2311
+ * `ownerTurn` metadata so callers can distinguish Q3/Q4 (owned by
2312
+ * gate-evaluate) from Q8 (owned by complete-slice) even though both are
2313
+ * scope:"slice". Pass `taskId` to narrow task-scoped results to one task.
2314
+ */
2315
+ export function getPendingGatesForTurn(
2316
+ milestoneId: string,
2317
+ sliceId: string,
2318
+ turn: OwnerTurn,
2319
+ taskId?: string,
2320
+ ): GateRow[] {
2321
+ if (!currentDb) return [];
2322
+ const ids = getGateIdsForTurn(turn);
2323
+ if (ids.size === 0) return [];
2324
+ const idList = [...ids];
2325
+ const placeholders = idList.map((_, i) => `:gid${i}`).join(",");
2326
+ const params: Record<string, unknown> = {
2327
+ ":mid": milestoneId,
2328
+ ":sid": sliceId,
2329
+ };
2330
+ idList.forEach((id, i) => {
2331
+ params[`:gid${i}`] = id;
2332
+ });
2333
+ let sql =
2334
+ `SELECT * FROM quality_gates
2335
+ WHERE milestone_id = :mid AND slice_id = :sid
2336
+ AND status = 'pending'
2337
+ AND gate_id IN (${placeholders})`;
2338
+ if (taskId !== undefined) {
2339
+ sql += ` AND task_id = :tid`;
2340
+ params[":tid"] = taskId;
2341
+ }
2342
+ return currentDb.prepare(sql).all(params).map(rowToGate);
2343
+ }
2344
+
2345
+ /**
2346
+ * Count pending gates for a turn. Convenience wrapper used by state
2347
+ * derivation to decide whether a phase transition should pause.
2348
+ */
2349
+ export function getPendingGateCountForTurn(
2350
+ milestoneId: string,
2351
+ sliceId: string,
2352
+ turn: OwnerTurn,
2353
+ ): number {
2354
+ return getPendingGatesForTurn(milestoneId, sliceId, turn).length;
2355
+ }
@@ -6,19 +6,13 @@
6
6
  * records in the DB. This module inserts milestone-level validation gates
7
7
  * that correspond to the validation checks performed.
8
8
  *
9
- * Gate IDs for milestone validation:
10
- * MV01 Success criteria checklist
11
- * MV02 Slice delivery audit
12
- * MV03 — Cross-slice integration
13
- * MV04 — Requirement coverage
14
- *
15
- * These use the existing quality_gates table with scope "milestone".
9
+ * Gate IDs for milestone validation (MV01–MV04) are sourced from the
10
+ * gate registry so the definitions stay in lockstep with prompt builders,
11
+ * dispatch rules, and state derivation. See gate-registry.ts.
16
12
  */
17
13
 
18
14
  import { _getAdapter } from "./gsd-db.js";
19
-
20
- /** Milestone validation gate IDs. */
21
- const MILESTONE_GATE_IDS = ["MV01", "MV02", "MV03", "MV04"] as const;
15
+ import { getGatesForTurn } from "./gate-registry.js";
22
16
 
23
17
  /**
24
18
  * Insert milestone-level quality_gates records for a validation run.
@@ -27,6 +21,9 @@ const MILESTONE_GATE_IDS = ["MV01", "MV02", "MV03", "MV04"] as const;
27
21
  * from the overall milestone validation verdict. Individual gate-level
28
22
  * verdicts are not available (the handler receives a single verdict),
29
23
  * so all gates share the overall verdict.
24
+ *
25
+ * Gate IDs come from the registry — adding/removing an MV-scoped gate
26
+ * in gate-registry.ts automatically flows through here.
30
27
  */
31
28
  export function insertMilestoneValidationGates(
32
29
  milestoneId: string,
@@ -38,8 +35,9 @@ export function insertMilestoneValidationGates(
38
35
  if (!db) return;
39
36
 
40
37
  const gateVerdict = verdict === "pass" ? "pass" : "flag";
38
+ const milestoneGates = getGatesForTurn("validate-milestone");
41
39
 
42
- for (const gateId of MILESTONE_GATE_IDS) {
40
+ for (const def of milestoneGates) {
43
41
  db.prepare(
44
42
  `INSERT OR REPLACE INTO quality_gates
45
43
  (milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
@@ -47,9 +45,9 @@ export function insertMilestoneValidationGates(
47
45
  ).run({
48
46
  ":mid": milestoneId,
49
47
  ":sid": sliceId,
50
- ":gid": gateId,
48
+ ":gid": def.id,
51
49
  ":verdict": gateVerdict,
52
- ":rationale": `Milestone validation verdict: ${verdict}`,
50
+ ":rationale": `${def.promptSection} — milestone validation verdict: ${verdict}`,
53
51
  ":evaluated_at": evaluatedAt,
54
52
  });
55
53
  }
@@ -9,6 +9,7 @@ import {
9
9
  readNotifications,
10
10
  markAllRead,
11
11
  clearNotifications,
12
+ onNotificationStoreChange,
12
13
  type NotificationEntry,
13
14
  type NotifySeverity,
14
15
  } from "./notification-store.js";
@@ -82,6 +83,7 @@ export class GSDNotificationOverlay {
82
83
  private refreshTimer: ReturnType<typeof setInterval>;
83
84
  private disposed = false;
84
85
  private resizeHandler: (() => void) | null = null;
86
+ private unsubscribeStore: (() => void) | null = null;
85
87
 
86
88
  constructor(
87
89
  tui: { requestRender: () => void },
@@ -105,19 +107,17 @@ export class GSDNotificationOverlay {
105
107
  };
106
108
  process.stdout.on("resize", this.resizeHandler);
107
109
 
108
- // Refresh every 3s for new notifications
110
+ // Subscribe to store mutations for immediate updates
111
+ this.unsubscribeStore = onNotificationStoreChange(() => {
112
+ if (this.disposed) return;
113
+ this._refreshFromDisk();
114
+ });
115
+
116
+ // 30s safety-net for cross-process edits (web subprocess, parallel workers)
109
117
  this.refreshTimer = setInterval(() => {
110
118
  if (this.disposed) return;
111
- const fresh = readNotifications();
112
- const signature = notificationSignature(fresh);
113
- if (signature !== this.entriesSignature) {
114
- markAllRead();
115
- this.entries = readNotifications();
116
- this.entriesSignature = notificationSignature(this.entries);
117
- this.invalidate();
118
- this.tui.requestRender();
119
- }
120
- }, 3000);
119
+ this._refreshFromDisk();
120
+ }, 30_000);
121
121
  }
122
122
 
123
123
  private get filter(): FilterMode {
@@ -215,12 +215,28 @@ export class GSDNotificationOverlay {
215
215
  dispose(): void {
216
216
  this.disposed = true;
217
217
  clearInterval(this.refreshTimer);
218
+ if (this.unsubscribeStore) {
219
+ this.unsubscribeStore();
220
+ this.unsubscribeStore = null;
221
+ }
218
222
  if (this.resizeHandler) {
219
223
  process.stdout.removeListener("resize", this.resizeHandler);
220
224
  this.resizeHandler = null;
221
225
  }
222
226
  }
223
227
 
228
+ private _refreshFromDisk(): void {
229
+ const fresh = readNotifications();
230
+ const signature = notificationSignature(fresh);
231
+ if (signature !== this.entriesSignature) {
232
+ markAllRead();
233
+ this.entries = readNotifications();
234
+ this.entriesSignature = notificationSignature(this.entries);
235
+ this.invalidate();
236
+ this.tui.requestRender();
237
+ }
238
+ }
239
+
224
240
  private wrapInBox(inner: string[], width: number): string[] {
225
241
  const th = this.theme;
226
242
  const border = (s: string) => th.fg("borderAccent", s);
@@ -323,10 +323,11 @@ function _withLock<T>(basePath: string, fn: () => T): T {
323
323
  }
324
324
  }
325
325
 
326
- // Only run the mutation if we actually own the lock
327
- const ownsLock = fd !== null;
326
+ // Best-effort: mutation runs regardless of lock status (idempotent overwrites).
327
+ // createdLock gates cleanup only — never skip fn() on lock failure.
328
+ const createdLock = fd !== null;
328
329
  try {
329
- if (ownsLock && fd !== null) {
330
+ if (createdLock && fd !== null) {
330
331
  // Write our PID timestamp into the lock for stale detection
331
332
  writeFileSync(lockPath, String(Date.now()), "utf-8");
332
333
  closeSync(fd);
@@ -334,7 +335,7 @@ function _withLock<T>(basePath: string, fn: () => T): T {
334
335
  return fn();
335
336
  } finally {
336
337
  // Only delete the lock if we created it — never remove another process's lock
337
- if (ownsLock) {
338
+ if (createdLock) {
338
339
  try { unlinkSync(lockPath); } catch { /* best-effort cleanup */ }
339
340
  }
340
341
  }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * GSD Prompt Validation — Validates enhanced context and turn output
3
+ * artifacts before writing.
4
+ *
5
+ * Implements R109 validation requirement: CONTEXT.md must have required
6
+ * sections before being written to disk. Additionally, per-turn validators
7
+ * check that artifacts produced by gate-owning turns contain the gate
8
+ * sections declared in gate-registry.ts, so a malformed summary/validation
9
+ * markdown file cannot silently drop a quality gate.
10
+ */
11
+
12
+ import { getGatesForTurn, type OwnerTurn } from "./gate-registry.js";
13
+
14
+ /**
15
+ * Result of validating enhanced context output.
16
+ */
17
+ export interface ValidationResult {
18
+ /** Whether all required sections are present. */
19
+ valid: boolean;
20
+ /** List of missing required sections. */
21
+ missing: string[];
22
+ }
23
+
24
+ /**
25
+ * Validate that enhanced context content has all required sections.
26
+ *
27
+ * Required sections per R109:
28
+ * - Scope section (## Scope, ## Milestone Scope, or ## Why This Milestone)
29
+ * - Architectural Decisions section (## Architectural Decisions)
30
+ * - Acceptance Criteria section (## Acceptance Criteria or ## Final Integrated Acceptance)
31
+ *
32
+ * Additionally validates that the Architectural Decisions section contains
33
+ * at least one decision entry (### heading or **Decision marker).
34
+ *
35
+ * @param content - The enhanced context markdown content
36
+ * @returns ValidationResult with valid flag and list of missing sections
37
+ */
38
+ export function validateEnhancedContext(content: string): ValidationResult {
39
+ const missing: string[] = [];
40
+
41
+ // Required section 1: Scope (multiple acceptable header variants)
42
+ const hasScopeSection =
43
+ /^## Scope\b/m.test(content) ||
44
+ /^## Milestone Scope\b/m.test(content) ||
45
+ /^## Why This Milestone\b/m.test(content);
46
+
47
+ if (!hasScopeSection) {
48
+ missing.push("Milestone Scope or Why This Milestone");
49
+ }
50
+
51
+ // Required section 2: Architectural Decisions
52
+ const hasArchitecturalDecisions = /^## Architectural Decisions\b/m.test(content);
53
+ if (!hasArchitecturalDecisions) {
54
+ missing.push("Architectural Decisions");
55
+ }
56
+
57
+ // Required section 3: Acceptance Criteria (multiple acceptable header variants)
58
+ const hasAcceptanceCriteria =
59
+ /^## Acceptance Criteria\b/m.test(content) ||
60
+ /^## Final Integrated Acceptance\b/m.test(content);
61
+
62
+ if (!hasAcceptanceCriteria) {
63
+ missing.push("Acceptance Criteria");
64
+ }
65
+
66
+ // Additional validation: Architectural Decisions must have at least one entry
67
+ if (hasArchitecturalDecisions) {
68
+ // Extract the section content between ## Architectural Decisions and the next ## heading.
69
+ // Uses indexOf-based extraction instead of regex with \z (which is invalid in JavaScript
70
+ // regex — it's PCRE/Ruby syntax and JS treats it as literal 'z').
71
+ const sectionStart = content.indexOf("## Architectural Decisions");
72
+ if (sectionStart === -1) {
73
+ missing.push("Architectural Decisions");
74
+ } else {
75
+ const afterHeading = content.slice(sectionStart + "## Architectural Decisions".length);
76
+ const nextSection = afterHeading.search(/^## /m);
77
+ const sectionContent = nextSection === -1 ? afterHeading : afterHeading.slice(0, nextSection);
78
+
79
+ // Check for actual decision entries:
80
+ // - ### heading (subsection per decision)
81
+ // - **Decision marker (inline decision format)
82
+ const hasDecisionEntry = /^### /m.test(sectionContent) || /^\*\*Decision/m.test(sectionContent);
83
+
84
+ if (!hasDecisionEntry) {
85
+ missing.push("At least one architectural decision entry");
86
+ }
87
+ }
88
+ }
89
+
90
+ return {
91
+ valid: missing.length === 0,
92
+ missing,
93
+ };
94
+ }
95
+
96
+ // ─── Per-Turn Gate Section Validators ─────────────────────────────────────
97
+ //
98
+ // Each validator checks that the artifact written by a turn contains a
99
+ // heading for every gate owned by that turn. The registry is the source
100
+ // of truth for which sections must exist; adding a new gate automatically
101
+ // flows through via `getGatesForTurn(turn)`.
102
+
103
+ /**
104
+ * Escape a string so it can be embedded safely inside a regular expression.
105
+ */
106
+ function escapeRegExp(value: string): string {
107
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
+ }
109
+
110
+ /**
111
+ * Validate that an artifact contains an `## H2` heading for every gate the
112
+ * named turn owns. Returns the list of missing gate section headers.
113
+ *
114
+ * Soft rule: a section counts as "present" if it is declared (H2 heading
115
+ * exists) — empty-body sections are allowed and handled by the tool
116
+ * handler, which will record such gates as `omitted`.
117
+ */
118
+ export function validateGateSections(
119
+ content: string,
120
+ turn: OwnerTurn,
121
+ ): ValidationResult {
122
+ const missing: string[] = [];
123
+ for (const def of getGatesForTurn(turn)) {
124
+ const pattern = new RegExp(`^##\\s+${escapeRegExp(def.promptSection)}\\b`, "m");
125
+ if (!pattern.test(content)) {
126
+ missing.push(`${def.id} (## ${def.promptSection})`);
127
+ }
128
+ }
129
+ return { valid: missing.length === 0, missing };
130
+ }
131
+
132
+ /**
133
+ * Validate a SUMMARY.md produced by the complete-slice turn. Requires
134
+ * an H2 heading for every gate owned by complete-slice (e.g. Q8 →
135
+ * "## Operational Readiness"). Intended for use in the tool handler's
136
+ * pre-write checks or in the post-unit validation sweep.
137
+ */
138
+ export function validateSliceSummaryOutput(content: string): ValidationResult {
139
+ return validateGateSections(content, "complete-slice");
140
+ }
141
+
142
+ /**
143
+ * Validate a task SUMMARY.md produced by the execute-task turn. Only
144
+ * flags gates that are still pending for the task; skips the check
145
+ * when no rows are seeded (simple task).
146
+ */
147
+ export function validateTaskSummaryOutput(content: string): ValidationResult {
148
+ return validateGateSections(content, "execute-task");
149
+ }
150
+
151
+ /**
152
+ * Validate a VALIDATION.md produced by the validate-milestone turn.
153
+ * Requires an H2 heading for every MV gate declared in the registry.
154
+ */
155
+ export function validateMilestoneValidationOutput(content: string): ValidationResult {
156
+ return validateGateSections(content, "validate-milestone");
157
+ }
@@ -16,6 +16,8 @@ All relevant context has been preloaded below — the slice plan, all task summa
16
16
 
17
17
  {{inlinedContext}}
18
18
 
19
+ {{gatesToClose}}
20
+
19
21
  **Match effort to complexity.** A simple slice with 1-2 tasks needs a brief summary and lightweight verification. A complex slice with 5 tasks across multiple subsystems needs thorough verification and a detailed summary. Scale the work below accordingly.
20
22
 
21
23
  Then:
@@ -23,7 +25,7 @@ Then:
23
25
  2. {{skillActivation}}
24
26
  3. Run all slice-level verification checks defined in the slice plan. All must pass before marking the slice done. If any fail, fix them first. Task artifacts use a **flat file layout** directly inside `tasks/` (for example `T01-SUMMARY.md`, `T02-SUMMARY.md`) rather than per-task subdirectories. If you need to count or re-read task summaries during verification, use `find .gsd/milestones/{{milestoneId}}/slices/{{sliceId}}/tasks -name "*-SUMMARY.md"` or `ls .gsd/milestones/{{milestoneId}}/slices/{{sliceId}}/tasks/*-SUMMARY.md`. Never use `tasks/*/SUMMARY.md` — that glob expects subdirectories that do not exist.
25
27
  4. If the slice plan includes observability/diagnostic surfaces, confirm they work. Skip this for simple slices that don't have observability sections.
26
- 5. If the slice involved runtime behavior, fill the **Operational Readiness** section (Q8) in the slice summary: health signal, failure signal, recovery procedure, and monitoring gaps. Omit entirely for simple slices with no runtime concerns.
28
+ 5. Address every gate listed in the **Gates to Close** section above — each gate maps to a specific slice-summary section the handler inspects (for example, Q8 maps to **Operational Readiness**: health signal, failure signal, recovery procedure, and monitoring gaps). Leaving a section empty records the gate as `omitted`.
27
29
  6. If this slice produced evidence that a requirement changed status (Active → Validated, Active → Deferred, etc.), call `gsd_requirement_update` with the requirement ID, updated `status`, and `validation` evidence. Do NOT write `.gsd/REQUIREMENTS.md` directly — the engine renders it from the database.
28
30
  7. Prepare the slice completion content you will pass to `gsd_complete_slice` using the camelCase fields `milestoneId`, `sliceId`, `sliceTitle`, `oneLiner`, `narrative`, `verification`, and `uatContent`. Do **not** manually write `{{sliceSummaryPath}}`. Do **not** manually write `{{sliceUatPath}}` — the DB-backed tool is the canonical write path for both artifacts.
29
31
  8. Draft the UAT content you will pass as `uatContent` — a concrete UAT script with real test cases derived from the slice plan and task summaries. Include preconditions, numbered steps with expected outcomes, and edge cases. This must NOT be a placeholder or generic template — tailor every test case to what this slice actually built.
@@ -22,6 +22,8 @@ A researcher explored the codebase and a planner decomposed the work — you are
22
22
 
23
23
  {{slicePlanExcerpt}}
24
24
 
25
+ {{gatesToClose}}
26
+
25
27
  ## Backing Source Artifacts
26
28
  - Slice plan: `{{planPath}}`
27
29
  - Task plan source: `{{taskPlanPath}}`
@@ -18,6 +18,8 @@ All relevant context has been preloaded below — the roadmap, all slice summari
18
18
 
19
19
  {{inlinedContext}}
20
20
 
21
+ {{gatesToEvaluate}}
22
+
21
23
  ## Execution Protocol
22
24
 
23
25
  ### Step 1 — Dispatch Parallel Reviewers
@@ -8,6 +8,8 @@ type GSDShortcutDef = {
8
8
  key: "g" | "n" | "p";
9
9
  action: string;
10
10
  command: string;
11
+ /** Whether the Ctrl+Shift fallback is registered (false when it conflicts with an app keybinding). */
12
+ hasFallback: boolean;
11
13
  };
12
14
 
13
15
  export const GSD_SHORTCUTS: Record<GSDShortcutId, GSDShortcutDef> = {
@@ -15,16 +17,19 @@ export const GSD_SHORTCUTS: Record<GSDShortcutId, GSDShortcutDef> = {
15
17
  key: "g",
16
18
  action: "Open GSD dashboard",
17
19
  command: "/gsd status",
20
+ hasFallback: true,
18
21
  },
19
22
  notifications: {
20
23
  key: "n",
21
24
  action: "Open notification history",
22
25
  command: "/gsd notifications",
26
+ hasFallback: true,
23
27
  },
24
28
  parallel: {
25
29
  key: "p",
26
30
  action: "Open parallel worker monitor",
27
31
  command: "/gsd parallel watch",
32
+ hasFallback: false, // Ctrl+Shift+P conflicts with cycleModelBackward
28
33
  },
29
34
  };
30
35
 
@@ -41,7 +46,9 @@ export function fallbackShortcutCombo(id: GSDShortcutId): string {
41
46
  }
42
47
 
43
48
  export function shortcutPair(id: GSDShortcutId, formatter: (combo: string) => string = (combo) => combo): string {
44
- return `${formatter(primaryShortcutCombo(id))} / ${formatter(fallbackShortcutCombo(id))}`;
49
+ const primary = formatter(primaryShortcutCombo(id));
50
+ if (!GSD_SHORTCUTS[id].hasFallback) return primary;
51
+ return `${primary} / ${formatter(fallbackShortcutCombo(id))}`;
45
52
  }
46
53
 
47
54
  export function formattedShortcutPair(id: GSDShortcutId): string {
@@ -58,7 +58,7 @@ import {
58
58
  insertSlice,
59
59
  insertTask,
60
60
  updateTaskStatus,
61
- getPendingSliceGateCount,
61
+ getPendingGateCountForTurn,
62
62
  type MilestoneRow,
63
63
  type SliceRow,
64
64
  type TaskRow,
@@ -864,7 +864,18 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
864
864
  }
865
865
  }
866
866
 
867
- const pendingGateCount = getPendingSliceGateCount(activeMilestone.id, activeSlice.id);
867
+ // ── Quality gate evaluation check ──────────────────────────────────
868
+ // Pause before execution only when gates owned by the `gate-evaluate`
869
+ // turn (Q3/Q4) are still pending. Q8 is also `scope:"slice"` but is
870
+ // owned by `complete-slice`, so it must NOT block the evaluating-gates
871
+ // phase — otherwise auto-loop stalls forever waiting for a gate that
872
+ // this turn never evaluates. See gate-registry.ts for the ownership map.
873
+ // Slices with zero gate rows (pre-feature or simple) skip straight through.
874
+ const pendingGateCount = getPendingGateCountForTurn(
875
+ activeMilestone.id,
876
+ activeSlice.id,
877
+ "gate-evaluate",
878
+ );
868
879
  if (pendingGateCount > 0) {
869
880
  return {
870
881
  activeMilestone, activeSlice, activeTask: null,
@@ -48,3 +48,17 @@ test("bootstrapAutoSession checks manual session override before preferences", (
48
48
  "manual override and preference fallback must be resolved before building startModelSnapshot",
49
49
  );
50
50
  });
51
+
52
+ test("bootstrapAutoSession validates preferred model against live registry auth (#unconfigured-models)", () => {
53
+ // The raw PREFERENCES.md value must be validated against getAvailable()
54
+ // before being captured as the snapshot, so an unconfigured provider
55
+ // (no API key / OAuth) can't become autoModeStartModel.
56
+ const validationIdx = source.indexOf("ctx.modelRegistry.getAvailable()");
57
+ assert.ok(validationIdx > -1, "auto-start.ts should validate preferred model against getAvailable()");
58
+
59
+ const resolveModelIdIdx = source.indexOf("resolveModelId");
60
+ assert.ok(resolveModelIdIdx > -1, "auto-start.ts should resolve preferred model against the registry");
61
+
62
+ const warningIdx = source.indexOf("is not configured; falling back to session default");
63
+ assert.ok(warningIdx > -1, "auto-start.ts should warn when preferred model is unconfigured");
64
+ });
@@ -0,0 +1,167 @@
1
+ /**
2
+ * complete-slice gate closure integration test.
3
+ *
4
+ * Pins the fix for the Q8-stall bug: complete-slice must close every gate
5
+ * owned by the complete-slice turn based on the content of the matching
6
+ * CompleteSliceParams field. Without this, Q8 stays pending forever and
7
+ * blocks state derivation on subsequent loops.
8
+ */
9
+
10
+ import { describe, test, beforeEach, afterEach } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import * as os from "node:os";
15
+
16
+ import {
17
+ openDatabase,
18
+ closeDatabase,
19
+ insertMilestone,
20
+ insertSlice,
21
+ insertTask,
22
+ insertGateRow,
23
+ getGateResults,
24
+ } from "../gsd-db.ts";
25
+ import { handleCompleteSlice } from "../tools/complete-slice.ts";
26
+ import type { CompleteSliceParams } from "../types.ts";
27
+
28
+ function makeValidSliceParams(overrides: Partial<CompleteSliceParams> = {}): CompleteSliceParams {
29
+ return {
30
+ sliceId: "S01",
31
+ milestoneId: "M001",
32
+ sliceTitle: "Test Slice",
33
+ oneLiner: "Implemented test slice",
34
+ narrative: "Built and tested.",
35
+ verification: "All tests pass.",
36
+ deviations: "None.",
37
+ knownLimitations: "None.",
38
+ followUps: "None.",
39
+ keyFiles: ["src/foo.ts"],
40
+ keyDecisions: [],
41
+ patternsEstablished: [],
42
+ observabilitySurfaces: [],
43
+ provides: [],
44
+ requirementsSurfaced: [],
45
+ drillDownPaths: [],
46
+ affects: [],
47
+ requirementsAdvanced: [],
48
+ requirementsValidated: [],
49
+ requirementsInvalidated: [],
50
+ filesModified: [],
51
+ requires: [],
52
+ uatContent: "## Smoke Test\n\nVerify happy path.",
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ describe("complete-slice closes complete-slice-owned gates", () => {
58
+ let dbPath: string;
59
+ let basePath: string;
60
+
61
+ beforeEach(() => {
62
+ dbPath = path.join(
63
+ fs.mkdtempSync(path.join(os.tmpdir(), "gsd-slice-gate-")),
64
+ "test.db",
65
+ );
66
+ openDatabase(dbPath);
67
+
68
+ basePath = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-slice-gate-handler-"));
69
+ const sliceDir = path.join(
70
+ basePath, ".gsd", "milestones", "M001", "slices", "S01", "tasks",
71
+ );
72
+ fs.mkdirSync(sliceDir, { recursive: true });
73
+ fs.writeFileSync(
74
+ path.join(basePath, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
75
+ [
76
+ "# M001: Test Milestone",
77
+ "",
78
+ "## Slices",
79
+ "",
80
+ '- [ ] **S01: Test Slice** `risk:medium` `depends:[]`',
81
+ " - After this: basic functionality works",
82
+ ].join("\n"),
83
+ );
84
+
85
+ insertMilestone({ id: "M001" });
86
+ insertSlice({ id: "S01", milestoneId: "M001" });
87
+ insertTask({
88
+ id: "T01", sliceId: "S01", milestoneId: "M001",
89
+ status: "complete", title: "Task 1",
90
+ });
91
+
92
+ // Seed Q8 as pending — this is what plan-slice does today.
93
+ insertGateRow({
94
+ milestoneId: "M001", sliceId: "S01",
95
+ gateId: "Q8", scope: "slice",
96
+ });
97
+ });
98
+
99
+ afterEach(() => {
100
+ closeDatabase();
101
+ fs.rmSync(path.dirname(dbPath), { recursive: true, force: true });
102
+ fs.rmSync(basePath, { recursive: true, force: true });
103
+ });
104
+
105
+ test("Q8 closes as 'pass' when operationalReadiness is populated", async () => {
106
+ const params = makeValidSliceParams({
107
+ operationalReadiness: [
108
+ "- Health signal: /health endpoint returns 200",
109
+ "- Failure signal: error rate alert in observability dashboard",
110
+ "- Recovery: systemd auto-restart",
111
+ ].join("\n"),
112
+ });
113
+
114
+ const result = await handleCompleteSlice(params, basePath);
115
+ assert.ok(!("error" in result), `handler failed: ${(result as any).error}`);
116
+
117
+ const gates = getGateResults("M001", "S01", "slice");
118
+ const q8 = gates.find((g) => g.gate_id === "Q8");
119
+ assert.ok(q8, "Q8 row must exist after complete-slice");
120
+ assert.equal(q8.status, "complete");
121
+ assert.equal(q8.verdict, "pass");
122
+ assert.ok(
123
+ q8.findings.includes("Health signal"),
124
+ "Q8 findings must capture the operationalReadiness content",
125
+ );
126
+ });
127
+
128
+ test("Q8 closes as 'omitted' when operationalReadiness is empty", async () => {
129
+ const params = makeValidSliceParams({ operationalReadiness: "" });
130
+
131
+ const result = await handleCompleteSlice(params, basePath);
132
+ assert.ok(!("error" in result), `handler failed: ${(result as any).error}`);
133
+
134
+ const gates = getGateResults("M001", "S01", "slice");
135
+ const q8 = gates.find((g) => g.gate_id === "Q8");
136
+ assert.ok(q8, "Q8 row must exist after complete-slice");
137
+ assert.equal(q8.status, "complete");
138
+ assert.equal(q8.verdict, "omitted");
139
+ });
140
+
141
+ test("Q8 also closes when operationalReadiness is omitted entirely", async () => {
142
+ // A model that doesn't pass operationalReadiness at all must still
143
+ // move Q8 out of 'pending' — leaving it pending produces the stall.
144
+ const params = makeValidSliceParams();
145
+ const result = await handleCompleteSlice(params, basePath);
146
+ assert.ok(!("error" in result), `handler failed: ${(result as any).error}`);
147
+
148
+ const gates = getGateResults("M001", "S01", "slice");
149
+ const q8 = gates.find((g) => g.gate_id === "Q8");
150
+ assert.ok(q8);
151
+ assert.notEqual(q8.status, "pending", "Q8 must never remain pending after complete-slice");
152
+ assert.equal(q8.verdict, "omitted");
153
+ });
154
+
155
+ test("summary markdown contains Operational Readiness section", async () => {
156
+ const params = makeValidSliceParams({
157
+ operationalReadiness: "- Health signal: /health\n- Failure signal: alert",
158
+ });
159
+ const result = await handleCompleteSlice(params, basePath);
160
+ assert.ok(!("error" in result));
161
+ if (!("error" in result)) {
162
+ const summary = fs.readFileSync(result.summaryPath, "utf-8");
163
+ assert.match(summary, /^## Operational Readiness/m);
164
+ assert.match(summary, /Health signal: \/health/);
165
+ }
166
+ });
167
+ });