toolroll 0.5.0

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 (626) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1113 -0
  3. package/THIRD_PARTY_NOTICES.md +449 -0
  4. package/dist/accent-colors.d.ts +48 -0
  5. package/dist/accent-colors.js +122 -0
  6. package/dist/action-ledger.d.ts +26 -0
  7. package/dist/action-ledger.js +75 -0
  8. package/dist/agent-fence.d.ts +42 -0
  9. package/dist/agent-fence.js +183 -0
  10. package/dist/agentconfig.d.ts +216 -0
  11. package/dist/agentconfig.js +454 -0
  12. package/dist/api-tokens.d.ts +16 -0
  13. package/dist/api-tokens.js +27 -0
  14. package/dist/approval-policy.d.ts +69 -0
  15. package/dist/approval-policy.js +137 -0
  16. package/dist/approval-rules-ui.d.ts +25 -0
  17. package/dist/approval-rules-ui.js +39 -0
  18. package/dist/assignment-adapters.d.ts +180 -0
  19. package/dist/assignment-adapters.js +239 -0
  20. package/dist/assignment-brief.d.ts +71 -0
  21. package/dist/assignment-brief.js +129 -0
  22. package/dist/assignment-delivery.d.ts +56 -0
  23. package/dist/assignment-delivery.js +160 -0
  24. package/dist/assignment-presentation.d.ts +26 -0
  25. package/dist/assignment-presentation.js +80 -0
  26. package/dist/assignment-status.d.ts +62 -0
  27. package/dist/assignment-status.js +152 -0
  28. package/dist/assignment-ui.d.ts +71 -0
  29. package/dist/assignment-ui.js +103 -0
  30. package/dist/assignment.d.ts +222 -0
  31. package/dist/assignment.js +399 -0
  32. package/dist/attest.d.ts +56 -0
  33. package/dist/attest.js +153 -0
  34. package/dist/backend.d.ts +97 -0
  35. package/dist/backend.js +166 -0
  36. package/dist/backup-ui.d.ts +22 -0
  37. package/dist/backup-ui.js +59 -0
  38. package/dist/backup.d.ts +84 -0
  39. package/dist/backup.js +421 -0
  40. package/dist/beads.d.ts +34 -0
  41. package/dist/beads.js +135 -0
  42. package/dist/bin.d.ts +2 -0
  43. package/dist/bin.js +22 -0
  44. package/dist/board.d.ts +150 -0
  45. package/dist/board.js +210 -0
  46. package/dist/boot-identity.d.ts +63 -0
  47. package/dist/boot-identity.js +99 -0
  48. package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
  49. package/dist/browser/workspace.css +4 -0
  50. package/dist/browser/workspace.js +225 -0
  51. package/dist/browser-crew.d.ts +11 -0
  52. package/dist/browser-crew.js +50 -0
  53. package/dist/browser-shell.d.ts +8 -0
  54. package/dist/browser-shell.js +60 -0
  55. package/dist/browser-workspace.d.ts +780 -0
  56. package/dist/browser-workspace.js +47 -0
  57. package/dist/budget-alerts.d.ts +19 -0
  58. package/dist/budget-alerts.js +46 -0
  59. package/dist/builder.d.ts +370 -0
  60. package/dist/builder.js +3262 -0
  61. package/dist/capscan.d.ts +35 -0
  62. package/dist/capscan.js +113 -0
  63. package/dist/chat-acceptance.d.ts +71 -0
  64. package/dist/chat-acceptance.js +120 -0
  65. package/dist/chat-actions.d.ts +265 -0
  66. package/dist/chat-actions.js +1273 -0
  67. package/dist/chat-channel.d.ts +109 -0
  68. package/dist/chat-channel.js +652 -0
  69. package/dist/chat-continuity.d.ts +32 -0
  70. package/dist/chat-continuity.js +317 -0
  71. package/dist/chat-controls.d.ts +113 -0
  72. package/dist/chat-controls.js +53 -0
  73. package/dist/chat-delivery-state.d.ts +159 -0
  74. package/dist/chat-delivery-state.js +309 -0
  75. package/dist/chat-delivery.d.ts +38 -0
  76. package/dist/chat-delivery.js +693 -0
  77. package/dist/chat-display.d.ts +1 -0
  78. package/dist/chat-display.js +18 -0
  79. package/dist/chat-evidence.d.ts +123 -0
  80. package/dist/chat-evidence.js +207 -0
  81. package/dist/chat-flow.d.ts +44 -0
  82. package/dist/chat-flow.js +215 -0
  83. package/dist/chat-inbox.d.ts +57 -0
  84. package/dist/chat-inbox.js +123 -0
  85. package/dist/chat-polish.d.ts +16 -0
  86. package/dist/chat-polish.js +135 -0
  87. package/dist/chat-review.d.ts +46 -0
  88. package/dist/chat-review.js +98 -0
  89. package/dist/chat-rooms.d.ts +123 -0
  90. package/dist/chat-rooms.js +216 -0
  91. package/dist/chat-task-actions.d.ts +44 -0
  92. package/dist/chat-task-actions.js +79 -0
  93. package/dist/check-progress.d.ts +51 -0
  94. package/dist/check-progress.js +290 -0
  95. package/dist/child-database.d.ts +12 -0
  96. package/dist/child-database.js +30 -0
  97. package/dist/claim.d.ts +688 -0
  98. package/dist/claim.js +1740 -0
  99. package/dist/cli.d.ts +137 -0
  100. package/dist/cli.js +1461 -0
  101. package/dist/codex-limits.d.ts +15 -0
  102. package/dist/codex-limits.js +145 -0
  103. package/dist/coding-context.d.ts +55 -0
  104. package/dist/coding-context.js +92 -0
  105. package/dist/coding-handoff.d.ts +57 -0
  106. package/dist/coding-handoff.js +277 -0
  107. package/dist/coding-provider.d.ts +72 -0
  108. package/dist/coding-provider.js +424 -0
  109. package/dist/coding-shipping-ui.d.ts +4 -0
  110. package/dist/coding-shipping-ui.js +12 -0
  111. package/dist/coding-types.d.ts +62 -0
  112. package/dist/coding-types.js +1 -0
  113. package/dist/coding-ui.d.ts +17 -0
  114. package/dist/coding-ui.js +364 -0
  115. package/dist/coding-update.d.ts +15 -0
  116. package/dist/coding-update.js +149 -0
  117. package/dist/coding-workspace.d.ts +124 -0
  118. package/dist/coding-workspace.js +768 -0
  119. package/dist/container-state.d.ts +5 -0
  120. package/dist/container-state.js +62 -0
  121. package/dist/containment.d.ts +197 -0
  122. package/dist/containment.js +559 -0
  123. package/dist/contest.d.ts +318 -0
  124. package/dist/contest.js +753 -0
  125. package/dist/control-setup.d.ts +35 -0
  126. package/dist/control-setup.js +40 -0
  127. package/dist/control-ui.d.ts +29 -0
  128. package/dist/control-ui.js +53 -0
  129. package/dist/controller-service.d.ts +1 -0
  130. package/dist/controller-service.js +19 -0
  131. package/dist/controller-supervisor.d.ts +30 -0
  132. package/dist/controller-supervisor.js +104 -0
  133. package/dist/converse.d.ts +304 -0
  134. package/dist/converse.js +849 -0
  135. package/dist/coordinator-proposals.d.ts +29 -0
  136. package/dist/coordinator-proposals.js +137 -0
  137. package/dist/coordinator.d.ts +186 -0
  138. package/dist/coordinator.js +447 -0
  139. package/dist/credentials-ui.d.ts +25 -0
  140. package/dist/credentials-ui.js +48 -0
  141. package/dist/daemon.d.ts +209 -0
  142. package/dist/daemon.js +604 -0
  143. package/dist/decision.d.ts +120 -0
  144. package/dist/decision.js +388 -0
  145. package/dist/demo.d.ts +55 -0
  146. package/dist/demo.js +992 -0
  147. package/dist/desktop-access.d.ts +28 -0
  148. package/dist/desktop-access.js +88 -0
  149. package/dist/desktop-bundle.d.ts +16 -0
  150. package/dist/desktop-bundle.js +74 -0
  151. package/dist/desktop-host.d.ts +54 -0
  152. package/dist/desktop-host.js +508 -0
  153. package/dist/desktop-update-gate.d.ts +12 -0
  154. package/dist/desktop-update-gate.js +91 -0
  155. package/dist/desktop-update-recovery.d.ts +14 -0
  156. package/dist/desktop-update-recovery.js +243 -0
  157. package/dist/desktop-update.d.ts +110 -0
  158. package/dist/desktop-update.js +740 -0
  159. package/dist/discord-api.d.ts +20 -0
  160. package/dist/discord-api.js +138 -0
  161. package/dist/discord-chat.d.ts +16 -0
  162. package/dist/discord-chat.js +381 -0
  163. package/dist/discord-settings.d.ts +7 -0
  164. package/dist/discord-settings.js +75 -0
  165. package/dist/discord.d.ts +10 -0
  166. package/dist/discord.js +169 -0
  167. package/dist/discover.d.ts +75 -0
  168. package/dist/discover.js +150 -0
  169. package/dist/dispatch.d.ts +100 -0
  170. package/dist/dispatch.js +311 -0
  171. package/dist/dispose.d.ts +161 -0
  172. package/dist/dispose.js +803 -0
  173. package/dist/email-settings.d.ts +34 -0
  174. package/dist/email-settings.js +50 -0
  175. package/dist/envelope.d.ts +47 -0
  176. package/dist/envelope.js +105 -0
  177. package/dist/evidence-pack.d.ts +185 -0
  178. package/dist/evidence-pack.js +258 -0
  179. package/dist/evidence.d.ts +345 -0
  180. package/dist/evidence.js +782 -0
  181. package/dist/exec.d.ts +285 -0
  182. package/dist/exec.js +1841 -0
  183. package/dist/exhaustion.d.ts +85 -0
  184. package/dist/exhaustion.js +141 -0
  185. package/dist/export-ui.d.ts +4 -0
  186. package/dist/export-ui.js +17 -0
  187. package/dist/export.d.ts +48 -0
  188. package/dist/export.js +305 -0
  189. package/dist/flow-actions.d.ts +77 -0
  190. package/dist/flow-actions.js +203 -0
  191. package/dist/flow-code.d.ts +50 -0
  192. package/dist/flow-code.js +159 -0
  193. package/dist/flow-draft.d.ts +38 -0
  194. package/dist/flow-draft.js +80 -0
  195. package/dist/flow-engine.d.ts +66 -0
  196. package/dist/flow-engine.js +416 -0
  197. package/dist/flow-insights.d.ts +95 -0
  198. package/dist/flow-insights.js +149 -0
  199. package/dist/flow-live.d.ts +24 -0
  200. package/dist/flow-live.js +150 -0
  201. package/dist/flow-people.d.ts +24 -0
  202. package/dist/flow-people.js +91 -0
  203. package/dist/flow-replies.d.ts +48 -0
  204. package/dist/flow-replies.js +113 -0
  205. package/dist/flow-scripts.d.ts +38 -0
  206. package/dist/flow-scripts.js +73 -0
  207. package/dist/flow-secrets.d.ts +13 -0
  208. package/dist/flow-secrets.js +45 -0
  209. package/dist/flow-sort.d.ts +82 -0
  210. package/dist/flow-sort.js +153 -0
  211. package/dist/flow-steps.d.ts +40 -0
  212. package/dist/flow-steps.js +408 -0
  213. package/dist/flow-triggers.d.ts +244 -0
  214. package/dist/flow-triggers.js +959 -0
  215. package/dist/flows-ui.d.ts +20 -0
  216. package/dist/flows-ui.js +175 -0
  217. package/dist/flows.d.ts +229 -0
  218. package/dist/flows.js +968 -0
  219. package/dist/fonts.d.ts +19 -0
  220. package/dist/fonts.js +19 -0
  221. package/dist/gaps.d.ts +35 -0
  222. package/dist/gaps.js +101 -0
  223. package/dist/gate-failure.d.ts +40 -0
  224. package/dist/gate-failure.js +66 -0
  225. package/dist/git.d.ts +54 -0
  226. package/dist/git.js +94 -0
  227. package/dist/google-mail.d.ts +58 -0
  228. package/dist/google-mail.js +151 -0
  229. package/dist/grant.d.ts +136 -0
  230. package/dist/grant.js +238 -0
  231. package/dist/graph.d.ts +164 -0
  232. package/dist/graph.js +383 -0
  233. package/dist/guides.d.ts +22 -0
  234. package/dist/guides.js +363 -0
  235. package/dist/held.d.ts +150 -0
  236. package/dist/held.js +799 -0
  237. package/dist/invoke.d.ts +112 -0
  238. package/dist/invoke.js +780 -0
  239. package/dist/issues.d.ts +41 -0
  240. package/dist/issues.js +131 -0
  241. package/dist/job-object-helper.ps1 +252 -0
  242. package/dist/jsonl-discriminants.d.ts +11 -0
  243. package/dist/jsonl-discriminants.js +93 -0
  244. package/dist/keys.d.ts +104 -0
  245. package/dist/keys.js +229 -0
  246. package/dist/kits-ui.d.ts +17 -0
  247. package/dist/kits-ui.js +46 -0
  248. package/dist/kits.d.ts +86 -0
  249. package/dist/kits.js +215 -0
  250. package/dist/knowledge-cli.d.ts +101 -0
  251. package/dist/knowledge-cli.js +92 -0
  252. package/dist/knowledge-ui.d.ts +24 -0
  253. package/dist/knowledge-ui.js +40 -0
  254. package/dist/lead-context.d.ts +8 -0
  255. package/dist/lead-context.js +56 -0
  256. package/dist/lead-follow.d.ts +22 -0
  257. package/dist/lead-follow.js +167 -0
  258. package/dist/lead-status.d.ts +75 -0
  259. package/dist/lead-status.js +285 -0
  260. package/dist/ledger-chain.d.ts +46 -0
  261. package/dist/ledger-chain.js +187 -0
  262. package/dist/ledger-csv.d.ts +4 -0
  263. package/dist/ledger-csv.js +11 -0
  264. package/dist/ledger-view.d.ts +24 -0
  265. package/dist/ledger-view.js +58 -0
  266. package/dist/limits-ui.d.ts +26 -0
  267. package/dist/limits-ui.js +67 -0
  268. package/dist/link.d.ts +72 -0
  269. package/dist/link.js +217 -0
  270. package/dist/live.d.ts +96 -0
  271. package/dist/live.js +366 -0
  272. package/dist/liveness.d.ts +30 -0
  273. package/dist/liveness.js +42 -0
  274. package/dist/log.d.ts +7 -0
  275. package/dist/log.js +25 -0
  276. package/dist/mailbox.d.ts +57 -0
  277. package/dist/mailbox.js +150 -0
  278. package/dist/maintenance.d.ts +11 -0
  279. package/dist/maintenance.js +35 -0
  280. package/dist/mate-cli.d.ts +52 -0
  281. package/dist/mate-cli.js +345 -0
  282. package/dist/mate-contract.d.ts +10 -0
  283. package/dist/mate-contract.js +30 -0
  284. package/dist/mate-doors.d.ts +69 -0
  285. package/dist/mate-doors.js +548 -0
  286. package/dist/mate-progress.d.ts +29 -0
  287. package/dist/mate-progress.js +121 -0
  288. package/dist/mate-tools.d.ts +94 -0
  289. package/dist/mate-tools.js +1878 -0
  290. package/dist/mate.d.ts +92 -0
  291. package/dist/mate.js +435 -0
  292. package/dist/mcp-connect.d.ts +103 -0
  293. package/dist/mcp-connect.js +252 -0
  294. package/dist/mcp.d.ts +27 -0
  295. package/dist/mcp.js +651 -0
  296. package/dist/memory-cli.d.ts +466 -0
  297. package/dist/memory-cli.js +111 -0
  298. package/dist/memory-pass.d.ts +160 -0
  299. package/dist/memory-pass.js +409 -0
  300. package/dist/metrics.d.ts +3 -0
  301. package/dist/metrics.js +56 -0
  302. package/dist/mobile-viewport.d.ts +3 -0
  303. package/dist/mobile-viewport.js +37 -0
  304. package/dist/model-catalog.d.ts +118 -0
  305. package/dist/model-catalog.js +376 -0
  306. package/dist/models-cli.d.ts +102 -0
  307. package/dist/models-cli.js +66 -0
  308. package/dist/models-ui.d.ts +34 -0
  309. package/dist/models-ui.js +68 -0
  310. package/dist/modes.d.ts +80 -0
  311. package/dist/modes.js +182 -0
  312. package/dist/monitoring-settings.d.ts +44 -0
  313. package/dist/monitoring-settings.js +173 -0
  314. package/dist/monitoring-ui.d.ts +20 -0
  315. package/dist/monitoring-ui.js +51 -0
  316. package/dist/monitoring.d.ts +34 -0
  317. package/dist/monitoring.js +221 -0
  318. package/dist/names.d.ts +67 -0
  319. package/dist/names.js +102 -0
  320. package/dist/observations.d.ts +40 -0
  321. package/dist/observations.js +211 -0
  322. package/dist/oidc.d.ts +71 -0
  323. package/dist/oidc.js +182 -0
  324. package/dist/onboard.d.ts +108 -0
  325. package/dist/onboard.js +325 -0
  326. package/dist/openrouter-models.d.ts +30 -0
  327. package/dist/openrouter-models.js +120 -0
  328. package/dist/operate.d.ts +118 -0
  329. package/dist/operate.js +11321 -0
  330. package/dist/peek-cli.d.ts +97 -0
  331. package/dist/peek-cli.js +215 -0
  332. package/dist/peek.d.ts +134 -0
  333. package/dist/peek.js +578 -0
  334. package/dist/phase-routing.d.ts +307 -0
  335. package/dist/phase-routing.js +658 -0
  336. package/dist/plan-auto.d.ts +14 -0
  337. package/dist/plan-auto.js +92 -0
  338. package/dist/plan.d.ts +186 -0
  339. package/dist/plan.js +401 -0
  340. package/dist/planner-source.d.ts +251 -0
  341. package/dist/planner-source.js +460 -0
  342. package/dist/planner.d.ts +75 -0
  343. package/dist/planner.js +992 -0
  344. package/dist/policy-ui.d.ts +20 -0
  345. package/dist/policy-ui.js +41 -0
  346. package/dist/policy.d.ts +111 -0
  347. package/dist/policy.js +231 -0
  348. package/dist/prepared-evidence.d.ts +16 -0
  349. package/dist/prepared-evidence.js +96 -0
  350. package/dist/pricing.d.ts +45 -0
  351. package/dist/pricing.js +77 -0
  352. package/dist/principal.d.ts +42 -0
  353. package/dist/principal.js +82 -0
  354. package/dist/probe.d.ts +46 -0
  355. package/dist/probe.js +79 -0
  356. package/dist/process-custody.d.ts +17 -0
  357. package/dist/process-custody.js +91 -0
  358. package/dist/process-liveness.d.ts +10 -0
  359. package/dist/process-liveness.js +81 -0
  360. package/dist/process-recovery-anchor.d.ts +75 -0
  361. package/dist/process-recovery-anchor.js +188 -0
  362. package/dist/process-recovery-coalition.d.ts +25 -0
  363. package/dist/process-recovery-coalition.js +105 -0
  364. package/dist/process-recovery-eligibility.d.ts +178 -0
  365. package/dist/process-recovery-eligibility.js +170 -0
  366. package/dist/process-recovery-native.d.ts +172 -0
  367. package/dist/process-recovery-native.js +592 -0
  368. package/dist/process-recovery-provenance.d.ts +163 -0
  369. package/dist/process-recovery-provenance.js +292 -0
  370. package/dist/process-recovery-services.d.ts +56 -0
  371. package/dist/process-recovery-services.js +191 -0
  372. package/dist/process-recovery-settlement.d.ts +32 -0
  373. package/dist/process-recovery-settlement.js +132 -0
  374. package/dist/process-recovery.d.ts +41 -0
  375. package/dist/process-recovery.js +91 -0
  376. package/dist/process-tree.d.ts +41 -0
  377. package/dist/process-tree.js +264 -0
  378. package/dist/project-access.d.ts +14 -0
  379. package/dist/project-access.js +34 -0
  380. package/dist/project-cli.d.ts +18 -0
  381. package/dist/project-cli.js +104 -0
  382. package/dist/project-delete-ui.d.ts +22 -0
  383. package/dist/project-delete-ui.js +48 -0
  384. package/dist/project-delete.d.ts +55 -0
  385. package/dist/project-delete.js +413 -0
  386. package/dist/project-knowledge.d.ts +111 -0
  387. package/dist/project-knowledge.js +241 -0
  388. package/dist/project-learning.d.ts +100 -0
  389. package/dist/project-learning.js +438 -0
  390. package/dist/project-memory.d.ts +81 -0
  391. package/dist/project-memory.js +264 -0
  392. package/dist/project-skills.d.ts +152 -0
  393. package/dist/project-skills.js +660 -0
  394. package/dist/project-tools.d.ts +219 -0
  395. package/dist/project-tools.js +796 -0
  396. package/dist/project.d.ts +61 -0
  397. package/dist/project.js +125 -0
  398. package/dist/prompt.d.ts +22 -0
  399. package/dist/prompt.js +72 -0
  400. package/dist/proof.d.ts +513 -0
  401. package/dist/proof.js +1140 -0
  402. package/dist/proposal.d.ts +82 -0
  403. package/dist/proposal.js +210 -0
  404. package/dist/provider-connection.d.ts +22 -0
  405. package/dist/provider-connection.js +98 -0
  406. package/dist/provider-limits.d.ts +35 -0
  407. package/dist/provider-limits.js +95 -0
  408. package/dist/provider.d.ts +279 -0
  409. package/dist/provider.js +935 -0
  410. package/dist/publish.d.ts +107 -0
  411. package/dist/publish.js +650 -0
  412. package/dist/pulls.d.ts +118 -0
  413. package/dist/pulls.js +240 -0
  414. package/dist/push.d.ts +95 -0
  415. package/dist/push.js +353 -0
  416. package/dist/quality.d.ts +8 -0
  417. package/dist/quality.js +6 -0
  418. package/dist/recipe-ui.d.ts +18 -0
  419. package/dist/recipe-ui.js +152 -0
  420. package/dist/recipes.d.ts +81 -0
  421. package/dist/recipes.js +332 -0
  422. package/dist/remote.d.ts +67 -0
  423. package/dist/remote.js +122 -0
  424. package/dist/render.d.ts +64 -0
  425. package/dist/render.js +587 -0
  426. package/dist/report-summary.d.ts +13 -0
  427. package/dist/report-summary.js +15 -0
  428. package/dist/repos.d.ts +65 -0
  429. package/dist/repos.js +277 -0
  430. package/dist/repository-context-ui.d.ts +2 -0
  431. package/dist/repository-context-ui.js +10 -0
  432. package/dist/repository-context.d.ts +67 -0
  433. package/dist/repository-context.js +376 -0
  434. package/dist/restart-certification.d.ts +134 -0
  435. package/dist/restart-certification.js +226 -0
  436. package/dist/result-actions.d.ts +28 -0
  437. package/dist/result-actions.js +270 -0
  438. package/dist/result-completion.d.ts +18 -0
  439. package/dist/result-completion.js +66 -0
  440. package/dist/result-review.d.ts +213 -0
  441. package/dist/result-review.js +430 -0
  442. package/dist/retention-ui.d.ts +12 -0
  443. package/dist/retention-ui.js +39 -0
  444. package/dist/retention.d.ts +72 -0
  445. package/dist/retention.js +288 -0
  446. package/dist/review-context.d.ts +243 -0
  447. package/dist/review-context.js +1043 -0
  448. package/dist/review-evidence.d.ts +33 -0
  449. package/dist/review-evidence.js +56 -0
  450. package/dist/reviewer.d.ts +97 -0
  451. package/dist/reviewer.js +202 -0
  452. package/dist/routine.d.ts +228 -0
  453. package/dist/routine.js +764 -0
  454. package/dist/runner.d.ts +266 -0
  455. package/dist/runner.js +399 -0
  456. package/dist/scan.d.ts +32 -0
  457. package/dist/scan.js +92 -0
  458. package/dist/scope.d.ts +695 -0
  459. package/dist/scope.js +1176 -0
  460. package/dist/scout-report.d.ts +42 -0
  461. package/dist/scout-report.js +108 -0
  462. package/dist/scout.d.ts +69 -0
  463. package/dist/scout.js +351 -0
  464. package/dist/serve.d.ts +313 -0
  465. package/dist/serve.js +21776 -0
  466. package/dist/session-brief.d.ts +8 -0
  467. package/dist/session-brief.js +24 -0
  468. package/dist/session-cli.d.ts +13 -0
  469. package/dist/session-cli.js +284 -0
  470. package/dist/session-contract.d.ts +183 -0
  471. package/dist/session-contract.js +122 -0
  472. package/dist/session-http.d.ts +19 -0
  473. package/dist/session-http.js +154 -0
  474. package/dist/session-server.d.ts +12 -0
  475. package/dist/session-server.js +37 -0
  476. package/dist/session-service.d.ts +22 -0
  477. package/dist/session-service.js +157 -0
  478. package/dist/setup-guide.d.ts +39 -0
  479. package/dist/setup-guide.js +93 -0
  480. package/dist/sign-in-guard.d.ts +45 -0
  481. package/dist/sign-in-guard.js +74 -0
  482. package/dist/skills-ui.d.ts +11 -0
  483. package/dist/skills-ui.js +58 -0
  484. package/dist/skills.d.ts +97 -0
  485. package/dist/skills.js +276 -0
  486. package/dist/slack-api.d.ts +55 -0
  487. package/dist/slack-api.js +229 -0
  488. package/dist/slack-chat.d.ts +28 -0
  489. package/dist/slack-chat.js +413 -0
  490. package/dist/slack-settings.d.ts +7 -0
  491. package/dist/slack-settings.js +75 -0
  492. package/dist/slack-state.d.ts +13 -0
  493. package/dist/slack-state.js +9 -0
  494. package/dist/slack.d.ts +13 -0
  495. package/dist/slack.js +167 -0
  496. package/dist/spend-ui.d.ts +28 -0
  497. package/dist/spend-ui.js +95 -0
  498. package/dist/spend.d.ts +142 -0
  499. package/dist/spend.js +313 -0
  500. package/dist/sqlite-runtime.d.ts +3 -0
  501. package/dist/sqlite-runtime.js +6 -0
  502. package/dist/sso-settings.d.ts +33 -0
  503. package/dist/sso-settings.js +103 -0
  504. package/dist/sso-ui.d.ts +22 -0
  505. package/dist/sso-ui.js +39 -0
  506. package/dist/storage.d.ts +15 -0
  507. package/dist/storage.js +84 -0
  508. package/dist/store.d.ts +7754 -0
  509. package/dist/store.js +23279 -0
  510. package/dist/structured-output.d.ts +58 -0
  511. package/dist/structured-output.js +103 -0
  512. package/dist/style-asset.d.ts +7 -0
  513. package/dist/style-asset.js +41 -0
  514. package/dist/subscription-chat.d.ts +46 -0
  515. package/dist/subscription-chat.js +275 -0
  516. package/dist/summary.d.ts +33 -0
  517. package/dist/summary.js +71 -0
  518. package/dist/supervisor.mjs +297 -0
  519. package/dist/surface.d.ts +63 -0
  520. package/dist/surface.js +352 -0
  521. package/dist/sync.d.ts +94 -0
  522. package/dist/sync.js +279 -0
  523. package/dist/task-composer.d.ts +13 -0
  524. package/dist/task-composer.js +52 -0
  525. package/dist/task-control.d.ts +165 -0
  526. package/dist/task-control.js +214 -0
  527. package/dist/task-outcome-cli.d.ts +10 -0
  528. package/dist/task-outcome-cli.js +92 -0
  529. package/dist/task-text.d.ts +30 -0
  530. package/dist/task-text.js +41 -0
  531. package/dist/team-cli.d.ts +15 -0
  532. package/dist/team-cli.js +618 -0
  533. package/dist/team-contract.d.ts +92 -0
  534. package/dist/team-contract.js +1 -0
  535. package/dist/team-http.d.ts +17 -0
  536. package/dist/team-http.js +144 -0
  537. package/dist/team-leads.d.ts +81 -0
  538. package/dist/team-leads.js +565 -0
  539. package/dist/team-runtime.d.ts +27 -0
  540. package/dist/team-runtime.js +264 -0
  541. package/dist/team-ui.d.ts +3 -0
  542. package/dist/team-ui.js +9 -0
  543. package/dist/team-updates.d.ts +8 -0
  544. package/dist/team-updates.js +57 -0
  545. package/dist/teammate-admin.d.ts +53 -0
  546. package/dist/teammate-admin.js +111 -0
  547. package/dist/teammate-desk.d.ts +60 -0
  548. package/dist/teammate-desk.js +154 -0
  549. package/dist/teammate-memory.d.ts +64 -0
  550. package/dist/teammate-memory.js +196 -0
  551. package/dist/teammate-question.d.ts +59 -0
  552. package/dist/teammate-question.js +175 -0
  553. package/dist/teammate-tools.d.ts +108 -0
  554. package/dist/teammate-tools.js +271 -0
  555. package/dist/teammate-week.d.ts +68 -0
  556. package/dist/teammate-week.js +132 -0
  557. package/dist/teammate-work.d.ts +76 -0
  558. package/dist/teammate-work.js +280 -0
  559. package/dist/teammates-ui.d.ts +20 -0
  560. package/dist/teammates-ui.js +166 -0
  561. package/dist/teammates.d.ts +180 -0
  562. package/dist/teammates.js +253 -0
  563. package/dist/teams-api.d.ts +33 -0
  564. package/dist/teams-api.js +204 -0
  565. package/dist/teams-chat.d.ts +26 -0
  566. package/dist/teams-chat.js +215 -0
  567. package/dist/teams-settings.d.ts +12 -0
  568. package/dist/teams-settings.js +57 -0
  569. package/dist/teams.d.ts +21 -0
  570. package/dist/teams.js +124 -0
  571. package/dist/telegram-flow.d.ts +49 -0
  572. package/dist/telegram-flow.js +110 -0
  573. package/dist/telegram-mate.d.ts +137 -0
  574. package/dist/telegram-mate.js +707 -0
  575. package/dist/telegram-progress.d.ts +14 -0
  576. package/dist/telegram-progress.js +129 -0
  577. package/dist/telegram-settings.d.ts +9 -0
  578. package/dist/telegram-settings.js +36 -0
  579. package/dist/telegram-status.d.ts +75 -0
  580. package/dist/telegram-status.js +265 -0
  581. package/dist/telegram-team.d.ts +85 -0
  582. package/dist/telegram-team.js +359 -0
  583. package/dist/telegram.d.ts +230 -0
  584. package/dist/telegram.js +1931 -0
  585. package/dist/templates.d.ts +65 -0
  586. package/dist/templates.js +118 -0
  587. package/dist/tool-launcher.d.ts +1 -0
  588. package/dist/tool-launcher.js +101 -0
  589. package/dist/tools-ui.d.ts +30 -0
  590. package/dist/tools-ui.js +40 -0
  591. package/dist/transitions-recipes.d.ts +1 -0
  592. package/dist/transitions-recipes.js +203 -0
  593. package/dist/tree-proof.d.ts +34 -0
  594. package/dist/tree-proof.js +58 -0
  595. package/dist/verification-evidence.d.ts +44 -0
  596. package/dist/verification-evidence.js +238 -0
  597. package/dist/version.d.ts +2 -0
  598. package/dist/version.js +10 -0
  599. package/dist/webhooks.d.ts +91 -0
  600. package/dist/webhooks.js +283 -0
  601. package/dist/work-index.d.ts +68 -0
  602. package/dist/work-index.js +384 -0
  603. package/dist/work-summary.d.ts +57 -0
  604. package/dist/work-summary.js +116 -0
  605. package/dist/workspace-motion.d.ts +4 -0
  606. package/dist/workspace-motion.js +133 -0
  607. package/dist/workspace-revision.d.ts +19 -0
  608. package/dist/workspace-revision.js +92 -0
  609. package/dist/workspace-ui.d.ts +204 -0
  610. package/dist/workspace-ui.js +411 -0
  611. package/dist/worktree-notices.d.ts +13 -0
  612. package/dist/worktree-notices.js +17 -0
  613. package/dist/worktree.d.ts +249 -0
  614. package/dist/worktree.js +720 -0
  615. package/package.json +122 -0
  616. package/scripts/canary-assertions.mjs +42 -0
  617. package/scripts/crash-canary.mjs +253 -0
  618. package/scripts/fixtures/crash-process.mjs +74 -0
  619. package/scripts/fixtures/pilot-scenarios.mjs +85 -0
  620. package/scripts/fixtures/restart-service.mjs +27 -0
  621. package/scripts/launchd-certification.mjs +143 -0
  622. package/scripts/pilot.mjs +121 -0
  623. package/scripts/proof-preflight.mjs +140 -0
  624. package/scripts/provider-canary.mjs +379 -0
  625. package/scripts/recovery-canary.mjs +135 -0
  626. package/scripts/restart-certification.mjs +98 -0
package/dist/scope.js ADDED
@@ -0,0 +1,1176 @@
1
+ import { passwordGuardOf } from "./sign-in-guard.js";
2
+ import { validateScopeText } from "./task-text.js";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ import { projectAuthority } from "./project-access.js";
5
+ /**
6
+ * What a task is allowed to become, agreed before anything builds it.
7
+ *
8
+ * The gap this closes is the one an operator notices immediately and a
9
+ * scheduler never does: a task called "fix the payouts flow" is a sentence, and
10
+ * an agent handed it at 3am will decide for itself what that means. It might
11
+ * mean a two-line guard. It might mean rewriting the billing model. Nobody
12
+ * agreed to the second one, and by morning it is a diff.
13
+ *
14
+ * So a scope is written down, and a human opts in to it, and the builder will
15
+ * not run on a task where that has not happened. Not a code review after the
16
+ * fact — an agreement about the goal, before the work.
17
+ *
18
+ * **Approval binds to the words that were approved.** The record keeps a digest
19
+ * of the scope text, and approval stores the digest it saw. Rewriting the scope
20
+ * afterwards does not carry the approval with it; the digests stop matching and
21
+ * the task is unapproved again. Without that, "approved" would mean "was
22
+ * approved once, in some form" — which is exactly the loophole an agent editing
23
+ * its own brief would walk through.
24
+ *
25
+ * The scope is deliberately three plain fields rather than a template. What is
26
+ * the goal, what is explicitly not in it, and which paths it expects to touch.
27
+ * An operator reading that at a glance can tell whether they agree, and that
28
+ * glance is the whole safety property.
29
+ */
30
+ import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
31
+ import { hasForbiddenControls } from "./decision.js";
32
+ import { NO_READINESS, exactModelId, isRiskLevel, legOf, projectRoute, routeDigestOf, routeFromJson, routeWords, } from "./phase-routing.js";
33
+ /** Same shape as a runner's credential, for the same reasons. */
34
+ function mintToken() {
35
+ return randomBytes(32).toString("base64url");
36
+ }
37
+ export function hashToken(token) {
38
+ return createHash("sha256").update(token, "utf8").digest("hex");
39
+ }
40
+ /**
41
+ * A person's chosen password gets a real KDF. Bare sha256 is fine for the
42
+ * minted 256-bit tokens — nothing brute-forces that space — but a password a
43
+ * human picked lives in a much smaller one, so it is salted and stretched
44
+ * (scrypt), and the stored string names its own scheme so both generations
45
+ * of credential verify side by side.
46
+ */
47
+ export function hashPassword(password) {
48
+ const salt = randomBytes(16).toString("hex");
49
+ const hash = scryptSync(password, salt, 32).toString("hex");
50
+ return `scrypt$${salt}$${hash}`;
51
+ }
52
+ /** Verify a presented secret against a stored hash of either scheme. */
53
+ export function verifyCredential(stored, presented) {
54
+ // v100: an account that signs in only with the identity provider has no password to match.
55
+ if (stored.startsWith("sso-only$"))
56
+ return false;
57
+ if (stored.startsWith("scrypt$")) {
58
+ const [, salt, hex] = stored.split("$");
59
+ if (salt === undefined || hex === undefined)
60
+ return false;
61
+ const known = Buffer.from(hex, "hex");
62
+ const candidate = scryptSync(presented, salt, 32);
63
+ return known.length === candidate.length && timingSafeEqual(known, candidate);
64
+ }
65
+ return sameDigest(stored, hashToken(presented));
66
+ }
67
+ function sameDigest(left, right) {
68
+ const a = Buffer.from(left, "utf8");
69
+ const b = Buffer.from(right, "utf8");
70
+ return a.length === b.length && timingSafeEqual(a, b);
71
+ }
72
+ /**
73
+ * The execution profile (Parity II foundations, findings 1/2/13/14/22):
74
+ * WHAT RUNS, bound into what the operator signs. A discriminated union —
75
+ * each variant asserts only what its provider actually supports, with the
76
+ * EFFECTIVE constants the argv will carry, never aspirations. The model
77
+ * is always an exact string and always emitted on the argv: nothing in an
78
+ * approved profile is left for later resolution to decide (ruling 10).
79
+ * providerVersion and resolvedFrom are PROVENANCE and live outside these
80
+ * shapes (finding 20) — they never enter a digest.
81
+ */
82
+ export const CLAUDE_LIMITS = {
83
+ // The activity watchdog is the real safety rail. Keep a very high turn
84
+ // breaker only for a pathological loop; ordinary long-horizon work should
85
+ // finish, checkpoint, or park long before it can reach this number.
86
+ maxTurns: 1_000,
87
+ repairMaxTurns: 4,
88
+ timeoutSeconds: 1_200,
89
+ repairTimeoutSeconds: 300,
90
+ };
91
+ export const CODEX_SHAPED_LIMITS = {
92
+ timeoutSeconds: 1200,
93
+ repairTimeoutSeconds: 300,
94
+ };
95
+ export const GEMINI_LIMITS = {
96
+ timeoutSeconds: 1200,
97
+ repairTimeoutSeconds: 300,
98
+ };
99
+ export const PROFILE_DIGEST_VERSION = 2;
100
+ /** Deterministic JSON: object keys sorted recursively, arrays in order. */
101
+ function canonicalJson(value) {
102
+ if (Array.isArray(value))
103
+ return `[${value.map(canonicalJson).join(",")}]`;
104
+ if (value !== null && typeof value === "object") {
105
+ const entries = Object.entries(value)
106
+ .filter(([, v]) => v !== undefined)
107
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
108
+ .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
109
+ return `{${entries.join(",")}}`;
110
+ }
111
+ return JSON.stringify(value);
112
+ }
113
+ /** The stored snapshot bytes: version embedded IN the snapshot (finding 17). */
114
+ export function canonicalProfileJson(profile) {
115
+ return canonicalJson({ digestVersion: PROFILE_DIGEST_VERSION, profile });
116
+ }
117
+ /** sha256 over a domain-separated canonical encoding, truncated to the
118
+ * same 128 bits every other safety digest here uses (finding 21). */
119
+ export function profileDigestOf(profile) {
120
+ return createHash("sha256")
121
+ .update(`standing-orders:profile:${canonicalProfileJson(profile)}`, "utf8")
122
+ .digest("hex")
123
+ .slice(0, 32);
124
+ }
125
+ // ---- fallback chains (v30) ------------------------------------------------
126
+ // The version this build stamps into a chain snapshot AND folds into the
127
+ // chain digest. Bumping it is a re-attestation, never a silent change.
128
+ export const CHAIN_DIGEST_VERSION = 1;
129
+ /** The stored snapshot bytes for a chain: the version rides IN the
130
+ * snapshot, exactly like a single profile's. */
131
+ export function canonicalChainJson(entries) {
132
+ return canonicalJson({ digestVersion: CHAIN_DIGEST_VERSION, chain: entries });
133
+ }
134
+ /** sha256 over a domain-separated canonical chain encoding — a DIFFERENT
135
+ * domain from a single profile ("standing-orders:chain:" vs
136
+ * ":profile:"), so a chain digest can never collide with a profile
137
+ * digest, truncated to the same 128 bits. */
138
+ export function chainDigestOf(entries) {
139
+ return createHash("sha256")
140
+ .update(`standing-orders:chain:v${CHAIN_DIGEST_VERSION}:${canonicalChainJson(entries)}`, "utf8")
141
+ .digest("hex")
142
+ .slice(0, 32);
143
+ }
144
+ /**
145
+ * One chain entry's BINDING digest (E3d): the profile digest + auth mode
146
+ * under their own domain. A run row pins this at admission (or base-cycle
147
+ * open), and the dispatch proof re-derives it from the approved chain at
148
+ * the run's index — so nothing downstream can swap WHICH entry a run
149
+ * spends as, in model or in credential.
150
+ */
151
+ export function entryDigestOf(entry) {
152
+ return createHash("sha256")
153
+ .update(`standing-orders:chain-entry:v${CHAIN_DIGEST_VERSION}:${profileDigestOf(entry.profile)}:${entry.authMode}`, "utf8")
154
+ .digest("hex")
155
+ .slice(0, 32);
156
+ }
157
+ /** Strict re-hydration of a stored chain — every entry proved through the
158
+ * single-profile rehydrator; anything unexpected is null. */
159
+ export function chainFromJson(json) {
160
+ if (json === null)
161
+ return null;
162
+ let parsed;
163
+ try {
164
+ parsed = JSON.parse(json);
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ if (parsed === null || typeof parsed !== "object")
170
+ return null;
171
+ const wrapper = parsed;
172
+ if (wrapper.digestVersion !== CHAIN_DIGEST_VERSION)
173
+ return null;
174
+ // Exact keys on the wrapper and on every entry (v48 integrity): a
175
+ // snapshot carrying anything this code never writes is not one it wrote.
176
+ if (!exactKeys(wrapper, ["digestVersion", "chain"], []))
177
+ return null;
178
+ if (!Array.isArray(wrapper.chain) || wrapper.chain.length === 0 || wrapper.chain.length > 4)
179
+ return null;
180
+ const entries = [];
181
+ const seen = new Set();
182
+ for (const raw of wrapper.chain) {
183
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
184
+ return null;
185
+ const e = raw;
186
+ if (!exactKeys(e, ["profile", "authMode"], []))
187
+ return null;
188
+ // A malformed auth mode is a malformed entry — never a default.
189
+ if (e.authMode !== "subscription" && e.authMode !== "api-key")
190
+ return null;
191
+ if (e.profile === null || typeof e.profile !== "object" || Array.isArray(e.profile))
192
+ return null;
193
+ // The profile rehydrates through the SAME strict path, wrapped as its
194
+ // own canonical snapshot so profileFromJson can prove it — and the
195
+ // canonical bytes must be the stored bytes' own keys (canonicalJson
196
+ // drops nothing, so an extra key survives into the strict parse).
197
+ const profile = profileFromJson(canonicalProfileJson(e.profile));
198
+ if (profile === null)
199
+ return null;
200
+ // Duplicate exact entries are rejected (open q i): the key is the
201
+ // profile digest + auth mode.
202
+ const key = `${profileDigestOf(profile)}:${e.authMode}`;
203
+ if (seen.has(key))
204
+ return null;
205
+ seen.add(key);
206
+ entries.push({ profile, authMode: e.authMode });
207
+ }
208
+ return entries;
209
+ }
210
+ /**
211
+ * THE STRICT RAW TERMS READ (raw authority repair): the stored columns of
212
+ * a scope or a standing order, proved EXACTLY — a JSON list is a list of
213
+ * strings and nothing else, a rubric is a list of whole criteria carrying
214
+ * exactly the keys this code writes and parsing with zero problems, a
215
+ * budget is a safe integer or null. Nothing is filtered, defaulted, or
216
+ * coerced on the way to an authority: the words say what is wrong, and
217
+ * the caller refuses.
218
+ */
219
+ export function exactStringList(value, what) {
220
+ if (typeof value !== "string")
221
+ return { ok: false, problem: `${what} is not stored as text` };
222
+ let parsed;
223
+ try {
224
+ parsed = JSON.parse(value);
225
+ }
226
+ catch {
227
+ return { ok: false, problem: `${what} is not valid JSON` };
228
+ }
229
+ if (!Array.isArray(parsed))
230
+ return { ok: false, problem: `${what} is not a JSON list` };
231
+ if (!parsed.every(one => typeof one === "string"))
232
+ return { ok: false, problem: `${what} carries an entry that is not a string` };
233
+ return { ok: true, list: parsed };
234
+ }
235
+ export function exactAcceptance(value) {
236
+ if (value === null || value === undefined)
237
+ return { ok: true, criteria: [] };
238
+ if (typeof value !== "string")
239
+ return { ok: false, problem: "the rubric is not stored as text" };
240
+ let parsed;
241
+ try {
242
+ parsed = JSON.parse(value);
243
+ }
244
+ catch {
245
+ return { ok: false, problem: "the rubric is not valid JSON" };
246
+ }
247
+ if (!Array.isArray(parsed))
248
+ return { ok: false, problem: "the rubric is not a JSON list" };
249
+ for (const [index, entry] of parsed.entries()) {
250
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
251
+ return { ok: false, problem: `rubric entry ${index + 1} is not an object` };
252
+ if (!exactKeys(entry, ["id", "statement", "evidence"], ["how"]))
253
+ return { ok: false, problem: `rubric entry ${index + 1} carries a key this code never writes` };
254
+ }
255
+ const read = parseAcceptanceCriteria(parsed);
256
+ if (read.problems.length > 0)
257
+ return { ok: false, problem: `the rubric does not parse: ${read.problems.map(one => one.message).join("; ")}` };
258
+ return { ok: true, criteria: read.criteria };
259
+ }
260
+ export function exactSafeIntegerOrNull(value, what) {
261
+ if (value === null || value === undefined)
262
+ return null;
263
+ if (typeof value !== "number" || !Number.isSafeInteger(value))
264
+ return `${what} is not a safe integer`;
265
+ return null;
266
+ }
267
+ /**
268
+ * The longest clock a snapshot may bind, in seconds: a JavaScript timer
269
+ * holds at most 2^31 − 1 milliseconds, and a bound past that fires at
270
+ * once instead of never — a "timeout" that is not one. Every stored
271
+ * clock is proved against this before it is believed (v48 integrity).
272
+ */
273
+ export const MAX_TIMER_SECONDS = Math.floor(2_147_483_647 / 1000);
274
+ /** A positive integer the platform can represent exactly. */
275
+ export function safePositiveInteger(v) {
276
+ return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
277
+ }
278
+ /** A positive whole number of seconds a timer can actually wait. */
279
+ export function timerSafeSeconds(v) {
280
+ return safePositiveInteger(v) && v <= MAX_TIMER_SECONDS;
281
+ }
282
+ /**
283
+ * Whether an object carries exactly the named keys: every required key
284
+ * present, only optional keys otherwise, nothing unknown. Stored snapshots
285
+ * are proved this way so a key this code never writes is a refusal, never
286
+ * an ignorable extra.
287
+ */
288
+ export function exactKeys(value, required, optional) {
289
+ const keys = Object.keys(value);
290
+ if (required.some(key => !keys.includes(key)))
291
+ return false;
292
+ return keys.every(key => required.includes(key) || optional.includes(key));
293
+ }
294
+ /** Strict re-hydration of a stored snapshot — every field type-proved;
295
+ * anything unexpected is null, never a guess. */
296
+ export function profileFromJson(json) {
297
+ if (json === null)
298
+ return null;
299
+ let parsed;
300
+ try {
301
+ parsed = JSON.parse(json);
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ if (parsed === null || typeof parsed !== "object")
307
+ return null;
308
+ const wrapper = parsed;
309
+ if (wrapper.digestVersion !== PROFILE_DIGEST_VERSION)
310
+ return null;
311
+ // EXACT KEYS (v48 integrity): the wrapper carries the version and the
312
+ // profile and nothing else, and the profile carries exactly its
313
+ // provider's fields — a snapshot with a key this code never writes was
314
+ // not written by this code and rehydrates as nothing, never as "the
315
+ // known part of it".
316
+ if (!exactKeys(wrapper, ["digestVersion", "profile"], []))
317
+ return null;
318
+ const p = wrapper.profile;
319
+ if (p === null || p === undefined || typeof p !== "object" || Array.isArray(p))
320
+ return null;
321
+ // STRICT fields (v48 authority repair): a model is an exact id (the same shape every
322
+ // provider argv accepts), a repair model is `inherit` or an exact id, a
323
+ // turn bound is a positive SAFE integer, and a clock is a positive whole
324
+ // number of seconds a timer can actually hold — a snapshot carrying
325
+ // anything else (a negative turn count, a fractional second, a model
326
+ // that is not an id, a clock past what setTimeout can wait) was not
327
+ // written by this code and rehydrates as nothing.
328
+ const str = (v) => exactModelId(v);
329
+ const repairRef = (v) => v === "inherit" || exactModelId(v);
330
+ const num = (v) => safePositiveInteger(v);
331
+ const clock = (v) => timerSafeSeconds(v);
332
+ const PROVIDER_KEYS = ["provider", "model", "maxTurns", "repairMaxTurns", "timeoutSeconds", "repairTimeoutSeconds", "repairModel"];
333
+ if (p["provider"] === "claude") {
334
+ if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind"]) &&
335
+ str(p["model"]) &&
336
+ (p["permissionArgv"] === "auto" || p["permissionArgv"] === "acceptEdits" || p["permissionArgv"] === "bypassPermissions") &&
337
+ num(p["maxTurns"]) && num(p["repairMaxTurns"]) &&
338
+ clock(p["timeoutSeconds"]) && (p["timeoutKind"] === undefined || p["timeoutKind"] === "idle") && clock(p["repairTimeoutSeconds"]) &&
339
+ repairRef(p["repairModel"])) {
340
+ return {
341
+ provider: "claude",
342
+ model: p["model"],
343
+ permissionArgv: p["permissionArgv"],
344
+ maxTurns: p["maxTurns"],
345
+ repairMaxTurns: p["repairMaxTurns"],
346
+ timeoutSeconds: p["timeoutSeconds"],
347
+ ...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
348
+ repairTimeoutSeconds: p["repairTimeoutSeconds"],
349
+ repairModel: p["repairModel"],
350
+ };
351
+ }
352
+ return null;
353
+ }
354
+ if (p["provider"] === "codex" || p["provider"] === "openrouter") {
355
+ if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind"]) &&
356
+ str(p["model"]) &&
357
+ (p["sandboxMode"] === "workspace-write" || p["sandboxMode"] === "danger-full-access") &&
358
+ p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
359
+ clock(p["timeoutSeconds"]) && (p["timeoutKind"] === undefined || p["timeoutKind"] === "idle") && clock(p["repairTimeoutSeconds"]) &&
360
+ repairRef(p["repairModel"])) {
361
+ return {
362
+ provider: p["provider"],
363
+ model: p["model"],
364
+ sandboxMode: p["sandboxMode"],
365
+ maxTurns: "unsupported",
366
+ repairMaxTurns: "unsupported",
367
+ timeoutSeconds: p["timeoutSeconds"],
368
+ ...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
369
+ repairTimeoutSeconds: p["repairTimeoutSeconds"],
370
+ repairModel: p["repairModel"],
371
+ };
372
+ }
373
+ return null;
374
+ }
375
+ if (p["provider"] === "gemini") {
376
+ if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind"]) &&
377
+ str(p["model"]) &&
378
+ (p["approvalArgv"] === "auto_edit" || p["approvalArgv"] === "yolo") &&
379
+ p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
380
+ clock(p["timeoutSeconds"]) && (p["timeoutKind"] === undefined || p["timeoutKind"] === "idle") && clock(p["repairTimeoutSeconds"]) &&
381
+ repairRef(p["repairModel"])) {
382
+ return {
383
+ provider: "gemini",
384
+ model: p["model"],
385
+ approvalArgv: p["approvalArgv"],
386
+ maxTurns: "unsupported",
387
+ repairMaxTurns: "unsupported",
388
+ timeoutSeconds: p["timeoutSeconds"],
389
+ ...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
390
+ repairTimeoutSeconds: p["repairTimeoutSeconds"],
391
+ repairModel: p["repairModel"],
392
+ };
393
+ }
394
+ return null;
395
+ }
396
+ return null;
397
+ }
398
+ export const EVIDENCE_KINDS = ["check", "screenshot", "changed-path", "manual-review"];
399
+ export const ACCEPTANCE_LIMITS = {
400
+ criteria: 12,
401
+ id: 40,
402
+ // A statement is one testable outcome; three hundred bytes forced people to
403
+ // drop the qualifying clause that made it testable.
404
+ statement: 1000,
405
+ how: 500,
406
+ evidenceKinds: EVIDENCE_KINDS.length,
407
+ };
408
+ function acceptanceDescribe(value) {
409
+ if (value === undefined)
410
+ return "nothing";
411
+ if (value === null)
412
+ return "null";
413
+ if (typeof value === "string")
414
+ return `a ${value.length}-char string`;
415
+ return `a ${Array.isArray(value) ? "array" : typeof value}`;
416
+ }
417
+ function acceptanceProse(value, field, cap, required, problems) {
418
+ if (value === undefined || value === null || value === "") {
419
+ if (required)
420
+ problems.push({ reason: `missing-${field}`, message: `${field} is required` });
421
+ return null;
422
+ }
423
+ if (typeof value !== "string") {
424
+ problems.push({ reason: `bad-${field}`, message: `${field} must be a string (got ${acceptanceDescribe(value)})` });
425
+ return null;
426
+ }
427
+ if (Buffer.byteLength(value, "utf8") > cap) {
428
+ problems.push({ reason: `${field}-too-long`, message: `${field} is over ${cap} bytes` });
429
+ return null;
430
+ }
431
+ if (hasForbiddenControls(value)) {
432
+ problems.push({ reason: `${field}-controls`, message: `${field} carries control characters that could become terminal escapes` });
433
+ return null;
434
+ }
435
+ return value;
436
+ }
437
+ /**
438
+ * Parse a rubric from already-JSON-parsed input (the planner's handoff, a
439
+ * console form, a routine or template definition): fail closed, every
440
+ * problem reported at once, stable reasons — the same discipline as every
441
+ * other quoted-data parser in this codebase (`plan.ts`, `proof.ts`). An
442
+ * absent or empty `value` parses to `[]` with no problems: whether that is
443
+ * ALLOWED is a question for the caller (`propose` says yes; every authoring
444
+ * road says no), never for this parser.
445
+ */
446
+ export function parseAcceptanceCriteria(value) {
447
+ const problems = [];
448
+ if (value === undefined || value === null)
449
+ return { criteria: [], problems };
450
+ if (!Array.isArray(value)) {
451
+ problems.push({ reason: "bad-acceptance", message: `acceptance must be an array (got ${acceptanceDescribe(value)})` });
452
+ return { criteria: [], problems };
453
+ }
454
+ if (value.length > ACCEPTANCE_LIMITS.criteria) {
455
+ problems.push({ reason: "acceptance-too-many", message: `acceptance lists ${value.length} — cap is ${ACCEPTANCE_LIMITS.criteria}` });
456
+ return { criteria: [], problems };
457
+ }
458
+ const criteria = [];
459
+ const seen = new Set();
460
+ for (const [index, entry] of value.entries()) {
461
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
462
+ problems.push({ reason: `acceptance[${index}]-shape`, message: `acceptance[${index}] must be an object` });
463
+ continue;
464
+ }
465
+ const one = entry;
466
+ const id = acceptanceProse(one["id"], `acceptance[${index}].id`, ACCEPTANCE_LIMITS.id, true, problems);
467
+ if (id !== null && seen.has(id)) {
468
+ problems.push({ reason: `acceptance[${index}]-duplicate-id`, message: `criterion id "${id}" appears twice` });
469
+ }
470
+ else if (id !== null) {
471
+ seen.add(id);
472
+ }
473
+ const statement = acceptanceProse(one["statement"], `acceptance[${index}].statement`, ACCEPTANCE_LIMITS.statement, true, problems);
474
+ const how = acceptanceProse(one["how"], `acceptance[${index}].how`, ACCEPTANCE_LIMITS.how, false, problems);
475
+ const rawEvidence = one["evidence"];
476
+ let evidence = null;
477
+ if (!Array.isArray(rawEvidence)) {
478
+ problems.push({
479
+ reason: `acceptance[${index}]-bad-evidence`,
480
+ message: `acceptance[${index}].evidence must be a non-empty array of evidence kinds (got ${acceptanceDescribe(rawEvidence)})`,
481
+ });
482
+ }
483
+ else if (rawEvidence.length === 0 || rawEvidence.length > ACCEPTANCE_LIMITS.evidenceKinds) {
484
+ problems.push({
485
+ reason: `acceptance[${index}]-evidence-count`,
486
+ message: `acceptance[${index}].evidence must name 1-${ACCEPTANCE_LIMITS.evidenceKinds} evidence kinds`,
487
+ });
488
+ }
489
+ else {
490
+ const kinds = [];
491
+ const seenKinds = new Set();
492
+ let bad = false;
493
+ for (const k of rawEvidence) {
494
+ if (typeof k !== "string" || !EVIDENCE_KINDS.includes(k)) {
495
+ problems.push({
496
+ reason: `acceptance[${index}]-bad-evidence-kind`,
497
+ message: `acceptance[${index}].evidence must draw from ${EVIDENCE_KINDS.join(", ")} (got ${acceptanceDescribe(k)})`,
498
+ });
499
+ bad = true;
500
+ continue;
501
+ }
502
+ if (seenKinds.has(k)) {
503
+ problems.push({ reason: `acceptance[${index}]-duplicate-evidence-kind`, message: `evidence kind "${k}" appears twice` });
504
+ bad = true;
505
+ continue;
506
+ }
507
+ seenKinds.add(k);
508
+ kinds.push(k);
509
+ }
510
+ if (!bad)
511
+ evidence = kinds;
512
+ }
513
+ if (id === null || statement === null || evidence === null)
514
+ continue;
515
+ criteria.push({ id, statement, how, evidence });
516
+ }
517
+ return { criteria, problems };
518
+ }
519
+ /** The exact bytes a rubric's SIGNED terms reduce to for the digest: sorted
520
+ * by id, `how` dropped (advisory, never signed), evidence kinds sorted so
521
+ * two equivalent lists never digest differently. */
522
+ /**
523
+ * The plain-text rubric encoding shared by every non-JSON authoring
524
+ * surface — the console's rubric textarea (one line each) and the CLI's
525
+ * `--acceptance` flag (one line per `;`-separated entry): one criterion
526
+ * per line, shaped
527
+ *
528
+ * [id:] statement | evidence,kinds [| how]
529
+ *
530
+ * `id` is optional — auto-numbered `c1`, `c2`, ... in encounter order when
531
+ * every line omits it, so a person can write the rubric without inventing
532
+ * ids by hand. This function never validates; it only turns text into the
533
+ * same plain-object shape `parseAcceptanceCriteria` already validates, so
534
+ * both entry points can never drift on what counts as a valid criterion.
535
+ */
536
+ /** The one criterion a filing may carry when nobody has written the rubric
537
+ * yet: the console's scout follow-ups file with it, and `--acceptance plan`
538
+ * on the command line expands to it. A mode with planAuto sends such a
539
+ * filing to the planner instead of building against it. */
540
+ export const PLACEHOLDER_RUBRIC_STATEMENT = "The operator has reviewed this follow-up and written a real rubric before approving it.";
541
+ export const PLACEHOLDER_RUBRIC = [{ id: "c1", statement: PLACEHOLDER_RUBRIC_STATEMENT, how: null, evidence: ["manual-review"] }];
542
+ export function rubricIsPlaceholder(acceptance) {
543
+ return acceptance.length === 1 && acceptance[0].statement === PLACEHOLDER_RUBRIC_STATEMENT &&
544
+ acceptance[0].evidence.length === 1 && acceptance[0].evidence[0] === "manual-review";
545
+ }
546
+ /** Split a `--acceptance` rubric into criteria. Criteria are `;`-separated,
547
+ * but a `;` inside a statement must not split it: every criterion ends with
548
+ * its `|<evidence kinds>` part, so a segment with no `|` is the front of the
549
+ * next one and is glued back on. The single word `plan` means the placeholder. */
550
+ export function splitAcceptanceRubric(text) {
551
+ if (text.trim().toLowerCase() === "plan")
552
+ return [`${PLACEHOLDER_RUBRIC_STATEMENT}|manual-review`];
553
+ const out = [];
554
+ let pending = "";
555
+ for (const segment of text.split(";")) {
556
+ const joined = pending === "" ? segment : `${pending};${segment}`;
557
+ if (joined.includes("|")) {
558
+ out.push(joined);
559
+ pending = "";
560
+ }
561
+ else
562
+ pending = joined;
563
+ }
564
+ if (pending.trim() !== "")
565
+ out.push(pending);
566
+ return out;
567
+ }
568
+ export const isCommitSha = (value) => /^[0-9a-f]{40}$/.test(value);
569
+ export function acceptanceLinesToInput(lines) {
570
+ const out = [];
571
+ let auto = 1;
572
+ for (const raw of lines) {
573
+ const line = raw.trim();
574
+ if (line === "")
575
+ continue;
576
+ const idMatch = /^([A-Za-z0-9_-]{1,40}):\s*(.*)$/.exec(line);
577
+ const id = idMatch?.[1];
578
+ const rest = idMatch ? (idMatch[2] ?? "") : line;
579
+ const parts = rest.split("|").map(p => p.trim());
580
+ const statement = parts[0] ?? "";
581
+ const evidence = (parts[1] ?? "").split(",").map(k => k.trim()).filter(k => k !== "");
582
+ const how = parts[2] !== undefined && parts[2] !== "" ? parts[2] : null;
583
+ out.push({ id: id ?? `c${auto++}`, statement, evidence, how });
584
+ }
585
+ return out;
586
+ }
587
+ /** The inverse of `acceptanceLinesToInput`, for pre-filling an editor from
588
+ * a stored rubric — round-trips through `parseAcceptanceCriteria` exactly. */
589
+ export function acceptanceToLines(criteria) {
590
+ return criteria.map(c => `${c.id}: ${c.statement} | ${c.evidence.join(",")}${c.how === null ? "" : ` | ${c.how}`}`);
591
+ }
592
+ export function canonicalAcceptance(criteria) {
593
+ return [...criteria]
594
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
595
+ .map(c => ({ id: c.id, statement: c.statement.trim(), evidence: [...c.evidence].sort() }));
596
+ }
597
+ /**
598
+ * The exact bytes an operator agreed to.
599
+ *
600
+ * Every field that constrains the work goes in. If a change to a field would
601
+ * change what an operator would have said, it has to move the digest, or the
602
+ * approval it invalidates would be an approval of something else.
603
+ */
604
+ export function digestOf(scope,
605
+ // The execution target: a single profile (legacy v24), OR an explicit
606
+ // fallback chain (v30). BOTH fold through the SAME outer `profileDigest`
607
+ // key — the discriminator lives INSIDE the digest value (a chain digest
608
+ // is domain-separated from a profile digest), NEVER as an outer field
609
+ // (an outer field would change the legacy bytes; C1). A chain-of-one is
610
+ // still an EXPLICIT chain and uses the chain digest, distinct from the
611
+ // single-profile digest by design.
612
+ target,
613
+ // v47: the phase route. EVERY route filed since v47 folds in — routine
614
+ // shaped ones included — so the approval binds exactly which agent runs
615
+ // each phase. Absent (null) only for a row proven to predate v47, whose
616
+ // digest bytes are therefore untouched.
617
+ route) {
618
+ const innerDigest = target == null
619
+ ? null
620
+ : "chain" in target
621
+ ? chainDigestOf(target.chain)
622
+ : profileDigestOf(target);
623
+ return createHash("sha256")
624
+ .update(JSON.stringify({
625
+ goal: scope.goal.trim(),
626
+ outOfScope: scope.outOfScope?.trim() ?? null,
627
+ touches: [...scope.touches].sort(),
628
+ // Absent and null digest identically, so every pre-v15 approval
629
+ // stays exactly as approved.
630
+ ...(scope.budgetMicrousd == null ? {} : { budget: scope.budgetMicrousd }),
631
+ // Same absent/[] equivalence as budget, one key up: a rubric-less
632
+ // scope (every scope before v39, and any road that sends none)
633
+ // digests identically to today.
634
+ ...(scope.acceptance === undefined || scope.acceptance.length === 0
635
+ ? {}
636
+ : { acceptance: canonicalAcceptance(scope.acceptance) }),
637
+ // Default is the historical behavior and therefore hashes exactly
638
+ // like an absent v41 field. Strict is an explicit signed promise.
639
+ ...(scope.qualityMode === "strict" ? { qualityMode: "strict" } : {}),
640
+ ...(scope.candidate ? { candidate: scope.candidate } : {}),
641
+ // The single outer key, whichever target produced its inner value:
642
+ // absent => the golden and every profileless approval are untouched;
643
+ // a legacy profile => its exact profileDigestOf; an explicit chain
644
+ // => its chainDigestOf. Same key, discriminated value.
645
+ ...(innerDigest == null ? {} : { profileDigest: innerDigest }),
646
+ // The route's own domain-separated digest, under its own key —
647
+ // absent only for a pre-v47 row.
648
+ ...(route == null ? {} : { route: routeDigestOf(route) }),
649
+ }), "utf8")
650
+ .digest("hex")
651
+ // 128 bits. This is a safety token binding an approval to exact content,
652
+ // so it has to be well past the reach of a deliberate collision search —
653
+ // 64 bits is not, and the only cost of the longer one is a longer thing to
654
+ // paste back.
655
+ .slice(0, 32);
656
+ }
657
+ export function propose(store, input) {
658
+ const { taskId, goal, outOfScope = null, touches = [], budgetMicrousd = null, acceptance = [], qualityMode = "default", now, mutation = {}, profile, permissionMode, posture, proposedVia = null, riskLevel } = input;
659
+ const draft = { goal, outOfScope, touches: [...touches], budgetMicrousd, acceptance: [...acceptance], qualityMode, candidate: input.candidate ?? null };
660
+ const previous = store.getScope(taskId);
661
+ const scope = {
662
+ taskId,
663
+ ...draft,
664
+ proposedAt: now.toISOString(),
665
+ digest: digestOf(draft),
666
+ // The previous approval is kept rather than cleared, and is invalidated by
667
+ // the digest no longer matching. Same safety — `approvalOf` requires them
668
+ // equal — but it preserves the fact that somebody once agreed to
669
+ // something, so the refusal can say "this was approved and then rewritten"
670
+ // instead of the much less useful "this was never approved".
671
+ approvedAt: previous?.approvedAt ?? null,
672
+ approvedBy: previous?.approvedBy ?? null,
673
+ approvedDigest: previous?.approvedDigest ?? null,
674
+ };
675
+ store.saveScope(scope, mutation, {
676
+ ...(profile === undefined ? {} : { profile }),
677
+ ...(permissionMode === undefined ? {} : { permissionMode }),
678
+ ...(input.qualityMode === undefined ? {} : { qualityMode: input.qualityMode }),
679
+ ...(riskLevel === undefined || !isRiskLevel(riskLevel) ? {} : { riskLevel }),
680
+ ...(posture === undefined ? {} : { posture }),
681
+ proposedVia,
682
+ });
683
+ // The store may have RECOMPUTED the digest to bind the resolved profile
684
+ // (v24 filing invariant) — what callers display must be what is stored.
685
+ const saved = store.getScope(taskId) ?? scope;
686
+ if (input.author !== undefined && input.author !== "")
687
+ store.recordScopeAuthor(taskId, saved.digest, input.author, now);
688
+ return saved;
689
+ }
690
+ /**
691
+ * A scope edit from a surface where the editor might be stale — two browser
692
+ * tabs, a form submitted after the world moved. `sawDigest` names the version
693
+ * the editor was looking at (null: they saw no scope at all); a mismatch is a
694
+ * refusal, never a silent overwrite of somebody else's words. Edits are also
695
+ * refused while a live claim holds the task: the running build read its scope
696
+ * at start, and rewording the agreement under it would make the digest lie
697
+ * about what the work was agreed to. Field caps and control-character rules
698
+ * live here because every one of these strings will later be rendered.
699
+ *
700
+ * v39: this is a scope-producing ROAD, not the `propose` primitive, so it is
701
+ * where the rubric becomes mandatory — every edit through here (a fresh
702
+ * proposal or a rewrite of an already-approved one) must carry at least one
703
+ * criterion, or it is refused with a clear `acceptance-required` reason
704
+ * rather than silently filing without one. A scope that already carries a
705
+ * live, unrewritten approval from before this code existed never reaches
706
+ * this function again unless somebody edits it — which is exactly when the
707
+ * requirement should start applying.
708
+ */
709
+ export function proposeGuarded(store, input) {
710
+ const touches = [...(input.touches ?? [])].map(one => one.trim()).filter(one => one !== "");
711
+ const badText = validateScopeText({ goal: input.goal, outOfScope: input.outOfScope ?? null, touches });
712
+ if (badText !== null)
713
+ return badText;
714
+ const goal = input.goal.trim();
715
+ const outOfScope = input.outOfScope?.trim() || null;
716
+ const acceptanceParse = parseAcceptanceCriteria(input.acceptance);
717
+ if (acceptanceParse.problems.length > 0) {
718
+ return { ok: false, reason: "bad-acceptance" };
719
+ }
720
+ if (acceptanceParse.criteria.length === 0) {
721
+ return { ok: false, reason: "acceptance-required" };
722
+ }
723
+ return store.transact(() => {
724
+ if (input.taskRef !== null && store.hasLiveClaim(input.taskRef, input.now)) {
725
+ return { ok: false, reason: "claimed" };
726
+ }
727
+ const previous = store.getScope(input.taskId);
728
+ if ((previous?.digest ?? null) !== input.sawDigest) {
729
+ return { ok: false, reason: "changed" };
730
+ }
731
+ const scope = propose(store, { ...input, goal, outOfScope, touches, acceptance: acceptanceParse.criteria });
732
+ return { ok: true, scope };
733
+ });
734
+ }
735
+ export function addApprover(store, name, now, by, newToken = mintToken, mutation = {},
736
+ /** A password the person chose. Absent, a token is minted and shown once. */
737
+ password) {
738
+ if (password !== undefined && password.length < 8) {
739
+ return { ok: false, reason: "weak-password" };
740
+ }
741
+ // The first one bootstraps, because somebody has to be able to create the
742
+ // first one and there is nobody to ask yet. Every one after it has to be
743
+ // vouched for by an existing approver.
744
+ //
745
+ // Without that second rule the credential is worth nothing: an agent that
746
+ // can run these commands would simply mint an approver for itself and
747
+ // approve its own scope with it. The bootstrap window is real and is why
748
+ // this is the first thing an operator should do, before anything else can
749
+ // reach the queue.
750
+ // ONE transaction around detection, vouching, and the write (Codex
751
+ // people round 2, finding 1): a voucher revoked between the check and
752
+ // the save must lose — BEGIN IMMEDIATE serializes against the
753
+ // revocation cascade, so there is no between.
754
+ return store.transact(() => {
755
+ const existing = store.listApprovers();
756
+ const bootstrap = existing.length === 0;
757
+ if (!bootstrap) {
758
+ // ACTIVE approver standing, not a bare hash match (Codex people round
759
+ // 1, finding 1): a viewer's or a revoked person's credential is real
760
+ // and still vouches for nothing — the CLI is a ceremony site like any
761
+ // other, and this was the one road that forgot.
762
+ const vouching = by === undefined ? { ok: false } : authenticateApprover(store, by.name, by.token);
763
+ if (!vouching.ok) {
764
+ return { ok: false, reason: "not-an-approver" };
765
+ }
766
+ }
767
+ if (password !== undefined) {
768
+ store.saveApprover(name, hashPassword(password), now, mutation);
769
+ return { ok: true, name, token: password, bootstrap, chosen: true };
770
+ }
771
+ const token = newToken();
772
+ store.saveApprover(name, hashToken(token), now, mutation);
773
+ return { ok: true, name, token, bootstrap, chosen: false };
774
+ });
775
+ }
776
+ /**
777
+ * Whether this name-and-token pair is a person the store knows. Shared by
778
+ * every act that requires a human's authority — approving a scope, answering
779
+ * a decision — because "who said yes" must never be a string the caller
780
+ * typed. Fails closed: with no approver registered there is nobody who can
781
+ * agree to anything, and treating that as "authority is not required here"
782
+ * would make the gate optional — which is the same as not having one.
783
+ */
784
+ /**
785
+ * IDENTITY, split from authority (modes chain, D2/E2): who this credential
786
+ * belongs to and what standing they hold. Viewers authenticate here and
787
+ * read; every consequential act goes through authenticateApprover below,
788
+ * which additionally demands ACTIVE approver standing — so the forty
789
+ * ceremony sites enforce the role without one of them changing.
790
+ */
791
+ export function authenticateAccount(store, by, secret) {
792
+ if (store.listApprovers().length === 0)
793
+ return { ok: false, reason: "no-approvers" };
794
+ // v99: wrong passwords in a row lock the name for a while, whatever road they came by.
795
+ const guard = passwordGuardOf(store), now = Date.now();
796
+ if (guard.lockedFor(by, now) > 0)
797
+ return { ok: false, reason: "locked" };
798
+ const account = store.accountOf(by);
799
+ if (account === null || !verifyCredential(account.credentialHash, secret)) {
800
+ guard.failed(by, now);
801
+ return { ok: false, reason: "unknown" };
802
+ }
803
+ guard.succeeded(by);
804
+ if (account.revokedAt !== null)
805
+ return { ok: false, reason: "revoked" };
806
+ return { ok: true, role: account.role, generation: account.generation };
807
+ }
808
+ /**
809
+ * v100: someone who signed in with the identity provider moments ago has no
810
+ * password to type again. The console marks such a request with this, for
811
+ * that person only, and an EMPTY password then stands for the fresh sign-in
812
+ * at every step-up that reaches `authenticateApprover` (scope and routine
813
+ * approval, chat-action review, the console's own ceremonies). A typed
814
+ * password is always checked, and nothing else stands in.
815
+ */
816
+ export const freshIdentitySignIn = new AsyncLocalStorage();
817
+ export const hasFreshIdentitySignIn = (by) => freshIdentitySignIn.getStore()?.actor === by;
818
+ export function authenticateApprover(store, by, token, repo) {
819
+ // An empty password is never checked as a password (and never counts toward a lockout).
820
+ if (token === "")
821
+ return hasFreshIdentitySignIn(by) ? approverStanding(store, by, repo) : { ok: false, reason: "not-an-approver" };
822
+ const account = authenticateAccount(store, by, token);
823
+ if (!account.ok) {
824
+ return { ok: false, reason: account.reason === "no-approvers" ? "no-approvers" : "not-an-approver" };
825
+ }
826
+ return approverStanding(store, by, repo);
827
+ }
828
+ /**
829
+ * What an approval needs besides the password (v100: an identity-provider
830
+ * sign-in moments ago stands in for the password; nothing stands in for
831
+ * this): an active account in the approver role, with access to the project.
832
+ */
833
+ export function approverStanding(store, by, repo) {
834
+ const account = store.accountOf(by);
835
+ // A viewer's credential is real and still cannot agree to anything —
836
+ // the words every refused ceremony shows are the viewer words.
837
+ if (account === null || account.revokedAt !== null || account.role !== "approver")
838
+ return { ok: false, reason: "not-an-approver" };
839
+ const context = projectAuthority.getStore();
840
+ const resource = repo === undefined ? (context?.actor === by ? context.repo : null) : repo;
841
+ if (!store.accountCanAccess(by, resource))
842
+ return { ok: false, reason: "not-an-approver" };
843
+ return { ok: true };
844
+ }
845
+ /**
846
+ * C1's atomic road: file the scope AND seal its approval in ONE
847
+ * transaction, under a mode the SAME transaction re-proves. The caller
848
+ * has already authenticated the actor and matched channel rules; this
849
+ * function owns the predicate's transactional half — mode active, actor
850
+ * IS the signer, repo matches, and the sealed digest is the filed one
851
+ * (trivially true here: they are the same transaction). Never
852
+ * sealScopeApproval after a filing — that was the TOCTOU.
853
+ */
854
+ /**
855
+ * C1's coverage question, answerable from ANY filing road: does a live
856
+ * mode make THIS actor's credentialed filing auto-approve here, and with
857
+ * which defaults? Callers re-ask INSIDE their filing transaction — an
858
+ * answer carried across transactions would be the TOCTOU again.
859
+ */
860
+ export function modeFilingCoverage(store, repo, actor, now) {
861
+ const mode = repo === null ? null : store.activeMode(repo, now);
862
+ if (mode === null || mode.signedBy !== actor)
863
+ return null;
864
+ try {
865
+ const terms = JSON.parse(mode.termsJson);
866
+ if (terms.autoApproveFiling !== true)
867
+ return null;
868
+ return {
869
+ digest: mode.digest,
870
+ escalated: terms.permissionDefault === "escalated",
871
+ defaultBudgetMicrousd: typeof terms.perAttemptBudgetMicrousd === "number" ? terms.perAttemptBudgetMicrousd : null,
872
+ };
873
+ }
874
+ catch {
875
+ return null;
876
+ }
877
+ }
878
+ export function fileAndSealUnderMode(store, input) {
879
+ return store.transact(() => {
880
+ const mode = input.repo === null ? null : store.activeMode(input.repo, input.now);
881
+ if (mode === null)
882
+ return { ok: false, reason: "no-mode" };
883
+ if (mode.signedBy !== input.actor)
884
+ return { ok: false, reason: "not-signer" };
885
+ let autoApprove = false;
886
+ let defaultBudget = null;
887
+ let escalated = false;
888
+ try {
889
+ const terms = JSON.parse(mode.termsJson);
890
+ autoApprove = terms.autoApproveFiling === true;
891
+ defaultBudget = typeof terms.perAttemptBudgetMicrousd === "number" ? terms.perAttemptBudgetMicrousd : null;
892
+ escalated = terms.permissionDefault === "escalated";
893
+ }
894
+ catch {
895
+ autoApprove = false;
896
+ }
897
+ if (!autoApprove)
898
+ return { ok: false, reason: "not-covered" };
899
+ // v39: a mode auto-approves a filing with NOBODY reading it at that
900
+ // instant — of every road, this one can least afford to seal a scope
901
+ // with no rubric to answer.
902
+ if (parseAcceptanceCriteria(input.acceptance).criteria.length === 0) {
903
+ return { ok: false, reason: "acceptance-required" };
904
+ }
905
+ const scope = propose(store, {
906
+ ...input,
907
+ ...(input.budgetMicrousd == null && defaultBudget !== null ? { budgetMicrousd: defaultBudget } : {}),
908
+ ...(escalated ? { posture: "escalated" } : {}),
909
+ });
910
+ // A scope that cannot say exactly which agents run (v47: every phase
911
+ // exact, the route readable) is never auto-sealed — the words say why.
912
+ if (scope.profileState === "unresolved") {
913
+ return { ok: false, reason: "profile-unresolved", ...(scope.unresolvedReason == null ? {} : { detail: scope.unresolvedReason }) };
914
+ }
915
+ const sealed = store.sealScopeApproval(input.taskId, input.actor, input.now, input.mutation ?? {}, { kind: "mode", modeDigest: mode.digest });
916
+ // The quarantine speaks through every caller (review finding 7): a
917
+ // coordinator-filed task is never mode-admitted, and pretending the
918
+ // seal landed would hide exactly the refusal the operator must see.
919
+ if (!sealed)
920
+ return { ok: false, reason: "coordinator-filed" };
921
+ return { ok: true, scope: store.getScope(input.taskId), basis: "mode" };
922
+ });
923
+ }
924
+ export function approve(store, taskId, by, now, sawDigest, token, mutation = {}) {
925
+ // Read and write in one transaction, re-reading inside it — the credential
926
+ // check included, so a token rotated between authentication and the write
927
+ // cannot leave an approval signed by an authority that no longer exists.
928
+ // Apart, a scope rewritten between the read and the write is overwritten by
929
+ // this approval — which would resurrect the old wording *and* mark it
930
+ // agreed, the precise opposite of what the digest is for.
931
+ return store.transact(() => {
932
+ const authenticated = authenticateApprover(store, by, token, store.lookupRef(taskId)?.repo ?? null);
933
+ if (!authenticated.ok)
934
+ return authenticated;
935
+ const scope = store.getScope(taskId);
936
+ if (scope === null)
937
+ return { ok: false, reason: "no-scope" };
938
+ // The digest is required, not optional. An operator reads a scope, somebody
939
+ // rewrites it, and an approval that did not name what it saw would agree to
940
+ // the new one in silence — which is the whole failure this guards.
941
+ if (sawDigest !== scope.digest)
942
+ return { ok: false, reason: "changed" };
943
+ // An unresolved scope cannot say exactly what would run, so nobody can
944
+ // agree to it (foundations finding 16) — restatement is the road.
945
+ if (scope.profileState === "unresolved") {
946
+ return { ok: false, reason: "profile-unresolved" };
947
+ }
948
+ // A row that predates agent routing cannot take a NEW yes (v48): an
949
+ // approval now names exactly which agent plans, builds, repairs, and
950
+ // reviews, and this row names none — re-filing routes it.
951
+ if (scope.routeEra == null) {
952
+ return { ok: false, reason: "unrouted" };
953
+ }
954
+ // SEPARATION OF DUTIES (v102): the project's rules, asked first so the
955
+ // answer is words, not a silent refusal from the seal. Protected work
956
+ // keeps each person's yes to these exact bytes and seals on the second.
957
+ // Sprint 8: the organisation policy, before any vote is kept: a disallowed provider or model, or terms above the
958
+ // permission ceiling, are refused in words that name the rule.
959
+ const policy = store.scopePolicyRefusal(taskId);
960
+ if (policy !== null)
961
+ return { ok: false, reason: "policy", message: policy };
962
+ const gate = store.approvalGate(taskId, by, "person");
963
+ if (gate.verdict === "refuse")
964
+ return { ok: false, reason: gate.reason };
965
+ if (gate.verdict === "vote") {
966
+ store.recordApprovalVote(taskId, scope.digest, by, now);
967
+ return { ok: false, reason: "second-approver", have: gate.have, need: gate.need, already: gate.already };
968
+ }
969
+ if (gate.protectedWork)
970
+ store.recordApprovalVote(taskId, scope.digest, by, now);
971
+ // SEAL, never re-resolve: the approval snapshots the stored working
972
+ // profile — the exact bytes the digest the approver signed was bound
973
+ // to. Routing saveScope here would re-run resolution and could sign a
974
+ // profile nobody saw.
975
+ const sealed = store.sealScopeApproval(taskId, by, now, mutation);
976
+ if (!sealed)
977
+ return { ok: false, reason: "changed" };
978
+ const approved = store.getScope(taskId);
979
+ if (approved === null)
980
+ return { ok: false, reason: "no-scope" };
981
+ return { ok: true, scope: approved };
982
+ });
983
+ }
984
+ /**
985
+ * Whether this task may be built.
986
+ *
987
+ * `changed` is the interesting answer and the reason the digest exists: the
988
+ * scope was approved, and then somebody — possibly an agent, possibly the
989
+ * operator, possibly a different session — rewrote it. The old yes does not
990
+ * transfer.
991
+ */
992
+ export function approvalOf(scope) {
993
+ if (scope === null)
994
+ return { approved: false, reason: "never-proposed" };
995
+ if (scope.approvedAt === null || scope.approvedBy === null) {
996
+ return { approved: false, reason: "none" };
997
+ }
998
+ if (scope.approvedDigest !== scope.digest)
999
+ return { approved: false, reason: "changed" };
1000
+ return { approved: true, at: scope.approvedAt, by: scope.approvedBy };
1001
+ }
1002
+ /**
1003
+ * THE PARITY every projection proves between a route and the profile and
1004
+ * terms it was filed beside (atomic authority closure): the build and
1005
+ * repair legs ARE the profile's exact pairs, and the route's signed risk
1006
+ * and quality ARE the row's — a row whose risk says high over a route
1007
+ * recommended for routine was not filed by this code, and neither the
1008
+ * seal, the consent door, nor the dispatch proof believes it. One
1009
+ * function, so the working side, the sealed side, and the last-mile
1010
+ * dispatch proof cannot drift. Null when everything agrees.
1011
+ */
1012
+ export function routeParityProblem(route, profile, terms) {
1013
+ const buildLeg = legOf(route, "build");
1014
+ const repairLeg = legOf(route, "repair");
1015
+ const repairModel = profile.repairModel === "inherit" ? profile.model : profile.repairModel;
1016
+ if (buildLeg.provider !== profile.provider || buildLeg.model !== profile.model || repairLeg.provider !== profile.provider || repairLeg.model !== repairModel) {
1017
+ return `the route builds on ${buildLeg.provider} · ${buildLeg.model} (repair ${repairLeg.provider} · ${repairLeg.model}) but the agent profile says ${profile.provider} · ${profile.model} (repair ${profile.provider} · ${repairModel})`;
1018
+ }
1019
+ if (route.risk !== terms.riskLevel) {
1020
+ return `the route was recommended for ${route.risk} risk but the scope's risk level is ${terms.riskLevel}`;
1021
+ }
1022
+ if (route.qualityMode !== terms.qualityMode) {
1023
+ return `the route was recommended for ${route.qualityMode} quality but the scope's quality mode is ${terms.qualityMode}`;
1024
+ }
1025
+ return null;
1026
+ }
1027
+ // Recovery reuses this exact raw-terms check before considering saved evidence.
1028
+ // Decoding authority does not renew approval or authorize a process-custody write.
1029
+ export function scopeAuthorityOf(scope, env = {}) {
1030
+ // THE RAW TERMS FIRST (raw authority repair): a row whose stored terms
1031
+ // or metadata do not read back exactly is no authority at all — not
1032
+ // the filtered, defaulted, or coerced reading of it.
1033
+ if (scope.termsProblem != null) {
1034
+ return { ok: false, reason: "terms", problem: `the scope's stored terms cannot be read exactly (${scope.termsProblem})` };
1035
+ }
1036
+ if (scope.routeEra == null) {
1037
+ return { ok: false, reason: "unrouted", problem: "this scope predates agent routing — its terms name no agent for any role; re-file it to route it under today's agents" };
1038
+ }
1039
+ if (scope.profileState === "unresolved") {
1040
+ return { ok: false, reason: "unresolved", problem: scope.unresolvedReason ?? "the scope cannot say exactly what would run" };
1041
+ }
1042
+ const profileJson = scope.profileJson === undefined ? (scope.profile == null ? null : canonicalProfileJson(scope.profile)) : scope.profileJson;
1043
+ const profile = profileFromJson(profileJson);
1044
+ if (profile === null) {
1045
+ return { ok: false, reason: "profile", problem: profileJson === null ? "the scope carries no agent profile" : "the scope's agent profile cannot be read exactly (unknown keys, an unsafe number, or a clock no timer can hold)" };
1046
+ }
1047
+ const chainJson = scope.proposedChainJson ?? null;
1048
+ const chain = chainJson === null ? null : chainFromJson(chainJson);
1049
+ if (chainJson !== null && chain === null) {
1050
+ return { ok: false, reason: "chain", problem: "the scope's fallback chain cannot be read exactly (unknown keys, a malformed auth mode, or an entry that is not a whole profile)" };
1051
+ }
1052
+ if (chain !== null) {
1053
+ const base = chain[0];
1054
+ if (base === undefined || profileDigestOf(base.profile) !== profileDigestOf(profile)) {
1055
+ return { ok: false, reason: "parity", problem: "the scope's agent profile is not its fallback chain's first entry" };
1056
+ }
1057
+ }
1058
+ const routeJson = scope.proposedRouteJson ?? null;
1059
+ const route = routeFromJson(routeJson);
1060
+ if (route === null) {
1061
+ return { ok: false, reason: "route", problem: routeJson === null ? "the scope was filed under agent routing but carries no route" : "the scope's agent route cannot be read exactly" };
1062
+ }
1063
+ const parity = routeParityProblem(route, profile, { riskLevel: scope.riskLevel ?? "routine", qualityMode: scope.qualityMode ?? "default" });
1064
+ if (parity !== null)
1065
+ return { ok: false, reason: "parity", problem: parity };
1066
+ const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default" }, chain !== null ? { chain } : profile, route);
1067
+ if (digest !== scope.digest) {
1068
+ return { ok: false, reason: "digest", problem: "the scope's reference does not re-derive from its own terms, agents, and route — file it again" };
1069
+ }
1070
+ // THE AUTH MODE, strictly (atomic authority closure): the credential the
1071
+ // profile's provider would spend under is read through the caller's
1072
+ // strict reader — a present mode file that says neither word closes
1073
+ // the door in its words, never the default a lenient read would coerce
1074
+ // it to (the spawn reads the same way, so nothing sealed here spends
1075
+ // on a credential the operator never named). A chain filing carries
1076
+ // its base entry's pinned mode and must agree with the live file too.
1077
+ let authMode = null;
1078
+ if (env.authMode !== undefined) {
1079
+ const read = env.authMode(profile.provider);
1080
+ if (!read.ok)
1081
+ return { ok: false, reason: "auth-mode", problem: read.problem };
1082
+ authMode = read.mode;
1083
+ // PINNED AND LIVE AUTH AGREE, or nothing (final authority closure): a
1084
+ // chain filed under one credential for its base entry, read beside a
1085
+ // live mode file that now says the other, is a scope whose signed
1086
+ // words no longer describe what would spend — a subscription chain
1087
+ // approved after the operator moved to an API key would bill the
1088
+ // key. It holds no authority until it is filed again under today's
1089
+ // mode.
1090
+ const base = chain === null ? null : chain[0] ?? null;
1091
+ if (base !== null && base.authMode !== authMode) {
1092
+ return {
1093
+ ok: false,
1094
+ reason: "auth-mode",
1095
+ problem: `the scope's fallback chain pins its base entry to ${base.authMode === "subscription" ? "your subscription" : "your API key"} for ${profile.provider}, but the live auth mode is now ${authMode === "subscription" ? "your subscription" : "your API key"} — re-file the scope under today's mode`,
1096
+ };
1097
+ }
1098
+ }
1099
+ return { ok: true, profile, chain, route, digest, authMode };
1100
+ }
1101
+ /** The rubric's approval-card lines (v39): one per criterion, the id in
1102
+ * front (the exact string a proof must answer by) and its required
1103
+ * evidence kinds after it — `how` is deliberately absent here, the same way
1104
+ * it is absent from the digest: it is guidance, never a signed term. */
1105
+ export function acceptanceWords(criteria) {
1106
+ if (criteria.length === 0)
1107
+ return [];
1108
+ return [
1109
+ ` acceptance ${criteria[0].id}: ${criteria[0].statement} [requires: ${criteria[0].evidence.join(", ")}]`,
1110
+ ...criteria.slice(1).map(c => ` ${c.id}: ${c.statement} [requires: ${c.evidence.join(", ")}]`),
1111
+ ];
1112
+ }
1113
+ /** The route's approval-card lines (v47): the WORKING route the digest
1114
+ * bound, projected with whatever readiness the caller can see — the same
1115
+ * bytes the task page and chat render. Empty on a scope with no route. */
1116
+ export function scopeRouteWords(scope, readiness = NO_READINESS) {
1117
+ const route = routeFromJson(scope.proposedRouteJson ?? null);
1118
+ return route === null ? [] : routeWords(projectRoute(route, readiness));
1119
+ }
1120
+ /** The scope, in the words an operator has to be able to agree or disagree with. */
1121
+ export function describeScope(scope, readiness = NO_READINESS) {
1122
+ const approval = approvalOf(scope);
1123
+ return [
1124
+ ` goal ${scope.goal}`,
1125
+ ...(scope.outOfScope === null ? [] : [` not this ${scope.outOfScope}`]),
1126
+ ...(scope.candidate ? [` candidate ${scope.candidate} — checked out by the machine, no agent runs`] : []),
1127
+ ...(scope.touches.length === 0 ? [] : [` touches ${scope.touches.join(", ")}`]),
1128
+ ...acceptanceWords(scope.acceptance),
1129
+ ...(scope.budgetMicrousd === null
1130
+ ? []
1131
+ : [` budget $${(scope.budgetMicrousd / 1_000_000).toFixed(2)} per build attempt — the agent is stopped at this figure`]),
1132
+ // The FALLBACK CHAIN, in the words the yes agrees to (Layer F): when
1133
+ // this scope files under configured fallbacks, the digest above binds
1134
+ // the WHOLE ordered chain — so the card says every entry, credential
1135
+ // included, before anyone signs.
1136
+ ...chainWords(chainFromJson(scope.proposedChainJson ?? null)),
1137
+ // The ROUTE, in the words the yes agrees to (v47): risk, posture, and
1138
+ // every leg with its reason and readiness — before anyone signs.
1139
+ ...(scope.riskLevel === undefined || scope.riskLevel === "routine" ? [] : [` risk ${scope.riskLevel}`]),
1140
+ ...scopeRouteWords(scope, readiness),
1141
+ ` reference ${scope.digest}`,
1142
+ ` approved ${describeApproval(approval)}`,
1143
+ ];
1144
+ }
1145
+ /** The chain's approval-card lines; empty for a single-profile scope. */
1146
+ export function chainWords(chain) {
1147
+ if (chain === null || chain.length < 2)
1148
+ return [];
1149
+ const credential = (mode) => mode === "subscription" ? "your subscription" : "your API key";
1150
+ const lines = [];
1151
+ const base = chain[0];
1152
+ lines.push(` runs on ${base.profile.provider} (${base.profile.model}) — ${credential(base.authMode)}`);
1153
+ for (const entry of chain.slice(1)) {
1154
+ lines.push(` if that runs out falls back to ${entry.profile.provider} (${entry.profile.model}) — ${credential(entry.authMode)}${entry.authMode === "api-key" ? "; spend moves to that account" : ""}`);
1155
+ }
1156
+ return lines;
1157
+ }
1158
+ function describeApproval(approval) {
1159
+ if (approval.approved)
1160
+ return `yes, by ${approval.by} at ${approval.at}`;
1161
+ if (approval.reason === "changed") {
1162
+ return "no — it was approved, then the scope was rewritten; approve it again";
1163
+ }
1164
+ return "no — nothing will build this until somebody agrees to it";
1165
+ }
1166
+ /** Deterministic bytes: sorted keys, undefined dropped — the signed text. */
1167
+ export function attendedTermsJson(terms) {
1168
+ return canonicalJson(terms);
1169
+ }
1170
+ /** The composite digest one password signs (ruling 12), domain-separated. */
1171
+ export function attendedDigestOf(terms) {
1172
+ return createHash("sha256")
1173
+ .update(`standing-orders:attended:${attendedTermsJson(terms)}`)
1174
+ .digest("hex")
1175
+ .slice(0, 32);
1176
+ }