@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.738160b9

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 (213) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +17 -4
  3. package/dist/headless-answers.js +1 -1
  4. package/dist/headless-ui.d.ts +1 -1
  5. package/dist/headless-ui.js +1 -1
  6. package/dist/headless.js +1 -1
  7. package/dist/resources/.managed-resources-content-hash +1 -1
  8. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  9. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  10. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-budget.js +5 -1
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  14. package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
  15. package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
  16. package/dist/resources/extensions/gsd/auto-verification.js +5 -2
  17. package/dist/resources/extensions/gsd/auto.js +19 -10
  18. package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
  20. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  21. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -2
  22. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
  23. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
  24. package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
  25. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  26. package/dist/resources/extensions/gsd/commands-usage.js +1 -1
  27. package/dist/resources/extensions/gsd/db/engine.js +4 -1
  28. package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
  29. package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
  30. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  31. package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
  32. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
  33. package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
  34. package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
  35. package/dist/resources/extensions/gsd/git-service.js +57 -0
  36. package/dist/resources/extensions/gsd/gitignore.js +4 -1
  37. package/dist/resources/extensions/gsd/json-persistence.js +20 -0
  38. package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
  39. package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
  40. package/dist/resources/extensions/gsd/metrics.js +4 -4
  41. package/dist/resources/extensions/gsd/migrate-external.js +11 -2
  42. package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
  43. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  45. package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
  46. package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
  47. package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
  48. package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
  49. package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
  50. package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
  51. package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
  52. package/dist/resources/extensions/gsd/undo.js +43 -6
  53. package/dist/resources/extensions/gsd/unit-registry.js +4 -2
  54. package/dist/resources/extensions/gsd/verification-gate.js +7 -0
  55. package/dist/resources/extensions/gsd/workflow-manifest.js +126 -13
  56. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  57. package/dist/web/standalone/.next/BUILD_ID +1 -1
  58. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  59. package/dist/web/standalone/.next/build-manifest.json +2 -2
  60. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  61. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.html +1 -1
  78. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  85. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  86. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  88. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  89. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  90. package/package.json +1 -1
  91. package/packages/cloud-mcp-gateway/package.json +2 -2
  92. package/packages/contracts/package.json +1 -1
  93. package/packages/daemon/package.json +4 -4
  94. package/packages/gsd-agent-core/package.json +5 -5
  95. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
  96. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
  98. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  99. package/packages/gsd-agent-modes/package.json +7 -7
  100. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  101. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  102. package/packages/mcp-server/package.json +4 -4
  103. package/packages/native/package.json +1 -1
  104. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  105. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  106. package/packages/pi-agent-core/package.json +1 -1
  107. package/packages/pi-ai/dist/models.generated.d.ts +46 -0
  108. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  109. package/packages/pi-ai/dist/models.generated.js +38 -0
  110. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  111. package/packages/pi-ai/package.json +1 -1
  112. package/packages/pi-coding-agent/package.json +7 -7
  113. package/packages/pi-tui/package.json +2 -2
  114. package/packages/rpc-client/package.json +2 -2
  115. package/pkg/package.json +1 -1
  116. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  117. package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
  118. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  119. package/src/resources/extensions/gsd/auto/session.ts +9 -0
  120. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
  121. package/src/resources/extensions/gsd/auto-budget.ts +12 -1
  122. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
  123. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  124. package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
  125. package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
  126. package/src/resources/extensions/gsd/auto-verification.ts +5 -1
  127. package/src/resources/extensions/gsd/auto.ts +23 -8
  128. package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
  129. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
  130. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  131. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +41 -2
  132. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
  133. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
  134. package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
  135. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  136. package/src/resources/extensions/gsd/commands-usage.ts +1 -1
  137. package/src/resources/extensions/gsd/db/engine.ts +8 -1
  138. package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
  139. package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
  140. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
  141. package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
  142. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
  143. package/src/resources/extensions/gsd/doctor-types.ts +3 -0
  144. package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
  145. package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
  146. package/src/resources/extensions/gsd/git-service.ts +64 -0
  147. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  148. package/src/resources/extensions/gsd/json-persistence.ts +20 -0
  149. package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
  150. package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
  151. package/src/resources/extensions/gsd/metrics.ts +4 -4
  152. package/src/resources/extensions/gsd/migrate-external.ts +8 -2
  153. package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
  154. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  155. package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
  156. package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
  157. package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
  158. package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
  159. package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
  160. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
  161. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
  162. package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
  163. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
  164. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
  165. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
  166. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
  167. package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
  168. package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
  169. package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
  170. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
  171. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
  172. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
  173. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
  174. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
  175. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +37 -2
  176. package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
  177. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
  178. package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
  179. package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
  180. package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
  181. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
  182. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
  183. package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
  184. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
  185. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
  186. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
  187. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
  188. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
  189. package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
  190. package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
  191. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
  192. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
  193. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
  194. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
  195. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
  196. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
  197. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
  198. package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
  199. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
  200. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
  201. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +96 -15
  202. package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
  203. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
  204. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
  205. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  206. package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
  207. package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
  208. package/src/resources/extensions/gsd/undo.ts +47 -6
  209. package/src/resources/extensions/gsd/unit-registry.ts +4 -2
  210. package/src/resources/extensions/gsd/verification-gate.ts +8 -0
  211. package/src/resources/extensions/gsd/workflow-manifest.ts +141 -7
  212. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_buildManifest.js +0 -0
  213. /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -158,7 +158,7 @@ Need help choosing settings? Use the [GSD Pi web configurator](https://pi.opengs
158
158
  gsd
159
159
  ```
160
160
 
161
- Run the setup flow, choose your preferred model provider, and open a project directory. GSD stores project planning and runtime state in `.gsd/`.
161
+ Run the setup flow, choose your preferred model provider, and open a project directory. GSD stores project planning and runtime state in `.gsd/`, with gitignored sibling runtime directories such as `.gsd-backups/` for migration snapshots. Stale `.gsd-backups/migrate-*` snapshots are pruned after 30 days once the flat-phase `.gsd/phases/` migration is complete.
162
162
 
163
163
  For a full first-run walkthrough, see [Getting Started With gsd-pi](./docs/user-docs/getting-started.md).
164
164
 
package/dist/cli.js CHANGED
@@ -22,15 +22,23 @@ import { markStartup, printStartupTimings } from './startup-timings.js';
22
22
  import { applyRtkProcessEnv, GSD_RTK_DISABLED_ENV, isTruthy } from './rtk-shared.js';
23
23
  let piCodingAgentModulePromise;
24
24
  let agentCoreModulePromise;
25
- let agentModesModulePromise;
25
+ let interactiveModeModulePromise;
26
+ let printModeModulePromise;
27
+ let rpcModeModulePromise;
26
28
  function loadPiCodingAgentModule() {
27
29
  return (piCodingAgentModulePromise ??= import('@gsd/pi-coding-agent'));
28
30
  }
29
31
  function loadAgentCoreModule() {
30
32
  return (agentCoreModulePromise ??= import('@gsd/agent-core'));
31
33
  }
32
- function loadAgentModesModule() {
33
- return (agentModesModulePromise ??= import('@gsd/agent-modes'));
34
+ function loadInteractiveModeModule() {
35
+ return (interactiveModeModulePromise ??= import('@gsd/agent-modes/modes/interactive/interactive-mode.js'));
36
+ }
37
+ function loadPrintModeModule() {
38
+ return (printModeModulePromise ??= import('@gsd/agent-modes/modes/print-mode.js'));
39
+ }
40
+ function loadRpcModeModule() {
41
+ return (rpcModeModulePromise ??= import('@gsd/agent-modes/modes/rpc/rpc-mode.js'));
34
42
  }
35
43
  // ---------------------------------------------------------------------------
36
44
  // V8 compile cache — Node 22+ can cache compiled bytecode across runs,
@@ -511,7 +519,6 @@ if (!isPrintMode &&
511
519
  }
512
520
  const { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, } = await loadPiCodingAgentModule();
513
521
  const { createAgentSession } = await loadAgentCoreModule();
514
- const { InteractiveMode, runPrintMode, runRpcMode } = await loadAgentModesModule();
515
522
  markStartup('loadPiCodingAgent');
516
523
  // Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
517
524
  // because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
@@ -530,6 +537,9 @@ markStartup('ModelRegistry');
530
537
  const settingsManager = SettingsManager.create(process.cwd(), agentDir);
531
538
  applySecurityOverrides(settingsManager);
532
539
  markStartup('SettingsManager.create');
540
+ const { configureHttpDispatcher } = await import('@gsd/pi-coding-agent/core/http-dispatcher.js');
541
+ configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
542
+ markStartup('configureHttpDispatcher');
533
543
  // Run onboarding wizard on first launch (no LLM provider configured)
534
544
  if (!isPrintMode && shouldRunOnboarding(authStorage, settingsManager.getDefaultProvider())) {
535
545
  await runOnboarding(authStorage);
@@ -651,6 +661,7 @@ if (isPrintMode) {
651
661
  applyModelOverride(session, modelRegistry, cliFlags.model);
652
662
  const mode = cliFlags.mode || 'text';
653
663
  if (mode === 'rpc') {
664
+ const { runRpcMode } = await loadRpcModeModule();
654
665
  printStartupTimings();
655
666
  await runRpcMode(session);
656
667
  process.exit(0);
@@ -674,6 +685,7 @@ if (isPrintMode) {
674
685
  // MCP server runs until the transport closes; keep alive
675
686
  await new Promise(() => { });
676
687
  }
688
+ const { runPrintMode } = await loadPrintModeModule();
677
689
  printStartupTimings();
678
690
  await runPrintMode(session, {
679
691
  mode: mode,
@@ -809,6 +821,7 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
809
821
  : 'stdout is';
810
822
  printNonTtyErrorAndExit(missing, true);
811
823
  }
824
+ const { InteractiveMode } = await loadInteractiveModeModule();
812
825
  const interactiveMode = new InteractiveMode(session);
813
826
  markStartup('InteractiveMode');
814
827
  printStartupTimings();
@@ -6,7 +6,7 @@
6
6
  * default auto-responder or supervised mode.
7
7
  */
8
8
  import { readFileSync } from 'node:fs';
9
- import { serializeJsonLine } from '@gsd/agent-modes';
9
+ import { serializeJsonLine } from '@gsd/agent-modes/modes/rpc/jsonl.js';
10
10
  import { canonicalHeadlessToolName } from './headless-events.js';
11
11
  export function normalizeQuestionId(id) {
12
12
  return id.trim().toLowerCase().replace(/[-\s]+/g, '_').replace(/_confirm$/, '');
@@ -5,7 +5,7 @@
5
5
  * formats progress events for stderr output, and reads orchestrator
6
6
  * commands from stdin in supervised mode.
7
7
  */
8
- import { RpcClient } from '@gsd/agent-modes';
8
+ import type { RpcClient } from '@gsd/agent-modes/modes/rpc/rpc-client.js';
9
9
  interface ExtensionUIRequest {
10
10
  type: 'extension_ui_request';
11
11
  id: string;
@@ -5,7 +5,7 @@
5
5
  * formats progress events for stderr output, and reads orchestrator
6
6
  * commands from stdin in supervised mode.
7
7
  */
8
- import { attachJsonlLineReader } from '@gsd/agent-modes';
8
+ import { attachJsonlLineReader } from '@gsd/agent-modes/modes/rpc/jsonl.js';
9
9
  // ---------------------------------------------------------------------------
10
10
  // ANSI Color Helpers
11
11
  // ---------------------------------------------------------------------------
package/dist/headless.js CHANGED
@@ -14,7 +14,7 @@
14
14
  import { existsSync, mkdirSync, writeFileSync, writeSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  import { resolve } from 'node:path';
17
- import { RpcClient } from '@gsd/agent-modes';
17
+ import { RpcClient } from '@gsd/agent-modes/modes/rpc/rpc-client.js';
18
18
  import { SessionManager } from '@gsd/pi-coding-agent';
19
19
  import { getProjectSessionsDir } from './project-sessions.js';
20
20
  import { loadAndValidateAnswerFile, AnswerInjector } from './headless-answers.js';
@@ -1 +1 @@
1
- f088e947f0ebb875
1
+ bd26f10d1f8c134c
@@ -46,6 +46,15 @@ export const CLAUDE_CODE_MODELS = [
46
46
  contextWindow: 1_000_000,
47
47
  maxTokens: 128_000,
48
48
  },
49
+ {
50
+ id: "claude-sonnet-5",
51
+ name: "Claude Sonnet 5 (via Claude Code)",
52
+ reasoning: true,
53
+ input: ["text", "image"],
54
+ cost: ZERO_COST,
55
+ contextWindow: 1_000_000,
56
+ maxTokens: 128_000,
57
+ },
49
58
  {
50
59
  id: "claude-sonnet-4-6",
51
60
  name: "Claude Sonnet 4.6 (via Claude Code)",
@@ -124,6 +124,13 @@ export class AutoSession {
124
124
  * the issues after MAX_PRE_EXEC_RETRIES re-attempts.
125
125
  */
126
126
  preExecRetryCount = new Map();
127
+ /**
128
+ * Tracks how many times each slice unit has been re-dispatched to plan-slice
129
+ * because task plan files were missing. Keyed by unitId (e.g. "M001/S01").
130
+ * Separate from preExecRetryCount so pre-exec gate failures do not block or
131
+ * conflate with missing-task-plan recovery.
132
+ */
133
+ missingTaskPlanRetryCount = new Map();
127
134
  // ── Tool invocation errors (#2883) ──────────────────────────────────
128
135
  /** Set when a GSD tool execution ends with isError due to malformed/truncated
129
136
  * JSON arguments. Checked by postUnitPreVerification to break retry loops. */
@@ -310,6 +317,7 @@ export class AutoSession {
310
317
  this.consecutiveCompleteBootstraps = 0;
311
318
  this.lastPreExecFailure = null;
312
319
  this.preExecRetryCount.clear();
320
+ this.missingTaskPlanRetryCount.clear();
313
321
  this.lastToolInvocationError = null;
314
322
  this.toolUnavailableRetries = 0;
315
323
  this.lastUnitAgentEndMessages = null;
@@ -25,7 +25,7 @@ export function decideVerificationRetry(input) {
25
25
  return { action: "pause", reason: "missing-retry-context" };
26
26
  }
27
27
  const key = verificationRetryKey(unitType, retryInfo.unitId);
28
- const failureHash = hashVerificationFailureContext(retryInfo.failureContext);
28
+ const failureHash = hashVerificationFailureContext(retryInfo.signature ?? retryInfo.failureContext);
29
29
  if (input.previousFailureHash === failureHash) {
30
30
  return {
31
31
  action: "pause",
@@ -68,6 +68,10 @@ export function resolveCompactionThresholdPercent(raw) {
68
68
  const value = typeof raw === "number" && Number.isFinite(raw) && raw >= 0.5 && raw <= 0.95 ? raw : 0.6;
69
69
  return value <= 1 ? value * 100 : value;
70
70
  }
71
- export function shouldRerootStepSessionForContext(contextPercent, compactionThresholdPercent) {
71
+ export const HARD_CONTEXT_REROOT_THRESHOLD_PERCENT = 90;
72
+ export function shouldWarnStepSessionForContext(contextPercent, compactionThresholdPercent) {
72
73
  return getContextPauseAction(contextPercent, resolveCompactionThresholdPercent(compactionThresholdPercent)) === "pause";
73
74
  }
75
+ export function shouldRerootStepSessionForContext(contextPercent) {
76
+ return getContextPauseAction(contextPercent, HARD_CONTEXT_REROOT_THRESHOLD_PERCENT) === "pause";
77
+ }
@@ -1238,6 +1238,7 @@ export const DISPATCH_RULES = [
1238
1238
  const sid = state.activeSlice.id;
1239
1239
  const sTitle = state.activeSlice.title;
1240
1240
  const tid = state.activeTask.id;
1241
+ const unitId = `${mid}/${sid}`;
1241
1242
  const artifactBasePath = resolveArtifactBasePath(basePath, mid, session);
1242
1243
  // Guard: if the slice plan exists but the individual task plan files are
1243
1244
  // missing, the planner created S##-PLAN.md with task entries but never
@@ -1263,6 +1264,17 @@ export const DISPATCH_RULES = [
1263
1264
  if ((!taskPlanPath || !existsSync(taskPlanPath)) &&
1264
1265
  !existsSync(projectionTaskPlanPath) &&
1265
1266
  !tasksEmbeddedInSlicePlan) {
1267
+ const MAX_MISSING_TASK_PLAN_RETRIES = 2;
1268
+ const retryCount = session?.missingTaskPlanRetryCount?.get(unitId) ?? 0;
1269
+ if (retryCount >= MAX_MISSING_TASK_PLAN_RETRIES) {
1270
+ session?.missingTaskPlanRetryCount?.delete(unitId);
1271
+ return {
1272
+ action: "stop",
1273
+ reason: `Missing task-plan recovery failed ${retryCount} times for ${unitId} - manual intervention required. Task plan ${tid} is still missing after regenerating the slice plan. Fix the task-plan files manually, then run /gsd auto to resume.`,
1274
+ level: "error",
1275
+ };
1276
+ }
1277
+ session?.missingTaskPlanRetryCount?.set(unitId, retryCount + 1);
1266
1278
  if (isDebugEnabled()) {
1267
1279
  const expectedTaskPlanPath = join(artifactBasePath, relTaskFile(artifactBasePath, mid, sid, tid, "PLAN"));
1268
1280
  const originalProjectRoot = session?.originalBasePath || basePath;
@@ -1286,10 +1298,11 @@ export const DISPATCH_RULES = [
1286
1298
  return {
1287
1299
  action: "dispatch",
1288
1300
  unitType: "plan-slice",
1289
- unitId: `${mid}/${sid}`,
1301
+ unitId,
1290
1302
  prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath, undefined, { sessionContextWindow, modelRegistry, sessionProvider }),
1291
1303
  };
1292
1304
  }
1305
+ session?.missingTaskPlanRetryCount?.delete(unitId);
1293
1306
  return null;
1294
1307
  },
1295
1308
  },
@@ -1834,7 +1834,7 @@ export async function postUnitPreVerification(pctx, opts) {
1834
1834
  saveCustomVerifyRetryCounts(s, { logFailure: err => debugLog("postUnit", { phase: "save-verify-retries-failed", error: err instanceof Error ? err.message : String(err) }) });
1835
1835
  s.pendingVerificationRetry = {
1836
1836
  unitId: s.currentUnit.id,
1837
- failureContext: `${failureDetails} (attempt ${attempt}/${MAX_ARTIFACT_VERIFICATION_RETRIES}).`,
1837
+ failureContext: failureDetails,
1838
1838
  attempt,
1839
1839
  };
1840
1840
  debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
@@ -10,7 +10,7 @@
10
10
  import { loadFile, parseContinue, parseSummary, loadActiveOverrides, formatOverridesSection } from "./files.js";
11
11
  import { hasVerdict, extractVerdict } from "./verdict-parser.js";
12
12
  import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
13
- import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile, } from "./paths.js";
13
+ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksDir, resolveTaskFiles, resolveTaskFile, relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath, relTaskFile, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile, } from "./paths.js";
14
14
  import { resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
15
15
  import { isContextModeEnabled } from "./preferences-types.js";
16
16
  import { parseRoadmap } from "./parsers-legacy.js";
@@ -2282,7 +2282,7 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
2282
2282
  : "- (no prior tasks)";
2283
2283
  const taskPlanPath = resolveTaskFile(base, mid, sid, tid, "PLAN");
2284
2284
  const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
2285
- const taskPlanRelPath = relSlicePath(base, mid, sid) + `/tasks/${tid}-PLAN.md`;
2285
+ const taskPlanRelPath = relTaskFile(base, mid, sid, tid, "PLAN");
2286
2286
  const taskPlanContext = taskPlanContent
2287
2287
  ? [
2288
2288
  "## Inlined Task Plan (authoritative local execution contract)",
@@ -3376,7 +3376,7 @@ opts) {
3376
3376
  const depPaths = await getDependencyTaskSummaryPaths(mid, sid, tid, node?.dependsOn ?? [], base);
3377
3377
  const taskPlanPath = resolveTaskFile(base, mid, sid, tid, "PLAN");
3378
3378
  const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
3379
- const taskPlanRelPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-PLAN.md`;
3379
+ const taskPlanRelPath = relTaskFile(base, mid, sid, tid, "PLAN");
3380
3380
  const taskPlanInline = taskPlanContent
3381
3381
  ? [
3382
3382
  "## Inlined Task Plan (authoritative local execution contract)",
@@ -16,7 +16,7 @@ import { isDbAvailable, getTask, updateTaskStatus, updateSliceStatus, insertSlic
16
16
  import { refreshWorkflowDatabaseFromDisk } from "./db-workspace.js";
17
17
  import { getErrorMessage } from "./error-utils.js";
18
18
  import { logWarning } from "./workflow-logger.js";
19
- import { isClosedStatus } from "./status-guards.js";
19
+ import { isClosedStatus, isInactiveStatus } from "./status-guards.js";
20
20
  import { resolveSlicePath, resolveTasksDir, resolveTaskFiles, relMilestoneFile, relSliceFile, clearPathCache, } from "./paths.js";
21
21
  import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
22
22
  import { dirname } from "node:path";
@@ -194,7 +194,7 @@ export function writeReactiveExecuteBlocker(unitId, base, reason) {
194
194
  transaction(() => {
195
195
  for (const tid of summaryPresent) {
196
196
  const task = getTask(mid, sid, tid);
197
- if (!task || isClosedStatus(task.status)) {
197
+ if (!task || isInactiveStatus(task.status)) {
198
198
  unchangedTaskIds.push(tid);
199
199
  continue;
200
200
  }
@@ -203,7 +203,7 @@ export function writeReactiveExecuteBlocker(unitId, base, reason) {
203
203
  }
204
204
  for (const tid of summaryMissing) {
205
205
  const task = getTask(mid, sid, tid);
206
- if (!task || isClosedStatus(task.status)) {
206
+ if (!task || isInactiveStatus(task.status)) {
207
207
  unchangedTaskIds.push(tid);
208
208
  continue;
209
209
  }
@@ -9,7 +9,7 @@ import { isDbAvailable, getTask, getSliceTasks, getMilestoneSlices } from "./gsd
9
9
  import { loadEffectiveGSDPreferences } from "./preferences.js";
10
10
  import { isClosedStatus } from "./status-guards.js";
11
11
  import { loadFile } from "./files.js";
12
- import { runVerificationGate, runVerificationGateForTargets, formatFailureContext, captureRuntimeErrors, runDependencyAudit, } from "./verification-gate.js";
12
+ import { runVerificationGate, runVerificationGateForTargets, formatFailureContext, formatFailureSignature, captureRuntimeErrors, runDependencyAudit, } from "./verification-gate.js";
13
13
  import { writeVerificationJSON } from "./verification-evidence.js";
14
14
  import { logWarning } from "./workflow-logger.js";
15
15
  import { runPostExecutionChecks } from "./post-execution-checks.js";
@@ -686,10 +686,13 @@ export async function runPostUnitVerification(vctx, pauseAuto) {
686
686
  ctx.ui.notify(`Unit ${s.currentUnit.id} cost spike detected (${unitCostUsd.toFixed(2)} vs avg ${rollingAvgUsd.toFixed(2)}) during verification retry; keeping verification failure as the authoritative blocker.`, "warning");
687
687
  }
688
688
  const nextAttempt = attempt + 1;
689
+ const failureContext = verdict.failureContext || formatFailureContext(result);
690
+ const failureSignature = formatFailureSignature(result);
689
691
  s.verificationRetryCount.set(retryKey, nextAttempt);
690
692
  s.pendingVerificationRetry = {
691
693
  unitId: s.currentUnit.id,
692
- failureContext: verdict.failureContext || formatFailureContext(result),
694
+ failureContext,
695
+ ...(failureSignature ? { signature: failureSignature } : {}),
693
696
  attempt: nextAttempt,
694
697
  };
695
698
  const failedCmds = result.checks
@@ -27,7 +27,7 @@ import { writeLock, clearLock, clearStaleWorkerLock, readCrashLock, isLockProces
27
27
  import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
28
28
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, loadEffectiveGSDPreferencesWithRegistry, resolveProfileAnchorProvider, getIsolationMode, resolveEffectiveUnitIsolationMode, } from "./preferences.js";
29
29
  import { playNotificationBell, sendDesktopNotification } from "./notifications.js";
30
- import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, } from "./auto-budget.js";
30
+ import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, HARD_CONTEXT_REROOT_THRESHOLD_PERCENT, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, shouldWarnStepSessionForContext, } from "./auto-budget.js";
31
31
  import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, isToolInvocationError, isQueuedUserMessageSkip, isDeterministicPolicyError, } from "./auto-tool-tracking.js";
32
32
  import { closeoutUnit } from "./auto-unit-closeout.js";
33
33
  import { selectAndApplyModel, resolveModelId, clearToolBaseline } from "./auto-model-selection.js";
@@ -369,7 +369,7 @@ function getEffectiveUnitIsolationMode(basePath) {
369
369
  * running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
370
370
  */
371
371
  // Re-export budget utilities for external consumers
372
- export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, getContextPauseAction, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, } from "./auto-budget.js";
372
+ export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, getContextPauseAction, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, shouldWarnStepSessionForContext, } from "./auto-budget.js";
373
373
  function closeOutSignalInterruptedUnit(currentBasePath) {
374
374
  const currentUnit = s.currentUnit;
375
375
  if (!currentUnit)
@@ -904,9 +904,10 @@ export async function rerootCommandSession(cmdCtx, workspaceRoot) {
904
904
  }
905
905
  }
906
906
  /**
907
- * After a completed step-mode unit, reclaim context headroom when usage is at
908
- * or above the compaction threshold. Writes a context-mode snapshot first, then
909
- * re-roots the visible command session while leaving the NEXT progress surface.
907
+ * After a completed step-mode unit, warn when usage reaches the configurable
908
+ * soft context threshold. At the fixed hard boundary, write a context-mode
909
+ * snapshot and re-root the visible command session while leaving the NEXT
910
+ * progress surface.
910
911
  */
911
912
  export async function maybeRerootStepSessionForHighContext(ctx, workspaceRoot, cmdCtx, originalBasePath) {
912
913
  if (!cmdCtx)
@@ -915,18 +916,24 @@ export async function maybeRerootStepSessionForHighContext(ctx, workspaceRoot, c
915
916
  const { loadEffectiveGSDPreferences } = await import("./preferences.js");
916
917
  const prefs = loadEffectiveGSDPreferences(workspaceRoot);
917
918
  const compactionThreshold = prefs?.preferences.context_management?.compaction_threshold_percent;
918
- if (!shouldRerootStepSessionForContext(contextPercent, compactionThreshold)) {
919
+ const shouldWarn = shouldWarnStepSessionForContext(contextPercent, compactionThreshold);
920
+ const shouldReroot = shouldRerootStepSessionForContext(contextPercent);
921
+ if (!shouldWarn && !shouldReroot) {
922
+ return { rerooted: false };
923
+ }
924
+ const displayPercent = contextPercent.toFixed(1);
925
+ const thresholdDisplay = resolveCompactionThresholdPercent(compactionThreshold).toFixed(0);
926
+ if (!shouldReroot) {
927
+ ctx.ui.notify(`Step complete — context at ${displayPercent}% (soft threshold: ${thresholdDisplay}%). Use /compact when convenient; continuing in this session is still safe.`, "info");
919
928
  return { rerooted: false };
920
929
  }
921
930
  const { resolveWorktreeProjectRoot } = await import("./worktree-root.js");
922
931
  const snapshotBase = resolveWorktreeProjectRoot(workspaceRoot, originalBasePath);
923
932
  const { writeContextModeCompactionSnapshot } = await import("./context-mode-snapshot.js");
924
933
  await writeContextModeCompactionSnapshot(snapshotBase);
925
- const displayPercent = contextPercent.toFixed(1);
926
- const thresholdDisplay = resolveCompactionThresholdPercent(compactionThreshold).toFixed(0);
927
934
  const result = await rerootCommandSession(cmdCtx, workspaceRoot);
928
935
  if (result.status === "ok") {
929
- ctx.ui.notify(`Step complete — context at ${displayPercent}% (threshold: ${thresholdDisplay}%). Fresh session ready for /gsd next.`, "info");
936
+ ctx.ui.notify(`Step complete — context at ${displayPercent}% (hard threshold: ${HARD_CONTEXT_REROOT_THRESHOLD_PERCENT}%). Fresh session ready for /gsd next.`, "info");
930
937
  return { rerooted: true };
931
938
  }
932
939
  if (result.status === "cancelled") {
@@ -1545,7 +1552,7 @@ export function _selectStopAutoWorktreeExit(args) {
1545
1552
  /**
1546
1553
  * Pause auto-mode without destroying state. Context is preserved.
1547
1554
  * The user can interact with the agent, then `/gsd auto` resumes
1548
- * from disk state. Called when the user presses Escape during auto-mode.
1555
+ * from disk state. Called when the user presses Escape or runs `/gsd pause`.
1549
1556
  */
1550
1557
  export async function pauseAuto(ctx, _pi, _errorContext, options = {}) {
1551
1558
  if (!s.active)
@@ -1564,6 +1571,8 @@ export async function pauseAuto(ctx, _pi, _errorContext, options = {}) {
1564
1571
  ].filter((path) => Boolean(path)));
1565
1572
  s.active = false;
1566
1573
  s.paused = true;
1574
+ if (options.abortActiveTurn && ctx && !ctx.isIdle())
1575
+ ctx.abort();
1567
1576
  clearUnitTimeout();
1568
1577
  stopAutoCommandPolling();
1569
1578
  // Flush queued follow-up messages (#3512).
@@ -25,6 +25,7 @@ const NON_REPEATABLE_FROM_MINIMAL_BASE = new Set([
25
25
  ]);
26
26
  /** Additional core tools not in MINIMAL_AUTO_BASE but routinely multi-called per turn. */
27
27
  const EXTRA_INHERENTLY_REPEATABLE = [
28
+ "gsd_exec",
28
29
  "multi_edit",
29
30
  "todo_write",
30
31
  "notebook_edit",
@@ -654,8 +654,9 @@ export function registerDbTools(pi) {
654
654
  promptSnippet: "Plan a slice via DB write + PLAN render + cache invalidation",
655
655
  promptGuidelines: [
656
656
  "Use gsd_plan_slice for slice planning instead of writing S##-PLAN.md or task PLAN files directly.",
657
- "Keep parameters flat and provide the full slice planning payload, including tasks.",
658
- "The tool validates input, requires an existing parent slice, writes slice/task planning data, renders PLAN.md and task plan files from DB, and clears both state and parse caches after success.",
657
+ "For incremental planning, call gsd_plan_slice with slice metadata only, then call gsd_plan_task once per task.",
658
+ "When tasks is omitted or empty, the tool writes only slice planning metadata and preserves existing tasks.",
659
+ "When tasks is non-empty, the tool validates input, requires an existing parent slice, writes slice/task planning data, renders PLAN.md and task plan files from DB, and clears both state and parse caches after success.",
659
660
  "Use the canonical name gsd_plan_slice; gsd_slice_plan is only an alias.",
660
661
  ],
661
662
  parameters: Type.Object({
@@ -663,7 +664,7 @@ export function registerDbTools(pi) {
663
664
  milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
664
665
  sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
665
666
  goal: Type.String({ description: "Slice goal" }),
666
- tasks: Type.Array(Type.Object({
667
+ tasks: Type.Optional(Type.Array(Type.Object({
667
668
  taskId: Type.String({ description: "Task ID (e.g. T01)" }),
668
669
  title: Type.String({ description: "Task title" }),
669
670
  description: Type.String({ description: "Task description / steps block" }),
@@ -673,7 +674,7 @@ export function registerDbTools(pi) {
673
674
  inputs: Type.Array(Type.String(), { description: "Array<string> of input files or references; pass [\"path\"] or [], never a single string" }),
674
675
  expectedOutput: Type.Array(Type.String(), { description: "Array<string> of files this task creates or overwrites; pass [\"path\"] or [], never prose or a single string" }),
675
676
  observabilityImpact: Type.Optional(Type.String({ description: "Task observability impact" })),
676
- }), { description: "Planned tasks for the slice" }),
677
+ }), { description: "Optional full task replacement for the slice. Omit for incremental planning, then call gsd_plan_task once per task." })),
677
678
  // ── Enrichment metadata (optional — defaults to empty) ────────────
678
679
  successCriteria: Type.Optional(Type.String({ description: "Slice success criteria block" })),
679
680
  proofLevel: Type.Optional(Type.String({ description: "Slice proof level" })),
@@ -14,6 +14,7 @@ import { registerShortcuts } from "./register-shortcuts.js";
14
14
  import { writeCrashLog } from "./crash-log.js";
15
15
  import { logWarning, isGsdExtensionStderrEnabled } from "../workflow-logger.js";
16
16
  import { UNIT_TOOL_CONTRACTS } from "../unit-tool-contracts.js";
17
+ import { installManifestFlushOnProcessTeardown } from "../workflow-manifest.js";
17
18
  // Static import so cmux event listeners are registered synchronously during
18
19
  // extension bootstrap. Prior implementation used `void import().then()` which
19
20
  // queued listener registration as a microtask — any CMUX_CHANNELS emit fired
@@ -177,6 +178,7 @@ export function registerGsdExtension(pi) {
177
178
  process.stderr.write(`[gsd] Failed to bootstrap hook-emitter bridge: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
178
179
  });
179
180
  installEpipeGuard();
181
+ installManifestFlushOnProcessTeardown();
180
182
  // Ecosystem handlers captured by the GSDExtensionAPI wrapper for the
181
183
  // GSD-owned `before_agent_start` dispatch step (#3338).
182
184
  const ecosystemHandlers = [];
@@ -14,7 +14,7 @@ import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from
14
14
  import { loadFile, saveFile, formatContinue } from "../files.js";
15
15
  import { clearAutoCompletionStopInProgress, clearToolInvocationError, getAutoRuntimeSnapshot, getSourceObservationStore, isAutoActive, isAutoCompletionStopInProgress, isAutoPaused, isInteractiveElicitationInFlight, markToolEnd, markToolStart, recordAutoToolSurfaceSnapshot, recordToolInvocationError, } from "../auto-runtime-state.js";
16
16
  import { applyProviderPayloadPolicy } from "../provider-payload-policy.js";
17
- import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
17
+ import { checkToolCallLoop, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
18
18
  import { MINIMAL_AUTO_BASE_TOOL_NAMES } from "./core-session-tools.js";
19
19
  import { maybePauseAutoForApprovalGate, resetPendingGatePauseGuard } from "./pending-gate-pause.js";
20
20
  import { saveActivityLog } from "../activity-log.js";
@@ -43,6 +43,7 @@ import { supportsSourceObservationsForUnit } from "../source-observations.js";
43
43
  import { clearPendingAutoStart } from "../pending-auto-start.js";
44
44
  import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
45
45
  import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
46
+ import { flushAllManifests } from "../workflow-manifest.js";
46
47
  let approvalQuestionAbortInFlight = false;
47
48
  async function loadWelcomeScreenModule() {
48
49
  const candidates = [];
@@ -435,6 +436,16 @@ function deferApprovalGateFromSnapshot(gateId, basePath, snapshot) {
435
436
  function contextBasePath(ctx) {
436
437
  return typeof ctx?.cwd === "string" ? ctx.cwd : process.cwd();
437
438
  }
439
+ const LOOP_GUARD_INTERACTIVE_INSTRUCTIONS = [
440
+ "Do not retry this tool or call other tools this turn — stop and respond to the user in text.",
441
+ "Do not retry this tool or pivot to other tools this turn — stop and respond to the user in text.",
442
+ ];
443
+ const LOOP_GUARD_AUTO_INSTRUCTION = "Do not re-issue this blocked tool. In /gsd auto, stop tool calls for this turn and return control to the auto-mode recovery/replan path.";
444
+ function formatLoopGuardBlockReason(reason) {
445
+ if (!reason || !getAutoRuntimeSnapshot().active)
446
+ return reason;
447
+ return LOOP_GUARD_INTERACTIVE_INSTRUCTIONS.reduce((formatted, instruction) => formatted.replace(instruction, LOOP_GUARD_AUTO_INSTRUCTION), reason);
448
+ }
438
449
  function beginSourceObservationStoreForCurrentUnit(ctx) {
439
450
  if (!isAutoActive())
440
451
  return null;
@@ -703,6 +714,12 @@ export function registerHooks(pi, ecosystemHandlers) {
703
714
  }
704
715
  }
705
716
  }
717
+ const projectRoot = resolveWorktreeProjectRoot(basePath);
718
+ const { pruneStaleFlatPhaseBackups } = await import("../flat-phase-migration.js");
719
+ const pruned = pruneStaleFlatPhaseBackups(projectRoot);
720
+ if (pruned > 0) {
721
+ safetyLogWarning("bootstrap", `pruned ${pruned} stale flat-phase migration backup(s) from .gsd-backups/ (retention exceeded)`);
722
+ }
706
723
  }
707
724
  catch (err) {
708
725
  safetyLogWarning("bootstrap", `flat-phase migration: ${err instanceof Error ? err.message : String(err)}`);
@@ -860,6 +877,17 @@ export function registerHooks(pi, ecosystemHandlers) {
860
877
  const { handleAgentEnd } = await import("./agent-end-recovery.js");
861
878
  const agentEndBasePath = contextBasePath(ctx);
862
879
  try {
880
+ // The manifest is a non-critical projection (the append-only event log is
881
+ // the authoritative recovery source), so a flush failure must not skip
882
+ // agent_end recovery. drainManifestWrites still propagates write failures
883
+ // to explicit flush callers; here we deliberately log and continue so
884
+ // handleAgentEnd (and the finally below) always run.
885
+ try {
886
+ await flushAllManifests();
887
+ }
888
+ catch (err) {
889
+ safetyLogWarning("manifest", `flushAllManifests on agent_end failed: ${err instanceof Error ? err.message : String(err)}`);
890
+ }
863
891
  await handleAgentEnd(pi, event, ctx);
864
892
  }
865
893
  finally {
@@ -1049,7 +1077,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1049
1077
  // ── Loop guard: block repeated identical tool calls ──
1050
1078
  const loopCheck = checkToolCallLoop(toolName, event.input);
1051
1079
  if (loopCheck.block) {
1052
- return { block: true, reason: loopCheck.reason };
1080
+ return { block: true, reason: formatLoopGuardBlockReason(loopCheck.reason) };
1053
1081
  }
1054
1082
  const deferredGateGuard = shouldBlockDeferredApprovalTool(toolName, event.input, discussionBasePath);
1055
1083
  if (deferredGateGuard.block) {
@@ -1255,6 +1283,7 @@ export function registerHooks(pi, ecosystemHandlers) {
1255
1283
  }
1256
1284
  }
1257
1285
  if (!event.isError) {
1286
+ recordToolCallLoopMutation(toolName);
1258
1287
  refreshSourceObservationAfterMutation(toolName, event.input, ctx);
1259
1288
  clearSourceObservationsAfterShell(toolName);
1260
1289
  }
@@ -14,10 +14,12 @@
14
14
  * to stop tooling for the rest of that turn and answer in text.
15
15
  *
16
16
  * The per-tool-name check (#783 Brief C) tracks call counts within a
17
- * turn regardless of args. This catches improvisation
17
+ * turn regardless of args, decaying after successful state-mutating tools.
18
+ * This catches improvisation
18
19
  * loops where the model attempts the same missing workflow tool through
19
20
  * varied surfaces (bash → `node -e` → CLI), each with a different
20
- * signature, so the identical-args streak never trips. Whichever guard
21
+ * signature, so the identical-args streak never trips, while allowing
22
+ * tool-heavy turns that are making file-mutation progress. Whichever guard
21
23
  * trips first blocks.
22
24
  */
23
25
  import { createHash } from "node:crypto";
@@ -38,12 +40,16 @@ const MAX_CONSECUTIVE_STRICT = 1;
38
40
  */
39
41
  const PER_TOOL_DEFAULT_CAP = 6;
40
42
  const PER_TOOL_REPEATABLE_CAP = 15;
43
+ const PER_TOOL_CAP_EXEMPT_TOOLS = new Set(["find", "glob", "grep", "ls", "read", "search_and_read"]);
44
+ const STATE_MUTATING_TOOL_SET = new Set(["edit", "write", "multi_edit", "notebook_edit"]);
41
45
  let consecutiveCount = 0;
42
46
  let lastSignature = "";
43
47
  let lastToolName = "";
44
48
  let enabled = true;
45
49
  /** Per-tool-name call counts within the current turn (#783 Brief C). */
46
50
  const perToolCounts = new Map();
51
+ let mutationEpoch = 0;
52
+ const perToolLastMutationEpoch = new Map();
47
53
  /** Hash tool name + args into a compact signature for comparison. */
48
54
  function hashToolCall(toolName, args) {
49
55
  const h = createHash("sha256");
@@ -67,7 +73,8 @@ function hashToolCall(toolName, args) {
67
73
  * 1. Identical-signature streak (MAX_CONSECUTIVE_IDENTICAL_CALLS, strict for
68
74
  * ask_user_questions).
69
75
  * 2. Per-tool-name cap (PER_TOOL_DEFAULT_CAP / PER_TOOL_REPEATABLE_CAP),
70
- * independent of args catches improvisation loops (#783).
76
+ * independent of args, reset after file-mutation progress — catches
77
+ * improvisation loops (#783).
71
78
  */
72
79
  export function checkToolCallLoop(toolName, args) {
73
80
  if (!enabled)
@@ -97,8 +104,17 @@ export function checkToolCallLoop(toolName, args) {
97
104
  // ── Guard 2: per-tool-name cap, independent of args (#783 Brief C) ──
98
105
  // Catches improvisation loops where the same tool is invoked many times with
99
106
  // varied args (e.g. retrying a missing workflow tool via bash/node -e/CLI).
100
- const perToolCount = (perToolCounts.get(toolName) ?? 0) + 1;
107
+ const priorPerToolCount = (perToolLastMutationEpoch.get(toolName) ?? mutationEpoch) < mutationEpoch
108
+ ? 0
109
+ : (perToolCounts.get(toolName) ?? 0);
110
+ const perToolCount = priorPerToolCount + 1;
101
111
  perToolCounts.set(toolName, perToolCount);
112
+ perToolLastMutationEpoch.set(toolName, mutationEpoch);
113
+ // Read-only navigation tools are normal context gathering; Guard 1 still
114
+ // catches true reread loops with identical arguments.
115
+ if (PER_TOOL_CAP_EXEMPT_TOOLS.has(toolName)) {
116
+ return { block: false, count: consecutiveCount };
117
+ }
102
118
  const perToolCap = INHERENTLY_REPEATABLE_TOOL_SET.has(toolName)
103
119
  ? PER_TOOL_REPEATABLE_CAP
104
120
  : PER_TOOL_DEFAULT_CAP;
@@ -114,6 +130,14 @@ export function checkToolCallLoop(toolName, args) {
114
130
  }
115
131
  return { block: false, count: consecutiveCount };
116
132
  }
133
+ /** Record successful mutation progress so Guard 2 can decay on the next count. */
134
+ export function recordToolCallLoopMutation(toolName) {
135
+ if (!enabled)
136
+ return;
137
+ if (!STATE_MUTATING_TOOL_SET.has(toolName))
138
+ return;
139
+ mutationEpoch++;
140
+ }
117
141
  /** Reset the guard state. Call at agent turn boundaries. */
118
142
  export function resetToolCallLoopGuard() {
119
143
  consecutiveCount = 0;
@@ -121,6 +145,8 @@ export function resetToolCallLoopGuard() {
121
145
  lastToolName = "";
122
146
  enabled = true;
123
147
  perToolCounts.clear();
148
+ mutationEpoch = 0;
149
+ perToolLastMutationEpoch.clear();
124
150
  }
125
151
  /** Disable the guard (e.g. during shutdown). */
126
152
  export function disableToolCallLoopGuard() {
@@ -129,6 +155,8 @@ export function disableToolCallLoopGuard() {
129
155
  lastSignature = "";
130
156
  lastToolName = "";
131
157
  perToolCounts.clear();
158
+ mutationEpoch = 0;
159
+ perToolLastMutationEpoch.clear();
132
160
  }
133
161
  /** Get current consecutive count for diagnostics. */
134
162
  export function getToolCallLoopCount() {