@moretea-labs/forge 1.8.1 → 1.8.2

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 (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -26,6 +26,7 @@ export interface GeneratedCacheRetentionOptions {
26
26
  maxRemovals?: number;
27
27
  processCommands?: string[];
28
28
  rootStartIndex?: number;
29
+ scanStartIndex?: number;
29
30
  }
30
31
 
31
32
  export interface GeneratedCacheRetentionReport {
@@ -95,6 +96,7 @@ export interface XCTestDeviceCleanupReport {
95
96
 
96
97
  interface Candidate {
97
98
  path: string;
99
+ cacheRoot?: string;
98
100
  kind:
99
101
  | 'forge_ios_derived_data'
100
102
  | 'harness_derived_data'
@@ -119,7 +121,7 @@ function pathInside(root: string, path: string): boolean {
119
121
  return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`);
120
122
  }
121
123
 
122
- function collectProcessCommands(): string[] {
124
+ function collectProcessCommands(): string[] | undefined {
123
125
  try {
124
126
  return execFileSync('ps', ['-axo', 'command='], {
125
127
  encoding: 'utf8',
@@ -127,7 +129,8 @@ function collectProcessCommands(): string[] {
127
129
  maxBuffer: 2 * 1024 * 1024,
128
130
  }).split('\n').map((line) => line.trim()).filter(Boolean);
129
131
  } catch {
130
- return [];
132
+ // Missing process evidence is not proof that a cache is idle.
133
+ return undefined;
131
134
  }
132
135
  }
133
136
 
@@ -142,6 +145,19 @@ function commandReferencesPath(commands: readonly string[], repoRoot: string, pa
142
145
  || (canonicalRelative && command.includes(canonicalRelative)));
143
146
  }
144
147
 
148
+ function hasSymlinkAncestor(root: string, path: string): boolean {
149
+ const rel = relative(resolve(root), resolve(path));
150
+ if (!rel || rel === '..' || rel.startsWith(`..${sep}`)) return true;
151
+ let current = resolve(root);
152
+ try {
153
+ for (const part of rel.split(sep)) {
154
+ current = join(current, part);
155
+ if (lstatSync(current).isSymbolicLink()) return true;
156
+ }
157
+ return false;
158
+ } catch { return true; }
159
+ }
160
+
145
161
  function containsTrackedFiles(repoRoot: string, path: string): boolean {
146
162
  const rel = relative(resolve(repoRoot), resolve(path));
147
163
  if (!rel || rel === '..' || rel.startsWith(`..${sep}`)) return true;
@@ -158,92 +174,106 @@ function containsTrackedFiles(repoRoot: string, path: string): boolean {
158
174
  }
159
175
  }
160
176
 
161
- function directChildDirectories(root: string, predicate: (name: string) => boolean, budget: { remaining: number }): string[] {
162
- if (!existsSync(root) || budget.remaining <= 0) return [];
177
+ const REBUILDABLE_XCODE_CHILDREN = [
178
+ 'Build', 'ModuleCache.noindex', 'Index.noindex', 'CompilationCache.noindex',
179
+ 'SDKExplicitPrecompiledModules', 'SDKStatCaches.noindex',
180
+ ] as const;
181
+
182
+ function rotatedEntries(root: string, round: number) {
183
+ try {
184
+ const entries = readdirSync(root, { withFileTypes: true }).sort((a, b) => String(a.name).localeCompare(String(b.name)));
185
+ if (entries.length > 1) {
186
+ const offset = ((Math.trunc(round) % entries.length) + entries.length) % entries.length;
187
+ entries.push(...entries.splice(0, offset));
188
+ }
189
+ return entries;
190
+ } catch { return []; }
191
+ }
192
+
193
+ function directChildDirectories(root: string, predicate: (name: string) => boolean, budget: { remaining: number }, round = 0): string[] {
163
194
  const found: string[] = [];
164
- let entries;
165
- try { entries = readdirSync(root, { withFileTypes: true }); } catch { return found; }
166
- for (const entry of entries) {
195
+ if (budget.remaining <= 0) return found;
196
+ for (const entry of rotatedEntries(root, round)) {
167
197
  if (budget.remaining <= 0) break;
168
198
  budget.remaining -= 1;
169
- const name = String(entry.name);
170
- if (entry.isDirectory() && predicate(name)) found.push(join(root, name));
199
+ if (entry.isDirectory() && predicate(String(entry.name))) found.push(join(root, String(entry.name)));
171
200
  }
172
201
  return found;
173
202
  }
174
203
 
175
204
  function directChildFiles(root: string, budget: { remaining: number }): string[] {
176
- if (!existsSync(root) || budget.remaining <= 0) return [];
177
205
  const found: string[] = [];
178
- let entries;
179
- try { entries = readdirSync(root, { withFileTypes: true }); } catch { return found; }
180
- for (const entry of entries) {
206
+ if (budget.remaining <= 0) return found;
207
+ for (const entry of rotatedEntries(root, 0)) {
181
208
  if (budget.remaining <= 0) break;
182
209
  budget.remaining -= 1;
183
- if (entry.isFile()) found.push(join(root, entry.name));
210
+ if (entry.isFile()) found.push(join(root, String(entry.name)));
184
211
  }
185
212
  return found;
186
213
  }
187
214
 
188
- function isDerivedDataDirectoryName(name: string): boolean {
189
- return name === 'DerivedData' || name.startsWith('DerivedData-');
215
+ function isXcodeCacheRoot(root: string): boolean {
216
+ try {
217
+ const info = lstatSync(join(root, 'info.plist'));
218
+ return info.isFile() && !info.isSymbolicLink() && REBUILDABLE_XCODE_CHILDREN.some((name) => {
219
+ try { const stat = lstatSync(join(root, name)); return stat.isDirectory() && !stat.isSymbolicLink(); } catch { return false; }
220
+ });
221
+ } catch { return false; }
190
222
  }
191
223
 
192
- function nestedDerivedDataDirectories(root: string, maxDepth: number, budget: { remaining: number }): string[] {
193
- if (!existsSync(root) || budget.remaining <= 0) return [];
224
+ function nestedDerivedDataDirectories(root: string, maxDepth: number, budget: { remaining: number }, round: number): string[] {
194
225
  const found: string[] = [];
195
- const visit = (dir: string, depth: number): void => {
226
+ const visit = (dir: string, depth: number, visitRound: number): void => {
196
227
  if (depth > maxDepth || budget.remaining <= 0) return;
197
- let entries;
198
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
228
+ const entries = rotatedEntries(dir, visitRound);
229
+ // Divide at each level rather than correlating parent/child rotations.
230
+ const childRound = Math.floor(visitRound / Math.max(1, entries.length));
199
231
  for (const entry of entries) {
200
232
  if (budget.remaining <= 0) return;
201
233
  budget.remaining -= 1;
202
- if (!entry.isDirectory()) continue;
203
- const path = join(dir, String(entry.name));
204
- if (isDerivedDataDirectoryName(String(entry.name))) {
205
- found.push(path);
206
- continue;
207
- }
208
- visit(path, depth + 1);
234
+ const name = String(entry.name);
235
+ if (!entry.isDirectory() || ['.git', 'node_modules', 'SourcePackages', 'Logs', ...REBUILDABLE_XCODE_CHILDREN].includes(name) || name.endsWith('.xcresult')) continue;
236
+ const path = join(dir, name);
237
+ if (isXcodeCacheRoot(path)) found.push(path);
238
+ else visit(path, depth + 1, childRound);
209
239
  }
210
240
  };
211
- visit(root, 0);
241
+ visit(root, 0, round);
212
242
  return found;
213
243
  }
214
244
 
215
- function discoverCandidates(repoRoot: string, maxEntries: number): { candidates: Candidate[]; inspected: number; truncated: boolean } {
245
+ function discoverCandidates(repoRoot: string, maxEntries: number, scanRound: number): { candidates: Candidate[]; inspected: number; truncated: boolean } {
216
246
  const root = resolve(repoRoot);
217
247
  const budget = { remaining: Math.max(1, maxEntries) };
218
248
  const initial = budget.remaining;
219
249
  const candidates: Candidate[] = [];
220
-
221
- // Browser artifacts are direct children of small, explicitly owned roots.
222
- // Scan them before recursively walking potentially high-cardinality cache
223
- // trees so cache history cannot starve their retention policy.
224
- for (const path of directChildFiles(join(root, '.forge', 'browser', 'uploads'), budget)) {
225
- candidates.push({ path, kind: 'forge_browser_upload' });
226
- }
227
- for (const path of directChildFiles(join(root, '.forge', 'browser', 'screenshots'), budget)) {
228
- candidates.push({ path, kind: 'forge_browser_screenshot' });
229
- }
230
- for (const path of directChildDirectories(join(root, '.forge', 'ios', 'DerivedData'), () => true, budget)) {
231
- candidates.push({ path, kind: 'forge_ios_derived_data' });
232
- }
233
- for (const path of directChildDirectories(join(root, '.ai', 'harness'), (name) => name.startsWith('deriveddata-'), budget)) {
234
- candidates.push({ path, kind: 'harness_derived_data' });
235
- }
236
- for (const path of nestedDerivedDataDirectories(join(root, '.repo-harness'), 5, budget)) {
237
- candidates.push({ path, kind: 'repo_harness_derived_data' });
238
- }
239
- for (const path of nestedDerivedDataDirectories(join(root, '.tmp'), 6, budget)) {
240
- candidates.push({ path, kind: 'tmp_derived_data' });
250
+ // Preserve browser staging priority. Cache namespace and nested entry rotations
251
+ // use independent visit ordinals, so protected early entries cannot starve peers.
252
+ for (const path of directChildFiles(join(root, '.forge', 'browser', 'uploads'), budget)) candidates.push({ path, kind: 'forge_browser_upload' });
253
+ for (const path of directChildFiles(join(root, '.forge', 'browser', 'screenshots'), budget)) candidates.push({ path, kind: 'forge_browser_screenshot' });
254
+ const namespaces: Array<{ root: string; kind: Candidate['kind']; depth?: number }> = [
255
+ { root: join(root, '.forge', 'ios', 'DerivedData'), kind: 'forge_ios_derived_data' },
256
+ { root: join(root, '.ai', 'harness'), kind: 'harness_derived_data' },
257
+ { root: join(root, '.repo-harness'), kind: 'repo_harness_derived_data', depth: 5 },
258
+ { root: join(root, '.tmp'), kind: 'tmp_derived_data', depth: 6 },
259
+ ];
260
+ const offset = ((Math.trunc(scanRound) % namespaces.length) + namespaces.length) % namespaces.length;
261
+ namespaces.push(...namespaces.splice(0, offset));
262
+ const entryRound = Math.floor(scanRound / namespaces.length);
263
+ for (const namespace of namespaces) {
264
+ const cacheRoots = namespace.depth === undefined
265
+ ? directChildDirectories(namespace.root, () => true, budget, entryRound).filter(isXcodeCacheRoot)
266
+ : nestedDerivedDataDirectories(namespace.root, namespace.depth, budget, entryRound);
267
+ for (const cacheRoot of cacheRoots) {
268
+ // Never remove the bucket: Logs, results, SourcePackages and unknown files
269
+ // are evidence/dependencies, not disposable build cache.
270
+ for (const name of REBUILDABLE_XCODE_CHILDREN) {
271
+ const path = join(cacheRoot, name);
272
+ if (existsSync(path)) candidates.push({ path, kind: namespace.kind, cacheRoot });
273
+ }
274
+ }
241
275
  }
242
- return {
243
- candidates: candidates.filter((candidate, index, values) => values.findIndex((item) => resolve(item.path) === resolve(candidate.path)) === index),
244
- inspected: initial - budget.remaining,
245
- truncated: budget.remaining <= 0,
246
- };
276
+ return { candidates, inspected: initial - budget.remaining, truncated: budget.remaining <= 0 };
247
277
  }
248
278
 
249
279
  function retentionGraceMs(candidate: Candidate, explicitGraceMs: number | undefined): number {
@@ -279,7 +309,7 @@ export function cleanupGeneratedRepositoryCaches(
279
309
  const maxEntries = Math.max(1, Math.floor(options.maxEntries ?? DEFAULT_SCAN_BUDGET));
280
310
  let remainingRemovals = Math.max(1, Math.floor(options.maxRemovals ?? DEFAULT_REMOVAL_BUDGET));
281
311
  const commands = options.processCommands ?? collectProcessCommands();
282
- const discovered = discoverCandidates(repoRoot, maxEntries);
312
+ const discovered = discoverCandidates(repoRoot, maxEntries, options.scanStartIndex ?? 0);
283
313
  const report: GeneratedCacheRetentionReport = {
284
314
  inspected: discovered.inspected,
285
315
  eligible: 0,
@@ -290,26 +320,34 @@ export function cleanupGeneratedRepositoryCaches(
290
320
  budgetExhausted: discovered.truncated,
291
321
  };
292
322
 
323
+ if (commands === undefined) {
324
+ report.errors.push('process_inventory_unavailable: generated-cache reclamation skipped');
325
+ report.skippedByReason.process_inventory_unavailable = 1;
326
+ return report;
327
+ }
293
328
  for (const candidate of discovered.candidates) {
294
329
  const ownedRoot = candidateOwnedRoot(repoRoot, candidate);
295
330
  const fileCandidate = candidate.kind === 'forge_browser_upload' || candidate.kind === 'forge_browser_screenshot';
296
331
  try {
297
332
  const stat = lstatSync(candidate.path);
298
- if ((fileCandidate ? !stat.isFile() : !stat.isDirectory()) || stat.isSymbolicLink() || !pathInside(ownedRoot, candidate.path)) {
333
+ if ((fileCandidate ? !stat.isFile() : !stat.isDirectory()) || stat.isSymbolicLink() || hasSymlinkAncestor(repoRoot, candidate.path) || !pathInside(ownedRoot, candidate.path)) {
299
334
  increment(report.skippedByReason, 'ownership_unproven');
300
335
  report.retainedPaths.push(relative(repoRoot, candidate.path));
301
336
  continue;
302
337
  }
303
- if (nowMs - stat.mtimeMs < retentionGraceMs(candidate, options.graceMs)) {
338
+ const usedAt = candidate.cacheRoot ? Math.max(stat.mtimeMs, lstatSync(candidate.cacheRoot).mtimeMs) : stat.mtimeMs;
339
+ if (nowMs - usedAt < retentionGraceMs(candidate, options.graceMs)) {
304
340
  increment(report.skippedByReason, 'retention_grace');
305
341
  continue;
306
342
  }
307
- if (commandReferencesPath(commands, repoRoot, candidate.path)) {
343
+ if (commandReferencesPath(commands, repoRoot, candidate.cacheRoot ?? candidate.path)
344
+ || (candidate.cacheRoot && commands.some((command) => /(?:^|[/\s])xcodebuild(?:\s|$)/.test(command)
345
+ && !/(?:^|\s)-derivedDataPath(?:\s|=)/.test(command)))) {
308
346
  increment(report.skippedByReason, 'active_process');
309
347
  report.retainedPaths.push(relative(repoRoot, candidate.path));
310
348
  continue;
311
349
  }
312
- if (containsTrackedFiles(repoRoot, candidate.path)) {
350
+ if (containsTrackedFiles(repoRoot, candidate.cacheRoot ?? candidate.path)) {
313
351
  increment(report.skippedByReason, 'tracked_content');
314
352
  report.retainedPaths.push(relative(repoRoot, candidate.path));
315
353
  continue;
@@ -320,6 +358,12 @@ export function cleanupGeneratedRepositoryCaches(
320
358
  increment(report.skippedByReason, 'cleanup_budget_exhausted');
321
359
  continue;
322
360
  }
361
+ // Recheck last-use immediately before removal: discovery and Git probes
362
+ // may overlap a cache reuse that refreshed the bucket after our first stat.
363
+ if (candidate.cacheRoot && lstatSync(candidate.cacheRoot).mtimeMs !== usedAt && nowMs - lstatSync(candidate.cacheRoot).mtimeMs < retentionGraceMs(candidate, options.graceMs)) {
364
+ increment(report.skippedByReason, 'retention_grace');
365
+ continue;
366
+ }
323
367
  remainingRemovals -= 1;
324
368
  rmSync(candidate.path, { recursive: true, force: true });
325
369
  report.removedPaths.push(relative(repoRoot, candidate.path).replace(/\\/g, '/'));
@@ -358,10 +402,10 @@ export function cleanupGeneratedRepositoryCheckoutCaches(
358
402
  }
359
403
  const rawRoots = roots.map((root) => resolve(root));
360
404
  const canonicalRoots = roots.map((root) => canonical(root));
361
- const processCommandsForRoot = (root: string): string[] => {
405
+ const processCommandsForRoot = (root: string): string[] | undefined => {
362
406
  const rawRoot = resolve(root);
363
407
  const canonicalRoot = canonical(root);
364
- return processCommands.filter((command) => {
408
+ return processCommands?.filter((command) => {
365
409
  const referencedRootIndexes = rawRoots
366
410
  .map((candidate, index) => command.includes(candidate) || command.includes(canonicalRoots[index]!) ? index : -1)
367
411
  .filter((index) => index >= 0);
@@ -381,6 +425,11 @@ export function cleanupGeneratedRepositoryCheckoutCaches(
381
425
  budgetExhausted: false,
382
426
  };
383
427
 
428
+ if (processCommands === undefined) {
429
+ aggregate.errors.push('process_inventory_unavailable: generated-cache reclamation skipped');
430
+ aggregate.skippedByReason.process_inventory_unavailable = 1;
431
+ return aggregate;
432
+ }
384
433
  for (const root of roots) {
385
434
  if (remainingEntries <= 0 || remainingRemovals <= 0) {
386
435
  aggregate.budgetExhausted = true;
@@ -389,6 +438,7 @@ export function cleanupGeneratedRepositoryCheckoutCaches(
389
438
  const report = cleanupGeneratedRepositoryCaches(root, {
390
439
  ...options,
391
440
  processCommands: processCommandsForRoot(root),
441
+ scanStartIndex: options.scanStartIndex ?? Math.floor((options.rootStartIndex ?? 0) / Math.max(1, roots.length)),
392
442
  maxEntries: remainingEntries,
393
443
  maxRemovals: remainingRemovals,
394
444
  });
@@ -565,6 +615,7 @@ export function cleanupIdleXCTestDevices(controllerHome: string): XCTestDeviceCl
565
615
  if (deviceDirectories <= 4) return { attempted: false, removedDevices: 0, skippedReason: 'below_cleanup_threshold' };
566
616
 
567
617
  const commands = collectProcessCommands();
618
+ if (commands === undefined) return { attempted: false, removedDevices: 0, skippedReason: 'process_inventory_unavailable' };
568
619
  if (commands.some((command) => /(?:^|\/)(?:xcodebuild|xctest|swiftc)(?:\s|$)/.test(command))) {
569
620
  return { attempted: false, removedDevices: 0, skippedReason: 'active_xcode_process' };
570
621
  }
@@ -80,6 +80,8 @@ export function planSchedulerPeriodicMaintenance(input: {
80
80
  }): {
81
81
  periodicSequence: number;
82
82
  deepRetentionSequence: number;
83
+ generatedCacheRepositoryIndex?: number;
84
+ generatedCacheRootStartIndex: number;
83
85
  runRetention: boolean;
84
86
  runDeepRetention: boolean;
85
87
  processGcRepositoryIndex?: number;
@@ -94,6 +96,9 @@ export function planSchedulerPeriodicMaintenance(input: {
94
96
  return {
95
97
  periodicSequence,
96
98
  deepRetentionSequence,
99
+ generatedCacheRepositoryIndex: repositoryCount > 0 && periodicSequence % retentionEvery === 0
100
+ ? Math.floor(periodicSequence / retentionEvery) % repositoryCount : undefined,
101
+ generatedCacheRootStartIndex: repositoryCount > 0 ? Math.floor(periodicSequence / retentionEvery / repositoryCount) : 0,
97
102
  runRetention: periodicSequence % retentionEvery === 0,
98
103
  runDeepRetention: periodicSequence % deepRetentionEvery === 0,
99
104
  processGcRepositoryIndex: repositoryCount > 0 ? periodicSequence % repositoryCount : undefined,
@@ -271,6 +276,21 @@ export async function runSchedulerPeriodicCleanup(input: {
271
276
  }
272
277
  }
273
278
 
279
+ // Cache production is much faster than evidence retention. Reuse the existing
280
+ // five-minute pass, one repository and one bounded budget per pass. The root
281
+ // ordinal advances per repository visit, not by the correlated global index.
282
+ if (plan.generatedCacheRepositoryIndex !== undefined) {
283
+ if (!runtimeMaintenanceAuthorityCurrent(input.controllerHome)) return;
284
+ const cacheRepo = input.repositories[plan.generatedCacheRepositoryIndex]!;
285
+ try {
286
+ const generated = cleanupGeneratedRepositoryCheckoutCaches(cacheRepo, {
287
+ nowMs: input.nowMs, rootStartIndex: plan.generatedCacheRootStartIndex,
288
+ });
289
+ if (generated.errors.length > 0) console.error(`[forge cleanup] generated-cache retention reported ${generated.errors.length} error(s) for ${cacheRepo.repoId}`);
290
+ } catch (error) {
291
+ console.error(`[forge cleanup] generated-cache retention failed for ${cacheRepo.repoId}:`, error);
292
+ }
293
+ }
274
294
  if (!plan.runDeepRetention) return;
275
295
  if (!runtimeMaintenanceAuthorityCurrent(input.controllerHome)) return;
276
296
 
@@ -285,14 +305,6 @@ export async function runSchedulerPeriodicCleanup(input: {
285
305
  }
286
306
  if (plan.deepRetentionRepositoryIndex === undefined) return;
287
307
  const repo = input.repositories[plan.deepRetentionRepositoryIndex]!;
288
- try {
289
- const generated = cleanupGeneratedRepositoryCheckoutCaches(repo, { nowMs: input.nowMs, rootStartIndex: plan.deepRetentionSequence });
290
- if (generated.errors.length > 0) {
291
- console.error(`[forge cleanup] generated-cache retention reported ${generated.errors.length} error(s) for ${repo.repoId}`);
292
- }
293
- } catch (error) {
294
- console.error(`[forge cleanup] generated-cache retention failed for ${repo.repoId}:`, error);
295
- }
296
308
  try {
297
309
  const browserArtifacts = cleanupControllerHomeBrowserArtifacts(input.controllerHome, repo.repoId, { nowMs: input.nowMs });
298
310
  if (browserArtifacts.errors.length > 0 || Object.values(browserArtifacts.classes).some((item) => item.overCapacity)) {
@@ -8,7 +8,7 @@ import {
8
8
  childReferenceFromJob,
9
9
  childReferenceFromReceipt,
10
10
  hasDurableChildReference,
11
- isAgentDelegationOperation,
11
+ isAgentDelegationJob,
12
12
  mergeChildReferences,
13
13
  buildDelegatedExecutionResult,
14
14
  } from '../../execution/jobs/child-reference';
@@ -34,16 +34,6 @@ function canAutomaticallyReplay(job: ExecutionJob): boolean {
34
34
  return !hasPotentialSideEffects(job);
35
35
  }
36
36
 
37
- function isAgentDelegationJob(job: ExecutionJob): boolean {
38
- return isAgentDelegationOperation(job.payload.operation)
39
- || job.type === 'agent-run'
40
- || job.type === 'dispatch-task'
41
- || job.payload.arguments?.agentDelegation === true
42
- || (job.payload.operation === 'legacy-local-job'
43
- && typeof job.payload.arguments?.localAction === 'string'
44
- && ['launch-task', 'quick-agent-session'].includes(String(job.payload.arguments.localAction)));
45
- }
46
-
47
37
  function ambiguousStartedOperation(controllerHome: string, job: ExecutionJob): boolean {
48
38
  const receipt = readOperationReceipt(controllerHome, job.repoId, job.jobId);
49
39
  if (!receipt || !operationReceiptMatchesJobOwnership(receipt, job)) return false;
@@ -3,7 +3,7 @@ const EXPLICIT_AUTHORIZATION_OPERATIONS = new Set([
3
3
  'close_github_issue',
4
4
  'repository_remove',
5
5
  'repository_disable',
6
- 'repository_command_execute',
6
+ 'execute_command',
7
7
  'configure_github_plugin',
8
8
  ]);
9
9
 
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
+
2
3
  import { isWslWindowsRuntime } from '../../../cli/chatgpt-browser/bridge-provider';
3
4
  import { createChatgptBrowserDeliveryHost } from '../../../../adapters/chatgpt/browser-delivery-host';
4
5
  import { createChatgptWslBridgeDeliveryHost } from '../../../../adapters/chatgpt/wsl-bridge-delivery-host';
@@ -14,7 +14,6 @@ import { cleanupTerminalEditSessionRecords } from '../../cli/editing/edit-sessio
14
14
  import { listRepositories } from '../../cli/repositories/registry';
15
15
  import { managedPathInside, managedWorktreeStorageRoot } from '../../cli/repositories/worktree-storage';
16
16
  import { migrateManagedWorkspacePhysicalDependenciesToCanonical } from '../execution/managed-workspace';
17
- import { cleanupStaleCodegraphLocators } from '../context/codegraph-cache-boundary';
18
17
  import { listActiveLeases } from '../resources/leases/store';
19
18
  import { cleanupScheduleOccurrenceHistory } from '../../../packages/kernel/scheduler/api/index';
20
19
  import { isTerminalSemanticWorkState, readWorkContractStore } from '../../../packages/kernel/work/api/index';
@@ -22,7 +21,6 @@ import { assertOwnedResourceCleanupTarget, listOwnedResources, markOwnedResource
22
21
  import { appendJsonLine, readJsonFile, writeJsonAtomic } from '../shared/json-files';
23
22
  import { cleanupControllerReleaseHistory } from './release-retention';
24
23
  import { cleanupWorkPreservationArtifacts } from './cleanup-artifact-retention';
25
- import { cleanupCodegraphCaches, CODEGRAPH_CACHE_RETENTION_POLICY_VERSION } from './codegraph-cache-retention';
26
24
  import { cleanupRetiredRepositoryNamespaces } from './repository-namespace-retention';
27
25
  import { maintainControlPlaneDatabase, type ControlPlaneDatabaseMaintenanceReport } from './persistence/sqlite-store';
28
26
  import { cleanupExpiredExperiences } from './persistence/experience-store';
@@ -168,8 +166,6 @@ export interface RuntimeCleanupOptions {
168
166
  editSessionMaxRetained?: number;
169
167
  /** Grace period before a non-Plan Work with no exact durable owner loses execution authority. */
170
168
  ownerlessWorkAuthorityGraceMs?: number;
171
- /** Minimum age before an unprotected Forge-owned CodeGraph cache can be rebuilt on demand and reclaimed. */
172
- codegraphCacheRetentionMs?: number;
173
169
  /** Grace before a removed repository may shed only explicit disposable Controller Home children. */
174
170
  repositoryNamespaceRetentionMs?: number;
175
171
  /** Minimum reclaimable SQLite free-page bytes before physical VACUUM is eligible. */
@@ -243,8 +239,6 @@ export interface RuntimeCleanupReport {
243
239
  removedScheduleOccurrencePaths: string[];
244
240
  removedScheduleDecisionPaths: string[];
245
241
  removedEditSessionPaths: string[];
246
- removedCodegraphLocatorPaths: string[];
247
- removedCodegraphCachePaths: string[];
248
242
  removedRepositoryNamespacePaths: string[];
249
243
  /** Physical SQLite checkpoint/compaction receipt; row lifecycle remains domain-owned. */
250
244
  sqliteMaintenance?: ControlPlaneDatabaseMaintenanceReport;
@@ -302,14 +296,14 @@ function addReclaimMeasurement(metrics: MutableReclaimMetrics, path: string): vo
302
296
  else metrics.unknownReclaimedByteCount += 1;
303
297
  }
304
298
 
305
- type RemovalPhase = 'worktrees' | 'temporary' | 'artifacts' | 'scheduler' | 'edit_sessions' | 'caches' | 'repository_namespaces' | 'releases';
306
- const REMOVAL_PHASES: readonly RemovalPhase[] = ['worktrees', 'temporary', 'artifacts', 'scheduler', 'edit_sessions', 'caches', 'repository_namespaces', 'releases'];
299
+ type RemovalPhase = 'worktrees' | 'temporary' | 'artifacts' | 'scheduler' | 'edit_sessions' | 'repository_namespaces' | 'releases';
300
+ const REMOVAL_PHASES: readonly RemovalPhase[] = ['worktrees', 'temporary', 'artifacts', 'scheduler', 'edit_sessions', 'repository_namespaces', 'releases'];
307
301
 
308
302
  export function cleanupRemovalPhaseOrder(reason: RuntimeCleanupReport['reason'], sequence = 0): RemovalPhase[] {
309
303
  if (reason !== 'periodic') return [...REMOVAL_PHASES];
310
304
  const offset = ((Math.trunc(sequence) % REMOVAL_PHASES.length) + REMOVAL_PHASES.length) % REMOVAL_PHASES.length;
311
305
  // Periodic maintenance is a bounded shard, not a reordered full sweep.
312
- // Eight one-minute generations cover every removal class once while startup/manual
306
+ // Seven one-minute generations cover every removal class once while startup/manual
313
307
  // cleanup retains the complete all-phase contract.
314
308
  return [REMOVAL_PHASES[offset]!];
315
309
  }
@@ -1058,8 +1052,6 @@ function shouldPersistCleanupAudit(report: RuntimeCleanupReport): boolean {
1058
1052
  || report.removedCleanupArtifactPaths.length
1059
1053
  || report.removedReleasePaths.length
1060
1054
  || report.releaseSessionCandidates.removedPaths.length
1061
- || report.removedCodegraphLocatorPaths.length
1062
- || report.removedCodegraphCachePaths.length
1063
1055
  || report.removedRepositoryNamespacePaths.length
1064
1056
  || report.sqliteMaintenance?.vacuumed
1065
1057
  || report.sqliteMaintenance?.skippedReason === 'database_busy'
@@ -1124,11 +1116,7 @@ export function cleanupControllerRuntimeState(
1124
1116
  let artifactRetention: ReturnType<typeof cleanupWorkPreservationArtifacts> | undefined;
1125
1117
  let releaseRetention: ReturnType<typeof cleanupControllerReleaseHistory> | undefined;
1126
1118
  let releaseSessionCandidates: ReleaseSessionCandidateRetentionReport | undefined;
1127
- let codegraphRetention: ReturnType<typeof cleanupCodegraphCaches> | undefined;
1128
1119
  let repositoryNamespaceRetention: ReturnType<typeof cleanupRetiredRepositoryNamespaces> | undefined;
1129
- const removedCodegraphLocatorPaths: string[] = [];
1130
- let codegraphLocatorInspected = 0;
1131
- let codegraphLocatorRetained = 0;
1132
1120
  const schedulerHistory = {
1133
1121
  removedOccurrencePaths: [] as string[],
1134
1122
  removedDecisionPaths: [] as string[],
@@ -1263,65 +1251,6 @@ export function cleanupControllerRuntimeState(
1263
1251
  errors.push(...editSessionHistory.errors.map((error) => `edit-session-history ${error}`));
1264
1252
  continue;
1265
1253
  }
1266
- if (phase === 'caches') {
1267
- const registered = listRepositories(home, { includeRemoved: true });
1268
- const repositoryRoots = new Set<string>();
1269
- for (const repository of registered) {
1270
- repositoryRoots.add(resolve(repository.canonicalRoot));
1271
- repositoryRoots.add(resolve(repository.localRoot));
1272
- for (const checkout of repository.checkouts) {
1273
- repositoryRoots.add(resolve(checkout.canonicalRoot));
1274
- repositoryRoots.add(resolve(checkout.localRoot));
1275
- }
1276
- }
1277
- const protectedRepositoryRoots = new Set<string>();
1278
- let locatorScanRemaining = maxEntries;
1279
- for (const repositoryRoot of [...repositoryRoots].sort()) {
1280
- if (!existsSync(repositoryRoot)) continue;
1281
- if (locatorScanRemaining <= 0) {
1282
- protectedRepositoryRoots.add(repositoryRoot);
1283
- skippedByReason.codegraph_locator_scan_budget_exhausted = (skippedByReason.codegraph_locator_scan_budget_exhausted ?? 0) + 1;
1284
- continue;
1285
- }
1286
- const locatorReport = cleanupStaleCodegraphLocators(
1287
- repositoryRoot,
1288
- home,
1289
- locatorScanRemaining,
1290
- removalBudget.remaining,
1291
- );
1292
- locatorScanRemaining = Math.max(0, locatorScanRemaining - locatorReport.inspected);
1293
- removalBudget.remaining = Math.max(0, removalBudget.remaining - locatorReport.attempted);
1294
- if (locatorReport.budgetExhausted && removalBudget.remaining <= 0) removalBudget.exhausted = true;
1295
- codegraphLocatorInspected += locatorReport.inspected;
1296
- codegraphLocatorRetained += Math.max(0, locatorReport.inspected - locatorReport.removed.length);
1297
- removedCodegraphLocatorPaths.push(...locatorReport.removed.map((name) => join(repositoryRoot, name)));
1298
- Object.entries(locatorReport.skippedByReason).forEach(([key, value]) => {
1299
- skippedByReason[`codegraph_locator_${key}`] = (skippedByReason[`codegraph_locator_${key}`] ?? 0) + value;
1300
- });
1301
- const protectionUncertain = locatorReport.budgetExhausted
1302
- || locatorReport.errors.length > 0
1303
- || (locatorReport.skippedByReason.locator_identity_unproven ?? 0) > 0;
1304
- if (locatorReport.active.length > 0 || protectionUncertain) protectedRepositoryRoots.add(repositoryRoot);
1305
- if (locatorReport.budgetExhausted) {
1306
- skippedByReason.codegraph_locator_scan_budget_exhausted = (skippedByReason.codegraph_locator_scan_budget_exhausted ?? 0) + 1;
1307
- }
1308
- errors.push(...locatorReport.errors.map((error) => `codegraph-locator ${repositoryRoot}: ${error}`));
1309
- }
1310
- codegraphRetention = cleanupCodegraphCaches(home, {
1311
- nowMs,
1312
- retentionMs: options.codegraphCacheRetentionMs,
1313
- maxEntries,
1314
- maxRemovals: removalBudget.remaining,
1315
- protectedRepositoryRoots: [...protectedRepositoryRoots],
1316
- });
1317
- removalBudget.remaining = Math.max(0, removalBudget.remaining - codegraphRetention.attempted);
1318
- if (codegraphRetention.budgetExhausted) removalBudget.exhausted = true;
1319
- Object.entries(codegraphRetention.skippedByReason).forEach(([key, value]) => {
1320
- skippedByReason[`codegraph_cache_${key}`] = (skippedByReason[`codegraph_cache_${key}`] ?? 0) + value;
1321
- });
1322
- errors.push(...codegraphRetention.errors.map((error) => `codegraph-cache ${error}`));
1323
- continue;
1324
- }
1325
1254
  if (phase === 'repository_namespaces') {
1326
1255
  repositoryNamespaceRetention = cleanupRetiredRepositoryNamespaces(home, {
1327
1256
  nowMs,
@@ -1363,12 +1292,11 @@ export function cleanupControllerRuntimeState(
1363
1292
  dependencyCleanup ??= { migrated: [], skippedByReason: {} } as ReturnType<typeof cleanupManagedWorktreeDependencyCopies>;
1364
1293
  temporaryCleanup ??= { removed: [], reclaim: emptyReclaimMetrics() } as ReturnType<typeof cleanupTemporaryStatePaths>;
1365
1294
  artifactRetention ??= { removedPaths: [], attempted: 0, budgetExhausted: false, skippedByReason: {}, errors: [], inspected: 0, eligible: 0, retained: 0, skipped: 0, reclaimedBytes: 0, unknownReclaimedByteCount: 0 } as ReturnType<typeof cleanupWorkPreservationArtifacts>;
1366
- codegraphRetention ??= { policyVersion: CODEGRAPH_CACHE_RETENTION_POLICY_VERSION, removedPaths: [], attempted: 0, budgetExhausted: false, skippedByReason: {}, errors: [], inspected: 0, eligible: 0, retained: 0, reclaimedBytes: 0, unknownReclaimedByteCount: 0, observedBytes: 0, unknownObservedByteCount: 0, protected: 0, protectedOverCapacity: 0, truncated: false } as ReturnType<typeof cleanupCodegraphCaches>;
1367
1295
  repositoryNamespaceRetention ??= { policyVersion: 'repository-namespace-retention-v1', removedPaths: [], attempted: 0, budgetExhausted: false, skippedByReason: {}, errors: [], inspected: 0, eligible: 0, retained: 0, reclaimedBytes: 0, unknownReclaimedByteCount: 0 } as ReturnType<typeof cleanupRetiredRepositoryNamespaces>;
1368
1296
  releaseRetention ??= { removedPaths: [], removedBackupPaths: [], attempted: 0, budgetExhausted: false, skippedByReason: {}, errors: [], inspected: 0, eligible: 0, retained: 0, skipped: 0, reclaimedBytes: 0, unknownReclaimedByteCount: 0, reclaimedBackupBytes: 0, unknownReclaimedBackupByteCount: 0 } as ReturnType<typeof cleanupControllerReleaseHistory>;
1369
1297
  releaseSessionCandidates ??= emptyReleaseSessionCandidateRetentionReport();
1370
1298
  }
1371
- if (!worktrees || !dependencyCleanup || !temporaryCleanup || !artifactRetention || !codegraphRetention || !repositoryNamespaceRetention || !releaseRetention || !releaseSessionCandidates) {
1299
+ if (!worktrees || !dependencyCleanup || !temporaryCleanup || !artifactRetention || !repositoryNamespaceRetention || !releaseRetention || !releaseSessionCandidates) {
1372
1300
  throw new Error('RUNTIME_CLEANUP_PHASE_INCOMPLETE');
1373
1301
  }
1374
1302
  const removedTemporaryPaths = temporaryCleanup.removed.sort();
@@ -1377,8 +1305,6 @@ export function cleanupControllerRuntimeState(
1377
1305
  const removedScheduleOccurrencePaths = schedulerHistory.removedOccurrencePaths.sort();
1378
1306
  const removedScheduleDecisionPaths = schedulerHistory.removedDecisionPaths.sort();
1379
1307
  const removedEditSessionPaths = editSessionHistory.removedPaths.sort();
1380
- removedCodegraphLocatorPaths.sort();
1381
- const removedCodegraphCachePaths = codegraphRetention.removedPaths.sort();
1382
1308
  const removedRepositoryNamespacePaths = repositoryNamespaceRetention.removedPaths.sort();
1383
1309
  let sqliteMaintenance: ControlPlaneDatabaseMaintenanceReport | undefined;
1384
1310
  if (!periodic || removalPhases.includes('releases')) try {
@@ -1392,7 +1318,7 @@ export function cleanupControllerRuntimeState(
1392
1318
  } catch (error) {
1393
1319
  errors.push(errorText('SQLite maintenance', error));
1394
1320
  }
1395
- const inspectedPaths = referenceBudget.inspected + worktreeBudget.inspected + dependencyBudget.inspected + tempBudget.inspected + schedulerBudget.inspected + editSessionBudget.inspected + artifactRetention.inspected + codegraphLocatorInspected + codegraphRetention.inspected + repositoryNamespaceRetention.inspected + releaseRetention.inspected + releaseSessionCandidates.inspected;
1321
+ const inspectedPaths = referenceBudget.inspected + worktreeBudget.inspected + dependencyBudget.inspected + tempBudget.inspected + schedulerBudget.inspected + editSessionBudget.inspected + artifactRetention.inspected + repositoryNamespaceRetention.inspected + releaseRetention.inspected + releaseSessionCandidates.inspected;
1396
1322
  const budgetExhausted = referenceBudget.exhausted || worktreeBudget.exhausted || dependencyBudget.exhausted || tempBudget.exhausted || schedulerBudget.exhausted || editSessionBudget.exhausted || removalBudget.exhausted || releaseSessionCandidates.truncated;
1397
1323
  if (pidFiles.skipped.length > 0 && removalBudget.exhausted) skippedByReason.cleanup_budget_exhausted = (skippedByReason.cleanup_budget_exhausted ?? 0) + pidFiles.skipped.length;
1398
1324
  if (pidFiles.skipped.length > 0 && !removalBudget.exhausted) skippedByReason.active_owner = (skippedByReason.active_owner ?? 0) + pidFiles.skipped.length;
@@ -1403,9 +1329,9 @@ export function cleanupControllerRuntimeState(
1403
1329
  unknownByteCount: worktrees.reclaim.unknownReclaimedByteCount,
1404
1330
  },
1405
1331
  runtime_temp: {
1406
- count: pidFiles.removed.length + removedTemporaryPaths.length + removedCodegraphLocatorPaths.length,
1332
+ count: pidFiles.removed.length + removedTemporaryPaths.length,
1407
1333
  bytes: pidFiles.reclaim.reclaimedBytes + temporaryCleanup.reclaim.reclaimedBytes,
1408
- unknownByteCount: pidFiles.reclaim.unknownReclaimedByteCount + temporaryCleanup.reclaim.unknownReclaimedByteCount + removedCodegraphLocatorPaths.length,
1334
+ unknownByteCount: pidFiles.reclaim.unknownReclaimedByteCount + temporaryCleanup.reclaim.unknownReclaimedByteCount,
1409
1335
  },
1410
1336
  work: {
1411
1337
  count: removedCleanupArtifactPaths.length,
@@ -1437,11 +1363,6 @@ export function cleanupControllerRuntimeState(
1437
1363
  bytes: editSessionHistory.reclaimedBytes,
1438
1364
  unknownByteCount: editSessionHistory.unknownReclaimedByteCount,
1439
1365
  },
1440
- codegraph_cache: {
1441
- count: removedCodegraphCachePaths.length,
1442
- bytes: codegraphRetention.reclaimedBytes,
1443
- unknownByteCount: codegraphRetention.unknownReclaimedByteCount,
1444
- },
1445
1366
  repository_controller_home_namespace: {
1446
1367
  count: removedRepositoryNamespacePaths.length,
1447
1368
  bytes: repositoryNamespaceRetention.reclaimedBytes,
@@ -1489,8 +1410,6 @@ export function cleanupControllerRuntimeState(
1489
1410
  removedScheduleOccurrencePaths,
1490
1411
  removedScheduleDecisionPaths,
1491
1412
  removedEditSessionPaths,
1492
- removedCodegraphLocatorPaths,
1493
- removedCodegraphCachePaths,
1494
1413
  removedRepositoryNamespacePaths,
1495
1414
  sqliteMaintenance,
1496
1415
  retiredOwnerlessWorkAuthorities: retiredOwnerlessWorkAuthorities.sort(),
@@ -1502,11 +1421,11 @@ export function cleanupControllerRuntimeState(
1502
1421
  lifecycleMetrics,
1503
1422
  cycle: {
1504
1423
  scanned: inspectedPaths,
1505
- eligible: retiredOwnerlessWorkAuthorities.length + pidFiles.removed.length + worktrees.removed.length + dependencyCleanup.migrated.length + removedTemporaryPaths.length + artifactRetention.eligible + schedulerHistory.eligible + editSessionHistory.eligible + removedCodegraphLocatorPaths.length + codegraphRetention.eligible + repositoryNamespaceRetention.eligible + releaseRetention.eligible + releaseSessionCandidates.eligible,
1506
- attempted: retiredOwnerlessWorkAuthorities.length + pidFiles.removed.length + worktrees.removed.length + dependencyCleanup.migrated.length + removedTemporaryPaths.length + artifactRetention.attempted + schedulerHistory.attempted + editSessionHistory.attempted + removedCodegraphLocatorPaths.length + codegraphRetention.attempted + repositoryNamespaceRetention.attempted + releaseRetention.attempted + releaseSessionCandidates.attempted + errors.length,
1507
- removed: retiredOwnerlessWorkAuthorities.length + pidFiles.removed.length + worktrees.removed.length + dependencyCleanup.migrated.length + removedTemporaryPaths.length + removedCleanupArtifactPaths.length + removedScheduleOccurrencePaths.length + removedScheduleDecisionPaths.length + removedEditSessionPaths.length + removedCodegraphLocatorPaths.length + removedCodegraphCachePaths.length + removedRepositoryNamespacePaths.length + removedReleasePaths.length + releaseSessionCandidates.removedPaths.length,
1508
- retained: pidFiles.skipped.length + worktrees.skippedActive.length + artifactRetention.retained + schedulerHistory.retained + editSessionHistory.retained + codegraphLocatorRetained + codegraphRetention.retained + repositoryNamespaceRetention.retained + releaseRetention.retained + releaseSessionCandidates.retainedCount,
1509
- skipped: Math.max(0, inspectedPaths - pidFiles.removed.length - worktrees.removed.length - dependencyCleanup.migrated.length - removedTemporaryPaths.length - removedCleanupArtifactPaths.length - removedScheduleOccurrencePaths.length - removedScheduleDecisionPaths.length - removedEditSessionPaths.length - removedCodegraphLocatorPaths.length - removedCodegraphCachePaths.length - removedRepositoryNamespacePaths.length - removedReleasePaths.length - releaseSessionCandidates.removedPaths.length - errors.length),
1424
+ eligible: retiredOwnerlessWorkAuthorities.length + pidFiles.removed.length + worktrees.removed.length + dependencyCleanup.migrated.length + removedTemporaryPaths.length + artifactRetention.eligible + schedulerHistory.eligible + editSessionHistory.eligible + repositoryNamespaceRetention.eligible + releaseRetention.eligible + releaseSessionCandidates.eligible,
1425
+ attempted: retiredOwnerlessWorkAuthorities.length + pidFiles.removed.length + worktrees.removed.length + dependencyCleanup.migrated.length + removedTemporaryPaths.length + artifactRetention.attempted + schedulerHistory.attempted + editSessionHistory.attempted + repositoryNamespaceRetention.attempted + releaseRetention.attempted + releaseSessionCandidates.attempted + errors.length,
1426
+ removed: retiredOwnerlessWorkAuthorities.length + pidFiles.removed.length + worktrees.removed.length + dependencyCleanup.migrated.length + removedTemporaryPaths.length + removedCleanupArtifactPaths.length + removedScheduleOccurrencePaths.length + removedScheduleDecisionPaths.length + removedEditSessionPaths.length + removedRepositoryNamespacePaths.length + removedReleasePaths.length + releaseSessionCandidates.removedPaths.length,
1427
+ retained: pidFiles.skipped.length + worktrees.skippedActive.length + artifactRetention.retained + schedulerHistory.retained + editSessionHistory.retained + repositoryNamespaceRetention.retained + releaseRetention.retained + releaseSessionCandidates.retainedCount,
1428
+ skipped: Math.max(0, inspectedPaths - pidFiles.removed.length - worktrees.removed.length - dependencyCleanup.migrated.length - removedTemporaryPaths.length - removedCleanupArtifactPaths.length - removedScheduleOccurrencePaths.length - removedScheduleDecisionPaths.length - removedEditSessionPaths.length - removedRepositoryNamespacePaths.length - removedReleasePaths.length - releaseSessionCandidates.removedPaths.length - errors.length),
1510
1429
  failed: errors.length,
1511
1430
  truncated: budgetExhausted,
1512
1431
  budgetExhausted,