@intentius/chant 0.90.0 → 0.93.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 (402) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/components.d.ts +8 -0
  4. package/dist/cli/handlers/components.d.ts.map +1 -1
  5. package/dist/cli/handlers/graph.d.ts.map +1 -1
  6. package/dist/cli/handlers/operator.d.ts +20 -0
  7. package/dist/cli/handlers/operator.d.ts.map +1 -1
  8. package/dist/cli/handlers/run.d.ts.map +1 -1
  9. package/dist/cli/main.d.ts.map +1 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts +7 -4
  11. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  12. package/dist/cli/plugins.d.ts.map +1 -1
  13. package/dist/cli/registry.d.ts +42 -1
  14. package/dist/cli/registry.d.ts.map +1 -1
  15. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  16. package/dist/components/verbs/vuln-scan.d.ts +72 -0
  17. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  18. package/dist/config.d.ts +22 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/lexicon-module.d.ts +22 -0
  21. package/dist/lexicon-module.d.ts.map +1 -1
  22. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  23. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  24. package/dist/lifecycle/git.d.ts +40 -5
  25. package/dist/lifecycle/git.d.ts.map +1 -1
  26. package/dist/lifecycle/lease.d.ts +72 -16
  27. package/dist/lifecycle/lease.d.ts.map +1 -1
  28. package/dist/lifecycle/member-ledger.d.ts +3 -2
  29. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  30. package/dist/lifecycle/plan-ledger.d.ts +114 -0
  31. package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
  32. package/dist/lifecycle/run-ledger.d.ts +2 -0
  33. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  34. package/dist/lifecycle/symptoms.d.ts +51 -0
  35. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  36. package/dist/lifecycle/work-lease.d.ts +140 -0
  37. package/dist/lifecycle/work-lease.d.ts.map +1 -0
  38. package/dist/lint/rules/index.d.ts +4 -2
  39. package/dist/lint/rules/index.d.ts.map +1 -1
  40. package/dist/lint/rules/op/index.d.ts +14 -3
  41. package/dist/lint/rules/op/index.d.ts.map +1 -1
  42. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  43. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  44. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  45. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  46. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  47. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  48. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  49. package/dist/op/activities/activity-contracts.d.ts +203 -2
  50. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  51. package/dist/op/activities/apply.d.ts.map +1 -1
  52. package/dist/op/activities/change-coverage.d.ts +44 -0
  53. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  54. package/dist/op/activities/converge.d.ts +24 -1
  55. package/dist/op/activities/converge.d.ts.map +1 -1
  56. package/dist/op/activities/decide.d.ts +107 -0
  57. package/dist/op/activities/decide.d.ts.map +1 -0
  58. package/dist/op/activities/index.d.ts +10 -0
  59. package/dist/op/activities/index.d.ts.map +1 -1
  60. package/dist/op/activities/shell.d.ts +32 -0
  61. package/dist/op/activities/shell.d.ts.map +1 -1
  62. package/dist/op/activities/source-release.d.ts +86 -0
  63. package/dist/op/activities/source-release.d.ts.map +1 -0
  64. package/dist/op/activities/source-rollback.d.ts +100 -0
  65. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  66. package/dist/op/activities/work-evidence.d.ts +42 -0
  67. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  68. package/dist/op/activity-contract.d.ts +1 -0
  69. package/dist/op/activity-contract.d.ts.map +1 -1
  70. package/dist/op/activity-profiles.d.ts +16 -0
  71. package/dist/op/activity-profiles.d.ts.map +1 -1
  72. package/dist/op/builders.d.ts +84 -2
  73. package/dist/op/builders.d.ts.map +1 -1
  74. package/dist/op/composites/converge-op.d.ts +31 -4
  75. package/dist/op/composites/converge-op.d.ts.map +1 -1
  76. package/dist/op/decide-backend.d.ts +116 -0
  77. package/dist/op/decide-backend.d.ts.map +1 -0
  78. package/dist/op/decide-config.d.ts +120 -0
  79. package/dist/op/decide-config.d.ts.map +1 -0
  80. package/dist/op/decide-read-inputs.d.ts +40 -0
  81. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  82. package/dist/op/discover.d.ts +25 -0
  83. package/dist/op/discover.d.ts.map +1 -1
  84. package/dist/op/gate-wait.d.ts +43 -0
  85. package/dist/op/gate-wait.d.ts.map +1 -0
  86. package/dist/op/gate.d.ts +5 -2
  87. package/dist/op/gate.d.ts.map +1 -1
  88. package/dist/op/index.d.ts +18 -5
  89. package/dist/op/index.d.ts.map +1 -1
  90. package/dist/op/lifecycle-receipt-store.d.ts +34 -0
  91. package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
  92. package/dist/op/local-executor.d.ts +48 -4
  93. package/dist/op/local-executor.d.ts.map +1 -1
  94. package/dist/op/local-output.d.ts +2 -0
  95. package/dist/op/local-output.d.ts.map +1 -1
  96. package/dist/op/op-ir.d.ts +12 -1
  97. package/dist/op/op-ir.d.ts.map +1 -1
  98. package/dist/op/op-verb-class.d.ts.map +1 -1
  99. package/dist/op/operator.d.ts +126 -1
  100. package/dist/op/operator.d.ts.map +1 -1
  101. package/dist/op/runtime.d.ts +46 -5
  102. package/dist/op/runtime.d.ts.map +1 -1
  103. package/dist/op/runtimes/local.d.ts.map +1 -1
  104. package/dist/op/source-archive.d.ts +63 -0
  105. package/dist/op/source-archive.d.ts.map +1 -0
  106. package/dist/op/step-output-ref.d.ts +2 -2
  107. package/dist/op/step-output-ref.d.ts.map +1 -1
  108. package/dist/op/steward-points.d.ts +136 -0
  109. package/dist/op/steward-points.d.ts.map +1 -0
  110. package/dist/op/steward-turn.d.ts +53 -0
  111. package/dist/op/steward-turn.d.ts.map +1 -0
  112. package/dist/op/steward.d.ts +161 -0
  113. package/dist/op/steward.d.ts.map +1 -0
  114. package/dist/op/types.d.ts +58 -0
  115. package/dist/op/types.d.ts.map +1 -1
  116. package/dist/op/work-lease-decl.d.ts +18 -0
  117. package/dist/op/work-lease-decl.d.ts.map +1 -0
  118. package/dist/op/work-lease-run.d.ts +173 -0
  119. package/dist/op/work-lease-run.d.ts.map +1 -0
  120. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  121. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  122. package/dist/workspace/answers-ledger.d.ts +76 -0
  123. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  124. package/dist/workspace/box-isolation.d.ts +99 -0
  125. package/dist/workspace/box-isolation.d.ts.map +1 -0
  126. package/dist/workspace/changes-cli.d.ts +16 -0
  127. package/dist/workspace/changes-cli.d.ts.map +1 -0
  128. package/dist/workspace/changes.d.ts +200 -0
  129. package/dist/workspace/changes.d.ts.map +1 -0
  130. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  131. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  132. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  133. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  134. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  135. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  136. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  137. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  138. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  139. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  140. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  141. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  142. package/dist/workspace/chant-migrations.d.ts +99 -0
  143. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  144. package/dist/workspace/checks/box-isolation.d.ts +18 -0
  145. package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
  146. package/dist/workspace/checks/boxes.d.ts +104 -0
  147. package/dist/workspace/checks/boxes.d.ts.map +1 -0
  148. package/dist/workspace/checks/diagrams.d.ts +31 -0
  149. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  150. package/dist/workspace/checks/records.d.ts +8 -0
  151. package/dist/workspace/checks/records.d.ts.map +1 -1
  152. package/dist/workspace/checks.d.ts +13 -1
  153. package/dist/workspace/checks.d.ts.map +1 -1
  154. package/dist/workspace/compose-graph.d.ts +2 -0
  155. package/dist/workspace/compose-graph.d.ts.map +1 -1
  156. package/dist/workspace/conformance/index.d.ts.map +1 -1
  157. package/dist/workspace/decide.d.ts +229 -0
  158. package/dist/workspace/decide.d.ts.map +1 -0
  159. package/dist/workspace/decision-points.schema.json +137 -0
  160. package/dist/workspace/declaration.d.ts +103 -0
  161. package/dist/workspace/declaration.d.ts.map +1 -1
  162. package/dist/workspace/declaration.schema.json +285 -0
  163. package/dist/workspace/declared-kinds.d.ts +32 -0
  164. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  165. package/dist/workspace/generated-files.d.ts +7 -0
  166. package/dist/workspace/generated-files.d.ts.map +1 -1
  167. package/dist/workspace/intent.d.ts.map +1 -1
  168. package/dist/workspace/lineage-check.d.ts +3 -0
  169. package/dist/workspace/lineage-check.d.ts.map +1 -1
  170. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  171. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  172. package/dist/workspace/ls.d.ts +39 -1
  173. package/dist/workspace/ls.d.ts.map +1 -1
  174. package/dist/workspace/point-answer.schema.json +95 -0
  175. package/dist/workspace/points-cli.d.ts +113 -0
  176. package/dist/workspace/points-cli.d.ts.map +1 -0
  177. package/dist/workspace/points.d.ts +320 -0
  178. package/dist/workspace/points.d.ts.map +1 -0
  179. package/dist/workspace/reason-codes.d.ts +38 -0
  180. package/dist/workspace/reason-codes.d.ts.map +1 -1
  181. package/dist/workspace/record-assets.d.ts.map +1 -1
  182. package/dist/workspace/records-cli.d.ts +23 -1
  183. package/dist/workspace/records-cli.d.ts.map +1 -1
  184. package/dist/workspace/records-close.d.ts +1 -1
  185. package/dist/workspace/records-close.d.ts.map +1 -1
  186. package/dist/workspace/records-write.d.ts +11 -5
  187. package/dist/workspace/records-write.d.ts.map +1 -1
  188. package/dist/workspace/records.d.ts +42 -6
  189. package/dist/workspace/records.d.ts.map +1 -1
  190. package/dist/workspace/status-gates.d.ts +7 -2
  191. package/dist/workspace/status-gates.d.ts.map +1 -1
  192. package/dist/workspace/status-stewards.d.ts +179 -0
  193. package/dist/workspace/status-stewards.d.ts.map +1 -0
  194. package/dist/workspace/status.d.ts +68 -1
  195. package/dist/workspace/status.d.ts.map +1 -1
  196. package/dist/workspace/work-cli.d.ts +189 -0
  197. package/dist/workspace/work-cli.d.ts.map +1 -0
  198. package/dist/workspace/work-evidence.d.ts +74 -0
  199. package/dist/workspace/work-evidence.d.ts.map +1 -0
  200. package/dist/workspace/work.d.ts +43 -1
  201. package/dist/workspace/work.d.ts.map +1 -1
  202. package/package.json +1 -1
  203. package/src/cli/commands/audit.test.ts +9 -1
  204. package/src/cli/commands/build.ts +3 -2
  205. package/src/cli/commands/lint.ts +7 -1
  206. package/src/cli/handlers/components.test.ts +93 -0
  207. package/src/cli/handlers/components.ts +44 -3
  208. package/src/cli/handlers/graph.test.ts +20 -0
  209. package/src/cli/handlers/graph.ts +16 -2
  210. package/src/cli/handlers/operator.ts +141 -4
  211. package/src/cli/handlers/run.test.ts +173 -2
  212. package/src/cli/handlers/run.ts +174 -3
  213. package/src/cli/main.test.ts +50 -0
  214. package/src/cli/main.ts +97 -2
  215. package/src/cli/mcp/workspace-tools.test.ts +48 -19
  216. package/src/cli/mcp/workspace-tools.ts +110 -8
  217. package/src/cli/plugins.ts +11 -3
  218. package/src/cli/registry.ts +42 -1
  219. package/src/codegen/pinned-upgrade.ts +2 -1
  220. package/src/codegen/release-wiring.test.ts +49 -2
  221. package/src/components/capability-plugin-loader.ts +3 -3
  222. package/src/components/cli-support.ts +2 -2
  223. package/src/components/verbs/vuln-scan.test.ts +124 -1
  224. package/src/components/verbs/vuln-scan.ts +142 -1
  225. package/src/config.ts +11 -0
  226. package/src/lexicon-module.ts +56 -1
  227. package/src/lexicon-package.test.ts +96 -0
  228. package/src/lifecycle/converge-ledger.ts +20 -0
  229. package/src/lifecycle/git.ts +65 -15
  230. package/src/lifecycle/lease.test.ts +22 -0
  231. package/src/lifecycle/lease.ts +133 -29
  232. package/src/lifecycle/member-ledger.ts +3 -2
  233. package/src/lifecycle/plan-ledger.test.ts +148 -0
  234. package/src/lifecycle/plan-ledger.ts +158 -0
  235. package/src/lifecycle/run-ledger.ts +6 -0
  236. package/src/lifecycle/symptoms.ts +95 -0
  237. package/src/lifecycle/work-lease.test.ts +236 -0
  238. package/src/lifecycle/work-lease.ts +426 -0
  239. package/src/lint/rules/index.ts +6 -2
  240. package/src/lint/rules/op/index.ts +17 -4
  241. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  242. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  243. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  244. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  245. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  246. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  247. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  248. package/src/op/activities/activity-contracts.ts +190 -1
  249. package/src/op/activities/apply.ts +7 -6
  250. package/src/op/activities/change-coverage.ts +60 -0
  251. package/src/op/activities/converge.test.ts +55 -1
  252. package/src/op/activities/converge.ts +208 -29
  253. package/src/op/activities/decide.live.test.ts +78 -0
  254. package/src/op/activities/decide.test.ts +300 -0
  255. package/src/op/activities/decide.ts +267 -0
  256. package/src/op/activities/index.ts +21 -0
  257. package/src/op/activities/shell.test.ts +91 -1
  258. package/src/op/activities/shell.ts +91 -2
  259. package/src/op/activities/source-release.test.ts +118 -0
  260. package/src/op/activities/source-release.ts +168 -0
  261. package/src/op/activities/source-rollback.test.ts +131 -0
  262. package/src/op/activities/source-rollback.ts +239 -0
  263. package/src/op/activities/work-evidence.ts +62 -0
  264. package/src/op/activity-contract-registry.test.ts +2 -0
  265. package/src/op/activity-contract-registry.ts +2 -2
  266. package/src/op/activity-contract.test.ts +12 -0
  267. package/src/op/activity-contract.ts +6 -1
  268. package/src/op/activity-profiles.ts +29 -0
  269. package/src/op/activity-registry.test.ts +1 -1
  270. package/src/op/activity-registry.ts +2 -2
  271. package/src/op/builders.ts +124 -5
  272. package/src/op/command-gate.test.ts +192 -0
  273. package/src/op/composites/converge-op.test.ts +27 -1
  274. package/src/op/composites/converge-op.ts +69 -15
  275. package/src/op/decide-backend.test.ts +44 -0
  276. package/src/op/decide-backend.ts +212 -0
  277. package/src/op/decide-config.test.ts +52 -0
  278. package/src/op/decide-config.ts +80 -0
  279. package/src/op/decide-read-inputs.ts +117 -0
  280. package/src/op/discover.ts +71 -0
  281. package/src/op/gate-approval.ts +2 -2
  282. package/src/op/gate-wait.ts +57 -0
  283. package/src/op/gate.ts +8 -3
  284. package/src/op/generate-pipeline.ts +2 -2
  285. package/src/op/index.ts +34 -6
  286. package/src/op/lifecycle-receipt-store.test.ts +60 -0
  287. package/src/op/lifecycle-receipt-store.ts +61 -0
  288. package/src/op/local-executor.ts +367 -33
  289. package/src/op/local-output.test.ts +33 -0
  290. package/src/op/local-output.ts +33 -2
  291. package/src/op/op-ir.test.ts +10 -0
  292. package/src/op/op-ir.ts +18 -0
  293. package/src/op/op-verb-class.ts +1 -0
  294. package/src/op/operator.ts +360 -10
  295. package/src/op/runtime.ts +37 -8
  296. package/src/op/runtimes/local.ts +6 -1
  297. package/src/op/source-archive.ts +161 -0
  298. package/src/op/step-output-ref.ts +6 -2
  299. package/src/op/steward-points.test.ts +313 -0
  300. package/src/op/steward-points.ts +194 -0
  301. package/src/op/steward-turn.ts +79 -0
  302. package/src/op/steward.test.ts +296 -0
  303. package/src/op/steward.ts +277 -0
  304. package/src/op/types.ts +60 -0
  305. package/src/op/work-lease-decl.ts +80 -0
  306. package/src/op/work-lease-run.test.ts +345 -0
  307. package/src/op/work-lease-run.ts +395 -0
  308. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  309. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  310. package/src/workspace/answers-ledger.ts +118 -0
  311. package/src/workspace/box-isolation.test.ts +261 -0
  312. package/src/workspace/box-isolation.ts +205 -0
  313. package/src/workspace/changes-cli.ts +56 -0
  314. package/src/workspace/changes.schema.json +472 -0
  315. package/src/workspace/changes.test.ts +315 -0
  316. package/src/workspace/changes.ts +512 -0
  317. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  318. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  319. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  320. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  321. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  322. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  323. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  324. package/src/workspace/chant-migrations.ts +164 -0
  325. package/src/workspace/check-contract.test.ts +5 -1
  326. package/src/workspace/check.schema.json +25 -7
  327. package/src/workspace/checks/box-isolation.ts +68 -0
  328. package/src/workspace/checks/boxes.test.ts +276 -0
  329. package/src/workspace/checks/boxes.ts +442 -0
  330. package/src/workspace/checks/diagrams.test.ts +134 -0
  331. package/src/workspace/checks/diagrams.ts +108 -0
  332. package/src/workspace/checks/records.ts +72 -0
  333. package/src/workspace/checks.test.ts +7 -0
  334. package/src/workspace/checks.ts +27 -3
  335. package/src/workspace/compose-graph.ts +2 -0
  336. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  337. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  338. package/src/workspace/conformance/index.ts +5 -0
  339. package/src/workspace/decide.test.ts +224 -0
  340. package/src/workspace/decide.ts +709 -0
  341. package/src/workspace/decision-points.schema.json +137 -0
  342. package/src/workspace/declaration.schema.json +285 -0
  343. package/src/workspace/declaration.test.ts +58 -0
  344. package/src/workspace/declaration.ts +241 -0
  345. package/src/workspace/declared-kinds.test.ts +5 -5
  346. package/src/workspace/declared-kinds.ts +71 -3
  347. package/src/workspace/generated-files.ts +14 -0
  348. package/src/workspace/graph-cli.ts +1 -1
  349. package/src/workspace/graph.schema.json +9 -1
  350. package/src/workspace/intent.schema.json +12 -3
  351. package/src/workspace/intent.ts +3 -10
  352. package/src/workspace/lineage-check.ts +16 -1
  353. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  354. package/src/workspace/lineage-upgrade.test.ts +70 -0
  355. package/src/workspace/lineage-upgrade.ts +81 -10
  356. package/src/workspace/ls-contract.test.ts +57 -0
  357. package/src/workspace/ls.schema.json +46 -1
  358. package/src/workspace/ls.ts +62 -3
  359. package/src/workspace/point-answer.schema.json +95 -0
  360. package/src/workspace/points-cli.ts +284 -0
  361. package/src/workspace/points-write.schema.json +499 -0
  362. package/src/workspace/points.schema.json +743 -0
  363. package/src/workspace/points.test.ts +264 -0
  364. package/src/workspace/points.ts +564 -0
  365. package/src/workspace/read-contract.test.ts +18 -1
  366. package/src/workspace/reason-codes.test.ts +24 -1
  367. package/src/workspace/reason-codes.ts +49 -0
  368. package/src/workspace/record-assets.test.ts +3 -2
  369. package/src/workspace/record-assets.ts +4 -1
  370. package/src/workspace/records-amend.schema.json +2 -0
  371. package/src/workspace/records-cli.ts +28 -5
  372. package/src/workspace/records-close.schema.json +2 -0
  373. package/src/workspace/records-contract.test.ts +3 -2
  374. package/src/workspace/records-new.schema.json +2 -0
  375. package/src/workspace/records-review.schema.json +2 -0
  376. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  377. package/src/workspace/records-sessions-write.test.ts +2 -34
  378. package/src/workspace/records-write.e2e.test.ts +108 -0
  379. package/src/workspace/records-write.test.ts +32 -41
  380. package/src/workspace/records-write.ts +33 -15
  381. package/src/workspace/records.schema.json +53 -2
  382. package/src/workspace/records.test.ts +48 -3
  383. package/src/workspace/records.ts +142 -6
  384. package/src/workspace/status-contract.test.ts +189 -6
  385. package/src/workspace/status-gates.ts +9 -4
  386. package/src/workspace/status-stewards.ts +329 -0
  387. package/src/workspace/status.schema.json +335 -5
  388. package/src/workspace/status.ts +142 -6
  389. package/src/workspace/work-acceptance.test.ts +274 -0
  390. package/src/workspace/work-cli.e2e.test.ts +180 -0
  391. package/src/workspace/work-cli.ts +429 -0
  392. package/src/workspace/work-evidence.schema.json +212 -0
  393. package/src/workspace/work-evidence.ts +169 -0
  394. package/src/workspace/work-history.schema.json +349 -0
  395. package/src/workspace/work-history.test.ts +156 -0
  396. package/src/workspace/work-lease.schema.json +233 -0
  397. package/src/workspace/work-readiness-chud.test.ts +145 -0
  398. package/src/workspace/work.test.ts +3 -2
  399. package/src/workspace/work.ts +100 -1
  400. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  401. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  402. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -25,13 +25,19 @@ import { resolveActivity, type ActivityFn, type ActivityProfile } from "./activi
25
25
  import type { ReceiptReadResult } from "./receipt-store";
26
26
  import { isStepOutputRef } from "./step-output-ref";
27
27
  import { parseDuration } from "./duration";
28
+ import { stepTimeoutProblem } from "./activity-profiles";
28
29
  import { describeGateMismatch, evaluateGate, gitGateLedgerPort, type GateCheck, type GateLedgerPort } from "./gate";
29
30
  import { gateName } from "./gate-name";
30
31
  import type { ResolvedGateApproval } from "./gate-approval";
31
32
  import type { PendingGateRecord } from "../lifecycle/gate-ledger";
33
+ import { isPointWait, type WaitingPoint } from "./steward-points";
34
+ import { isGateWait } from "./gate-wait";
35
+ import { currentStewardTurn, enterStewardTurn } from "./steward-turn";
32
36
  import { appendRunRecord, buildRunRecord } from "../lifecycle/run-ledger";
33
37
  import type { OpRunRecord } from "./runtime";
34
38
  import { randomUUID } from "node:crypto";
39
+ import { RunWorkLease, LEASE_LOST, workLeaseProblems, workLeaseNeedsRunItem, type WorkLeaseRunResult } from "./work-lease-run";
40
+ import { WORK_LEASE_STEP_ID } from "./types";
35
41
 
36
42
  export { parseDuration } from "./duration";
37
43
 
@@ -76,6 +82,18 @@ export interface StepRecord {
76
82
  * was applied.
77
83
  */
78
84
  refusal?: string;
85
+ /**
86
+ * Set on an activity step that asked a decision point still open (#2749):
87
+ * the question the run now waits on. The step's status is `skipped` and the
88
+ * run's is `waiting`: nothing broke, and a person answers the question.
89
+ */
90
+ point?: WaitingPoint;
91
+ /**
92
+ * Set on an activity step whose command stopped at a gate of its own
93
+ * (#2779): the op the gate is recorded under and its name, what `chant
94
+ * approve` takes. The step's status is `skipped` and the run's is `gated`.
95
+ */
96
+ gate?: { op: string; gate: string };
79
97
  }
80
98
 
81
99
  export interface OpRunResult {
@@ -89,11 +107,17 @@ export interface OpRunResult {
89
107
  *
90
108
  * Written to the run ledger as the record's own `status` (#2118).
91
109
  */
92
- status: "ok" | "fail" | "gated";
110
+ status: "ok" | "fail" | "gated" | "waiting";
93
111
  /** ISO-8601 start of this run. */
94
112
  startedAt: string;
95
113
  /** Present when `status === "gated"`: the pending fact the run ended on. */
96
114
  gate?: PendingGateRecord;
115
+ /**
116
+ * Present when `status === "waiting"` (#2749): the open decision point the
117
+ * run stopped on. Like a gate, it is a fact and not a wait: a person answers
118
+ * the question, and the next run reads the answer.
119
+ */
120
+ point?: WaitingPoint;
97
121
  /**
98
122
  * Present when `status === "gated"` and this run's own append tried to push:
99
123
  * whether it reached the remote (#2310). Absent when the run stopped on a
@@ -110,6 +134,12 @@ export interface OpRunResult {
110
134
  * document.
111
135
  */
112
136
  record: OpRunRecord;
137
+ /**
138
+ * How the run's work lease went, for an Op that declares `workLease`
139
+ * (#2748): the item it claimed, or why it claimed none, whether the lease
140
+ * was released and with which outcome, or why it was lost.
141
+ */
142
+ workLease?: WorkLeaseRunResult;
113
143
  }
114
144
 
115
145
  // ── Errors ────────────────────────────────────────────────────────────────��─
@@ -165,6 +195,35 @@ class GateStop extends Error {
165
195
  }
166
196
  }
167
197
 
198
+ /** Internal: a step asked a decision point that is still open (#2749). Not a failure — no compensation follows it. */
199
+ class PointStop extends Error {
200
+ constructor(
201
+ public readonly records: StepRecord[],
202
+ public readonly question: WaitingPoint,
203
+ public readonly phase: string,
204
+ ) {
205
+ super(`waiting on decision point ${question.id}`);
206
+ this.name = "PointStop";
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Internal: a step boundary found the run can't go on under its work lease
212
+ * (#2748). `lost` false is a claim that found nothing to claim: not a
213
+ * failure, the run ends `ok` with the rest skipped. `lost` true is a lease
214
+ * that was lost: the run fails with `lease-lost`.
215
+ */
216
+ class WorkStop extends Error {
217
+ constructor(
218
+ public readonly records: StepRecord[],
219
+ public readonly phase: string,
220
+ public readonly lost: boolean,
221
+ ) {
222
+ super(lost ? LEASE_LOST : "nothing to claim");
223
+ this.name = "WorkStop";
224
+ }
225
+ }
226
+
168
227
  // ── Helpers ───────────────────────────────────────────────────────────────��─
169
228
 
170
229
  const DEFAULT_PROFILE = "fastIdempotent";
@@ -287,6 +346,8 @@ async function callWithTimeout(
287
346
  interface RanStep {
288
347
  record: StepRecord;
289
348
  result?: unknown;
349
+ /** The pending fact a step's command stopped at (#2779), when it threw {@link GateWait}. */
350
+ pending?: PendingGateRecord;
290
351
  }
291
352
 
292
353
  /**
@@ -316,9 +377,16 @@ async function runStep(
316
377
  }
317
378
 
318
379
  const profile = profiles[step.profile ?? DEFAULT_PROFILE] ?? {};
319
- const timeoutMs = profile.timeout
320
- ? parseDuration(profile.timeout)
321
- : FALLBACK_TIMEOUT_MS;
380
+ // A step's own timeout (#2787) replaces the profile's and keeps its retries.
381
+ if (step.timeout !== undefined) {
382
+ const problem = stepTimeoutProblem(step.timeout);
383
+ if (problem) return { record: { ...base, status: "fail", durationMs: 0, error: problem } };
384
+ }
385
+ const timeoutMs = step.timeout !== undefined
386
+ ? parseDuration(step.timeout)
387
+ : profile.timeout
388
+ ? parseDuration(profile.timeout)
389
+ : FALLBACK_TIMEOUT_MS;
322
390
  const maxAttempts =
323
391
  profile.retry?.maximumAttempts && profile.retry.maximumAttempts > 0
324
392
  ? profile.retry.maximumAttempts
@@ -343,6 +411,17 @@ async function runStep(
343
411
  }
344
412
  return { record, result };
345
413
  } catch (err) {
414
+ // An open decision point (#2749) is not a failure and not worth a retry:
415
+ // the answer comes from a person, on a later run.
416
+ if (isPointWait(err)) {
417
+ return { record: { ...base, status: "skipped", durationMs: Date.now() - start, point: err.question } };
418
+ }
419
+ // Nor is a command that stopped at a gate of its own (#2779): the run
420
+ // ends `gated` there, and the next run asks the command again.
421
+ if (isGateWait(err)) {
422
+ const gate = { op: err.pending.op, gate: err.pending.gate };
423
+ return { record: { ...base, status: "skipped", durationMs: Date.now() - start, gate }, pending: err.pending };
424
+ }
346
425
  lastErr = err;
347
426
  // Stop retrying on abort (Ctrl-C / timeout cascade) or a non-retryable error.
348
427
  const fatal =
@@ -391,6 +470,68 @@ interface GateContext {
391
470
  runId?: string;
392
471
  /** Called once per settled step, in production order (#2121) — what `--progress-json` streams from. */
393
472
  onRecord?: (record: StepRecord) => void;
473
+ /** The run's hold on its work item's lease (#2748). Absent in compensation phases, which run whatever the lease did. */
474
+ work?: RunWorkLease;
475
+ }
476
+
477
+ /**
478
+ * The work lease at a step boundary (#2748): claim it when it is due, and
479
+ * say whether the run can go on. The claim's record is pushed onto `sink`.
480
+ * Returns `undefined` to go on, `"unclaimed"` when nothing could be claimed,
481
+ * and `"lost"` when the lease was lost.
482
+ */
483
+ async function workBoundary(
484
+ phaseName: string,
485
+ ctx: GateContext,
486
+ resultsById: Map<string, unknown>,
487
+ sink: StepRecord[],
488
+ ): Promise<"unclaimed" | "lost" | undefined> {
489
+ const work = ctx.work;
490
+ if (!work) return undefined;
491
+ if (work.due(resultsById)) {
492
+ const start = Date.now();
493
+ let claimed: Awaited<ReturnType<RunWorkLease["claim"]>>;
494
+ try {
495
+ claimed = await work.claim(resultsById);
496
+ } catch (err) {
497
+ pushRecord(sink, ctx, {
498
+ phase: phaseName,
499
+ fn: "workLease:claim",
500
+ args: {},
501
+ status: "fail",
502
+ durationMs: Date.now() - start,
503
+ error: errMessage(err),
504
+ });
505
+ throw new PhaseFailure(sink);
506
+ }
507
+ if (claimed.kind === "unclaimed") {
508
+ pushRecord(sink, ctx, {
509
+ phase: phaseName,
510
+ fn: "workLease:claim",
511
+ args: { items: claimed.tried },
512
+ status: "skipped",
513
+ durationMs: Date.now() - start,
514
+ refusal: claimed.refusal,
515
+ outcome: { name: "WorkItem", value: null },
516
+ outcomes: [{ name: "WorkItem", value: null }],
517
+ });
518
+ return "unclaimed";
519
+ }
520
+ resultsById.set(WORK_LEASE_STEP_ID, claimed.lease);
521
+ pushRecord(sink, ctx, {
522
+ phase: phaseName,
523
+ fn: "workLease:claim",
524
+ args: { items: claimed.tried },
525
+ status: "ok",
526
+ durationMs: Date.now() - start,
527
+ outcome: { name: "WorkItem", value: claimed.lease.item },
528
+ outcomes: [
529
+ { name: "WorkItem", value: claimed.lease.item },
530
+ { name: "WorkLeaseToken", value: claimed.lease.token },
531
+ ],
532
+ });
533
+ }
534
+ return work.lost !== undefined ? "lost" : undefined;
394
535
  }
395
536
 
396
537
  /** Collect records and hand each to the caller's progress sink in one move. */
@@ -553,6 +694,7 @@ async function runEffectStep(
553
694
  pending?: PendingGateRecord;
554
695
  pushed?: boolean;
555
696
  pushWarning?: string;
697
+ point?: WaitingPoint;
556
698
  }> {
557
699
  const records: StepRecord[] = [];
558
700
 
@@ -618,6 +760,18 @@ async function runEffectStep(
618
760
  }
619
761
  const ran = await runStep(nested, phaseName, activities, profiles, resultsById, signal);
620
762
  pushRecord(records, gates, ran.record);
763
+ if (ran.pending) {
764
+ // A command's own gate (#2779): the receipt is left untouched, as for a
765
+ // pending gate step.
766
+ skipRest(i + 1);
767
+ return { records, failed: false, pending: ran.pending };
768
+ }
769
+ if (ran.record.point) {
770
+ // Receipt left untouched, as for a pending gate: the next run re-proposes
771
+ // the effect and asks the point again, and finds the answer by then.
772
+ skipRest(i + 1);
773
+ return { records, failed: false, point: ran.record.point };
774
+ }
621
775
  if (ran.record.status === "fail") {
622
776
  // Receipt left untouched (stale) — the next run re-proposes the effect.
623
777
  skipRest(i + 1);
@@ -666,6 +820,13 @@ async function runPhase(
666
820
  // read of the ledger, not work, and starting activities that a pending
667
821
  // gate is about to strand would defeat the point of stopping at it.
668
822
  const gateRecords: StepRecord[] = [];
823
+ const stop = await workBoundary(phase.name, gates, resultsById, gateRecords);
824
+ if (stop) {
825
+ for (const skipped of phase.steps) {
826
+ pushRecord(gateRecords, gates, isGate(skipped) ? skippedRecord(phase.name, gateFn(skipped)) : skippedRecord(phase.name, (skipped as ActivityStep).fn, (skipped as ActivityStep).args));
827
+ }
828
+ throw new WorkStop(gateRecords, phase.name, stop === "lost");
829
+ }
669
830
  for (const step of phase.steps.filter(isGate)) {
670
831
  const { record, pending, pushed, pushWarning } = await runGateStep(step, phase.name, gates, resultsById);
671
832
  pushRecord(gateRecords, gates, record);
@@ -685,12 +846,19 @@ async function runPhase(
685
846
  }
686
847
  }
687
848
  const steps = phase.steps.filter(isActivity);
688
- const ran = (
689
- await Promise.all(steps.map((s) => runStep(s, phase.name, activities, profiles, resultsById, signal)))
690
- ).map((r) => r.record);
849
+ const settled = await Promise.all(steps.map((s) => runStep(s, phase.name, activities, profiles, resultsById, signal)));
850
+ const ran = settled.map((r) => r.record);
691
851
  for (const r of ran) gates.onRecord?.(r);
692
852
  const records = gateRecords.concat(ran);
693
853
  if (records.some((r) => r.status === "fail")) throw new PhaseFailure(records);
854
+ // A step whose command stopped at its own gate (#2779) stops the run at
855
+ // this phase, as a gate step would, once its siblings have finished.
856
+ const gated = settled.find((r) => r.pending)?.pending;
857
+ if (gated) throw new GateStop(records, gated, phase.name);
858
+ // The fan-out has already run, so the siblings of an open decision point
859
+ // finished; the run still stops at this phase (#2749).
860
+ const waiting = ran.find((r) => r.point);
861
+ if (waiting?.point) throw new PointStop(records, waiting.point, phase.name);
694
862
  return records;
695
863
  }
696
864
 
@@ -711,6 +879,13 @@ async function runPhase(
711
879
 
712
880
  for (let i = 0; i < steps.length; i++) {
713
881
  const step = steps[i];
882
+ // The work lease (#2748): claimed here when it is due, and checked before
883
+ // every step, so no step starts once the lease is lost.
884
+ const stop = await workBoundary(phase.name, gates, resultsById, records);
885
+ if (stop) {
886
+ skipRemaining(i);
887
+ throw new WorkStop(records, phase.name, stop === "lost");
888
+ }
714
889
  if (isGate(step)) {
715
890
  const { record, pending, pushed, pushWarning } = await runGateStep(step, phase.name, gates, resultsById);
716
891
  pushRecord(records, gates, record);
@@ -727,10 +902,14 @@ async function runPhase(
727
902
  continue;
728
903
  }
729
904
  if (isEffect(step)) {
730
- const { records: effRecords, failed, pending, pushed, pushWarning } = await runEffectStep(
905
+ const { records: effRecords, failed, pending, pushed, pushWarning, point } = await runEffectStep(
731
906
  step, phase.name, activities, profiles, resultsById, gates, signal,
732
907
  );
733
908
  records.push(...effRecords); // already emitted by runEffectStep
909
+ if (point) {
910
+ skipRemaining(i + 1);
911
+ throw new PointStop(records, point, phase.name);
912
+ }
734
913
  if (pending) {
735
914
  skipRemaining(i + 1);
736
915
  throw new GateStop(records, pending, phase.name, pushed, pushWarning);
@@ -741,8 +920,19 @@ async function runPhase(
741
920
  }
742
921
  continue;
743
922
  }
744
- const { record } = await runStep(step, phase.name, activities, profiles, resultsById, signal);
923
+ const { record, pending } = await runStep(step, phase.name, activities, profiles, resultsById, signal);
745
924
  pushRecord(records, gates, record);
925
+ if (pending) {
926
+ // The step's command stopped at its own gate (#2779): the run ends
927
+ // `gated` there, as at a pending gate step.
928
+ skipRemaining(i + 1);
929
+ throw new GateStop(records, pending, phase.name);
930
+ }
931
+ if (record.point) {
932
+ // An open decision point (#2749) ends the run as a pending gate does.
933
+ skipRemaining(i + 1);
934
+ throw new PointStop(records, record.point, phase.name);
935
+ }
746
936
  if (record.status === "fail") {
747
937
  // Mark the remaining steps in this phase as skipped, then abort.
748
938
  skipRemaining(i + 1);
@@ -789,6 +979,17 @@ export interface RunOpOptions {
789
979
  * never promoted into a run failure.
790
980
  */
791
981
  onLedgerError?: (err: unknown) => void;
982
+ /** The steward whose turn this run is (#2749), recorded on its ledger record. */
983
+ steward?: string;
984
+ /**
985
+ * For an Op that declares `workLease` (#2748): the item this run is for
986
+ * (`chant run <op> --work <id>`), which takes the place of the Op's own, and
987
+ * who holds the lease (a steward's turn passes `stewardWorkHolder`).
988
+ * Defaults: the Op's item, and this process's holder id.
989
+ */
990
+ work?: { item?: string; holder?: string };
991
+ /** Called when a work-lease renewal fails without losing the lease; the next beat retries it. */
992
+ onWorkLeaseWarning?: (message: string) => void;
792
993
  }
793
994
 
794
995
  /**
@@ -797,9 +998,10 @@ export interface RunOpOptions {
797
998
  * Resolves with the run result when every phase succeeds (`status: "ok"`), and
798
999
  * also when the run stopped at an unapproved gate (`status: "gated"`, with the
799
1000
  * pending fact on `result.gate`) — a gate is a fact, not an error, so it is not
800
- * thrown. Rejects with `OpRunFailure` (carrying the partial result) on terminal
801
- * failure, after running any `onFailure` phases in reverse order; a gated run
802
- * runs no `onFailure` phase, because nothing failed and nothing was left
1001
+ * thrown. So does an activity's open decision point (`status: "waiting"`, with
1002
+ * the question on `result.point`, #2749). Rejects with `OpRunFailure` (carrying the partial result) on terminal
1003
+ * failure, after running any `onFailure` phases in reverse order; a gated or
1004
+ * waiting run runs no `onFailure` phase, because nothing failed and nothing was left
803
1005
  * half-applied to compensate for.
804
1006
  *
805
1007
  * Whichever of the three ways it settles, the run carries an
@@ -818,6 +1020,27 @@ export async function runOpLocally(
818
1020
  // The run id is the gate ledger's and the run ledger's alike: a pending gate
819
1021
  // fact and the record naming that gate carry the same string.
820
1022
  const runId = options.runId ?? randomUUID();
1023
+ // In a steward's turn (#2749: `chant operator --steward`, or `chant acp
1024
+ // --steward` on Fountain) the run is the steward's: its record names the
1025
+ // steward, and a decision point asked during it names this run.
1026
+ const turn = currentStewardTurn();
1027
+ const steward = options.steward ?? turn?.steward;
1028
+ const restore = turn && turn.run !== runId ? enterStewardTurn({ ...turn, run: runId }) : undefined;
1029
+ try {
1030
+ return await runOpInTurn(config, activities, profiles, signal, { ...options, runId, ...(steward ? { steward } : {}) });
1031
+ } finally {
1032
+ restore?.();
1033
+ }
1034
+ }
1035
+
1036
+ async function runOpInTurn(
1037
+ config: OpConfig,
1038
+ activities: Map<string, ActivityFn>,
1039
+ profiles: Record<string, ActivityProfile>,
1040
+ signal: AbortSignal | undefined,
1041
+ options: RunOpOptions & { runId: string },
1042
+ ): Promise<OpRunResult> {
1043
+ const runId = options.runId;
821
1044
 
822
1045
  const gates: GateContext = {
823
1046
  op: config.name,
@@ -839,6 +1062,68 @@ export async function runOpLocally(
839
1062
  }
840
1063
  }
841
1064
 
1065
+ // The work lease (#2748). An Op that changes the checkout without one, or
1066
+ // whose lease names no item and whose run names none, is refused before
1067
+ // anything runs.
1068
+ const workProblems = workLeaseProblems(config);
1069
+ if (workProblems.length > 0) throw new Error(workProblems.join("\n"));
1070
+ if (workLeaseNeedsRunItem(config) && !options.work?.item) {
1071
+ throw new Error(
1072
+ `Op "${config.name}" runs under a work item's lease and names no item: pass one with \`chant run ${config.name} --work <id>\``,
1073
+ );
1074
+ }
1075
+ const work = config.workLease
1076
+ ? new RunWorkLease(config, {
1077
+ cwd: options.ledger?.cwd ?? options.cwd ?? process.cwd(),
1078
+ ...(options.work?.holder ? { holder: options.work.holder } : {}),
1079
+ ...(options.work?.item ? { item: options.work.item } : {}),
1080
+ ...(options.onWorkLeaseWarning ? { onRenewError: options.onWorkLeaseWarning } : {}),
1081
+ })
1082
+ : undefined;
1083
+ if (work) gates.work = work;
1084
+
1085
+ // Steps run under a signal that also fires when the work lease is lost, so
1086
+ // the step in flight stops (chud's dispatcher stopped its agent the same way).
1087
+ let stepSignal = signal;
1088
+ if (work) {
1089
+ const both = new AbortController();
1090
+ const forward = () => both.abort();
1091
+ if (signal?.aborted) both.abort();
1092
+ else signal?.addEventListener("abort", forward, { once: true });
1093
+ work.lostSignal.addEventListener("abort", forward, { once: true });
1094
+ stepSignal = both.signal;
1095
+ }
1096
+
1097
+ /** Release (or not) the work lease once the run's outcome is settled. */
1098
+ const finishWork = async (status: OpRunResult["status"]): Promise<{ workLease?: WorkLeaseRunResult }> =>
1099
+ work ? { workLease: await work.finish(status, resultsById) } : {};
1100
+
1101
+ /** The record a lost lease leaves, naming why. */
1102
+ const lostRecord = (): StepRecord => ({
1103
+ phase: UNATTRIBUTED_PHASE,
1104
+ fn: "workLease:lost",
1105
+ args: {},
1106
+ status: "fail",
1107
+ durationMs: 0,
1108
+ error: `${LEASE_LOST}: ${work?.lost ?? "the lease was lost"}. The run stopped and was not recorded as done.`,
1109
+ });
1110
+
1111
+ const skipLaterPhases = (after: string) => {
1112
+ const stoppedAt = config.phases.findIndex((p) => p.name === after);
1113
+ if (stoppedAt < 0) return;
1114
+ for (const phase of config.phases.slice(stoppedAt + 1)) {
1115
+ for (const step of phase.steps) {
1116
+ records.push(
1117
+ isEffect(step)
1118
+ ? skippedRecord(phase.name, `effect:${step.receipt.name}`)
1119
+ : isGate(step)
1120
+ ? skippedRecord(phase.name, gateFn(step))
1121
+ : skippedRecord(phase.name, step.fn, step.args),
1122
+ );
1123
+ }
1124
+ }
1125
+ };
1126
+
842
1127
  const records: StepRecord[] = [];
843
1128
  const start = Date.now();
844
1129
  const startedAt = options.now ?? new Date(start).toISOString();
@@ -851,13 +1136,18 @@ export async function runOpLocally(
851
1136
  settled: StepRecord[],
852
1137
  status: OpRunResult["status"],
853
1138
  gate?: PendingGateRecord,
1139
+ point?: WaitingPoint,
854
1140
  ): Promise<OpRunRecord> => {
1141
+ const ended = options.now ?? new Date().toISOString();
855
1142
  const input = buildRunRecord(config, settled, {
856
1143
  started: startedAt,
857
- ended: options.now ?? new Date().toISOString(),
1144
+ ended,
858
1145
  status,
859
1146
  id: runId,
860
- ...(gate ? { gate: { name: gate.gate, since: gate.timestamp } } : {}),
1147
+ // A gate recorded under another op (#2779: a command's own gate) names it.
1148
+ ...(gate ? { gate: { name: gate.gate, since: gate.timestamp, ...(gate.op !== config.name ? { op: gate.op } : {}) } } : {}),
1149
+ ...(point ? { point: { ...point, since: ended } } : {}),
1150
+ ...(options.steward ? { steward: options.steward } : {}),
861
1151
  });
862
1152
  const record: OpRunRecord = { version: 1, ...input, id: runId };
863
1153
  if (!options.ledger) return record;
@@ -877,25 +1167,53 @@ export async function runOpLocally(
877
1167
  try {
878
1168
  for (const phase of config.phases) {
879
1169
  if (signal?.aborted) throw new PhaseFailure([]);
880
- records.push(...(await runPhase(phase, activities, profiles, resultsById, gates, signal)));
1170
+ records.push(...(await runPhase(phase, activities, profiles, resultsById, gates, stepSignal)));
881
1171
  }
1172
+ // The last renewal before the run is recorded (#2748): a run whose lease
1173
+ // was lost is never recorded as done.
1174
+ if (work && !(await work.fence())) throw new WorkStop([], UNATTRIBUTED_PHASE, true);
882
1175
  } catch (err) {
1176
+ // Nothing to claim, or every candidate held (#2748): not a failure. The
1177
+ // rest of the run is skipped and the claim's record says why.
1178
+ if (err instanceof WorkStop && !err.lost) {
1179
+ records.push(...err.records);
1180
+ skipLaterPhases(err.phase);
1181
+ const record = await settle(records, "ok");
1182
+ return {
1183
+ op: config.name,
1184
+ records,
1185
+ totalMs: Date.now() - start,
1186
+ status: "ok",
1187
+ startedAt,
1188
+ record,
1189
+ ...(await finishWork("ok")),
1190
+ };
1191
+ }
1192
+
1193
+ // An open decision point ends the run the way a pending gate does (#2749):
1194
+ // no later phase and no compensation, and the run is `waiting`.
1195
+ if (err instanceof PointStop) {
1196
+ records.push(...err.records);
1197
+ skipLaterPhases(err.phase);
1198
+ const record = await settle(records, "waiting", undefined, err.question);
1199
+ return {
1200
+ op: config.name,
1201
+ records,
1202
+ totalMs: Date.now() - start,
1203
+ status: "waiting",
1204
+ startedAt,
1205
+ point: err.question,
1206
+ record,
1207
+ ...(await finishWork("waiting")),
1208
+ };
1209
+ }
1210
+
883
1211
  // A pending gate ends the run where it stands: no later phase, and no
884
1212
  // `onFailure` compensation — nothing failed, so there is nothing to undo.
885
1213
  if (err instanceof GateStop) {
886
1214
  records.push(...err.records);
887
- const stoppedAt = config.phases.findIndex((p) => p.name === err.phase);
888
- for (const phase of config.phases.slice(stoppedAt + 1)) {
889
- for (const step of phase.steps) {
890
- records.push(
891
- isEffect(step)
892
- ? skippedRecord(phase.name, `effect:${step.receipt.name}`)
893
- : isGate(step)
894
- ? skippedRecord(phase.name, gateFn(step))
895
- : skippedRecord(phase.name, step.fn, step.args),
896
- );
897
- }
898
- }
1215
+ skipLaterPhases(err.phase);
1216
+ const record = await settle(records, "gated", err.pending);
899
1217
  return {
900
1218
  op: config.name,
901
1219
  records,
@@ -905,11 +1223,17 @@ export async function runOpLocally(
905
1223
  gate: err.pending,
906
1224
  ...(err.pushed !== undefined ? { gatePushed: err.pushed } : {}),
907
1225
  ...(err.pushWarning ? { gatePushWarning: err.pushWarning } : {}),
908
- record: await settle(records, "gated", err.pending),
1226
+ record,
1227
+ ...(await finishWork("gated")),
909
1228
  };
910
1229
  }
911
1230
 
912
- if (err instanceof PhaseFailure) {
1231
+ if (err instanceof WorkStop) {
1232
+ // The lease was lost (#2748): the rest is skipped, and the lost record
1233
+ // below says why.
1234
+ records.push(...err.records);
1235
+ skipLaterPhases(err.phase);
1236
+ } else if (err instanceof PhaseFailure) {
913
1237
  records.push(...err.records);
914
1238
  } else {
915
1239
  // Everything else the phase loop can throw (#2301). `PhaseFailure` and
@@ -934,14 +1258,20 @@ export async function runOpLocally(
934
1258
  });
935
1259
  }
936
1260
 
1261
+ // A step that failed because the lease was lost mid-step, or a lease lost
1262
+ // at any boundary: one record names it (#2748).
1263
+ if (work?.lost !== undefined) records.push(lostRecord());
1264
+
937
1265
  // Compensation: run onFailure phases in reverse order (best-effort). Skipped
938
1266
  // on abort (Ctrl-C) — the user asked to stop, so don't start new work.
1267
+ // It runs whatever the work lease did, so it takes no work boundary.
1268
+ const compensation: GateContext = { ...gates, work: undefined };
939
1269
  if (!signal?.aborted) {
940
1270
  for (const phase of [...(config.onFailure ?? [])].reverse()) {
941
1271
  try {
942
- records.push(...(await runPhase(phase, activities, profiles, resultsById, gates, signal)));
1272
+ records.push(...(await runPhase(phase, activities, profiles, resultsById, compensation, signal)));
943
1273
  } catch (compErr) {
944
- if (compErr instanceof PhaseFailure || compErr instanceof GateStop) {
1274
+ if (compErr instanceof PhaseFailure || compErr instanceof GateStop || compErr instanceof PointStop) {
945
1275
  records.push(...compErr.records);
946
1276
  } else {
947
1277
  // The same swallow, one level down (#2301): a compensation phase
@@ -959,6 +1289,7 @@ export async function runOpLocally(
959
1289
  }
960
1290
  }
961
1291
 
1292
+ const failed = await settle(records, "fail");
962
1293
  throw new OpRunFailure(
963
1294
  {
964
1295
  op: config.name,
@@ -966,19 +1297,22 @@ export async function runOpLocally(
966
1297
  totalMs: Date.now() - start,
967
1298
  status: "fail",
968
1299
  startedAt,
969
- record: await settle(records, "fail"),
1300
+ record: failed,
1301
+ ...(await finishWork("fail")),
970
1302
  },
971
1303
  { cause: err },
972
1304
  );
973
1305
  }
974
1306
 
1307
+ const record = await settle(records, "ok");
975
1308
  return {
976
1309
  op: config.name,
977
1310
  records,
978
1311
  totalMs: Date.now() - start,
979
1312
  status: "ok",
980
1313
  startedAt,
981
- record: await settle(records, "ok"),
1314
+ record,
1315
+ ...(await finishWork("ok")),
982
1316
  };
983
1317
  }
984
1318
 
@@ -75,6 +75,23 @@ const GATED: OpRunResult = withRecord({
75
75
  ],
76
76
  });
77
77
 
78
+ /** A gate bound to a plan (#2300), for the --plan tests (#2832). */
79
+ const GATED_WITH_PLAN: OpRunResult = withRecord({
80
+ op: "prod-apply",
81
+ totalMs: 300,
82
+ status: "gated",
83
+ startedAt: "2026-09-05T12:00:00.000Z",
84
+ gate: {
85
+ version: 1, kind: "pending", op: "prod-apply", gate: "rollout-gate",
86
+ timestamp: "2026-09-05T12:00:00.000Z",
87
+ expiresAt: "2026-09-07T12:00:00.000Z",
88
+ planDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
89
+ },
90
+ records: [
91
+ { phase: "Apply", fn: "gate:rollout-gate", args: {}, status: "skipped", durationMs: 0 },
92
+ ],
93
+ });
94
+
78
95
  describe("renderHuman — gated (#2119)", () => {
79
96
  test("names the gate, the approve line and the expiry, and points at no other runtime", () => {
80
97
  const lines: string[] = [];
@@ -88,6 +105,14 @@ describe("renderHuman — gated (#2119)", () => {
88
105
  expect(out).not.toContain("--on");
89
106
  });
90
107
 
108
+ test("a gate bound to a plan (#2300) carries --plan on the approve line (#2832)", () => {
109
+ const lines: string[] = [];
110
+ renderHuman(GATED_WITH_PLAN, (l) => lines.push(l));
111
+ const out = lines.join("\n");
112
+ expect(out).toContain(`plan : ${GATED_WITH_PLAN.gate!.planDigest}`);
113
+ expect(out).toContain(`approve : chant approve prod-apply rollout-gate --plan ${GATED_WITH_PLAN.gate!.planDigest}`);
114
+ });
115
+
91
116
  test("shows the approver on a gate that passed", () => {
92
117
  const lines: string[] = [];
93
118
  renderHuman(withRecord({
@@ -157,6 +182,14 @@ describe("renderJson", () => {
157
182
  expect(parsed.approve).toBe("chant approve prod-apply rollout-gate");
158
183
  });
159
184
 
185
+ test("a gate bound to a plan (#2300) carries --plan on the approve line (#2832)", () => {
186
+ const lines: string[] = [];
187
+ renderJson(GATED_WITH_PLAN, (l) => lines.push(l));
188
+ const parsed = JSON.parse(lines[0]) as OpRunRecord & { approve: string };
189
+ expect(parsed.status).toBe("gated");
190
+ expect(parsed.approve).toBe(`chant approve prod-apply rollout-gate --plan ${GATED_WITH_PLAN.gate!.planDigest}`);
191
+ });
192
+
160
193
  // #2310: a JSON consumer (CI tooling reading `chant run --json`) needs the
161
194
  // same fact the human render shows — not just a silent success.
162
195
  test("a gated record whose push failed carries pushed:false and why", () => {