@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
@@ -4,16 +4,16 @@
4
4
  * scheduled Ops under its own lease, and the rule that a steward turn and a
5
5
  * coding agent share one checkout without colliding.
6
6
  */
7
- import { describe, test, expect } from "vitest";
7
+ import { describe, test, expect, vi } from "vitest";
8
8
  import { withTestDir } from "@intentius/chant-test-utils";
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import type { ActivityFn, ActivityProfile } from "./activity-registry";
13
13
  import type { OpConfig } from "./types";
14
- import { declareSteward, isStewardDeclaration, pickSteward, stewardFormFor, stewardLeaseName } from "./steward";
14
+ import { declareSteward, isStewardDeclaration, pickSteward, stewardFormFor, stewardLeaseName, stewardTurnLeaseName } from "./steward";
15
15
  import { discoverOps, discoverStewards } from "./discover";
16
- import { runOperatorRound, acquireStewardLease, formatRoundLine } from "./operator";
16
+ import { runOperatorRound, acquireStewardLease, acquireStewardTurn, formatRoundLine } from "./operator";
17
17
  import { readLease } from "../lifecycle/lease";
18
18
  import { readRunLedger } from "../lifecycle/run-ledger";
19
19
 
@@ -210,3 +210,87 @@ describe("a local steward round", () => {
210
210
  });
211
211
  });
212
212
  });
213
+
214
+ describe("a hand run waits its turn (#2750)", () => {
215
+ test("a round holds the turn lease for exactly the length of its tick — a concurrent hand run sees it held, then free again once the tick ends", async () => {
216
+ await withTestDir(async (dir) => {
217
+ await initRepo(dir);
218
+ let releaseTurn: () => void = () => {};
219
+ const gate = new Promise<void>((resolve) => { releaseTurn = resolve; });
220
+ const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *", "slowTurn")] });
221
+ const activities = new Map<string, ActivityFn>([
222
+ ["slowTurn", async () => { await gate; return { ok: true }; }],
223
+ ]);
224
+ // Real time on both sides here, deliberately: a fixed `now` on only one
225
+ // of the round's and the hand run's leases would make one see the
226
+ // other's lease as already expired by wall-clock time, which is not
227
+ // the race this test is about.
228
+ const roundPromise = runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities, profiles: PROFILES });
229
+
230
+ // Wait for the round to actually be mid-tick, holding the turn lease.
231
+ await vi.waitFor(async () => {
232
+ const turn = await readLease(stewardTurnLeaseName("box-steward"), { cwd: dir });
233
+ expect(turn.record?.holder).toBe("steward-proc");
234
+ });
235
+
236
+ // A hand run's own attempt (no `waitMs`) sees it taken and gives up
237
+ // without retrying — this is what `chant run <op>` names in its refusal.
238
+ const midTurn = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
239
+ expect(midTurn.acquired).toBe(false);
240
+ expect(midTurn.heldBy?.holder).toBe("steward-proc");
241
+
242
+ releaseTurn();
243
+ const events = await roundPromise;
244
+ expect(events.map((e) => e.kind)).toEqual(["ticked"]);
245
+
246
+ // The tick released the turn the moment it finished — succeeds again
247
+ // right away, not only once the whole steward process stops.
248
+ const afterTurn = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
249
+ expect(afterTurn.acquired).toBe(true);
250
+ });
251
+ });
252
+
253
+ test("a hand run's turn blocks a round: it reports turn-busy and ticks nothing, and releases the op lease it had just taken", async () => {
254
+ await withTestDir(async (dir) => {
255
+ await initRepo(dir);
256
+ const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
257
+ const held = await acquireStewardTurn("box-steward", "hand-run", { cwd: dir });
258
+ expect(held.acquired).toBe(true);
259
+
260
+ const log: string[] = [];
261
+ const now = () => new Date(2026, 8, 25, 10, 1, 0);
262
+ const events = await runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities: turns(log), profiles: PROFILES, now });
263
+
264
+ expect(events).toEqual([{ kind: "turn-busy", op: "box-converge", env: "local", steward: "box-steward", heldBy: "hand-run" }]);
265
+ expect(formatRoundLine(events[0])).toContain("turn-held:hand-run");
266
+ expect(log).toEqual([]);
267
+ // The op's own lease, freshly acquired before the turn check, was let go again.
268
+ expect((await readLease("box-converge", { cwd: dir })).record).toBeUndefined();
269
+ });
270
+ });
271
+
272
+ test("a stale .lock on the turn ref reports lease-error, never a thrown exception out of the round, and still releases the op lease it had just taken (#1959 finding 2, applied to #2750's turn lease)", async () => {
273
+ await withTestDir(async (dir) => {
274
+ await initRepo(dir);
275
+ const steward = declareSteward({ name: "box-steward", ops: [op("box-converge", "* * * * *")] });
276
+
277
+ // A previous holder of the turn lease was killed mid-write — its
278
+ // `.lock` file is still sitting there, blocking every future write to
279
+ // `_turns/box-steward`, the same way #1959 finding 2 covers the
280
+ // per-op lease.
281
+ mkdirSync(join(dir, ".git", "refs", "chant", "lease", "_turns"), { recursive: true });
282
+ writeFileSync(join(dir, ".git", "refs", "chant", "lease", "_turns", "box-steward.lock"), "");
283
+
284
+ const log: string[] = [];
285
+ const now = () => new Date(2026, 8, 25, 10, 1, 0);
286
+ const events = await runOperatorRound({ cwd: dir, holder: "steward-proc", steward, activities: turns(log), profiles: PROFILES, now });
287
+
288
+ expect(events).toHaveLength(1);
289
+ expect(events[0]).toMatchObject({ kind: "lease-error", op: "_turns/box-steward", env: "local" });
290
+ expect((events[0] as { error: string }).error).toContain("box-steward.lock");
291
+ expect(log).toEqual([]);
292
+ // The op's own lease, freshly acquired before the failed turn attempt, was let go again.
293
+ expect((await readLease("box-converge", { cwd: dir })).record).toBeUndefined();
294
+ });
295
+ });
296
+ });
package/src/op/steward.ts CHANGED
@@ -41,7 +41,8 @@
41
41
  * ## What a steward writes
42
42
  *
43
43
  * A steward's writes are chant writes: run, converge and gate records on the
44
- * `chant/lifecycle` branch and lease refs under `refs/chant/lease/`, all made
44
+ * `chant/lifecycle` branch, the answer records of the decision points its
45
+ * Ops ask (`_answers/`, #2786), and lease refs under `refs/chant/lease/`, all made
45
46
  * with git plumbing (`../lifecycle/git.ts`: `hash-object`, `mktree`,
46
47
  * `commit-tree`, `update-ref`). None of them reads or writes the checkout's
47
48
  * index or working tree, so a coding agent editing the app in the same
@@ -233,6 +234,29 @@ export function stewardLeaseName(steward: string): string {
233
234
  return `_stewards/${steward}`;
234
235
  }
235
236
 
237
+ /**
238
+ * The lease a local steward holds for as long as one of its Ops is actually
239
+ * running: one turn (#2750). Distinct from {@link stewardLeaseName}, which one
240
+ * `chant operator --steward` process holds for its whole life (renewed
241
+ * between ticks, released only when the process stops) — this one is taken
242
+ * right before a run starts and released right after it ends, whether the
243
+ * run is a round's scheduled tick or `chant run <op>` typed by hand. That is
244
+ * what makes an on-request run wait its turn behind a turn already in
245
+ * progress, and succeed again the moment that turn ends, rather than only
246
+ * once the whole steward process stops.
247
+ *
248
+ * `_turns/<name>`, not `_stewards/<name>/turn`: a git ref name is a path
249
+ * component in the loose-refs tree, and `refs/chant/lease/_stewards/<name>`
250
+ * (the lease {@link stewardLeaseName} names) already exists as a *leaf* —
251
+ * git refuses to also create anything *under* it (`fatal: ... exists;
252
+ * cannot create ...`), so nesting the turn lease inside the steward lease's
253
+ * own name is unusable the moment both are live at once. A sibling
254
+ * top-level segment sidesteps that entirely.
255
+ */
256
+ export function stewardTurnLeaseName(steward: string): string {
257
+ return `_turns/${steward}`;
258
+ }
259
+
236
260
  /**
237
261
  * Pick the steward `--steward [<name>]` names, or the project's only one.
238
262
  * Returns the steward, or the message to refuse with.
package/src/op/types.ts CHANGED
@@ -153,6 +153,13 @@ export interface ActivityStep {
153
153
  * Default: "fastIdempotent"
154
154
  */
155
155
  profile?: ActivityProfileName;
156
+ /**
157
+ * How long one attempt may run, such as `"45m"`, in place of the
158
+ * profile's timeout (#2787). The profile's retry policy is kept, so a
159
+ * `shell` step stays one attempt. At most `MAX_STEP_TIMEOUT` (six hours); a longer
160
+ * wait is a gate, not a step.
161
+ */
162
+ timeout?: string;
156
163
  /**
157
164
  * Surface this activity's return value as a named run outcome.
158
165
  *
@@ -14,7 +14,8 @@ import type { OpConfig, StepDefinition } from "./types";
14
14
  import { runOpLocally, OpRunFailure, type OpRunResult } from "./local-executor";
15
15
  import { workLeaseOutput, workLeaseProblems, stewardWorkHolder, WORK_LEASE_STEP_ID } from "./work-lease-run";
16
16
  import { declareSteward } from "./steward";
17
- import { Op } from "./builders";
17
+ import { Op, shell } from "./builders";
18
+ import { shellCmd } from "./activities/shell";
18
19
  import { WORK_LEASE_ITEM_PATTERN } from "./work-lease-decl";
19
20
  import { runOperatorRound } from "./operator";
20
21
  import { stepOutput, validateStepOutputRefScope } from "./step-output-ref";
@@ -164,6 +165,24 @@ describe("a run under a work lease", () => {
164
165
  });
165
166
  });
166
167
 
168
+ test("a shell pick step that prints JSON hands the lease its candidates, and the next is claimed when the first is held (#2787)", async () => {
169
+ await withTestDir(async (dir) => {
170
+ await initRepo(dir);
171
+ await claimWorkLease("W-4", "other", { cwd: dir, ttlMs: 60_000 });
172
+ const pick = shell(`echo '["W-4","W-5"]'`, { id: "pick", json: true });
173
+ const op = dispatchOp({ workLease: { item: pick.out.json } }, [pick, step("build", { item: workLeaseOutput("item") })]);
174
+ const activities = new Map<string, ActivityFn>([
175
+ ["shellCmd", shellCmd as unknown as ActivityFn],
176
+ ["build", async () => ({})],
177
+ ]);
178
+ const result = await run(op, activities, dir);
179
+ expect(result.status).toBe("ok");
180
+ expect(result.records.map((r) => r.fn)).toEqual(["shellCmd", "workLease:claim", "build"]);
181
+ expect(result.records[2].args).toEqual({ item: "W-5" });
182
+ expect(result.workLease).toMatchObject({ item: "W-5", released: true });
183
+ });
184
+ });
185
+
167
186
  test("--work takes the place of the Op's item, and a failed run releases not_done", async () => {
168
187
  await withTestDir(async (dir) => {
169
188
  await initRepo(dir);
@@ -354,7 +354,7 @@ export class RunWorkLease {
354
354
  * keeps what was committed), and give the lease back with the outcome
355
355
  * unless it was lost.
356
356
  */
357
- async finish(status: "ok" | "fail" | "gated", resultsById: ReadonlyMap<string, unknown>): Promise<WorkLeaseRunResult> {
357
+ async finish(status: "ok" | "fail" | "gated" | "waiting", resultsById: ReadonlyMap<string, unknown>): Promise<WorkLeaseRunResult> {
358
358
  this.stopHeartbeat();
359
359
  await this.beat;
360
360
  const lease = this.lease;
@@ -375,7 +375,7 @@ export class RunWorkLease {
375
375
  await git(["worktree", "prune"], this.opts.cwd).catch(() => undefined);
376
376
  }
377
377
  if (this.lost !== undefined) return base;
378
- let outcome = status === "ok" ? "done" : status === "gated" ? "gated" : "not_done";
378
+ let outcome = status === "ok" ? "done" : status === "gated" || status === "waiting" ? status : "not_done";
379
379
  if (status === "ok" && this.spec.outcome && isStepOutputRef(this.spec.outcome)) {
380
380
  const declared = resolvePath(resultsById.get(this.spec.outcome.step), this.spec.outcome.path);
381
381
  if (typeof declared === "string" && declared.trim() !== "") outcome = declared.trim();
@@ -0,0 +1,129 @@
1
+ /**
2
+ * A throwaway workspace for the decide tests: a git repository with an answer
3
+ * kind (the reference workspace's), a task kind the points read through the
4
+ * read contract, three points (a noul, a choice and a score), and a box member
5
+ * that declares one brokered capability and one without a broker.
6
+ */
7
+
8
+ import { execFileSync } from "node:child_process";
9
+ import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+
13
+ /** The chant checkout the tests run in. */
14
+ export const REPO = realpathSync(join(import.meta.dirname, "..", "..", "..", "..", ".."));
15
+ const REF = join(REPO, "reference-workspace");
16
+
17
+ export const MODEL = "jev-1.13.0";
18
+
19
+ const model = (extra: Record<string, unknown> = {}) => ({ kind: "model", backend: "systemone", model: MODEL, threshold: 0.8, ...extra });
20
+
21
+ export const POINTS = {
22
+ points: {
23
+ triage: {
24
+ title: "Does this task need a person now",
25
+ question: { type: "noul", instructions: "Does the task need a person to look at it today?", criteria: { true: "A person looks at it today.", false: "It can wait." } },
26
+ inputs: { "record.size": "the task's size in points", "record.risky": "whether the task touches production" },
27
+ deciders: [{ kind: "table", rows: [{ when: { "record.size": 0 }, answer: false }] }, model(), { kind: "quorum", count: 1 }],
28
+ },
29
+ route: {
30
+ title: "Which team takes this task",
31
+ question: { type: "choice", instructions: "Pick the team that takes the task.", criteria: { platform: "The platform team.", app: "The app team.", docs: "The docs team." } },
32
+ inputs: { "record.title": "the task's title", "record.size": "the task's size in points" },
33
+ deciders: [model({ unreachable: "fail" }), { kind: "quorum", count: 1 }],
34
+ },
35
+ effort: {
36
+ title: "How much effort this task takes",
37
+ question: { type: "score", instructions: "Rate the effort the task takes.", criteria: ["low", "medium", "high"] },
38
+ inputs: { "record.size": "the task's size in points", "member.kind": "the kind of the member the task is in" },
39
+ deciders: [model(), { kind: "quorum", count: 1 }],
40
+ },
41
+ },
42
+ };
43
+
44
+ const TASK_KIND = `export const recordKind = {
45
+ name: "task",
46
+ location: { dir: ".", match: "^T-[0-9]+\\\\.md$" },
47
+ format: "markdown-front-matter",
48
+ schema: { id: "urn:intentius:chant:test-task:1", path: "task.schema.json" },
49
+ idField: "id",
50
+ stateField: "state",
51
+ states: ["open", "done"],
52
+ closedStates: ["done"],
53
+ };
54
+ `;
55
+
56
+ const TASK_SCHEMA = {
57
+ $schema: "http://json-schema.org/draft-07/schema#",
58
+ $id: "urn:intentius:chant:test-task:1",
59
+ type: "object",
60
+ required: ["id", "title", "state"],
61
+ properties: {
62
+ id: { type: "string" },
63
+ title: { type: "string" },
64
+ state: { enum: ["open", "done"] },
65
+ size: { type: "integer" },
66
+ risky: { type: "boolean" },
67
+ },
68
+ additionalProperties: false,
69
+ };
70
+
71
+ const task = (id: string, title: string, size: number, risky: boolean) =>
72
+ `---\nid: "${id}"\ntitle: "${title}"\nstate: "open"\nsize: ${size}\nrisky: ${risky}\n---\n\n# ${title}\n`;
73
+
74
+ const scratch: string[] = [];
75
+
76
+ export function cleanScratch(): void {
77
+ for (const d of scratch.splice(0)) rmSync(d, { recursive: true, force: true });
78
+ }
79
+
80
+ export function git(cwd: string, ...args: string[]): string {
81
+ return execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
82
+ }
83
+
84
+ /** Make the workspace and commit it. Returns its root. */
85
+ export function workspace(points: unknown = POINTS): string {
86
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-decide-")));
87
+ scratch.push(root);
88
+ const files: Record<string, string> = {
89
+ "chant.workspace.json": JSON.stringify(
90
+ {
91
+ name: "decide-test",
92
+ schema: 1,
93
+ members: [
94
+ { name: "app", dir: "app", kind: "other", because: "a plain Node server" },
95
+ {
96
+ name: "box",
97
+ dir: "box",
98
+ kind: "other",
99
+ because: "the box the steward runs in",
100
+ box: { capabilities: [{ name: "inference", broker: "lobby", scope: ["agent"] }, { name: "raw" }] },
101
+ },
102
+ ],
103
+ records: [{ kind: "answers/answer.kind.mjs" }, { kind: "tasks/task.kind.mjs" }],
104
+ },
105
+ null,
106
+ 2,
107
+ ),
108
+ "app/server.mjs": "export const port = 8080;\n",
109
+ "box/README.md": "The box.\n",
110
+ "decisions/points.json": JSON.stringify(points, null, 2),
111
+ "tasks/task.kind.mjs": TASK_KIND,
112
+ "tasks/task.schema.json": JSON.stringify(TASK_SCHEMA, null, 2),
113
+ "tasks/T-1.md": task("T-1", "Rotate the deploy key", 5, true),
114
+ "tasks/T-2.md": task("T-2", "Fix a typo in the README", 1, false),
115
+ };
116
+ for (const [path, text] of Object.entries(files)) {
117
+ mkdirSync(dirname(join(root, path)), { recursive: true });
118
+ writeFileSync(join(root, path), text);
119
+ }
120
+ cpSync(join(REF, "answers", "answer.kind.mjs"), join(root, "answers", "answer.kind.mjs"));
121
+ cpSync(join(REF, "answers", "answer.schema.json"), join(root, "answers", "answer.schema.json"));
122
+ git(root, "init", "-q");
123
+ git(root, "add", "-A");
124
+ git(root, "commit", "-q", "-m", "one");
125
+ return root;
126
+ }
127
+
128
+ /** The front matter text of a written record. */
129
+ export const recordText = (root: string, path: string): string => readFileSync(join(root, path), "utf-8");