akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +731 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +361 -751
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -441
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -6,25 +6,24 @@
6
6
  *
7
7
  * Each task is registered under the `\akm\` Task Scheduler folder so the
8
8
  * backend never touches user-managed tasks. The full task definition is
9
- * sent through `schtasks /Create /TN \akm\<id> /XML <path>` so we can
9
+ * sent through `schtasks /Create /TN \akm\<id> /XML <path> /F` so we can
10
10
  * express triggers/principals/actions without quoting hell.
11
11
  *
12
12
  * Platform notes:
13
13
  * • `LogonType=InteractiveToken` means the task runs in the context of
14
14
  * the registering user only when they are logged in — there is no
15
15
  * stored password and the task will not fire at the lock screen.
16
- * • `<Principal>` records the current user SID so sync can detect identity
17
- * drift instead of silently accepting a task registered to another user.
16
+ * • `<Principal>` records the current user SID; it is part of the
17
+ * fingerprint in `<Source>`, so a sync run by another user re-registers
18
+ * the task under that user.
18
19
  * • `<DisallowStartIfOnBatteries>false</…>` and `<StopIfGoingOnBatteries>
19
- * false</…>` allow the task to run on battery — utility tasks would
20
- * otherwise be silently skipped on laptops.
21
- * • `MultipleInstancesPolicy=IgnoreNew` makes overlapping triggers safe:
22
- * while a task is still running, a new fire is dropped rather than
23
- * queued or run in parallel.
20
+ * false</…>` allow the task to run on battery.
21
+ * • `MultipleInstancesPolicy=IgnoreNew` makes overlapping triggers safe.
24
22
  * • `/Query /FO CSV /NH` (without `/V`) outputs three columns:
25
23
  * `TaskName,Next Run Time,Status` — so the regex anchors on the task
26
- * name as the leading quoted field. Adding `/V` would shift HostName
27
- * into column 0; we deliberately don't.
24
+ * name as the leading quoted field.
25
+ * • Task Scheduler runs a task with the account's own environment, so
26
+ * PATH is not carried anywhere; the descriptor holds directories only.
28
27
  *
29
28
  * Tests inject a fake exec + filesystem.
30
29
  */
@@ -37,143 +36,56 @@ import { ConfigError } from "../../core/errors.js";
37
36
  import { getTaskLogDir } from "../../core/paths.js";
38
37
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
39
38
  import { parseSchedule, translateToSchtasks } from "../schedule.js";
40
- import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "../scheduler-binding.js";
39
+ import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
41
40
  import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
42
41
  import { escapeXml, nodeExec, nodeFs, normalizeXmlForUtf16File, runOrThrow, } from "./exec-utils.js";
43
42
  export const DEFAULT_FOLDER_PREFIX = "\\akm\\";
44
43
  const SIGNATURE_PREFIX = "akm:v1:";
45
- const SCHTASKS_SNAPSHOT = Symbol("akm-schtasks-binding-snapshot");
46
44
  export function SCHTASKS_BACKEND(options = {}) {
47
- const exec = options.exec ?? defaultSchtasksExec();
45
+ const exec = options.exec ?? nodeExec();
48
46
  const fsLike = options.fs ?? defaultSchtasksFs();
49
47
  const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
50
48
  const logDir = options.logDir ?? getTaskLogDir();
51
49
  const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
52
50
  const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
53
- const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext, process.env.PATH ?? ""));
51
+ const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext));
54
52
  const userSid = options.userSid ?? resolveCurrentUserSid(exec);
55
53
  const taskName = (nativeId) => `${folder}${nativeId}`;
54
+ const xmlFor = (task, opts) => buildSchtasksXml(task, akmArgv, logDir, {
55
+ folderPrefix: folder,
56
+ contextPath: opts?.contextPath ?? defaultContextPath,
57
+ userSid,
58
+ binding: [...(opts?.binding ?? akmArgv)],
59
+ });
60
+ const queryXml = (nativeId) => runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(nativeId), "/XML"], {
61
+ message: (result) => `schtasks /Query /XML for "${taskName(nativeId)}" failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
62
+ }).stdout;
56
63
  return {
57
64
  name: "schtasks",
58
- install(task, opts, expected) {
59
- if (expected)
60
- assertSchedulerExpectationIdentity(expected, task);
65
+ install(task, opts) {
66
+ const xml = normalizeXmlForUtf16File(xmlFor(task, opts));
61
67
  const nativeId = schedulerBindingNativeId(task);
62
- const xml = normalizeXmlForUtf16File(buildSchtasksXml(task, akmArgv, logDir, {
63
- folderPrefix: folder,
64
- contextPath: opts?.contextPath ?? defaultContextPath,
65
- userSid,
66
- binding: [...(opts?.binding ?? akmArgv)],
67
- ...(opts?.target !== undefined ? { target: opts.target } : {}),
68
- }));
69
- const initialArtifact = expected
70
- ? assertSchedulerNativeArtifactCardinality(inspectSchtasksState(exec, folder, taskName).artifacts, nativeId, expected.state === "absent" ? 0 : 1)
71
- : undefined;
72
- const existingNativeId = expected ? initialArtifact?.nativeId : nativeId;
73
- const query = existingNativeId
74
- ? runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(existingNativeId), "/XML"], {
75
- isOk: (r) => r.status === 0 || isMissingTaskResult(r),
76
- message: (r) => `schtasks /Query failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
77
- })
78
- : { status: 1, stdout: "", stderr: "missing" };
79
- if (expected) {
80
- assertSchedulerMutationArtifact(query.status === 0 && existingNativeId ? schtasksArtifact(existingNativeId, query.stdout) : undefined, expected);
81
- }
82
- let previous;
83
- if (query.status === 0) {
84
- if (!expected) {
85
- assertSchedulerNativeArtifactOwner(nativeId, task, extractSchtasksInvocation(query.stdout)?.invocation);
86
- }
87
- const enabled = taskXmlEnabled(query.stdout);
88
- if (enabled === undefined) {
89
- throw new ConfigError(`schtasks /Query returned an unreadable definition for "${taskName(nativeId)}"; refusing to replace it.`, "INVALID_CONFIG_FILE");
90
- }
91
- previous = { xml: normalizeXmlForUtf16File(query.stdout), enabled };
92
- }
93
68
  fsLike.ensureDir(logDir);
94
69
  const tmpFile = path.join(fsLike.tmpdir(), `akm-task-${nativeId}-${Date.now()}.xml`);
95
70
  fsLike.writeFile(tmpFile, xml);
96
71
  try {
97
- if (expected) {
98
- const finalArtifact = assertSchedulerNativeArtifactCardinality(inspectSchtasksState(exec, folder, taskName).artifacts, nativeId, expected.state === "absent" ? 0 : 1);
99
- const finalNativeId = finalArtifact?.nativeId;
100
- const current = finalNativeId
101
- ? runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(finalNativeId), "/XML"], {
102
- isOk: (result) => result.status === 0 || isMissingTaskResult(result),
103
- message: (result) => `schtasks /Query failed during final install CAS (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
104
- })
105
- : undefined;
106
- assertSchedulerMutationArtifact(current?.status === 0 && finalNativeId ? schtasksArtifact(finalNativeId, current.stdout) : undefined, expected);
107
- }
108
- else {
109
- assertSchtasksArtifactUnchanged(exec, taskName(nativeId), nativeId, task);
110
- }
111
- try {
112
- // /F forces overwrite if a task with the same name exists.
113
- runOrThrow(exec, ["schtasks", "/Create", "/TN", taskName(nativeId), "/XML", tmpFile, "/F"], {
114
- message: (r) => `schtasks /Create failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
72
+ // /F forces overwrite if a task with the same name exists.
73
+ runOrThrow(exec, ["schtasks", "/Create", "/TN", taskName(nativeId), "/XML", tmpFile, "/F"], {
74
+ message: (r) => `schtasks /Create failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
75
+ });
76
+ if (!task.enabled) {
77
+ runOrThrow(exec, ["schtasks", "/Change", "/TN", taskName(nativeId), "/DISABLE"], {
78
+ message: (r) => `schtasks /Change /DISABLE failed: ${r.stderr || r.stdout || "no output"}.`,
115
79
  });
116
- if (!task.enabled) {
117
- runOrThrow(exec, ["schtasks", "/Change", "/TN", taskName(nativeId), "/DISABLE"], {
118
- message: (r) => `schtasks /Change /DISABLE failed: ${r.stderr || r.stdout || "no output"}.`,
119
- });
120
- }
121
- }
122
- catch (err) {
123
- const rollbackErrors = [];
124
- if (previous === undefined) {
125
- try {
126
- const remove = exec.run(["schtasks", "/Delete", "/TN", taskName(nativeId), "/F"]);
127
- if (remove.status !== 0 && !isMissingTaskResult(remove)) {
128
- rollbackErrors.push(new ConfigError(`schtasks /Delete during rollback failed: ${remove.stderr || remove.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
129
- }
130
- }
131
- catch (rollbackError) {
132
- rollbackErrors.push(rollbackError);
133
- }
134
- }
135
- else {
136
- try {
137
- fsLike.writeFile(tmpFile, previous.xml);
138
- runOrThrow(exec, ["schtasks", "/Create", "/TN", taskName(nativeId), "/XML", tmpFile, "/F"], {
139
- message: (r) => `schtasks /Create during rollback failed: ${r.stderr || r.stdout || "no output"}.`,
140
- });
141
- const stateFlag = previous.enabled ? "/ENABLE" : "/DISABLE";
142
- runOrThrow(exec, ["schtasks", "/Change", "/TN", taskName(nativeId), stateFlag], {
143
- message: (r) => `schtasks /Change ${stateFlag} during rollback failed: ${r.stderr || r.stdout || "no output"}.`,
144
- });
145
- }
146
- catch (rollbackError) {
147
- rollbackErrors.push(rollbackError);
148
- }
149
- }
150
- if (rollbackErrors.length > 0) {
151
- const message = err instanceof Error ? err.message : String(err);
152
- throw new AggregateError([err, ...rollbackErrors], `${message}; rollback for Task Scheduler task "${task.id}" was incomplete.`);
153
- }
154
- throw err;
155
80
  }
156
81
  }
157
82
  finally {
158
83
  fsLike.removeFile(tmpFile);
159
84
  }
160
85
  },
161
- uninstall(nativeId, expected) {
162
- if (expected) {
163
- assertSchedulerExpectationIdentity({ ...expected, state: "present" });
164
- const artifact = assertSchedulerNativeArtifactCardinality(inspectSchtasksState(exec, folder, taskName).artifacts, nativeId, 1);
165
- const currentNativeId = artifact?.nativeId ?? nativeId;
166
- const current = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(currentNativeId), "/XML"], {
167
- isOk: (result) => result.status === 0 || isMissingTaskResult(result),
168
- message: (result) => `schtasks /Query failed during final removal ownership check (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
169
- });
170
- if (current.status !== 0) {
171
- throw new ConfigError(`Task Scheduler artifact ${JSON.stringify(nativeId)} disappeared after coherent inspection.`, "INVALID_CONFIG_FILE");
172
- }
173
- assertSchedulerRemovalArtifact(currentNativeId, expected, extractSchtasksInvocation(current.stdout)?.invocation, installedSignature(current.stdout));
174
- }
86
+ uninstall(nativeId) {
175
87
  runOrThrow(exec, ["schtasks", "/Delete", "/TN", taskName(nativeId), "/F"], {
176
- isOk: (r) => r.status === 0 || /cannot find/i.test(r.stderr ?? ""),
88
+ isOk: (r) => r.status === 0 || isMissingTaskResult(r),
177
89
  message: (r) => `schtasks /Delete failed: ${r.stderr || r.stdout || "no output"}.`,
178
90
  });
179
91
  },
@@ -184,219 +96,38 @@ export function SCHTASKS_BACKEND(options = {}) {
184
96
  });
185
97
  },
186
98
  list() {
187
- return [...inspectSchtasksState(exec, folder, taskName).installed];
188
- },
189
- listForRebind() {
190
- return listSchtasksForRebind(exec, folder, taskName);
191
- },
192
- listNativeArtifacts() {
193
- return [...inspectSchtasksState(exec, folder, taskName).artifacts];
194
- },
195
- inspectBindings() {
196
- return inspectSchtasksState(exec, folder, taskName);
197
- },
198
- snapshotBindings(ids) {
199
- return snapshotSchtasksBindings(ids, exec, folder, taskName);
200
- },
201
- restoreBindings(snapshot, expectedCurrent) {
202
- restoreSchtasksBindings(snapshot, { exec, fsLike, folder, taskName }, expectedCurrent);
99
+ const listing = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
100
+ message: (result) => `schtasks /Query failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
101
+ });
102
+ const rows = [];
103
+ for (const line of (listing.stdout ?? "").split(/\r?\n/)) {
104
+ const name = line.match(/^"([^"]+)",/)?.[1];
105
+ if (!name?.startsWith(folder))
106
+ continue;
107
+ const nativeId = name.slice(folder.length);
108
+ const xml = queryXml(nativeId);
109
+ const parsed = extractSchtasksInvocation(xml);
110
+ if (!parsed)
111
+ continue;
112
+ rows.push({
113
+ id: schedulerLogicalBindingId(nativeId, parsed.invocation),
114
+ nativeId,
115
+ enabled: taskXmlEnabled(xml),
116
+ signature: taskXmlSignature(xml),
117
+ ...(parsed.target !== undefined ? { target: parsed.target } : {}),
118
+ binding: parsed.binding,
119
+ contextPath: parsed.contextPath,
120
+ invocation: parsed.invocation,
121
+ });
122
+ }
123
+ return rows;
203
124
  },
204
125
  expectedSignature(task, opts) {
205
- const signature = taskXmlSignature(buildSchtasksXml(task, akmArgv, logDir, {
206
- folderPrefix: folder,
207
- contextPath: opts?.contextPath ?? defaultContextPath,
208
- userSid,
209
- binding: [...(opts?.binding ?? akmArgv)],
210
- ...(opts?.target !== undefined ? { target: opts.target } : {}),
211
- }));
212
- if (signature === undefined)
213
- throw new Error("Failed to fingerprint generated Task Scheduler XML.");
214
- return signature;
126
+ return taskXmlSignature(xmlFor(task, opts));
215
127
  },
216
128
  };
217
129
  }
218
- function listSchtasksForRebind(exec, folder, taskName) {
219
- const result = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
220
- message: (query) => `schtasks /Query failed (exit ${query.status}): ${query.stderr || query.stdout || "no output"}.`,
221
- });
222
- const refs = [];
223
- for (const line of (result.stdout ?? "").split(/\r?\n/)) {
224
- const name = line.match(/^"([^"]+)",/)?.[1];
225
- if (!name?.startsWith(folder))
226
- continue;
227
- const id = name.slice(folder.length);
228
- const query = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(id), "/XML"], {
229
- message: (current) => `schtasks /Query /XML for "${taskName(id)}" failed (exit ${current.status}): ${current.stderr || current.stdout || "no output"}.`,
230
- });
231
- const signature = installedSignature(query.stdout);
232
- const installed = extractSchtasksInvocation(query.stdout);
233
- const ref = {
234
- id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
235
- ...(signature !== undefined ? { signature } : {}),
236
- ...(installed?.target !== undefined ? { target: installed.target } : {}),
237
- };
238
- Object.defineProperty(ref, "nativeId", { value: id });
239
- if (installed)
240
- Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
241
- refs.push(ref);
242
- }
243
- return refs;
244
- }
245
- function inspectSchtasksState(exec, folder, taskName) {
246
- const listing = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
247
- message: (result) => `schtasks /Query failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
248
- });
249
- const installed = [];
250
- const artifacts = [];
251
- for (const nativeId of schtasksNativeIds(listing.stdout, folder)) {
252
- const query = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(nativeId), "/XML"], {
253
- message: (result) => `schtasks /Query /XML for "${taskName(nativeId)}" failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
254
- });
255
- const artifact = schtasksArtifact(nativeId, query.stdout);
256
- artifacts.push(artifact);
257
- const parsed = extractSchtasksInvocation(query.stdout);
258
- if (!parsed)
259
- continue;
260
- const enabled = taskXmlEnabled(query.stdout);
261
- const ref = {
262
- id: schedulerLogicalBindingId(nativeId, parsed.invocation),
263
- ...(enabled !== undefined ? { enabled } : {}),
264
- ...(artifact.fingerprint !== undefined ? { signature: artifact.fingerprint } : {}),
265
- ...(parsed.target !== undefined ? { target: parsed.target } : {}),
266
- binding: parsed.binding,
267
- contextPath: parsed.contextPath,
268
- };
269
- Object.defineProperty(ref, "nativeId", { value: nativeId });
270
- Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
271
- installed.push(ref);
272
- }
273
- return Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) });
274
- }
275
- function schtasksNativeIds(stdout, folder) {
276
- const ids = [];
277
- for (const line of (stdout ?? "").split(/\r?\n/)) {
278
- const match = line.match(/^"([^"]+)",/);
279
- const name = match?.[1];
280
- if (name?.startsWith(folder))
281
- ids.push(name.slice(folder.length));
282
- }
283
- return ids;
284
- }
285
- function schtasksArtifact(nativeId, xml) {
286
- const parsed = extractSchtasksInvocation(xml);
287
- const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
288
- const artifact = parsed
289
- ? {
290
- nativeId,
291
- ...(owner !== undefined ? { bindingId: owner } : {}),
292
- invocation: Object.freeze([...parsed.invocation]),
293
- }
294
- : { nativeId };
295
- const fingerprint = installedSignature(xml);
296
- if (fingerprint !== undefined)
297
- Object.defineProperty(artifact, "fingerprint", { value: fingerprint });
298
- return artifact;
299
- }
300
- function snapshotSchtasksBindings(ids, exec, folder, taskName) {
301
- const listing = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
302
- message: (result) => `schtasks /Query failed while snapshotting bindings (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
303
- });
304
- const requestedKeys = new Set(ids.map(schedulerNativeArtifactKey));
305
- const enumeratedIds = new Set(ids);
306
- for (const nativeId of schtasksNativeIds(listing.stdout, folder)) {
307
- if (requestedKeys.has(schedulerNativeArtifactKey(nativeId)))
308
- enumeratedIds.add(nativeId);
309
- }
310
- const entries = [...enumeratedIds].map((id) => {
311
- const query = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(id), "/XML"], {
312
- isOk: (result) => result.status === 0 || isMissingTaskResult(result),
313
- message: (result) => `schtasks /Query failed while snapshotting "${taskName(id)}": ${result.stderr || result.stdout || "no output"}.`,
314
- });
315
- if (query.status !== 0)
316
- return Object.freeze({ id });
317
- const enabled = taskXmlEnabled(query.stdout);
318
- if (enabled === undefined) {
319
- throw new ConfigError(`schtasks /Query returned an unreadable definition while snapshotting "${taskName(id)}".`, "INVALID_CONFIG_FILE");
320
- }
321
- return Object.freeze({ id, xml: normalizeXmlForUtf16File(query.stdout), enabled });
322
- });
323
- const artifacts = entries.flatMap((entry) => entry.xml === undefined ? [] : [schtasksArtifact(entry.id, entry.xml)]);
324
- return Object.freeze({
325
- kind: SCHTASKS_SNAPSHOT,
326
- nativeIds: Object.freeze([...ids]),
327
- artifacts: Object.freeze(artifacts),
328
- entries: Object.freeze(entries),
329
- });
330
- }
331
- function restoreSchtasksBindings(snapshot, context, expectedCurrent) {
332
- if (!isSchtasksBindingSnapshot(snapshot)) {
333
- throw new ConfigError("Invalid Task Scheduler snapshot.", "INVALID_CONFIG_FILE");
334
- }
335
- const errors = [];
336
- let rollbackInventory;
337
- if (expectedCurrent) {
338
- try {
339
- rollbackInventory = inspectSchtasksState(context.exec, context.folder, context.taskName);
340
- }
341
- catch (error) {
342
- errors.push(error);
343
- }
344
- }
345
- for (const entry of snapshot.entries) {
346
- try {
347
- if (expectedCurrent) {
348
- if (!rollbackInventory)
349
- continue;
350
- const expected = expectedCurrent.find((candidate) => candidate.nativeId === entry.id);
351
- if (!expected) {
352
- throw new ConfigError(`Missing Task Scheduler rollback expectation for ${JSON.stringify(entry.id)}.`, "INVALID_CONFIG_FILE");
353
- }
354
- assertSchedulerRollbackArtifactCardinality(rollbackInventory.artifacts, expected);
355
- }
356
- restoreSchtasksEntry(entry, context);
357
- }
358
- catch (error) {
359
- errors.push(error);
360
- }
361
- }
362
- if (errors.length > 0) {
363
- throw new AggregateError(errors, `Failed to completely restore ${errors.length} Task Scheduler operation(s).`);
364
- }
365
- }
366
- function restoreSchtasksEntry(entry, context) {
367
- if (entry.xml === undefined) {
368
- runOrThrow(context.exec, ["schtasks", "/Delete", "/TN", context.taskName(entry.id), "/F"], {
369
- isOk: (result) => result.status === 0 || isMissingTaskResult(result),
370
- message: (result) => `schtasks /Delete failed while restoring "${context.taskName(entry.id)}": ${result.stderr || result.stdout || "no output"}.`,
371
- });
372
- return;
373
- }
374
- const tmpFile = path.join(context.fsLike.tmpdir(), `akm-task-restore-${schedulerNativeBindingId(entry.id)}-${Date.now()}.xml`);
375
- context.fsLike.writeFile(tmpFile, entry.xml);
376
- try {
377
- runOrThrow(context.exec, ["schtasks", "/Create", "/TN", context.taskName(entry.id), "/XML", tmpFile, "/F"], {
378
- message: (result) => `schtasks /Create failed while restoring "${context.taskName(entry.id)}": ${result.stderr || result.stdout || "no output"}.`,
379
- });
380
- const stateFlag = entry.enabled === false ? "/DISABLE" : "/ENABLE";
381
- runOrThrow(context.exec, ["schtasks", "/Change", "/TN", context.taskName(entry.id), stateFlag], {
382
- message: (result) => `schtasks /Change ${stateFlag} failed while restoring "${context.taskName(entry.id)}": ${result.stderr || result.stdout || "no output"}.`,
383
- });
384
- }
385
- finally {
386
- context.fsLike.removeFile(tmpFile);
387
- }
388
- }
389
- function isSchtasksBindingSnapshot(value) {
390
- return (typeof value === "object" &&
391
- value !== null &&
392
- value.kind === SCHTASKS_SNAPSHOT &&
393
- Array.isArray(value.entries));
394
- }
395
- /**
396
- * Recover the bundle name embedded as a `--bundle <bundle>` pair in the
397
- * PowerShell `<Arguments>` of an installed Task Scheduler definition. Returns
398
- * undefined for the primary/default form.
399
- */
130
+ /** The `--bundle <bundle>` token of an installed Task Scheduler definition; undefined for the primary form. */
400
131
  export function extractSchtasksTarget(xml) {
401
132
  return extractSchtasksInvocation(xml)?.target;
402
133
  }
@@ -415,12 +146,10 @@ function findPowerShellInvocationOperator(script) {
415
146
  for (let index = 0; index < script.length; index += 1) {
416
147
  const char = script[index];
417
148
  if (char === "'") {
418
- if (inSingleQuote && script[index + 1] === "'") {
149
+ if (inSingleQuote && script[index + 1] === "'")
419
150
  index += 1;
420
- }
421
- else {
151
+ else
422
152
  inSingleQuote = !inSingleQuote;
423
- }
424
153
  continue;
425
154
  }
426
155
  if (!inSingleQuote && char === "&" && /\s/.test(script[index + 1] ?? ""))
@@ -463,70 +192,32 @@ function parsePowerShellSingleQuotedArgs(script, start) {
463
192
  }
464
193
  return argv;
465
194
  }
466
- /**
467
- * Re-verify ownership of the native artifact immediately before `/Create /F`
468
- * overwrites it (the read/prepare-to-/Create ownership race at the native
469
- * boundary).
470
- *
471
- * This used to also compare the fresh query against the first read taken
472
- * earlier in `install` and refuse the whole operation if anything about the
473
- * XML had changed in between -- a freshness re-check on top of the identity
474
- * re-check right below it. That extra comparison never caught anything the
475
- * owner check didn't already cover (a foreign/changed owner still fails
476
- * `assertSchedulerNativeArtifactOwner`), and `task sync` is idempotent, so a
477
- * spurious refusal here only cost the user a rerun of a command they'd
478
- * already asked for. Dropped; the owner re-check (the actual corruption/
479
- * clobber guard -- no fallback exists if a foreign task got silently
480
- * overwritten) stays.
481
- */
482
- function assertSchtasksArtifactUnchanged(exec, nativeTaskName, nativeId, task) {
483
- // This runs outside install's rollback region: no scheduler mutation has
484
- // occurred, so a raced owner must remain untouched rather than be restored
485
- // from the stale first read.
486
- const current = runOrThrow(exec, ["schtasks", "/Query", "/TN", nativeTaskName, "/XML"], {
487
- isOk: (result) => result.status === 0 || isMissingTaskResult(result),
488
- message: (result) => `schtasks /Query failed during final ownership check (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
489
- });
490
- if (current.status === 0) {
491
- assertSchedulerNativeArtifactOwner(nativeId, task, extractSchtasksInvocation(current.stdout)?.invocation);
492
- }
493
- }
494
195
  export function buildSchtasksXml(task, akmArgv, logDir, options) {
495
196
  const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
496
197
  const now = options.now ? options.now() : new Date();
497
- const definition = buildSchtasksDefinition(task, options.binding ?? akmArgv, logDir, folder, options.contextPath, options.userSid, options.target);
498
- const triggerXml = renderSchtasksTrigger(definition.trigger, now);
499
- const nativeId = schedulerBindingNativeId(task);
500
- return schtasksTemplate
501
- .replaceAll("{{TASK_ID}}", escapeXml(nativeId))
502
- .replaceAll("{{FOLDER}}", escapeXml(folder))
503
- .replace("{{SIGNATURE}}", definition.signature)
504
- .replace("{{TRIGGER_XML}}", triggerXml)
505
- .replace('<Principal id="Author">', `<Principal id="Author">\n <UserId>${escapeXml(options.userSid)}</UserId>`)
506
- .replace("{{ENABLED}}", task.enabled ? "true" : "false")
507
- .replace("{{COMMAND}}", escapeXml(definition.command))
508
- .replace("{{ARGS}}", escapeXml(definition.args))
509
- .replace("{{LOG_PATH}}", escapeXml(definition.logPath));
510
- }
511
- function buildSchtasksDefinition(task, akmArgv, logDir, folder, contextPath, userSid, _target) {
512
- const spec = parseSchedule(task.cron, "schtasks");
513
- const trigger = translateToSchtasks(spec);
514
- const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
515
- const invoke = `& ${invocation.argv.map((arg) => quotePowerShell(arg)).join(" ")}`;
516
- const executionEvidence = task.executionEvidenceDigest === undefined
517
- ? ""
518
- : `$null=${quotePowerShell(`akm-workflow-evidence:${assertSchedulerExecutionEvidenceDigest(task.executionEvidenceDigest)}`)}; `;
519
- const script = `${executionEvidence}${invoke}; exit $LASTEXITCODE`;
198
+ const trigger = translateToSchtasks(parseSchedule(task.cron, "schtasks"));
199
+ const invocation = buildScheduledBindingInvocation(options.binding ?? akmArgv, options.contextPath, task.invocation);
200
+ const script = `& ${invocation.argv.map((arg) => quotePowerShell(arg)).join(" ")}; exit $LASTEXITCODE`;
520
201
  const command = "powershell.exe";
521
202
  const args = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script].map(quoteArg).join(" ");
522
203
  const nativeId = schedulerBindingNativeId(task);
523
204
  const logPath = path.join(logDir, `${nativeId}.log`);
524
- // The boundary changes on reinstall, and enabled state can change via /Change.
525
- // Keep both outside the stored definition fingerprint so no-op sync stays stable.
205
+ // The boundary changes on reinstall, and enabled state can change via
206
+ // /Change. Keep both outside the stored definition fingerprint so no-op
207
+ // sync stays stable.
526
208
  const fingerprint = createHash("sha256")
527
- .update(JSON.stringify({ folder, id: nativeId, trigger, command, args, logPath, userSid }))
209
+ .update(JSON.stringify({ folder, id: nativeId, trigger, command, args, logPath, userSid: options.userSid }))
528
210
  .digest("hex");
529
- return { trigger, command, args, logPath, signature: `${SIGNATURE_PREFIX}${fingerprint}` };
211
+ return schtasksTemplate
212
+ .replaceAll("{{TASK_ID}}", escapeXml(nativeId))
213
+ .replaceAll("{{FOLDER}}", escapeXml(folder))
214
+ .replace("{{SIGNATURE}}", `${SIGNATURE_PREFIX}${fingerprint}`)
215
+ .replace("{{TRIGGER_XML}}", renderSchtasksTrigger(trigger, now))
216
+ .replace('<Principal id="Author">', `<Principal id="Author">\n <UserId>${escapeXml(options.userSid)}</UserId>`)
217
+ .replace("{{ENABLED}}", task.enabled ? "true" : "false")
218
+ .replace("{{COMMAND}}", escapeXml(command))
219
+ .replace("{{ARGS}}", escapeXml(args))
220
+ .replace("{{LOG_PATH}}", escapeXml(logPath));
530
221
  }
531
222
  function renderSchtasksTrigger(trigger, now) {
532
223
  return expandNativeTriggers(trigger)
@@ -540,14 +231,7 @@ function expandNativeTriggers(trigger) {
540
231
  case "minuteValues":
541
232
  return trigger.minutes.map((atMinute) => ({ kind: "daily", atHour: 0, atMinute, repeatEveryMinutes: 60 }));
542
233
  case "hour":
543
- return [
544
- {
545
- kind: "daily",
546
- atHour: 0,
547
- atMinute: trigger.atMinute,
548
- repeatEveryMinutes: trigger.everyHours * 60,
549
- },
550
- ];
234
+ return [{ kind: "daily", atHour: 0, atMinute: trigger.atMinute, repeatEveryMinutes: trigger.everyHours * 60 }];
551
235
  case "hourValues":
552
236
  return trigger.hours.map((atHour) => ({ kind: "daily", atHour, atMinute: trigger.atMinute }));
553
237
  case "daily":
@@ -563,8 +247,8 @@ function renderNativeTrigger(trigger, startBoundary) {
563
247
  const repetition = trigger.repeatEveryMinutes === undefined
564
248
  ? ""
565
249
  : ` <Repetition>
566
- <Interval>${formatRepetitionInterval(trigger.repeatEveryMinutes)}</Interval>
567
- <Duration>${formatRepetitionDuration(trigger.repeatEveryMinutes)}</Duration>
250
+ <Interval>${formatMinuteDuration(trigger.repeatEveryMinutes)}</Interval>
251
+ <Duration>${formatMinuteDuration(24 * 60 - trigger.repeatEveryMinutes)}</Duration>
568
252
  <StopAtDurationEnd>false</StopAtDurationEnd>
569
253
  </Repetition>
570
254
  `;
@@ -617,18 +301,10 @@ ${months}
617
301
  </ScheduleByMonth>
618
302
  </CalendarTrigger>`;
619
303
  }
620
- function formatRepetitionInterval(minutes) {
621
- return formatMinuteDuration(minutes);
622
- }
623
- function formatRepetitionDuration(intervalMinutes) {
624
- return formatMinuteDuration(24 * 60 - intervalMinutes);
625
- }
626
304
  function formatMinuteDuration(minutes) {
627
305
  const hours = Math.floor(minutes / 60);
628
306
  const remainingMinutes = minutes % 60;
629
- const hourPart = hours > 0 ? `${hours}H` : "";
630
- const minutePart = remainingMinutes > 0 ? `${remainingMinutes}M` : "";
631
- return `PT${hourPart}${minutePart}`;
307
+ return `PT${hours > 0 ? `${hours}H` : ""}${remainingMinutes > 0 ? `${remainingMinutes}M` : ""}`;
632
308
  }
633
309
  function nextStartBoundary(trigger, now) {
634
310
  const boundary = new Date(now.getTime());
@@ -669,16 +345,11 @@ function nextStartBoundary(trigger, now) {
669
345
  }
670
346
  }
671
347
  function formatStartBoundary(d) {
672
- // Local-time ISO-8601 (no zone suffix) — Task Scheduler interprets a
673
- // bare boundary in the registering user's timezone, which matches what
674
- // a user typing "0 9 * * *" intuitively means ("9am local").
675
- const yyyy = d.getFullYear();
676
- const mm = String(d.getMonth() + 1).padStart(2, "0");
677
- const dd = String(d.getDate()).padStart(2, "0");
678
- const hh = String(d.getHours()).padStart(2, "0");
679
- const mi = String(d.getMinutes()).padStart(2, "0");
680
- const ss = String(d.getSeconds()).padStart(2, "0");
681
- return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}`;
348
+ // Local-time ISO-8601 (no zone suffix) — Task Scheduler interprets a bare
349
+ // boundary in the registering user's timezone, which matches what a user
350
+ // typing "0 9 * * *" means ("9am local").
351
+ const pad = (value) => String(value).padStart(2, "0");
352
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
682
353
  }
683
354
  function quoteArg(s) {
684
355
  if (/^[A-Za-z0-9_\-./@:%=+,\\]+$/.test(s))
@@ -690,110 +361,24 @@ function quoteArg(s) {
690
361
  function quotePowerShell(value) {
691
362
  return `'${value.replaceAll("'", "''")}'`;
692
363
  }
693
- function installedSignature(xml) {
694
- return taskXmlSignature(xml);
695
- }
696
364
  function isMissingTaskResult(result) {
697
365
  return /cannot find|not found/i.test(`${result.stderr ?? ""}\n${result.stdout ?? ""}`);
698
366
  }
367
+ // ── Installed-definition signature ──────────────────────────────────────────
368
+ //
369
+ // Task Scheduler rewrites a registered definition (schema defaults filled in,
370
+ // StartBoundary moved on), so a row is compared by the fingerprint akm stamps
371
+ // into `<Source>` — everything it renders except the enabled state — plus the
372
+ // enabled state `/Change` toggles. A task edited in Task Scheduler that keeps
373
+ // that fingerprint is left as it is.
699
374
  function taskXmlSignature(xml) {
700
- try {
701
- const document = parseXml(xml);
702
- const task = findChild(document, "Task");
703
- const triggers = findChild(task, "Triggers");
704
- const principals = findChild(task, "Principals");
705
- const settings = findChild(task, "Settings");
706
- const actions = findChild(task, "Actions");
707
- if (!triggers || !principals || !settings || !actions)
708
- return undefined;
709
- normalizeTriggerBoundaries(triggers);
710
- normalizeNativeDefaults(triggers, principals, settings);
711
- const enabledElement = findChild(settings, "Enabled");
712
- const enabledValue = enabledElement ? elementText(enabledElement).toLowerCase() : undefined;
713
- const enabled = enabledValue === undefined || enabledValue === "true" || enabledValue === "1";
714
- // Enabled is represented explicitly in the signature suffix. Removing it
715
- // here also treats an omitted Enabled element as its schema default, true.
716
- settings.children = settings.children.filter((child) => typeof child === "string" || child.name.toLowerCase() !== "enabled");
717
- const canonical = [triggers, principals, settings, actions].map(canonicalXmlElement).join("\n");
718
- const fingerprint = createHash("sha256").update(canonical).digest("hex");
719
- return signatureWithEnabled(`${SIGNATURE_PREFIX}${fingerprint}`, enabled);
720
- }
721
- catch {
722
- return undefined;
723
- }
375
+ const source = xml.match(/<(?:[\w.-]+:)?Source>([^<]*)<\//i)?.[1]?.trim() ?? "";
376
+ return `${decodeXml(source)}|enabled=${taskXmlEnabled(xml)}`;
724
377
  }
725
378
  function taskXmlEnabled(xml) {
726
- try {
727
- const document = parseXml(xml);
728
- const settings = findChild(findChild(document, "Task"), "Settings");
729
- if (!settings)
730
- return undefined;
731
- const enabledElement = findChild(settings, "Enabled");
732
- if (!enabledElement)
733
- return true;
734
- const enabled = elementText(enabledElement).toLowerCase();
735
- if (enabled === "true" || enabled === "1")
736
- return true;
737
- if (enabled === "false" || enabled === "0")
738
- return false;
739
- return undefined;
740
- }
741
- catch {
742
- return undefined;
743
- }
744
- }
745
- function parseXml(xml) {
746
- const document = { name: "#document", attributes: {}, children: [] };
747
- const stack = [document];
748
- const tokens = xml.match(/<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<[^>]+>|[^<]+/g) ?? [];
749
- for (const token of tokens) {
750
- if (token.startsWith("<!--") || token.startsWith("<?"))
751
- continue;
752
- if (token.startsWith("</")) {
753
- if (stack.length === 1)
754
- throw new Error("Unexpected XML closing tag.");
755
- const closingName = localXmlName(token.slice(2, -1).trim());
756
- const current = stack.pop();
757
- if (current?.name !== closingName)
758
- throw new Error("Mismatched XML closing tag.");
759
- continue;
760
- }
761
- if (token.startsWith("<")) {
762
- const selfClosing = /\/\s*>$/.test(token);
763
- const match = token.match(/^<\s*([^\s/>]+)([\s\S]*?)\/?\s*>$/);
764
- if (!match)
765
- throw new Error("Invalid XML opening tag.");
766
- const element = {
767
- name: localXmlName(match[1]),
768
- attributes: parseXmlAttributes(match[2]),
769
- children: [],
770
- };
771
- stack[stack.length - 1].children.push(element);
772
- if (!selfClosing)
773
- stack.push(element);
774
- continue;
775
- }
776
- const text = decodeXml(token.trim());
777
- if (text.length > 0)
778
- stack[stack.length - 1].children.push(text);
779
- }
780
- if (stack.length !== 1)
781
- throw new Error("Unclosed XML tag.");
782
- return document;
783
- }
784
- function parseXmlAttributes(raw) {
785
- const attributes = {};
786
- const pattern = /([^\s=]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
787
- for (const match of raw.matchAll(pattern)) {
788
- const name = localXmlName(match[1]);
789
- if (match[1] === "xmlns" || match[1].startsWith("xmlns:"))
790
- continue;
791
- attributes[name] = decodeXml(match[2] ?? match[3] ?? "");
792
- }
793
- return attributes;
794
- }
795
- function localXmlName(name) {
796
- return name.slice(name.lastIndexOf(":") + 1);
379
+ const settings = xml.match(/<(?:[\w.-]+:)?Settings>([\s\S]*?)<\/(?:[\w.-]+:)?Settings>/i)?.[1] ?? "";
380
+ const value = settings.match(/<(?:[\w.-]+:)?Enabled>\s*([^<]*?)\s*<\//i)?.[1]?.toLowerCase();
381
+ return value !== "false" && value !== "0";
797
382
  }
798
383
  function decodeXml(value) {
799
384
  return value.replace(/&(?:#(\d+)|#x([\da-f]+)|amp|lt|gt|quot|apos);/gi, (entity, decimal, hex) => {
@@ -817,124 +402,20 @@ function decodeXml(value) {
817
402
  }
818
403
  });
819
404
  }
820
- function findChild(parent, name) {
821
- return parent?.children.find((child) => typeof child !== "string" && child.name.toLowerCase() === name.toLowerCase());
822
- }
823
- function elementText(element) {
824
- return element.children.filter((child) => typeof child === "string").join("");
825
- }
826
- function normalizeTriggerBoundaries(triggers) {
827
- for (const trigger of triggers.children) {
828
- if (typeof trigger === "string")
829
- continue;
830
- const boundary = findChild(trigger, "StartBoundary");
831
- if (!boundary)
832
- continue;
833
- const time = elementText(boundary).match(/T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?/);
834
- if (!time)
835
- continue;
836
- const secondsSinceMidnight = Number(time[1]) * 3600 + Number(time[2]) * 60 + Number(time[3]);
837
- const interval = findChild(findChild(trigger, "Repetition"), "Interval");
838
- const intervalSeconds = interval ? parseIsoDurationSeconds(elementText(interval)) : undefined;
839
- boundary.children = [
840
- intervalSeconds === undefined
841
- ? `dynamic-date;time=${time[1]}:${time[2]}:${time[3]}`
842
- : `dynamic-cycle;phase-seconds=${secondsSinceMidnight % intervalSeconds}`,
843
- ];
844
- }
845
- }
846
- const MATERIALIZED_SETTING_DEFAULTS = {
847
- allowstartondemand: "true",
848
- allowhardterminate: "true",
849
- startwhenavailable: "false",
850
- runonlyifnetworkavailable: "false",
851
- hidden: "false",
852
- runonlyifidle: "false",
853
- waketorun: "false",
854
- executiontimelimit: "PT72H",
855
- priority: "7",
856
- compatibility: "Vista",
857
- useunifiedschedulingengine: "true",
858
- disallowstartonremoteappsession: "false",
859
- volatile: "false",
860
- };
861
- const MATERIALIZED_IDLE_DEFAULTS = {
862
- duration: "PT10M",
863
- waittimeout: "PT1H",
864
- stoponidleend: "true",
865
- restartonidle: "false",
866
- };
867
- function normalizeNativeDefaults(triggers, principals, settings) {
868
- for (const principal of elementChildren(principals)) {
869
- principal.children = principal.children.filter((child) => {
870
- if (typeof child === "string")
871
- return true;
872
- return child.name.toLowerCase() !== "runlevel" || elementText(child).toLowerCase() !== "leastprivilege";
873
- });
874
- }
875
- settings.children = settings.children.filter((child) => {
876
- if (typeof child === "string")
877
- return true;
878
- const name = child.name.toLowerCase();
879
- if (name === "idlesettings")
880
- return !containsOnlyDefaults(child, MATERIALIZED_IDLE_DEFAULTS);
881
- const defaultValue = MATERIALIZED_SETTING_DEFAULTS[name];
882
- return defaultValue === undefined || elementText(child) !== defaultValue;
883
- });
884
- for (const trigger of elementChildren(triggers)) {
885
- trigger.children = trigger.children.filter((child) => {
886
- if (typeof child === "string")
887
- return true;
888
- const name = child.name.toLowerCase();
889
- if (name === "enabled")
890
- return elementText(child).toLowerCase() !== "true";
891
- return name !== "executiontimelimit" || elementText(child) !== "PT72H";
892
- });
893
- }
894
- }
895
405
  function resolveCurrentUserSid(exec) {
896
406
  const result = runOrThrow(exec, ["whoami", "/user", "/fo", "csv", "/nh"], {
897
407
  message: (r) => `whoami /user failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
898
408
  });
899
409
  const match = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/\bS-\d+(?:-\d+){2,}\b/i);
900
- if (!match) {
410
+ if (!match)
901
411
  throw new ConfigError("whoami /user returned no Windows user SID.", "INVALID_CONFIG_FILE");
902
- }
903
412
  return `S${match[0].slice(1)}`;
904
413
  }
905
- function elementChildren(element) {
906
- return element.children.filter((child) => typeof child !== "string");
907
- }
908
- function containsOnlyDefaults(element, defaults) {
909
- const children = elementChildren(element);
910
- return (children.length > 0 &&
911
- children.length === element.children.length &&
912
- children.every((child) => defaults[child.name.toLowerCase()] === elementText(child)));
913
- }
914
- function parseIsoDurationSeconds(value) {
915
- const match = value.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i);
916
- if (!match)
917
- return undefined;
918
- const seconds = Number(match[1] ?? 0) * 3600 + Number(match[2] ?? 0) * 60 + Number(match[3] ?? 0);
919
- return seconds > 0 ? seconds : undefined;
920
- }
921
- function canonicalXmlElement(element) {
922
- const attributes = Object.entries(element.attributes).sort(([a], [b]) => a.localeCompare(b));
923
- const children = element.children.map((child) => typeof child === "string" ? ["text", child] : ["element", canonicalXmlElement(child)]);
924
- children.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
925
- return JSON.stringify([element.name, attributes, children]);
926
- }
927
- function signatureWithEnabled(signature, enabled) {
928
- return `${signature}|enabled=${enabled ? "true" : "false"}`;
929
- }
930
- function defaultSchtasksExec() {
931
- return nodeExec();
932
- }
933
414
  function defaultSchtasksFs() {
934
415
  return {
935
416
  ...nodeFs(),
936
417
  writeFile(file, content) {
937
- fs.writeFileSync(file, `\uFEFF${content}`, { encoding: "utf16le" });
418
+ fs.writeFileSync(file, `${content}`, { encoding: "utf16le" });
938
419
  },
939
420
  removeFile(file) {
940
421
  try {