create-walle 0.10.1 → 0.10.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 (341) hide show
  1. package/README.md +4 -0
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +6 -4
  6. package/package.json +1 -1
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  18. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  19. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  20. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  21. package/template/coding-task-manager/db.js +705 -286
  22. package/template/coding-task-manager/git-utils.js +44 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  24. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  25. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  26. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  27. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  28. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  29. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  30. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  31. package/template/coding-task-manager/lib/api/machines.js +461 -0
  32. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  33. package/template/coding-task-manager/lib/api/providers.js +53 -6
  34. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  35. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  36. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  37. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  38. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  39. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  40. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  41. package/template/coding-task-manager/lib/auth-context.js +32 -2
  42. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  43. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  44. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  45. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  46. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  47. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  48. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  49. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  50. package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
  51. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  52. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  53. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  54. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  55. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  56. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  57. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  58. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  59. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  60. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  61. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  62. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  63. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  64. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  65. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  66. package/template/coding-task-manager/lib/db-domains.js +21 -2
  67. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  68. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  69. package/template/coding-task-manager/lib/feature-manifest.js +1 -1
  70. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  71. package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
  72. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  73. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  74. package/template/coding-task-manager/lib/headless-term-service.js +55 -17
  75. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  76. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  77. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  78. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  79. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  80. package/template/coding-task-manager/lib/host-stats.js +328 -0
  81. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  82. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  83. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  84. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  85. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  86. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  87. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  88. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  89. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  90. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  91. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  92. package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
  93. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  94. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  95. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  96. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  97. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  98. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  99. package/template/coding-task-manager/lib/machine-self.js +66 -0
  100. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
  101. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  102. package/template/coding-task-manager/lib/media-journal.js +84 -27
  103. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  104. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  105. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  106. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  107. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  108. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  109. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  110. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  111. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  112. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  113. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  114. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  115. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  116. package/template/coding-task-manager/lib/native-history-control.js +2 -2
  117. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  118. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  119. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  120. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  121. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  122. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  123. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  124. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  125. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  126. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  127. package/template/coding-task-manager/lib/read-pool-client.js +5 -2
  128. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  129. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  130. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  131. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  132. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  133. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  134. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  135. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  136. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  137. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  138. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  139. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  140. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  141. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  142. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  143. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  144. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  145. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  146. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  147. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  148. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  149. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  150. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  151. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  152. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  153. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  154. package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
  155. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  156. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  157. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  158. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  159. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  160. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  161. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  162. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  163. package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
  164. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  165. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  166. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  167. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  168. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  169. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  170. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  171. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  172. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  173. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  174. package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
  175. package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
  176. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  177. package/template/coding-task-manager/lib/session-standup.js +67 -4
  178. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  179. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  180. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  181. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  182. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  183. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  184. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  185. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  186. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  187. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  188. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  189. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  190. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  191. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  192. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  193. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  194. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  195. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  196. package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
  197. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  198. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  199. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  200. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  201. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  202. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  203. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  204. package/template/coding-task-manager/lib/ws-router.js +68 -1
  205. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  206. package/template/coding-task-manager/oauth-proxy.js +99 -55
  207. package/template/coding-task-manager/package-lock.json +24 -0
  208. package/template/coding-task-manager/package.json +3 -0
  209. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  210. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  211. package/template/coding-task-manager/public/css/app.css +279 -1
  212. package/template/coding-task-manager/public/css/reviews.css +4 -0
  213. package/template/coding-task-manager/public/index.html +294 -223
  214. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  215. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  216. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  217. package/template/coding-task-manager/public/js/background-work.js +13 -7
  218. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  219. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  220. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  221. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  222. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  223. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  224. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  225. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  226. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  227. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  228. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  229. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  230. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  231. package/template/coding-task-manager/public/js/reviews.js +7 -3
  232. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  233. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  234. package/template/coding-task-manager/public/js/row-join.js +4 -2
  235. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  236. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  237. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  238. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  239. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  240. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  241. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  242. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  243. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  244. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  245. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  246. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  247. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  248. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  249. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  250. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  251. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  252. package/template/coding-task-manager/public/m/app.css +226 -8
  253. package/template/coding-task-manager/public/m/app.js +502 -9
  254. package/template/coding-task-manager/public/m/index.html +38 -1
  255. package/template/coding-task-manager/public/m/machines.js +1313 -0
  256. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  257. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  258. package/template/coding-task-manager/public/m/sw.js +5 -0
  259. package/template/coding-task-manager/queue-engine.js +13 -0
  260. package/template/coding-task-manager/server/approvals.js +5 -6
  261. package/template/coding-task-manager/server/boot.js +136 -17
  262. package/template/coding-task-manager/server.js +902 -239
  263. package/template/coding-task-manager/session-integrity.js +7 -9
  264. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  265. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  266. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  267. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  268. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  269. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  270. package/template/package-lock.json +2 -2
  271. package/template/package.json +1 -1
  272. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  273. package/template/shared/conversation-storage-v2-contract.js +42 -6
  274. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  275. package/template/shared/git-remote-key.js +83 -0
  276. package/template/shared/host-identity.js +98 -2
  277. package/template/shared/owner-write-safety.js +135 -0
  278. package/template/shared/public-runtime.js +13 -5
  279. package/template/shared/release-schema.json +3 -2
  280. package/template/shared/report-citations.js +14 -0
  281. package/template/shared/sqlite-write-lock.js +361 -223
  282. package/template/shared/storage/contract.js +35 -3
  283. package/template/wall-e/agent.js +21 -0
  284. package/template/wall-e/api-walle.js +5 -0
  285. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  286. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  287. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  288. package/template/wall-e/brain/migrations.js +51 -22
  289. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  290. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  291. package/template/wall-e/brain/schema-ensures.js +22 -0
  292. package/template/wall-e/brain.js +1 -1
  293. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  294. package/template/wall-e/coding/session-journal.js +28 -40
  295. package/template/wall-e/coding-orchestrator.js +23 -26
  296. package/template/wall-e/http/auth.js +18 -1
  297. package/template/wall-e/http/blog-views-api.js +318 -0
  298. package/template/wall-e/http/helper-rules-api.js +321 -0
  299. package/template/wall-e/http/model-proxy-providers.js +3 -1
  300. package/template/wall-e/http/telemetry-api.js +12 -0
  301. package/template/wall-e/lib/blog-view-policy.js +88 -0
  302. package/template/wall-e/lib/blog-view-store.js +169 -0
  303. package/template/wall-e/lib/client-config-lock.js +2 -1
  304. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  305. package/template/wall-e/lib/error-kinds.js +10 -1
  306. package/template/wall-e/lib/feature-manifest.js +1 -1
  307. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  308. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  309. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  310. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  311. package/template/wall-e/lib/helper-rules/label.js +247 -0
  312. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  313. package/template/wall-e/lib/helper-rules/store.js +667 -0
  314. package/template/wall-e/lib/mcp-integration.js +5 -6
  315. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  316. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  317. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  318. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  319. package/template/wall-e/llm/default-fallback.js +4 -0
  320. package/template/wall-e/llm/model-catalog.js +13 -4
  321. package/template/wall-e/llm/model-roles.js +6 -0
  322. package/template/wall-e/llm/portkey-http.js +150 -0
  323. package/template/wall-e/llm/portkey-sync.js +15 -145
  324. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  325. package/template/wall-e/llm/worker-route.js +43 -1
  326. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  327. package/template/wall-e/mcp-protocol-contract.js +41 -1
  328. package/template/wall-e/mcp-server.js +13 -2
  329. package/template/wall-e/mcp-worker-schemas.js +20 -2
  330. package/template/wall-e/memory/ctm-context-client.js +25 -5
  331. package/template/wall-e/shared/release-schema.json +3 -2
  332. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  333. package/template/wall-e/shared/storage/contract.js +35 -3
  334. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  335. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  336. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  337. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  338. package/template/wall-e/tools/local-tools.js +6 -4
  339. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  340. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  341. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -0,0 +1,564 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // WP-D — offline repair CLI for a pre-WP-2 cloned CTM data dir.
5
+ // Design: docs/remote-hub-fences-followups-r2-clone-truth-and-repair-2026-10-23.html
6
+ // §1.1 (the measured Spark shape this tool repairs), §3 P-3..P-6, §5 D-D1..D-D7, §9 WP-D.
7
+ //
8
+ // The incident: a data dir was file-copied from one machine to another while running pre-WP-2
9
+ // code. The clone's boot re-minted the instance id and WP-2's `restampOwnerLineage`-predecessor
10
+ // claimed every row the source machine owned (Spark: 3,775 sessions + 64 startup tasks) as its
11
+ // own. WP-2 fences that at write time (`cross_host = 1` stops the claim) but cannot undo a claim
12
+ // that already happened, and a pre-WP-2 database has no `cross_host` column at all — so the
13
+ // re-mint edge reads as "same host" forever.
14
+ //
15
+ // This tool replays WP-2's D-3 for ONE operator-named legacy edge, offline, on a STOPPED instance:
16
+ // * operator asserts the edge (`--edge-prior <instanceId>`); there is no auto-detection (P-2);
17
+ // * dry-run is the default; `--apply` is required to mutate;
18
+ // * it refuses while the server runs (lock artifact with a live pid, or a failing
19
+ // `BEGIN IMMEDIATE` probe — D-D3);
20
+ // * `--apply` takes a verified `VACUUM INTO` backup first (D-D4);
21
+ // * two independent discriminators (write-once `origin_instance_id ∈ foreign component`,
22
+ // `created_at` strictly before the clone) must agree on EVERY row or it refuses (D-D5);
23
+ // * the mutation is one transaction that re-stamps owners, parks foreign tasks and un-commits
24
+ // the edge — never rewrites `origin_instance_id`, never touches drafts, never deletes a row
25
+ // (D-D6), and a re-run is a no-op (P-6).
26
+ //
27
+ // It opens the database directly through lib/sqlite-driver.js — never db.initDb(), never a
28
+ // connection outside `--data-dir`.
29
+ //
30
+ // Usage:
31
+ // node bin/repair-foreign-clone.js --edge-prior <instanceId> [--data-dir <dir>] [--apply] [--json]
32
+ //
33
+ // Exit codes:
34
+ // 0 ok / nothing to repair 2 usage 3 server live
35
+ // 4 discriminator disagreement 5 pre-clone drafts present
36
+ // 6 edge/db precondition 7 backup failed
37
+
38
+ const fs = require('node:fs');
39
+ const path = require('node:path');
40
+
41
+ const { loadSqliteDriver } = require('../lib/sqlite-driver');
42
+ const { getDataDir } = require('../lib/data-dir');
43
+ const { ownerLineageComponent } = require('../lib/host-identity');
44
+
45
+ const EXIT = Object.freeze({
46
+ OK: 0,
47
+ USAGE: 2,
48
+ SERVER_LIVE: 3,
49
+ DISAGREEMENT: 4,
50
+ DRAFTS: 5,
51
+ PRECONDITION: 6,
52
+ BACKUP_FAILED: 7,
53
+ });
54
+
55
+ // The two database files a CTM data dir can carry the core tables in: `core.db` under the
56
+ // core/telemetry domain split (the default, and what Spark runs), `task-manager.db` pre-split.
57
+ const CANDIDATE_DB_FILES = Object.freeze(['core.db', 'task-manager.db']);
58
+ const REQUIRED_TABLES = Object.freeze(['instance_identity', 'instance_lineage', 'ctm_sessions', 'startup_tasks']);
59
+ const SERVER_LIVE_ARTIFACTS = Object.freeze(['task-manager.db.walle-history-v2.lock', 'walle.pid']);
60
+ const CLONE_REASON = 'clone_detected';
61
+ const PARK_REASON = 'foreign-clone';
62
+ const DISAGREEMENT_LIMIT = 20;
63
+ const USAGE = 'usage: node bin/repair-foreign-clone.js --edge-prior <instanceId> [--data-dir <dir>] [--apply] [--json]';
64
+
65
+ function refusal(exitCode, message) {
66
+ const error = new Error(message);
67
+ error.exitCode = exitCode;
68
+ error.repairRefusal = true;
69
+ return error;
70
+ }
71
+
72
+ // ── primitives ──────────────────────────────────────────────────────────────────────────────
73
+
74
+ // One `new Database(...)` site: the read-write branch is the file's single registered SQLite
75
+ // writer surface; the read-only branch never mutates (and never checkpoints a WAL on close).
76
+ function openDatabase(dbPath, { readonly = false } = {}) {
77
+ const Database = loadSqliteDriver();
78
+ if (readonly) return new Database(dbPath, { readonly: true, fileMustExist: true });
79
+ return new Database(dbPath, { fileMustExist: true });
80
+ }
81
+
82
+ function tableNames(db) {
83
+ return new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((row) => String(row.name)));
84
+ }
85
+
86
+ function tableColumns(db, table) {
87
+ try {
88
+ return new Set(db.pragma(`table_info(${table})`).map((row) => String(row.name)));
89
+ } catch {
90
+ return new Set(); // expected: an absent table has no columns; callers gate on tableExists
91
+ }
92
+ }
93
+
94
+ // SQLite text timestamps come in two shapes: `datetime('now')` writes `'YYYY-MM-DD HH:MM:SS'`
95
+ // (UTC, no zone marker) and every newer writer uses `Date#toISOString()`. Both land on the same
96
+ // absolute scale here; anything else is UNKNOWN (null), which callers must treat as "cannot
97
+ // agree", never as "false" (D-D5).
98
+ function normalizeTimestampMs(value) {
99
+ const raw = String(value ?? '').trim();
100
+ if (!raw) return null;
101
+ const iso = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw) ? `${raw.replace(' ', 'T')}Z` : raw;
102
+ const ms = Date.parse(iso);
103
+ return Number.isFinite(ms) ? ms : null;
104
+ }
105
+
106
+ function readPragmaScalar(db, pragma) {
107
+ const result = db.pragma(pragma);
108
+ if (Array.isArray(result)) {
109
+ const first = result[0];
110
+ if (first === undefined) return '';
111
+ if (first !== null && typeof first === 'object') return String(Object.values(first)[0] ?? '');
112
+ return String(first);
113
+ }
114
+ return String(result ?? '');
115
+ }
116
+
117
+ // ── step 1: which file is the database (D-D2) ───────────────────────────────────────────────
118
+
119
+ function resolveDatabasePath(dataDir) {
120
+ const dir = path.resolve(String(dataDir || '').trim() || getDataDir());
121
+ const matches = [];
122
+ const inspected = [];
123
+ for (const name of CANDIDATE_DB_FILES) {
124
+ const file = path.join(dir, name);
125
+ if (!fs.existsSync(file)) {
126
+ inspected.push(`${name} (missing)`);
127
+ continue;
128
+ }
129
+ let tables;
130
+ try {
131
+ const db = openDatabase(file, { readonly: true });
132
+ try { tables = tableNames(db); } finally { db.close(); }
133
+ } catch (error) {
134
+ // expected: an unreadable/foreign file cannot be the CTM database; report it as such
135
+ inspected.push(`${name} (unreadable: ${String(error?.message || error)})`);
136
+ continue;
137
+ }
138
+ const missing = REQUIRED_TABLES.filter((table) => !tables.has(table));
139
+ if (missing.length) {
140
+ inspected.push(`${name} (missing tables: ${missing.join(', ')})`);
141
+ continue;
142
+ }
143
+ matches.push(file);
144
+ }
145
+ if (matches.length !== 1) {
146
+ throw refusal(
147
+ EXIT.PRECONDITION,
148
+ `expected exactly one database carrying ${REQUIRED_TABLES.join(', ')} in ${dir}; found ${matches.length} — ${inspected.join('; ')}`,
149
+ );
150
+ }
151
+ return matches[0];
152
+ }
153
+
154
+ // ── step 2: prove the server is stopped (D-D3) ──────────────────────────────────────────────
155
+
156
+ function _pidFromArtifact(artifact) {
157
+ const raw = (() => {
158
+ try {
159
+ return fs.readFileSync(artifact, 'utf8');
160
+ } catch {
161
+ return null; // expected: missing/unreadable artifact proves nothing is running
162
+ }
163
+ })();
164
+ if (raw === null) return null;
165
+ const text = String(raw).trim();
166
+ if (!text) return null;
167
+ try {
168
+ const parsed = JSON.parse(text);
169
+ if (parsed && typeof parsed === 'object') {
170
+ const pid = Number(parsed.pid);
171
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
172
+ }
173
+ } catch {
174
+ // expected: walle.pid is a bare integer, the migration lock is JSON — fall through
175
+ }
176
+ const bare = Number.parseInt(text, 10);
177
+ return Number.isInteger(bare) && bare > 0 && String(bare) === text ? bare : null;
178
+ }
179
+
180
+ function _processAlive(pid) {
181
+ try {
182
+ process.kill(pid, 0);
183
+ return true;
184
+ } catch (error) {
185
+ return error?.code === 'EPERM';
186
+ }
187
+ }
188
+
189
+ function assertOffline(dataDir, dbPath) {
190
+ const dir = path.resolve(String(dataDir || '').trim() || getDataDir());
191
+ for (const name of SERVER_LIVE_ARTIFACTS) {
192
+ const artifact = path.join(dir, name);
193
+ const pid = _pidFromArtifact(artifact);
194
+ if (pid !== null && _processAlive(pid)) {
195
+ throw refusal(EXIT.SERVER_LIVE, `refusing to run: ${artifact} names live pid ${pid} — stop CTM (and Wall-E) first`);
196
+ }
197
+ }
198
+ // The exclusivity proof has to be a READ-WRITE connection: a read-only connection's
199
+ // BEGIN IMMEDIATE is downgraded and reports success even while another process holds the write
200
+ // lock (measured, 2026-10-23). BEGIN IMMEDIATE takes the write lock without writing pages, so
201
+ // a successful probe leaves the file byte-identical.
202
+ const probe = openDatabase(dbPath);
203
+ try {
204
+ probe.pragma('busy_timeout = 0');
205
+ probe.exec('BEGIN IMMEDIATE');
206
+ probe.exec('ROLLBACK');
207
+ } catch (cause) {
208
+ try { probe.exec('ROLLBACK'); } catch { /* expected: the failed BEGIN opened no transaction */ }
209
+ if (cause?.code === 'SQLITE_BUSY' || cause?.code === 'SQLITE_LOCKED') {
210
+ throw refusal(EXIT.SERVER_LIVE, `refusing to run: ${dbPath} is still held by another writer (${cause.code})`);
211
+ }
212
+ throw cause;
213
+ } finally {
214
+ try { probe.close(); } catch { /* expected: already-closed probe */ }
215
+ }
216
+ }
217
+
218
+ // ── steps 3-7: the plan (D-D1/D-D2/D-D5/D-D7) ───────────────────────────────────────────────
219
+
220
+ function _singleIdentity(db) {
221
+ const rows = db.prepare('SELECT instance_id FROM instance_identity').all();
222
+ if (rows.length !== 1) {
223
+ throw refusal(EXIT.PRECONDITION, `instance_identity holds ${rows.length} row(s); expected exactly 1 — refusing to guess this host's identity`);
224
+ }
225
+ const currentId = String(rows[0].instance_id || '').trim();
226
+ if (!currentId) throw refusal(EXIT.PRECONDITION, 'instance_identity.instance_id is empty');
227
+ return currentId;
228
+ }
229
+
230
+ function _matchEdge(db, { edgePrior, currentId }) {
231
+ const rows = db.prepare('SELECT * FROM instance_lineage').all();
232
+ const matches = rows.filter((row) => String(row.prior_instance_id || '').trim() === edgePrior
233
+ && String(row.new_instance_id || '').trim() === currentId
234
+ && String(row.reason || '').trim() === CLONE_REASON);
235
+ if (matches.length !== 1) {
236
+ throw refusal(
237
+ EXIT.PRECONDITION,
238
+ `expected exactly one instance_lineage row with prior_instance_id=${edgePrior}, new_instance_id=${currentId}, reason=${CLONE_REASON}; found ${matches.length}`,
239
+ );
240
+ }
241
+ const edge = matches[0];
242
+ const hasCrossHost = Object.prototype.hasOwnProperty.call(edge, 'cross_host');
243
+ // The verdict column belongs to db.js's hosted schema (the runner-owned DDL); this tool never
244
+ // issues DDL (ddl-outside-runner lint). A pre-WP-2 data dir gains it by booting once on WP-2+
245
+ // code — harmless before the repair, since the poisoned rows are already claimed.
246
+ if (!hasCrossHost) {
247
+ throw refusal(EXIT.PRECONDITION, 'instance_lineage has no cross_host column — boot this data dir once on WP-2+ code (db.js adds the column), stop it, then rerun');
248
+ }
249
+ // `Number(null) === 0`, so NULL must be excluded explicitly: a legacy edge carries no verdict
250
+ // and every pre-WP-2 edge is exactly that (D-D1: NULL or 1 proceed).
251
+ if (edge.cross_host !== null && Number(edge.cross_host) === 0) {
252
+ throw refusal(EXIT.PRECONDITION, `lineage edge ${edge.id} is marked cross_host = 0 — a proven same-host re-mint must not be repaired`);
253
+ }
254
+ return { edge, rows };
255
+ }
256
+
257
+ function _planRows(db, { table, keyColumn, detectedAtMs, foreignComponent, currentId }) {
258
+ const columns = tableColumns(db, table);
259
+ if (!columns.has('current_owner_instance_id')) return { foreign: [], native: 0, disagreements: [] };
260
+ const select = ['current_owner_instance_id', keyColumn];
261
+ if (columns.has('origin_instance_id')) select.push('origin_instance_id');
262
+ if (columns.has('created_at')) select.push('created_at');
263
+ if (columns.has('status')) select.push('status');
264
+ const rows = db.prepare(`SELECT ${select.join(', ')} FROM ${table} WHERE current_owner_instance_id = ?`).all(currentId);
265
+ const foreign = [];
266
+ const disagreements = [];
267
+ let native = 0;
268
+ for (const row of rows) {
269
+ const key = String(row[keyColumn] ?? '');
270
+ const origin = String(row.origin_instance_id ?? '').trim();
271
+ const createdAtMs = columns.has('created_at') ? normalizeTimestampMs(row.created_at) : null;
272
+ const isForeignOrigin = foreignComponent.has(origin);
273
+ const isPreClone = createdAtMs !== null && createdAtMs < detectedAtMs;
274
+ if (createdAtMs === null) {
275
+ disagreements.push({ table, key, reason: 'unparseable created_at' });
276
+ } else if (isForeignOrigin && isPreClone) {
277
+ foreign.push(row);
278
+ } else if (!isForeignOrigin && !isPreClone) {
279
+ native += 1;
280
+ } else {
281
+ disagreements.push({
282
+ table,
283
+ key,
284
+ reason: isForeignOrigin ? 'foreign origin but created after the clone' : 'no foreign origin but created before the clone',
285
+ });
286
+ }
287
+ }
288
+ return { foreign, native, disagreements };
289
+ }
290
+
291
+ function planRepair(db, { edgePrior, dataDir = '', dbPath = '' } = {}) {
292
+ const prior = String(edgePrior || '').trim();
293
+ if (!prior) throw refusal(EXIT.USAGE, `--edge-prior is required. ${USAGE}`);
294
+ const currentId = _singleIdentity(db);
295
+ const { edge, rows: lineageRows } = _matchEdge(db, { edgePrior: prior, currentId });
296
+ const detectedAtMs = normalizeTimestampMs(edge.detected_at);
297
+ if (detectedAtMs === null) {
298
+ throw refusal(EXIT.PRECONDITION, `lineage edge ${edge.id} has an unparseable detected_at (${JSON.stringify(edge.detected_at ?? null)})`);
299
+ }
300
+ // The foreign component is the transitive closure of the prior id in the lineage graph with
301
+ // THIS edge removed — "every id that reaches this host only through the cloned-DB edge", which
302
+ // is exactly the set of ids belonging to the other machine (P-2). `traverseCrossHost: true`
303
+ // keeps that closure complete once WP-C makes cross_host = 1 edges non-traversable by default.
304
+ const foreignComponent = ownerLineageComponent(prior, lineageRows.filter((row) => row !== edge), { traverseCrossHost: true });
305
+ if (foreignComponent.has(currentId)) {
306
+ throw refusal(EXIT.PRECONDITION, `${prior} is still linked to ${currentId} by another lineage path — this is not a foreign component`);
307
+ }
308
+ const sessions = _planRows(db, { table: 'ctm_sessions', keyColumn: 'id', detectedAtMs, foreignComponent, currentId });
309
+ const tasks = _planRows(db, { table: 'startup_tasks', keyColumn: 'ctm_session_id', detectedAtMs, foreignComponent, currentId });
310
+ const disagreements = [...sessions.disagreements, ...tasks.disagreements];
311
+ if (disagreements.length) {
312
+ const listed = disagreements.slice(0, DISAGREEMENT_LIMIT).map((row) => `${row.table}:${row.key} (${row.reason})`).join(', ');
313
+ const more = disagreements.length > DISAGREEMENT_LIMIT ? `, … ${disagreements.length - DISAGREEMENT_LIMIT} more` : '';
314
+ throw refusal(EXIT.DISAGREEMENT, `refusing: the two discriminators disagree on ${disagreements.length} row(s) owned by ${currentId}: ${listed}${more}`);
315
+ }
316
+ // D-D7: drafts move only through the media-store version allocator, never through raw SQL.
317
+ if (tableColumns(db, 'terminal_draft_images').has('owner_host_instance_id')) {
318
+ const drafts = db.prepare(`
319
+ SELECT attachment_id, created_at FROM terminal_draft_images WHERE owner_host_instance_id = ?
320
+ `).all(currentId);
321
+ const preClone = drafts.filter((row) => {
322
+ const ms = normalizeTimestampMs(row.created_at);
323
+ return ms !== null && ms < detectedAtMs;
324
+ });
325
+ if (preClone.length) {
326
+ throw refusal(EXIT.DRAFTS, `refusing: ${preClone.length} terminal_draft_images row(s) owned by ${currentId} predate the clone (e.g. ${String(preClone[0].attachment_id)}) — drafts move only through the media-store allocator`);
327
+ }
328
+ }
329
+ const sessionIds = sessions.foreign.map((row) => String(row.id));
330
+ const taskRows = tasks.foreign;
331
+ const leaseIds = [];
332
+ if (tableColumns(db, 'resource_leases').has('holder_instance_id') && sessionIds.length) {
333
+ const placeholders = sessionIds.map(() => '?').join(', ');
334
+ const leases = db.prepare(`
335
+ SELECT resource_id FROM resource_leases
336
+ WHERE resource_type = 'ctm_session' AND holder_instance_id = ? AND resource_id IN (${placeholders})
337
+ `).all(currentId, ...sessionIds);
338
+ for (const row of leases) leaseIds.push(String(row.resource_id));
339
+ }
340
+ const bindingIds = [];
341
+ if (tableColumns(db, 'workspace_bindings').has('host_instance_id')) {
342
+ const bindings = db.prepare('SELECT logical_id, created_at FROM workspace_bindings WHERE host_instance_id = ?').all(currentId);
343
+ for (const row of bindings) {
344
+ const ms = normalizeTimestampMs(row.created_at);
345
+ if (ms === null || ms >= detectedAtMs) continue;
346
+ // Moving host_instance_id is a PRIMARY KEY (logical_id, host_instance_id) rewrite: skip a
347
+ // binding the live instance already re-registered under the prior id (db.js's own
348
+ // conflict rule — never throw a UNIQUE/PK error, never lose the authoritative row).
349
+ const collision = db.prepare('SELECT 1 AS present FROM workspace_bindings WHERE logical_id = ? AND host_instance_id = ?').get(String(row.logical_id), prior);
350
+ if (collision) continue;
351
+ bindingIds.push(String(row.logical_id));
352
+ }
353
+ }
354
+ return {
355
+ dataDir,
356
+ dbPath,
357
+ currentId,
358
+ edge: {
359
+ id: Number(edge.id),
360
+ prior,
361
+ next: currentId,
362
+ detectedAt: String(edge.detected_at),
363
+ crossHost: edge.cross_host,
364
+ },
365
+ foreignComponent: [...foreignComponent],
366
+ rows: {
367
+ ctm_sessions: { foreign: sessionIds, native: sessions.native },
368
+ startup_tasks: {
369
+ foreign: taskRows.map((row) => String(row.ctm_session_id)),
370
+ park: taskRows.filter((row) => String(row.status ?? 'running') !== 'exited').map((row) => String(row.ctm_session_id)),
371
+ native: tasks.native,
372
+ },
373
+ resource_leases: { ids: leaseIds },
374
+ workspace_bindings: { logicalIds: bindingIds },
375
+ },
376
+ disagreements: [],
377
+ };
378
+ }
379
+
380
+ function isNothingToRepair(plan) {
381
+ if (!plan || !plan.rows) return true;
382
+ const { ctm_sessions: sessions, startup_tasks: tasks, resource_leases: leases, workspace_bindings: bindings } = plan.rows;
383
+ return sessions.foreign.length === 0
384
+ && tasks.foreign.length === 0
385
+ && leases.ids.length === 0
386
+ && bindings.logicalIds.length === 0
387
+ && Number(plan.edge?.crossHost) === 1;
388
+ }
389
+
390
+ // ── steps 8-10: backup + one transactional apply (D-D4/D-D6) ────────────────────────────────
391
+
392
+ function _backupStamp(now) {
393
+ return new Date(now).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
394
+ }
395
+
396
+ function createVerifiedBackup(db, { dataDir, dbPath, now = new Date() }) {
397
+ const dir = path.resolve(String(dataDir || '').trim() || getDataDir());
398
+ const backupDir = path.join(dir, 'backups', `foreign-clone-repair-${_backupStamp(now)}`);
399
+ if (fs.existsSync(backupDir)) {
400
+ throw refusal(EXIT.BACKUP_FAILED, `refusing to overwrite an existing backup directory: ${backupDir}`);
401
+ }
402
+ fs.mkdirSync(backupDir, { recursive: true });
403
+ const backupPath = path.join(backupDir, path.basename(dbPath));
404
+ try {
405
+ // VACUUM INTO is WAL-safe and takes a consistent snapshot of the committed state; a plain
406
+ // file copy of a live WAL database can silently omit committed frames.
407
+ db.prepare('VACUUM INTO ?').run(backupPath);
408
+ } catch (error) {
409
+ throw refusal(EXIT.BACKUP_FAILED, `VACUUM INTO ${backupPath} failed: ${String(error?.message || error)}`);
410
+ }
411
+ let check = '';
412
+ try {
413
+ const copy = openDatabase(backupPath, { readonly: true });
414
+ try { check = readPragmaScalar(copy, 'quick_check'); } finally { copy.close(); }
415
+ } catch (error) {
416
+ throw refusal(EXIT.BACKUP_FAILED, `backup ${backupPath} could not be verified: ${String(error?.message || error)}`);
417
+ }
418
+ if (check !== 'ok') throw refusal(EXIT.BACKUP_FAILED, `PRAGMA quick_check on ${backupPath} returned ${JSON.stringify(check)} — refusing to continue`);
419
+ return { backupDir, backupPath };
420
+ }
421
+
422
+ function applyRepair(db, plan, { dataDir = '', dbPath = '', now = new Date() } = {}) {
423
+ const { backupDir, backupPath } = createVerifiedBackup(db, { dataDir, dbPath, now });
424
+ const prior = String(plan.edge.prior);
425
+ const currentId = String(plan.currentId);
426
+ const parkedAt = new Date(now).toISOString();
427
+ const sessionColumns = tableColumns(db, 'ctm_sessions');
428
+ const changed = { ctm_sessions: [], startup_tasks: [], parked: [], resource_leases: 0, workspace_bindings: 0 };
429
+ const run = db.transaction(() => {
430
+ db.prepare('UPDATE instance_lineage SET cross_host = 1 WHERE id = ?').run(Number(plan.edge.id));
431
+ for (const id of plan.rows.ctm_sessions.foreign) {
432
+ db.prepare('UPDATE ctm_sessions SET current_owner_instance_id = ? WHERE id = ?').run(prior, id);
433
+ if (sessionColumns.has('execution_host_instance_id')) {
434
+ // execution_host_instance_id is a separate pointer and can name the current host while the
435
+ // owner already names the prior one; only the current host's value is the wrong one.
436
+ db.prepare('UPDATE ctm_sessions SET execution_host_instance_id = ? WHERE id = ? AND execution_host_instance_id = ?').run(prior, id, currentId);
437
+ }
438
+ changed.ctm_sessions.push(String(id));
439
+ }
440
+ for (const id of plan.rows.startup_tasks.foreign) {
441
+ db.prepare('UPDATE startup_tasks SET current_owner_instance_id = ? WHERE ctm_session_id = ?').run(prior, id);
442
+ if (plan.rows.startup_tasks.park.includes(id)) {
443
+ db.prepare(`
444
+ UPDATE startup_tasks SET status = 'parked', park_reason = ?, parked_at = ? WHERE ctm_session_id = ?
445
+ `).run(PARK_REASON, parkedAt, id);
446
+ changed.parked.push(String(id));
447
+ }
448
+ changed.startup_tasks.push(String(id));
449
+ }
450
+ for (const resourceId of plan.rows.resource_leases.ids) {
451
+ changed.resource_leases += db.prepare(`
452
+ UPDATE resource_leases SET holder_instance_id = ?
453
+ WHERE resource_type = 'ctm_session' AND holder_instance_id = ? AND resource_id = ?
454
+ `).run(prior, currentId, resourceId).changes || 0;
455
+ }
456
+ for (const logicalId of plan.rows.workspace_bindings.logicalIds) {
457
+ changed.workspace_bindings += db.prepare(`
458
+ UPDATE workspace_bindings SET host_instance_id = ?
459
+ WHERE logical_id = ? AND host_instance_id = ?
460
+ `).run(prior, logicalId, currentId).changes || 0;
461
+ }
462
+ });
463
+ run();
464
+ const report = {
465
+ at: parkedAt,
466
+ dbPath,
467
+ backupPath,
468
+ currentId,
469
+ edge: plan.edge,
470
+ foreignComponent: plan.foreignComponent,
471
+ changed,
472
+ };
473
+ fs.writeFileSync(path.join(backupDir, 'report.json'), `${JSON.stringify(report, null, 2)}\n`);
474
+ return { applied: true, backupDir, backupPath, report, changed };
475
+ }
476
+
477
+ // ── CLI ─────────────────────────────────────────────────────────────────────────────────────
478
+
479
+ function parseArgs(argv) {
480
+ const args = { edgePrior: '', dataDir: '', apply: false, json: false };
481
+ const list = Array.isArray(argv) ? argv : [];
482
+ for (let i = 0; i < list.length; i += 1) {
483
+ const token = String(list[i] ?? '');
484
+ if (token === '--apply') { args.apply = true; continue; }
485
+ if (token === '--json') { args.json = true; continue; }
486
+ if (token === '--edge-prior' || token === '--data-dir') {
487
+ const value = list[i + 1];
488
+ if (value === undefined || String(value).startsWith('--')) throw refusal(EXIT.USAGE, `${token} requires a value. ${USAGE}`);
489
+ i += 1;
490
+ if (token === '--edge-prior') args.edgePrior = String(value).trim();
491
+ else args.dataDir = String(value).trim();
492
+ continue;
493
+ }
494
+ if (token.startsWith('--edge-prior=')) { args.edgePrior = token.slice('--edge-prior='.length).trim(); continue; }
495
+ if (token.startsWith('--data-dir=')) { args.dataDir = token.slice('--data-dir='.length).trim(); continue; }
496
+ throw refusal(EXIT.USAGE, `unknown argument: ${token}. ${USAGE}`);
497
+ }
498
+ // An explicitly EMPTY --data-dir must not silently fall back to the operator's real data dir.
499
+ if (args.dataDir === '' && (Array.isArray(argv) && argv.some((token) => String(token).startsWith('--data-dir')))) {
500
+ throw refusal(EXIT.USAGE, `--data-dir requires a value. ${USAGE}`);
501
+ }
502
+ return args;
503
+ }
504
+
505
+ function printPlan(write, plan, { apply }) {
506
+ write(`[repair-foreign-clone] data dir: ${plan.dataDir}\n`);
507
+ write(`[repair-foreign-clone] database: ${plan.dbPath}\n`);
508
+ write(`[repair-foreign-clone] current id: ${plan.currentId}\n`);
509
+ write(`[repair-foreign-clone] edge: id=${plan.edge.id} prior=${plan.edge.prior} detected_at=${plan.edge.detectedAt} cross_host=${JSON.stringify(plan.edge.crossHost)}\n`);
510
+ write(`[repair-foreign-clone] foreign component: ${plan.foreignComponent.length} id(s)\n`);
511
+ write(`[repair-foreign-clone] ctm_sessions: foreign=${plan.rows.ctm_sessions.foreign.length} native=${plan.rows.ctm_sessions.native}\n`);
512
+ write(`[repair-foreign-clone] startup_tasks: foreign=${plan.rows.startup_tasks.foreign.length} (to park ${plan.rows.startup_tasks.park.length}) native=${plan.rows.startup_tasks.native}\n`);
513
+ write(`[repair-foreign-clone] resource_leases to restore: ${plan.rows.resource_leases.ids.length}\n`);
514
+ write(`[repair-foreign-clone] workspace_bindings to restore: ${plan.rows.workspace_bindings.logicalIds.length}\n`);
515
+ if (!apply) write('[repair-foreign-clone] dry run — nothing written. Re-run with --apply to repair.\n');
516
+ }
517
+
518
+ function main(argv, io = {}) {
519
+ const out = io.stdout || process.stdout;
520
+ const err = io.stderr || process.stderr;
521
+ try {
522
+ const args = parseArgs(argv);
523
+ if (!args.edgePrior) throw refusal(EXIT.USAGE, `--edge-prior is required. ${USAGE}`);
524
+ const dataDir = path.resolve(String(args.dataDir || '').trim() || getDataDir());
525
+ const dbPath = resolveDatabasePath(dataDir);
526
+ assertOffline(dataDir, dbPath);
527
+ const db = openDatabase(dbPath, { readonly: !args.apply });
528
+ try {
529
+ const plan = planRepair(db, { edgePrior: args.edgePrior, dataDir, dbPath });
530
+ if (args.json) out.write(`${JSON.stringify(plan, null, 2)}\n`);
531
+ else printPlan((text) => out.write(text), plan, { apply: args.apply });
532
+ if (!args.apply) return EXIT.OK;
533
+ if (isNothingToRepair(plan)) {
534
+ out.write('[repair-foreign-clone] nothing to repair — no backup taken\n');
535
+ return EXIT.OK;
536
+ }
537
+ out.write('[repair-foreign-clone] --apply: taking a verified backup, then repairing in one transaction\n');
538
+ const result = applyRepair(db, plan, { dataDir, dbPath });
539
+ out.write(`[repair-foreign-clone] repaired ${result.changed.ctm_sessions.length} session(s), ${result.changed.startup_tasks.length} task(s) (${result.changed.parked.length} parked)\n`);
540
+ out.write(`[repair-foreign-clone] backup: ${result.backupPath}\n`);
541
+ out.write(`[repair-foreign-clone] report: ${path.join(result.backupDir, 'report.json')}\n`);
542
+ return EXIT.OK;
543
+ } finally {
544
+ try { db.close(); } catch { /* expected: already-closed handle */ }
545
+ }
546
+ } catch (error) {
547
+ err.write(`[repair-foreign-clone] ${String(error?.message || error)}\n`);
548
+ return Number.isInteger(error?.exitCode) ? error.exitCode : 1;
549
+ }
550
+ }
551
+
552
+ if (require.main === module) {
553
+ process.exit(main(process.argv.slice(2)));
554
+ }
555
+
556
+ module.exports = {
557
+ EXIT,
558
+ resolveDatabasePath,
559
+ assertOffline,
560
+ planRepair,
561
+ applyRepair,
562
+ isNothingToRepair,
563
+ main,
564
+ };