akm-cli 0.9.16 → 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 (403) hide show
  1. package/CHANGELOG.md +2101 -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/consolidate-system.md +4 -11
  15. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  17. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  18. package/dist/assets/templates/html/health.html +3 -5
  19. package/dist/cli/retired-commands.js +1 -1
  20. package/dist/cli/shared.js +6 -2
  21. package/dist/cli/unknown-flags.js +24 -1
  22. package/dist/cli.js +68 -10
  23. package/dist/commands/agent/agent-dispatch.js +1 -1
  24. package/dist/commands/command/command-execution.js +24 -62
  25. package/dist/commands/feedback-cli.js +0 -1
  26. package/dist/commands/health/accept-rate.js +6 -0
  27. package/dist/commands/health/archive-usage.js +92 -0
  28. package/dist/commands/health/checks.js +83 -74
  29. package/dist/commands/health/config-skew.js +38 -0
  30. package/dist/commands/health/data-dir-usage.js +25 -13
  31. package/dist/commands/health/egress.js +54 -0
  32. package/dist/commands/health/html-report.js +1 -42
  33. package/dist/commands/health/improve-metrics.js +136 -591
  34. package/dist/commands/health/md-report.js +1 -6
  35. package/dist/commands/health/plugin-staleness.js +53 -3
  36. package/dist/commands/health/renderers.js +12 -4
  37. package/dist/commands/health/report-view-model.js +14 -120
  38. package/dist/commands/health/types-improve.js +4 -19
  39. package/dist/commands/health/windows.js +64 -74
  40. package/dist/commands/health.js +145 -143
  41. package/dist/commands/improve/consolidate/chunking.js +26 -117
  42. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  43. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  44. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  45. package/dist/commands/improve/consolidate.js +589 -1127
  46. package/dist/commands/improve/content-hash.js +16 -24
  47. package/dist/commands/improve/distill/content-repair.js +18 -100
  48. package/dist/commands/improve/distill-guards.js +20 -81
  49. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  50. package/dist/commands/improve/distill.js +608 -1041
  51. package/dist/commands/improve/eligibility.js +126 -390
  52. package/dist/commands/improve/execution.js +8 -10
  53. package/dist/commands/improve/extract-prompt.js +1 -2
  54. package/dist/commands/improve/extract.js +487 -1046
  55. package/dist/commands/improve/feedback-valence.js +0 -25
  56. package/dist/commands/improve/improve-cli.js +75 -169
  57. package/dist/commands/improve/improve-result-file.js +10 -66
  58. package/dist/commands/improve/improve-strategies.js +52 -4
  59. package/dist/commands/improve/improve-usage-report.js +18 -64
  60. package/dist/commands/improve/improve.js +480 -1074
  61. package/dist/commands/improve/ledger.js +119 -0
  62. package/dist/commands/improve/locks.js +2 -8
  63. package/dist/commands/improve/loop-stages.js +415 -1073
  64. package/dist/commands/improve/memory/derived-ref.js +12 -77
  65. package/dist/commands/improve/memory/memory-belief.js +16 -118
  66. package/dist/commands/improve/memory/memory-improve.js +266 -14
  67. package/dist/commands/improve/outcome-loop.js +28 -156
  68. package/dist/commands/improve/planner.js +5 -15
  69. package/dist/commands/improve/preparation.js +779 -2319
  70. package/dist/commands/improve/proactive-maintenance.js +34 -101
  71. package/dist/commands/improve/reflect-noise.js +104 -280
  72. package/dist/commands/improve/reflect.js +642 -1353
  73. package/dist/commands/improve/retrieval-gate.js +127 -0
  74. package/dist/commands/improve/retrieval-scope.js +92 -0
  75. package/dist/commands/improve/salience.js +41 -240
  76. package/dist/commands/improve/session-asset.js +19 -100
  77. package/dist/commands/improve/stage.js +322 -0
  78. package/dist/commands/lint/base-linter.js +37 -15
  79. package/dist/commands/proposal/drain.js +261 -578
  80. package/dist/commands/proposal/proposal-cli.js +19 -20
  81. package/dist/commands/proposal/proposal-types.js +31 -24
  82. package/dist/commands/proposal/proposal.js +38 -8
  83. package/dist/commands/proposal/propose.js +134 -160
  84. package/dist/commands/proposal/repository.js +1097 -1394
  85. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  86. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  87. package/dist/commands/proposal/validators/proposals.js +22 -89
  88. package/dist/commands/read/curate.js +105 -462
  89. package/dist/commands/read/knowledge.js +3 -2
  90. package/dist/commands/read/search-cli.js +16 -33
  91. package/dist/commands/read/search.js +17 -23
  92. package/dist/commands/read/show.js +57 -108
  93. package/dist/commands/sources/bundle-cli.js +25 -2
  94. package/dist/commands/sources/bundle-config-ops.js +4 -0
  95. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  96. package/dist/commands/sources/info.js +127 -29
  97. package/dist/commands/sources/installed-stashes.js +197 -746
  98. package/dist/commands/sources/schema-repair.js +98 -129
  99. package/dist/commands/sources/source-add.js +62 -12
  100. package/dist/commands/sources/source-manage.js +9 -2
  101. package/dist/commands/sources/stash-cli.js +24 -4
  102. package/dist/commands/tasks/explain.js +10 -13
  103. package/dist/commands/tasks/tasks-cli.js +12 -13
  104. package/dist/commands/tasks/tasks.js +350 -936
  105. package/dist/commands/tasks/validate.js +26 -24
  106. package/dist/commands/workflow/plan.js +22 -29
  107. package/dist/commands/workflow-cli.js +4 -4
  108. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  109. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  110. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  111. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  112. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  113. package/dist/core/adapter/execution-source.js +17 -29
  114. package/dist/core/asset/asset-placement.js +4 -13
  115. package/dist/core/asset/frontmatter.js +106 -1
  116. package/dist/core/asset/resolve-ref.js +1 -1
  117. package/dist/core/bundle-id.js +42 -5
  118. package/dist/core/bundle-rename.js +285 -0
  119. package/dist/core/config/config-io.js +1 -2
  120. package/dist/core/config/config-schema.js +9 -34
  121. package/dist/core/config/config-walker.js +1 -1
  122. package/dist/core/config/config.js +184 -111
  123. package/dist/core/config/engine-semantics.js +0 -2
  124. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  125. package/dist/core/config/schema/embedding.js +20 -5
  126. package/dist/core/config/schema/engines.js +5 -0
  127. package/dist/core/config/schema/execution.js +1 -1
  128. package/dist/core/config/schema/experimental.js +1 -1
  129. package/dist/core/config/schema/improve-processes.js +54 -125
  130. package/dist/core/config/schema/improve.js +4 -42
  131. package/dist/core/config/schema/index-config.js +9 -48
  132. package/dist/core/config/schema/scheduler.js +12 -12
  133. package/dist/core/config/schema/search.js +6 -22
  134. package/dist/core/env-secret-ref.js +0 -1
  135. package/dist/core/errors.js +8 -9
  136. package/dist/core/file-change.js +13 -5
  137. package/dist/core/file-lock.js +76 -173
  138. package/dist/core/improve-result.js +35 -7
  139. package/dist/core/improve-types.js +0 -1
  140. package/dist/core/logs-db.js +2 -2
  141. package/dist/core/loopback.js +7 -12
  142. package/dist/core/non-task-input.js +20 -0
  143. package/dist/core/parse.js +13 -16
  144. package/dist/core/paths.js +0 -24
  145. package/dist/core/redaction.js +109 -2
  146. package/dist/core/run-lock.js +2 -5
  147. package/dist/core/spawn-env.js +1 -1
  148. package/dist/core/state/migrations.js +123 -61
  149. package/dist/core/state-db-scope.js +2 -4
  150. package/dist/core/state-db.js +126 -692
  151. package/dist/core/time.js +0 -20
  152. package/dist/core/type-presentation.js +1 -9
  153. package/dist/core/write-source.js +294 -1005
  154. package/dist/execution/input-contract.js +1 -1
  155. package/dist/execution/resolved-request.js +135 -689
  156. package/dist/execution/source.js +63 -257
  157. package/dist/execution/target-ref.js +1 -1
  158. package/dist/indexer/bundle-identity-guard.js +2 -2
  159. package/dist/indexer/db/llm-cache.js +2 -2
  160. package/dist/indexer/ensure-index.js +77 -73
  161. package/dist/indexer/index-rebuild-lock.js +3 -11
  162. package/dist/indexer/index-writer-lock.js +8 -17
  163. package/dist/indexer/index-written-assets.js +141 -154
  164. package/dist/indexer/indexer.js +400 -1124
  165. package/dist/indexer/links/declared-links.js +90 -0
  166. package/dist/indexer/materialize-embeddings.js +60 -397
  167. package/dist/indexer/passes/memory-inference.js +96 -90
  168. package/dist/indexer/passes/metadata.js +132 -219
  169. package/dist/indexer/read-preflight.js +0 -7
  170. package/dist/indexer/scan/doc-to-entry.js +2 -3
  171. package/dist/indexer/scan/drain-dir.js +1 -1
  172. package/dist/indexer/search/db-search.js +190 -590
  173. package/dist/indexer/search/fts-query.js +30 -41
  174. package/dist/indexer/search/ranking.js +28 -154
  175. package/dist/indexer/search/search-attribution.js +12 -32
  176. package/dist/indexer/search/search-fields.js +11 -15
  177. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  178. package/dist/indexer/search/search-source.js +1 -4
  179. package/dist/indexer/usage/usage-events.js +36 -7
  180. package/dist/indexer/walk/walker.js +3 -4
  181. package/dist/integrations/agent/engine-fallback.js +23 -40
  182. package/dist/integrations/agent/engine-resolution.js +93 -183
  183. package/dist/integrations/agent/execution.js +507 -0
  184. package/dist/integrations/agent/model-map.js +28 -156
  185. package/dist/integrations/agent/request-lowering.js +66 -141
  186. package/dist/integrations/agent/runner-dispatch.js +143 -321
  187. package/dist/integrations/agent/runner.js +54 -14
  188. package/dist/integrations/lockfile.js +53 -101
  189. package/dist/llm/client.js +18 -6
  190. package/dist/llm/embedders/deterministic.js +2 -3
  191. package/dist/llm/embedders/profile.js +71 -0
  192. package/dist/llm/embedders/remote.js +11 -17
  193. package/dist/llm/feature-gate.js +0 -8
  194. package/dist/llm/index-passes.js +3 -5
  195. package/dist/llm/memory-infer.js +1 -2
  196. package/dist/llm/structured-call.js +5 -24
  197. package/dist/output/generic-render.js +23 -11
  198. package/dist/output/html-render.js +13 -10
  199. package/dist/output/render-registry.js +3 -32
  200. package/dist/output/shapes/helpers.js +25 -38
  201. package/dist/output/shapes/passthrough.js +1 -9
  202. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  203. package/dist/output/text/command-format.js +69 -31
  204. package/dist/output/text/helpers.js +1 -1
  205. package/dist/output/text/migrate.js +5 -14
  206. package/dist/output/text/proposal-format.js +48 -3
  207. package/dist/output/text/show-format.js +13 -17
  208. package/dist/output/text/workflow-format.js +0 -32
  209. package/dist/output/text.js +2 -0
  210. package/dist/registry/factory.js +4 -19
  211. package/dist/registry/network.js +66 -220
  212. package/dist/registry/providers/index.js +0 -2
  213. package/dist/registry/providers/skills-sh.js +3 -14
  214. package/dist/registry/providers/static-index.js +24 -26
  215. package/dist/registry/resolve.js +55 -131
  216. package/dist/scripts/akm-migrate-node.js +42948 -92369
  217. package/dist/scripts/akm-migrate.js +42935 -92354
  218. package/dist/setup/registry-stash-loader.js +4 -13
  219. package/dist/setup/semantic-assets.js +3 -44
  220. package/dist/setup/setup.js +1 -1
  221. package/dist/setup/steps/connection.js +5 -6
  222. package/dist/setup/steps/platforms.js +2 -2
  223. package/dist/setup/steps/tasks.js +25 -15
  224. package/dist/sources/provider-factory.js +17 -18
  225. package/dist/sources/providers/filesystem.js +2 -3
  226. package/dist/sources/providers/git-install.js +7 -1
  227. package/dist/sources/providers/git-provider.js +0 -3
  228. package/dist/sources/providers/git-stash.js +83 -21
  229. package/dist/sources/providers/npm.js +2 -4
  230. package/dist/sources/providers/provider-utils.js +5 -10
  231. package/dist/sources/providers/website.js +0 -2
  232. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  233. package/dist/sources/website-url.js +2 -2
  234. package/dist/storage/database.js +9 -35
  235. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  236. package/dist/storage/repositories/index-connection.js +39 -72
  237. package/dist/storage/repositories/index-entries-repository.js +131 -129
  238. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  239. package/dist/storage/repositories/index-entry-schema.js +101 -268
  240. package/dist/storage/repositories/index-fts-repository.js +86 -256
  241. package/dist/storage/repositories/index-links-repository.js +143 -0
  242. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  243. package/dist/storage/repositories/index-meta-repository.js +6 -4
  244. package/dist/storage/repositories/index-schema.js +257 -325
  245. package/dist/storage/repositories/index-utility-repository.js +8 -29
  246. package/dist/storage/repositories/index-vec-repository.js +133 -414
  247. package/dist/storage/repositories/outcome-repository.js +2 -1
  248. package/dist/storage/repositories/proposals-repository.js +104 -1
  249. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  250. package/dist/storage/repositories/salience-repository.js +1 -19
  251. package/dist/storage/repositories/task-history-repository.js +26 -4
  252. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  253. package/dist/storage/sqlite-migrations.js +136 -0
  254. package/dist/storage/sqlite-pragmas.js +11 -9
  255. package/dist/storage/sqlite-transaction.js +170 -0
  256. package/dist/storage/state-db-integrity.js +130 -0
  257. package/dist/tasks/activation-config.js +134 -62
  258. package/dist/tasks/backends/cron.js +191 -302
  259. package/dist/tasks/backends/exec-utils.js +2 -5
  260. package/dist/tasks/backends/launchd.js +141 -748
  261. package/dist/tasks/backends/schtasks.js +119 -623
  262. package/dist/tasks/prepare/prepare-support.js +5 -15
  263. package/dist/tasks/prepare/prepare.js +0 -2
  264. package/dist/tasks/resolve-akm-bin.js +20 -79
  265. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  266. package/dist/tasks/run/load-task.js +1 -1
  267. package/dist/tasks/scheduler-binding.js +20 -238
  268. package/dist/tasks/scheduler-invocation.js +136 -244
  269. package/dist/tasks/scheduler-lock.js +53 -0
  270. package/dist/tasks/scheduler-sync.js +368 -679
  271. package/dist/tasks/source/parse-task-source.js +55 -9
  272. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  273. package/dist/tasks/source/task-to-v4.js +464 -88
  274. package/dist/workflows/authoring/authoring.js +3 -12
  275. package/dist/workflows/compile.js +211 -0
  276. package/dist/workflows/concurrency-policy.js +13 -74
  277. package/dist/workflows/exec/child-invocation.js +3 -17
  278. package/dist/workflows/exec/child-workflow.js +32 -141
  279. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  280. package/dist/workflows/exec/environment.js +98 -0
  281. package/dist/workflows/exec/exec-unit.js +33 -140
  282. package/dist/workflows/exec/frozen-judge.js +7 -59
  283. package/dist/workflows/exec/native-executor.js +82 -341
  284. package/dist/workflows/exec/param-secrets.js +29 -47
  285. package/dist/workflows/exec/run-workflow.js +154 -387
  286. package/dist/workflows/exec/scheduler.js +9 -36
  287. package/dist/workflows/exec/step-work.js +127 -430
  288. package/dist/workflows/exec/unit-dispatch.js +11 -63
  289. package/dist/workflows/exec/unit-writer.js +8 -52
  290. package/dist/workflows/exec/worktree.js +39 -273
  291. package/dist/workflows/freeze/child-output-references.js +4 -15
  292. package/dist/workflows/freeze/environment.js +99 -92
  293. package/dist/workflows/freeze/freeze.js +172 -0
  294. package/dist/workflows/freeze/step-values.js +19 -21
  295. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  296. package/dist/workflows/freeze/targets/command.js +10 -33
  297. package/dist/workflows/freeze/targets/script.js +5 -12
  298. package/dist/workflows/freeze/targets/shell.js +3 -6
  299. package/dist/workflows/freeze/targets/task.js +25 -80
  300. package/dist/workflows/freeze/task-bindings.js +20 -67
  301. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  302. package/dist/workflows/ir/params.js +6 -51
  303. package/dist/workflows/ir/plan-hash.js +2 -34
  304. package/dist/workflows/parser.js +140 -43
  305. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  306. package/dist/workflows/renderer.js +36 -69
  307. package/dist/workflows/resource-limits.js +12 -120
  308. package/dist/workflows/runtime/agent-identity.js +8 -40
  309. package/dist/workflows/runtime/run-outputs.js +3 -6
  310. package/dist/workflows/runtime/run-plan.js +316 -0
  311. package/dist/workflows/runtime/runs.js +48 -200
  312. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  313. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  314. package/dist/workflows/validate-summary.js +2 -7
  315. package/docs/integration/bundling-akm.md +49 -42
  316. package/docs/migration/README.md +1 -0
  317. package/docs/migration/release-notes/0.9.17.md +43 -0
  318. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  319. package/docs/reference/cli.md +232 -135
  320. package/docs/reference/configuration.md +71 -57
  321. package/docs/reference/data-and-telemetry.md +20 -21
  322. package/docs/reference/tasks.md +105 -39
  323. package/docs/reference/workflow-schema.md +14 -18
  324. package/docs/reference/workflows.md +6 -9
  325. package/package.json +1 -1
  326. package/schemas/akm-config.json +115 -738
  327. package/schemas/akm-workflow.json +1 -0
  328. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  329. package/dist/assets/prompts/contradiction-judge.md +0 -33
  330. package/dist/assets/prompts/graph-extract-system.md +0 -1
  331. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  332. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  333. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  334. package/dist/commands/health/advisories.js +0 -150
  335. package/dist/commands/health/metrics.js +0 -329
  336. package/dist/commands/health/surfaces.js +0 -102
  337. package/dist/commands/improve/anti-collapse.js +0 -83
  338. package/dist/commands/improve/collapse-detector.js +0 -432
  339. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  340. package/dist/commands/improve/consolidate/merge.js +0 -149
  341. package/dist/commands/improve/distill/promote-memory.js +0 -291
  342. package/dist/commands/improve/distill/quality-gate.js +0 -337
  343. package/dist/commands/improve/eval-cases.js +0 -52
  344. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  345. package/dist/commands/improve/proposal-envelope.js +0 -31
  346. package/dist/commands/improve/run-context.js +0 -123
  347. package/dist/commands/improve/shared.js +0 -31
  348. package/dist/commands/improve/source-identity.js +0 -28
  349. package/dist/commands/improve/triage.js +0 -96
  350. package/dist/commands/proposal/drain-policies.js +0 -151
  351. package/dist/commands/sources/update-transaction.js +0 -220
  352. package/dist/core/action-contributors.js +0 -28
  353. package/dist/core/config/config-version-shim.js +0 -101
  354. package/dist/core/fs-txn.js +0 -405
  355. package/dist/core/lexical-score.js +0 -25
  356. package/dist/core/maintenance-barrier.js +0 -167
  357. package/dist/execution/executable-identity.js +0 -105
  358. package/dist/execution/guarded-source.js +0 -427
  359. package/dist/indexer/db/graph-db.js +0 -444
  360. package/dist/indexer/graph/graph-boost.js +0 -427
  361. package/dist/indexer/graph/graph-dedup.js +0 -95
  362. package/dist/indexer/graph/graph-extraction.js +0 -1108
  363. package/dist/indexer/search/name-match.js +0 -35
  364. package/dist/indexer/search/ranking-contributors.js +0 -515
  365. package/dist/indexer/search/ranking-types.js +0 -4
  366. package/dist/indexer/walk/project-context.js +0 -192
  367. package/dist/integrations/agent/execution-cascade.js +0 -566
  368. package/dist/integrations/agent/execution-definitions.js +0 -202
  369. package/dist/integrations/agent/execution-lowering.js +0 -841
  370. package/dist/integrations/agent/execution-preparation.js +0 -98
  371. package/dist/integrations/agent/inline-execution.js +0 -74
  372. package/dist/llm/graph-extract.js +0 -728
  373. package/dist/llm/metadata-enhance.js +0 -96
  374. package/dist/registry/create-provider-registry.js +0 -29
  375. package/dist/registry/pinned-request-helper.js +0 -247
  376. package/dist/registry/pinned-transport.js +0 -717
  377. package/dist/sources/providers/index.js +0 -14
  378. package/dist/storage/engines/sqlite-migrations.js +0 -271
  379. package/dist/storage/repositories/canaries-repository.js +0 -107
  380. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  381. package/dist/storage/repositories/registry-cache.js +0 -113
  382. package/dist/tasks/scheduler-sync-preview.js +0 -52
  383. package/dist/tasks/source/task-to-v3.js +0 -507
  384. package/dist/workflows/freeze/resolve-steps.js +0 -86
  385. package/dist/workflows/freeze/source-freeze.js +0 -64
  386. package/dist/workflows/ir/compile.js +0 -321
  387. package/dist/workflows/ir/environment-v4.js +0 -330
  388. package/dist/workflows/ir/freeze-v4.js +0 -153
  389. package/dist/workflows/ir/schema-v4.js +0 -745
  390. package/dist/workflows/ir/schema.js +0 -354
  391. package/dist/workflows/program/schema.js +0 -77
  392. package/dist/workflows/runtime/checkin.js +0 -57
  393. package/dist/workflows/runtime/plan-classifier.js +0 -196
  394. package/dist/workflows/runtime/unit-checkin.js +0 -45
  395. package/dist/workflows/runtime/unit-phases.js +0 -20
  396. package/dist/workflows/schema.js +0 -4
  397. package/dist/workflows/source-ir/compile.js +0 -200
  398. package/dist/workflows/source-ir/program.js +0 -50
  399. package/dist/workflows/source-ir/result.js +0 -26
  400. package/dist/workflows/source-ir/schema.js +0 -786
  401. package/dist/workflows/source-ir/triggers.js +0 -79
  402. package/dist/workflows/source-ir/uses.js +0 -40
  403. 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
  }