@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-ai",
3
- "version": "1.3.0-dev.72e3af2a",
3
+ "version": "1.3.0-dev.738160b9",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "gsd": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-coding-agent",
3
- "version": "1.3.0-dev.72e3af2a",
3
+ "version": "1.3.0-dev.738160b9",
4
4
  "description": "Coding agent CLI (vendored from earendil-works/pi)",
5
5
  "type": "module",
6
6
  "gsd": {
@@ -33,7 +33,7 @@
33
33
  "copy-assets": "node scripts/copy-assets.cjs"
34
34
  },
35
35
  "dependencies": {
36
- "@opengsd/contracts": "^1.3.0-dev.72e3af2a",
36
+ "@opengsd/contracts": "^1.3.0-dev.738160b9",
37
37
  "@mariozechner/jiti": "^2.6.2",
38
38
  "@silvia-odwyer/photon-node": "0.3.4",
39
39
  "chalk": "5.6.2",
@@ -53,11 +53,11 @@
53
53
  "typebox": "1.1.38",
54
54
  "undici": "7.28.0",
55
55
  "yaml": "2.9.0",
56
- "@gsd/agent-core": "^1.3.0-dev.72e3af2a",
57
- "@gsd/native": "^1.3.0-dev.72e3af2a",
58
- "@gsd/pi-agent-core": "^1.3.0-dev.72e3af2a",
59
- "@gsd/pi-ai": "^1.3.0-dev.72e3af2a",
60
- "@gsd/pi-tui": "^1.3.0-dev.72e3af2a",
56
+ "@gsd/agent-core": "^1.3.0-dev.738160b9",
57
+ "@gsd/native": "^1.3.0-dev.738160b9",
58
+ "@gsd/pi-agent-core": "^1.3.0-dev.738160b9",
59
+ "@gsd/pi-ai": "^1.3.0-dev.738160b9",
60
+ "@gsd/pi-tui": "^1.3.0-dev.738160b9",
61
61
  "@sinclair/typebox": "^0.34.41"
62
62
  },
63
63
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-tui",
3
- "version": "1.3.0-dev.72e3af2a",
3
+ "version": "1.3.0-dev.738160b9",
4
4
  "description": "Terminal UI library (vendored from earendil-works/pi)",
5
5
  "type": "module",
6
6
  "gsd": {
@@ -21,7 +21,7 @@
21
21
  "build": "node ../../scripts/clean-package-dist.cjs && tsc -p tsconfig.json --incremental false"
22
22
  },
23
23
  "dependencies": {
24
- "@gsd/native": "^1.3.0-dev.72e3af2a",
24
+ "@gsd/native": "^1.3.0-dev.738160b9",
25
25
  "get-east-asian-width": "1.6.0",
26
26
  "marked": "15.0.12",
27
27
  "@sinclair/typebox": "^0.34.41"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/rpc-client",
3
- "version": "1.3.0-dev.72e3af2a",
3
+ "version": "1.3.0-dev.738160b9",
4
4
  "description": "Standalone RPC client SDK for GSD — zero internal dependencies",
5
5
  "license": "MIT",
6
6
  "gsd": {
@@ -34,7 +34,7 @@
34
34
  "test": "node --test dist/rpc-client.test.js"
35
35
  },
36
36
  "dependencies": {
37
- "@opengsd/contracts": "^1.3.0-dev.72e3af2a"
37
+ "@opengsd/contracts": "^1.3.0-dev.738160b9"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=22.0.0"
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "1.3.0-dev.72e3af2a",
3
+ "version": "1.3.0-dev.738160b9",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -48,6 +48,15 @@ export const CLAUDE_CODE_MODELS = [
48
48
  contextWindow: 1_000_000,
49
49
  maxTokens: 128_000,
50
50
  },
51
+ {
52
+ id: "claude-sonnet-5",
53
+ name: "Claude Sonnet 5 (via Claude Code)",
54
+ reasoning: true,
55
+ input: ["text", "image"] as ("text" | "image")[],
56
+ cost: ZERO_COST,
57
+ contextWindow: 1_000_000,
58
+ maxTokens: 128_000,
59
+ },
51
60
  {
52
61
  id: "claude-sonnet-4-6",
53
62
  name: "Claude Sonnet 4.6 (via Claude Code)",
@@ -30,6 +30,20 @@ test("registers the claude-code provider with a streamSimple delegate", () => {
30
30
  assert.equal(typeof providers[0].config.streamSimple, "function");
31
31
  });
32
32
 
33
+ test("registers Claude Sonnet 5 as a Claude Code CLI model", () => {
34
+ const { pi, providers } = makeMockPi();
35
+ claudeCodeCli(pi as never);
36
+
37
+ const models = providers[0].config.models as Array<Record<string, unknown>>;
38
+ const sonnet5 = models.find((model) => model.id === "claude-sonnet-5");
39
+
40
+ assert.ok(sonnet5, "claude-sonnet-5 must be selectable via the claude-code provider");
41
+ assert.equal(sonnet5.name, "Claude Sonnet 5 (via Claude Code)");
42
+ assert.equal(sonnet5.reasoning, true);
43
+ assert.equal(sonnet5.contextWindow, 1_000_000);
44
+ assert.equal(sonnet5.maxTokens, 128_000);
45
+ });
46
+
33
47
  test("registers a before_provider_request hook to capture the UI context", () => {
34
48
  const { pi, handlers } = makeMockPi();
35
49
  claudeCodeCli(pi as never);
@@ -52,6 +52,7 @@ export type PauseAutoUnitIdentity = {
52
52
 
53
53
  export interface PauseAutoOptions {
54
54
  expectedCurrentUnit?: PauseAutoUnitIdentity | null;
55
+ abortActiveTurn?: boolean;
55
56
  }
56
57
 
57
58
  type PauseAutoFn = (
@@ -56,6 +56,7 @@ export type ThinkingLevelSnapshot = ReturnType<ExtensionAPI["getThinkingLevel"]>
56
56
  export interface PendingVerificationRetry {
57
57
  unitId: string;
58
58
  failureContext: string;
59
+ signature?: string;
59
60
  attempt: number;
60
61
  }
61
62
 
@@ -211,6 +212,13 @@ export class AutoSession {
211
212
  * the issues after MAX_PRE_EXEC_RETRIES re-attempts.
212
213
  */
213
214
  readonly preExecRetryCount: Map<string, number> = new Map();
215
+ /**
216
+ * Tracks how many times each slice unit has been re-dispatched to plan-slice
217
+ * because task plan files were missing. Keyed by unitId (e.g. "M001/S01").
218
+ * Separate from preExecRetryCount so pre-exec gate failures do not block or
219
+ * conflate with missing-task-plan recovery.
220
+ */
221
+ readonly missingTaskPlanRetryCount: Map<string, number> = new Map();
214
222
 
215
223
  // ── Tool invocation errors (#2883) ──────────────────────────────────
216
224
  /** Set when a GSD tool execution ends with isError due to malformed/truncated
@@ -412,6 +420,7 @@ export class AutoSession {
412
420
  this.consecutiveCompleteBootstraps = 0;
413
421
  this.lastPreExecFailure = null;
414
422
  this.preExecRetryCount.clear();
423
+ this.missingTaskPlanRetryCount.clear();
415
424
  this.lastToolInvocationError = null;
416
425
  this.toolUnavailableRetries = 0;
417
426
  this.lastUnitAgentEndMessages = null;
@@ -63,7 +63,9 @@ export function decideVerificationRetry(input: {
63
63
  }
64
64
 
65
65
  const key = verificationRetryKey(unitType, retryInfo.unitId);
66
- const failureHash = hashVerificationFailureContext(retryInfo.failureContext);
66
+ const failureHash = hashVerificationFailureContext(
67
+ retryInfo.signature ?? retryInfo.failureContext,
68
+ );
67
69
  if (input.previousFailureHash === failureHash) {
68
70
  return {
69
71
  action: "pause",
@@ -79,7 +79,9 @@ export function resolveCompactionThresholdPercent(raw: number | undefined): numb
79
79
  return value <= 1 ? value * 100 : value;
80
80
  }
81
81
 
82
- export function shouldRerootStepSessionForContext(
82
+ export const HARD_CONTEXT_REROOT_THRESHOLD_PERCENT = 90;
83
+
84
+ export function shouldWarnStepSessionForContext(
83
85
  contextPercent: number | null | undefined,
84
86
  compactionThresholdPercent?: number,
85
87
  ): boolean {
@@ -88,3 +90,12 @@ export function shouldRerootStepSessionForContext(
88
90
  resolveCompactionThresholdPercent(compactionThresholdPercent),
89
91
  ) === "pause";
90
92
  }
93
+
94
+ export function shouldRerootStepSessionForContext(
95
+ contextPercent: number | null | undefined,
96
+ ): boolean {
97
+ return getContextPauseAction(
98
+ contextPercent,
99
+ HARD_CONTEXT_REROOT_THRESHOLD_PERCENT,
100
+ ) === "pause";
101
+ }
@@ -1523,6 +1523,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
1523
1523
  const sid = state.activeSlice!.id;
1524
1524
  const sTitle = state.activeSlice!.title;
1525
1525
  const tid = state.activeTask.id;
1526
+ const unitId = `${mid}/${sid}`;
1526
1527
  const artifactBasePath = resolveArtifactBasePath(basePath, mid, session);
1527
1528
 
1528
1529
  // Guard: if the slice plan exists but the individual task plan files are
@@ -1562,6 +1563,17 @@ export const DISPATCH_RULES: DispatchRule[] = [
1562
1563
  !existsSync(projectionTaskPlanPath) &&
1563
1564
  !tasksEmbeddedInSlicePlan
1564
1565
  ) {
1566
+ const MAX_MISSING_TASK_PLAN_RETRIES = 2;
1567
+ const retryCount = session?.missingTaskPlanRetryCount?.get(unitId) ?? 0;
1568
+ if (retryCount >= MAX_MISSING_TASK_PLAN_RETRIES) {
1569
+ session?.missingTaskPlanRetryCount?.delete(unitId);
1570
+ return {
1571
+ action: "stop",
1572
+ 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.`,
1573
+ level: "error",
1574
+ };
1575
+ }
1576
+ session?.missingTaskPlanRetryCount?.set(unitId, retryCount + 1);
1565
1577
  if (isDebugEnabled()) {
1566
1578
  const expectedTaskPlanPath = join(artifactBasePath, relTaskFile(artifactBasePath, mid, sid, tid, "PLAN"));
1567
1579
  const originalProjectRoot = session?.originalBasePath || basePath;
@@ -1585,7 +1597,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
1585
1597
  return {
1586
1598
  action: "dispatch",
1587
1599
  unitType: "plan-slice",
1588
- unitId: `${mid}/${sid}`,
1600
+ unitId,
1589
1601
  prompt: await buildPlanSlicePrompt(
1590
1602
  mid,
1591
1603
  midTitle,
@@ -1598,6 +1610,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
1598
1610
  };
1599
1611
  }
1600
1612
 
1613
+ session?.missingTaskPlanRetryCount?.delete(unitId);
1601
1614
  return null;
1602
1615
  },
1603
1616
  },
@@ -2212,7 +2212,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
2212
2212
  saveCustomVerifyRetryCounts(s, { logFailure: err => debugLog("postUnit", { phase: "save-verify-retries-failed", error: err instanceof Error ? err.message : String(err) }) });
2213
2213
  s.pendingVerificationRetry = {
2214
2214
  unitId: s.currentUnit.id,
2215
- failureContext: `${failureDetails} (attempt ${attempt}/${MAX_ARTIFACT_VERIFICATION_RETRIES}).`,
2215
+ failureContext: failureDetails,
2216
2216
  attempt,
2217
2217
  };
2218
2218
  debugLog("postUnit", { phase: "artifact-verify-retry", unitType: s.currentUnit.type, unitId: s.currentUnit.id, attempt });
@@ -17,7 +17,7 @@ import {
17
17
  resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
18
18
  resolveTasksDir, resolveTaskFiles, resolveTaskFile,
19
19
  relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath,
20
- resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
20
+ relTaskFile, resolveGsdRootFile, relGsdRootFile, resolveRuntimeFile,
21
21
  } from "./paths.js";
22
22
  import { resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
23
23
  import { isContextModeEnabled } from "./preferences-types.js";
@@ -2662,7 +2662,7 @@ export async function buildExecuteTaskPrompt(
2662
2662
 
2663
2663
  const taskPlanPath = resolveTaskFile(base, mid, sid, tid, "PLAN");
2664
2664
  const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
2665
- const taskPlanRelPath = relSlicePath(base, mid, sid) + `/tasks/${tid}-PLAN.md`;
2665
+ const taskPlanRelPath = relTaskFile(base, mid, sid, tid, "PLAN");
2666
2666
  const taskPlanContext = taskPlanContent
2667
2667
  ? [
2668
2668
  "## Inlined Task Plan (authoritative local execution contract)",
@@ -3901,7 +3901,7 @@ export async function buildReactiveExecutePrompt(
3901
3901
 
3902
3902
  const taskPlanPath = resolveTaskFile(base, mid, sid, tid, "PLAN");
3903
3903
  const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
3904
- const taskPlanRelPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-PLAN.md`;
3904
+ const taskPlanRelPath = relTaskFile(base, mid, sid, tid, "PLAN");
3905
3905
  const taskPlanInline = taskPlanContent
3906
3906
  ? [
3907
3907
  "## Inlined Task Plan (authoritative local execution contract)",
@@ -35,7 +35,7 @@ import { isValidationTerminal } from "./state.js";
35
35
  import { getErrorMessage } from "./error-utils.js";
36
36
  import { logWarning, logError } from "./workflow-logger.js";
37
37
  import { readIntegrationBranch } from "./git-service.js";
38
- import { isClosedStatus } from "./status-guards.js";
38
+ import { isClosedStatus, isInactiveStatus } from "./status-guards.js";
39
39
  import {
40
40
  resolveSlicePath,
41
41
  resolveSliceFile,
@@ -295,7 +295,7 @@ export function writeReactiveExecuteBlocker(
295
295
  transaction(() => {
296
296
  for (const tid of summaryPresent) {
297
297
  const task = getTask(mid, sid, tid);
298
- if (!task || isClosedStatus(task.status)) {
298
+ if (!task || isInactiveStatus(task.status)) {
299
299
  unchangedTaskIds.push(tid);
300
300
  continue;
301
301
  }
@@ -304,7 +304,7 @@ export function writeReactiveExecuteBlocker(
304
304
  }
305
305
  for (const tid of summaryMissing) {
306
306
  const task = getTask(mid, sid, tid);
307
- if (!task || isClosedStatus(task.status)) {
307
+ if (!task || isInactiveStatus(task.status)) {
308
308
  unchangedTaskIds.push(tid);
309
309
  continue;
310
310
  }
@@ -29,6 +29,7 @@ import {
29
29
  runVerificationGate,
30
30
  runVerificationGateForTargets,
31
31
  formatFailureContext,
32
+ formatFailureSignature,
32
33
  captureRuntimeErrors,
33
34
  runDependencyAudit,
34
35
  } from "./verification-gate.js";
@@ -871,10 +872,13 @@ export async function runPostUnitVerification(
871
872
  );
872
873
  }
873
874
  const nextAttempt = attempt + 1;
875
+ const failureContext = verdict.failureContext || formatFailureContext(result);
876
+ const failureSignature = formatFailureSignature(result);
874
877
  s.verificationRetryCount.set(retryKey, nextAttempt);
875
878
  s.pendingVerificationRetry = {
876
879
  unitId: s.currentUnit.id,
877
- failureContext: verdict.failureContext || formatFailureContext(result),
880
+ failureContext,
881
+ ...(failureSignature ? { signature: failureSignature } : {}),
878
882
  attempt: nextAttempt,
879
883
  };
880
884
  const failedCmds = result.checks
@@ -97,8 +97,10 @@ import {
97
97
  getNewBudgetAlertLevel,
98
98
  getBudgetEnforcementAction,
99
99
  getContextPauseAction,
100
+ HARD_CONTEXT_REROOT_THRESHOLD_PERCENT,
100
101
  resolveCompactionThresholdPercent,
101
102
  shouldRerootStepSessionForContext,
103
+ shouldWarnStepSessionForContext,
102
104
  } from "./auto-budget.js";
103
105
  import {
104
106
  markToolStart as _markToolStart,
@@ -701,6 +703,7 @@ export {
701
703
  getContextPauseAction,
702
704
  resolveCompactionThresholdPercent,
703
705
  shouldRerootStepSessionForContext,
706
+ shouldWarnStepSessionForContext,
704
707
  } from "./auto-budget.js";
705
708
 
706
709
  function closeOutSignalInterruptedUnit(currentBasePath: string): void {
@@ -1375,9 +1378,10 @@ export async function rerootCommandSession(
1375
1378
  }
1376
1379
 
1377
1380
  /**
1378
- * After a completed step-mode unit, reclaim context headroom when usage is at
1379
- * or above the compaction threshold. Writes a context-mode snapshot first, then
1380
- * re-roots the visible command session while leaving the NEXT progress surface.
1381
+ * After a completed step-mode unit, warn when usage reaches the configurable
1382
+ * soft context threshold. At the fixed hard boundary, write a context-mode
1383
+ * snapshot and re-root the visible command session while leaving the NEXT
1384
+ * progress surface.
1381
1385
  */
1382
1386
  export async function maybeRerootStepSessionForHighContext(
1383
1387
  ctx: ExtensionContext,
@@ -1392,7 +1396,19 @@ export async function maybeRerootStepSessionForHighContext(
1392
1396
  const prefs = loadEffectiveGSDPreferences(workspaceRoot);
1393
1397
  const compactionThreshold = prefs?.preferences.context_management?.compaction_threshold_percent;
1394
1398
 
1395
- if (!shouldRerootStepSessionForContext(contextPercent, compactionThreshold)) {
1399
+ const shouldWarn = shouldWarnStepSessionForContext(contextPercent, compactionThreshold);
1400
+ const shouldReroot = shouldRerootStepSessionForContext(contextPercent);
1401
+ if (!shouldWarn && !shouldReroot) {
1402
+ return { rerooted: false };
1403
+ }
1404
+
1405
+ const displayPercent = (contextPercent as number).toFixed(1);
1406
+ const thresholdDisplay = resolveCompactionThresholdPercent(compactionThreshold).toFixed(0);
1407
+ if (!shouldReroot) {
1408
+ ctx.ui.notify(
1409
+ `Step complete — context at ${displayPercent}% (soft threshold: ${thresholdDisplay}%). Use /compact when convenient; continuing in this session is still safe.`,
1410
+ "info",
1411
+ );
1396
1412
  return { rerooted: false };
1397
1413
  }
1398
1414
 
@@ -1401,12 +1417,10 @@ export async function maybeRerootStepSessionForHighContext(
1401
1417
  const { writeContextModeCompactionSnapshot } = await import("./context-mode-snapshot.js");
1402
1418
  await writeContextModeCompactionSnapshot(snapshotBase);
1403
1419
 
1404
- const displayPercent = (contextPercent as number).toFixed(1);
1405
- const thresholdDisplay = resolveCompactionThresholdPercent(compactionThreshold).toFixed(0);
1406
1420
  const result = await rerootCommandSession(cmdCtx, workspaceRoot);
1407
1421
  if (result.status === "ok") {
1408
1422
  ctx.ui.notify(
1409
- `Step complete — context at ${displayPercent}% (threshold: ${thresholdDisplay}%). Fresh session ready for /gsd next.`,
1423
+ `Step complete — context at ${displayPercent}% (hard threshold: ${HARD_CONTEXT_REROOT_THRESHOLD_PERCENT}%). Fresh session ready for /gsd next.`,
1410
1424
  "info",
1411
1425
  );
1412
1426
  return { rerooted: true };
@@ -2102,7 +2116,7 @@ export function _selectStopAutoWorktreeExit(args: {
2102
2116
  /**
2103
2117
  * Pause auto-mode without destroying state. Context is preserved.
2104
2118
  * The user can interact with the agent, then `/gsd auto` resumes
2105
- * from disk state. Called when the user presses Escape during auto-mode.
2119
+ * from disk state. Called when the user presses Escape or runs `/gsd pause`.
2106
2120
  */
2107
2121
  export async function pauseAuto(
2108
2122
  ctx?: ExtensionContext,
@@ -2129,6 +2143,7 @@ export async function pauseAuto(
2129
2143
 
2130
2144
  s.active = false;
2131
2145
  s.paused = true;
2146
+ if (options.abortActiveTurn && ctx && !ctx.isIdle()) ctx.abort();
2132
2147
  clearUnitTimeout();
2133
2148
  stopAutoCommandPolling();
2134
2149
 
@@ -28,6 +28,7 @@ const NON_REPEATABLE_FROM_MINIMAL_BASE = new Set<string>([
28
28
 
29
29
  /** Additional core tools not in MINIMAL_AUTO_BASE but routinely multi-called per turn. */
30
30
  const EXTRA_INHERENTLY_REPEATABLE = [
31
+ "gsd_exec",
31
32
  "multi_edit",
32
33
  "todo_write",
33
34
  "notebook_edit",
@@ -690,8 +690,9 @@ export function registerDbTools(pi: ExtensionAPI): void {
690
690
  promptSnippet: "Plan a slice via DB write + PLAN render + cache invalidation",
691
691
  promptGuidelines: [
692
692
  "Use gsd_plan_slice for slice planning instead of writing S##-PLAN.md or task PLAN files directly.",
693
- "Keep parameters flat and provide the full slice planning payload, including tasks.",
694
- "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.",
693
+ "For incremental planning, call gsd_plan_slice with slice metadata only, then call gsd_plan_task once per task.",
694
+ "When tasks is omitted or empty, the tool writes only slice planning metadata and preserves existing tasks.",
695
+ "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.",
695
696
  "Use the canonical name gsd_plan_slice; gsd_slice_plan is only an alias.",
696
697
  ],
697
698
  parameters: Type.Object({
@@ -699,7 +700,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
699
700
  milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
700
701
  sliceId: Type.String({ description: "Slice ID (e.g. S01)" }),
701
702
  goal: Type.String({ description: "Slice goal" }),
702
- tasks: Type.Array(Type.Object({
703
+ tasks: Type.Optional(Type.Array(Type.Object({
703
704
  taskId: Type.String({ description: "Task ID (e.g. T01)" }),
704
705
  title: Type.String({ description: "Task title" }),
705
706
  description: Type.String({ description: "Task description / steps block" }),
@@ -709,7 +710,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
709
710
  inputs: Type.Array(Type.String(), { description: "Array<string> of input files or references; pass [\"path\"] or [], never a single string" }),
710
711
  expectedOutput: Type.Array(Type.String(), { description: "Array<string> of files this task creates or overwrites; pass [\"path\"] or [], never prose or a single string" }),
711
712
  observabilityImpact: Type.Optional(Type.String({ description: "Task observability impact" })),
712
- }), { description: "Planned tasks for the slice" }),
713
+ }), { description: "Optional full task replacement for the slice. Omit for incremental planning, then call gsd_plan_task once per task." })),
713
714
  // ── Enrichment metadata (optional — defaults to empty) ────────────
714
715
  successCriteria: Type.Optional(Type.String({ description: "Slice success criteria block" })),
715
716
  proofLevel: Type.Optional(Type.String({ description: "Slice proof level" })),
@@ -18,6 +18,7 @@ import { registerShortcuts } from "./register-shortcuts.js";
18
18
  import { writeCrashLog } from "./crash-log.js";
19
19
  import { logWarning, isGsdExtensionStderrEnabled } from "../workflow-logger.js";
20
20
  import { UNIT_TOOL_CONTRACTS } from "../unit-tool-contracts.js";
21
+ import { installManifestFlushOnProcessTeardown } from "../workflow-manifest.js";
21
22
  // Static import so cmux event listeners are registered synchronously during
22
23
  // extension bootstrap. Prior implementation used `void import().then()` which
23
24
  // queued listener registration as a microtask — any CMUX_CHANNELS emit fired
@@ -196,6 +197,7 @@ export function registerGsdExtension(pi: ExtensionAPI): void {
196
197
  });
197
198
 
198
199
  installEpipeGuard();
200
+ installManifestFlushOnProcessTeardown();
199
201
 
200
202
  // Ecosystem handlers captured by the GSDExtensionAPI wrapper for the
201
203
  // GSD-owned `before_agent_start` dispatch step (#3338).
@@ -34,7 +34,7 @@ import {
34
34
  } from "../auto-runtime-state.js";
35
35
  import { applyProviderPayloadPolicy } from "../provider-payload-policy.js";
36
36
 
37
- import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
37
+ import { checkToolCallLoop, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
38
38
  import { MINIMAL_AUTO_BASE_TOOL_NAMES } from "./core-session-tools.js";
39
39
  import { maybePauseAutoForApprovalGate, resetPendingGatePauseGuard } from "./pending-gate-pause.js";
40
40
  import { saveActivityLog } from "../activity-log.js";
@@ -76,6 +76,7 @@ import { supportsSourceObservationsForUnit } from "../source-observations.js";
76
76
  import { clearPendingAutoStart } from "../pending-auto-start.js";
77
77
  import { resolveWorkflowToolBasePath } from "./dynamic-tools.js";
78
78
  import { getRequiredWorkflowToolsForUnit } from "../unit-tool-contracts.js";
79
+ import { flushAllManifests } from "../workflow-manifest.js";
79
80
 
80
81
  let approvalQuestionAbortInFlight = false;
81
82
 
@@ -583,6 +584,21 @@ function contextBasePath(ctx?: { cwd?: string }): string {
583
584
  return typeof ctx?.cwd === "string" ? ctx.cwd : process.cwd();
584
585
  }
585
586
 
587
+ const LOOP_GUARD_INTERACTIVE_INSTRUCTIONS = [
588
+ "Do not retry this tool or call other tools this turn — stop and respond to the user in text.",
589
+ "Do not retry this tool or pivot to other tools this turn — stop and respond to the user in text.",
590
+ ];
591
+ const LOOP_GUARD_AUTO_INSTRUCTION =
592
+ "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.";
593
+
594
+ function formatLoopGuardBlockReason(reason: string | undefined): string | undefined {
595
+ if (!reason || !getAutoRuntimeSnapshot().active) return reason;
596
+ return LOOP_GUARD_INTERACTIVE_INSTRUCTIONS.reduce(
597
+ (formatted, instruction) => formatted.replace(instruction, LOOP_GUARD_AUTO_INSTRUCTION),
598
+ reason,
599
+ );
600
+ }
601
+
586
602
  function beginSourceObservationStoreForCurrentUnit(
587
603
  ctx?: { cwd?: string },
588
604
  ): ReturnType<typeof getSourceObservationStore> | null {
@@ -906,6 +922,15 @@ export function registerHooks(
906
922
  }
907
923
  }
908
924
  }
925
+ const projectRoot = resolveWorktreeProjectRoot(basePath);
926
+ const { pruneStaleFlatPhaseBackups } = await import("../flat-phase-migration.js");
927
+ const pruned = pruneStaleFlatPhaseBackups(projectRoot);
928
+ if (pruned > 0) {
929
+ safetyLogWarning(
930
+ "bootstrap",
931
+ `pruned ${pruned} stale flat-phase migration backup(s) from .gsd-backups/ (retention exceeded)`,
932
+ );
933
+ }
909
934
  } catch (err) {
910
935
  safetyLogWarning("bootstrap", `flat-phase migration: ${err instanceof Error ? err.message : String(err)}`);
911
936
  }
@@ -1071,6 +1096,19 @@ export function registerHooks(
1071
1096
  const { handleAgentEnd } = await import("./agent-end-recovery.js");
1072
1097
  const agentEndBasePath = contextBasePath(ctx);
1073
1098
  try {
1099
+ // The manifest is a non-critical projection (the append-only event log is
1100
+ // the authoritative recovery source), so a flush failure must not skip
1101
+ // agent_end recovery. drainManifestWrites still propagates write failures
1102
+ // to explicit flush callers; here we deliberately log and continue so
1103
+ // handleAgentEnd (and the finally below) always run.
1104
+ try {
1105
+ await flushAllManifests();
1106
+ } catch (err) {
1107
+ safetyLogWarning(
1108
+ "manifest",
1109
+ `flushAllManifests on agent_end failed: ${err instanceof Error ? err.message : String(err)}`,
1110
+ );
1111
+ }
1074
1112
  await handleAgentEnd(pi, event, ctx);
1075
1113
  } finally {
1076
1114
  activateDeferredApprovalGate(agentEndBasePath);
@@ -1277,7 +1315,7 @@ export function registerHooks(
1277
1315
  // ── Loop guard: block repeated identical tool calls ──
1278
1316
  const loopCheck = checkToolCallLoop(toolName, event.input as Record<string, unknown>);
1279
1317
  if (loopCheck.block) {
1280
- return { block: true, reason: loopCheck.reason };
1318
+ return { block: true, reason: formatLoopGuardBlockReason(loopCheck.reason) };
1281
1319
  }
1282
1320
 
1283
1321
  const deferredGateGuard = shouldBlockDeferredApprovalTool(
@@ -1549,6 +1587,7 @@ export function registerHooks(
1549
1587
  }
1550
1588
  }
1551
1589
  if (!event.isError) {
1590
+ recordToolCallLoopMutation(toolName);
1552
1591
  refreshSourceObservationAfterMutation(toolName, event.input, ctx);
1553
1592
  clearSourceObservationsAfterShell(toolName);
1554
1593
  }