gsd-pi 2.72.0-dev.3159350 → 2.72.0-dev.4f3264a

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 (189) hide show
  1. package/dist/resources/extensions/async-jobs/await-tool.js +4 -7
  2. package/dist/resources/extensions/async-jobs/job-manager.js +3 -28
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +26 -27
  4. package/dist/resources/extensions/gsd/auto/loop.js +1 -84
  5. package/dist/resources/extensions/gsd/auto-observability.js +54 -0
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +0 -6
  7. package/dist/resources/extensions/gsd/auto.js +19 -25
  8. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -9
  9. package/dist/resources/extensions/gsd/commands-handlers.js +1 -4
  10. package/dist/resources/extensions/gsd/context-injector.js +1 -1
  11. package/dist/resources/extensions/gsd/custom-workflow-engine.js +7 -3
  12. package/dist/resources/extensions/gsd/file-watcher.js +80 -0
  13. package/dist/resources/extensions/gsd/gsd-db.js +5 -47
  14. package/dist/resources/extensions/gsd/key-manager.js +0 -2
  15. package/dist/resources/extensions/gsd/preferences-skills.js +34 -2
  16. package/dist/resources/extensions/gsd/preferences-types.js +0 -15
  17. package/dist/resources/extensions/gsd/preferences.js +3 -16
  18. package/dist/resources/extensions/gsd/prompt-loader.js +1 -4
  19. package/dist/resources/extensions/gsd/rtk-status.js +43 -0
  20. package/dist/resources/extensions/gsd/state.js +1 -21
  21. package/dist/resources/extensions/gsd/write-intercept.js +1 -10
  22. package/dist/resources/extensions/ollama/index.js +5 -4
  23. package/dist/resources/extensions/ollama/ollama-client.js +6 -35
  24. package/dist/resources/extensions/ollama/ollama-discovery.js +6 -32
  25. package/dist/web/standalone/.next/BUILD_ID +1 -1
  26. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  27. package/dist/web/standalone/.next/build-manifest.json +2 -2
  28. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  29. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  30. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  40. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  53. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  59. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  69. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  74. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  77. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  84. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  85. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  86. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +3 -3
  87. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/index.html +1 -1
  92. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/page.js +2 -2
  99. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  101. package/dist/web/standalone/.next/server/chunks/2331.js +16 -16
  102. package/dist/web/standalone/.next/server/chunks/4741.js +12 -12
  103. package/dist/web/standalone/.next/server/chunks/5822.js +2 -2
  104. package/dist/web/standalone/.next/server/chunks/63.js +8 -8
  105. package/dist/web/standalone/.next/server/chunks/6897.js +3 -3
  106. package/dist/web/standalone/.next/server/functions-config-manifest.json +9 -0
  107. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/middleware-manifest.json +2 -29
  109. package/dist/web/standalone/.next/server/middleware.js +12 -4
  110. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  111. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  112. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  113. package/dist/web/standalone/.next/server/webpack-runtime.js +1 -1
  114. package/package.json +1 -1
  115. package/packages/pi-ai/dist/env-api-keys.js +0 -1
  116. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  117. package/packages/pi-ai/dist/models.custom.d.ts +0 -105
  118. package/packages/pi-ai/dist/models.custom.d.ts.map +1 -1
  119. package/packages/pi-ai/dist/models.custom.js +0 -97
  120. package/packages/pi-ai/dist/models.custom.js.map +1 -1
  121. package/packages/pi-ai/dist/models.generated.d.ts +140 -648
  122. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  123. package/packages/pi-ai/dist/models.generated.js +364 -861
  124. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  125. package/packages/pi-ai/dist/models.test.js +0 -105
  126. package/packages/pi-ai/dist/models.test.js.map +1 -1
  127. package/packages/pi-ai/dist/types.d.ts +1 -1
  128. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  129. package/packages/pi-ai/dist/types.js.map +1 -1
  130. package/packages/pi-ai/src/env-api-keys.ts +0 -1
  131. package/packages/pi-ai/src/models.custom.ts +0 -98
  132. package/packages/pi-ai/src/models.generated.ts +364 -861
  133. package/packages/pi-ai/src/models.test.ts +0 -135
  134. package/packages/pi-ai/src/types.ts +0 -1
  135. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/model-resolver.js +0 -1
  137. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  138. package/packages/pi-coding-agent/src/core/model-resolver.ts +0 -1
  139. package/src/resources/extensions/async-jobs/await-tool.test.ts +7 -40
  140. package/src/resources/extensions/async-jobs/await-tool.ts +4 -7
  141. package/src/resources/extensions/async-jobs/job-manager.ts +3 -33
  142. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +26 -27
  143. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +2 -20
  144. package/src/resources/extensions/gsd/auto/loop.ts +1 -89
  145. package/src/resources/extensions/gsd/auto-observability.ts +72 -0
  146. package/src/resources/extensions/gsd/auto-post-unit.ts +0 -7
  147. package/src/resources/extensions/gsd/auto.ts +20 -25
  148. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +10 -8
  149. package/src/resources/extensions/gsd/commands-handlers.ts +1 -5
  150. package/src/resources/extensions/gsd/context-injector.ts +1 -1
  151. package/src/resources/extensions/gsd/custom-workflow-engine.ts +8 -4
  152. package/src/resources/extensions/gsd/file-watcher.ts +100 -0
  153. package/src/resources/extensions/gsd/gsd-db.ts +5 -52
  154. package/src/resources/extensions/gsd/key-manager.ts +0 -2
  155. package/src/resources/extensions/gsd/preferences-skills.ts +36 -2
  156. package/src/resources/extensions/gsd/preferences-types.ts +0 -16
  157. package/src/resources/extensions/gsd/preferences.ts +6 -19
  158. package/src/resources/extensions/gsd/prompt-loader.ts +1 -6
  159. package/src/resources/extensions/gsd/rtk-status.ts +53 -0
  160. package/src/resources/extensions/gsd/state.ts +0 -20
  161. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +0 -74
  162. package/src/resources/extensions/gsd/tests/key-manager.test.ts +0 -63
  163. package/src/resources/extensions/gsd/tests/preferences.test.ts +0 -53
  164. package/src/resources/extensions/gsd/write-intercept.ts +1 -10
  165. package/src/resources/extensions/ollama/index.ts +5 -4
  166. package/src/resources/extensions/ollama/ollama-client.ts +6 -35
  167. package/src/resources/extensions/ollama/ollama-discovery.ts +6 -37
  168. package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +0 -54
  169. package/dist/resources/extensions/gsd/definition-io.js +0 -15
  170. package/dist/web/standalone/.next/server/edge-runtime-webpack.js +0 -2
  171. package/packages/pi-ai/dist/models.generated.test.d.ts +0 -2
  172. package/packages/pi-ai/dist/models.generated.test.d.ts.map +0 -1
  173. package/packages/pi-ai/dist/models.generated.test.js +0 -334
  174. package/packages/pi-ai/dist/models.generated.test.js.map +0 -1
  175. package/packages/pi-ai/src/models.generated.test.ts +0 -373
  176. package/src/resources/extensions/gsd/definition-io.ts +0 -18
  177. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +0 -27
  178. package/src/resources/extensions/gsd/tests/block-db-writes.test.ts +0 -63
  179. package/src/resources/extensions/gsd/tests/definition-io.test.ts +0 -57
  180. package/src/resources/extensions/gsd/tests/doctor-heal-fixable-warnings.test.ts +0 -14
  181. package/src/resources/extensions/gsd/tests/false-degraded-mode-warning.test.ts +0 -104
  182. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +0 -54
  183. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +0 -34
  184. package/src/resources/extensions/gsd/tests/preferences-formatting.test.ts +0 -87
  185. package/src/resources/extensions/gsd/tests/prompt-loader-working-directory.test.ts +0 -19
  186. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +0 -97
  187. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +0 -41
  188. /package/dist/web/standalone/.next/static/{eR2tLKungpmiiOyUIhqjF → vr6Pbde48w4rMUplqDdh_}/_buildManifest.js +0 -0
  189. /package/dist/web/standalone/.next/static/{eR2tLKungpmiiOyUIhqjF → vr6Pbde48w4rMUplqDdh_}/_ssgManifest.js +0 -0
@@ -29,69 +29,6 @@ import {
29
29
  import { debugLog } from "../debug-logger.js";
30
30
  import { isInfrastructureError, isTransientCooldownError, getCooldownRetryAfterMs, COOLDOWN_FALLBACK_WAIT_MS, MAX_COOLDOWN_RETRIES } from "./infra-errors.js";
31
31
  import { resolveEngine } from "../engine-resolver.js";
32
- import { logWarning } from "../workflow-logger.js";
33
- import { gsdRoot } from "../paths.js";
34
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
35
- import { join } from "node:path";
36
-
37
- // ── Stuck detection persistence (#3704) ──────────────────────────────────
38
- // Persist stuck detection state to disk so it survives session restarts.
39
- // Without this, restarting auto-mode resets all counters, allowing the
40
- // same blocked unit to burn a full retry budget each session.
41
- function stuckStatePath(basePath: string): string {
42
- return join(gsdRoot(basePath), "runtime", "stuck-state.json");
43
- }
44
-
45
- function loadStuckState(basePath: string): { recentUnits: Array<{ key: string }>; stuckRecoveryAttempts: number } {
46
- try {
47
- const data = JSON.parse(readFileSync(stuckStatePath(basePath), "utf-8"));
48
- return {
49
- recentUnits: Array.isArray(data.recentUnits) ? data.recentUnits : [],
50
- stuckRecoveryAttempts: typeof data.stuckRecoveryAttempts === "number" ? data.stuckRecoveryAttempts : 0,
51
- };
52
- } catch (err) {
53
- debugLog("autoLoop", { phase: "load-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
54
- return { recentUnits: [], stuckRecoveryAttempts: 0 };
55
- }
56
- }
57
-
58
- function saveStuckState(basePath: string, state: LoopState): void {
59
- try {
60
- const filePath = stuckStatePath(basePath);
61
- mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
62
- writeFileSync(filePath, JSON.stringify({
63
- recentUnits: state.recentUnits.slice(-20), // keep last 20 entries
64
- stuckRecoveryAttempts: state.stuckRecoveryAttempts,
65
- updatedAt: new Date().toISOString(),
66
- }) + "\n");
67
- } catch (err) {
68
- debugLog("autoLoop", { phase: "save-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
69
- }
70
- }
71
-
72
- // ── Memory pressure monitoring (#3331) ──────────────────────────────────
73
- // Check heap usage every N iterations and trigger graceful shutdown before
74
- // the OS OOM killer sends SIGKILL. The threshold is 90% of the V8 heap
75
- // limit (--max-old-space-size or default ~1.5-4GB depending on platform).
76
- const MEMORY_CHECK_INTERVAL = 5; // check every 5 iterations
77
- const MEMORY_PRESSURE_THRESHOLD = 0.85; // 85% of heap limit
78
-
79
- function checkMemoryPressure(): { pressured: boolean; heapMB: number; limitMB: number; pct: number } {
80
- const mem = process.memoryUsage();
81
- // v8.getHeapStatistics() gives heap_size_limit but requires import
82
- // Use a conservative estimate: RSS > 3GB is danger zone on most systems
83
- const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
84
- const rssMB = Math.round(mem.rss / 1024 / 1024);
85
- // Try to get the actual V8 heap limit
86
- let limitMB = 4096; // conservative default
87
- try {
88
- const v8 = require("node:v8");
89
- const stats = v8.getHeapStatistics();
90
- limitMB = Math.round(stats.heap_size_limit / 1024 / 1024);
91
- } catch { limitMB = 4096; /* v8 stats unavailable — use conservative default */ }
92
- const pct = heapMB / limitMB;
93
- return { pressured: pct > MEMORY_PRESSURE_THRESHOLD, heapMB, limitMB, pct };
94
- }
95
32
 
96
33
  /**
97
34
  * Main auto-mode execution loop. Iterates: derive → dispatch → guards →
@@ -109,13 +46,7 @@ export async function autoLoop(
109
46
  ): Promise<void> {
110
47
  debugLog("autoLoop", { phase: "enter" });
111
48
  let iteration = 0;
112
- // Load persisted stuck state so counters survive session restarts (#3704)
113
- const persisted = loadStuckState(s.basePath);
114
- const loopState: LoopState = {
115
- recentUnits: persisted.recentUnits,
116
- stuckRecoveryAttempts: persisted.stuckRecoveryAttempts,
117
- consecutiveFinalizeTimeouts: 0,
118
- };
49
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
119
50
  let consecutiveErrors = 0;
120
51
  let consecutiveCooldowns = 0;
121
52
  const recentErrorMessages: string[] = [];
@@ -143,24 +74,6 @@ export async function autoLoop(
143
74
  break;
144
75
  }
145
76
 
146
- // ── Memory pressure check (#3331) ──
147
- // Graceful shutdown before OOM killer sends SIGKILL.
148
- if (iteration % MEMORY_CHECK_INTERVAL === 0) {
149
- const mem = checkMemoryPressure();
150
- debugLog("autoLoop", { phase: "memory-check", ...mem });
151
- if (mem.pressured) {
152
- logWarning("dispatch", `Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping auto-mode to prevent OOM kill`);
153
- await deps.stopAuto(
154
- ctx,
155
- pi,
156
- `Memory pressure: heap at ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%). ` +
157
- `Stopping gracefully to prevent OOM kill after ${iteration} iterations. ` +
158
- `Resume with /gsd auto to continue from where you left off.`,
159
- );
160
- break;
161
- }
162
- }
163
-
164
77
  if (!s.cmdCtx) {
165
78
  debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
166
79
  break;
@@ -294,7 +207,6 @@ export async function autoLoop(
294
207
  consecutiveCooldowns = 0;
295
208
  recentErrorMessages.length = 0;
296
209
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
297
- saveStuckState(s.basePath, loopState); // persist across session restarts (#3704)
298
210
  debugLog("autoLoop", { phase: "iteration-complete", iteration });
299
211
 
300
212
  if (reconcileResult.outcome === "milestone-complete") {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pre-dispatch observability checks for auto-mode units.
3
+ * Validates plan/summary file quality and builds repair instructions
4
+ * for the agent to fix gaps before proceeding with the unit.
5
+ */
6
+
7
+ import type { ExtensionContext } from "@gsd/pi-coding-agent";
8
+ import {
9
+ validatePlanBoundary,
10
+ validateExecuteBoundary,
11
+ validateCompleteBoundary,
12
+ formatValidationIssues,
13
+ } from "./observability-validator.js";
14
+ import type { ValidationIssue } from "./observability-validator.js";
15
+ import { parseUnitId } from "./unit-id.js";
16
+
17
+ export async function collectObservabilityWarnings(
18
+ ctx: ExtensionContext,
19
+ basePath: string,
20
+ unitType: string,
21
+ unitId: string,
22
+ ): Promise<ValidationIssue[]> {
23
+ // Hook units have custom artifacts — skip standard observability checks
24
+ if (unitType.startsWith("hook/")) return [];
25
+
26
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
27
+
28
+ if (!mid || !sid) return [];
29
+
30
+ let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
31
+
32
+ if (unitType === "plan-slice") {
33
+ issues = await validatePlanBoundary(basePath, mid, sid);
34
+ } else if (unitType === "execute-task" && tid) {
35
+ issues = await validateExecuteBoundary(basePath, mid, sid, tid);
36
+ } else if (unitType === "complete-slice") {
37
+ issues = await validateCompleteBoundary(basePath, mid, sid);
38
+ }
39
+
40
+ if (issues.length > 0) {
41
+ ctx.ui.notify(
42
+ `Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
43
+ "warning",
44
+ );
45
+ }
46
+
47
+ return issues;
48
+ }
49
+
50
+ export function buildObservabilityRepairBlock(issues: ValidationIssue[]): string {
51
+ if (issues.length === 0) return "";
52
+ const items = issues.map(issue => {
53
+ const fileName = issue.file.split("/").pop() || issue.file;
54
+ let line = `- **${fileName}**: ${issue.message}`;
55
+ if (issue.suggestion) line += ` → ${issue.suggestion}`;
56
+ return line;
57
+ });
58
+ return [
59
+ "",
60
+ "---",
61
+ "",
62
+ "## Pre-flight: Observability gaps to fix FIRST",
63
+ "",
64
+ "The following issues were detected in plan/summary files for this unit.",
65
+ "**Read each flagged file, apply the fix described, then proceed with the unit.**",
66
+ "",
67
+ ...items,
68
+ "",
69
+ "---",
70
+ "",
71
+ ].join("\n");
72
+ }
@@ -25,7 +25,6 @@ import {
25
25
  buildTaskFileName,
26
26
  } from "./paths.js";
27
27
  import { invalidateAllCaches } from "./cache.js";
28
- import { rebuildState } from "./doctor.js";
29
28
  import { parseUnitId } from "./unit-id.js";
30
29
  import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
31
30
  import {
@@ -368,12 +367,6 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
368
367
  }
369
368
  });
370
369
 
371
- // Keep the on-disk STATE.md aligned with the live derived state after
372
- // ordinary unit completion, before any worktree state is synced back.
373
- await runSafely("postUnit", "state-rebuild", async () => {
374
- await rebuildState(s.basePath);
375
- });
376
-
377
370
  // Sync worktree state back to project root (skipped for lightweight sidecars)
378
371
  if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
379
372
  await runSafely("postUnit", "worktree-sync", () => {
@@ -677,13 +677,9 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
677
677
  logWarning("session", `lock cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
678
678
  }
679
679
 
680
- // A transient provider-error pause intentionally leaves the paused badge
681
- // visible so the user still has a resumable auto-mode signal on screen.
682
- if (!s.paused) {
683
- ctx.ui.setStatus("gsd-auto", undefined);
684
- ctx.ui.setWidget("gsd-progress", undefined);
685
- ctx.ui.setFooter(undefined);
686
- }
680
+ ctx.ui.setStatus("gsd-auto", undefined);
681
+ ctx.ui.setWidget("gsd-progress", undefined);
682
+ ctx.ui.setFooter(undefined);
687
683
 
688
684
  // Restore CWD out of worktree back to original project root
689
685
  if (s.originalBasePath) {
@@ -795,22 +791,7 @@ export async function stopAuto(
795
791
  debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
796
792
  }
797
793
 
798
- // ── Step 5: Rebuild state while DB is still open (#3599) ──
799
- // rebuildState() calls deriveState() which needs the DB for authoritative
800
- // state. Previously this ran after closeDatabase(), forcing a filesystem
801
- // fallback that could disagree with the DB-backed dispatch decisions —
802
- // a split-brain where dispatch says "blocked" but STATE.md shows work.
803
- if (s.basePath) {
804
- try {
805
- await rebuildState(s.basePath);
806
- } catch (e) {
807
- debugLog("stop-rebuild-state-failed", {
808
- error: e instanceof Error ? e.message : String(e),
809
- });
810
- }
811
- }
812
-
813
- // ── Step 6: DB cleanup ──
794
+ // ── Step 5: DB cleanup ──
814
795
  if (isDbAvailable()) {
815
796
  try {
816
797
  const { closeDatabase } = await import("./gsd-db.js");
@@ -822,7 +803,7 @@ export async function stopAuto(
822
803
  }
823
804
  }
824
805
 
825
- // ── Step 7: Restore basePath and chdir ──
806
+ // ── Step 6: Restore basePath and chdir ──
826
807
  try {
827
808
  if (s.originalBasePath) {
828
809
  s.basePath = s.originalBasePath;
@@ -837,7 +818,7 @@ export async function stopAuto(
837
818
  debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
838
819
  }
839
820
 
840
- // ── Step 8: Ledger notification ──
821
+ // ── Step 7: Ledger notification ──
841
822
  try {
842
823
  const ledger = getLedger();
843
824
  if (ledger && ledger.units.length > 0) {
@@ -853,6 +834,17 @@ export async function stopAuto(
853
834
  debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
854
835
  }
855
836
 
837
+ // ── Step 8: Rebuild state ──
838
+ if (s.basePath) {
839
+ try {
840
+ await rebuildState(s.basePath);
841
+ } catch (e) {
842
+ debugLog("stop-rebuild-state-failed", {
843
+ error: e instanceof Error ? e.message : String(e),
844
+ });
845
+ }
846
+ }
847
+
856
848
  // ── Step 9: Cmux sidebar / event log ──
857
849
  try {
858
850
  clearCmuxSidebar(loadedPreferences);
@@ -1721,6 +1713,9 @@ export async function dispatchHookUnit(
1721
1713
  return true;
1722
1714
  }
1723
1715
 
1716
+ // Direct phase dispatch → auto-direct-dispatch.ts
1717
+ export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
1718
+
1724
1719
  // Re-export recovery functions for external consumers
1725
1720
  export {
1726
1721
  buildLoopRemediationSteps,
@@ -181,10 +181,14 @@ export function registerHooks(pi: ExtensionAPI): void {
181
181
  // Only gate-shaped ask_user_questions calls should block execution.
182
182
  // The gate stays pending until the user selects the approval option.
183
183
  if (event.toolName === "ask_user_questions") {
184
- const questions: any[] = (event.input as any)?.questions ?? [];
185
- const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
186
- if (typeof questionId === "string") {
187
- setPendingGate(questionId);
184
+ const milestoneId = getDiscussionMilestoneId(discussionBasePath);
185
+ const inDiscussion = milestoneId !== null || isQueuePhaseActive();
186
+ if (inDiscussion) {
187
+ const questions: any[] = (event.input as any)?.questions ?? [];
188
+ const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
189
+ if (typeof questionId === "string") {
190
+ setPendingGate(questionId);
191
+ }
188
192
  }
189
193
  }
190
194
 
@@ -282,6 +286,7 @@ export function registerHooks(pi: ExtensionAPI): void {
282
286
  if (event.toolName !== "ask_user_questions") return;
283
287
  const milestoneId = getDiscussionMilestoneId(process.cwd());
284
288
  const queueActive = isQueuePhaseActive();
289
+ if (!milestoneId && !queueActive) return;
285
290
 
286
291
  const details = event.details as any;
287
292
 
@@ -314,16 +319,13 @@ export function registerHooks(pi: ExtensionAPI): void {
314
319
  // Only unlock the gate if the user selected the first option (confirmation).
315
320
  // Cross-references against the question's defined options to reject free-form "Other" text.
316
321
  const answer = details.response?.answers?.[question.id];
317
- const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
318
322
  if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
319
- markDepthVerified(inferredMilestoneId);
320
- clearPendingGate();
323
+ markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
321
324
  }
322
325
  break;
323
326
  }
324
327
  }
325
328
 
326
- if (!milestoneId && !queueActive) return;
327
329
  if (!milestoneId) return;
328
330
 
329
331
  const basePath = process.cwd();
@@ -78,10 +78,6 @@ export function parseDoctorArgs(args: string) {
78
78
  return { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope };
79
79
  }
80
80
 
81
- export function isDoctorHealActionable(issue: { fixable: boolean; severity: string }): boolean {
82
- return issue.fixable && issue.severity !== "info";
83
- }
84
-
85
81
  export async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
86
82
  const { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope } = parseDoctorArgs(args);
87
83
  const scope = await selectDoctorScope(projectRoot(), requestedScope);
@@ -113,7 +109,7 @@ export async function handleDoctor(args: string, ctx: ExtensionCommandContext, p
113
109
  scope: effectiveScope,
114
110
  includeWarnings: true,
115
111
  });
116
- const actionable = unresolved.filter(isDoctorHealActionable);
112
+ const actionable = unresolved.filter(issue => issue.severity === "error");
117
113
  if (actionable.length === 0) {
118
114
  ctx.ui.notify("Doctor heal found nothing actionable to hand off to the LLM.", "info");
119
115
  return;
@@ -16,7 +16,7 @@
16
16
  import { readFileSync, existsSync } from "node:fs";
17
17
  import { join, resolve, sep } from "node:path";
18
18
  import type { StepDefinition } from "./definition-loader.js";
19
- import { readFrozenDefinition } from "./definition-io.js";
19
+ import { readFrozenDefinition } from "./custom-workflow-engine.js";
20
20
 
21
21
  /** Maximum characters per artifact to prevent context window blowout. */
22
22
  const MAX_CONTEXT_CHARS = 10_000;
@@ -22,6 +22,7 @@ import type {
22
22
  } from "./engine-types.js";
23
23
  import { readFileSync } from "node:fs";
24
24
  import { join } from "node:path";
25
+ import { parse } from "yaml";
25
26
  import {
26
27
  readGraph,
27
28
  writeGraph,
@@ -31,13 +32,16 @@ import {
31
32
  type WorkflowGraph,
32
33
  } from "./graph.js";
33
34
  import { injectContext } from "./context-injector.js";
34
- import type { StepDefinition } from "./definition-loader.js";
35
- import { readFrozenDefinition } from "./definition-io.js";
35
+ import type { WorkflowDefinition, StepDefinition } from "./definition-loader.js";
36
36
  import { parseUnitId } from "./unit-id.js";
37
37
  import { withFileLock } from "./file-lock.js";
38
38
 
39
- // Re-export for downstream consumers
40
- export { readFrozenDefinition } from "./definition-io.js";
39
+ /** Read and parse the frozen DEFINITION.yaml from a run directory. */
40
+ export function readFrozenDefinition(runDir: string): WorkflowDefinition {
41
+ const defPath = join(runDir, "DEFINITION.yaml");
42
+ const raw = readFileSync(defPath, "utf-8");
43
+ return parse(raw, { schema: "core" }) as WorkflowDefinition;
44
+ }
41
45
 
42
46
  export class CustomWorkflowEngine implements WorkflowEngine {
43
47
  readonly engineId = "custom";
@@ -0,0 +1,100 @@
1
+ import type { FSWatcher } from "chokidar";
2
+ import type { EventBus } from "@gsd/pi-coding-agent";
3
+ import { relative } from "node:path";
4
+
5
+ let watcher: FSWatcher | null = null;
6
+ let pending = new Map<string, ReturnType<typeof setTimeout>>();
7
+
8
+ const EVENT_MAP: Record<string, string> = {
9
+ "settings.json": "settings-changed",
10
+ "auth.json": "auth-changed",
11
+ "models.json": "models-changed",
12
+ };
13
+
14
+ const EXTENSIONS_DIR = "extensions";
15
+
16
+ const IGNORED_PATTERNS = [
17
+ "**/sessions/**",
18
+ "**/*.tmp",
19
+ "**/*.swp",
20
+ "**/*~",
21
+ "**/.DS_Store",
22
+ ];
23
+
24
+ const DEBOUNCE_MS = 300;
25
+
26
+ /**
27
+ * Start watching `agentDir` (e.g. `~/.gsd/agent/`) for config changes.
28
+ * Emits events on the supplied EventBus when watched files are modified.
29
+ */
30
+ export async function startFileWatcher(
31
+ agentDir: string,
32
+ eventBus: EventBus,
33
+ ): Promise<void> {
34
+ if (watcher) {
35
+ await watcher.close();
36
+ }
37
+
38
+ const { watch } = await import("chokidar");
39
+
40
+ pending = new Map<string, ReturnType<typeof setTimeout>>();
41
+
42
+ function debounceEmit(event: string): void {
43
+ const existing = pending.get(event);
44
+ if (existing) clearTimeout(existing);
45
+ pending.set(
46
+ event,
47
+ setTimeout(() => {
48
+ pending.delete(event);
49
+ eventBus.emit(event, { timestamp: Date.now() });
50
+ }, DEBOUNCE_MS),
51
+ );
52
+ }
53
+
54
+ function resolveEvent(filePath: string): string | null {
55
+ const rel = relative(agentDir, filePath);
56
+ if (rel.startsWith("..")) return null;
57
+
58
+ // Check direct file matches
59
+ for (const [file, event] of Object.entries(EVENT_MAP)) {
60
+ if (rel === file) return event;
61
+ }
62
+
63
+ // Check extensions directory
64
+ if (rel.startsWith(EXTENSIONS_DIR + "/") || rel === EXTENSIONS_DIR) {
65
+ return "extensions-changed";
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ watcher = watch(agentDir, {
72
+ ignoreInitial: true,
73
+ depth: 2,
74
+ ignored: IGNORED_PATTERNS,
75
+ });
76
+
77
+ for (const eventType of ["add", "change", "unlink"] as const) {
78
+ watcher.on(eventType, (filePath: string) => {
79
+ const event = resolveEvent(filePath);
80
+ if (event) debounceEmit(event);
81
+ });
82
+ }
83
+
84
+ // Wait for watcher to be ready
85
+ await new Promise<void>((resolve) => {
86
+ watcher!.on("ready", resolve);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Stop the file watcher and clean up resources.
92
+ */
93
+ export async function stopFileWatcher(): Promise<void> {
94
+ for (const timer of pending.values()) clearTimeout(timer);
95
+ pending.clear();
96
+ if (watcher) {
97
+ await watcher.close();
98
+ watcher = null;
99
+ }
100
+ }
@@ -163,29 +163,6 @@ function openRawDb(path: string): unknown {
163
163
 
164
164
  const SCHEMA_VERSION = 14;
165
165
 
166
- function indexExists(db: DbAdapter, name: string): boolean {
167
- return !!db.prepare(
168
- "SELECT 1 as present FROM sqlite_master WHERE type = 'index' AND name = ?",
169
- ).get(name);
170
- }
171
-
172
- function dedupeVerificationEvidenceRows(db: DbAdapter): void {
173
- db.exec(`
174
- DELETE FROM verification_evidence
175
- WHERE rowid NOT IN (
176
- SELECT MIN(rowid)
177
- FROM verification_evidence
178
- GROUP BY task_id, slice_id, milestone_id, command, verdict
179
- )
180
- `);
181
- }
182
-
183
- function ensureVerificationEvidenceDedupIndex(db: DbAdapter): void {
184
- if (indexExists(db, "idx_verification_evidence_dedup")) return;
185
- dedupeVerificationEvidenceRows(db);
186
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
187
- }
188
-
189
166
  function initSchema(db: DbAdapter, fileBacked: boolean): void {
190
167
  if (fileBacked) db.exec("PRAGMA journal_mode=WAL");
191
168
  if (fileBacked) db.exec("PRAGMA busy_timeout = 5000");
@@ -433,7 +410,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
433
410
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestones_status ON milestones(status)");
434
411
  db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)");
435
412
  db.exec("CREATE INDEX IF NOT EXISTS idx_verification_evidence_task ON verification_evidence(milestone_id, slice_id, task_id)");
436
- ensureVerificationEvidenceDedupIndex(db);
413
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
437
414
 
438
415
  // v14 index — slice dependency lookups
439
416
  db.exec("CREATE INDEX IF NOT EXISTS idx_slice_deps_target ON slice_dependencies(milestone_id, depends_on_slice_id)");
@@ -766,7 +743,7 @@ function migrateSchema(db: DbAdapter): void {
766
743
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestones_status ON milestones(status)");
767
744
  db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)");
768
745
  db.exec("CREATE INDEX IF NOT EXISTS idx_verification_evidence_task ON verification_evidence(milestone_id, slice_id, task_id)");
769
- ensureVerificationEvidenceDedupIndex(db);
746
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
770
747
  db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
771
748
  ":version": 13,
772
749
  ":applied_at": new Date().toISOString(),
@@ -1565,30 +1542,6 @@ export interface TaskRow {
1565
1542
  }
1566
1543
 
1567
1544
  function rowToTask(row: Record<string, unknown>): TaskRow {
1568
- const parseTaskArray = (value: unknown): string[] => {
1569
- if (Array.isArray(value)) {
1570
- return value.filter((entry): entry is string => typeof entry === "string");
1571
- }
1572
- if (typeof value !== "string") return [];
1573
-
1574
- const trimmed = value.trim();
1575
- if (!trimmed) return [];
1576
-
1577
- try {
1578
- const parsed = JSON.parse(trimmed);
1579
- if (Array.isArray(parsed)) {
1580
- return parsed.filter((entry): entry is string => typeof entry === "string");
1581
- }
1582
- if (typeof parsed === "string" && parsed.trim()) {
1583
- return [parsed.trim()];
1584
- }
1585
- } catch {
1586
- // Older/corrupt DB rows may contain raw comma-separated paths instead of JSON arrays.
1587
- }
1588
-
1589
- return trimmed.split(",").map((entry) => entry.trim()).filter(Boolean);
1590
- };
1591
-
1592
1545
  return {
1593
1546
  milestone_id: row["milestone_id"] as string,
1594
1547
  slice_id: row["slice_id"] as string,
@@ -1608,10 +1561,10 @@ function rowToTask(row: Record<string, unknown>): TaskRow {
1608
1561
  full_summary_md: row["full_summary_md"] as string,
1609
1562
  description: (row["description"] as string) ?? "",
1610
1563
  estimate: (row["estimate"] as string) ?? "",
1611
- files: parseTaskArray(row["files"]),
1564
+ files: JSON.parse((row["files"] as string) || "[]"),
1612
1565
  verify: (row["verify"] as string) ?? "",
1613
- inputs: parseTaskArray(row["inputs"]),
1614
- expected_output: parseTaskArray(row["expected_output"]),
1566
+ inputs: JSON.parse((row["inputs"] as string) || "[]"),
1567
+ expected_output: JSON.parse((row["expected_output"] as string) || "[]"),
1615
1568
  observability_impact: (row["observability_impact"] as string) ?? "",
1616
1569
  full_plan_md: (row["full_plan_md"] as string) ?? "",
1617
1570
  sequence: (row["sequence"] as number) ?? 0,
@@ -49,8 +49,6 @@ export const PROVIDER_REGISTRY: ProviderInfo[] = [
49
49
  { id: "custom-openai", label: "Custom (OpenAI-compat)", category: "llm", envVar: "CUSTOM_OPENAI_API_KEY" },
50
50
  { id: "cerebras", label: "Cerebras", category: "llm", envVar: "CEREBRAS_API_KEY" },
51
51
  { id: "azure-openai-responses", label: "Azure OpenAI", category: "llm", envVar: "AZURE_OPENAI_API_KEY" },
52
- { id: "alibaba-coding-plan", label: "Alibaba Coding Plan", category: "llm", envVar: "ALIBABA_API_KEY", dashboardUrl: "bailian.console.aliyun.com" },
53
- { id: "alibaba-dashscope", label: "Alibaba DashScope", category: "llm", envVar: "DASHSCOPE_API_KEY", dashboardUrl: "dashscope.console.aliyun.com" },
54
52
 
55
53
  // Tool Keys
56
54
  { id: "context7", label: "Context7 Docs", category: "tool", envVar: "CONTEXT7_API_KEY", dashboardUrl: "context7.com/dashboard" },
@@ -17,6 +17,7 @@ import type {
17
17
  SkillResolutionReport,
18
18
  } from "./preferences-types.js";
19
19
  import { validatePreferences } from "./preferences-validation.js";
20
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
20
21
 
21
22
  // Re-export types so existing consumers of ./preferences-skills.js keep working
22
23
  export type { GSDSkillRule, SkillDiscoveryMode, SkillResolution, SkillResolutionReport } from "./preferences-types.js";
@@ -142,5 +143,38 @@ export function resolveAllSkillReferences(preferences: GSDPreferences, cwd: stri
142
143
  return { resolutions, warnings };
143
144
  }
144
145
 
145
- // resolveSkillDiscoveryMode and resolveSkillStalenessDays moved to
146
- // preferences.ts to break circular dependency (they need loadEffectiveGSDPreferences).
146
+ /**
147
+ * Format a skill reference for the system prompt.
148
+ * If resolved, shows the path so the agent knows exactly where to read.
149
+ * If unresolved, marks it clearly.
150
+ */
151
+ export function formatSkillRef(ref: string, resolutions: Map<string, SkillResolution>): string {
152
+ const resolution = resolutions.get(ref);
153
+ if (!resolution || resolution.method === "unresolved") {
154
+ return `${ref} (⚠ not found — check skill name or path)`;
155
+ }
156
+ // For absolute paths where SKILL.md is just appended, don't clutter the output
157
+ if (resolution.method === "absolute-path" || resolution.method === "absolute-dir") {
158
+ return ref;
159
+ }
160
+ // For bare names resolved from skill directories, show the resolved path
161
+ return `${ref} → \`${resolution.resolvedPath}\``;
162
+ }
163
+
164
+ /**
165
+ * Resolve the skill discovery mode from effective preferences.
166
+ * Defaults to "suggest" -- skills are identified during research but not installed automatically.
167
+ */
168
+ export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
169
+ const prefs = loadEffectiveGSDPreferences();
170
+ return prefs?.preferences.skill_discovery ?? "suggest";
171
+ }
172
+
173
+ /**
174
+ * Resolve the skill staleness threshold in days.
175
+ * Returns 0 if disabled, default 60 if not configured.
176
+ */
177
+ export function resolveSkillStalenessDays(): number {
178
+ const prefs = loadEffectiveGSDPreferences();
179
+ return prefs?.preferences.skill_staleness_days ?? 60;
180
+ }