gsd-pi 2.72.0-dev.3118184 → 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 (183) 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-post-unit.js +0 -6
  6. package/dist/resources/extensions/gsd/auto.js +19 -25
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -9
  8. package/dist/resources/extensions/gsd/commands-handlers.js +1 -4
  9. package/dist/resources/extensions/gsd/context-injector.js +1 -1
  10. package/dist/resources/extensions/gsd/custom-workflow-engine.js +7 -3
  11. package/dist/resources/extensions/gsd/gsd-db.js +5 -47
  12. package/dist/resources/extensions/gsd/key-manager.js +0 -2
  13. package/dist/resources/extensions/gsd/preferences-skills.js +34 -2
  14. package/dist/resources/extensions/gsd/preferences-types.js +0 -15
  15. package/dist/resources/extensions/gsd/preferences.js +3 -16
  16. package/dist/resources/extensions/gsd/prompt-loader.js +1 -4
  17. package/dist/resources/extensions/gsd/state.js +1 -21
  18. package/dist/resources/extensions/gsd/write-intercept.js +1 -10
  19. package/dist/resources/extensions/ollama/index.js +5 -4
  20. package/dist/resources/extensions/ollama/ollama-client.js +6 -35
  21. package/dist/resources/extensions/ollama/ollama-discovery.js +6 -32
  22. package/dist/web/standalone/.next/BUILD_ID +1 -1
  23. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  24. package/dist/web/standalone/.next/build-manifest.json +2 -2
  25. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  37. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  50. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  56. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  66. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  71. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  74. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  81. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  82. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  83. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +3 -3
  84. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/index.html +1 -1
  89. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/page.js +2 -2
  96. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  98. package/dist/web/standalone/.next/server/chunks/2331.js +16 -16
  99. package/dist/web/standalone/.next/server/chunks/4741.js +12 -12
  100. package/dist/web/standalone/.next/server/chunks/5822.js +2 -2
  101. package/dist/web/standalone/.next/server/chunks/63.js +8 -8
  102. package/dist/web/standalone/.next/server/chunks/6897.js +3 -3
  103. package/dist/web/standalone/.next/server/functions-config-manifest.json +9 -0
  104. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/middleware-manifest.json +2 -29
  106. package/dist/web/standalone/.next/server/middleware.js +12 -4
  107. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  108. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  109. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  110. package/dist/web/standalone/.next/server/webpack-runtime.js +1 -1
  111. package/package.json +1 -1
  112. package/packages/pi-ai/dist/env-api-keys.js +0 -1
  113. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  114. package/packages/pi-ai/dist/models.custom.d.ts +0 -105
  115. package/packages/pi-ai/dist/models.custom.d.ts.map +1 -1
  116. package/packages/pi-ai/dist/models.custom.js +0 -97
  117. package/packages/pi-ai/dist/models.custom.js.map +1 -1
  118. package/packages/pi-ai/dist/models.generated.d.ts +140 -648
  119. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  120. package/packages/pi-ai/dist/models.generated.js +364 -861
  121. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  122. package/packages/pi-ai/dist/models.test.js +0 -105
  123. package/packages/pi-ai/dist/models.test.js.map +1 -1
  124. package/packages/pi-ai/dist/types.d.ts +1 -1
  125. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  126. package/packages/pi-ai/dist/types.js.map +1 -1
  127. package/packages/pi-ai/src/env-api-keys.ts +0 -1
  128. package/packages/pi-ai/src/models.custom.ts +0 -98
  129. package/packages/pi-ai/src/models.generated.ts +364 -861
  130. package/packages/pi-ai/src/models.test.ts +0 -135
  131. package/packages/pi-ai/src/types.ts +0 -1
  132. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/model-resolver.js +0 -1
  134. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  135. package/packages/pi-coding-agent/src/core/model-resolver.ts +0 -1
  136. package/src/resources/extensions/async-jobs/await-tool.test.ts +7 -40
  137. package/src/resources/extensions/async-jobs/await-tool.ts +4 -7
  138. package/src/resources/extensions/async-jobs/job-manager.ts +3 -33
  139. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +26 -27
  140. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +2 -20
  141. package/src/resources/extensions/gsd/auto/loop.ts +1 -89
  142. package/src/resources/extensions/gsd/auto-post-unit.ts +0 -7
  143. package/src/resources/extensions/gsd/auto.ts +20 -25
  144. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +10 -8
  145. package/src/resources/extensions/gsd/commands-handlers.ts +1 -5
  146. package/src/resources/extensions/gsd/context-injector.ts +1 -1
  147. package/src/resources/extensions/gsd/custom-workflow-engine.ts +8 -4
  148. package/src/resources/extensions/gsd/gsd-db.ts +5 -52
  149. package/src/resources/extensions/gsd/key-manager.ts +0 -2
  150. package/src/resources/extensions/gsd/preferences-skills.ts +36 -2
  151. package/src/resources/extensions/gsd/preferences-types.ts +0 -16
  152. package/src/resources/extensions/gsd/preferences.ts +6 -19
  153. package/src/resources/extensions/gsd/prompt-loader.ts +1 -6
  154. package/src/resources/extensions/gsd/state.ts +0 -20
  155. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +0 -74
  156. package/src/resources/extensions/gsd/tests/key-manager.test.ts +0 -63
  157. package/src/resources/extensions/gsd/tests/preferences.test.ts +0 -53
  158. package/src/resources/extensions/gsd/write-intercept.ts +1 -10
  159. package/src/resources/extensions/ollama/index.ts +5 -4
  160. package/src/resources/extensions/ollama/ollama-client.ts +6 -35
  161. package/src/resources/extensions/ollama/ollama-discovery.ts +6 -37
  162. package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +0 -54
  163. package/dist/resources/extensions/gsd/definition-io.js +0 -15
  164. package/dist/web/standalone/.next/server/edge-runtime-webpack.js +0 -2
  165. package/packages/pi-ai/dist/models.generated.test.d.ts +0 -2
  166. package/packages/pi-ai/dist/models.generated.test.d.ts.map +0 -1
  167. package/packages/pi-ai/dist/models.generated.test.js +0 -334
  168. package/packages/pi-ai/dist/models.generated.test.js.map +0 -1
  169. package/packages/pi-ai/src/models.generated.test.ts +0 -373
  170. package/src/resources/extensions/gsd/definition-io.ts +0 -18
  171. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +0 -27
  172. package/src/resources/extensions/gsd/tests/block-db-writes.test.ts +0 -63
  173. package/src/resources/extensions/gsd/tests/definition-io.test.ts +0 -57
  174. package/src/resources/extensions/gsd/tests/doctor-heal-fixable-warnings.test.ts +0 -14
  175. package/src/resources/extensions/gsd/tests/false-degraded-mode-warning.test.ts +0 -104
  176. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +0 -54
  177. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +0 -34
  178. package/src/resources/extensions/gsd/tests/preferences-formatting.test.ts +0 -87
  179. package/src/resources/extensions/gsd/tests/prompt-loader-working-directory.test.ts +0 -19
  180. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +0 -97
  181. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +0 -41
  182. /package/dist/web/standalone/.next/static/{NzO79SOz9jHX-VY5-0t2O → vr6Pbde48w4rMUplqDdh_}/_buildManifest.js +0 -0
  183. /package/dist/web/standalone/.next/static/{NzO79SOz9jHX-VY5-0t2O → vr6Pbde48w4rMUplqDdh_}/_ssgManifest.js +0 -0
@@ -54,14 +54,11 @@ export function createAwaitTool(getManager) {
54
54
  };
55
55
  }
56
56
  }
57
- // Suppress follow-up notifications for all watched jobs upfront.
58
- // suppressFollowUp() cancels the pending delivery timer (if any), which
59
- // handles both the within-turn case (job completes while we await) and
60
- // the cross-turn case (job already completed before await_job was called).
61
- // Previously this only set j.awaited = true, which missed the cross-turn
62
- // case because the queueMicrotask had already fired (#3787).
57
+ // Mark all watched jobs as awaited upfront so the onJobComplete
58
+ // callback (which fires synchronously in the promise .then()) knows
59
+ // to suppress the follow-up message.
63
60
  for (const j of watched)
64
- manager.suppressFollowUp(j.id);
61
+ j.awaited = true;
65
62
  // If all watched jobs are already done, return immediately
66
63
  const running = watched.filter((j) => j.status === "running");
67
64
  if (running.length === 0) {
@@ -118,38 +118,13 @@ export class AsyncJobManager {
118
118
  }
119
119
  }
120
120
  // ── Private ────────────────────────────────────────────────────────────
121
- /**
122
- * Suppress follow-up notification for a job — cancels any pending delivery
123
- * timer and marks the job as awaited. Safe to call at any time, including
124
- * before or after the job completes (#3787).
125
- */
126
- suppressFollowUp(id) {
127
- const job = this.jobs.get(id);
128
- if (!job)
129
- return;
130
- job.awaited = true;
131
- if (job.deliveryTimer !== undefined) {
132
- clearTimeout(job.deliveryTimer);
133
- job.deliveryTimer = undefined;
134
- }
135
- }
136
121
  deliverResult(job) {
137
122
  if (!this.onJobComplete)
138
123
  return;
139
- // Use setTimeout(0) instead of queueMicrotask so the handle is cancellable.
140
- // suppressFollowUp() can clear this timer even when await_job is called in
141
- // a later LLM turn (after the job already completed). queueMicrotask ran
142
- // immediately and could not be cancelled (#2762, #3787).
124
+ // Defer delivery by one microtask so await_job's .then() chain runs first
125
+ // and can set job.awaited = true before onJobComplete checks it (#2762).
143
126
  const cb = this.onJobComplete;
144
- job.deliveryTimer = setTimeout(() => {
145
- job.deliveryTimer = undefined;
146
- if (!job.awaited)
147
- cb(job);
148
- }, 0);
149
- // Allow process to exit even if timer is pending
150
- if (typeof job.deliveryTimer === "object" && "unref" in job.deliveryTimer) {
151
- job.deliveryTimer.unref();
152
- }
127
+ queueMicrotask(() => cb(job));
153
128
  }
154
129
  scheduleEviction(id) {
155
130
  const existing = this.evictionTimers.get(id);
@@ -405,25 +405,32 @@ export function makeAbortedMessage(model, lastTextContent) {
405
405
  /**
406
406
  * Resolve the Claude Code permission mode for the current run.
407
407
  *
408
- * GSD subagents run underneath a host Claude Code session the user has
409
- * already consented to, and their work (edits, shell inspection, MCP calls)
410
- * spans the full workflow toolset. Defaulting the inner SDK to
411
- * `bypassPermissions` avoids per-tool approval prompts that offer no
412
- * meaningful safety beyond what the host session and the subagent prompts
413
- * already enforce. `GSD_CLAUDE_CODE_PERMISSION_MODE` lets security-conscious
414
- * users opt into a stricter mode (`acceptEdits`, `default`, `plan`).
408
+ * - Auto-mode / headless runs bypass permissions so tool calls don't block
409
+ * on prompts the user isn't watching.
410
+ * - Interactive runs default to `acceptEdits` so file/bash writes still
411
+ * land quickly but the SDK retains a permission gate.
412
+ * - `GSD_CLAUDE_CODE_PERMISSION_MODE` forces a specific mode when set.
415
413
  *
416
- * Tradeoff: bypass means a prompt-injection payload read from an untrusted
417
- * file could trigger tool calls without a second gate. Accepted for GSD
418
- * because the workflow is explicit user intent and the alternative
419
- * (#4099) is continuous approval fatigue that blocks real work.
414
+ * Cross-extension coupling is kept minimal by dynamically importing
415
+ * `isAutoActive` and falling back to the bypass default if the import
416
+ * fails (e.g. in unit tests that load stream-adapter in isolation).
420
417
  */
421
418
  export async function resolveClaudePermissionMode(env = process.env) {
422
419
  const override = env.GSD_CLAUDE_CODE_PERMISSION_MODE?.trim();
423
420
  if (override === "bypassPermissions" || override === "acceptEdits" || override === "default" || override === "plan") {
424
421
  return override;
425
422
  }
426
- return "bypassPermissions";
423
+ try {
424
+ const autoMod = (await import("../gsd/auto.js"));
425
+ if (typeof autoMod.isAutoActive === "function" && autoMod.isAutoActive()) {
426
+ return "bypassPermissions";
427
+ }
428
+ return "acceptEdits";
429
+ }
430
+ catch {
431
+ // auto.ts unavailable (tests, non-GSD contexts) — stay permissive.
432
+ return "bypassPermissions";
433
+ }
427
434
  }
428
435
  /**
429
436
  * Build the options object passed to the Claude Agent SDK's `query()` call.
@@ -440,21 +447,13 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
440
447
  const mcpServers = buildWorkflowMcpServers();
441
448
  const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
442
449
  const disallowedTools = ["AskUserQuestion"];
443
- // Pre-authorize the safe built-ins and every registered workflow MCP
444
- // server's tools. `acceptEdits` mode (the interactive default) only
445
- // auto-approves file edits Read/Glob/Grep, basic shell inspection, and
446
- // every `mcp__gsd-workflow__*` call still surface as "This command
447
- // requires approval" and block GSD actions (#4099).
448
- const allowedTools = [
449
- "Read",
450
- "Write",
451
- "Edit",
452
- "Glob",
453
- "Grep",
454
- "Bash(ls:*)",
455
- "Bash(pwd)",
456
- ...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
457
- ];
450
+ // Pre-authorize every registered workflow MCP server's tools. Without this,
451
+ // `acceptEdits` mode (the interactive default) auto-approves built-in
452
+ // Edit/Write/Bash but still gates MCP calls like `mcp__gsd-workflow__*`,
453
+ // surfacing "This command requires approval" on every GSD action (#4099).
454
+ const allowedTools = mcpServers
455
+ ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`)
456
+ : [];
458
457
  return {
459
458
  pathToClaudeCodeExecutable: getClaudePath(),
460
459
  model: modelId,
@@ -13,69 +13,6 @@ import { runPreDispatch, runDispatch, runGuards, runUnitPhase, runFinalize, } fr
13
13
  import { debugLog } from "../debug-logger.js";
14
14
  import { isInfrastructureError, isTransientCooldownError, getCooldownRetryAfterMs, COOLDOWN_FALLBACK_WAIT_MS, MAX_COOLDOWN_RETRIES } from "./infra-errors.js";
15
15
  import { resolveEngine } from "../engine-resolver.js";
16
- import { logWarning } from "../workflow-logger.js";
17
- import { gsdRoot } from "../paths.js";
18
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
19
- import { join } from "node:path";
20
- // ── Stuck detection persistence (#3704) ──────────────────────────────────
21
- // Persist stuck detection state to disk so it survives session restarts.
22
- // Without this, restarting auto-mode resets all counters, allowing the
23
- // same blocked unit to burn a full retry budget each session.
24
- function stuckStatePath(basePath) {
25
- return join(gsdRoot(basePath), "runtime", "stuck-state.json");
26
- }
27
- function loadStuckState(basePath) {
28
- try {
29
- const data = JSON.parse(readFileSync(stuckStatePath(basePath), "utf-8"));
30
- return {
31
- recentUnits: Array.isArray(data.recentUnits) ? data.recentUnits : [],
32
- stuckRecoveryAttempts: typeof data.stuckRecoveryAttempts === "number" ? data.stuckRecoveryAttempts : 0,
33
- };
34
- }
35
- catch (err) {
36
- debugLog("autoLoop", { phase: "load-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
37
- return { recentUnits: [], stuckRecoveryAttempts: 0 };
38
- }
39
- }
40
- function saveStuckState(basePath, state) {
41
- try {
42
- const filePath = stuckStatePath(basePath);
43
- mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
44
- writeFileSync(filePath, JSON.stringify({
45
- recentUnits: state.recentUnits.slice(-20), // keep last 20 entries
46
- stuckRecoveryAttempts: state.stuckRecoveryAttempts,
47
- updatedAt: new Date().toISOString(),
48
- }) + "\n");
49
- }
50
- catch (err) {
51
- debugLog("autoLoop", { phase: "save-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
52
- }
53
- }
54
- // ── Memory pressure monitoring (#3331) ──────────────────────────────────
55
- // Check heap usage every N iterations and trigger graceful shutdown before
56
- // the OS OOM killer sends SIGKILL. The threshold is 90% of the V8 heap
57
- // limit (--max-old-space-size or default ~1.5-4GB depending on platform).
58
- const MEMORY_CHECK_INTERVAL = 5; // check every 5 iterations
59
- const MEMORY_PRESSURE_THRESHOLD = 0.85; // 85% of heap limit
60
- function checkMemoryPressure() {
61
- const mem = process.memoryUsage();
62
- // v8.getHeapStatistics() gives heap_size_limit but requires import
63
- // Use a conservative estimate: RSS > 3GB is danger zone on most systems
64
- const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
65
- const rssMB = Math.round(mem.rss / 1024 / 1024);
66
- // Try to get the actual V8 heap limit
67
- let limitMB = 4096; // conservative default
68
- try {
69
- const v8 = require("node:v8");
70
- const stats = v8.getHeapStatistics();
71
- limitMB = Math.round(stats.heap_size_limit / 1024 / 1024);
72
- }
73
- catch {
74
- limitMB = 4096; /* v8 stats unavailable — use conservative default */
75
- }
76
- const pct = heapMB / limitMB;
77
- return { pressured: pct > MEMORY_PRESSURE_THRESHOLD, heapMB, limitMB, pct };
78
- }
79
16
  /**
80
17
  * Main auto-mode execution loop. Iterates: derive → dispatch → guards →
81
18
  * runUnit → finalize → repeat. Exits when s.active becomes false or a
@@ -87,13 +24,7 @@ function checkMemoryPressure() {
87
24
  export async function autoLoop(ctx, pi, s, deps) {
88
25
  debugLog("autoLoop", { phase: "enter" });
89
26
  let iteration = 0;
90
- // Load persisted stuck state so counters survive session restarts (#3704)
91
- const persisted = loadStuckState(s.basePath);
92
- const loopState = {
93
- recentUnits: persisted.recentUnits,
94
- stuckRecoveryAttempts: persisted.stuckRecoveryAttempts,
95
- consecutiveFinalizeTimeouts: 0,
96
- };
27
+ const loopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
97
28
  let consecutiveErrors = 0;
98
29
  let consecutiveCooldowns = 0;
99
30
  const recentErrorMessages = [];
@@ -113,19 +44,6 @@ export async function autoLoop(ctx, pi, s, deps) {
113
44
  await deps.stopAuto(ctx, pi, `Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`);
114
45
  break;
115
46
  }
116
- // ── Memory pressure check (#3331) ──
117
- // Graceful shutdown before OOM killer sends SIGKILL.
118
- if (iteration % MEMORY_CHECK_INTERVAL === 0) {
119
- const mem = checkMemoryPressure();
120
- debugLog("autoLoop", { phase: "memory-check", ...mem });
121
- if (mem.pressured) {
122
- logWarning("dispatch", `Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping auto-mode to prevent OOM kill`);
123
- await deps.stopAuto(ctx, pi, `Memory pressure: heap at ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%). ` +
124
- `Stopping gracefully to prevent OOM kill after ${iteration} iterations. ` +
125
- `Resume with /gsd auto to continue from where you left off.`);
126
- break;
127
- }
128
- }
129
47
  if (!s.cmdCtx) {
130
48
  debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
131
49
  break;
@@ -244,7 +162,6 @@ export async function autoLoop(ctx, pi, s, deps) {
244
162
  consecutiveCooldowns = 0;
245
163
  recentErrorMessages.length = 0;
246
164
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
247
- saveStuckState(s.basePath, loopState); // persist across session restarts (#3704)
248
165
  debugLog("autoLoop", { phase: "iteration-complete", iteration });
249
166
  if (reconcileResult.outcome === "milestone-complete") {
250
167
  await deps.stopAuto(ctx, pi, "Workflow complete");
@@ -16,7 +16,6 @@ import { loadFile, parseSummary, resolveAllOverrides } from "./files.js";
16
16
  import { loadPrompt } from "./prompt-loader.js";
17
17
  import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveMilestoneFile, resolveTasksDir, buildTaskFileName, } from "./paths.js";
18
18
  import { invalidateAllCaches } from "./cache.js";
19
- import { rebuildState } from "./doctor.js";
20
19
  import { parseUnitId } from "./unit-id.js";
21
20
  import { closeoutUnit } from "./auto-unit-closeout.js";
22
21
  import { autoCommitCurrentBranch, } from "./worktree.js";
@@ -289,11 +288,6 @@ export async function postUnitPreVerification(pctx, opts) {
289
288
  debugLog("postUnit", { phase: "browser-teardown", status: "closed" });
290
289
  }
291
290
  });
292
- // Keep the on-disk STATE.md aligned with the live derived state after
293
- // ordinary unit completion, before any worktree state is synced back.
294
- await runSafely("postUnit", "state-rebuild", async () => {
295
- await rebuildState(s.basePath);
296
- });
297
291
  // Sync worktree state back to project root (skipped for lightweight sidecars)
298
292
  if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
299
293
  await runSafely("postUnit", "worktree-sync", () => {
@@ -425,13 +425,9 @@ function cleanupAfterLoopExit(ctx) {
425
425
  /* best-effort — mirror stopAuto cleanup */
426
426
  logWarning("session", `lock cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
427
427
  }
428
- // A transient provider-error pause intentionally leaves the paused badge
429
- // visible so the user still has a resumable auto-mode signal on screen.
430
- if (!s.paused) {
431
- ctx.ui.setStatus("gsd-auto", undefined);
432
- ctx.ui.setWidget("gsd-progress", undefined);
433
- ctx.ui.setFooter(undefined);
434
- }
428
+ ctx.ui.setStatus("gsd-auto", undefined);
429
+ ctx.ui.setWidget("gsd-progress", undefined);
430
+ ctx.ui.setFooter(undefined);
435
431
  // Restore CWD out of worktree back to original project root
436
432
  if (s.originalBasePath) {
437
433
  s.basePath = s.originalBasePath;
@@ -533,22 +529,7 @@ export async function stopAuto(ctx, pi, reason) {
533
529
  catch (e) {
534
530
  debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
535
531
  }
536
- // ── Step 5: Rebuild state while DB is still open (#3599) ──
537
- // rebuildState() calls deriveState() which needs the DB for authoritative
538
- // state. Previously this ran after closeDatabase(), forcing a filesystem
539
- // fallback that could disagree with the DB-backed dispatch decisions —
540
- // a split-brain where dispatch says "blocked" but STATE.md shows work.
541
- if (s.basePath) {
542
- try {
543
- await rebuildState(s.basePath);
544
- }
545
- catch (e) {
546
- debugLog("stop-rebuild-state-failed", {
547
- error: e instanceof Error ? e.message : String(e),
548
- });
549
- }
550
- }
551
- // ── Step 6: DB cleanup ──
532
+ // ── Step 5: DB cleanup ──
552
533
  if (isDbAvailable()) {
553
534
  try {
554
535
  const { closeDatabase } = await import("./gsd-db.js");
@@ -560,7 +541,7 @@ export async function stopAuto(ctx, pi, reason) {
560
541
  });
561
542
  }
562
543
  }
563
- // ── Step 7: Restore basePath and chdir ──
544
+ // ── Step 6: Restore basePath and chdir ──
564
545
  try {
565
546
  if (s.originalBasePath) {
566
547
  s.basePath = s.originalBasePath;
@@ -576,7 +557,7 @@ export async function stopAuto(ctx, pi, reason) {
576
557
  catch (e) {
577
558
  debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
578
559
  }
579
- // ── Step 8: Ledger notification ──
560
+ // ── Step 7: Ledger notification ──
580
561
  try {
581
562
  const ledger = getLedger();
582
563
  if (ledger && ledger.units.length > 0) {
@@ -590,6 +571,17 @@ export async function stopAuto(ctx, pi, reason) {
590
571
  catch (e) {
591
572
  debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
592
573
  }
574
+ // ── Step 8: Rebuild state ──
575
+ if (s.basePath) {
576
+ try {
577
+ await rebuildState(s.basePath);
578
+ }
579
+ catch (e) {
580
+ debugLog("stop-rebuild-state-failed", {
581
+ error: e instanceof Error ? e.message : String(e),
582
+ });
583
+ }
584
+ }
593
585
  // ── Step 9: Cmux sidebar / event log ──
594
586
  try {
595
587
  clearCmuxSidebar(loadedPreferences);
@@ -1302,6 +1294,8 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
1302
1294
  pi.sendMessage({ customType: "gsd-auto", content: hookPrompt, display: true }, { triggerTurn: true });
1303
1295
  return true;
1304
1296
  }
1297
+ // Direct phase dispatch → auto-direct-dispatch.ts
1298
+ export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
1305
1299
  // Re-export recovery functions for external consumers
1306
1300
  export { buildLoopRemediationSteps, } from "./auto-recovery.js";
1307
1301
  export { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
@@ -172,10 +172,14 @@ export function registerHooks(pi) {
172
172
  // Only gate-shaped ask_user_questions calls should block execution.
173
173
  // The gate stays pending until the user selects the approval option.
174
174
  if (event.toolName === "ask_user_questions") {
175
- const questions = event.input?.questions ?? [];
176
- const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
177
- if (typeof questionId === "string") {
178
- setPendingGate(questionId);
175
+ const milestoneId = getDiscussionMilestoneId(discussionBasePath);
176
+ const inDiscussion = milestoneId !== null || isQueuePhaseActive();
177
+ if (inDiscussion) {
178
+ const questions = event.input?.questions ?? [];
179
+ const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
180
+ if (typeof questionId === "string") {
181
+ setPendingGate(questionId);
182
+ }
179
183
  }
180
184
  }
181
185
  // ── Discussion gate enforcement: block tool calls while gate is pending ──
@@ -257,6 +261,8 @@ export function registerHooks(pi) {
257
261
  return;
258
262
  const milestoneId = getDiscussionMilestoneId(process.cwd());
259
263
  const queueActive = isQueuePhaseActive();
264
+ if (!milestoneId && !queueActive)
265
+ return;
260
266
  const details = event.details;
261
267
  // ── Discussion gate enforcement: handle gate question responses ──
262
268
  // If the result is cancelled or has no response, the pending gate stays active
@@ -287,16 +293,12 @@ export function registerHooks(pi) {
287
293
  // Only unlock the gate if the user selected the first option (confirmation).
288
294
  // Cross-references against the question's defined options to reject free-form "Other" text.
289
295
  const answer = details.response?.answers?.[question.id];
290
- const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
291
296
  if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
292
- markDepthVerified(inferredMilestoneId);
293
- clearPendingGate();
297
+ markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
294
298
  }
295
299
  break;
296
300
  }
297
301
  }
298
- if (!milestoneId && !queueActive)
299
- return;
300
302
  if (!milestoneId)
301
303
  return;
302
304
  const basePath = process.cwd();
@@ -61,9 +61,6 @@ export function parseDoctorArgs(args) {
61
61
  const requestedScope = mode === "doctor" ? parts[0] : parts[1];
62
62
  return { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope };
63
63
  }
64
- export function isDoctorHealActionable(issue) {
65
- return issue.fixable && issue.severity !== "info";
66
- }
67
64
  export async function handleDoctor(args, ctx, pi) {
68
65
  const { jsonMode, dryRun, fixFlag, includeBuild, includeTests, mode, requestedScope } = parseDoctorArgs(args);
69
66
  const scope = await selectDoctorScope(projectRoot(), requestedScope);
@@ -91,7 +88,7 @@ export async function handleDoctor(args, ctx, pi) {
91
88
  scope: effectiveScope,
92
89
  includeWarnings: true,
93
90
  });
94
- const actionable = unresolved.filter(isDoctorHealActionable);
91
+ const actionable = unresolved.filter(issue => issue.severity === "error");
95
92
  if (actionable.length === 0) {
96
93
  ctx.ui.notify("Doctor heal found nothing actionable to hand off to the LLM.", "info");
97
94
  return;
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { readFileSync, existsSync } from "node:fs";
16
16
  import { resolve, sep } from "node:path";
17
- import { readFrozenDefinition } from "./definition-io.js";
17
+ import { readFrozenDefinition } from "./custom-workflow-engine.js";
18
18
  /** Maximum characters per artifact to prevent context window blowout. */
19
19
  const MAX_CONTEXT_CHARS = 10_000;
20
20
  /**
@@ -13,13 +13,17 @@
13
13
  */
14
14
  import { readFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
+ import { parse } from "yaml";
16
17
  import { readGraph, writeGraph, getNextPendingStep, markStepComplete, expandIteration, } from "./graph.js";
17
18
  import { injectContext } from "./context-injector.js";
18
- import { readFrozenDefinition } from "./definition-io.js";
19
19
  import { parseUnitId } from "./unit-id.js";
20
20
  import { withFileLock } from "./file-lock.js";
21
- // Re-export for downstream consumers
22
- export { readFrozenDefinition } from "./definition-io.js";
21
+ /** Read and parse the frozen DEFINITION.yaml from a run directory. */
22
+ export function readFrozenDefinition(runDir) {
23
+ const defPath = join(runDir, "DEFINITION.yaml");
24
+ const raw = readFileSync(defPath, "utf-8");
25
+ return parse(raw, { schema: "core" });
26
+ }
23
27
  export class CustomWorkflowEngine {
24
28
  engineId = "custom";
25
29
  runDir;
@@ -121,25 +121,6 @@ function openRawDb(path) {
121
121
  return new Database(path);
122
122
  }
123
123
  const SCHEMA_VERSION = 14;
124
- function indexExists(db, name) {
125
- return !!db.prepare("SELECT 1 as present FROM sqlite_master WHERE type = 'index' AND name = ?").get(name);
126
- }
127
- function dedupeVerificationEvidenceRows(db) {
128
- db.exec(`
129
- DELETE FROM verification_evidence
130
- WHERE rowid NOT IN (
131
- SELECT MIN(rowid)
132
- FROM verification_evidence
133
- GROUP BY task_id, slice_id, milestone_id, command, verdict
134
- )
135
- `);
136
- }
137
- function ensureVerificationEvidenceDedupIndex(db) {
138
- if (indexExists(db, "idx_verification_evidence_dedup"))
139
- return;
140
- dedupeVerificationEvidenceRows(db);
141
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
142
- }
143
124
  function initSchema(db, fileBacked) {
144
125
  if (fileBacked)
145
126
  db.exec("PRAGMA journal_mode=WAL");
@@ -377,7 +358,7 @@ function initSchema(db, fileBacked) {
377
358
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestones_status ON milestones(status)");
378
359
  db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)");
379
360
  db.exec("CREATE INDEX IF NOT EXISTS idx_verification_evidence_task ON verification_evidence(milestone_id, slice_id, task_id)");
380
- ensureVerificationEvidenceDedupIndex(db);
361
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
381
362
  // v14 index — slice dependency lookups
382
363
  db.exec("CREATE INDEX IF NOT EXISTS idx_slice_deps_target ON slice_dependencies(milestone_id, depends_on_slice_id)");
383
364
  db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`);
@@ -687,7 +668,7 @@ function migrateSchema(db) {
687
668
  db.exec("CREATE INDEX IF NOT EXISTS idx_milestones_status ON milestones(status)");
688
669
  db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)");
689
670
  db.exec("CREATE INDEX IF NOT EXISTS idx_verification_evidence_task ON verification_evidence(milestone_id, slice_id, task_id)");
690
- ensureVerificationEvidenceDedupIndex(db);
671
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_evidence_dedup ON verification_evidence(task_id, slice_id, milestone_id, command, verdict)");
691
672
  db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
692
673
  ":version": 13,
693
674
  ":applied_at": new Date().toISOString(),
@@ -1353,29 +1334,6 @@ export function setSliceSummaryMd(milestoneId, sliceId, summaryMd, uatMd) {
1353
1334
  currentDb.prepare(`UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`).run({ ":mid": milestoneId, ":sid": sliceId, ":summary_md": summaryMd, ":uat_md": uatMd });
1354
1335
  }
1355
1336
  function rowToTask(row) {
1356
- const parseTaskArray = (value) => {
1357
- if (Array.isArray(value)) {
1358
- return value.filter((entry) => typeof entry === "string");
1359
- }
1360
- if (typeof value !== "string")
1361
- return [];
1362
- const trimmed = value.trim();
1363
- if (!trimmed)
1364
- return [];
1365
- try {
1366
- const parsed = JSON.parse(trimmed);
1367
- if (Array.isArray(parsed)) {
1368
- return parsed.filter((entry) => typeof entry === "string");
1369
- }
1370
- if (typeof parsed === "string" && parsed.trim()) {
1371
- return [parsed.trim()];
1372
- }
1373
- }
1374
- catch {
1375
- // Older/corrupt DB rows may contain raw comma-separated paths instead of JSON arrays.
1376
- }
1377
- return trimmed.split(",").map((entry) => entry.trim()).filter(Boolean);
1378
- };
1379
1337
  return {
1380
1338
  milestone_id: row["milestone_id"],
1381
1339
  slice_id: row["slice_id"],
@@ -1395,10 +1353,10 @@ function rowToTask(row) {
1395
1353
  full_summary_md: row["full_summary_md"],
1396
1354
  description: row["description"] ?? "",
1397
1355
  estimate: row["estimate"] ?? "",
1398
- files: parseTaskArray(row["files"]),
1356
+ files: JSON.parse(row["files"] || "[]"),
1399
1357
  verify: row["verify"] ?? "",
1400
- inputs: parseTaskArray(row["inputs"]),
1401
- expected_output: parseTaskArray(row["expected_output"]),
1358
+ inputs: JSON.parse(row["inputs"] || "[]"),
1359
+ expected_output: JSON.parse(row["expected_output"] || "[]"),
1402
1360
  observability_impact: row["observability_impact"] ?? "",
1403
1361
  full_plan_md: row["full_plan_md"] ?? "",
1404
1362
  sequence: row["sequence"] ?? 0,
@@ -26,8 +26,6 @@ export const PROVIDER_REGISTRY = [
26
26
  { id: "custom-openai", label: "Custom (OpenAI-compat)", category: "llm", envVar: "CUSTOM_OPENAI_API_KEY" },
27
27
  { id: "cerebras", label: "Cerebras", category: "llm", envVar: "CEREBRAS_API_KEY" },
28
28
  { id: "azure-openai-responses", label: "Azure OpenAI", category: "llm", envVar: "AZURE_OPENAI_API_KEY" },
29
- { id: "alibaba-coding-plan", label: "Alibaba Coding Plan", category: "llm", envVar: "ALIBABA_API_KEY", dashboardUrl: "bailian.console.aliyun.com" },
30
- { id: "alibaba-dashscope", label: "Alibaba DashScope", category: "llm", envVar: "DASHSCOPE_API_KEY", dashboardUrl: "dashscope.console.aliyun.com" },
31
29
  // Tool Keys
32
30
  { id: "context7", label: "Context7 Docs", category: "tool", envVar: "CONTEXT7_API_KEY", dashboardUrl: "context7.com/dashboard" },
33
31
  { id: "jina", label: "Jina Page Extract", category: "tool", envVar: "JINA_API_KEY", dashboardUrl: "jina.ai/api" },
@@ -9,6 +9,7 @@ import { homedir } from "node:os";
9
9
  import { isAbsolute, join } from "node:path";
10
10
  import { statSync } from "node:fs";
11
11
  import { validatePreferences } from "./preferences-validation.js";
12
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
12
13
  /**
13
14
  * Known skill directories, in priority order.
14
15
  * Searches both the skills.sh ecosystem directory (~/.agents/skills/) and
@@ -129,5 +130,36 @@ export function resolveAllSkillReferences(preferences, cwd) {
129
130
  }
130
131
  return { resolutions, warnings };
131
132
  }
132
- // resolveSkillDiscoveryMode and resolveSkillStalenessDays moved to
133
- // preferences.ts to break circular dependency (they need loadEffectiveGSDPreferences).
133
+ /**
134
+ * Format a skill reference for the system prompt.
135
+ * If resolved, shows the path so the agent knows exactly where to read.
136
+ * If unresolved, marks it clearly.
137
+ */
138
+ export function formatSkillRef(ref, resolutions) {
139
+ const resolution = resolutions.get(ref);
140
+ if (!resolution || resolution.method === "unresolved") {
141
+ return `${ref} (⚠ not found — check skill name or path)`;
142
+ }
143
+ // For absolute paths where SKILL.md is just appended, don't clutter the output
144
+ if (resolution.method === "absolute-path" || resolution.method === "absolute-dir") {
145
+ return ref;
146
+ }
147
+ // For bare names resolved from skill directories, show the resolved path
148
+ return `${ref} → \`${resolution.resolvedPath}\``;
149
+ }
150
+ /**
151
+ * Resolve the skill discovery mode from effective preferences.
152
+ * Defaults to "suggest" -- skills are identified during research but not installed automatically.
153
+ */
154
+ export function resolveSkillDiscoveryMode() {
155
+ const prefs = loadEffectiveGSDPreferences();
156
+ return prefs?.preferences.skill_discovery ?? "suggest";
157
+ }
158
+ /**
159
+ * Resolve the skill staleness threshold in days.
160
+ * Returns 0 if disabled, default 60 if not configured.
161
+ */
162
+ export function resolveSkillStalenessDays() {
163
+ const prefs = loadEffectiveGSDPreferences();
164
+ return prefs?.preferences.skill_staleness_days ?? 60;
165
+ }
@@ -92,18 +92,3 @@ export const KNOWN_UNIT_TYPES = [
92
92
  "discuss-milestone", "discuss-slice", "worktree-merge",
93
93
  ];
94
94
  export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
95
- /**
96
- * Format a skill reference for the system prompt.
97
- * If resolved, shows the path so the agent knows exactly where to read.
98
- * If unresolved, marks it clearly.
99
- */
100
- export function formatSkillRef(ref, resolutions) {
101
- const resolution = resolutions.get(ref);
102
- if (!resolution || resolution.method === "unresolved") {
103
- return `${ref} (⚠ not found — check skill name or path)`;
104
- }
105
- if (resolution.method === "absolute-path" || resolution.method === "absolute-dir") {
106
- return ref;
107
- }
108
- return `${ref} → \`${resolution.resolvedPath}\``;
109
- }