@intentius/chant 0.91.0 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) 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-tools.d.ts +2 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  10. package/dist/cli/plugins.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +12 -2
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/lexicon-module.d.ts +22 -0
  17. package/dist/lexicon-module.d.ts.map +1 -1
  18. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  19. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  20. package/dist/lifecycle/run-ledger.d.ts +2 -0
  21. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  22. package/dist/lifecycle/symptoms.d.ts +51 -0
  23. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  24. package/dist/lint/rules/index.d.ts +4 -2
  25. package/dist/lint/rules/index.d.ts.map +1 -1
  26. package/dist/lint/rules/op/index.d.ts +14 -3
  27. package/dist/lint/rules/op/index.d.ts.map +1 -1
  28. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  29. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  31. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  33. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  35. package/dist/op/activities/activity-contracts.d.ts +201 -0
  36. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  37. package/dist/op/activities/apply.d.ts.map +1 -1
  38. package/dist/op/activities/change-coverage.d.ts +44 -0
  39. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  40. package/dist/op/activities/converge.d.ts +24 -1
  41. package/dist/op/activities/converge.d.ts.map +1 -1
  42. package/dist/op/activities/decide.d.ts +107 -0
  43. package/dist/op/activities/decide.d.ts.map +1 -0
  44. package/dist/op/activities/index.d.ts +10 -0
  45. package/dist/op/activities/index.d.ts.map +1 -1
  46. package/dist/op/activities/shell.d.ts +32 -0
  47. package/dist/op/activities/shell.d.ts.map +1 -1
  48. package/dist/op/activities/source-release.d.ts +86 -0
  49. package/dist/op/activities/source-release.d.ts.map +1 -0
  50. package/dist/op/activities/source-rollback.d.ts +100 -0
  51. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  52. package/dist/op/activities/work-evidence.d.ts +42 -0
  53. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  54. package/dist/op/activity-contract.d.ts +1 -0
  55. package/dist/op/activity-contract.d.ts.map +1 -1
  56. package/dist/op/activity-profiles.d.ts +16 -0
  57. package/dist/op/activity-profiles.d.ts.map +1 -1
  58. package/dist/op/builders.d.ts +84 -2
  59. package/dist/op/builders.d.ts.map +1 -1
  60. package/dist/op/composites/converge-op.d.ts +31 -4
  61. package/dist/op/composites/converge-op.d.ts.map +1 -1
  62. package/dist/op/decide-backend.d.ts +116 -0
  63. package/dist/op/decide-backend.d.ts.map +1 -0
  64. package/dist/op/decide-config.d.ts +120 -0
  65. package/dist/op/decide-config.d.ts.map +1 -0
  66. package/dist/op/decide-read-inputs.d.ts +40 -0
  67. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  68. package/dist/op/gate-wait.d.ts +43 -0
  69. package/dist/op/gate-wait.d.ts.map +1 -0
  70. package/dist/op/gate.d.ts +5 -2
  71. package/dist/op/gate.d.ts.map +1 -1
  72. package/dist/op/index.d.ts +11 -4
  73. package/dist/op/index.d.ts.map +1 -1
  74. package/dist/op/local-executor.d.ts +29 -4
  75. package/dist/op/local-executor.d.ts.map +1 -1
  76. package/dist/op/local-output.d.ts +2 -0
  77. package/dist/op/local-output.d.ts.map +1 -1
  78. package/dist/op/op-ir.d.ts +2 -0
  79. package/dist/op/op-ir.d.ts.map +1 -1
  80. package/dist/op/op-verb-class.d.ts.map +1 -1
  81. package/dist/op/operator.d.ts +97 -1
  82. package/dist/op/operator.d.ts.map +1 -1
  83. package/dist/op/runtime.d.ts +37 -5
  84. package/dist/op/runtime.d.ts.map +1 -1
  85. package/dist/op/runtimes/local.d.ts.map +1 -1
  86. package/dist/op/source-archive.d.ts +63 -0
  87. package/dist/op/source-archive.d.ts.map +1 -0
  88. package/dist/op/steward-points.d.ts +136 -0
  89. package/dist/op/steward-points.d.ts.map +1 -0
  90. package/dist/op/steward-turn.d.ts +53 -0
  91. package/dist/op/steward-turn.d.ts.map +1 -0
  92. package/dist/op/steward.d.ts +22 -1
  93. package/dist/op/steward.d.ts.map +1 -1
  94. package/dist/op/types.d.ts +7 -0
  95. package/dist/op/types.d.ts.map +1 -1
  96. package/dist/op/work-lease-run.d.ts +1 -1
  97. package/dist/op/work-lease-run.d.ts.map +1 -1
  98. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  100. package/dist/workspace/answers-ledger.d.ts +76 -0
  101. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  102. package/dist/workspace/changes-cli.d.ts +16 -0
  103. package/dist/workspace/changes-cli.d.ts.map +1 -0
  104. package/dist/workspace/changes.d.ts +200 -0
  105. package/dist/workspace/changes.d.ts.map +1 -0
  106. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  107. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  108. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  118. package/dist/workspace/chant-migrations.d.ts +99 -0
  119. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  120. package/dist/workspace/checks/boxes.d.ts +34 -1
  121. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  122. package/dist/workspace/checks/diagrams.d.ts +31 -0
  123. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  124. package/dist/workspace/checks/records.d.ts +7 -0
  125. package/dist/workspace/checks/records.d.ts.map +1 -1
  126. package/dist/workspace/checks.d.ts +3 -0
  127. package/dist/workspace/checks.d.ts.map +1 -1
  128. package/dist/workspace/compose-graph.d.ts +2 -0
  129. package/dist/workspace/compose-graph.d.ts.map +1 -1
  130. package/dist/workspace/conformance/index.d.ts.map +1 -1
  131. package/dist/workspace/decide.d.ts +49 -4
  132. package/dist/workspace/decide.d.ts.map +1 -1
  133. package/dist/workspace/declaration.d.ts +52 -0
  134. package/dist/workspace/declaration.d.ts.map +1 -1
  135. package/dist/workspace/declaration.schema.json +114 -1
  136. package/dist/workspace/declared-kinds.d.ts +20 -0
  137. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  138. package/dist/workspace/generated-files.d.ts +7 -0
  139. package/dist/workspace/generated-files.d.ts.map +1 -1
  140. package/dist/workspace/intent.d.ts.map +1 -1
  141. package/dist/workspace/lineage-check.d.ts +3 -0
  142. package/dist/workspace/lineage-check.d.ts.map +1 -1
  143. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  144. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  145. package/dist/workspace/ls.d.ts +39 -1
  146. package/dist/workspace/ls.d.ts.map +1 -1
  147. package/dist/workspace/point-answer.schema.json +95 -0
  148. package/dist/workspace/points-cli.d.ts.map +1 -1
  149. package/dist/workspace/points.d.ts +1 -1
  150. package/dist/workspace/reason-codes.d.ts +14 -2
  151. package/dist/workspace/reason-codes.d.ts.map +1 -1
  152. package/dist/workspace/records-cli.d.ts +11 -1
  153. package/dist/workspace/records-cli.d.ts.map +1 -1
  154. package/dist/workspace/records-close.d.ts +1 -1
  155. package/dist/workspace/records-close.d.ts.map +1 -1
  156. package/dist/workspace/records-write.d.ts +11 -5
  157. package/dist/workspace/records-write.d.ts.map +1 -1
  158. package/dist/workspace/records.d.ts +34 -4
  159. package/dist/workspace/records.d.ts.map +1 -1
  160. package/dist/workspace/status-gates.d.ts +7 -2
  161. package/dist/workspace/status-gates.d.ts.map +1 -1
  162. package/dist/workspace/status-stewards.d.ts +59 -1
  163. package/dist/workspace/status-stewards.d.ts.map +1 -1
  164. package/dist/workspace/status.d.ts +16 -0
  165. package/dist/workspace/status.d.ts.map +1 -1
  166. package/dist/workspace/work-cli.d.ts +113 -2
  167. package/dist/workspace/work-cli.d.ts.map +1 -1
  168. package/dist/workspace/work-evidence.d.ts +74 -0
  169. package/dist/workspace/work-evidence.d.ts.map +1 -0
  170. package/dist/workspace/work.d.ts +43 -1
  171. package/dist/workspace/work.d.ts.map +1 -1
  172. package/package.json +1 -1
  173. package/src/cli/commands/audit.test.ts +9 -1
  174. package/src/cli/commands/build.ts +3 -2
  175. package/src/cli/commands/lint.ts +7 -1
  176. package/src/cli/handlers/graph.test.ts +20 -0
  177. package/src/cli/handlers/graph.ts +16 -2
  178. package/src/cli/handlers/operator.ts +34 -2
  179. package/src/cli/handlers/run.test.ts +154 -2
  180. package/src/cli/handlers/run.ts +121 -2
  181. package/src/cli/main.test.ts +10 -0
  182. package/src/cli/main.ts +19 -0
  183. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  184. package/src/cli/mcp/workspace-tools.ts +62 -4
  185. package/src/cli/plugins.ts +11 -3
  186. package/src/cli/registry.ts +12 -2
  187. package/src/codegen/pinned-upgrade.ts +2 -1
  188. package/src/codegen/release-wiring.test.ts +49 -2
  189. package/src/components/capability-plugin-loader.ts +3 -3
  190. package/src/components/cli-support.ts +2 -2
  191. package/src/config.ts +11 -0
  192. package/src/lexicon-module.ts +56 -1
  193. package/src/lexicon-package.test.ts +96 -0
  194. package/src/lifecycle/converge-ledger.ts +20 -0
  195. package/src/lifecycle/run-ledger.ts +6 -0
  196. package/src/lifecycle/symptoms.ts +95 -0
  197. package/src/lint/rules/index.ts +6 -2
  198. package/src/lint/rules/op/index.ts +17 -4
  199. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  200. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  201. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  202. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  203. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  204. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  205. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  206. package/src/op/activities/activity-contracts.ts +190 -1
  207. package/src/op/activities/apply.ts +7 -6
  208. package/src/op/activities/change-coverage.ts +60 -0
  209. package/src/op/activities/converge.test.ts +55 -1
  210. package/src/op/activities/converge.ts +208 -29
  211. package/src/op/activities/decide.live.test.ts +78 -0
  212. package/src/op/activities/decide.test.ts +300 -0
  213. package/src/op/activities/decide.ts +267 -0
  214. package/src/op/activities/index.ts +21 -0
  215. package/src/op/activities/shell.test.ts +91 -1
  216. package/src/op/activities/shell.ts +91 -2
  217. package/src/op/activities/source-release.test.ts +118 -0
  218. package/src/op/activities/source-release.ts +168 -0
  219. package/src/op/activities/source-rollback.test.ts +131 -0
  220. package/src/op/activities/source-rollback.ts +239 -0
  221. package/src/op/activities/work-evidence.ts +62 -0
  222. package/src/op/activity-contract-registry.test.ts +2 -0
  223. package/src/op/activity-contract-registry.ts +2 -2
  224. package/src/op/activity-contract.test.ts +12 -0
  225. package/src/op/activity-contract.ts +6 -1
  226. package/src/op/activity-profiles.ts +29 -0
  227. package/src/op/activity-registry.test.ts +1 -1
  228. package/src/op/activity-registry.ts +2 -2
  229. package/src/op/builders.ts +119 -5
  230. package/src/op/command-gate.test.ts +192 -0
  231. package/src/op/composites/converge-op.test.ts +27 -1
  232. package/src/op/composites/converge-op.ts +69 -15
  233. package/src/op/decide-backend.test.ts +44 -0
  234. package/src/op/decide-backend.ts +212 -0
  235. package/src/op/decide-config.test.ts +52 -0
  236. package/src/op/decide-config.ts +80 -0
  237. package/src/op/decide-read-inputs.ts +117 -0
  238. package/src/op/gate-approval.ts +2 -2
  239. package/src/op/gate-wait.ts +57 -0
  240. package/src/op/gate.ts +8 -3
  241. package/src/op/generate-pipeline.ts +2 -2
  242. package/src/op/index.ts +18 -3
  243. package/src/op/local-executor.ts +151 -15
  244. package/src/op/local-output.test.ts +33 -0
  245. package/src/op/local-output.ts +20 -2
  246. package/src/op/op-ir.test.ts +10 -0
  247. package/src/op/op-ir.ts +4 -0
  248. package/src/op/op-verb-class.ts +1 -0
  249. package/src/op/operator.ts +290 -11
  250. package/src/op/runtime.ts +31 -8
  251. package/src/op/runtimes/local.ts +3 -1
  252. package/src/op/source-archive.ts +161 -0
  253. package/src/op/steward-points.test.ts +313 -0
  254. package/src/op/steward-points.ts +194 -0
  255. package/src/op/steward-turn.ts +79 -0
  256. package/src/op/steward.test.ts +87 -3
  257. package/src/op/steward.ts +25 -1
  258. package/src/op/types.ts +7 -0
  259. package/src/op/work-lease-run.test.ts +20 -1
  260. package/src/op/work-lease-run.ts +2 -2
  261. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  262. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  263. package/src/workspace/answers-ledger.ts +118 -0
  264. package/src/workspace/changes-cli.ts +56 -0
  265. package/src/workspace/changes.schema.json +472 -0
  266. package/src/workspace/changes.test.ts +315 -0
  267. package/src/workspace/changes.ts +512 -0
  268. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  269. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  270. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  271. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  272. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  273. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  274. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  275. package/src/workspace/chant-migrations.ts +164 -0
  276. package/src/workspace/check-contract.test.ts +4 -2
  277. package/src/workspace/check.schema.json +15 -5
  278. package/src/workspace/checks/boxes.test.ts +80 -1
  279. package/src/workspace/checks/boxes.ts +136 -1
  280. package/src/workspace/checks/diagrams.test.ts +134 -0
  281. package/src/workspace/checks/diagrams.ts +108 -0
  282. package/src/workspace/checks/records.ts +47 -0
  283. package/src/workspace/checks.ts +8 -1
  284. package/src/workspace/compose-graph.ts +2 -0
  285. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  286. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  287. package/src/workspace/conformance/index.ts +5 -0
  288. package/src/workspace/decide.test.ts +1 -1
  289. package/src/workspace/decide.ts +156 -23
  290. package/src/workspace/declaration.schema.json +114 -1
  291. package/src/workspace/declaration.test.ts +58 -0
  292. package/src/workspace/declaration.ts +104 -0
  293. package/src/workspace/declared-kinds.test.ts +5 -5
  294. package/src/workspace/declared-kinds.ts +46 -1
  295. package/src/workspace/generated-files.ts +14 -0
  296. package/src/workspace/graph-cli.ts +1 -1
  297. package/src/workspace/graph.schema.json +9 -1
  298. package/src/workspace/intent.schema.json +8 -2
  299. package/src/workspace/intent.ts +3 -10
  300. package/src/workspace/lineage-check.ts +16 -1
  301. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  302. package/src/workspace/lineage-upgrade.test.ts +70 -0
  303. package/src/workspace/lineage-upgrade.ts +81 -10
  304. package/src/workspace/ls-contract.test.ts +57 -0
  305. package/src/workspace/ls.schema.json +46 -1
  306. package/src/workspace/ls.ts +62 -3
  307. package/src/workspace/points-cli.ts +14 -3
  308. package/src/workspace/points-write.schema.json +11 -1
  309. package/src/workspace/points.schema.json +33 -0
  310. package/src/workspace/points.test.ts +1 -1
  311. package/src/workspace/points.ts +1 -1
  312. package/src/workspace/read-contract.test.ts +1 -1
  313. package/src/workspace/reason-codes.test.ts +11 -1
  314. package/src/workspace/reason-codes.ts +18 -2
  315. package/src/workspace/records-amend.schema.json +2 -0
  316. package/src/workspace/records-cli.ts +13 -3
  317. package/src/workspace/records-close.schema.json +2 -0
  318. package/src/workspace/records-new.schema.json +2 -0
  319. package/src/workspace/records-review.schema.json +2 -0
  320. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  321. package/src/workspace/records-sessions-write.test.ts +2 -34
  322. package/src/workspace/records-write.e2e.test.ts +108 -0
  323. package/src/workspace/records-write.test.ts +32 -41
  324. package/src/workspace/records-write.ts +33 -15
  325. package/src/workspace/records.schema.json +36 -2
  326. package/src/workspace/records.test.ts +48 -3
  327. package/src/workspace/records.ts +109 -3
  328. package/src/workspace/status-contract.test.ts +13 -8
  329. package/src/workspace/status-gates.ts +9 -4
  330. package/src/workspace/status-stewards.ts +108 -4
  331. package/src/workspace/status.schema.json +111 -7
  332. package/src/workspace/status.ts +36 -1
  333. package/src/workspace/work-acceptance.test.ts +274 -0
  334. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  335. package/src/workspace/work-cli.ts +193 -10
  336. package/src/workspace/work-evidence.schema.json +212 -0
  337. package/src/workspace/work-evidence.ts +169 -0
  338. package/src/workspace/work-history.schema.json +349 -0
  339. package/src/workspace/work-history.test.ts +156 -0
  340. package/src/workspace/work.test.ts +3 -2
  341. package/src/workspace/work.ts +100 -1
  342. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  343. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  344. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -35,6 +35,7 @@
35
35
  import { exec } from "node:child_process";
36
36
  import { promisify } from "node:util";
37
37
  import { normalizeApply, type ApplyResult } from "../../apply";
38
+ import { importLexiconPackage } from "../../lexicon-module";
38
39
 
39
40
  const execAsync = promisify(exec);
40
41
 
@@ -241,7 +242,7 @@ async function loadK8sApplier(): Promise<K8sApplier> {
241
242
  const spec = "@intentius/chant-lexicon-k8s/op/activities";
242
243
  let mod: { applyManifest?: K8sApplier };
243
244
  try {
244
- mod = (await import(spec)) as { applyManifest?: K8sApplier };
245
+ mod = (await importLexiconPackage(spec)) as { applyManifest?: K8sApplier };
245
246
  } catch (err) {
246
247
  throw new Error(
247
248
  `apply target "kubectl" needs @intentius/chant-lexicon-k8s, which could not be loaded ` +
@@ -269,7 +270,7 @@ async function loadAzureApplier(): Promise<AzureApplier> {
269
270
  };
270
271
  let mod: AzureModule;
271
272
  try {
272
- mod = (await import(spec)) as AzureModule;
273
+ mod = (await importLexiconPackage(spec)) as AzureModule;
273
274
  } catch (err) {
274
275
  throw new Error(
275
276
  `apply target "arm" needs @intentius/chant-lexicon-azure, which could not be loaded ` +
@@ -304,7 +305,7 @@ async function loadGcpApplier(): Promise<GcpApplier> {
304
305
  };
305
306
  let mod: GcpModule;
306
307
  try {
307
- mod = (await import(spec)) as GcpModule;
308
+ mod = (await importLexiconPackage(spec)) as GcpModule;
308
309
  } catch (err) {
309
310
  throw new Error(
310
311
  `apply target "gcp" needs @intentius/chant-lexicon-gcp, which could not be loaded ` +
@@ -339,7 +340,7 @@ async function loadFlyApplier(): Promise<FlyApplier> {
339
340
  };
340
341
  let mod: FlyModule;
341
342
  try {
342
- mod = (await import(spec)) as FlyModule;
343
+ mod = (await importLexiconPackage(spec)) as FlyModule;
343
344
  } catch (err) {
344
345
  throw new Error(
345
346
  `apply target "fly" needs @intentius/chant-lexicon-fly, which could not be loaded ` +
@@ -370,7 +371,7 @@ async function loadAwsApplier(): Promise<AwsApplier> {
370
371
  const spec = "@intentius/chant-lexicon-aws/op/activities";
371
372
  let mod: { awsApply?: AwsApplier };
372
373
  try {
373
- mod = (await import(spec)) as { awsApply?: AwsApplier };
374
+ mod = (await importLexiconPackage(spec)) as { awsApply?: AwsApplier };
374
375
  } catch (err) {
375
376
  throw new Error(
376
377
  `apply target "cloudformation" needs @intentius/chant-lexicon-aws, which could not be loaded ` +
@@ -395,7 +396,7 @@ async function loadAwsRollback(): Promise<AwsRollback> {
395
396
  const spec = "@intentius/chant-lexicon-aws/op/activities";
396
397
  let mod: { rollbackStack?: AwsRollback };
397
398
  try {
398
- mod = (await import(spec)) as { rollbackStack?: AwsRollback };
399
+ mod = (await importLexiconPackage(spec)) as { rollbackStack?: AwsRollback };
399
400
  } catch (err) {
400
401
  throw new Error(
401
402
  `rollback for target "cloudformation" needs @intentius/chant-lexicon-aws, which could not be ` +
@@ -0,0 +1,60 @@
1
+ /**
2
+ * changeCoverage — the forward coverage check (#2773) as an Op activity, for
3
+ * an Op that changes the checkout (#2748) to run on its own diff.
4
+ *
5
+ * Such an Op's leased steps work in a worktree of their own on
6
+ * `chant/work/[<member>/]<item>`. Given that worktree as `cwd`
7
+ * (`workLeaseOutput("worktree")`) and no `range`, the activity checks the
8
+ * commits the branch holds beyond the main worktree's HEAD, with the branch's
9
+ * item as the work item in hand, exactly as `chant workspace check --changes
10
+ * <base>..HEAD --work <item>` would. A `range` or `work` given replaces what
11
+ * the branch says.
12
+ *
13
+ * The step fails when the check's severity is `fail` and it has a finding,
14
+ * or when the check can't run; otherwise it returns the document's findings
15
+ * and summary for later steps. The workspace code loads on first call, so
16
+ * importing core's activities loads nothing under `workspace/` (#2525 rule 5).
17
+ */
18
+
19
+ import type { ChangeFinding, ChangesDocument } from "../../workspace/changes";
20
+
21
+ export interface ChangeCoverageArgs {
22
+ /** The checkout to check: the Op's worktree. Default: the working directory. */
23
+ cwd?: string;
24
+ /** `<base>..<head>`, `<base>...<head>` or `<base>`. Default: the work branch's own commits. */
25
+ range?: string;
26
+ /** The work item in hand. Default: the work branch's item. */
27
+ work?: string;
28
+ /** In place of the declaration's `changes.severity`. */
29
+ severity?: "off" | "warn" | "fail";
30
+ }
31
+
32
+ export interface ChangeCoverageResult {
33
+ ok: boolean;
34
+ range: { spec: string; base: string; head: string };
35
+ work: string | null;
36
+ severity: "off" | "warn" | "fail";
37
+ findings: Pick<ChangeFinding, "code" | "path" | "message" | "severity" | "records" | "triage">[];
38
+ summary: Exclude<ChangesDocument, { error: unknown }>["summary"];
39
+ }
40
+
41
+ export async function changeCoverage(args: ChangeCoverageArgs = {}): Promise<ChangeCoverageResult> {
42
+ const { checkChanges, workBranchChanges } = await import("../../workspace/changes");
43
+ const cwd = args.cwd ?? process.cwd();
44
+ const branch = args.range === undefined ? workBranchChanges(cwd) : undefined;
45
+ const range = args.range ?? branch?.range;
46
+ if (range === undefined) throw new Error(`changeCoverage: ${cwd} is not on a chant/work/ branch, so give the range to check`);
47
+ const work = args.work ?? branch?.work;
48
+ const { doc } = await checkChanges({ cwd, range, ...(work !== undefined ? { work } : {}), ...(args.severity ? { severity: args.severity } : {}) });
49
+ if ("error" in doc) throw new Error(`changeCoverage: ${doc.error.code}: ${doc.error.message}`);
50
+ const result: ChangeCoverageResult = {
51
+ ok: doc.ok,
52
+ range: doc.range,
53
+ work: doc.work?.record ?? null,
54
+ severity: doc.severity,
55
+ findings: doc.findings.map(({ code, path, message, severity, records, triage }) => ({ code, path, message, severity, records, triage })),
56
+ summary: doc.summary,
57
+ };
58
+ if (!doc.ok) throw new Error(`changeCoverage: ${doc.findings.length} change ${doc.findings.length === 1 ? "finding" : "findings"} at severity fail: ${doc.findings.map((f) => `${f.code} ${f.path}`).join(", ")}`);
59
+ return result;
60
+ }
@@ -1,9 +1,11 @@
1
1
  import { describe, test, expect } from "vitest";
2
2
  import { eq, gt, run, report, when } from "../converge-rule";
3
- import type { ConvergeSymptom } from "../../lifecycle/symptoms";
3
+ import { parseResourceObservation, type ConvergeSymptom, type ObservedResource, type ResourceSymptom } from "../../lifecycle/symptoms";
4
4
  import type { ConvergeTickRecord, ConvergeRuleOutcome } from "../../lifecycle/converge-ledger";
5
5
  import {
6
6
  planConvergeTick,
7
+ planResourceTick,
8
+ consecutiveResourceFires,
7
9
  verbClassAllowedToDispatch,
8
10
  enforceVerbClassAtDispatch,
9
11
  sanitizeOneLine,
@@ -354,3 +356,55 @@ describe("classifyDispatchFailure", () => {
354
356
  expect(classifyDispatchFailure("")).toBeUndefined();
355
357
  });
356
358
  });
359
+
360
+ describe("planResourceTick — rules evaluated once per observed resource (#2778)", () => {
361
+ const restart = when<ResourceSymptom>(eq("status", "drifted"), run("restart-service"), { id: "restart-drifted", why: "Restart a drifted service." });
362
+ const unknownR = when<ResourceSymptom>(eq("status", "unknown"), report("not observed"), { id: "report-unknown", why: "unknown never remediates." });
363
+ const resources: ObservedResource[] = [
364
+ { name: "app", status: "drifted", detail: "stopped" },
365
+ { name: "hud", status: "in-sync" },
366
+ { name: "door", status: "drifted" },
367
+ { name: "site", status: "unknown", detail: "no health" },
368
+ ];
369
+
370
+ test("a fired run() is planned for each drifted resource, and names it", () => {
371
+ const plan = planResourceTick("box", resources, [restart, unknownR], [], "box-converge", "apply", 3);
372
+ expect(plan.firedRuleIds).toEqual(["restart-drifted", "report-unknown"]);
373
+ expect(plan.outcomes).toEqual([
374
+ { ruleId: "restart-drifted", resource: "app", action: "ran", op: "restart-service" },
375
+ { ruleId: "restart-drifted", resource: "door", action: "ran", op: "restart-service" },
376
+ { ruleId: "report-unknown", resource: "site", action: "reported", reason: "not observed" },
377
+ ]);
378
+ });
379
+
380
+ test("unknown never remediates, the dial and the budget hold per tick", () => {
381
+ const anyStatus = when<ResourceSymptom>(eq("env", "box"), run("restart-service"), { id: "restart-all", why: "x" });
382
+ const plan = planResourceTick("box", [{ name: "site", status: "unknown" }], [anyStatus], [], "box-converge", "apply", 3);
383
+ expect(plan.outcomes[0]).toMatchObject({ action: "reported", resource: "site", reason: expect.stringMatching(/unknown never remediates/) });
384
+ expect(planResourceTick("box", resources, [restart], [], "box-converge", "observe", 3).outcomes.map((o) => o.action)).toEqual(["reported", "reported"]);
385
+ expect(planResourceTick("box", resources, [restart], [], "box-converge", "apply", 1).outcomes.map((o) => o.action)).toEqual(["ran", "skipped-budget"]);
386
+ });
387
+
388
+ test("flap damping counts one rule's fires for one resource, on this op's ticks", () => {
389
+ const fired = (resource: string, op = "box-converge") =>
390
+ tick({ op, firedRuleIds: ["restart-drifted"], outcomes: [{ ruleId: "restart-drifted", action: "ran", op: "restart-service", resource }] });
391
+ const prior = [fired("app"), fired("app"), fired("hud", "other-converge"), fired("app")];
392
+ expect(consecutiveResourceFires(prior, "box-converge", "restart-drifted", "app")).toBe(3);
393
+ expect(consecutiveResourceFires(prior, "box-converge", "restart-drifted", "door")).toBe(0);
394
+ const plan = planResourceTick("box", resources, [restart], prior, "box-converge", "apply", 3);
395
+ expect(plan.outcomes.map((o) => [o.resource, o.action])).toEqual([["app", "skipped-flap"], ["door", "ran"]]);
396
+ });
397
+ });
398
+
399
+ describe("parseResourceObservation (#2778)", () => {
400
+ test("reads an observation, or a shell result that prints one, and refuses anything else", () => {
401
+ const observation = { resources: [{ name: "app", status: "drifted", detail: "stopped" }] };
402
+ expect(parseResourceObservation(observation)).toEqual(observation);
403
+ expect(parseResourceObservation({ stdout: JSON.stringify(observation), stderr: "", exitCode: 0 })).toEqual(observation);
404
+ expect(() => parseResourceObservation({ stdout: "nope", stderr: "", exitCode: 0 })).toThrow(/not JSON/);
405
+ expect(() => parseResourceObservation({})).toThrow(/no `resources` array/);
406
+ expect(() => parseResourceObservation({ resources: [{ name: "app", status: "down" }] })).toThrow(/in-sync, drifted or unknown/);
407
+ expect(() => parseResourceObservation({ resources: [{ status: "drifted" }] })).toThrow(/has no name/);
408
+ expect(() => parseResourceObservation({ resources: [{ name: "a", status: "drifted" }, { name: "a", status: "in-sync" }] })).toThrow(/twice/);
409
+ });
410
+ });
@@ -40,7 +40,14 @@ import {
40
40
  } from "../converge-rule";
41
41
  import { classifyOpVerbClass, type OpVerbClass } from "../op-verb-class";
42
42
  import { discoverOps } from "../discover";
43
- import { deriveSymptoms, type ConvergeSymptom } from "../../lifecycle/symptoms";
43
+ import {
44
+ deriveSymptoms,
45
+ parseResourceObservation,
46
+ CONVERGE_RESOURCE_ENV,
47
+ type ConvergeSymptom,
48
+ type ObservedResource,
49
+ type ResourceSymptom,
50
+ } from "../../lifecycle/symptoms";
44
51
  import {
45
52
  appendConvergeRecord,
46
53
  readConvergeLedger,
@@ -88,6 +95,14 @@ export interface ConvergeTickArgs {
88
95
  * (this activity re-derives its own symptom independently).
89
96
  */
90
97
  preflightDrift?: boolean;
98
+ /**
99
+ * The observer step's result, for a ConvergeOp declared with `observe`
100
+ * (#2778): a {@link ResourceObservation}, or a `shell()` result whose
101
+ * stdout is one. Present, the tick observes nothing itself: it evaluates
102
+ * `rules` once per reported resource, against a `ResourceSymptom`, and
103
+ * dispatches a fired `run()` for that resource.
104
+ */
105
+ observed?: unknown;
91
106
  }
92
107
 
93
108
  export interface ConvergeTickResult {
@@ -209,9 +224,10 @@ export function classifyDispatchFailure(raw: string): { gateName: string } | und
209
224
  async function dispatchOp(
210
225
  opName: string,
211
226
  signal?: AbortSignal,
227
+ env?: Record<string, string>,
212
228
  ): Promise<{ ok: true } | { ok: false; error: string; gateName?: string }> {
213
229
  try {
214
- await execAsync(`chant run ${shellQuote(opName)} --json`, { signal });
230
+ await execAsync(`chant run ${shellQuote(opName)} --json`, { signal, ...(env ? { env: { ...process.env, ...env } } : {}) });
215
231
  return { ok: true };
216
232
  } catch (err) {
217
233
  const e = err as { code?: number; stdout?: string; stderr?: string; message?: string };
@@ -383,25 +399,106 @@ function renderLog(env: string, s: ConvergeSymptom, counts: ReturnType<typeof su
383
399
  );
384
400
  }
385
401
 
386
- export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal): Promise<ConvergeTickResult> {
387
- await fetchLifecycle().catch(() => undefined);
402
+ // ── Resources an observer step reports (#2778) ──────────────────────────────
388
403
 
389
- const [cs, statusRows, ledger] = await Promise.all([
390
- observeChangeSet(args.env, signal),
391
- observeStatusRows(args.env, signal),
392
- readConvergeLedger(args.env),
393
- ]);
404
+ /**
405
+ * How many consecutive most-recent ticks of `op` fired `ruleId` for
406
+ * `resource`. A rule of a ConvergeOp with an observer step fires once per
407
+ * resource, so its flap damping is counted per resource: a rule that
408
+ * restarts `app` on one tick and `hud` on the next is not flapping.
409
+ */
410
+ export function consecutiveResourceFires(records: ConvergeTickRecord[], op: string, ruleId: string, resource: string): number {
411
+ let count = 0;
412
+ for (let i = records.length - 1; i >= 0; i--) {
413
+ if (records[i].op !== op) continue;
414
+ if (!records[i].outcomes.some((o) => o.ruleId === ruleId && o.resource === resource)) break;
415
+ count++;
416
+ }
417
+ return count;
418
+ }
394
419
 
395
- const symptom = deriveSymptoms(args.env, cs, statusRows);
396
- const plan = planConvergeTick(symptom, args.rules, ledger.records, args.dial, args.budget);
420
+ /**
421
+ * The pure planning core of a resource tick (#2778): every rule against every
422
+ * observed resource, in authored order within each resource. The rules of
423
+ * {@link planConvergeTick} hold per resource: `unknown` never remediates, a
424
+ * `report()` never dispatches, the dial decides whether a `run()` may, and
425
+ * `budget` bounds the dispatches across the whole tick.
426
+ */
427
+ export function planResourceTick(
428
+ env: string,
429
+ resources: ObservedResource[],
430
+ rules: ConvergeRule<ResourceSymptom>[],
431
+ priorRecords: ConvergeTickRecord[],
432
+ op: string,
433
+ dial: ConvergeTickArgs["dial"],
434
+ budget: number,
435
+ ): ConvergeTickPlan {
436
+ const fired = new Set<string>();
437
+ const outcomes: ConvergeRuleOutcome[] = [];
438
+ let dispatched = 0;
397
439
 
398
- // Execute: only "ran" outcomes cause a subprocess dispatch. Before that
399
- // dispatch, the runtime backstop (Finding A, #1954 pre-merge review)
400
- // re-classifies the target and downgrades to "reported" if this dial/verb
401
- // class pairing was never supposed to reach dispatch — OPS014 (build time)
402
- // is the primary defense; this is what catches a rule table that reached
403
- // `convergeTick` without going through it.
404
- for (const outcome of plan.outcomes) {
440
+ for (const resource of resources) {
441
+ const symptom: ResourceSymptom = { env, resource: resource.name, status: resource.status, detail: resource.detail ?? "" };
442
+ for (const rule of rules) {
443
+ if (!evaluatePredicate(rule.when, symptom)) continue;
444
+ fired.add(rule.id);
445
+ const base = { ruleId: rule.id, resource: resource.name };
446
+
447
+ const threshold = rule.flapThreshold ?? DEFAULT_FLAP_THRESHOLD;
448
+ const consecutive = consecutiveResourceFires(priorRecords, op, rule.id, resource.name);
449
+ if (consecutive >= threshold) {
450
+ outcomes.push({
451
+ ...base,
452
+ action: "skipped-flap",
453
+ reason: `fired for ${resource.name} ${consecutive + 1} consecutive ticks without clearing (threshold ${threshold}) — escalated to report`,
454
+ });
455
+ continue;
456
+ }
457
+
458
+ const action: RuleAction = rule.then;
459
+ if (resource.status === "unknown" && action.kind === "run") {
460
+ outcomes.push({
461
+ ...base,
462
+ action: "reported",
463
+ reason: `${resource.name} is unknown (${resource.detail || "unobserved"}) — unknown never remediates`,
464
+ });
465
+ continue;
466
+ }
467
+ if (action.kind === "report") {
468
+ outcomes.push({ ...base, action: "reported", reason: action.reason });
469
+ continue;
470
+ }
471
+ if (!dialAllowsDispatch(dial)) {
472
+ outcomes.push({ ...base, action: "reported", reason: `dial "${dial}" does not permit dispatch — report only`, op: action.op });
473
+ continue;
474
+ }
475
+ if (dispatched >= budget) {
476
+ outcomes.push({ ...base, action: "skipped-budget", op: action.op });
477
+ continue;
478
+ }
479
+ dispatched++;
480
+ outcomes.push({ ...base, action: "ran", op: action.op });
481
+ }
482
+ }
483
+ return { firedRuleIds: [...fired], outcomes };
484
+ }
485
+
486
+ function renderResourceLog(env: string, resources: ObservedResource[], counts: ReturnType<typeof summarizeOutcomes>): string {
487
+ const n = (status: ObservedResource["status"]) => resources.filter((r) => r.status === status).length;
488
+ return (
489
+ `converge(${env}): resources=${resources.length} in-sync=${n("in-sync")} drifted=${n("drifted")} unknown=${n("unknown")} ` +
490
+ `remediated=${counts.remediated} reported=${counts.reported} skipped-budget=${counts.skippedBudget} ` +
491
+ `skipped-flap=${counts.skippedFlap} gated=${counts.gated}`
492
+ );
493
+ }
494
+
495
+ /** Run the planned dispatches of a tick, in place: the verb-class backstop, then `chant run`, then what came of it. */
496
+ async function dispatchPlanned(
497
+ outcomes: ConvergeRuleOutcome[],
498
+ args: ConvergeTickArgs,
499
+ signal?: AbortSignal,
500
+ ): Promise<void> {
501
+ for (const outcome of outcomes) {
405
502
  if (outcome.action !== "ran" || !outcome.op) continue;
406
503
 
407
504
  const verbClass = await classifyDispatchTarget(outcome.op);
@@ -410,7 +507,9 @@ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal)
410
507
  outcome.reason = backstopped.reason;
411
508
  if (outcome.action !== "ran") continue;
412
509
 
413
- const result = await dispatchOp(outcome.op, signal);
510
+ // A rule fired for one resource (#2778) hands the dispatched Op its name.
511
+ const env = outcome.resource !== undefined ? { [CONVERGE_RESOURCE_ENV]: outcome.resource } : undefined;
512
+ const result = await dispatchOp(outcome.op, signal, env);
414
513
  if (!result.ok && result.gateName) {
415
514
  // Gate-as-fact (#1485, closed by #2119): a terminal, durable,
416
515
  // non-blocking fact — not an ordinary dispatch failure the operator
@@ -432,6 +531,95 @@ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal)
432
531
  outcome.reason = `dispatch of "${outcome.op}" failed: ${result.error}`;
433
532
  }
434
533
  }
534
+ }
535
+
536
+ /** Push the tick's ledger record, and say whether it reached the remote (chant#2337). */
537
+ async function pushTick(): Promise<{ pushed: boolean; pushWarning?: string }> {
538
+ try {
539
+ const pushed = await pushLifecycle();
540
+ return pushed ? { pushed } : { pushed, pushWarning: "no remote is configured for chant/lifecycle — the tick record was recorded locally only" };
541
+ } catch (err) {
542
+ return { pushed: false, pushWarning: err instanceof Error ? err.message : String(err) };
543
+ }
544
+ }
545
+
546
+ /** A tick of a ConvergeOp with an observer step (#2778): the observation is already in `args.observed`. */
547
+ async function resourceTick(args: ConvergeTickArgs, signal?: AbortSignal): Promise<ConvergeTickResult> {
548
+ const { resources } = parseResourceObservation(args.observed);
549
+ const ledger = await readConvergeLedger(args.env);
550
+ const plan = planResourceTick(
551
+ args.env,
552
+ resources,
553
+ args.rules as unknown as ConvergeRule<ResourceSymptom>[],
554
+ ledger.records,
555
+ args.opName,
556
+ args.dial,
557
+ args.budget,
558
+ );
559
+ await dispatchPlanned(plan.outcomes, args, signal);
560
+
561
+ const counts = summarizeOutcomes(plan.outcomes);
562
+ const log = renderResourceLog(args.env, resources, counts);
563
+ const drifted = resources.filter((r) => r.status === "drifted").length;
564
+ const { record } = await appendConvergeRecord({
565
+ op: args.opName,
566
+ env: args.env,
567
+ timestamp: new Date().toISOString(),
568
+ firedRuleIds: plan.firedRuleIds,
569
+ outcomes: plan.outcomes,
570
+ resources: resources.map((r) => ({
571
+ name: r.name,
572
+ status: r.status,
573
+ ...(r.detail ? { detail: sanitizeLedgerText(r.detail) } : {}),
574
+ })),
575
+ summary: {
576
+ drifted,
577
+ remediated: counts.remediated,
578
+ reported: counts.reported,
579
+ skippedBudget: counts.skippedBudget,
580
+ skippedFlap: counts.skippedFlap,
581
+ gated: counts.gated,
582
+ unobserved: resources.filter((r) => r.status === "unknown").length,
583
+ adopted: 0,
584
+ },
585
+ log,
586
+ });
587
+ const push = await pushTick();
588
+ return {
589
+ id: record.id,
590
+ drifted: drifted > 0,
591
+ remediated: record.summary.remediated,
592
+ reported: record.summary.reported,
593
+ skippedBudget: record.summary.skippedBudget,
594
+ skippedFlap: record.summary.skippedFlap,
595
+ gated: record.summary.gated ?? 0,
596
+ unobserved: record.summary.unobserved,
597
+ adopted: 0,
598
+ log: record.log,
599
+ ...push,
600
+ };
601
+ }
602
+
603
+ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal): Promise<ConvergeTickResult> {
604
+ await fetchLifecycle().catch(() => undefined);
605
+ if (args.observed !== undefined) return resourceTick(args, signal);
606
+
607
+ const [cs, statusRows, ledger] = await Promise.all([
608
+ observeChangeSet(args.env, signal),
609
+ observeStatusRows(args.env, signal),
610
+ readConvergeLedger(args.env),
611
+ ]);
612
+
613
+ const symptom = deriveSymptoms(args.env, cs, statusRows);
614
+ const plan = planConvergeTick(symptom, args.rules, ledger.records, args.dial, args.budget);
615
+
616
+ // Execute: only "ran" outcomes cause a subprocess dispatch. Before that
617
+ // dispatch, the runtime backstop (Finding A, #1954 pre-merge review)
618
+ // re-classifies the target and downgrades to "reported" if this dial/verb
619
+ // class pairing was never supposed to reach dispatch — OPS014 (build time)
620
+ // is the primary defense; this is what catches a rule table that reached
621
+ // `convergeTick` without going through it.
622
+ await dispatchPlanned(plan.outcomes, args, signal);
435
623
 
436
624
  const counts = summarizeOutcomes(plan.outcomes);
437
625
  const log = renderLog(args.env, symptom, counts);
@@ -461,16 +649,7 @@ export async function convergeTick(args: ConvergeTickArgs, signal?: AbortSignal)
461
649
  // chant#2337 — reported rather than swallowed, the way #2336 does it for the
462
650
  // gate. A push that did not land leaves a correct local record nobody else
463
651
  // can read, and the caller is the only one positioned to say so.
464
- let pushed = false;
465
- let pushWarning: string | undefined;
466
- try {
467
- pushed = await pushLifecycle();
468
- if (!pushed) {
469
- pushWarning = "no remote is configured for chant/lifecycle — the tick record was recorded locally only";
470
- }
471
- } catch (err) {
472
- pushWarning = err instanceof Error ? err.message : String(err);
473
- }
652
+ const { pushed, pushWarning } = await pushTick();
474
653
 
475
654
  return {
476
655
  id: record.id,
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The decide activity against a real Jev-compatible server (#2740). Gated:
3
+ * it runs only when TYPESAFE_API_KEY is set, and is skipped otherwise, so the
4
+ * suite never needs an account or the network.
5
+ *
6
+ * TYPESAFE_API_KEY=... npx vitest run packages/core/src/op/activities/decide.live.test.ts
7
+ *
8
+ * TYPESAFE_BASE_URL points it at another compatible server (the default is
9
+ * https://api.typesafe.ai), and TYPESAFE_MODEL at another pinned model id (the
10
+ * default is jev-1.13.0). A model id is always a versioned one, never an alias.
11
+ */
12
+
13
+ import { afterAll, describe, expect, test } from "vitest";
14
+ import { isPointWait } from "../steward-points";
15
+ import { workspacePoints } from "../../workspace/points-cli";
16
+ import { postQuestion } from "../decide-backend";
17
+ import { cleanScratch, workspace } from "../../workspace/__fixtures__/decide-workspace";
18
+ import { runDecide } from "./decide";
19
+
20
+ const key = process.env.TYPESAFE_API_KEY;
21
+ const url = process.env.TYPESAFE_BASE_URL ?? "https://api.typesafe.ai";
22
+ const model = process.env.TYPESAFE_MODEL ?? "jev-1.13.0";
23
+ const backend = { url, key: { env: "TYPESAFE_API_KEY" }, timeoutMs: 60_000 };
24
+
25
+ afterAll(cleanScratch);
26
+
27
+ describe.skipIf(!key)("decide against a real Jev-compatible server (TYPESAFE_API_KEY is set)", () => {
28
+ test("the server answers each question type in the wire format", async () => {
29
+ const cwd = process.cwd();
30
+ const noul = await postQuestion(backend, "systemone", {
31
+ point: "live-noul",
32
+ backend: "systemone",
33
+ model,
34
+ question: { type: "noul", instructions: "Is the sky in the state blue?", criteria: { true: "It is blue.", false: "It is not blue." } },
35
+ state: { sky: "a clear blue sky at noon" },
36
+ }, { cwd });
37
+ expect(noul.model).toBe(model);
38
+ expect(noul.answer.type).toBe("noul");
39
+ expect((noul.answer as { noul: number }).noul).toBeGreaterThanOrEqual(0);
40
+
41
+ const choice = await postQuestion(backend, "systemone", {
42
+ point: "live-choice",
43
+ backend: "systemone",
44
+ model,
45
+ question: { type: "choice", instructions: "Which team takes the ticket in the state?", criteria: { billing: "Payments and invoices.", platform: "Servers and deploys." } },
46
+ state: { ticket: "The production deploy failed with a disk full error." },
47
+ }, { cwd });
48
+ expect(choice.answer).toMatchObject({ type: "choice" });
49
+ expect(["billing", "platform"]).toContain((choice.answer as { choice: string }).choice);
50
+
51
+ const score = await postQuestion(backend, "systemone", {
52
+ point: "live-score",
53
+ backend: "systemone",
54
+ model,
55
+ question: { type: "score", instructions: "How urgent is the ticket in the state?", criteria: ["not urgent", "somewhat urgent", "very urgent"] },
56
+ state: { ticket: "Every customer is locked out of their account." },
57
+ }, { cwd });
58
+ expect(score.answer).toMatchObject({ type: "score" });
59
+ }, 120_000);
60
+
61
+ test("the activity asks a point and records the model's answer or its escalation", async () => {
62
+ const root = workspace();
63
+ const outcome = await runDecide({ cwd: root, point: "triage", inputs: { "record.size": 5, "record.risky": true }, backends: { systemone: backend } }, {}).then(
64
+ (r) => r.id,
65
+ (e: unknown) => {
66
+ if (isPointWait(e)) return e.question.id;
67
+ throw e;
68
+ },
69
+ );
70
+ const doc = await workspacePoints({ cwd: root });
71
+ if ("error" in doc) throw new Error(doc.error.message);
72
+ const q = doc.questions.find((x) => x.id === outcome)!;
73
+ // The server answered: a proposal at the pin, or an escalation below the threshold with the model's lean. Never unreachable.
74
+ expect(["proposed", "escalated"]).toContain(q.state);
75
+ if (q.state === "proposed") expect(q.decider).toMatchObject({ kind: "model", model });
76
+ else expect(q.escalations.find((e) => e.kind === "model")?.reason).not.toContain("could not answer");
77
+ }, 120_000);
78
+ });