akm-cli 0.9.17-alpha.1 → 0.9.17-alpha.10

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 (399) hide show
  1. package/CHANGELOG.md +1623 -0
  2. package/STABILITY.md +11 -10
  3. package/dist/akm +124 -193
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  16. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  17. package/dist/assets/templates/html/health.html +3 -5
  18. package/dist/cli/retired-commands.js +1 -1
  19. package/dist/cli/shared.js +6 -2
  20. package/dist/cli/unknown-flags.js +24 -1
  21. package/dist/cli.js +68 -10
  22. package/dist/commands/agent/agent-dispatch.js +1 -1
  23. package/dist/commands/command/command-execution.js +24 -62
  24. package/dist/commands/feedback-cli.js +0 -1
  25. package/dist/commands/health/accept-rate.js +2 -2
  26. package/dist/commands/health/archive-usage.js +92 -0
  27. package/dist/commands/health/checks.js +30 -75
  28. package/dist/commands/health/config-skew.js +38 -0
  29. package/dist/commands/health/data-dir-usage.js +25 -13
  30. package/dist/commands/health/egress.js +54 -0
  31. package/dist/commands/health/html-report.js +1 -42
  32. package/dist/commands/health/improve-metrics.js +136 -587
  33. package/dist/commands/health/md-report.js +1 -6
  34. package/dist/commands/health/plugin-staleness.js +53 -3
  35. package/dist/commands/health/renderers.js +12 -4
  36. package/dist/commands/health/report-view-model.js +14 -119
  37. package/dist/commands/health/types-improve.js +4 -19
  38. package/dist/commands/health/windows.js +64 -74
  39. package/dist/commands/health.js +135 -143
  40. package/dist/commands/improve/consolidate/chunking.js +25 -100
  41. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  42. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  43. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  44. package/dist/commands/improve/consolidate.js +573 -1124
  45. package/dist/commands/improve/content-hash.js +16 -24
  46. package/dist/commands/improve/distill/content-repair.js +18 -100
  47. package/dist/commands/improve/distill-guards.js +20 -81
  48. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  49. package/dist/commands/improve/distill.js +608 -1075
  50. package/dist/commands/improve/eligibility.js +126 -400
  51. package/dist/commands/improve/execution.js +8 -10
  52. package/dist/commands/improve/extract-prompt.js +1 -2
  53. package/dist/commands/improve/extract.js +487 -1046
  54. package/dist/commands/improve/feedback-valence.js +0 -25
  55. package/dist/commands/improve/improve-cli.js +54 -171
  56. package/dist/commands/improve/improve-result-file.js +10 -66
  57. package/dist/commands/improve/improve-strategies.js +35 -9
  58. package/dist/commands/improve/improve-usage-report.js +18 -64
  59. package/dist/commands/improve/improve.js +451 -1082
  60. package/dist/commands/improve/ledger.js +119 -0
  61. package/dist/commands/improve/locks.js +2 -8
  62. package/dist/commands/improve/loop-stages.js +401 -1192
  63. package/dist/commands/improve/memory/derived-ref.js +12 -77
  64. package/dist/commands/improve/memory/memory-belief.js +16 -118
  65. package/dist/commands/improve/memory/memory-improve.js +266 -14
  66. package/dist/commands/improve/outcome-loop.js +28 -156
  67. package/dist/commands/improve/planner.js +5 -15
  68. package/dist/commands/improve/preparation.js +776 -2349
  69. package/dist/commands/improve/proactive-maintenance.js +34 -101
  70. package/dist/commands/improve/reflect-noise.js +104 -280
  71. package/dist/commands/improve/reflect.js +642 -1364
  72. package/dist/commands/improve/retrieval-gate.js +127 -0
  73. package/dist/commands/improve/retrieval-scope.js +92 -0
  74. package/dist/commands/improve/salience.js +41 -240
  75. package/dist/commands/improve/session-asset.js +19 -100
  76. package/dist/commands/improve/stage.js +322 -0
  77. package/dist/commands/lint/base-linter.js +37 -15
  78. package/dist/commands/proposal/drain.js +258 -644
  79. package/dist/commands/proposal/proposal-cli.js +19 -20
  80. package/dist/commands/proposal/proposal-types.js +27 -41
  81. package/dist/commands/proposal/proposal.js +38 -8
  82. package/dist/commands/proposal/propose.js +134 -160
  83. package/dist/commands/proposal/repository.js +1099 -1475
  84. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  85. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  86. package/dist/commands/proposal/validators/proposals.js +22 -89
  87. package/dist/commands/read/curate.js +105 -462
  88. package/dist/commands/read/knowledge.js +3 -2
  89. package/dist/commands/read/search-cli.js +16 -33
  90. package/dist/commands/read/search.js +17 -23
  91. package/dist/commands/read/show.js +57 -108
  92. package/dist/commands/sources/bundle-cli.js +25 -2
  93. package/dist/commands/sources/bundle-config-ops.js +4 -0
  94. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  95. package/dist/commands/sources/info.js +127 -29
  96. package/dist/commands/sources/installed-stashes.js +197 -746
  97. package/dist/commands/sources/schema-repair.js +98 -129
  98. package/dist/commands/sources/source-add.js +62 -12
  99. package/dist/commands/sources/source-manage.js +9 -2
  100. package/dist/commands/sources/stash-cli.js +24 -4
  101. package/dist/commands/tasks/explain.js +10 -13
  102. package/dist/commands/tasks/tasks-cli.js +12 -13
  103. package/dist/commands/tasks/tasks.js +350 -936
  104. package/dist/commands/tasks/validate.js +26 -24
  105. package/dist/commands/workflow/plan.js +22 -29
  106. package/dist/commands/workflow-cli.js +4 -4
  107. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  108. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  109. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  110. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  111. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  112. package/dist/core/adapter/execution-source.js +17 -29
  113. package/dist/core/asset/asset-placement.js +4 -13
  114. package/dist/core/asset/resolve-ref.js +1 -1
  115. package/dist/core/bundle-id.js +42 -5
  116. package/dist/core/bundle-rename.js +285 -0
  117. package/dist/core/config/config-io.js +1 -2
  118. package/dist/core/config/config-schema.js +9 -34
  119. package/dist/core/config/config-walker.js +1 -1
  120. package/dist/core/config/config.js +184 -111
  121. package/dist/core/config/engine-semantics.js +0 -2
  122. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  123. package/dist/core/config/schema/embedding.js +20 -5
  124. package/dist/core/config/schema/engines.js +5 -0
  125. package/dist/core/config/schema/execution.js +1 -1
  126. package/dist/core/config/schema/experimental.js +1 -1
  127. package/dist/core/config/schema/improve-processes.js +37 -135
  128. package/dist/core/config/schema/improve.js +4 -42
  129. package/dist/core/config/schema/index-config.js +9 -48
  130. package/dist/core/config/schema/scheduler.js +12 -12
  131. package/dist/core/config/schema/search.js +6 -22
  132. package/dist/core/env-secret-ref.js +0 -1
  133. package/dist/core/errors.js +8 -9
  134. package/dist/core/file-change.js +13 -5
  135. package/dist/core/file-lock.js +76 -173
  136. package/dist/core/improve-result.js +26 -7
  137. package/dist/core/improve-types.js +0 -1
  138. package/dist/core/logs-db.js +2 -2
  139. package/dist/core/loopback.js +7 -12
  140. package/dist/core/non-task-input.js +20 -0
  141. package/dist/core/parse.js +13 -16
  142. package/dist/core/paths.js +0 -31
  143. package/dist/core/redaction.js +109 -2
  144. package/dist/core/run-lock.js +2 -5
  145. package/dist/core/spawn-env.js +1 -1
  146. package/dist/core/state/migrations.js +123 -61
  147. package/dist/core/state-db-scope.js +2 -4
  148. package/dist/core/state-db.js +126 -692
  149. package/dist/core/time.js +0 -20
  150. package/dist/core/type-presentation.js +1 -9
  151. package/dist/core/write-source.js +294 -1005
  152. package/dist/execution/input-contract.js +1 -1
  153. package/dist/execution/resolved-request.js +135 -689
  154. package/dist/execution/source.js +63 -257
  155. package/dist/execution/target-ref.js +1 -1
  156. package/dist/indexer/bundle-identity-guard.js +2 -2
  157. package/dist/indexer/db/llm-cache.js +2 -2
  158. package/dist/indexer/ensure-index.js +46 -87
  159. package/dist/indexer/index-rebuild-lock.js +3 -11
  160. package/dist/indexer/index-writer-lock.js +8 -17
  161. package/dist/indexer/index-written-assets.js +141 -154
  162. package/dist/indexer/indexer.js +400 -1124
  163. package/dist/indexer/links/declared-links.js +90 -0
  164. package/dist/indexer/materialize-embeddings.js +60 -397
  165. package/dist/indexer/passes/memory-inference.js +81 -90
  166. package/dist/indexer/passes/metadata.js +132 -219
  167. package/dist/indexer/read-preflight.js +0 -7
  168. package/dist/indexer/scan/doc-to-entry.js +2 -3
  169. package/dist/indexer/scan/drain-dir.js +1 -1
  170. package/dist/indexer/search/db-search.js +190 -590
  171. package/dist/indexer/search/fts-query.js +30 -41
  172. package/dist/indexer/search/ranking.js +28 -154
  173. package/dist/indexer/search/search-attribution.js +12 -32
  174. package/dist/indexer/search/search-fields.js +11 -15
  175. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  176. package/dist/indexer/search/search-source.js +1 -4
  177. package/dist/indexer/usage/usage-events.js +36 -7
  178. package/dist/indexer/walk/walker.js +3 -4
  179. package/dist/integrations/agent/engine-fallback.js +23 -40
  180. package/dist/integrations/agent/engine-resolution.js +93 -183
  181. package/dist/integrations/agent/execution.js +507 -0
  182. package/dist/integrations/agent/model-map.js +28 -156
  183. package/dist/integrations/agent/request-lowering.js +66 -141
  184. package/dist/integrations/agent/runner-dispatch.js +143 -321
  185. package/dist/integrations/agent/runner.js +54 -14
  186. package/dist/integrations/lockfile.js +53 -101
  187. package/dist/llm/client.js +8 -10
  188. package/dist/llm/embedders/deterministic.js +2 -3
  189. package/dist/llm/embedders/profile.js +71 -0
  190. package/dist/llm/embedders/remote.js +11 -17
  191. package/dist/llm/feature-gate.js +0 -8
  192. package/dist/llm/index-passes.js +3 -5
  193. package/dist/llm/memory-infer.js +1 -2
  194. package/dist/llm/structured-call.js +5 -24
  195. package/dist/output/generic-render.js +23 -11
  196. package/dist/output/html-render.js +13 -10
  197. package/dist/output/render-registry.js +3 -32
  198. package/dist/output/shapes/helpers.js +25 -38
  199. package/dist/output/shapes/passthrough.js +1 -9
  200. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  201. package/dist/output/text/command-format.js +69 -31
  202. package/dist/output/text/helpers.js +1 -1
  203. package/dist/output/text/migrate.js +5 -14
  204. package/dist/output/text/proposal-format.js +48 -3
  205. package/dist/output/text/show-format.js +13 -17
  206. package/dist/output/text/workflow-format.js +0 -32
  207. package/dist/output/text.js +2 -0
  208. package/dist/registry/factory.js +4 -19
  209. package/dist/registry/network.js +66 -220
  210. package/dist/registry/providers/index.js +0 -2
  211. package/dist/registry/providers/skills-sh.js +3 -14
  212. package/dist/registry/providers/static-index.js +24 -26
  213. package/dist/registry/resolve.js +55 -131
  214. package/dist/scripts/akm-migrate-node.js +42938 -92375
  215. package/dist/scripts/akm-migrate.js +42930 -92365
  216. package/dist/setup/registry-stash-loader.js +4 -13
  217. package/dist/setup/semantic-assets.js +3 -44
  218. package/dist/setup/setup.js +1 -1
  219. package/dist/setup/steps/connection.js +5 -6
  220. package/dist/setup/steps/platforms.js +2 -2
  221. package/dist/setup/steps/tasks.js +25 -15
  222. package/dist/sources/provider-factory.js +17 -18
  223. package/dist/sources/providers/filesystem.js +2 -3
  224. package/dist/sources/providers/git-install.js +7 -1
  225. package/dist/sources/providers/git-provider.js +0 -3
  226. package/dist/sources/providers/git-stash.js +83 -21
  227. package/dist/sources/providers/npm.js +2 -4
  228. package/dist/sources/providers/provider-utils.js +5 -10
  229. package/dist/sources/providers/website.js +0 -2
  230. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  231. package/dist/sources/website-url.js +2 -2
  232. package/dist/storage/database.js +9 -35
  233. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  234. package/dist/storage/repositories/index-connection.js +39 -72
  235. package/dist/storage/repositories/index-entries-repository.js +88 -129
  236. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  237. package/dist/storage/repositories/index-entry-schema.js +101 -268
  238. package/dist/storage/repositories/index-fts-repository.js +86 -256
  239. package/dist/storage/repositories/index-links-repository.js +143 -0
  240. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  241. package/dist/storage/repositories/index-meta-repository.js +6 -4
  242. package/dist/storage/repositories/index-schema.js +257 -325
  243. package/dist/storage/repositories/index-utility-repository.js +8 -29
  244. package/dist/storage/repositories/index-vec-repository.js +133 -414
  245. package/dist/storage/repositories/outcome-repository.js +2 -1
  246. package/dist/storage/repositories/proposals-repository.js +100 -0
  247. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  248. package/dist/storage/repositories/salience-repository.js +1 -19
  249. package/dist/storage/repositories/task-history-repository.js +26 -4
  250. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  251. package/dist/storage/sqlite-migrations.js +136 -0
  252. package/dist/storage/sqlite-pragmas.js +11 -9
  253. package/dist/storage/sqlite-transaction.js +170 -0
  254. package/dist/storage/state-db-integrity.js +34 -27
  255. package/dist/tasks/activation-config.js +134 -62
  256. package/dist/tasks/backends/cron.js +191 -302
  257. package/dist/tasks/backends/exec-utils.js +2 -5
  258. package/dist/tasks/backends/launchd.js +141 -748
  259. package/dist/tasks/backends/schtasks.js +119 -623
  260. package/dist/tasks/prepare/prepare-support.js +5 -15
  261. package/dist/tasks/prepare/prepare.js +0 -2
  262. package/dist/tasks/resolve-akm-bin.js +20 -79
  263. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  264. package/dist/tasks/run/load-task.js +1 -1
  265. package/dist/tasks/scheduler-binding.js +20 -238
  266. package/dist/tasks/scheduler-invocation.js +136 -244
  267. package/dist/tasks/scheduler-lock.js +53 -0
  268. package/dist/tasks/scheduler-sync.js +368 -679
  269. package/dist/tasks/source/parse-task-source.js +55 -9
  270. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  271. package/dist/tasks/source/task-to-v4.js +464 -88
  272. package/dist/workflows/authoring/authoring.js +3 -12
  273. package/dist/workflows/compile.js +211 -0
  274. package/dist/workflows/concurrency-policy.js +13 -74
  275. package/dist/workflows/exec/child-invocation.js +3 -17
  276. package/dist/workflows/exec/child-workflow.js +32 -141
  277. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  278. package/dist/workflows/exec/environment.js +98 -0
  279. package/dist/workflows/exec/exec-unit.js +33 -140
  280. package/dist/workflows/exec/frozen-judge.js +7 -59
  281. package/dist/workflows/exec/native-executor.js +82 -341
  282. package/dist/workflows/exec/param-secrets.js +29 -47
  283. package/dist/workflows/exec/run-workflow.js +154 -387
  284. package/dist/workflows/exec/scheduler.js +9 -36
  285. package/dist/workflows/exec/step-work.js +127 -430
  286. package/dist/workflows/exec/unit-dispatch.js +11 -63
  287. package/dist/workflows/exec/unit-writer.js +8 -52
  288. package/dist/workflows/exec/worktree.js +39 -273
  289. package/dist/workflows/freeze/child-output-references.js +4 -15
  290. package/dist/workflows/freeze/environment.js +99 -92
  291. package/dist/workflows/freeze/freeze.js +172 -0
  292. package/dist/workflows/freeze/step-values.js +19 -21
  293. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  294. package/dist/workflows/freeze/targets/command.js +10 -33
  295. package/dist/workflows/freeze/targets/script.js +5 -12
  296. package/dist/workflows/freeze/targets/shell.js +3 -6
  297. package/dist/workflows/freeze/targets/task.js +25 -80
  298. package/dist/workflows/freeze/task-bindings.js +20 -67
  299. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  300. package/dist/workflows/ir/params.js +6 -51
  301. package/dist/workflows/ir/plan-hash.js +2 -34
  302. package/dist/workflows/parser.js +140 -43
  303. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  304. package/dist/workflows/renderer.js +36 -69
  305. package/dist/workflows/resource-limits.js +12 -120
  306. package/dist/workflows/runtime/agent-identity.js +8 -40
  307. package/dist/workflows/runtime/run-outputs.js +3 -6
  308. package/dist/workflows/runtime/run-plan.js +316 -0
  309. package/dist/workflows/runtime/runs.js +48 -200
  310. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  311. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  312. package/dist/workflows/validate-summary.js +2 -7
  313. package/docs/integration/bundling-akm.md +49 -42
  314. package/docs/migration/README.md +1 -0
  315. package/docs/migration/release-notes/0.9.17.md +43 -0
  316. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  317. package/docs/reference/cli.md +229 -133
  318. package/docs/reference/configuration.md +71 -57
  319. package/docs/reference/data-and-telemetry.md +19 -21
  320. package/docs/reference/tasks.md +105 -39
  321. package/docs/reference/workflow-schema.md +14 -18
  322. package/docs/reference/workflows.md +6 -9
  323. package/package.json +1 -1
  324. package/schemas/akm-config.json +87 -754
  325. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  326. package/dist/assets/prompts/contradiction-judge.md +0 -33
  327. package/dist/assets/prompts/graph-extract-system.md +0 -1
  328. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  329. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  330. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  331. package/dist/commands/health/advisories.js +0 -150
  332. package/dist/commands/health/metrics.js +0 -329
  333. package/dist/commands/health/surfaces.js +0 -102
  334. package/dist/commands/improve/anti-collapse.js +0 -83
  335. package/dist/commands/improve/collapse-detector.js +0 -432
  336. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  337. package/dist/commands/improve/consolidate/merge.js +0 -146
  338. package/dist/commands/improve/distill/promote-memory.js +0 -329
  339. package/dist/commands/improve/distill/quality-gate.js +0 -500
  340. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  341. package/dist/commands/improve/proposal-envelope.js +0 -31
  342. package/dist/commands/improve/run-context.js +0 -123
  343. package/dist/commands/improve/shared.js +0 -21
  344. package/dist/commands/improve/source-identity.js +0 -28
  345. package/dist/commands/improve/triage.js +0 -96
  346. package/dist/commands/proposal/drain-policies.js +0 -151
  347. package/dist/commands/sources/update-transaction.js +0 -220
  348. package/dist/core/action-contributors.js +0 -28
  349. package/dist/core/config/config-version-shim.js +0 -101
  350. package/dist/core/fs-txn.js +0 -405
  351. package/dist/core/lexical-score.js +0 -25
  352. package/dist/core/maintenance-barrier.js +0 -167
  353. package/dist/execution/executable-identity.js +0 -105
  354. package/dist/execution/guarded-source.js +0 -427
  355. package/dist/indexer/db/graph-db.js +0 -444
  356. package/dist/indexer/graph/graph-boost.js +0 -427
  357. package/dist/indexer/graph/graph-dedup.js +0 -95
  358. package/dist/indexer/graph/graph-extraction.js +0 -1182
  359. package/dist/indexer/search/name-match.js +0 -35
  360. package/dist/indexer/search/ranking-contributors.js +0 -515
  361. package/dist/indexer/search/ranking-types.js +0 -4
  362. package/dist/indexer/walk/project-context.js +0 -192
  363. package/dist/integrations/agent/execution-cascade.js +0 -566
  364. package/dist/integrations/agent/execution-definitions.js +0 -202
  365. package/dist/integrations/agent/execution-lowering.js +0 -841
  366. package/dist/integrations/agent/execution-preparation.js +0 -98
  367. package/dist/integrations/agent/inline-execution.js +0 -74
  368. package/dist/llm/graph-extract.js +0 -872
  369. package/dist/llm/metadata-enhance.js +0 -96
  370. package/dist/registry/create-provider-registry.js +0 -29
  371. package/dist/registry/pinned-request-helper.js +0 -247
  372. package/dist/registry/pinned-transport.js +0 -717
  373. package/dist/sources/providers/index.js +0 -14
  374. package/dist/storage/engines/sqlite-migrations.js +0 -271
  375. package/dist/storage/repositories/canaries-repository.js +0 -107
  376. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  377. package/dist/storage/repositories/registry-cache.js +0 -113
  378. package/dist/tasks/scheduler-sync-preview.js +0 -52
  379. package/dist/tasks/source/task-to-v3.js +0 -507
  380. package/dist/workflows/freeze/resolve-steps.js +0 -86
  381. package/dist/workflows/freeze/source-freeze.js +0 -64
  382. package/dist/workflows/ir/compile.js +0 -321
  383. package/dist/workflows/ir/environment-v4.js +0 -330
  384. package/dist/workflows/ir/freeze-v4.js +0 -153
  385. package/dist/workflows/ir/schema-v4.js +0 -745
  386. package/dist/workflows/ir/schema.js +0 -354
  387. package/dist/workflows/program/schema.js +0 -78
  388. package/dist/workflows/runtime/checkin.js +0 -57
  389. package/dist/workflows/runtime/plan-classifier.js +0 -196
  390. package/dist/workflows/runtime/unit-checkin.js +0 -45
  391. package/dist/workflows/runtime/unit-phases.js +0 -20
  392. package/dist/workflows/schema.js +0 -4
  393. package/dist/workflows/source-ir/compile.js +0 -200
  394. package/dist/workflows/source-ir/program.js +0 -50
  395. package/dist/workflows/source-ir/result.js +0 -26
  396. package/dist/workflows/source-ir/schema.js +0 -786
  397. package/dist/workflows/source-ir/triggers.js +0 -79
  398. package/dist/workflows/source-ir/uses.js +0 -40
  399. package/dist/workflows/validator.js +0 -60
@@ -7,11 +7,18 @@
7
7
  // its other lines untouched:
8
8
  //
9
9
  // # akm:task <id> BEGIN
10
- // [SCHED] /abs/akm task run <id> > /home/.../tasks/logs/<id>.log 2>&1
10
+ // [SCHED] AKM_BUNDLE_DIR=<working stash> /abs/akm task run <id> ... > <log> 2>&1
11
11
  // # akm:task <id> END
12
12
  //
13
+ // The row sets its own environment inline, as a `VAR=value` prefix (see
14
+ // `src/tasks/scheduler-invocation.ts`); PATH is the `# akm:env` block. A
15
+ // command over the portable line limit runs `sh <wrapper script>` instead,
16
+ // and the script holds the same environment and argv.
17
+ //
13
18
  // The backend reads/writes the user's crontab via `crontab -l` and
14
- // `crontab -`. Disabling a task comments the entry with `# akm:disabled `
19
+ // `crontab -`. Every mutation is one read → modify the akm blocks (and the
20
+ // `# akm:env` PATH header) → one write, with the prior crontab restored when
21
+ // the write fails. Disabling a task comments the entry with `# akm:disabled `
15
22
  // rather than removing it, so re-enabling preserves the original schedule.
16
23
  //
17
24
  // Platform notes:
@@ -30,282 +37,131 @@
30
37
  // Tests inject a fake exec so unit tests don't touch the real crontab.
31
38
  import { spawnSync } from "node:child_process";
32
39
  import { createHash } from "node:crypto";
40
+ import fs from "node:fs";
33
41
  import path from "node:path";
34
42
  import { ConfigError } from "../../core/errors.js";
35
43
  import { getTaskLogDir } from "../../core/paths.js";
36
44
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
37
45
  import { parseSchedule, translateToCron } from "../schedule.js";
38
- import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
39
- import { buildScheduledBindingInvocation, parsePublicSchedulerInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, SCHEDULER_CONTEXT_ARG, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
46
+ import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
47
+ import { buildScheduledInvocation, parseScheduledInvocationArgv, scheduledRowEnvironmentEntries, scheduledRowEnvironmentFrom, } from "../scheduler-invocation.js";
40
48
  import { nodeFs, throwIfNotOk } from "./exec-utils.js";
41
49
  const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
42
50
  const END = (id) => `# akm:task ${assertCronValue(id)} END`;
43
51
  const DISABLED_PREFIX = "# akm:disabled ";
44
- const SOURCE_ENABLED_PREFIX = "# akm:source-enabled ";
45
52
  const BLOCK_RE = /^# akm:task ([\w.@:_-]+) BEGIN$/;
46
53
  const BLOCK_END_RE = /^# akm:task ([\w.@:_-]+) END$/;
54
+ const ENV_BEGIN = "# akm:env BEGIN";
55
+ const ENV_END = "# akm:env END";
47
56
  export const PORTABLE_CRON_LINE_LIMIT = 1000;
48
- const CRON_SNAPSHOT = Symbol("akm-cron-binding-snapshot");
57
+ const CRON_WRAPPER_PREFIX = ".akm-cron-wrapper-";
49
58
  export function CRON_BACKEND(options = {}) {
50
59
  const exec = options.exec ?? defaultCronExec();
51
60
  const fsLike = options.fs ?? nodeFs();
52
61
  const logDir = options.logDir ?? getTaskLogDir();
53
62
  const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
54
63
  const envPath = options.envPath === false ? undefined : (options.envPath ?? process.env.PATH);
55
- const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
56
- const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext, envPath ?? ""));
64
+ const readFile = options.fs?.readFile ?? ((file) => fs.readFileSync(file, "utf8"));
65
+ const lineFor = (task, opts) => buildCronLineParts(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.environment);
57
66
  return {
58
67
  name: "cron",
59
- install(task, opts, expected) {
60
- if (expected)
61
- assertSchedulerExpectationIdentity(expected, task);
62
- // Create the log directory before writing the crontab line — the
63
- // redirect target's parent directory must exist or the surrounding
64
- // shell will fail the entire entry.
65
- const cronLineParts = buildCronLineParts(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
66
- const cronLine = cronLineParts.line;
67
- assertPortableCronLine(cronLine);
68
+ install(task, opts) {
69
+ const parts = lineFor(task, opts);
70
+ assertPortableCronLine(parts.line);
71
+ // The redirect target's parent directory must exist or the surrounding
72
+ // shell fails the entire entry.
68
73
  fsLike.ensureDir(logDir);
69
- if (cronLineParts.wrapper) {
74
+ if (parts.wrapper) {
70
75
  if (!fsLike.writeFile) {
71
76
  throw new ConfigError("Cron backend needs to write a wrapper script for this task's long invocation, but the configured filesystem cannot write files.", "INVALID_CONFIG_FILE");
72
77
  }
73
- fsLike.writeFile(cronLineParts.wrapper.path, cronLineParts.wrapper.content);
78
+ fsLike.writeFile(parts.wrapper.path, parts.wrapper.content);
74
79
  }
75
80
  const existing = readCrontab(exec);
76
81
  const nativeId = schedulerBindingNativeId(task);
77
- const blocks = listBlocks(existing);
78
- const matching = blocks.filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
79
- const artifacts = matching.map((block) => cronArtifact(block.id, block.body));
80
- const priorArtifact = expected
81
- ? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, expected.state === "absent" ? 0 : 1)
82
- : matching.length > 1
83
- ? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, 1)
84
- : artifacts[0];
85
- const prior = priorArtifact ? matching.find((block) => block.id === priorArtifact.nativeId) : undefined;
86
- if (expected) {
87
- assertSchedulerMutationArtifact(priorArtifact, expected);
88
- }
89
- else if (prior) {
90
- assertSchedulerNativeArtifactOwner(prior.id, task, extractCronInvocation(prior.body)?.invocation);
91
- }
92
- const block = renderBlock(nativeId, cronLine, task.enabled, task.executionEvidenceDigest);
93
- const next = upsertBlock(existing, nativeId, block);
94
- replaceCrontab(exec, existing, next);
82
+ const block = renderBlock(nativeId, parts.line, task.enabled);
83
+ replaceCrontab(exec, existing, upsertEnvBlock(upsertBlock(existing, nativeId, block), envPath));
95
84
  },
96
- uninstall(nativeId, expected) {
97
- if (expected)
98
- assertSchedulerExpectationIdentity({ ...expected, state: "present" });
85
+ uninstall(nativeId) {
99
86
  const existing = readCrontab(exec);
100
- const matching = listBlocks(existing).filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
101
- const priorArtifact = expected
102
- ? assertSchedulerNativeArtifactCardinality(matching.map((block) => cronArtifact(block.id, block.body)), nativeId, 1)
103
- : undefined;
104
- const prior = expected
105
- ? matching.find((block) => block.id === priorArtifact?.nativeId)
106
- : matching.find(({ id }) => id === nativeId);
107
- if (!prior)
87
+ if (!listBlocks(existing).some((block) => block.id === nativeId))
108
88
  return;
109
- if (expected) {
110
- assertSchedulerRemovalArtifact(nativeId, expected, extractCronInvocation(prior.body)?.invocation, normalizeSignature(prior.body));
111
- }
112
- const next = removeBlock(existing, nativeId);
113
- replaceCrontab(exec, existing, next);
89
+ replaceCrontab(exec, existing, upsertEnvBlock(removeBlock(existing, nativeId), envPath));
114
90
  },
115
91
  setEnabled(nativeId, enabled) {
116
92
  const existing = readCrontab(exec);
117
93
  const next = toggleBlock(existing, nativeId, enabled);
118
- replaceCrontab(exec, existing, next);
94
+ if (next !== existing)
95
+ replaceCrontab(exec, existing, next);
119
96
  },
120
97
  list() {
121
- return [...inspectCronState(readCrontab(exec), defaultContextPath).installed];
122
- },
123
- listForRebind() {
124
- const existing = readCrontab(exec);
125
- return listBlocks(existing).map(({ id, body }) => {
126
- const installed = extractCronInvocation(body);
127
- const ref = {
128
- id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
98
+ const rows = [];
99
+ for (const { id, body } of listBlocks(readCrontab(exec))) {
100
+ const parsed = extractCronInvocation(body) ?? extractCronWrapperInvocation(body, readFile);
101
+ if (!parsed)
102
+ continue;
103
+ rows.push({
104
+ id: schedulerLogicalBindingId(id, parsed.invocation),
105
+ nativeId: id,
106
+ enabled: !cronIsDisabled(body),
129
107
  signature: normalizeSignature(body),
130
- ...(installed?.target !== undefined ? { target: installed.target } : {}),
131
- };
132
- Object.defineProperty(ref, "nativeId", { value: id });
133
- if (installed)
134
- Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
135
- return ref;
136
- });
137
- },
138
- listNativeArtifacts() {
139
- return [...inspectCronState(readCrontab(exec), defaultContextPath).artifacts];
140
- },
141
- inspectBindings() {
142
- return inspectCronState(readCrontab(exec), defaultContextPath);
143
- },
144
- snapshotBindings(ids) {
145
- const crontab = readCrontab(exec);
146
- const inspection = inspectCronState(crontab, defaultContextPath);
147
- const keys = new Set(ids.map(schedulerNativeArtifactKey));
148
- return Object.freeze({
149
- kind: CRON_SNAPSHOT,
150
- nativeIds: Object.freeze([...ids]),
151
- artifacts: Object.freeze(inspection.artifacts.filter((artifact) => keys.has(schedulerNativeArtifactKey(artifact.nativeId)))),
152
- crontab,
153
- });
154
- },
155
- restoreBindings(snapshot, expectedCurrent) {
156
- if (!isCronBindingSnapshot(snapshot)) {
157
- throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
158
- }
159
- const existing = readCrontab(exec);
160
- const current = inspectCronState(existing, defaultContextPath);
161
- const safeNativeIds = [];
162
- const errors = [];
163
- if (expectedCurrent) {
164
- for (const nativeId of snapshot.nativeIds) {
165
- try {
166
- const expected = expectedCurrent.find((candidate) => candidate.nativeId === nativeId);
167
- if (!expected) {
168
- throw new ConfigError(`Missing cron rollback expectation for ${JSON.stringify(nativeId)}.`, "INVALID_CONFIG_FILE");
169
- }
170
- assertSchedulerRollbackArtifactCardinality(current.artifacts, expected);
171
- safeNativeIds.push(nativeId);
172
- }
173
- catch (error) {
174
- errors.push(error);
175
- }
176
- }
177
- }
178
- else {
179
- safeNativeIds.push(...snapshot.nativeIds);
180
- }
181
- const priorBlocks = new Map(listBlocks(snapshot.crontab).map((block) => [block.id, block]));
182
- let restored = existing;
183
- for (const nativeId of safeNativeIds) {
184
- const prior = priorBlocks.get(nativeId);
185
- restored = prior
186
- ? upsertBlock(restored, nativeId, [BEGIN(nativeId), prior.body, END(nativeId)].join("\n"))
187
- : removeBlock(restored, nativeId);
188
- }
189
- if (restored !== existing) {
190
- try {
191
- replaceCrontab(exec, existing, restored);
192
- }
193
- catch (error) {
194
- errors.push(error);
195
- }
196
- }
197
- if (errors.length > 0) {
198
- throw new AggregateError(errors, "Cron rollback CAS rejected one or more changed native artifacts.");
108
+ ...(parsed.target !== undefined ? { target: parsed.target } : {}),
109
+ binding: parsed.binding,
110
+ ...(parsed.contextPath !== undefined ? { contextPath: parsed.contextPath } : {}),
111
+ ...(parsed.environment !== undefined ? { environment: parsed.environment } : {}),
112
+ invocation: parsed.invocation,
113
+ });
199
114
  }
115
+ return rows;
200
116
  },
201
117
  expectedSignature(task, opts) {
202
- const cronLine = buildCronLine(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
203
- assertPortableCronLine(cronLine);
204
- return normalizeSignature(cronBlockBody(cronLine, task.enabled, task.executionEvidenceDigest));
118
+ const line = lineFor(task, opts).line;
119
+ assertPortableCronLine(line);
120
+ return normalizeSignature(cronBlockBody(line, task.enabled));
205
121
  },
206
122
  };
207
123
  }
208
- function inspectCronState(crontab, fallbackContextPath) {
209
- const installed = [];
210
- const artifacts = [];
211
- for (const { id, body } of listBlocks(crontab)) {
212
- const parsed = extractCronInvocation(body);
213
- const fingerprint = normalizeSignature(body);
214
- const artifact = cronArtifact(id, body);
215
- artifacts.push(artifact);
216
- if (!parsed)
217
- continue;
218
- const ref = {
219
- id: schedulerLogicalBindingId(id, parsed.invocation),
220
- enabled: !cronIsDisabled(body),
221
- signature: fingerprint,
222
- ...(parsed.target !== undefined ? { target: parsed.target } : {}),
223
- binding: parsed.binding,
224
- // A legacy (pre-`--scheduler-context`) row has no real descriptor
225
- // path to report — `extractLegacyCronInvocation` leaves it "". Fall
226
- // back to the current default so downstream consumers (context
227
- // validation in `akm task prune`/`explain`, `sync`'s reuse of an
228
- // existing binding's contextPath) see a real, resolvable descriptor
229
- // rather than an empty path, since the row is about to be reconciled
230
- // to a current one anyway (#881).
231
- contextPath: parsed.contextPath || fallbackContextPath,
232
- };
233
- Object.defineProperty(ref, "nativeId", { value: id });
234
- Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
235
- installed.push(ref);
236
- }
237
- return Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) });
238
- }
239
- function cronArtifact(nativeId, body) {
240
- const parsed = extractCronInvocation(body);
241
- const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
242
- const artifact = parsed
243
- ? {
244
- nativeId,
245
- ...(owner !== undefined ? { bindingId: owner } : {}),
246
- invocation: Object.freeze([...parsed.invocation]),
247
- }
248
- : { nativeId };
249
- Object.defineProperty(artifact, "fingerprint", { value: normalizeSignature(body) });
250
- return artifact;
251
- }
252
- function isCronBindingSnapshot(value) {
253
- return (typeof value === "object" &&
254
- value !== null &&
255
- value.kind === CRON_SNAPSHOT &&
256
- typeof value.crontab === "string");
257
- }
258
- function buildCronLineParts(task, akmArgv, logDir, contextPath, _target) {
259
- const spec = parseSchedule(task.cron, "cron");
260
- const cronExpr = translateToCron(spec);
124
+ function buildCronLineParts(task, akmArgv, logDir, environment) {
125
+ const cronExpr = translateToCron(parseSchedule(task.cron, "cron"));
261
126
  const nativeId = schedulerBindingNativeId(task);
262
127
  const logPath = path.join(logDir, `${nativeId}.log`);
263
- const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
264
- const cmd = invocation.argv.map((part) => quoteForCron(part)).join(" ");
128
+ const argv = buildScheduledInvocation(akmArgv, task.invocation);
129
+ const variables = scheduledRowEnvironmentEntries(environment);
130
+ // An assignment prefix: the shell cron hands the line to sets it for this
131
+ // command only. Only the value is quoted, or the word is not an assignment.
132
+ const assignments = variables.map(([name, value]) => `${name}=${quoteForCron(value)} `).join("");
133
+ const cmd = argv.map((part) => quoteForCron(part)).join(" ");
265
134
  // #951: truncate (not append) so this bootstrap safety-net file always
266
- // holds exactly the latest run's raw output rather than growing forever.
267
- // akm's own per-run log (src/tasks/run/task-log.ts) already keeps history.
268
- const directLine = `${cronExpr} ${cmd} > ${quoteForCron(logPath)} 2>&1`;
269
- if (Buffer.byteLength(directLine, "utf8") <= PORTABLE_CRON_LINE_LIMIT) {
135
+ // holds exactly the latest run's raw output. akm's own per-run log
136
+ // (src/tasks/run/task-log.ts) keeps history.
137
+ const directLine = `${cronExpr} ${assignments}${cmd} > ${quoteForCron(logPath)} 2>&1`;
138
+ if (Buffer.byteLength(directLine, "utf8") <= PORTABLE_CRON_LINE_LIMIT)
270
139
  return { line: directLine };
271
- }
272
- const content = cronWrapperScriptContent(invocation.argv);
140
+ // A command over vixie-cron's MAX_COMMAND is spilled into a short wrapper
141
+ // script under logDir instead of being truncated or refused.
142
+ const exports = variables.map(([name, value]) => `export ${name}=${quoteForShellScript(value)}\n`).join("");
143
+ const content = `#!/bin/sh\n${exports}exec ${argv.map(quoteForShellScript).join(" ")}\n`;
273
144
  const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
274
145
  const wrapperPath = path.join(logDir, `${CRON_WRAPPER_PREFIX}${nativeId}-${contentHash}.sh`);
275
146
  const line = `${cronExpr} sh ${quoteForCron(wrapperPath)} > ${quoteForCron(logPath)} 2>&1`;
276
147
  return { line, wrapper: { path: wrapperPath, content } };
277
148
  }
278
- const CRON_WRAPPER_PREFIX = ".akm-cron-wrapper-";
279
149
  function quoteForShellScript(part) {
280
150
  if (/^[A-Za-z0-9_\-./@:=+,]+$/.test(part))
281
151
  return part;
282
152
  return `'${part.replace(/'/g, `'\\''`)}'`;
283
153
  }
284
- function cronWrapperScriptContent(argv) {
285
- const cmd = argv.map(quoteForShellScript).join(" ");
286
- return `#!/bin/sh\nexec ${cmd}\n`;
287
- }
288
- export function buildCronLine(task, akmArgv, logDir, contextPath, _target) {
289
- return buildCronLineParts(task, akmArgv, logDir, contextPath, _target).line;
154
+ export function buildCronLine(task, akmArgv, logDir, environment) {
155
+ return buildCronLineParts(task, akmArgv, logDir, environment).line;
290
156
  }
291
157
  /** The crontab line as it appears inside a block — commented when disabled. */
292
- export function cronBlockBody(cronLine, enabled, executionEvidenceDigest) {
293
- const lines = [
294
- cronLine,
295
- ...(executionEvidenceDigest === undefined
296
- ? []
297
- : [`# akm:workflow-evidence ${assertSchedulerExecutionEvidenceDigest(executionEvidenceDigest)}`]),
298
- ];
299
- return (enabled ? lines : lines.map((line) => `${DISABLED_PREFIX}${line}`)).join("\n");
158
+ export function cronBlockBody(cronLine, enabled) {
159
+ return enabled ? cronLine : `${DISABLED_PREFIX}${cronLine}`;
300
160
  }
301
- export function renderBlock(id, cronLine, enabled, executionEvidenceDigest) {
302
- return [BEGIN(id), cronBlockBody(cronLine, enabled, executionEvidenceDigest), END(id)].join("\n");
161
+ export function renderBlock(id, cronLine, enabled) {
162
+ return [BEGIN(id), cronBlockBody(cronLine, enabled), END(id)].join("\n");
303
163
  }
304
- /**
305
- * Parse the akm-owned blocks out of a crontab, returning each task id with the
306
- * raw body line(s) between its BEGIN/END markers. Used by `list()` to build a
307
- * drift signature, and exported for tests.
308
- */
164
+ /** The akm-owned blocks of a crontab: each id with the raw body between its BEGIN/END markers. */
309
165
  export function listBlocks(existing) {
310
166
  return parseBlocks(existing).map(({ id, body }) => ({ id, body }));
311
167
  }
@@ -345,69 +201,78 @@ function parseBlocks(existing) {
345
201
  function malformedBlockError(id) {
346
202
  return new ConfigError(`Crontab contains a malformed akm task block for "${id}"; refusing to modify it.`, "INVALID_CONFIG_FILE");
347
203
  }
348
- /**
349
- * Recover the bundle name from an installed cron block body by reading the
350
- * `--bundle <bundle>` token embedded in the scheduled `akm task run …`
351
- * invocation. Returns undefined for the byte-identical primary/default form
352
- * (no `--bundle`). Bundle slugs never contain whitespace (config-schema
353
- * `isBundleSlug`), so the quoted token is a single whitespace-delimited field
354
- * even when cron-quoted — a plain field split recovers it, and
355
- * the shell-word parser below reverses the quoting.
356
- */
204
+ /** The `--bundle <bundle>` token of an installed cron body; undefined for the primary/default form. */
357
205
  export function extractInstalledTarget(body) {
358
206
  return extractCronInvocation(body)?.target;
359
207
  }
208
+ /**
209
+ * Parse an installed cron body: its inline `VAR=value` environment, its
210
+ * launcher, and its public tail — a current row, a row naming a
211
+ * `--scheduler-context` descriptor (0.9.0 – 0.9.17-alpha.6), or an older
212
+ * row with neither (#881). This only ever runs on a body already isolated
213
+ * between akm's own BEGIN/END markers, so no trust extends to unmarked lines.
214
+ */
360
215
  export function extractCronInvocation(body) {
361
216
  const line = body.startsWith(DISABLED_PREFIX) ? body.slice(DISABLED_PREFIX.length) : body;
362
217
  const fields = splitCronShellWords(line);
363
218
  if (fields.length < 6)
364
219
  return undefined;
365
220
  let commandStart = 5;
366
- while (commandStart < fields.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(fields[commandStart]))
221
+ const variables = {};
222
+ for (let field = fields[commandStart]; field !== undefined; field = fields[commandStart]) {
223
+ const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(field);
224
+ if (!assignment)
225
+ break;
226
+ variables[assignment[1]] = assignment[2];
367
227
  commandStart += 1;
368
- // #951: newly-installed rows redirect with `>` (truncate); tolerate `>>`
369
- // (append) too, since that's what every row written before this change —
370
- // including ones a still-running older akm binary installs during a
371
- // rolling upgrade — looks like on disk.
228
+ }
229
+ // #951: new rows redirect with `>`; rows written before that use `>>`.
372
230
  const redirectIndex = fields.findIndex((field, index) => index >= commandStart && (field === ">" || field === ">>"));
373
231
  if (redirectIndex === -1)
374
232
  return undefined;
375
- const tail = fields.slice(commandStart, redirectIndex);
376
- const parsed = parseScheduledBindingArgv(tail);
377
- if (parsed)
378
- return parsed;
379
- // Rows written by akm < 0.9.2 (before `--scheduler-context` existed) have
380
- // no context argument at all — just the akm argv immediately followed by
381
- // the public `task run …` / `workflow run …` tail. `extractCronInvocation`
382
- // only ever runs on a body already isolated between this backend's own
383
- // `# akm:task … BEGIN/END` sentinels (see `parseBlocks`), so recognizing
384
- // this older shape here doesn't extend trust to any unmarked crontab
385
- // line — it only lets sync see and reconcile a row akm already owns
386
- // instead of treating it as absent and colliding with the still-present
387
- // artifact (#881). Guarded on the marker's absence so a row that DOES
388
- // carry `--scheduler-context` but fails to parse for some other reason
389
- // is never silently reinterpreted as legacy.
390
- if (tail.includes(SCHEDULER_CONTEXT_ARG))
233
+ const parsed = parseScheduledInvocationArgv(fields.slice(commandStart, redirectIndex));
234
+ if (!parsed)
391
235
  return undefined;
392
- return extractLegacyCronInvocation(tail);
236
+ const environment = scheduledRowEnvironmentFrom(variables);
237
+ return environment ? { ...parsed, environment } : parsed;
393
238
  }
394
- function extractLegacyCronInvocation(tail) {
395
- for (let index = 0; index < tail.length - 1; index += 1) {
396
- if ((tail[index] === "task" || tail[index] === "workflow") && tail[index + 1] === "run") {
397
- const publicInvocation = parsePublicSchedulerInvocation(tail.slice(index));
398
- if (!publicInvocation)
399
- return undefined;
400
- return {
401
- binding: tail.slice(0, index),
402
- contextPath: "",
403
- invocation: publicInvocation.invocation,
404
- ...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
405
- };
406
- }
239
+ /**
240
+ * Parse a row spilled into a wrapper script (`sh <script> > <log> 2>&1`):
241
+ * the script's `export NAME=value` lines and its `exec` argv. Only akm's own
242
+ * wrapper scripts are read; one that is gone or does not parse leaves the
243
+ * row unlisted, as any unparsable row is.
244
+ */
245
+ export function extractCronWrapperInvocation(body, readFile) {
246
+ const line = body.startsWith(DISABLED_PREFIX) ? body.slice(DISABLED_PREFIX.length) : body;
247
+ const fields = splitCronShellWords(line);
248
+ const script = fields[6];
249
+ if (fields[5] !== "sh" || !script || !path.basename(script).startsWith(CRON_WRAPPER_PREFIX))
250
+ return undefined;
251
+ if (fields[7] !== ">" && fields[7] !== ">>")
252
+ return undefined;
253
+ let content;
254
+ try {
255
+ content = readFile(script);
256
+ }
257
+ catch {
258
+ return undefined;
259
+ }
260
+ const variables = {};
261
+ let parsed;
262
+ for (const scriptLine of content.split("\n")) {
263
+ const words = splitCronShellWords(scriptLine);
264
+ const assignment = words[0] === "export" ? /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(words[1] ?? "") : null;
265
+ if (assignment)
266
+ variables[assignment[1]] = assignment[2];
267
+ else if (words[0] === "exec")
268
+ parsed = parseScheduledInvocationArgv(words.slice(1));
407
269
  }
408
- return undefined;
270
+ if (!parsed)
271
+ return undefined;
272
+ const environment = scheduledRowEnvironmentFrom(variables);
273
+ return environment ? { ...parsed, environment } : parsed;
409
274
  }
410
- /** Reverse {@link quoteForCron} for a single whitespace-free token. */
275
+ /** Reverse {@link quoteForCron}: sh word splitting, where a backslash is literal inside single quotes. */
411
276
  function splitCronShellWords(value) {
412
277
  const words = [];
413
278
  let current = "";
@@ -418,7 +283,7 @@ function splitCronShellWords(value) {
418
283
  quoted = !quoted;
419
284
  continue;
420
285
  }
421
- if (char === "\\" && index + 1 < value.length) {
286
+ if (!quoted && char === "\\" && index + 1 < value.length) {
422
287
  current += value[index + 1];
423
288
  index += 1;
424
289
  continue;
@@ -439,14 +304,10 @@ function splitCronShellWords(value) {
439
304
  function normalizeSignature(body) {
440
305
  return body
441
306
  .split(/\r?\n/)
442
- .map((l) => l.trim())
443
- .filter((l) => !stripDisabledPrefix(l).startsWith(SOURCE_ENABLED_PREFIX))
444
- .filter((l) => l.length > 0)
307
+ .map((line) => line.trim())
308
+ .filter((line) => line.length > 0)
445
309
  .join("\n");
446
310
  }
447
- function stripDisabledPrefix(line) {
448
- return line.startsWith(DISABLED_PREFIX) ? line.slice(DISABLED_PREFIX.length) : line;
449
- }
450
311
  function cronIsDisabled(body) {
451
312
  return body.trimStart().startsWith(DISABLED_PREFIX);
452
313
  }
@@ -462,17 +323,55 @@ export function removeBlock(existing, id) {
462
323
  if (blocks.length === 0)
463
324
  return existing;
464
325
  const out = lines.filter((_, index) => !blocks.some((block) => index >= block.start && index <= block.end));
465
- // Collapse trailing blank lines.
466
326
  while (out.length > 0 && out[out.length - 1] === "")
467
327
  out.pop();
468
328
  return out.join("\n");
469
329
  }
330
+ /**
331
+ * akm's managed environment section: one `PATH=` line directly above the
332
+ * first akm task block. Cron applies an environment assignment to every
333
+ * command line after it, which is why the line sits immediately above akm's
334
+ * own rows rather than at the top of the crontab; it is plain text a person
335
+ * can edit with `crontab -e`, rewritten on every akm crontab write and
336
+ * removed together with the last akm task block.
337
+ */
338
+ export function renderEnvBlock(envPath) {
339
+ return [ENV_BEGIN, `PATH=${assertCronValue(dedupeSearchPath(envPath))}`, ENV_END].join("\n");
340
+ }
341
+ export function upsertEnvBlock(existing, envPath) {
342
+ const lines = removeEnvBlock(existing).split(/\r?\n/);
343
+ const firstBlock = lines.findIndex((line) => BLOCK_RE.test(line));
344
+ if (!envPath || firstBlock === -1)
345
+ return lines.join("\n");
346
+ return [...lines.slice(0, firstBlock), renderEnvBlock(envPath), ...lines.slice(firstBlock)].join("\n");
347
+ }
348
+ export function removeEnvBlock(existing) {
349
+ const lines = existing.split(/\r?\n/);
350
+ const start = lines.indexOf(ENV_BEGIN);
351
+ if (start === -1)
352
+ return existing;
353
+ const end = lines.indexOf(ENV_END, start);
354
+ if (end === -1) {
355
+ throw new ConfigError("Crontab contains a malformed akm environment section; refusing to modify it.", "INVALID_CONFIG_FILE");
356
+ }
357
+ return [...lines.slice(0, start), ...lines.slice(end + 1)].join("\n");
358
+ }
359
+ function dedupeSearchPath(value) {
360
+ const seen = new Set();
361
+ const entries = [];
362
+ for (const entry of value.split(path.delimiter)) {
363
+ if (entry.length === 0 || seen.has(entry))
364
+ continue;
365
+ seen.add(entry);
366
+ entries.push(entry);
367
+ }
368
+ return entries.join(path.delimiter);
369
+ }
470
370
  export function toggleBlock(existing, id, enabled) {
471
371
  parseBlocks(existing);
472
- const lines = existing.split(/\r?\n/);
473
372
  const out = [];
474
373
  let inBlock = false;
475
- for (const line of lines) {
374
+ for (const line of existing.split(/\r?\n/)) {
476
375
  if (!inBlock && line === BEGIN(id)) {
477
376
  inBlock = true;
478
377
  out.push(line);
@@ -485,15 +384,12 @@ export function toggleBlock(existing, id, enabled) {
485
384
  }
486
385
  if (inBlock) {
487
386
  const isComment = line.startsWith(DISABLED_PREFIX);
488
- if (enabled && isComment) {
387
+ if (enabled && isComment)
489
388
  out.push(line.slice(DISABLED_PREFIX.length));
490
- }
491
- else if (!enabled && !isComment) {
389
+ else if (!enabled && !isComment)
492
390
  out.push(`${DISABLED_PREFIX}${line}`);
493
- }
494
- else {
391
+ else
495
392
  out.push(line);
496
- }
497
393
  continue;
498
394
  }
499
395
  out.push(line);
@@ -529,17 +425,16 @@ function readCrontab(exec) {
529
425
  const result = exec.read();
530
426
  if (result.status === 0)
531
427
  return result.stdout ?? "";
532
- // The spawn itself failed to find the binary (ENOENT) — this is the only
533
- // case where "install/PATH the crontab binary" is the correct remedy.
428
+ // The spawn itself failed to find the binary (ENOENT) — the only case
429
+ // where "install/PATH the crontab binary" is the correct remedy.
534
430
  if (result.enoent) {
535
431
  throw new ConfigError("crontab -l failed: the `crontab` binary was not found on PATH.", "INVALID_CONFIG_FILE", "Install the `crontab` binary (e.g. cron/cronie/vixie-cron) or add one to PATH.");
536
432
  }
537
433
  // #910: a nonzero exit that says nothing at all, or whose stderr says
538
434
  // "no crontab" (BSD's "no crontab for <user>"; a supercronic-managed PATH
539
435
  // shim like OpenPalm's before any spool exists), is cron's own contract
540
- // for "empty crontab" — not evidence the binary is missing or broken. A
541
- // nonzero exit that DID say something else (a permission refusal) is
542
- // reported as what it said, below.
436
+ // for "empty crontab". A nonzero exit that DID say something else (a
437
+ // permission refusal) is reported as what it said.
543
438
  const stderr = (result.stderr ?? "").trim();
544
439
  if (((result.stdout ?? "").trim() === "" && stderr === "") || /no crontab/i.test(stderr))
545
440
  return "";
@@ -552,6 +447,7 @@ function writeCrontab(exec, content) {
552
447
  hint: "Ensure the `crontab` binary is on PATH and your shell can write the user crontab.",
553
448
  });
554
449
  }
450
+ /** Write the next crontab; when that fails, put the prior one back before rethrowing. */
555
451
  function replaceCrontab(exec, existing, next) {
556
452
  try {
557
453
  writeCrontab(exec, next);
@@ -568,24 +464,17 @@ function replaceCrontab(exec, existing, next) {
568
464
  }
569
465
  }
570
466
  function defaultCronExec() {
467
+ const run = (args, input) => {
468
+ const r = spawnSync("crontab", args, { encoding: "utf8", ...(input !== undefined ? { input } : {}) });
469
+ return {
470
+ status: r.status ?? 1,
471
+ stdout: r.stdout ?? "",
472
+ stderr: r.stderr ?? "",
473
+ enoent: r.error?.code === "ENOENT",
474
+ };
475
+ };
571
476
  return {
572
- read() {
573
- const r = spawnSync("crontab", ["-l"], { encoding: "utf8" });
574
- return {
575
- status: r.status ?? 1,
576
- stdout: r.stdout ?? "",
577
- stderr: r.stderr ?? "",
578
- enoent: r.error?.code === "ENOENT",
579
- };
580
- },
581
- write(content) {
582
- const r = spawnSync("crontab", ["-"], { encoding: "utf8", input: content });
583
- return {
584
- status: r.status ?? 1,
585
- stdout: r.stdout ?? "",
586
- stderr: r.stderr ?? "",
587
- enoent: r.error?.code === "ENOENT",
588
- };
589
- },
477
+ read: () => run(["-l"]),
478
+ write: (content) => run(["-"], content),
590
479
  };
591
480
  }