gsd-pi 2.71.0-dev.e17e0ce → 2.72.0-dev.593fa74

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 (169) 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/partial-builder.js +40 -12
  15. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +103 -6
  16. package/dist/resources/extensions/gsd/auto/phases.js +4 -0
  17. package/dist/resources/extensions/gsd/auto-prompts.js +88 -33
  18. package/dist/resources/extensions/gsd/auto-start.js +24 -4
  19. package/dist/resources/extensions/gsd/auto.js +4 -0
  20. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  21. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +2 -5
  22. package/dist/resources/extensions/gsd/doctor-providers.js +23 -0
  23. package/dist/resources/extensions/gsd/error-classifier.js +4 -1
  24. package/dist/resources/extensions/gsd/gate-registry.js +208 -0
  25. package/dist/resources/extensions/gsd/gsd-db.js +41 -0
  26. package/dist/resources/extensions/gsd/milestone-validation-gates.js +11 -12
  27. package/dist/resources/extensions/gsd/notification-overlay.js +26 -12
  28. package/dist/resources/extensions/gsd/notification-store.js +5 -4
  29. package/dist/resources/extensions/gsd/prompt-validation.js +126 -0
  30. package/dist/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  31. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  32. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  33. package/dist/resources/extensions/gsd/shortcut-defs.js +7 -1
  34. package/dist/resources/extensions/gsd/state.js +9 -2
  35. package/dist/resources/extensions/gsd/tools/complete-slice.js +52 -1
  36. package/dist/resources/extensions/gsd/tools/complete-task.js +51 -1
  37. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +4 -1
  38. package/dist/resources/extensions/ollama/index.js +13 -5
  39. package/dist/resources/extensions/shared/gsd-phase-state.js +35 -0
  40. package/dist/resources/extensions/subagent/agents.js +8 -0
  41. package/dist/resources/extensions/subagent/index.js +17 -0
  42. package/dist/startup-model-validation.d.ts +0 -1
  43. package/dist/startup-model-validation.js +6 -2
  44. package/dist/web/standalone/.next/BUILD_ID +1 -1
  45. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  46. package/dist/web/standalone/.next/build-manifest.json +2 -2
  47. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/package.json +1 -1
  77. package/packages/mcp-server/dist/server.d.ts +12 -1
  78. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  79. package/packages/mcp-server/dist/server.js +90 -42
  80. package/packages/mcp-server/dist/server.js.map +1 -1
  81. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  82. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  83. package/packages/mcp-server/src/server.ts +110 -38
  84. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  85. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts +8 -0
  86. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-resolver.test.js +75 -0
  88. package/packages/pi-coding-agent/dist/core/model-resolver.test.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +5 -0
  90. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/retry-handler.js +55 -1
  92. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +57 -0
  94. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +87 -12
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +6 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -1
  106. package/packages/pi-coding-agent/package.json +1 -1
  107. package/packages/pi-coding-agent/src/core/model-resolver.test.ts +85 -0
  108. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +83 -0
  109. package/packages/pi-coding-agent/src/core/retry-handler.ts +60 -1
  110. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +72 -0
  111. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +15 -6
  112. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -12
  113. package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +6 -1
  114. package/pkg/package.json +1 -1
  115. package/src/resources/agents/debugger.md +58 -0
  116. package/src/resources/agents/doc-writer.md +43 -0
  117. package/src/resources/agents/git-ops.md +56 -0
  118. package/src/resources/agents/javascript-pro.md +46 -271
  119. package/src/resources/agents/planner.md +55 -0
  120. package/src/resources/agents/refactorer.md +47 -0
  121. package/src/resources/agents/reviewer.md +48 -0
  122. package/src/resources/agents/security.md +59 -0
  123. package/src/resources/agents/tester.md +50 -0
  124. package/src/resources/agents/typescript-pro.md +41 -235
  125. package/src/resources/extensions/claude-code-cli/partial-builder.ts +45 -12
  126. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +109 -3
  127. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +91 -2
  128. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +133 -2
  129. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  130. package/src/resources/extensions/gsd/auto-prompts.ts +111 -33
  131. package/src/resources/extensions/gsd/auto-start.ts +31 -4
  132. package/src/resources/extensions/gsd/auto.ts +4 -0
  133. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  134. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +2 -5
  135. package/src/resources/extensions/gsd/doctor-providers.ts +24 -0
  136. package/src/resources/extensions/gsd/error-classifier.ts +4 -1
  137. package/src/resources/extensions/gsd/gate-registry.ts +251 -0
  138. package/src/resources/extensions/gsd/gsd-db.ts +51 -0
  139. package/src/resources/extensions/gsd/milestone-validation-gates.ts +11 -13
  140. package/src/resources/extensions/gsd/notification-overlay.ts +27 -11
  141. package/src/resources/extensions/gsd/notification-store.ts +5 -4
  142. package/src/resources/extensions/gsd/prompt-validation.ts +157 -0
  143. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  144. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  145. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  146. package/src/resources/extensions/gsd/shortcut-defs.ts +8 -1
  147. package/src/resources/extensions/gsd/state.ts +13 -2
  148. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +14 -0
  149. package/src/resources/extensions/gsd/tests/complete-slice-gate-closure.test.ts +167 -0
  150. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +36 -0
  151. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +16 -0
  152. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +27 -0
  153. package/src/resources/extensions/gsd/tests/gate-registry.test.ts +140 -0
  154. package/src/resources/extensions/gsd/tests/prompt-system-gate-coverage.test.ts +208 -0
  155. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +9 -0
  156. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +3 -2
  157. package/src/resources/extensions/gsd/tools/complete-slice.ts +63 -0
  158. package/src/resources/extensions/gsd/tools/complete-task.ts +63 -0
  159. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +4 -1
  160. package/src/resources/extensions/gsd/types.ts +26 -0
  161. package/src/resources/extensions/ollama/index.ts +13 -3
  162. package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +28 -0
  163. package/src/resources/extensions/shared/gsd-phase-state.ts +42 -0
  164. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +48 -0
  165. package/src/resources/extensions/subagent/agents.ts +10 -0
  166. package/src/resources/extensions/subagent/index.ts +18 -0
  167. package/src/resources/extensions/subagent/tests/agents-conflicts.test.ts +33 -0
  168. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → h8B07q4xc-ujHRD7esO6O}/_buildManifest.js +0 -0
  169. /package/dist/web/standalone/.next/static/{cYPZv_bAhZk2ms-Pz6vsY → h8B07q4xc-ujHRD7esO6O}/_ssgManifest.js +0 -0
@@ -6,17 +6,12 @@
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
  import { _getAdapter } from "./gsd-db.js";
18
- /** Milestone validation gate IDs. */
19
- const MILESTONE_GATE_IDS = ["MV01", "MV02", "MV03", "MV04"];
14
+ import { getGatesForTurn } from "./gate-registry.js";
20
15
  /**
21
16
  * Insert milestone-level quality_gates records for a validation run.
22
17
  *
@@ -24,21 +19,25 @@ const MILESTONE_GATE_IDS = ["MV01", "MV02", "MV03", "MV04"];
24
19
  * from the overall milestone validation verdict. Individual gate-level
25
20
  * verdicts are not available (the handler receives a single verdict),
26
21
  * so all gates share the overall verdict.
22
+ *
23
+ * Gate IDs come from the registry — adding/removing an MV-scoped gate
24
+ * in gate-registry.ts automatically flows through here.
27
25
  */
28
26
  export function insertMilestoneValidationGates(milestoneId, sliceId, verdict, evaluatedAt) {
29
27
  const db = _getAdapter();
30
28
  if (!db)
31
29
  return;
32
30
  const gateVerdict = verdict === "pass" ? "pass" : "flag";
33
- for (const gateId of MILESTONE_GATE_IDS) {
31
+ const milestoneGates = getGatesForTurn("validate-milestone");
32
+ for (const def of milestoneGates) {
34
33
  db.prepare(`INSERT OR REPLACE INTO quality_gates
35
34
  (milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
36
35
  VALUES (:mid, :sid, :gid, 'milestone', '', 'complete', :verdict, :rationale, '', :evaluated_at)`).run({
37
36
  ":mid": milestoneId,
38
37
  ":sid": sliceId,
39
- ":gid": gateId,
38
+ ":gid": def.id,
40
39
  ":verdict": gateVerdict,
41
- ":rationale": `Milestone validation verdict: ${verdict}`,
40
+ ":rationale": `${def.promptSection} — milestone validation verdict: ${verdict}`,
42
41
  ":evaluated_at": evaluatedAt,
43
42
  });
44
43
  }
@@ -2,7 +2,7 @@
2
2
  // Scrollable panel showing all persisted notifications with severity filtering.
3
3
  // Toggled with Ctrl+Alt+N (⌃⌥N on macOS), Ctrl+Shift+N fallback, or /gsd notifications.
4
4
  import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
5
- import { readNotifications, markAllRead, clearNotifications, } from "./notification-store.js";
5
+ import { readNotifications, markAllRead, clearNotifications, onNotificationStoreChange, } from "./notification-store.js";
6
6
  import { formattedShortcutPair } from "./shortcut-defs.js";
7
7
  import { padRight, joinColumns } from "../shared/mod.js";
8
8
  const FILTER_CYCLE = ["all", "error", "warning", "info"];
@@ -74,6 +74,7 @@ export class GSDNotificationOverlay {
74
74
  refreshTimer;
75
75
  disposed = false;
76
76
  resizeHandler = null;
77
+ unsubscribeStore = null;
77
78
  constructor(tui, theme, onClose) {
78
79
  this.tui = tui;
79
80
  this.theme = theme;
@@ -90,20 +91,18 @@ export class GSDNotificationOverlay {
90
91
  this.tui.requestRender();
91
92
  };
92
93
  process.stdout.on("resize", this.resizeHandler);
93
- // Refresh every 3s for new notifications
94
+ // Subscribe to store mutations for immediate updates
95
+ this.unsubscribeStore = onNotificationStoreChange(() => {
96
+ if (this.disposed)
97
+ return;
98
+ this._refreshFromDisk();
99
+ });
100
+ // 30s safety-net for cross-process edits (web subprocess, parallel workers)
94
101
  this.refreshTimer = setInterval(() => {
95
102
  if (this.disposed)
96
103
  return;
97
- const fresh = readNotifications();
98
- const signature = notificationSignature(fresh);
99
- if (signature !== this.entriesSignature) {
100
- markAllRead();
101
- this.entries = readNotifications();
102
- this.entriesSignature = notificationSignature(this.entries);
103
- this.invalidate();
104
- this.tui.requestRender();
105
- }
106
- }, 3000);
104
+ this._refreshFromDisk();
105
+ }, 30_000);
107
106
  }
108
107
  get filter() {
109
108
  return FILTER_CYCLE[this.filterIndex];
@@ -188,11 +187,26 @@ export class GSDNotificationOverlay {
188
187
  dispose() {
189
188
  this.disposed = true;
190
189
  clearInterval(this.refreshTimer);
190
+ if (this.unsubscribeStore) {
191
+ this.unsubscribeStore();
192
+ this.unsubscribeStore = null;
193
+ }
191
194
  if (this.resizeHandler) {
192
195
  process.stdout.removeListener("resize", this.resizeHandler);
193
196
  this.resizeHandler = null;
194
197
  }
195
198
  }
199
+ _refreshFromDisk() {
200
+ const fresh = readNotifications();
201
+ const signature = notificationSignature(fresh);
202
+ if (signature !== this.entriesSignature) {
203
+ markAllRead();
204
+ this.entries = readNotifications();
205
+ this.entriesSignature = notificationSignature(this.entries);
206
+ this.invalidate();
207
+ this.tui.requestRender();
208
+ }
209
+ }
196
210
  wrapInBox(inner, width) {
197
211
  const th = this.theme;
198
212
  const border = (s) => th.fg("borderAccent", s);
@@ -301,10 +301,11 @@ function _withLock(basePath, fn) {
301
301
  break;
302
302
  }
303
303
  }
304
- // Only run the mutation if we actually own the lock
305
- const ownsLock = fd !== null;
304
+ // Best-effort: mutation runs regardless of lock status (idempotent overwrites).
305
+ // createdLock gates cleanup only — never skip fn() on lock failure.
306
+ const createdLock = fd !== null;
306
307
  try {
307
- if (ownsLock && fd !== null) {
308
+ if (createdLock && fd !== null) {
308
309
  // Write our PID timestamp into the lock for stale detection
309
310
  writeFileSync(lockPath, String(Date.now()), "utf-8");
310
311
  closeSync(fd);
@@ -313,7 +314,7 @@ function _withLock(basePath, fn) {
313
314
  }
314
315
  finally {
315
316
  // Only delete the lock if we created it — never remove another process's lock
316
- if (ownsLock) {
317
+ if (createdLock) {
317
318
  try {
318
319
  unlinkSync(lockPath);
319
320
  }
@@ -0,0 +1,126 @@
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
+ import { getGatesForTurn } from "./gate-registry.js";
12
+ /**
13
+ * Validate that enhanced context content has all required sections.
14
+ *
15
+ * Required sections per R109:
16
+ * - Scope section (## Scope, ## Milestone Scope, or ## Why This Milestone)
17
+ * - Architectural Decisions section (## Architectural Decisions)
18
+ * - Acceptance Criteria section (## Acceptance Criteria or ## Final Integrated Acceptance)
19
+ *
20
+ * Additionally validates that the Architectural Decisions section contains
21
+ * at least one decision entry (### heading or **Decision marker).
22
+ *
23
+ * @param content - The enhanced context markdown content
24
+ * @returns ValidationResult with valid flag and list of missing sections
25
+ */
26
+ export function validateEnhancedContext(content) {
27
+ const missing = [];
28
+ // Required section 1: Scope (multiple acceptable header variants)
29
+ const hasScopeSection = /^## Scope\b/m.test(content) ||
30
+ /^## Milestone Scope\b/m.test(content) ||
31
+ /^## Why This Milestone\b/m.test(content);
32
+ if (!hasScopeSection) {
33
+ missing.push("Milestone Scope or Why This Milestone");
34
+ }
35
+ // Required section 2: Architectural Decisions
36
+ const hasArchitecturalDecisions = /^## Architectural Decisions\b/m.test(content);
37
+ if (!hasArchitecturalDecisions) {
38
+ missing.push("Architectural Decisions");
39
+ }
40
+ // Required section 3: Acceptance Criteria (multiple acceptable header variants)
41
+ const hasAcceptanceCriteria = /^## Acceptance Criteria\b/m.test(content) ||
42
+ /^## Final Integrated Acceptance\b/m.test(content);
43
+ if (!hasAcceptanceCriteria) {
44
+ missing.push("Acceptance Criteria");
45
+ }
46
+ // Additional validation: Architectural Decisions must have at least one entry
47
+ if (hasArchitecturalDecisions) {
48
+ // Extract the section content between ## Architectural Decisions and the next ## heading.
49
+ // Uses indexOf-based extraction instead of regex with \z (which is invalid in JavaScript
50
+ // regex — it's PCRE/Ruby syntax and JS treats it as literal 'z').
51
+ const sectionStart = content.indexOf("## Architectural Decisions");
52
+ if (sectionStart === -1) {
53
+ missing.push("Architectural Decisions");
54
+ }
55
+ else {
56
+ const afterHeading = content.slice(sectionStart + "## Architectural Decisions".length);
57
+ const nextSection = afterHeading.search(/^## /m);
58
+ const sectionContent = nextSection === -1 ? afterHeading : afterHeading.slice(0, nextSection);
59
+ // Check for actual decision entries:
60
+ // - ### heading (subsection per decision)
61
+ // - **Decision marker (inline decision format)
62
+ const hasDecisionEntry = /^### /m.test(sectionContent) || /^\*\*Decision/m.test(sectionContent);
63
+ if (!hasDecisionEntry) {
64
+ missing.push("At least one architectural decision entry");
65
+ }
66
+ }
67
+ }
68
+ return {
69
+ valid: missing.length === 0,
70
+ missing,
71
+ };
72
+ }
73
+ // ─── Per-Turn Gate Section Validators ─────────────────────────────────────
74
+ //
75
+ // Each validator checks that the artifact written by a turn contains a
76
+ // heading for every gate owned by that turn. The registry is the source
77
+ // of truth for which sections must exist; adding a new gate automatically
78
+ // flows through via `getGatesForTurn(turn)`.
79
+ /**
80
+ * Escape a string so it can be embedded safely inside a regular expression.
81
+ */
82
+ function escapeRegExp(value) {
83
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
84
+ }
85
+ /**
86
+ * Validate that an artifact contains an `## H2` heading for every gate the
87
+ * named turn owns. Returns the list of missing gate section headers.
88
+ *
89
+ * Soft rule: a section counts as "present" if it is declared (H2 heading
90
+ * exists) — empty-body sections are allowed and handled by the tool
91
+ * handler, which will record such gates as `omitted`.
92
+ */
93
+ export function validateGateSections(content, turn) {
94
+ const missing = [];
95
+ for (const def of getGatesForTurn(turn)) {
96
+ const pattern = new RegExp(`^##\\s+${escapeRegExp(def.promptSection)}\\b`, "m");
97
+ if (!pattern.test(content)) {
98
+ missing.push(`${def.id} (## ${def.promptSection})`);
99
+ }
100
+ }
101
+ return { valid: missing.length === 0, missing };
102
+ }
103
+ /**
104
+ * Validate a SUMMARY.md produced by the complete-slice turn. Requires
105
+ * an H2 heading for every gate owned by complete-slice (e.g. Q8 →
106
+ * "## Operational Readiness"). Intended for use in the tool handler's
107
+ * pre-write checks or in the post-unit validation sweep.
108
+ */
109
+ export function validateSliceSummaryOutput(content) {
110
+ return validateGateSections(content, "complete-slice");
111
+ }
112
+ /**
113
+ * Validate a task SUMMARY.md produced by the execute-task turn. Only
114
+ * flags gates that are still pending for the task; skips the check
115
+ * when no rows are seeded (simple task).
116
+ */
117
+ export function validateTaskSummaryOutput(content) {
118
+ return validateGateSections(content, "execute-task");
119
+ }
120
+ /**
121
+ * Validate a VALIDATION.md produced by the validate-milestone turn.
122
+ * Requires an H2 heading for every MV gate declared in the registry.
123
+ */
124
+ export function validateMilestoneValidationOutput(content) {
125
+ return validateGateSections(content, "validate-milestone");
126
+ }
@@ -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
@@ -5,16 +5,19 @@ export const GSD_SHORTCUTS = {
5
5
  key: "g",
6
6
  action: "Open GSD dashboard",
7
7
  command: "/gsd status",
8
+ hasFallback: true,
8
9
  },
9
10
  notifications: {
10
11
  key: "n",
11
12
  action: "Open notification history",
12
13
  command: "/gsd notifications",
14
+ hasFallback: true,
13
15
  },
14
16
  parallel: {
15
17
  key: "p",
16
18
  action: "Open parallel worker monitor",
17
19
  command: "/gsd parallel watch",
20
+ hasFallback: false, // Ctrl+Shift+P conflicts with cycleModelBackward
18
21
  },
19
22
  };
20
23
  function combo(prefix, key) {
@@ -27,7 +30,10 @@ export function fallbackShortcutCombo(id) {
27
30
  return combo("Ctrl+Shift+", GSD_SHORTCUTS[id].key);
28
31
  }
29
32
  export function shortcutPair(id, formatter = (combo) => combo) {
30
- return `${formatter(primaryShortcutCombo(id))} / ${formatter(fallbackShortcutCombo(id))}`;
33
+ const primary = formatter(primaryShortcutCombo(id));
34
+ if (!GSD_SHORTCUTS[id].hasFallback)
35
+ return primary;
36
+ return `${primary} / ${formatter(fallbackShortcutCombo(id))}`;
31
37
  }
32
38
  export function formattedShortcutPair(id) {
33
39
  return shortcutPair(id, formatShortcut);
@@ -13,7 +13,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
13
13
  import { debugCount, debugTime } from './debug-logger.js';
14
14
  import { logWarning, logError } from './workflow-logger.js';
15
15
  import { extractVerdict } from './verdict-parser.js';
16
- import { isDbAvailable, wasDbOpenAttempted, getAllMilestones, getMilestone, getMilestoneSlices, getSliceTasks, getReplanHistory, getSlice, insertMilestone, insertSlice, insertTask, updateTaskStatus, getPendingSliceGateCount, } from './gsd-db.js';
16
+ import { isDbAvailable, wasDbOpenAttempted, getAllMilestones, getMilestone, getMilestoneSlices, getSliceTasks, getReplanHistory, getSlice, insertMilestone, insertSlice, insertTask, updateTaskStatus, getPendingGateCountForTurn, } from './gsd-db.js';
17
17
  /**
18
18
  * A "ghost" milestone directory contains only META.json (and no substantive
19
19
  * files like CONTEXT, CONTEXT-DRAFT, ROADMAP, or SUMMARY). These appear when
@@ -724,7 +724,14 @@ export async function deriveStateFromDb(basePath) {
724
724
  };
725
725
  }
726
726
  }
727
- const pendingGateCount = getPendingSliceGateCount(activeMilestone.id, activeSlice.id);
727
+ // ── Quality gate evaluation check ──────────────────────────────────
728
+ // Pause before execution only when gates owned by the `gate-evaluate`
729
+ // turn (Q3/Q4) are still pending. Q8 is also `scope:"slice"` but is
730
+ // owned by `complete-slice`, so it must NOT block the evaluating-gates
731
+ // phase — otherwise auto-loop stalls forever waiting for a gate that
732
+ // this turn never evaluates. See gate-registry.ts for the ownership map.
733
+ // Slices with zero gate rows (pre-feature or simple) skip straight through.
734
+ const pendingGateCount = getPendingGateCountForTurn(activeMilestone.id, activeSlice.id, "gate-evaluate");
728
735
  if (pendingGateCount > 0) {
729
736
  return {
730
737
  activeMilestone, activeSlice, activeTask: null,
@@ -9,7 +9,8 @@
9
9
  import { join } from "node:path";
10
10
  import { mkdirSync } from "node:fs";
11
11
  import { isClosedStatus } from "../status-guards.js";
12
- import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, setSliceSummaryMd, } from "../gsd-db.js";
12
+ import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, setSliceSummaryMd, saveGateResult, getPendingGatesForTurn, } from "../gsd-db.js";
13
+ import { getGatesForTurn } from "../gate-registry.js";
13
14
  import { resolveSlicePath, clearPathCache } from "../paths.js";
14
15
  import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
15
16
  import { saveFile, clearParseCache } from "../files.js";
@@ -19,6 +20,19 @@ import { renderAllProjections } from "../workflow-projections.js";
19
20
  import { writeManifest } from "../workflow-manifest.js";
20
21
  import { appendEvent } from "../workflow-events.js";
21
22
  import { logWarning, logError } from "../workflow-logger.js";
23
+ /**
24
+ * Map a complete-slice-owned gate id to the CompleteSliceParams field
25
+ * whose presence drives `pass` vs. `omitted`. Keep this in lockstep with
26
+ * the gates declared in gate-registry.ts under ownerTurn "complete-slice".
27
+ */
28
+ function sliceGateFieldForId(id, params) {
29
+ switch (id) {
30
+ case "Q8":
31
+ return params.operationalReadiness;
32
+ default:
33
+ return undefined;
34
+ }
35
+ }
22
36
  /**
23
37
  * Render slice summary markdown matching the template format.
24
38
  * YAML frontmatter uses snake_case keys for parseSummary() compatibility.
@@ -134,6 +148,10 @@ ${reqSurfaced}
134
148
 
135
149
  ${reqInvalidated}
136
150
 
151
+ ## Operational Readiness
152
+
153
+ ${params.operationalReadiness?.trim() || "None."}
154
+
137
155
  ## Deviations
138
156
 
139
157
  ${params.deviations || "None."}
@@ -271,6 +289,39 @@ export async function handleCompleteSlice(params, basePath) {
271
289
  }
272
290
  // Store rendered markdown in DB for D004 recovery
273
291
  setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
292
+ // ── Close gates owned by complete-slice (Q8) ───────────────────────────
293
+ // Each owned gate maps to a specific summary section via the registry.
294
+ // If the caller populated the corresponding field, record `pass`; if the
295
+ // field is empty, record `omitted`. Without this loop, Q8 would stay
296
+ // pending forever and block future state derivation (see gate-registry).
297
+ try {
298
+ const pendingGates = getPendingGatesForTurn(params.milestoneId, params.sliceId, "complete-slice");
299
+ if (pendingGates.length > 0) {
300
+ const ownedDefs = new Map(getGatesForTurn("complete-slice").map((g) => [g.id, g]));
301
+ for (const row of pendingGates) {
302
+ const def = ownedDefs.get(row.gate_id);
303
+ if (!def)
304
+ continue;
305
+ // Map gate id → param field it maps to. Keep the map local so
306
+ // adding a new complete-slice gate is a single place change.
307
+ const field = sliceGateFieldForId(def.id, params);
308
+ const hasContent = typeof field === "string" && field.trim().length > 0;
309
+ saveGateResult({
310
+ milestoneId: params.milestoneId,
311
+ sliceId: params.sliceId,
312
+ gateId: def.id,
313
+ verdict: hasContent ? "pass" : "omitted",
314
+ rationale: hasContent
315
+ ? `${def.promptSection} section populated in slice summary`
316
+ : `${def.promptSection} section left empty — recorded as omitted`,
317
+ findings: hasContent ? field.trim() : "",
318
+ });
319
+ }
320
+ }
321
+ }
322
+ catch (gateErr) {
323
+ logWarning("tool", `complete-slice gate close warning for ${params.milestoneId}/${params.sliceId}: ${gateErr.message}`);
324
+ }
274
325
  // Invalidate all caches
275
326
  invalidateStateCache();
276
327
  clearPathCache();
@@ -9,7 +9,8 @@
9
9
  import { join } from "node:path";
10
10
  import { mkdirSync } from "node:fs";
11
11
  import { isClosedStatus } from "../status-guards.js";
12
- import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, setTaskSummaryMd, deleteVerificationEvidence, } from "../gsd-db.js";
12
+ import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, setTaskSummaryMd, deleteVerificationEvidence, saveGateResult, getPendingGatesForTurn, } from "../gsd-db.js";
13
+ import { getGatesForTurn } from "../gate-registry.js";
13
14
  import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
14
15
  import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
15
16
  import { saveFile, clearParseCache } from "../files.js";
@@ -19,6 +20,23 @@ import { renderAllProjections, renderSummaryContent } from "../workflow-projecti
19
20
  import { writeManifest } from "../workflow-manifest.js";
20
21
  import { appendEvent } from "../workflow-events.js";
21
22
  import { logWarning, logError } from "../workflow-logger.js";
23
+ /**
24
+ * Map an execute-task-owned gate id to the CompleteTaskParams field whose
25
+ * presence drives `pass` vs. `omitted`. Keep in lockstep with the gates
26
+ * declared in gate-registry.ts under ownerTurn "execute-task".
27
+ */
28
+ function taskGateFieldForId(id, params) {
29
+ switch (id) {
30
+ case "Q5":
31
+ return params.failureModes;
32
+ case "Q6":
33
+ return params.loadProfile;
34
+ case "Q7":
35
+ return params.negativeTests;
36
+ default:
37
+ return undefined;
38
+ }
39
+ }
22
40
  /**
23
41
  * Normalize a list parameter that may arrive as a string (newline-delimited
24
42
  * bullet list from the LLM) into a string array (#3361).
@@ -189,6 +207,38 @@ export async function handleCompleteTask(params, basePath) {
189
207
  }
190
208
  // Store rendered markdown in DB for D004 recovery
191
209
  setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd);
210
+ // ── Close gates owned by execute-task (Q5/Q6/Q7) for this task ────────
211
+ // Each gate id maps to a specific params field via taskGateFieldForId.
212
+ // When the model populates the field, record `pass`; when it's empty,
213
+ // record `omitted`. Task-scoped rows are filtered by taskId so a single
214
+ // task's completion doesn't touch sibling tasks' gate rows.
215
+ try {
216
+ const pendingGates = getPendingGatesForTurn(params.milestoneId, params.sliceId, "execute-task", params.taskId);
217
+ if (pendingGates.length > 0) {
218
+ const ownedDefs = new Map(getGatesForTurn("execute-task").map((g) => [g.id, g]));
219
+ for (const row of pendingGates) {
220
+ const def = ownedDefs.get(row.gate_id);
221
+ if (!def)
222
+ continue;
223
+ const field = taskGateFieldForId(def.id, params);
224
+ const hasContent = typeof field === "string" && field.trim().length > 0;
225
+ saveGateResult({
226
+ milestoneId: params.milestoneId,
227
+ sliceId: params.sliceId,
228
+ taskId: params.taskId,
229
+ gateId: def.id,
230
+ verdict: hasContent ? "pass" : "omitted",
231
+ rationale: hasContent
232
+ ? `${def.promptSection} section populated in task summary`
233
+ : `${def.promptSection} section left empty — recorded as omitted`,
234
+ findings: hasContent ? field.trim() : "",
235
+ });
236
+ }
237
+ }
238
+ }
239
+ catch (gateErr) {
240
+ logWarning("tool", `complete-task gate close warning for ${params.milestoneId}/${params.sliceId}/${params.taskId}: ${gateErr.message}`);
241
+ }
192
242
  // Invalidate all caches
193
243
  invalidateStateCache();
194
244
  clearPathCache();
@@ -2,6 +2,7 @@ import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
2
2
  import { sanitizeCompleteMilestoneParams } from "../bootstrap/sanitize-complete-milestone.js";
3
3
  import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot } from "../bootstrap/write-gate.js";
4
4
  import { getMilestone, getSliceStatusSummary, getSliceTaskCounts, _getAdapter, saveGateResult, } from "../gsd-db.js";
5
+ import { GATE_REGISTRY } from "../gate-registry.js";
5
6
  import { saveArtifactToDb } from "../db-writer.js";
6
7
  import { handleCompleteMilestone } from "./complete-milestone.js";
7
8
  import { handleCompleteTask } from "./complete-task.js";
@@ -323,7 +324,9 @@ export async function executeSaveGateResult(params, basePath = process.cwd()) {
323
324
  isError: true,
324
325
  };
325
326
  }
326
- const validGates = ["Q3", "Q4", "Q5", "Q6", "Q7", "Q8"];
327
+ // Source of truth: gate-registry.ts. Every declared GateId is accepted,
328
+ // so adding a new gate in one place automatically flows through here.
329
+ const validGates = Object.keys(GATE_REGISTRY);
327
330
  if (!validGates.includes(params.gateId)) {
328
331
  return {
329
332
  content: [{ type: "text", text: `Error: Invalid gateId "${params.gateId}". Must be one of: ${validGates.join(", ")}` }],
@@ -49,8 +49,15 @@ async function probeAndRegister(pi) {
49
49
  return false;
50
50
  }
51
51
  const models = await discoverModels();
52
- if (models.length === 0)
53
- return true; // Running but no models pulled
52
+ if (models.length === 0) {
53
+ // No local models means there's nothing usable to register in GSD.
54
+ // Keep the footer/status clean instead of advertising Ollama availability.
55
+ if (providerRegistered) {
56
+ pi.unregisterProvider("ollama");
57
+ providerRegistered = false;
58
+ }
59
+ return false;
60
+ }
54
61
  const baseUrl = client.getOllamaHost();
55
62
  // Use authMode "apiKey" with a dummy key (#3440).
56
63
  // authMode "none" requires a custom streamSimple handler, but Ollama uses
@@ -102,10 +109,11 @@ export default function ollama(pi) {
102
109
  else {
103
110
  probeAndRegister(pi)
104
111
  .then((found) => {
105
- if (found)
106
- ctx.ui.setStatus("ollama", "Ollama");
112
+ ctx.ui.setStatus("ollama", found ? "Ollama" : undefined);
107
113
  })
108
- .catch(() => { });
114
+ .catch(() => {
115
+ ctx.ui.setStatus("ollama", undefined);
116
+ });
109
117
  }
110
118
  });
111
119
  pi.on("session_shutdown", async () => {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * GSD Phase State — cross-extension coordination
3
+ * Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
4
+ *
5
+ * Lightweight module-level state that GSD auto-mode writes to and the
6
+ * subagent tool reads from. Both extensions run in the same process so
7
+ * a module variable is sufficient — no file I/O needed.
8
+ */
9
+ let _active = false;
10
+ let _currentPhase = null;
11
+ /** Mark GSD auto-mode as active. */
12
+ export function activateGSD() {
13
+ _active = true;
14
+ }
15
+ /** Mark GSD auto-mode as inactive and clear the current phase. */
16
+ export function deactivateGSD() {
17
+ _active = false;
18
+ _currentPhase = null;
19
+ }
20
+ /** Set the currently dispatched GSD phase (e.g. "plan-milestone"). */
21
+ export function setCurrentPhase(phase) {
22
+ _currentPhase = phase;
23
+ }
24
+ /** Clear the current phase (unit completed or aborted). */
25
+ export function clearCurrentPhase() {
26
+ _currentPhase = null;
27
+ }
28
+ /** Returns true if GSD auto-mode is currently active. */
29
+ export function isGSDActive() {
30
+ return _active;
31
+ }
32
+ /** Returns the current GSD phase, or null if none is active. */
33
+ export function getCurrentPhase() {
34
+ return _active ? _currentPhase : null;
35
+ }
@@ -5,6 +5,12 @@ import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { getAgentDir, parseFrontmatter } from "@gsd/pi-coding-agent";
7
7
  const PROJECT_AGENT_DIR_CANDIDATES = [".gsd", ".pi"];
8
+ export function parseConflictsWith(value) {
9
+ if (typeof value !== "string")
10
+ return undefined;
11
+ const conflicts = value.split(",").map((s) => s.trim()).filter(Boolean);
12
+ return conflicts.length > 0 ? conflicts : undefined;
13
+ }
8
14
  function parseAgentTools(value) {
9
15
  if (typeof value === "string") {
10
16
  const tools = value
@@ -52,11 +58,13 @@ function loadAgentsFromDir(dir, source) {
52
58
  continue;
53
59
  }
54
60
  const tools = parseAgentTools(frontmatter.tools);
61
+ const conflictsWith = parseConflictsWith(frontmatter.conflicts_with);
55
62
  agents.push({
56
63
  name: frontmatter.name,
57
64
  description: frontmatter.description,
58
65
  tools: tools && tools.length > 0 ? tools : undefined,
59
66
  model: frontmatter.model,
67
+ conflictsWith,
60
68
  systemPrompt: body,
61
69
  source,
62
70
  filePath,