@intentius/chant 0.91.0 → 0.94.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 (361) 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/graph.d.ts.map +1 -1
  4. package/dist/cli/handlers/operator.d.ts +6 -0
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/workspace-plugins.d.ts +6 -6
  9. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  11. package/dist/cli/plugins.d.ts.map +1 -1
  12. package/dist/cli/registry.d.ts +12 -2
  13. package/dist/cli/registry.d.ts.map +1 -1
  14. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/lexicon-module.d.ts +22 -0
  18. package/dist/lexicon-module.d.ts.map +1 -1
  19. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  20. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  21. package/dist/lifecycle/run-ledger.d.ts +2 -0
  22. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  23. package/dist/lifecycle/symptoms.d.ts +51 -0
  24. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  25. package/dist/lint/rules/index.d.ts +4 -2
  26. package/dist/lint/rules/index.d.ts.map +1 -1
  27. package/dist/lint/rules/op/index.d.ts +14 -3
  28. package/dist/lint/rules/op/index.d.ts.map +1 -1
  29. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  31. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  33. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  35. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  36. package/dist/op/activities/activity-contracts.d.ts +201 -0
  37. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  38. package/dist/op/activities/apply.d.ts.map +1 -1
  39. package/dist/op/activities/change-coverage.d.ts +44 -0
  40. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  41. package/dist/op/activities/converge.d.ts +24 -1
  42. package/dist/op/activities/converge.d.ts.map +1 -1
  43. package/dist/op/activities/decide.d.ts +107 -0
  44. package/dist/op/activities/decide.d.ts.map +1 -0
  45. package/dist/op/activities/index.d.ts +10 -0
  46. package/dist/op/activities/index.d.ts.map +1 -1
  47. package/dist/op/activities/shell.d.ts +32 -0
  48. package/dist/op/activities/shell.d.ts.map +1 -1
  49. package/dist/op/activities/source-release.d.ts +86 -0
  50. package/dist/op/activities/source-release.d.ts.map +1 -0
  51. package/dist/op/activities/source-rollback.d.ts +100 -0
  52. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  53. package/dist/op/activities/work-evidence.d.ts +42 -0
  54. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  55. package/dist/op/activity-contract.d.ts +1 -0
  56. package/dist/op/activity-contract.d.ts.map +1 -1
  57. package/dist/op/activity-profiles.d.ts +16 -0
  58. package/dist/op/activity-profiles.d.ts.map +1 -1
  59. package/dist/op/builders.d.ts +84 -2
  60. package/dist/op/builders.d.ts.map +1 -1
  61. package/dist/op/composites/converge-op.d.ts +31 -4
  62. package/dist/op/composites/converge-op.d.ts.map +1 -1
  63. package/dist/op/decide-backend.d.ts +116 -0
  64. package/dist/op/decide-backend.d.ts.map +1 -0
  65. package/dist/op/decide-config.d.ts +120 -0
  66. package/dist/op/decide-config.d.ts.map +1 -0
  67. package/dist/op/decide-read-inputs.d.ts +40 -0
  68. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  69. package/dist/op/gate-wait.d.ts +43 -0
  70. package/dist/op/gate-wait.d.ts.map +1 -0
  71. package/dist/op/gate.d.ts +5 -2
  72. package/dist/op/gate.d.ts.map +1 -1
  73. package/dist/op/index.d.ts +11 -4
  74. package/dist/op/index.d.ts.map +1 -1
  75. package/dist/op/local-executor.d.ts +29 -4
  76. package/dist/op/local-executor.d.ts.map +1 -1
  77. package/dist/op/local-output.d.ts +2 -0
  78. package/dist/op/local-output.d.ts.map +1 -1
  79. package/dist/op/op-ir.d.ts +2 -0
  80. package/dist/op/op-ir.d.ts.map +1 -1
  81. package/dist/op/op-verb-class.d.ts.map +1 -1
  82. package/dist/op/operator.d.ts +97 -1
  83. package/dist/op/operator.d.ts.map +1 -1
  84. package/dist/op/runtime.d.ts +37 -5
  85. package/dist/op/runtime.d.ts.map +1 -1
  86. package/dist/op/runtimes/local.d.ts.map +1 -1
  87. package/dist/op/source-archive.d.ts +63 -0
  88. package/dist/op/source-archive.d.ts.map +1 -0
  89. package/dist/op/steward-points.d.ts +136 -0
  90. package/dist/op/steward-points.d.ts.map +1 -0
  91. package/dist/op/steward-turn.d.ts +53 -0
  92. package/dist/op/steward-turn.d.ts.map +1 -0
  93. package/dist/op/steward.d.ts +22 -1
  94. package/dist/op/steward.d.ts.map +1 -1
  95. package/dist/op/types.d.ts +7 -0
  96. package/dist/op/types.d.ts.map +1 -1
  97. package/dist/op/work-lease-run.d.ts +1 -1
  98. package/dist/op/work-lease-run.d.ts.map +1 -1
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  100. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  101. package/dist/workspace/answers-ledger.d.ts +76 -0
  102. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  103. package/dist/workspace/box-intent.d.ts +85 -0
  104. package/dist/workspace/box-intent.d.ts.map +1 -0
  105. package/dist/workspace/changes-cli.d.ts +16 -0
  106. package/dist/workspace/changes-cli.d.ts.map +1 -0
  107. package/dist/workspace/changes.d.ts +200 -0
  108. package/dist/workspace/changes.d.ts.map +1 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  118. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  119. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  120. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  121. package/dist/workspace/chant-migrations.d.ts +104 -0
  122. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  123. package/dist/workspace/checks/boxes.d.ts +47 -1
  124. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  125. package/dist/workspace/checks/diagrams.d.ts +31 -0
  126. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  127. package/dist/workspace/checks/records.d.ts +7 -0
  128. package/dist/workspace/checks/records.d.ts.map +1 -1
  129. package/dist/workspace/checks.d.ts +7 -0
  130. package/dist/workspace/checks.d.ts.map +1 -1
  131. package/dist/workspace/compose-graph.d.ts +2 -0
  132. package/dist/workspace/compose-graph.d.ts.map +1 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/decide.d.ts +49 -4
  135. package/dist/workspace/decide.d.ts.map +1 -1
  136. package/dist/workspace/decision-points.schema.json +3 -3
  137. package/dist/workspace/declaration.d.ts +58 -0
  138. package/dist/workspace/declaration.d.ts.map +1 -1
  139. package/dist/workspace/declaration.schema.json +119 -1
  140. package/dist/workspace/declared-kinds.d.ts +20 -0
  141. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  142. package/dist/workspace/generated-files.d.ts +7 -0
  143. package/dist/workspace/generated-files.d.ts.map +1 -1
  144. package/dist/workspace/intent-joins.d.ts +5 -5
  145. package/dist/workspace/intent.d.ts.map +1 -1
  146. package/dist/workspace/lineage-check.d.ts +3 -0
  147. package/dist/workspace/lineage-check.d.ts.map +1 -1
  148. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  149. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  150. package/dist/workspace/ls.d.ts +39 -1
  151. package/dist/workspace/ls.d.ts.map +1 -1
  152. package/dist/workspace/point-answer.schema.json +95 -0
  153. package/dist/workspace/points-cli.d.ts +2 -0
  154. package/dist/workspace/points-cli.d.ts.map +1 -1
  155. package/dist/workspace/points.d.ts +4 -6
  156. package/dist/workspace/points.d.ts.map +1 -1
  157. package/dist/workspace/reason-codes.d.ts +16 -2
  158. package/dist/workspace/reason-codes.d.ts.map +1 -1
  159. package/dist/workspace/records-cli.d.ts +11 -1
  160. package/dist/workspace/records-cli.d.ts.map +1 -1
  161. package/dist/workspace/records-close.d.ts +1 -1
  162. package/dist/workspace/records-close.d.ts.map +1 -1
  163. package/dist/workspace/records-write.d.ts +11 -5
  164. package/dist/workspace/records-write.d.ts.map +1 -1
  165. package/dist/workspace/records.d.ts +34 -4
  166. package/dist/workspace/records.d.ts.map +1 -1
  167. package/dist/workspace/status-gates.d.ts +7 -2
  168. package/dist/workspace/status-gates.d.ts.map +1 -1
  169. package/dist/workspace/status-stewards.d.ts +65 -1
  170. package/dist/workspace/status-stewards.d.ts.map +1 -1
  171. package/dist/workspace/status.d.ts +24 -0
  172. package/dist/workspace/status.d.ts.map +1 -1
  173. package/dist/workspace/work-cli.d.ts +113 -2
  174. package/dist/workspace/work-cli.d.ts.map +1 -1
  175. package/dist/workspace/work-evidence.d.ts +74 -0
  176. package/dist/workspace/work-evidence.d.ts.map +1 -0
  177. package/dist/workspace/work.d.ts +43 -1
  178. package/dist/workspace/work.d.ts.map +1 -1
  179. package/package.json +1 -1
  180. package/src/cli/commands/audit.test.ts +9 -1
  181. package/src/cli/commands/build.ts +3 -2
  182. package/src/cli/commands/lint.ts +7 -1
  183. package/src/cli/handlers/graph.test.ts +20 -0
  184. package/src/cli/handlers/graph.ts +16 -2
  185. package/src/cli/handlers/operator.ts +34 -2
  186. package/src/cli/handlers/run.test.ts +154 -2
  187. package/src/cli/handlers/run.ts +121 -2
  188. package/src/cli/main.test.ts +10 -0
  189. package/src/cli/main.ts +23 -3
  190. package/src/cli/mcp/workspace-plugins.ts +6 -6
  191. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  192. package/src/cli/mcp/workspace-tools.ts +62 -4
  193. package/src/cli/plugins.ts +11 -3
  194. package/src/cli/registry.ts +12 -2
  195. package/src/cli/serve-mcp-workspace.test.ts +6 -6
  196. package/src/codegen/pinned-upgrade.ts +2 -1
  197. package/src/codegen/release-wiring.test.ts +49 -2
  198. package/src/components/capability-plugin-loader.ts +3 -3
  199. package/src/components/cli-support.ts +2 -2
  200. package/src/config.ts +11 -0
  201. package/src/lexicon-module.ts +56 -1
  202. package/src/lexicon-package.test.ts +96 -0
  203. package/src/lifecycle/converge-ledger.ts +20 -0
  204. package/src/lifecycle/run-ledger.ts +6 -0
  205. package/src/lifecycle/symptoms.ts +95 -0
  206. package/src/lint/rules/index.ts +6 -2
  207. package/src/lint/rules/op/index.ts +17 -4
  208. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  209. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  210. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  211. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  212. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  213. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  214. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  215. package/src/op/activities/activity-contracts.ts +190 -1
  216. package/src/op/activities/apply.ts +7 -6
  217. package/src/op/activities/change-coverage.ts +60 -0
  218. package/src/op/activities/converge.test.ts +55 -1
  219. package/src/op/activities/converge.ts +208 -29
  220. package/src/op/activities/decide.live.test.ts +78 -0
  221. package/src/op/activities/decide.test.ts +308 -0
  222. package/src/op/activities/decide.ts +267 -0
  223. package/src/op/activities/index.ts +21 -0
  224. package/src/op/activities/shell.test.ts +91 -1
  225. package/src/op/activities/shell.ts +91 -2
  226. package/src/op/activities/source-release.test.ts +118 -0
  227. package/src/op/activities/source-release.ts +168 -0
  228. package/src/op/activities/source-rollback.test.ts +131 -0
  229. package/src/op/activities/source-rollback.ts +239 -0
  230. package/src/op/activities/work-evidence.ts +62 -0
  231. package/src/op/activity-contract-registry.test.ts +2 -0
  232. package/src/op/activity-contract-registry.ts +2 -2
  233. package/src/op/activity-contract.test.ts +12 -0
  234. package/src/op/activity-contract.ts +6 -1
  235. package/src/op/activity-profiles.ts +29 -0
  236. package/src/op/activity-registry.test.ts +1 -1
  237. package/src/op/activity-registry.ts +2 -2
  238. package/src/op/builders.ts +119 -5
  239. package/src/op/command-gate.test.ts +192 -0
  240. package/src/op/composites/converge-op.test.ts +27 -1
  241. package/src/op/composites/converge-op.ts +69 -15
  242. package/src/op/decide-backend.test.ts +44 -0
  243. package/src/op/decide-backend.ts +212 -0
  244. package/src/op/decide-config.test.ts +52 -0
  245. package/src/op/decide-config.ts +80 -0
  246. package/src/op/decide-read-inputs.ts +117 -0
  247. package/src/op/gate-approval.ts +2 -2
  248. package/src/op/gate-wait.ts +57 -0
  249. package/src/op/gate.ts +8 -3
  250. package/src/op/generate-pipeline.ts +2 -2
  251. package/src/op/index.ts +18 -3
  252. package/src/op/local-executor.ts +151 -15
  253. package/src/op/local-output.test.ts +33 -0
  254. package/src/op/local-output.ts +20 -2
  255. package/src/op/op-ir.test.ts +10 -0
  256. package/src/op/op-ir.ts +4 -0
  257. package/src/op/op-verb-class.ts +1 -0
  258. package/src/op/operator.ts +290 -11
  259. package/src/op/runtime.ts +31 -8
  260. package/src/op/runtimes/local.ts +3 -1
  261. package/src/op/source-archive.ts +161 -0
  262. package/src/op/steward-points.test.ts +364 -0
  263. package/src/op/steward-points.ts +194 -0
  264. package/src/op/steward-turn.ts +79 -0
  265. package/src/op/steward.test.ts +87 -3
  266. package/src/op/steward.ts +25 -1
  267. package/src/op/types.ts +7 -0
  268. package/src/op/work-lease-run.test.ts +20 -1
  269. package/src/op/work-lease-run.ts +2 -2
  270. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  271. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  272. package/src/workspace/answers-ledger.ts +118 -0
  273. package/src/workspace/box-intent.test.ts +205 -0
  274. package/src/workspace/box-intent.ts +159 -0
  275. package/src/workspace/changes-cli.ts +56 -0
  276. package/src/workspace/changes.schema.json +472 -0
  277. package/src/workspace/changes.test.ts +315 -0
  278. package/src/workspace/changes.ts +512 -0
  279. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  280. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  281. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  282. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  283. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  284. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  285. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  286. package/src/workspace/chant-migrations.ts +169 -0
  287. package/src/workspace/check-contract.test.ts +4 -2
  288. package/src/workspace/check.schema.json +19 -5
  289. package/src/workspace/checks/boxes.test.ts +82 -2
  290. package/src/workspace/checks/boxes.ts +202 -1
  291. package/src/workspace/checks/diagrams.test.ts +134 -0
  292. package/src/workspace/checks/diagrams.ts +108 -0
  293. package/src/workspace/checks/records.ts +47 -0
  294. package/src/workspace/checks.ts +20 -2
  295. package/src/workspace/compose-graph.ts +2 -0
  296. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  297. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  298. package/src/workspace/conformance/index.ts +5 -0
  299. package/src/workspace/decide.test.ts +1 -1
  300. package/src/workspace/decide.ts +156 -23
  301. package/src/workspace/decision-points.schema.json +3 -3
  302. package/src/workspace/declaration.schema.json +119 -1
  303. package/src/workspace/declaration.test.ts +58 -0
  304. package/src/workspace/declaration.ts +112 -0
  305. package/src/workspace/declared-kinds.test.ts +31 -5
  306. package/src/workspace/declared-kinds.ts +46 -1
  307. package/src/workspace/generated-files.ts +14 -0
  308. package/src/workspace/graph-cli.ts +1 -1
  309. package/src/workspace/graph.schema.json +9 -1
  310. package/src/workspace/intent-joins.test.ts +5 -5
  311. package/src/workspace/intent-joins.ts +5 -5
  312. package/src/workspace/intent.schema.json +8 -2
  313. package/src/workspace/intent.ts +3 -10
  314. package/src/workspace/lineage-check.ts +16 -1
  315. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  316. package/src/workspace/lineage-upgrade.test.ts +70 -0
  317. package/src/workspace/lineage-upgrade.ts +81 -10
  318. package/src/workspace/ls-contract.test.ts +57 -0
  319. package/src/workspace/ls.schema.json +46 -1
  320. package/src/workspace/ls.ts +62 -3
  321. package/src/workspace/points-cli.ts +17 -3
  322. package/src/workspace/points-write.schema.json +11 -1
  323. package/src/workspace/points.schema.json +51 -0
  324. package/src/workspace/points.test.ts +16 -15
  325. package/src/workspace/points.ts +5 -17
  326. package/src/workspace/read-contract.test.ts +9 -1
  327. package/src/workspace/reason-codes.test.ts +18 -8
  328. package/src/workspace/reason-codes.ts +21 -2
  329. package/src/workspace/records-amend.schema.json +2 -0
  330. package/src/workspace/records-cli.ts +27 -4
  331. package/src/workspace/records-close.schema.json +2 -0
  332. package/src/workspace/records-formats.test.ts +15 -15
  333. package/src/workspace/records-new.schema.json +2 -0
  334. package/src/workspace/records-review.schema.json +2 -0
  335. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  336. package/src/workspace/records-sessions-write.test.ts +2 -34
  337. package/src/workspace/records-since.test.ts +8 -7
  338. package/src/workspace/records-write.e2e.test.ts +108 -0
  339. package/src/workspace/records-write.test.ts +32 -41
  340. package/src/workspace/records-write.ts +33 -15
  341. package/src/workspace/records.schema.json +36 -2
  342. package/src/workspace/records.test.ts +49 -4
  343. package/src/workspace/records.ts +109 -3
  344. package/src/workspace/status-contract.test.ts +14 -8
  345. package/src/workspace/status-gates.ts +9 -4
  346. package/src/workspace/status-stewards.ts +155 -5
  347. package/src/workspace/status.schema.json +129 -8
  348. package/src/workspace/status.ts +46 -1
  349. package/src/workspace/work-acceptance.test.ts +274 -0
  350. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  351. package/src/workspace/work-cli.ts +193 -10
  352. package/src/workspace/work-evidence.schema.json +212 -0
  353. package/src/workspace/work-evidence.ts +169 -0
  354. package/src/workspace/work-history.schema.json +349 -0
  355. package/src/workspace/work-history.test.ts +156 -0
  356. package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
  357. package/src/workspace/work.test.ts +20 -2
  358. package/src/workspace/work.ts +100 -1
  359. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  360. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  361. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * The rollback activities for a release a release Op shipped as a source tree
3
+ * (#2800): plan the rollback from the release ledger, and record it once the
4
+ * site serves the earlier release again.
5
+ *
6
+ * - {@link releaseRollbackPlan}: pick the release to go back to, read its plan
7
+ * from `_plans/` (ws-055), archive the same directory of the same commit
8
+ * again and refuse unless it hashes to the digest that plan recorded, then
9
+ * write a rollback plan named by its own sha256. The rollback gate binds to
10
+ * that digest. The step that restores the Machine reads the archive again,
11
+ * checked against the same digest, before the Machine changes.
12
+ * - {@link releaseRollbackRecord}: append a release record with the restored
13
+ * release's digest and commit, `restores` naming that release, the actor who
14
+ * ran it and the gate's approver, the way `chant components rollback`
15
+ * records one. Nothing is written when the ledger already records it.
16
+ *
17
+ * Which release: by default, the one the site served before the latest
18
+ * release the release Op shipped. A rollback's own record does not count as
19
+ * a release here, so running the rollback again plans the same rollback, its
20
+ * approval still holds, the Machine already serves it, and nothing is
21
+ * recorded twice. `to` names another release by its digest.
22
+ */
23
+
24
+ import { execFileSync } from "node:child_process";
25
+ import { realpathSync } from "node:fs";
26
+ import { join, resolve } from "node:path";
27
+ import type { ReleaseRecord, ReleaseRef } from "../../lifecycle/release-ledger";
28
+ import { archiveSourceTree } from "../source-archive";
29
+ import { readReleasePlan, releasePlan, releasePlanDigest } from "./source-release";
30
+
31
+ // ── releaseRollbackPlan ───────────────────────────────────────────────────────
32
+
33
+ export interface ReleaseRollbackPlanArgs {
34
+ /** The component the releases are recorded under. */
35
+ component: string;
36
+ /** The environment whose ledger records them. */
37
+ env: string;
38
+ /** The digest of the release to go back to. Default: the release the site served before the latest one. */
39
+ to?: string;
40
+ /**
41
+ * The directory the release archived, relative to the Op's working
42
+ * directory, for a plan that does not record `artifact.dir`.
43
+ */
44
+ path?: string;
45
+ /** Where plans are written. Default: `dist/plans`. */
46
+ dir?: string;
47
+ cwd?: string;
48
+ }
49
+
50
+ export interface ReleaseRollbackPlanResult {
51
+ /** `sha256:` of the rollback plan. The rollback gate binds to it. */
52
+ digest: string;
53
+ /** Where the rollback plan was written. */
54
+ file: string;
55
+ /** The release it goes back to: that release's plan digest, which the ledger and the Machine's metadata name. */
56
+ to: string;
57
+ /** The release it replaces. */
58
+ from: string;
59
+ /** The commit the restored release shipped. */
60
+ gitSha: string;
61
+ /** The restored release's source tree, archived again from that commit. */
62
+ archive: string;
63
+ /** Its sha256, the one the restored release's plan recorded. */
64
+ archiveDigest: string;
65
+ /** The directory the archive holds. */
66
+ dir: string;
67
+ }
68
+
69
+ /** A record's reference, the way `restores` and `promotedFrom` name one. */
70
+ const refTo = (r: ReleaseRecord): ReleaseRef => ({ env: r.env, runId: r.runId, timestamp: r.timestamp });
71
+
72
+ /**
73
+ * The release a rollback goes back to, and the release it replaces. Pure.
74
+ *
75
+ * The replaced release is the component's latest record that is not itself a
76
+ * rollback. By default the target is the latest record before it with another
77
+ * digest: what the site served before that release. With `to`, the target is
78
+ * the latest record with that digest.
79
+ */
80
+ export function pickRollback(
81
+ records: ReleaseRecord[],
82
+ component: string,
83
+ env: string,
84
+ to?: string,
85
+ ): { target: ReleaseRecord; replaced: ReleaseRecord } | { error: string } {
86
+ // Ledger order, oldest first; a later append wins a tied timestamp.
87
+ const own = records
88
+ .map((r, i) => ({ r, i }))
89
+ .filter(({ r }) => r.component === component)
90
+ .sort((a, b) => (a.r.timestamp === b.r.timestamp ? a.i - b.i : a.r.timestamp < b.r.timestamp ? -1 : 1))
91
+ .map(({ r }) => r);
92
+ if (own.length === 0) return { error: `no release of "${component}" is recorded in "${env}"` };
93
+ const releasedAt = own.map((r) => !r.restores).lastIndexOf(true);
94
+ if (releasedAt < 0) return { error: `"${component}" has no release in "${env}" that was not itself a rollback` };
95
+ const replaced = own[releasedAt];
96
+ if (to !== undefined) {
97
+ const target = own.filter((r) => r.digest === to).at(-1);
98
+ if (!target) return { error: `no release of "${component}" in "${env}" has digest ${to}` };
99
+ return { target, replaced };
100
+ }
101
+ const target = own.slice(0, releasedAt).filter((r) => r.digest !== replaced.digest).at(-1);
102
+ if (!target) return { error: `"${component}" has no release in "${env}" before ${replaced.digest} to roll back to` };
103
+ return { target, replaced };
104
+ }
105
+
106
+ /** Plan a rollback: the earlier release, its plan, and its source tree archived again and checked against the recorded digest. */
107
+ export async function releaseRollbackPlan(args: ReleaseRollbackPlanArgs): Promise<ReleaseRollbackPlanResult> {
108
+ if (!args.component || !args.env) throw new Error("releaseRollbackPlan: name the component and the environment");
109
+ const cwd = resolve(args.cwd ?? process.cwd());
110
+ const opts = { cwd };
111
+ const { readReleaseLedger } = await import("../../lifecycle/release-ledger");
112
+ const { readReleasePlan: readPersistedPlan } = await import("../../lifecycle/plan-ledger");
113
+
114
+ const picked = pickRollback((await readReleaseLedger(args.env, opts)).records, args.component, args.env, args.to);
115
+ if ("error" in picked) throw new Error(`releaseRollbackPlan: ${picked.error}`);
116
+ const { target, replaced } = picked;
117
+
118
+ // The restored release's plan, as it was approved: its content still hashes to its name.
119
+ const plan = await readPersistedPlan(target.digest, opts);
120
+ if (!plan) throw new Error(`releaseRollbackPlan: no plan is recorded for ${target.digest} (_plans/ on chant/lifecycle), so its source tree is unknown`);
121
+ const planDigest = releasePlanDigest(plan);
122
+ if (planDigest !== target.digest) throw new Error(`releaseRollbackPlan: the plan recorded for ${target.digest} hashes to ${planDigest}`);
123
+ const artifact = plan.artifact as { digest?: unknown; dir?: unknown } | undefined;
124
+ if (typeof artifact?.digest !== "string") throw new Error(`releaseRollbackPlan: the plan for ${target.digest} records no artifact digest`);
125
+ const gitSha = (plan.gitSha as string | undefined) ?? target.gitSha;
126
+
127
+ // The same directory of the same commit, archived again: git archive gives the same bytes.
128
+ let dir: string;
129
+ if (typeof artifact.dir === "string") {
130
+ const root = realpathSync(execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" }).trim());
131
+ dir = join(root, artifact.dir);
132
+ } else if (args.path) {
133
+ dir = args.path;
134
+ } else {
135
+ throw new Error(`releaseRollbackPlan: the plan for ${target.digest} records no artifact.dir; pass path, the directory the release archived`);
136
+ }
137
+ const archive = archiveSourceTree({ dir, ref: gitSha, cwd });
138
+ if (archive.digest !== artifact.digest) {
139
+ throw new Error(
140
+ `releaseRollbackPlan: ${archive.dir} at ${gitSha.slice(0, 12)} archives to ${archive.digest}, not the ${artifact.digest} release ${target.digest} recorded; refusing to restore it`,
141
+ );
142
+ }
143
+
144
+ const written = await releasePlan({
145
+ component: args.component,
146
+ env: args.env,
147
+ gitSha,
148
+ content: {
149
+ rollback: { to: target.digest, from: replaced.digest, restores: refTo(target) },
150
+ artifact: { ...(plan.artifact as Record<string, unknown>), digest: archive.digest, dir: archive.dir },
151
+ },
152
+ ...(args.dir ? { dir: args.dir } : {}),
153
+ cwd,
154
+ });
155
+ return {
156
+ digest: written.digest,
157
+ file: written.file,
158
+ to: target.digest,
159
+ from: replaced.digest,
160
+ gitSha,
161
+ archive: archive.archive,
162
+ archiveDigest: archive.digest,
163
+ dir: archive.dir,
164
+ };
165
+ }
166
+
167
+ // ── releaseRollbackRecord ─────────────────────────────────────────────────────
168
+
169
+ export interface ReleaseRollbackRecordArgs {
170
+ /** The rollback plan `releaseRollbackPlan` wrote. */
171
+ plan: string;
172
+ /** The digest the gate approved. The plan must be that one. */
173
+ digest?: string;
174
+ /** Who approved it: the resolution bound to this digest on this Op's gate is looked up. */
175
+ approval?: { op: string; gate: string };
176
+ /** Who ran the rollback. Default: GITHUB_ACTOR, GITLAB_USER_LOGIN, USER. */
177
+ actor?: string;
178
+ runId?: string;
179
+ cwd?: string;
180
+ }
181
+
182
+ export interface ReleaseRollbackRecordResult {
183
+ /** False when the ledger already recorded this rollback. */
184
+ recorded: boolean;
185
+ /** The restored release's digest, which the record carries. */
186
+ digest: string;
187
+ env: string;
188
+ component: string;
189
+ approver: string | null;
190
+ }
191
+
192
+ /** Record a rollback in the release ledger, once. */
193
+ export async function releaseRollbackRecord(args: ReleaseRollbackRecordArgs): Promise<ReleaseRollbackRecordResult> {
194
+ const cwd = args.cwd;
195
+ const opts = cwd ? { cwd } : undefined;
196
+ const plan = readReleasePlan(resolve(cwd ?? process.cwd(), args.plan), args.digest);
197
+ const rollback = plan.rollback as { to?: string; restores?: ReleaseRef } | undefined;
198
+ if (!rollback?.to || !rollback.restores) throw new Error(`releaseRollbackRecord: ${args.plan} is not a rollback plan (no rollback.to)`);
199
+ const component = plan.component as string;
200
+ const env = plan.env as string;
201
+ const gitSha = plan.gitSha as string;
202
+
203
+ const { readReleaseLedger, latestPerComponent, appendReleaseRecord, resolveRunId } = await import("../../lifecycle/release-ledger");
204
+ const { pushLifecycle } = await import("../../lifecycle/git");
205
+
206
+ let approver: string | null = null;
207
+ if (args.approval) {
208
+ const { readGateLedger } = await import("../../lifecycle/gate-ledger");
209
+ const { resolutions } = await readGateLedger(args.approval.op, opts);
210
+ const bound = resolutions.filter((r) => r.gate === args.approval!.gate && r.planDigest === plan.digest);
211
+ approver = bound.length > 0 ? [...new Set(bound.map((r) => r.resolvedBy))].join(", ") : null;
212
+ }
213
+
214
+ const latest = latestPerComponent((await readReleaseLedger(env, opts)).records.filter((r) => r.component === component)).get(component);
215
+ if (latest?.digest === rollback.to && latest.restores?.runId === rollback.restores.runId && latest.restores.timestamp === rollback.restores.timestamp) {
216
+ return { recorded: false, digest: rollback.to, env, component, approver };
217
+ }
218
+
219
+ const actor = args.actor || process.env.GITHUB_ACTOR || process.env.GITLAB_USER_LOGIN || process.env.USER;
220
+ if (!actor) throw new Error("releaseRollbackRecord: no actor: pass one, or set GITHUB_ACTOR, GITLAB_USER_LOGIN or USER");
221
+ const { runId, runOrigin } = resolveRunId(args.runId);
222
+ await appendReleaseRecord(
223
+ {
224
+ component,
225
+ env,
226
+ digest: rollback.to,
227
+ gitSha,
228
+ runId,
229
+ ...(runOrigin ? { runOrigin } : {}),
230
+ timestamp: new Date().toISOString(),
231
+ actor,
232
+ ...(approver ? { approver } : {}),
233
+ restores: rollback.restores,
234
+ },
235
+ opts,
236
+ );
237
+ await pushLifecycle(opts);
238
+ return { recorded: true, digest: rollback.to, env, component, approver };
239
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `workEvidence` (#2772): attach one piece of evidence to an acceptance
3
+ * criterion of the work item the run holds the lease on.
4
+ *
5
+ * Meant for an Op that declares `workLease` (#2748), a steward's among them:
6
+ * the builder passes the run's lease (`workLeaseOutput()`) as `lease`, and the
7
+ * write goes through only while that lease is still the run's, under its
8
+ * token. The rules are `attachWorkEvidence`'s (`../../workspace/work-evidence.ts`):
9
+ * the criterion must be one the record lists, and a `manual` criterion is
10
+ * refused, since the run holding the lease is the item's implementer.
11
+ * A refusal fails the step with its code.
12
+ */
13
+
14
+ import type { EvidenceResult, WorkAcceptance } from "../../workspace/work";
15
+
16
+ export interface WorkEvidenceArgs {
17
+ /** The run's work lease: `workLeaseOutput()`, which the builder passes. */
18
+ lease: { item: string; holder: string; token: string };
19
+ /** The criterion the evidence is for, by id. */
20
+ criterion: string;
21
+ result: EvidenceResult;
22
+ title: string;
23
+ /** A public https:// link, or */
24
+ url?: string;
25
+ /** a workspace file, pinned by the hash of its bytes now. */
26
+ path?: string;
27
+ /** The work kind file, when the declaration names more than one with the item. */
28
+ kind?: string;
29
+ /** Where the workspace is found; the run's directory by default. */
30
+ cwd?: string;
31
+ }
32
+
33
+ export interface WorkEvidenceResult {
34
+ item: string;
35
+ /** The work record, from the repository root. */
36
+ path: string;
37
+ evidence: Record<string, unknown>;
38
+ acceptance: WorkAcceptance;
39
+ }
40
+
41
+ export async function workEvidence(args: WorkEvidenceArgs): Promise<WorkEvidenceResult> {
42
+ const lease = args.lease;
43
+ if (!lease || typeof lease.item !== "string" || typeof lease.holder !== "string" || typeof lease.token !== "string") {
44
+ throw new Error("workEvidence runs under a work lease: pass the run's lease as `lease` (workLeaseOutput()) from an Op that declares workLease");
45
+ }
46
+ const { attachWorkEvidence } = await import("../../workspace/work-evidence");
47
+ const out = await attachWorkEvidence({
48
+ cwd: args.cwd ?? process.cwd(),
49
+ item: lease.item,
50
+ holder: lease.holder,
51
+ token: lease.token,
52
+ criterion: args.criterion,
53
+ result: args.result,
54
+ title: args.title,
55
+ ...(args.url !== undefined ? { url: args.url } : {}),
56
+ ...(args.path !== undefined ? { path: args.path } : {}),
57
+ ...(args.kind !== undefined ? { kind: args.kind } : {}),
58
+ });
59
+ if ("error" in out) throw new Error(`workEvidence refused for ${lease.item} ${args.criterion}: ${out.error.code}: ${out.error.message}`);
60
+ console.log(`workEvidence: ${out.item} ${args.criterion} ${args.result} (${out.acceptance.met}/${out.acceptance.total} met)`);
61
+ return { item: out.item, path: out.path, evidence: out.evidence, acceptance: out.acceptance };
62
+ }
@@ -12,6 +12,8 @@ describe("loadActivityContracts", () => {
12
12
  expect(contracts.get("predictBehaviour")?.name).toBe("predictBehaviour");
13
13
  expect(contracts.get("behaviourFinding")?.returns).toBeDefined();
14
14
  expect(contracts.get("lifecycleDiff")?.name).toBe("lifecycleDiff");
15
+ // The forward coverage check over an Op's own diff (#2773).
16
+ expect(contracts.get("changeCoverage")?.returns).toBeDefined();
15
17
  });
16
18
 
17
19
  test("a lexicon's contracts are resolved by the same subpath convention loadActivities uses", async () => {
@@ -36,7 +36,7 @@
36
36
 
37
37
  import * as coreContracts from "./activities/activity-contracts";
38
38
  import { collectActivityContracts, type ActivityContract } from "./activity-contract";
39
- import { importLexiconModule } from "../lexicon-module";
39
+ import { importLexiconModule, importLexiconPackage } from "../lexicon-module";
40
40
 
41
41
  /**
42
42
  * The shape {@link loadActivityContracts} reads off a plugin: an optional
@@ -89,7 +89,7 @@ export async function loadActivityContracts(
89
89
  );
90
90
  } else {
91
91
  const spec = `@intentius/chant-lexicon-${name}/op/activity-contracts`;
92
- collectActivityContracts((await import(spec)) as Record<string, unknown>, contracts);
92
+ collectActivityContracts((await importLexiconPackage(spec)) as Record<string, unknown>, contracts);
93
93
  }
94
94
  } catch {
95
95
  // Lexicon absent or declares no contracts at the conventional subpath.
@@ -67,6 +67,18 @@ function opWith(config: Partial<OpConfig> & Pick<OpConfig, "phases">): Pick<OpCo
67
67
  return { name: "test-op", ...config };
68
68
  }
69
69
 
70
+ describe("validateActivitySteps() — a step's own timeout (#2787)", () => {
71
+ it("accepts a duration up to six hours, and flags one that is not a duration, is zero, or is longer", () => {
72
+ const withTimeout = (timeout: string) => opWith({ phases: [phase("Build", [activity("lifecycleDiff", { env: "prod" }, { timeout })])] });
73
+ const contracts = new Map([["lifecycleDiff", lifecycleDiffContract]]);
74
+ for (const ok of ["45m", "1h30m", "6h"]) expect(validateActivitySteps(withTimeout(ok), contracts), ok).toEqual([]);
75
+ for (const [bad, why] of [["forever", /not a duration/], ["45 minutes", /not a duration/], ["0m", /more than zero/], ["7h", /longer than a step may run \(6h\)/]] as const) {
76
+ const issues = validateActivitySteps(withTimeout(bad), contracts);
77
+ expect(issues.map((i) => i.message), bad).toEqual([expect.stringMatching(why)]);
78
+ }
79
+ });
80
+ });
81
+
70
82
  describe("validateActivitySteps() — passing Ops", () => {
71
83
  it("returns no issues for a step matching its contract exactly", () => {
72
84
  const config = opWith({ phases: [phase("Diff", [activity("lifecycleDiff", { env: "prod", live: true })])] });
@@ -41,7 +41,7 @@
41
41
  import { z } from "zod";
42
42
  import { outcomeAttributesOf } from "./types";
43
43
  import type { OpConfig, PhaseDefinition, ActivityStep, StepDefinition } from "./types";
44
- import { ACTIVITY_PROFILE_NAMES } from "./activity-profiles";
44
+ import { ACTIVITY_PROFILE_NAMES, stepTimeoutProblem } from "./activity-profiles";
45
45
 
46
46
  /**
47
47
  * Every literal value {@link ActivityStep.profile} may hold, read off
@@ -237,6 +237,7 @@ export function primitiveKindOf(schema: z.ZodTypeAny): PrimitiveSchemaKind | und
237
237
  *
238
238
  * Catches the four failure classes chant #1288 names:
239
239
  * - an unrecognized `profile` (checked against {@link KNOWN_ACTIVITY_PROFILES}, independent of whether `fn` has a contract),
240
+ * - a step's own `timeout` that is not a duration, or is longer than `MAX_STEP_TIMEOUT` (#2787),
240
241
  * - an args key the declared schema doesn't recognize,
241
242
  * - an args value of the wrong type (including a required key that's missing),
242
243
  * - an `outcomeAttribute.from` path that can't exist on the declared return type.
@@ -258,6 +259,10 @@ export function validateActivitySteps(
258
259
  message: `unknown profile "${step.profile}" (known: ${KNOWN_ACTIVITY_PROFILES.join(", ")})`,
259
260
  });
260
261
  }
262
+ if (step.timeout !== undefined) {
263
+ const problem = stepTimeoutProblem(step.timeout);
264
+ if (problem) issues.push({ opName: config.name, phase: phase.name, fn: step.fn, message: problem });
265
+ }
261
266
 
262
267
  const contract = contracts.get(step.fn);
263
268
  if (!contract) continue;
@@ -15,6 +15,8 @@
15
15
  * heartbeats to anything.
16
16
  */
17
17
 
18
+ import { parseDuration } from "./duration";
19
+
18
20
  export interface ActivityProfile {
19
21
  /** Maximum time allowed for a single activity execution attempt (e.g. `"20m"`). */
20
22
  timeout: string;
@@ -132,3 +134,30 @@ export type ActivityProfileName = keyof typeof ACTIVITY_PROFILES;
132
134
 
133
135
  /** Every profile name, as a runtime list (`KNOWN_ACTIVITY_PROFILES`'s source). */
134
136
  export const ACTIVITY_PROFILE_NAMES = Object.keys(ACTIVITY_PROFILES) as ActivityProfileName[];
137
+
138
+ /**
139
+ * The longest timeout one step may declare for itself (#2787): six hours, the
140
+ * ceiling hosted CI puts on a single job. A step's own `timeout` replaces its
141
+ * profile's and keeps the profile's retries, so a builder `shell` step can
142
+ * run 45 minutes on its one attempt. Anything that waits longer is waiting on
143
+ * someone, which is a gate's job (`humanGate`, 48 hours), not a step's.
144
+ */
145
+ export const MAX_STEP_TIMEOUT = "6h";
146
+
147
+ /** {@link MAX_STEP_TIMEOUT} in milliseconds. */
148
+ export const MAX_STEP_TIMEOUT_MS = 6 * 3_600_000;
149
+
150
+ /**
151
+ * Why `timeout` can't be a step's own timeout, or null when it can: it must
152
+ * be a duration such as `45m` or `1h30m`, more than zero, and at most
153
+ * {@link MAX_STEP_TIMEOUT}.
154
+ */
155
+ export function stepTimeoutProblem(timeout: unknown): string | null {
156
+ if (typeof timeout !== "string" || !/^(\d+(ms|s|m|h|d))+$/.test(timeout)) {
157
+ return `timeout ${JSON.stringify(timeout)} is not a duration such as "45m" or "1h30m"`;
158
+ }
159
+ const ms = parseDuration(timeout);
160
+ if (ms <= 0) return `timeout "${timeout}" must be more than zero`;
161
+ if (ms > MAX_STEP_TIMEOUT_MS) return `timeout "${timeout}" is longer than a step may run (${MAX_STEP_TIMEOUT}); a longer wait is a gate's`;
162
+ return null;
163
+ }
@@ -26,7 +26,7 @@ describe("loadActivities", () => {
26
26
  "shellCmd", "chantBuild", "waitForStack", "lifecycleSnapshot", "lifecycleDiff",
27
27
  "httpCheck", "chantTeardown", "envTeardown", "convergeTick", "reconcilePr",
28
28
  "guardValidate", "policyGate", "workflowSupplyChainAudit", "pipelineSupplyChainAudit",
29
- "lexiconUpgrade", "nativeApply",
29
+ "lexiconUpgrade", "nativeApply", "changeCoverage",
30
30
  ]) {
31
31
  expect(typeof a.get(fn), `${fn} should resolve from the base library`).toBe("function");
32
32
  }
@@ -12,7 +12,7 @@
12
12
 
13
13
  import * as baseActivities from "./activities";
14
14
  import { ACTIVITY_PROFILES, type ActivityProfile } from "./activity-profiles";
15
- import { importLexiconModule } from "../lexicon-module";
15
+ import { importLexiconModule, importLexiconPackage } from "../lexicon-module";
16
16
 
17
17
  export type { ActivityProfile } from "./activity-profiles";
18
18
 
@@ -64,7 +64,7 @@ export async function loadActivities(lexicons: string[] = []): Promise<Map<strin
64
64
  continue;
65
65
  }
66
66
  const spec = `@intentius/chant-lexicon-${name}/op/activities`;
67
- collectActivities((await import(spec)) as Record<string, unknown>, activities);
67
+ collectActivities((await importLexiconPackage(spec)) as Record<string, unknown>, activities);
68
68
  } catch {
69
69
  // Lexicon absent or contributes no activities — fine.
70
70
  }
@@ -1,10 +1,10 @@
1
1
  import { OpResource } from "./resource";
2
2
  import { workLeaseProblems } from "./work-lease-decl";
3
- import type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep, EffectStep } from "./types";
3
+ import { WORK_LEASE_STEP_ID, type OpConfig, type PhaseDefinition, type StepDefinition, type ActivityStep, type GateStep, type EffectStep } from "./types";
4
4
  import { isEffectReceipt, type EffectReceiptDeclaration } from "../effect-receipt";
5
5
  import { receiptCheckInput } from "./receipt-store";
6
6
  import { gateApprovalProblems } from "./gate-approval";
7
- import { makeOutProxy, type StepOutputRef, type WithStepRefs } from "./step-output-ref";
7
+ import { makeOutProxy, stepOutput, type StepOutputRef, type WithStepRefs } from "./step-output-ref";
8
8
  import type { ChantBuildArgs } from "./activities/build";
9
9
  import type { ShellCmdArgs } from "./activities/shell";
10
10
  import type { WaitForStackArgs } from "./activities/wait";
@@ -12,8 +12,12 @@ import type { LifecycleSnapshotArgs } from "./activities/lifecycle";
12
12
  import type { ChantTeardownArgs } from "./activities/teardown";
13
13
  import type { EnvTeardownArgs } from "./activities/env-teardown";
14
14
  import type { HttpCheckArgs } from "./activities/http-check";
15
+ import type { SourceArchiveArgs, ReleasePlanArgs, ReleaseRecordArgs } from "./activities/source-release";
16
+ import type { ReleaseRollbackPlanArgs, ReleaseRollbackRecordArgs } from "./activities/source-rollback";
15
17
  import type { PolicyGateArgs } from "./activities/policy";
16
18
  import type { GuardValidateArgs } from "./activities/guard-validate";
19
+ import type { WorkEvidenceArgs } from "./activities/work-evidence";
20
+ import type { DecideArgs } from "./activities/decide";
17
21
  import { isValidCronExpression, cronSyntaxMessage } from "./cron";
18
22
 
19
23
  /** An `activity()` result — the plain `ActivityStep` shape plus the `.out` reference sugar (#1290). */
@@ -82,16 +86,18 @@ export function phase(
82
86
  export function activity(
83
87
  fn: string,
84
88
  args?: Record<string, unknown>,
85
- opts?: ActivityStep["profile"] | { profile?: ActivityStep["profile"]; id?: string },
89
+ opts?: ActivityStep["profile"] | { profile?: ActivityStep["profile"]; id?: string; timeout?: string },
86
90
  ): NamedActivityStep {
87
91
  const profile = typeof opts === "string" ? opts : opts?.profile;
88
92
  const id = typeof opts === "string" ? undefined : opts?.id;
93
+ const timeout = typeof opts === "string" ? undefined : opts?.timeout;
89
94
  const step = {
90
95
  kind: "activity",
91
96
  fn,
92
97
  ...(args && Object.keys(args).length > 0 ? { args } : {}),
93
98
  ...(profile ? { profile } : {}),
94
99
  ...(id ? { id } : {}),
100
+ ...(timeout !== undefined ? { timeout } : {}),
95
101
  } as NamedActivityStep;
96
102
  Object.defineProperty(step, "out", {
97
103
  enumerable: false,
@@ -337,6 +343,20 @@ export const lifecycleSnapshot = (env: string, opts?: { id?: string }): NamedAct
337
343
  * shell("./smoke.sh", { env: { HOST: host.out.stdout } });
338
344
  * ```
339
345
  *
346
+ * With `json: true`, stdout is parsed as JSON and published as `json`
347
+ * (#2787), so a list or an object reaches the next step or the Op's work
348
+ * lease as itself. A pick step that prints `["W-3","W-7"]` hands
349
+ * `workLease.item` its candidates:
350
+ *
351
+ * ```ts
352
+ * const pick = shell("node pick.mjs", { id: "pick", json: true });
353
+ * Op({ workLease: { item: pick.out.json }, ... });
354
+ * ```
355
+ *
356
+ * `timeout` sets how long the one attempt may run, in place of the profile's
357
+ * twenty minutes, and keeps the profile's retries: `shell("./build.sh", {
358
+ * timeout: "45m" })`. At most six hours (`MAX_STEP_TIMEOUT`).
359
+ *
340
360
  * `cmd` stays a plain `string` and takes no references. A value spliced into
341
361
  * a command line is a quoting decision chant would then be making on the
342
362
  * author's behalf, and `env` carries the same value into the same command
@@ -344,10 +364,88 @@ export const lifecycleSnapshot = (env: string, opts?: { id?: string }): NamedAct
344
364
  */
345
365
  export const shell = (
346
366
  cmd: string,
347
- opts?: WithStepRefs<Omit<ShellCmdArgs, "cmd">> & StepOpts,
367
+ opts?: WithStepRefs<Omit<ShellCmdArgs, "cmd">> & StepOpts & { timeout?: string },
368
+ ): NamedActivityStep => {
369
+ const { timeout, ...rest } = (opts ?? {}) as Record<string, unknown> & { timeout?: string };
370
+ const { args, profile, id } = takeProfileAndId(rest);
371
+ return activity("shellCmd", { cmd, ...args }, { profile: profile ?? "atMostOnce", ...(id ? { id } : {}), ...(timeout !== undefined ? { timeout } : {}) });
372
+ };
373
+
374
+ /**
375
+ * Archive one directory of HEAD as a release artifact (#2782): a tar written by
376
+ * `git archive`, named by its sha256, the same bytes for the same commit.
377
+ * Defaults to the `fastIdempotent` profile.
378
+ *
379
+ * ```ts
380
+ * const archive = sourceArchive("../app", { id: "archive" });
381
+ * ```
382
+ */
383
+ export const sourceArchive = (
384
+ path: string,
385
+ opts?: WithStepRefs<Omit<SourceArchiveArgs, "path">> & StepOpts,
348
386
  ): NamedActivityStep => {
349
387
  const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
350
- return activity("shellCmd", { cmd, ...args }, { profile: profile ?? "atMostOnce", ...(id ? { id } : {}) });
388
+ return activity("sourceArchive", { path, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
389
+ };
390
+
391
+ /**
392
+ * Plan a release (#2782): a JSON object named by the sha256 of its canonical
393
+ * form, which the ship gate binds to (`gate("ship", { plan: plan.out.digest })`)
394
+ * and the release ledger records. Defaults to the `fastIdempotent` profile.
395
+ */
396
+ export const releasePlan = (args: WithStepRefs<ReleasePlanArgs> & StepOpts): NamedActivityStep => {
397
+ const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
398
+ return activity("releasePlan", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
399
+ };
400
+
401
+ /**
402
+ * Record a release in the release ledger with its plan (#2782, ws-055), once:
403
+ * a retry whose release the ledger already names records nothing. Defaults to
404
+ * the `fastIdempotent` profile.
405
+ */
406
+ export const releaseRecord = (args: WithStepRefs<ReleaseRecordArgs> & StepOpts): NamedActivityStep => {
407
+ const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
408
+ return activity("releaseRecord", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
409
+ };
410
+
411
+ /**
412
+ * Ask a decision point and record its answer (ws-058, #2740): the `decide`
413
+ * activity, core's since #2828. `opts` is the activity's own
414
+ * {@link DecideArgs}, minus the positional `point`, so the builder and the
415
+ * activity cannot drift. Defaults to the `fastIdempotent` profile: asking
416
+ * again with the same point, declaration and inputs returns the record
417
+ * already written. An open question stops the run `waiting`.
418
+ *
419
+ * ```ts
420
+ * decide("slice-tier", { read: { "work-item": "W-002" }, subject: "W-002" })
421
+ * ```
422
+ */
423
+ export const decide = (point: string, opts?: WithStepRefs<Omit<DecideArgs, "point">> & StepOpts): NamedActivityStep => {
424
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
425
+ return activity("decide", { point, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
426
+ };
427
+
428
+ /**
429
+ * Plan a rollback of a source release (#2800): the release the site served
430
+ * before the latest one (or `to`), its plan read back from the ledger, and its
431
+ * source tree archived again from its commit and refused unless it hashes to
432
+ * the digest that plan recorded. Writes a rollback plan named by its own
433
+ * sha256, which the rollback gate binds to (`gate("rollback", { plan:
434
+ * plan.out.digest })`). Defaults to the `fastIdempotent` profile.
435
+ */
436
+ export const releaseRollbackPlan = (args: WithStepRefs<ReleaseRollbackPlanArgs> & StepOpts): NamedActivityStep => {
437
+ const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
438
+ return activity("releaseRollbackPlan", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
439
+ };
440
+
441
+ /**
442
+ * Record a rollback in the release ledger (#2800): the restored release's
443
+ * digest and commit, `restores` naming that release, the actor and the gate's
444
+ * approver, once. Defaults to the `fastIdempotent` profile.
445
+ */
446
+ export const releaseRollbackRecord = (args: WithStepRefs<ReleaseRollbackRecordArgs> & StepOpts): NamedActivityStep => {
447
+ const { args: rest, profile, id } = takeProfileAndId(args as Record<string, unknown>);
448
+ return activity("releaseRollbackRecord", rest, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
351
449
  };
352
450
 
353
451
  /**
@@ -924,6 +1022,22 @@ export const policyGate = (opts?: WithStepRefs<PolicyGateArgs> & { id?: string }
924
1022
  );
925
1023
  };
926
1024
 
1025
+ /**
1026
+ * Attach evidence to an acceptance criterion of the work item the run holds
1027
+ * the lease on (#2772), for an Op that declares `workLease`. The run's lease
1028
+ * is passed for it, so the write happens only while the lease is still the
1029
+ * run's. `opts` takes the `result` (pass or fail), a `title`, and a `url` or a
1030
+ * workspace `path` to pin by hash. A `manual` criterion is refused: the run
1031
+ * is the item's implementer. The `atMostOnce` profile: each attempt appends.
1032
+ */
1033
+ export const workEvidence = (
1034
+ criterion: string,
1035
+ opts: WithStepRefs<Omit<WorkEvidenceArgs, "criterion" | "lease">> & { id?: string },
1036
+ ): NamedActivityStep => {
1037
+ const { id, ...rest } = opts as Omit<WorkEvidenceArgs, "criterion" | "lease"> & { id?: string };
1038
+ return activity("workEvidence", { criterion, lease: stepOutput(WORK_LEASE_STEP_ID), ...rest }, { profile: "atMostOnce", ...(id ? { id } : {}) });
1039
+ };
1040
+
927
1041
  /**
928
1042
  * Run an external CloudFormation Guard rules pack against the project's built
929
1043
  * CloudFormation template (chant #522). `opts` is {@link GuardValidateArgs}