create-walle 0.10.0 → 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 (381) hide show
  1. package/README.md +88 -41
  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 +7 -5
  6. package/package.json +10 -4
  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/capture-codex-rollout-records.js +417 -0
  11. package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
  12. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  13. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  14. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  16. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  17. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  18. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  21. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  22. package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
  23. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  24. package/template/coding-task-manager/db.js +705 -286
  25. package/template/coding-task-manager/git-utils.js +44 -0
  26. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  27. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  28. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  29. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  30. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  31. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  32. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  33. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  34. package/template/coding-task-manager/lib/api/machines.js +461 -0
  35. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  36. package/template/coding-task-manager/lib/api/providers.js +53 -6
  37. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  38. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  39. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  40. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  41. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  42. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  43. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  44. package/template/coding-task-manager/lib/auth-context.js +32 -2
  45. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  46. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  47. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  48. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  49. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  50. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  51. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  52. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  53. package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
  54. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  55. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  56. package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
  57. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  58. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  59. package/template/coding-task-manager/lib/codex-local-links.js +267 -0
  60. package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
  61. package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
  62. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  63. package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
  64. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  65. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  66. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  67. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  68. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  69. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  70. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  71. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  72. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  73. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  74. package/template/coding-task-manager/lib/db-domains.js +21 -2
  75. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  76. package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
  77. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  78. package/template/coding-task-manager/lib/feature-manifest.js +1 -3
  79. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  80. package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
  81. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  82. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  83. package/template/coding-task-manager/lib/headless-term-service.js +60 -23
  84. package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
  85. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  86. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  87. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  88. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  89. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  90. package/template/coding-task-manager/lib/host-stats.js +328 -0
  91. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  92. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  93. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  94. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  95. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  96. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  97. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  98. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  99. package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
  100. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  101. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  102. package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
  103. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  104. package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
  105. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
  106. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  107. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  108. package/template/coding-task-manager/lib/login-tripwire.js +18 -0
  109. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  110. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  111. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  112. package/template/coding-task-manager/lib/machine-self.js +66 -0
  113. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
  114. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  115. package/template/coding-task-manager/lib/media-journal.js +84 -27
  116. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  117. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  118. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  119. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
  120. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  121. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  122. package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
  123. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  124. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  125. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  126. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  127. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  128. package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
  129. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  130. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  131. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  132. package/template/coding-task-manager/lib/native-history-control.js +62 -14
  133. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  134. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  135. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  136. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  137. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  138. package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
  139. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  140. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  141. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  142. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  143. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  144. package/template/coding-task-manager/lib/raw-history-block.js +3 -1
  145. package/template/coding-task-manager/lib/read-pool-client.js +36 -24
  146. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  147. package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
  148. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  149. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  150. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  151. package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
  152. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  153. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  154. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  155. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  156. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  157. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  158. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  159. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  160. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  161. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  162. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  163. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  164. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  165. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  166. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  167. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  168. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  169. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  170. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  171. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  172. package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
  173. package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
  174. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  175. package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
  176. package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
  177. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  178. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  179. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  180. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  181. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  182. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  183. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  184. package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
  185. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  186. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  187. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  188. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  189. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  190. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  191. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  192. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  193. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  194. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  195. package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
  196. package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
  197. package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
  198. package/template/coding-task-manager/lib/session-standup.js +67 -4
  199. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  200. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  201. package/template/coding-task-manager/lib/session-status.js +2 -2
  202. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  203. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  204. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  205. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  206. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  207. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  208. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  209. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  210. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  211. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  212. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  213. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  214. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  215. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  216. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  217. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  218. package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
  219. package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
  220. package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
  221. package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
  222. package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
  223. package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
  224. package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
  225. package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
  226. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  227. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  228. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  229. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  230. package/template/coding-task-manager/lib/ws/input.js +1 -0
  231. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  232. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  233. package/template/coding-task-manager/lib/ws-router.js +68 -1
  234. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  235. package/template/coding-task-manager/oauth-proxy.js +99 -55
  236. package/template/coding-task-manager/package-lock.json +24 -0
  237. package/template/coding-task-manager/package.json +3 -0
  238. package/template/coding-task-manager/protocol/message-table.json +0 -10
  239. package/template/coding-task-manager/protocol/messages.js +0 -10
  240. package/template/coding-task-manager/protocol/ws-schema.json +0 -20
  241. package/template/coding-task-manager/protocol/ws-schema.md +0 -2
  242. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  243. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  244. package/template/coding-task-manager/public/css/app.css +279 -2
  245. package/template/coding-task-manager/public/css/reviews.css +4 -0
  246. package/template/coding-task-manager/public/index.html +297 -240
  247. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  248. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  249. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  250. package/template/coding-task-manager/public/js/background-work.js +13 -7
  251. package/template/coding-task-manager/public/js/client-health-core.js +81 -0
  252. package/template/coding-task-manager/public/js/client-health-main.js +61 -0
  253. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  254. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  255. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  256. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  257. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  258. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  259. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  260. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  261. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  262. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  263. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
  264. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  265. package/template/coding-task-manager/public/js/protocol.js +0 -10
  266. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  267. package/template/coding-task-manager/public/js/reviews.js +7 -3
  268. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  269. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  270. package/template/coding-task-manager/public/js/row-join.js +18 -3
  271. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  272. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  273. package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
  274. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  275. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  276. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  277. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  278. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  279. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  280. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  281. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  282. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  283. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  284. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  285. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
  286. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  287. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  288. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  289. package/template/coding-task-manager/public/m/app.css +226 -8
  290. package/template/coding-task-manager/public/m/app.js +502 -9
  291. package/template/coding-task-manager/public/m/index.html +38 -1
  292. package/template/coding-task-manager/public/m/machines.js +1313 -0
  293. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  294. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  295. package/template/coding-task-manager/public/m/sw.js +5 -0
  296. package/template/coding-task-manager/queue-engine.js +13 -0
  297. package/template/coding-task-manager/server/approvals.js +5 -6
  298. package/template/coding-task-manager/server/boot.js +136 -17
  299. package/template/coding-task-manager/server.js +964 -425
  300. package/template/coding-task-manager/session-integrity.js +7 -9
  301. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  302. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  303. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  304. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  305. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  306. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  307. package/template/package-lock.json +2 -2
  308. package/template/package.json +1 -1
  309. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  310. package/template/shared/conversation-storage-v2-contract.js +42 -6
  311. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  312. package/template/shared/git-remote-key.js +83 -0
  313. package/template/shared/host-identity.js +98 -2
  314. package/template/shared/owner-write-safety.js +135 -0
  315. package/template/shared/public-runtime.js +13 -5
  316. package/template/shared/release-schema.json +3 -2
  317. package/template/shared/report-citations.js +14 -0
  318. package/template/shared/sqlite-write-lock.js +361 -223
  319. package/template/shared/storage/contract.js +35 -3
  320. package/template/wall-e/agent.js +21 -0
  321. package/template/wall-e/api-walle.js +5 -0
  322. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  323. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  324. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  325. package/template/wall-e/brain/migrations.js +51 -22
  326. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  327. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  328. package/template/wall-e/brain/schema-ensures.js +22 -0
  329. package/template/wall-e/brain.js +1 -1
  330. package/template/wall-e/coding/host-capabilities.js +2 -2
  331. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  332. package/template/wall-e/coding/session-journal.js +28 -40
  333. package/template/wall-e/coding-orchestrator.js +23 -26
  334. package/template/wall-e/fly.toml +1 -1
  335. package/template/wall-e/http/auth.js +18 -1
  336. package/template/wall-e/http/blog-views-api.js +318 -0
  337. package/template/wall-e/http/helper-rules-api.js +321 -0
  338. package/template/wall-e/http/model-proxy-providers.js +3 -1
  339. package/template/wall-e/http/telemetry-api.js +12 -0
  340. package/template/wall-e/lib/blog-view-policy.js +88 -0
  341. package/template/wall-e/lib/blog-view-store.js +169 -0
  342. package/template/wall-e/lib/client-config-lock.js +2 -1
  343. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  344. package/template/wall-e/lib/error-kinds.js +10 -1
  345. package/template/wall-e/lib/feature-manifest.js +1 -1
  346. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  347. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  348. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  349. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  350. package/template/wall-e/lib/helper-rules/label.js +247 -0
  351. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  352. package/template/wall-e/lib/helper-rules/store.js +667 -0
  353. package/template/wall-e/lib/mcp-integration.js +5 -6
  354. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  355. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  356. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  357. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  358. package/template/wall-e/llm/default-fallback.js +4 -0
  359. package/template/wall-e/llm/model-catalog.js +13 -4
  360. package/template/wall-e/llm/model-roles.js +6 -0
  361. package/template/wall-e/llm/portkey-http.js +150 -0
  362. package/template/wall-e/llm/portkey-sync.js +15 -145
  363. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  364. package/template/wall-e/llm/worker-route.js +43 -1
  365. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  366. package/template/wall-e/mcp-protocol-contract.js +41 -1
  367. package/template/wall-e/mcp-server.js +13 -2
  368. package/template/wall-e/mcp-worker-schemas.js +20 -2
  369. package/template/wall-e/memory/ctm-context-client.js +25 -5
  370. package/template/wall-e/shared/release-schema.json +3 -2
  371. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  372. package/template/wall-e/shared/storage/contract.js +35 -3
  373. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  374. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  375. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  376. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  377. package/template/wall-e/tools/local-tools.js +12 -5
  378. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  379. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  380. package/template/wall-e/workers/telemetry-worker.js +8 -0
  381. package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
@@ -0,0 +1,321 @@
1
+ 'use strict';
2
+
3
+ // WP-4 (lead-cost-routing-round1-2026-10-21.html §6.4): the read-only state endpoint the stdio
4
+ // proxy calls before it answers a client's `initialize` locally.
5
+ //
6
+ // GET /api/wall-e/helper-rules/guidance?agent=codex|claude
7
+ // -> { ok: true, explore: 'on' | 'off' | 'probe' }
8
+ //
9
+ // Read-only and idempotent: it never creates the cell table (WP-5 owns the migration) and it
10
+ // never writes. An unknown/missing `agent` reads as 'claude'; a missing table or row reads 'on'
11
+ // (lib/helper-rules/cell-state.js owns that fail-open default).
12
+ //
13
+ // Mounted from api-walle.js's routeWalleApi() -- deliberately NOT inside handleWalleApi(), whose
14
+ // body is frozen by lint/file-size.config.json.
15
+ //
16
+ // WP-7 (design §6.9, D-18): the same module carries the panel's four routes -- the cell list with
17
+ // its trailing-window metrics, the labelled-outcome window, and the user's two switches:
18
+ //
19
+ // GET /api/wall-e/helper-rules/cells
20
+ // GET /api/wall-e/helper-rules/cells/:key/outcomes?limit=20
21
+ // POST /api/wall-e/helper-rules/cells/:key/veto -> state 'off', user_veto 1
22
+ // POST /api/wall-e/helper-rules/cells/:key/enable -> state 'on', user_veto 0
23
+ //
24
+ // The reads go through lib/helper-rules/store.js (the record's one reader) on the brain handle;
25
+ // the two switches go through brainWrite('helperRulesSetCellState', ...) -- the owner path, so
26
+ // this module holds no write SQL of its own (AC-7c). Every read is fail-open: a brain without the
27
+ // migration 100 tables answers 200 with the two explore cells and an empty window, never a 500.
28
+
29
+ const { jsonResponse } = require('./api-utils');
30
+ const { readExploreCellState } = require('../lib/helper-rules/cell-state');
31
+ const {
32
+ CELL_STATES,
33
+ kindFromCellKey,
34
+ listCells,
35
+ readCell,
36
+ readTrailingOutcomes,
37
+ } = require('../lib/helper-rules/store');
38
+
39
+ const BASE_PATH = '/api/wall-e/helper-rules';
40
+ const GUIDANCE_PATH = `${BASE_PATH}/guidance`;
41
+ const CELLS_PATH = `${BASE_PATH}/cells`;
42
+ // `:key` carries the cell key verbatim (URL-encoded); the action is the last path segment.
43
+ const CELL_ACTION_RE = /^\/api\/wall-e\/helper-rules\/cells\/(.+)\/(outcomes|veto|enable)$/;
44
+ // The two shapes the record mints: wait-watch|default|<route alias> and explore|default|<harness>.
45
+ const CELL_KEY_RE = /^(wait-watch|explore)\|default\|[A-Za-z0-9._:-]{1,120}$/;
46
+ // The panel must always show the exploration switch, even before the first outcome row exists.
47
+ const SYNTHETIC_EXPLORE_KEYS = Object.freeze(['explore|default|claude', 'explore|default|codex']);
48
+ const COLLECTING_MIN = 10;
49
+ const OUTCOMES_DEFAULT_LIMIT = 20;
50
+ const OUTCOMES_MAX_LIMIT = 100;
51
+
52
+ // brainWrite is required lazily: api-walle.js boots this module on every start, and the write
53
+ // path is only reachable from the two POST routes (same lazy-require idiom as
54
+ // coding-context.js). Tests inject their own writer through `deps.brainWrite`.
55
+ let _brainWriteImpl = null;
56
+ function _brainWrite(method, ...args) {
57
+ if (!_brainWriteImpl) ({ brainWrite: _brainWriteImpl } = require('../loops/brain-write'));
58
+ return _brainWriteImpl(method, ...args);
59
+ }
60
+
61
+ function _servedBrainDb(brain, ensureBrainInit) {
62
+ try {
63
+ if (!brain || (typeof ensureBrainInit === 'function' && !ensureBrainInit())) return null;
64
+ return typeof brain.getDb === 'function' ? brain.getDb() : null;
65
+ } catch {
66
+ // expected: the brain has not opened its DB yet -- readExploreCellState answers 'on'
67
+ return null;
68
+ }
69
+ }
70
+
71
+ function _cellState(row) {
72
+ const state = row && typeof row.state === 'string' ? row.state.trim() : '';
73
+ return CELL_STATES.includes(state) ? state : 'on';
74
+ }
75
+
76
+ function _parseMetrics(raw) {
77
+ if (raw && typeof raw === 'object') return raw;
78
+ if (raw == null || raw === '') return {};
79
+ try {
80
+ const parsed = JSON.parse(String(raw));
81
+ return parsed && typeof parsed === 'object' ? parsed : {};
82
+ } catch {
83
+ // expected: a malformed metrics_json carries no metrics -- the panel shows '-'
84
+ return {};
85
+ }
86
+ }
87
+
88
+ /** The most recent state event for one cell, or null. Read-only; a missing table is "no event". */
89
+ function _lastEvent(d, cellKey) {
90
+ if (!d || typeof d.prepare !== 'function') return null;
91
+ try {
92
+ const row = d.prepare('SELECT to_state, reason, created_at FROM helper_rule_events WHERE cell_key = ? ORDER BY id DESC LIMIT 1')
93
+ .get(String(cellKey));
94
+ return row ? { to_state: row.to_state, reason: row.reason, created_at: row.created_at } : null;
95
+ } catch {
96
+ // expected: helper_rule_events is absent until migration 100 -- no event to show
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * WP-6 (§6.9): the label version the panel stamps on the window -- the max the rows carry, or null
103
+ * when the window is empty (nothing has been labelled yet, which is not "v1").
104
+ */
105
+ function _windowLabelVersion(rows) {
106
+ let max = null;
107
+ for (const row of rows) {
108
+ const version = Number(row?.label_version);
109
+ if (Number.isFinite(version) && (max === null || version > max)) max = version;
110
+ }
111
+ return max;
112
+ }
113
+
114
+ /**
115
+ * One panel row. `n_window` is the trailing window the evaluator reads (readTrailingOutcomes'
116
+ * default limit), not a stored counter, so it cannot drift from the outcomes table.
117
+ * `collecting` prefers the stored metric (evaluate.js's D-7 measurability guard) and only falls
118
+ * back to the round-1 window-size rule when the row carries no stored verdict.
119
+ */
120
+ function _cellView(d, row) {
121
+ const cellKey = String(row.cell_key);
122
+ const window = readTrailingOutcomes(d, cellKey);
123
+ const metrics = _parseMetrics(row.metrics_json);
124
+ return {
125
+ cell_key: cellKey,
126
+ kind: row.kind || kindFromCellKey(cellKey),
127
+ state: _cellState(row),
128
+ user_veto: Number(row.user_veto) === 1 ? 1 : 0,
129
+ n_window: window.length,
130
+ label_version: _windowLabelVersion(window),
131
+ metrics,
132
+ collecting: typeof metrics.collecting === 'boolean' ? metrics.collecting : window.length < COLLECTING_MIN,
133
+ last_event: _lastEvent(d, cellKey),
134
+ };
135
+ }
136
+
137
+ function _syntheticExploreCell(cellKey) {
138
+ return {
139
+ cell_key: cellKey,
140
+ kind: 'explore',
141
+ state: 'on',
142
+ user_veto: 0,
143
+ n_window: 0,
144
+ label_version: null,
145
+ metrics: {},
146
+ collecting: true,
147
+ last_event: null,
148
+ };
149
+ }
150
+
151
+ function _cellsPayload(d) {
152
+ const views = listCells(d).map((row) => _cellView(d, row));
153
+ const known = new Set(views.map((view) => view.cell_key));
154
+ for (const key of SYNTHETIC_EXPLORE_KEYS) {
155
+ if (!known.has(key)) views.push(_syntheticExploreCell(key));
156
+ }
157
+ return views.sort((a, b) => (a.cell_key < b.cell_key ? -1 : a.cell_key > b.cell_key ? 1 : 0));
158
+ }
159
+
160
+ /** project the grader's JSON down to the verdict the panel shows -- never the raw blob. */
161
+ function _gradeView(raw) {
162
+ if (raw == null || raw === '') return null;
163
+ let parsed = raw;
164
+ if (typeof raw === 'string') {
165
+ try {
166
+ parsed = JSON.parse(raw);
167
+ } catch {
168
+ // expected: an unparsable grade_json carries no verdict -- the row reads as ungraded
169
+ return null;
170
+ }
171
+ }
172
+ if (!parsed || typeof parsed !== 'object') return null;
173
+ if (parsed.unavailable === true) return { unavailable: true };
174
+ const view = {};
175
+ for (const field of ['sufficient', 'coverage', 'inaccuracies']) {
176
+ if (field in parsed) view[field] = parsed[field];
177
+ }
178
+ return Object.keys(view).length ? view : null;
179
+ }
180
+
181
+ // WP-6 (§6.9): the ONLY fact fields the panel shows. The stored facts_json also holds the
182
+ // delegation's report/brief paths (grade.js reads them) and the wait's next_call arguments -- none
183
+ // of those may leave the brain over this read route, so the projection is a fixed allow-list,
184
+ // never a spread.
185
+ const FACT_VIEW_KEYS = Object.freeze([
186
+ 'terminal_status',
187
+ 'report_source',
188
+ 'report_citation_rows',
189
+ 'wait_variant',
190
+ 'harness',
191
+ ]);
192
+
193
+ /** The five displayable facts, or null when the row kept none (round-1 rows, unparsable blobs). */
194
+ function _factsView(raw) {
195
+ if (raw == null || raw === '') return null;
196
+ let parsed = raw;
197
+ if (typeof raw === 'string') {
198
+ try {
199
+ parsed = JSON.parse(raw);
200
+ } catch {
201
+ // expected: a row whose facts_json cannot be parsed carries no observed facts -- the panel shows '-'
202
+ return null;
203
+ }
204
+ }
205
+ if (!parsed || typeof parsed !== 'object') return null;
206
+ const view = {};
207
+ for (const key of FACT_VIEW_KEYS) {
208
+ if (key in parsed) view[key] = parsed[key];
209
+ }
210
+ return view;
211
+ }
212
+
213
+ /**
214
+ * One outcome row, field by field. Deliberately NOT a spread of the table row: the outcome
215
+ * window never carries the delegation's brief/report text, the grade's free-text `why`, the facts'
216
+ * paths/next-call args, or any session id beyond the row id itself (which, for wait-watch rows, is
217
+ * '<wait_id>:<worker_id>:<n>' by the schema -- an opaque handle the panel never decodes).
218
+ */
219
+ function _outcomeView(row) {
220
+ return {
221
+ id: row.id,
222
+ seq: row.seq,
223
+ kind: row.kind,
224
+ label: row.label ?? null,
225
+ decision: row.decision ?? null,
226
+ helper_usd: row.helper_usd ?? null,
227
+ lead_usd: row.lead_usd ?? null,
228
+ seconds_to_report: row.seconds_to_report ?? null,
229
+ report_rows: row.report_rows ?? null,
230
+ worker_id: row.worker_id ?? null,
231
+ label_version: row.label_version ?? null,
232
+ facts: _factsView(row.facts_json),
233
+ grade: _gradeView(row.grade_json),
234
+ created_at: row.created_at ?? null,
235
+ };
236
+ }
237
+
238
+ function _limitOf(url) {
239
+ const raw = Number.parseInt(url.searchParams.get('limit'), 10);
240
+ if (!Number.isFinite(raw)) return OUTCOMES_DEFAULT_LIMIT;
241
+ return Math.min(OUTCOMES_MAX_LIMIT, Math.max(1, raw));
242
+ }
243
+
244
+ function _decodeCellKey(raw) {
245
+ try {
246
+ return decodeURIComponent(raw);
247
+ } catch {
248
+ // expected: a malformed percent-escape is not a cell key -- answered as cell_key_invalid
249
+ return null;
250
+ }
251
+ }
252
+
253
+ function _methodNotAllowed(res) {
254
+ jsonResponse(res, { ok: false, error: 'Method not allowed' }, 405);
255
+ return true;
256
+ }
257
+
258
+ // The two switches. D-18: a veto is final -- only the user's enable clears it, and the decision
259
+ // is recorded as an event row by store.setCellState (reason 'user_veto' / 'user_enable').
260
+ async function _applyCellState(res, deps, cellKey, action) {
261
+ const patch = action === 'veto'
262
+ ? { state: 'off', user_veto: 1, reason: 'user_veto', evidence: { by: 'user' } }
263
+ : { state: 'on', user_veto: 0, reason: 'user_enable', evidence: { by: 'user' } };
264
+ const write = typeof deps.brainWrite === 'function' ? deps.brainWrite : _brainWrite;
265
+ try {
266
+ await write('helperRulesSetCellState', cellKey, patch);
267
+ } catch (err) {
268
+ jsonResponse(res, { ok: false, error: err?.message || 'helper_rules_write_failed' }, 503);
269
+ return true;
270
+ }
271
+ const d = _servedBrainDb(deps.brain, deps.ensureBrainInit);
272
+ const row = readCell(d, cellKey);
273
+ const cell = row ? _cellView(d, row) : { ..._syntheticExploreCell(cellKey), kind: kindFromCellKey(cellKey), state: patch.state, user_veto: patch.user_veto };
274
+ jsonResponse(res, { ok: true, cell }, 200);
275
+ return true;
276
+ }
277
+
278
+ function handleHelperRulesApi(req, res, url, deps = {}) {
279
+ const { brain, ensureBrainInit } = deps;
280
+
281
+ if (url.pathname === GUIDANCE_PATH) {
282
+ if (req.method !== 'GET') return _methodNotAllowed(res);
283
+ const agent = url.searchParams.get('agent') === 'codex' ? 'codex' : 'claude';
284
+ jsonResponse(res, { ok: true, explore: readExploreCellState(_servedBrainDb(brain, ensureBrainInit), agent) }, 200);
285
+ return true;
286
+ }
287
+
288
+ if (url.pathname === CELLS_PATH) {
289
+ if (req.method !== 'GET') return _methodNotAllowed(res);
290
+ jsonResponse(res, { ok: true, cells: _cellsPayload(_servedBrainDb(brain, ensureBrainInit)) }, 200);
291
+ return true;
292
+ }
293
+
294
+ const match = CELL_ACTION_RE.exec(url.pathname);
295
+ if (!match) return false;
296
+ const cellKey = _decodeCellKey(match[1]);
297
+ if (!cellKey || !CELL_KEY_RE.test(cellKey)) {
298
+ jsonResponse(res, { ok: false, error: 'cell_key_invalid' }, 400);
299
+ return true;
300
+ }
301
+ const action = match[2];
302
+
303
+ if (action === 'outcomes') {
304
+ if (req.method !== 'GET') return _methodNotAllowed(res);
305
+ const outcomes = readTrailingOutcomes(_servedBrainDb(brain, ensureBrainInit), cellKey, _limitOf(url))
306
+ .map((row) => _outcomeView(row));
307
+ jsonResponse(res, { ok: true, outcomes }, 200);
308
+ return true;
309
+ }
310
+
311
+ if (req.method !== 'POST') return _methodNotAllowed(res);
312
+ // Same precondition the sibling mutating Wall-E route next to the mount applies
313
+ // (routeWalleApi's POST /api/wall-e/background/completion): no brain, no write.
314
+ if (!brain || (typeof ensureBrainInit === 'function' && !ensureBrainInit())) {
315
+ jsonResponse(res, { error: 'Brain module not available' }, 503);
316
+ return true;
317
+ }
318
+ return _applyCellState(res, deps, cellKey, action);
319
+ }
320
+
321
+ module.exports = { handleHelperRulesApi };
@@ -54,7 +54,9 @@ function handleModelProxyProvidersFeed(req, res, url, {
54
54
  // D-3/D-5: the route's CLI-expressible effort levels, resolved from the
55
55
  // same row the route descriptor is built from, so the brain's deployment
56
56
  // facts carry them (they were always [] — the levels never reached CTM's
57
- // Codex model catalog).
57
+ // Codex model catalog). The per-(route, wire model) half of D-2 is
58
+ // derived inside the deployment projection from `protocol` (the only
59
+ // serializable route fact that survives this read's JSON IPC hop).
58
60
  const routeReasoningLevels = routeReasoningLevelsForRow(fullRow);
59
61
  profileFacts = await readBrain('workerRouteProfileFacts', [row.id, protocol, routeConfigRevision, { routeReasoningLevels }]);
60
62
  } catch {
@@ -17,6 +17,11 @@
17
17
  // module-state rate-limit bookkeeping (renamed from handleWalleApi._rlMap/
18
18
  // _rlTimer, moved verbatim).
19
19
  const { jsonResponse, readBody } = require('./api-utils');
20
+ // WP-1 (blog-view-counts, docs/blog-view-counts-2026-10-22.html §5 D-4): the
21
+ // public blog-view counter is a sibling of the telemetry routes — same module
22
+ // boundary, same injected deps, same dispatch chain — and is claimed FIRST so
23
+ // its exact path can never fall through to a telemetry handler.
24
+ const { handleBlogViewsApi } = require('./blog-views-api');
20
25
 
21
26
  function handleTelemetryApi(req, res, url, deps) {
22
27
  const {
@@ -47,6 +52,13 @@ function handleTelemetryApi(req, res, url, deps) {
47
52
  const m = req.method;
48
53
  const params = url.searchParams;
49
54
 
55
+ // --- Blog view counts (public, unauthenticated, exact path only) ---
56
+ // Before the telemetry routes: the handler claims only
57
+ // /api/wall-e/telemetry/blog-views and returns false for everything else, so the
58
+ // existing dispatch order and behavior below are untouched.
59
+ const blogViewsHandled = handleBlogViewsApi(req, res, url, { getTelemetryDb, errorSink });
60
+ if (blogViewsHandled !== false) return blogViewsHandled;
61
+
50
62
  // --- Telemetry endpoints (server mode only) ---
51
63
 
52
64
  // POST /api/wall-e/telemetry/ingest — receive anonymous telemetry from users
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ // WP-1 (docs/blog-view-counts-2026-10-22.html §5/§6): the ONE place the blog
4
+ // view counter's public surface is described — route, article allowlist, policy
5
+ // ceilings, accepted browser origins and the approximate bot filter. Pure data
6
+ // and pure predicates: no env read, no database handle, no module state, no
7
+ // side effects at require time.
8
+ //
9
+ // Counting semantics are deliberately approximate (design §11): one visible
10
+ // article opening per 30-minute per-tab token, no unique-person claim, no
11
+ // historical backfill.
12
+
13
+ // The EXACT public path. It lives under the /api/wall-e/telemetry/ prefix on
14
+ // purpose (design §5 D-4, lead review correction): api-walle.js already routes
15
+ // that prefix to lib/telemetry-process-client.js in the daemon role, so the
16
+ // counter runs in the isolated telemetry process — no SQLite on the foreground
17
+ // daemon — without touching the frozen dispatcher.
18
+ const BLOG_VIEWS_PATH = '/api/wall-e/telemetry/blog-views';
19
+
20
+ // The three published static articles. A slug outside this frozen list is never
21
+ // a valid counter key, so an arbitrary/hostile value can never reach a query.
22
+ const BLOG_POSTS = Object.freeze(['delegation-economics', 'delegation-guide', 'paste-the-link']);
23
+
24
+ // Repeat-visit suppression window: a per-tab random token counts once per 30 min.
25
+ const BLOG_VIEW_WINDOW_MS = 1800000;
26
+
27
+ // Product policy ceilings (conservative, not measured traffic — design §11).
28
+ const BLOG_VIEW_MAX_RECEIPTS = 100000;
29
+ const BLOG_VIEW_MAX_BODY_BYTES = 1024;
30
+
31
+ // Bounded receipt pruning: a POST removes at most this many expired receipts.
32
+ const BLOG_VIEW_PRUNE_BATCH = 1000;
33
+
34
+ // The only browser origins whose POST may increment a counter (and the only ones
35
+ // reflected back in CORS). Both the apex and the www host serve the site.
36
+ const BLOG_VIEW_ORIGINS = Object.freeze(['https://walle.sh', 'https://www.walle.sh']);
37
+
38
+ // User-Agent input is truncated before matching; the pattern is an approximate
39
+ // filter (design D-4), never proof that a visitor is human.
40
+ const BLOG_VIEW_USER_AGENT_MAX = 1024;
41
+ const BLOG_VIEW_BOT_PATTERN = /bot|crawler|spider|headless|curl|wget|python|preview|facebookexternalhit|meta-externalagent|bytespider|gptbot|claudebot/i;
42
+
43
+ // Canonical lowercase 8-4-4-4-12 UUID text (the shape crypto.randomUUID emits).
44
+ const BLOG_VIEW_TOKEN_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
45
+
46
+ function isBlogViewsPath(pathname) {
47
+ return pathname === BLOG_VIEWS_PATH;
48
+ }
49
+
50
+ // Returns the request's origin only when it is EXACTLY one of the two published
51
+ // site origins; every other present origin is null (the caller turns that into a
52
+ // refusal — the private API's localhost-only set is never widened).
53
+ function getBlogViewOrigin(origin) {
54
+ if (typeof origin !== 'string') return null;
55
+ return BLOG_VIEW_ORIGINS.includes(origin) ? origin : null;
56
+ }
57
+
58
+ function isBlogPostSlug(slug) {
59
+ return typeof slug === 'string' && BLOG_POSTS.includes(slug);
60
+ }
61
+
62
+ function isBlogViewToken(token) {
63
+ return typeof token === 'string' && BLOG_VIEW_TOKEN_PATTERN.test(token);
64
+ }
65
+
66
+ // Truncated, case-insensitive, deliberately approximate reconnaissance filter.
67
+ function isKnownBotUserAgent(userAgent) {
68
+ if (typeof userAgent !== 'string' || userAgent === '') return false;
69
+ return BLOG_VIEW_BOT_PATTERN.test(userAgent.slice(0, BLOG_VIEW_USER_AGENT_MAX));
70
+ }
71
+
72
+ module.exports = {
73
+ BLOG_POSTS,
74
+ BLOG_VIEWS_PATH,
75
+ BLOG_VIEW_BOT_PATTERN,
76
+ BLOG_VIEW_MAX_BODY_BYTES,
77
+ BLOG_VIEW_MAX_RECEIPTS,
78
+ BLOG_VIEW_ORIGINS,
79
+ BLOG_VIEW_PRUNE_BATCH,
80
+ BLOG_VIEW_TOKEN_PATTERN,
81
+ BLOG_VIEW_USER_AGENT_MAX,
82
+ BLOG_VIEW_WINDOW_MS,
83
+ getBlogViewOrigin,
84
+ isBlogPostSlug,
85
+ isBlogViewsPath,
86
+ isBlogViewToken,
87
+ isKnownBotUserAgent,
88
+ };
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ // WP-1 (docs/blog-view-counts-2026-10-22.html §5 D-2, §6): durable per-article
4
+ // view counters on the EXISTING telemetry.db. Two additive tables — a running
5
+ // total per article and a bounded set of short-lived per-tab receipts — updated
6
+ // in one IMMEDIATE transaction, so a replay is a no-op and a concurrent writer
7
+ // can never double-count.
8
+ //
9
+ // The handle is ALWAYS injected: nothing here opens a database, calls initDb(),
10
+ // schedules a job or reads the environment. This module contains NO DDL: the two
11
+ // tables are declared and created by lib/telemetry-db-schema.js (the
12
+ // walle-telemetry migration owner), whose baseline and additive migration 2 both
13
+ // call its own createBlogViewTables(). Only queries live here, and the module
14
+ // requires nothing but the pure policy module — no cycle back through the schema.
15
+ //
16
+ // Privacy (design D-5): a receipt stores the slug, the opaque random per-tab
17
+ // token and its expiry — nothing else. No IP address, User-Agent, referrer,
18
+ // account or derived fingerprint is ever written; the raw token is a
19
+ // deduplication key, not a credential, and expires in 30 minutes.
20
+ //
21
+ // Counting semantics are approximate (design §11): a 30-minute per-tab
22
+ // suppression window, not unique people.
23
+
24
+ const {
25
+ BLOG_VIEW_MAX_RECEIPTS,
26
+ BLOG_VIEW_PRUNE_BATCH,
27
+ BLOG_VIEW_WINDOW_MS,
28
+ BLOG_POSTS,
29
+ isBlogPostSlug,
30
+ isBlogViewToken,
31
+ } = require('./blog-view-policy');
32
+
33
+ // Typed store errors: the API layer maps them to status codes without ever
34
+ // having to parse a message.
35
+ const BLOG_VIEW_INVALID_SLUG = 'blog_view_invalid_slug';
36
+ const BLOG_VIEW_INVALID_TOKEN = 'blog_view_invalid_token';
37
+ const BLOG_VIEW_INVALID_STATE = 'blog_view_invalid_state';
38
+ const BLOG_VIEW_CAPACITY = 'blog_view_capacity';
39
+ const BLOG_VIEW_OVERFLOW = 'blog_view_overflow';
40
+
41
+ function blogViewError(code, message) {
42
+ const error = new Error(message);
43
+ error.code = code;
44
+ return error;
45
+ }
46
+
47
+ // Current totals for every published article, as {slug: views}. A known article
48
+ // with no row yet is authoritatively 0; an unsafe or negative stored value is
49
+ // refused rather than rendered.
50
+ function getBlogViewCounts(db) {
51
+ const placeholders = BLOG_POSTS.map(() => '?').join(', ');
52
+ const rows = db.prepare(
53
+ `SELECT slug, views FROM blog_view_counts WHERE slug IN (${placeholders})`
54
+ ).all(...BLOG_POSTS);
55
+ const counts = {};
56
+ for (const slug of BLOG_POSTS) counts[slug] = 0;
57
+ for (const row of rows) {
58
+ // Prototype-safe: only the frozen allowlist's own keys are ever assigned to,
59
+ // so a hostile stored slug can never write through to Object.prototype.
60
+ if (!Object.prototype.hasOwnProperty.call(counts, row.slug)) continue;
61
+ if (!Number.isSafeInteger(row.views) || row.views < 0) {
62
+ throw blogViewError(BLOG_VIEW_INVALID_STATE, 'stored blog view count is not a safe non-negative integer');
63
+ }
64
+ counts[row.slug] = row.views;
65
+ }
66
+ return counts;
67
+ }
68
+
69
+ // Records one view for `slug` from the per-tab `token` at `nowMs`, returning
70
+ // {slug, views, counted}. `counted` is false when this exact token already has
71
+ // an unexpired receipt (a refresh or a duplicate submission) — the stored total
72
+ // is then returned unchanged, even when the receipt table is at its cap.
73
+ //
74
+ // Throws a typed error for an unknown slug, a non-canonical token, a full
75
+ // receipt table or a total at the safe-integer ceiling; the caller maps those to
76
+ // a refusal and never acknowledges an uncounted view.
77
+ function recordBlogView(db, slug, token, nowMs = Date.now()) {
78
+ // Validate the ONLY two things that can reach SQL, before any statement runs:
79
+ // a frozen-allowlist slug and canonical UUID text. No arbitrary key ever
80
+ // becomes a query parameter.
81
+ if (!isBlogPostSlug(slug)) {
82
+ throw blogViewError(BLOG_VIEW_INVALID_SLUG, 'unknown blog post slug');
83
+ }
84
+ // Canonical text only, and nothing below folds case: isBlogViewToken() accepts
85
+ // ONLY the lowercase 8-4-4-4-12 spelling, so an uppercased uuid is refused as a
86
+ // malformed token rather than silently normalized into somebody else's receipt
87
+ // key.
88
+ if (!isBlogViewToken(token)) {
89
+ throw blogViewError(BLOG_VIEW_INVALID_TOKEN, 'token is not a canonical uuid');
90
+ }
91
+ const now = Number.isSafeInteger(nowMs) ? nowMs : Date.now();
92
+
93
+ const record = db.transaction(() => {
94
+ // 1. Bounded, indexed prune of expired receipts (at most
95
+ // BLOG_VIEW_PRUNE_BATCH rows): the table stays bounded with no job.
96
+ db.prepare(
97
+ `DELETE FROM blog_view_receipts WHERE rowid IN (
98
+ SELECT rowid FROM blog_view_receipts
99
+ WHERE expires_at_ms <= ?
100
+ ORDER BY expires_at_ms
101
+ LIMIT ?
102
+ )`
103
+ ).run(now, BLOG_VIEW_PRUNE_BATCH);
104
+
105
+ // 2. An EXACT unexpired receipt is a duplicate: the stored total is returned
106
+ // unchanged, even when the table is at its cap.
107
+ const existing = db.prepare(
108
+ 'SELECT expires_at_ms FROM blog_view_receipts WHERE slug = ? AND token = ?'
109
+ ).get(slug, token);
110
+ if (existing && existing.expires_at_ms > now) {
111
+ const current = db.prepare('SELECT views FROM blog_view_counts WHERE slug = ?').get(slug);
112
+ const views = current ? current.views : 0;
113
+ if (!Number.isSafeInteger(views) || views < 0) {
114
+ throw blogViewError(BLOG_VIEW_INVALID_STATE, 'stored blog view count is not a safe non-negative integer');
115
+ }
116
+ return { slug, views, counted: false };
117
+ }
118
+ // An expired receipt for this exact token is removed before its replacement
119
+ // is inserted, so the primary key never collides and no stale expiry is
120
+ // reused. (Step 1 usually removed it already; this is the exact case.)
121
+ if (existing) {
122
+ db.prepare('DELETE FROM blog_view_receipts WHERE slug = ? AND token = ?').run(slug, token);
123
+ }
124
+
125
+ // 3. Capacity is a policy bound, not memory: a full table refuses the view
126
+ // (the transaction rolls back, so the cap stays full) instead of
127
+ // acknowledging an uncounted one.
128
+ const receiptCount = db.prepare('SELECT count(*) AS n FROM blog_view_receipts').get().n;
129
+ if (receiptCount >= BLOG_VIEW_MAX_RECEIPTS) {
130
+ throw blogViewError(BLOG_VIEW_CAPACITY, 'blog view receipt capacity reached');
131
+ }
132
+
133
+ // 4. Never wrap: a total at the safe-integer ceiling refuses the view.
134
+ const currentRow = db.prepare('SELECT views FROM blog_view_counts WHERE slug = ?').get(slug);
135
+ const currentViews = currentRow ? currentRow.views : 0;
136
+ if (!Number.isSafeInteger(currentViews) || currentViews < 0) {
137
+ throw blogViewError(BLOG_VIEW_INVALID_STATE, 'stored blog view count is not a safe non-negative integer');
138
+ }
139
+ if (currentViews >= Number.MAX_SAFE_INTEGER) {
140
+ throw blogViewError(BLOG_VIEW_OVERFLOW, 'blog view count reached the safe integer ceiling');
141
+ }
142
+
143
+ db.prepare(
144
+ `INSERT INTO blog_view_receipts (slug, token, expires_at_ms) VALUES (?, ?, ?)
145
+ ON CONFLICT(slug, token) DO UPDATE SET expires_at_ms = excluded.expires_at_ms`
146
+ ).run(slug, token, now + BLOG_VIEW_WINDOW_MS);
147
+ db.prepare(
148
+ `INSERT INTO blog_view_counts (slug, views) VALUES (?, 1)
149
+ ON CONFLICT(slug) DO UPDATE SET views = views + 1`
150
+ ).run(slug);
151
+
152
+ const committed = db.prepare('SELECT views FROM blog_view_counts WHERE slug = ?').get(slug);
153
+ return { slug, views: committed ? committed.views : 0, counted: true };
154
+ });
155
+
156
+ // IMMEDIATE: take the write lock at BEGIN, so two competing writers serialize
157
+ // instead of one failing late with SQLITE_BUSY after a stale read.
158
+ return record.immediate();
159
+ }
160
+
161
+ module.exports = {
162
+ BLOG_VIEW_CAPACITY,
163
+ BLOG_VIEW_INVALID_SLUG,
164
+ BLOG_VIEW_INVALID_STATE,
165
+ BLOG_VIEW_INVALID_TOKEN,
166
+ BLOG_VIEW_OVERFLOW,
167
+ getBlogViewCounts,
168
+ recordBlogView,
169
+ };
@@ -43,6 +43,7 @@ const os = require('os');
43
43
  const path = require('path');
44
44
 
45
45
  const { ClientConfigError, isClientConfigError } = require('./mcp-client-config');
46
+ const { isWritableOnlyByOwner } = require('../../shared/owner-write-safety');
46
47
 
47
48
  const POLL_MS = 100; // admission cadence, never a performance budget
48
49
  const DEFAULT_TIMEOUT_MS = 10000; // admission deadline only
@@ -145,7 +146,7 @@ function _modeBits(stat) {
145
146
  }
146
147
 
147
148
  function _isGroupOrOtherWritable(stat) {
148
- return (_exactInteger(stat.mode) & 0o022n) !== 0n;
149
+ return !isWritableOnlyByOwner(stat);
149
150
  }
150
151
 
151
152
  function _hasStickyBit(stat) {