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.
- package/CHANGELOG.md +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +83 -21
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -31
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/tasks/source/task-to-v3.js +0 -507
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -77
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
* Each handler exported here is a pure function that performs the real work;
|
|
8
8
|
* `src/cli.ts` wraps these in citty `defineCommand`s and shapes their return
|
|
9
9
|
* values via `output()`.
|
|
10
|
+
*
|
|
11
|
+
* Every command that writes the native scheduler (`add`, `enable`,
|
|
12
|
+
* `disable`, `sync`, `prune --yes`) runs under the one scheduler lock
|
|
13
|
+
* (`withSchedulerLock`): read the installed rows once, plan against the
|
|
14
|
+
* sources, then install, update, or remove row by row. One row that fails is
|
|
15
|
+
* reported and the rest still apply.
|
|
10
16
|
*/
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
17
|
import fs from "node:fs";
|
|
13
18
|
import path from "node:path";
|
|
14
19
|
import { stringify as yamlStringify } from "yaml";
|
|
@@ -17,28 +22,28 @@ import { assetPathForName } from "../../core/asset/asset-placement.js";
|
|
|
17
22
|
import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
18
23
|
import { conceptIdFromTypeName, isFullRefInput } from "../../core/asset/resolve-ref.js";
|
|
19
24
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
20
|
-
import { loadConfig } from "../../core/config/config.js";
|
|
21
|
-
import {
|
|
25
|
+
import { loadConfig, mutateConfig, resetConfigCache } from "../../core/config/config.js";
|
|
26
|
+
import { bundleKeyForContentRoot, resolveActiveConfiguredSources, resolveConfiguredSources, } from "../../core/config/config-sources.js";
|
|
22
27
|
import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../core/config/experimental.js";
|
|
23
28
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
24
29
|
import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
|
|
25
30
|
import { warn } from "../../core/warn.js";
|
|
26
|
-
import { commitWriteTargetBoundary,
|
|
31
|
+
import { commitWriteTargetBoundary, isWriteCapableSourceKind, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
27
32
|
import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
|
|
28
33
|
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
29
|
-
import {
|
|
34
|
+
import { enabledRefsFromInstalled, schedulerEnabledRefs, setSchedulerRefEnabled } from "../../tasks/activation-config.js";
|
|
30
35
|
import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
|
|
31
36
|
import { prepareTaskV3Execution } from "../../tasks/prepare/prepare.js";
|
|
32
|
-
import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
|
|
37
|
+
import { isCheckoutInvocation, resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
|
|
33
38
|
import { createExecutionProvenanceContext } from "../../tasks/run/provenance.js";
|
|
34
39
|
import { runTask } from "../../tasks/run/run-task.js";
|
|
35
40
|
import { readTaskHistory } from "../../tasks/run/task-history.js";
|
|
36
41
|
import { exitCodeForStatus } from "../../tasks/run/task-result.js";
|
|
37
42
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { renderSchedulerPlanPreview,
|
|
43
|
+
import { compileTaskSchedulerBindings, schedulerBindingNativeId, } from "../../tasks/scheduler-binding.js";
|
|
44
|
+
import { readLegacySchedulerContext, scheduledRowEnvironment } from "../../tasks/scheduler-invocation.js";
|
|
45
|
+
import { withSchedulerLock } from "../../tasks/scheduler-lock.js";
|
|
46
|
+
import { compileSchedulerSources, installedRowNativeId, installedRowOwner, installedRowScope, planSchedulerSync, renderSchedulerPlanPreview, scheduledInvocationBundle, } from "../../tasks/scheduler-sync.js";
|
|
42
47
|
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
43
48
|
import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
|
|
44
49
|
import { normaliseTaskConceptId, normaliseTaskId } from "../../tasks/task-id.js";
|
|
@@ -47,27 +52,17 @@ import { resolveImproveStrategy } from "../improve/improve-strategies.js";
|
|
|
47
52
|
export async function akmTasksAdd(input, deps = {}) {
|
|
48
53
|
const id = normaliseTaskId(input.id);
|
|
49
54
|
assertTaskAddTargetShape(input);
|
|
50
|
-
// Validate the schedule for the active backend before writing anything.
|
|
51
|
-
//
|
|
52
|
-
// from `deps.backend` when present — retiring the `_setBackendsForTests` seam.
|
|
55
|
+
// Validate the schedule for the active backend before writing anything. An
|
|
56
|
+
// injected backend (tests) carries its own name.
|
|
53
57
|
const backend = deps.backend?.name ?? backendNameForPlatform();
|
|
54
58
|
parseSchedule(input.schedule, backend);
|
|
55
|
-
const bundle = resolveTaskBundle(input.target
|
|
56
|
-
const writeTarget = bundle.resolved;
|
|
59
|
+
const bundle = resolveTaskBundle(input.target);
|
|
57
60
|
const stashDir = bundle.stashDir;
|
|
58
|
-
const installOpts = bundle.installTarget !== undefined ? { target: bundle.installTarget } : undefined;
|
|
59
61
|
const typeRoot = path.join(stashDir, "tasks");
|
|
60
62
|
const assetPath = assetPathForName("task", typeRoot, id);
|
|
61
63
|
if (!isWithin(assetPath, typeRoot)) {
|
|
62
64
|
throw new UsageError(`Resolved task path escapes the stash: "${id}".`, "PATH_ESCAPE_VIOLATION");
|
|
63
65
|
}
|
|
64
|
-
if (fs.existsSync(assetPath) && !input.force) {
|
|
65
|
-
throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or delete its file and run \`akm task sync\` first.`, "RESOURCE_ALREADY_EXISTS");
|
|
66
|
-
}
|
|
67
|
-
const sourceExpectation = captureTaskSourceExpectation(assetPath, stashDir);
|
|
68
|
-
if (sourceExpectation.state === "present" && !input.force) {
|
|
69
|
-
throw new UsageError(`Task "${id}" appeared while add was preparing. Pass --force only after reviewing the current owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
70
|
-
}
|
|
71
66
|
const yaml = renderTaskYaml({
|
|
72
67
|
id,
|
|
73
68
|
schedule: input.schedule,
|
|
@@ -94,12 +89,9 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
94
89
|
config: bundle.config,
|
|
95
90
|
resolveAsset: taskProjectionAssetResolver(bundle.config, bundle.bundleName, stashDir),
|
|
96
91
|
});
|
|
97
|
-
|
|
98
|
-
// schedule entry's inputs reach the generated invocation.
|
|
99
|
-
const taskBindings = compileTaskSchedulerBindings({
|
|
92
|
+
const bindings = compileTaskSchedulerBindings({
|
|
100
93
|
id,
|
|
101
94
|
qualifiedRef,
|
|
102
|
-
...(bundle.installTarget ? { bundleTarget: bundle.installTarget } : {}),
|
|
103
95
|
schedules: parsedTask.v4.schedule.map((schedule) => ({
|
|
104
96
|
cron: schedule.cron,
|
|
105
97
|
ordinal: schedule.ordinal,
|
|
@@ -107,136 +99,49 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
107
99
|
inputs: schedule.inputs,
|
|
108
100
|
})),
|
|
109
101
|
});
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
102
|
+
const first = bindings[0];
|
|
103
|
+
if (!first)
|
|
112
104
|
throw new UsageError(`Task "${id}" has no schedulable trigger.`, "INVALID_FLAG_VALUE");
|
|
113
|
-
const ref = taskAssetRef(id);
|
|
114
105
|
const sched = deps.backend ?? selectBackend();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
106
|
+
return withSchedulerLock(async () => {
|
|
107
|
+
if (fs.existsSync(assetPath) && !input.force) {
|
|
108
|
+
throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or delete its file and run \`akm task sync\` first.`, "RESOURCE_ALREADY_EXISTS");
|
|
109
|
+
}
|
|
110
|
+
// Native ids are shared by every bundle and installation: refuse before
|
|
111
|
+
// writing anything when another one already schedules this id.
|
|
112
|
+
const scope = bundleScope(bundle.bundleName, stashDir);
|
|
113
|
+
const installed = await listInstalledRows(sched);
|
|
114
|
+
for (const binding of bindings) {
|
|
115
|
+
const row = installed.find((candidate) => installedRowNativeId(candidate) === schedulerBindingNativeId(binding));
|
|
116
|
+
if (row && !installedRowScope(row, [scope])) {
|
|
117
|
+
throw new UsageError(`Task id "${id}" is already scheduled from ${installedRowOwner(row)}; rename the task or disable the existing one first.`, "RESOURCE_ALREADY_EXISTS");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await ensureSchedulerChoice({ backend: sched });
|
|
121
|
+
if (input.disabled)
|
|
122
|
+
setSchedulerRefEnabled(qualifiedRef, false);
|
|
123
|
+
await (deps.writeAsset ?? writeAssetToSource)(bundle.resolved.source, bundle.resolved.config, taskAssetRef(id), yaml);
|
|
124
|
+
(deps.commitBoundary ?? commitWriteTargetBoundary)(bundle.resolved, `Update tasks/${id}`);
|
|
125
|
+
if (!input.disabled)
|
|
126
|
+
setSchedulerRefEnabled(qualifiedRef, true);
|
|
127
|
+
const sync = await akmTasksSync(deps, bundle.bundleName, { rebind: input.rebind === true });
|
|
128
|
+
for (const warning of sync.warnings ?? [])
|
|
129
|
+
warn(warning);
|
|
130
|
+
const failure = sync.failures.find((candidate) => candidate.ref === qualifiedRef);
|
|
131
|
+
if (failure) {
|
|
132
|
+
throw new ConfigError(`Task "${id}" was written to ${assetPath}${input.disabled ? "" : " and enabled"}, but it could not be scheduled: ${failure.reason}`, "INVALID_CONFIG_FILE", "Fix the cause and run `akm task sync`.");
|
|
133
|
+
}
|
|
126
134
|
return {
|
|
127
135
|
id,
|
|
128
136
|
ref: conceptIdFromTypeName("task", id),
|
|
129
137
|
path: assetPath,
|
|
130
138
|
bundleDir: stashDir,
|
|
131
|
-
schedule:
|
|
132
|
-
enabled:
|
|
139
|
+
schedule: first.cron,
|
|
140
|
+
enabled: input.disabled !== true,
|
|
133
141
|
backend,
|
|
134
142
|
target: task.target,
|
|
135
143
|
};
|
|
136
|
-
}
|
|
137
|
-
const transaction = await prepareTaskAddSchedulerTransaction({
|
|
138
|
-
id,
|
|
139
|
-
installTarget: bundle.installTarget,
|
|
140
|
-
ownerTarget: bundle.bundleName,
|
|
141
|
-
installOpts,
|
|
142
|
-
taskBindings,
|
|
143
|
-
sched,
|
|
144
|
-
deps,
|
|
145
|
-
rebind: input.rebind === true,
|
|
146
|
-
});
|
|
147
|
-
let sourceMutationReceipt;
|
|
148
|
-
let sourcePublished = false;
|
|
149
|
-
let sourcePublicationAttempted = false;
|
|
150
|
-
const publishSource = async () => {
|
|
151
|
-
assertTaskSourceExpectation(sourceExpectation);
|
|
152
|
-
sourcePublicationAttempted = true;
|
|
153
|
-
await writeAsset(writeTarget.source, writeTarget.config, ref, yaml);
|
|
154
|
-
const publishedSource = captureTaskSourceExpectation(assetPath, stashDir);
|
|
155
|
-
if (publishedSource.state !== "present" || publishedSource.sha256 !== hashTaskSource(yaml)) {
|
|
156
|
-
throw new UsageError(`Task source ${JSON.stringify(assetPath)} changed during publication.`, "RESOURCE_ALREADY_EXISTS");
|
|
157
|
-
}
|
|
158
|
-
sourceMutationReceipt = publishedSource;
|
|
159
|
-
sourcePublished = true;
|
|
160
|
-
transaction.publishRuntime?.();
|
|
161
|
-
};
|
|
162
|
-
await applySchedulerTransaction(sched, transaction.operations, {
|
|
163
|
-
initialExpectations: transaction.initialExpectations,
|
|
164
|
-
assertReadSet: () => {
|
|
165
|
-
if (sourcePublished) {
|
|
166
|
-
if (!sourceMutationReceipt) {
|
|
167
|
-
throw new ConfigError("Published task source lost its transaction receipt.", "INVALID_CONFIG_FILE");
|
|
168
|
-
}
|
|
169
|
-
assertTaskSourceExpectation(sourceMutationReceipt);
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
assertTaskSourceExpectation(sourceExpectation);
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
beforeOperation: async (_operation, index) => {
|
|
176
|
-
if (index === transaction.publishOperationIndex && !sourcePublished)
|
|
177
|
-
await publishSource();
|
|
178
|
-
},
|
|
179
|
-
afterOperations: () => commitBoundary(writeTarget, `Update tasks/${id}`),
|
|
180
|
-
rollbackExternal: async () => {
|
|
181
|
-
if (!sourcePublicationAttempted)
|
|
182
|
-
return;
|
|
183
|
-
if (!sourceMutationReceipt) {
|
|
184
|
-
const current = captureTaskSourceExpectation(assetPath, stashDir);
|
|
185
|
-
if (sameTaskSourceExpectation(current, sourceExpectation))
|
|
186
|
-
return;
|
|
187
|
-
if (current.state !== "present" || current.sha256 !== hashTaskSource(yaml)) {
|
|
188
|
-
throw new UsageError(`Task source ${JSON.stringify(assetPath)} has an unowned publication state; refusing rollback over a possible concurrent owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
189
|
-
}
|
|
190
|
-
sourceMutationReceipt = current;
|
|
191
|
-
}
|
|
192
|
-
assertTaskSourceExpectation(sourceMutationReceipt);
|
|
193
|
-
try {
|
|
194
|
-
if (sourceExpectation.state === "absent") {
|
|
195
|
-
await deleteAsset(writeTarget.source, writeTarget.config, ref);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
await writeAsset(writeTarget.source, writeTarget.config, ref, sourceExpectation.content);
|
|
199
|
-
const providerRestored = captureTaskSourceExpectation(assetPath, stashDir);
|
|
200
|
-
if (providerRestored.state !== "present" || providerRestored.sha256 !== sourceExpectation.sha256) {
|
|
201
|
-
// Provider writers conventionally normalize a trailing newline.
|
|
202
|
-
// Rollback is byte-exact, so finish the already-owned restore with
|
|
203
|
-
// the frozen bytes before checking the physical source state.
|
|
204
|
-
fs.writeFileSync(assetPath, Buffer.from(sourceExpectation.bytesBase64, "base64"));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
assertTaskSourceRestored(sourceExpectation);
|
|
208
|
-
commitBoundary(writeTarget, `Restore tasks/${id}`);
|
|
209
|
-
}
|
|
210
|
-
catch (cause) {
|
|
211
|
-
// A write/commit seam may report failure after it has already restored
|
|
212
|
-
// the exact source bytes. Prove that state before allowing native
|
|
213
|
-
// rollback; otherwise preserve the possible concurrent source owner.
|
|
214
|
-
try {
|
|
215
|
-
assertTaskSourceRestored(sourceExpectation);
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
throw cause;
|
|
219
|
-
}
|
|
220
|
-
throw new TaskSourceRestoredBoundaryError(cause);
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
suppressNativeRollbackWhenExternalFails: true,
|
|
224
|
-
allowNativeRollbackAfterExternalFailure: (error) => error instanceof TaskSourceRestoredBoundaryError,
|
|
225
144
|
});
|
|
226
|
-
// Publish the host-local grant only after source and native state commit.
|
|
227
|
-
// A failure before this point therefore leaves any new binding inert at
|
|
228
|
-
// its scheduled-fire gate instead of authorizing partially committed code.
|
|
229
|
-
setSchedulerRefEnabled("task", qualifiedRef, true);
|
|
230
|
-
return {
|
|
231
|
-
id,
|
|
232
|
-
ref: conceptIdFromTypeName("task", id),
|
|
233
|
-
path: assetPath,
|
|
234
|
-
bundleDir: stashDir,
|
|
235
|
-
schedule: taskBinding.cron,
|
|
236
|
-
enabled: true,
|
|
237
|
-
backend,
|
|
238
|
-
target: task.target,
|
|
239
|
-
};
|
|
240
145
|
}
|
|
241
146
|
function assertTaskAddTargetShape(input) {
|
|
242
147
|
const hasCommand = input.command !== undefined &&
|
|
@@ -264,11 +169,6 @@ export async function akmTasksRun(id, options = {}) {
|
|
|
264
169
|
const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
|
|
265
170
|
const resolvedId = taskIdForAdapter(parsed.id, adapterId);
|
|
266
171
|
const scheduled = options.scheduled === true;
|
|
267
|
-
const conceptId = adapterId === "akm" ? `tasks/${resolvedId}` : resolvedId;
|
|
268
|
-
const qualifiedRef = makeBundleRef(bundle.source.name, conceptId);
|
|
269
|
-
if (scheduled && !isSchedulerRefEnabled(loadConfig(), "task", qualifiedRef)) {
|
|
270
|
-
throw new UsageError(`Scheduled task ${JSON.stringify(qualifiedRef)} is not enabled in local scheduler config; run \`akm task enable ${qualifiedRef}\`.`, "INVALID_FLAG_VALUE");
|
|
271
|
-
}
|
|
272
172
|
// D5 "Construction" (spec docs/plans/specs/p1b-model-extraction.md §1.2/
|
|
273
173
|
// §5.2): built ONCE at this invocation boundary. eventSource is "task"
|
|
274
174
|
// whether or not --scheduled was passed (§1.6 D5-N1) — scheduled stays a
|
|
@@ -342,17 +242,41 @@ function resolveTaskActivation(ref, target, requireSchedulable) {
|
|
|
342
242
|
}
|
|
343
243
|
return { qualifiedRef: makeBundleRef(bundle.source.name, conceptId), sourcePath, bundleName: bundle.source.name };
|
|
344
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* A config that predates `scheduler.enabled` means "keep what is installed":
|
|
247
|
+
* write that as the host's explicit list before one ref is added or removed,
|
|
248
|
+
* so enabling or disabling a single task never silently drops the others.
|
|
249
|
+
*/
|
|
250
|
+
async function ensureSchedulerChoice(deps) {
|
|
251
|
+
const config = loadConfig();
|
|
252
|
+
if (schedulerEnabledRefs(config) !== undefined)
|
|
253
|
+
return;
|
|
254
|
+
initializeSchedulerChoice(await (deps.backend ?? selectBackend()).list(), config, { write: true });
|
|
255
|
+
}
|
|
256
|
+
function initializeSchedulerChoice(installed, config, options) {
|
|
257
|
+
const refs = enabledRefsFromInstalled(installed, config);
|
|
258
|
+
if (options.write) {
|
|
259
|
+
mutateConfig((current) => ({ ...current, scheduler: { ...current.scheduler, enabled: [...refs] } }));
|
|
260
|
+
resetConfigCache();
|
|
261
|
+
}
|
|
262
|
+
warn(`This host had no scheduler.enabled list; ${options.write ? "it was" : "it would be"} initialized from the ` +
|
|
263
|
+
`${refs.length} installed akm scheduler ${refs.length === 1 ? "binding" : "bindings"}` +
|
|
264
|
+
`${refs.length > 0 ? `: ${refs.join(", ")}` : ""}. Use \`akm task enable\`/\`akm task disable <ref>\` to change it.`);
|
|
265
|
+
return refs;
|
|
266
|
+
}
|
|
345
267
|
export async function akmTasksEnable(ref, options = {}, deps = {}) {
|
|
346
|
-
|
|
347
|
-
const activation = setSchedulerRefEnabled("task", resolved.qualifiedRef, true);
|
|
348
|
-
const sync = await akmTasksSync(deps, resolved.bundleName);
|
|
349
|
-
return { ref: resolved.qualifiedRef, enabled: true, changed: activation.changed, sync };
|
|
268
|
+
return setTaskActivation(resolveTaskActivation(ref, options.target, true), true, deps);
|
|
350
269
|
}
|
|
351
270
|
export async function akmTasksDisable(ref, options = {}, deps = {}) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return
|
|
271
|
+
return setTaskActivation(resolveTaskActivation(ref, options.target, false), false, deps);
|
|
272
|
+
}
|
|
273
|
+
async function setTaskActivation(resolved, enabled, deps) {
|
|
274
|
+
return withSchedulerLock(async () => {
|
|
275
|
+
await ensureSchedulerChoice(deps);
|
|
276
|
+
const activation = setSchedulerRefEnabled(resolved.qualifiedRef, enabled);
|
|
277
|
+
const sync = await akmTasksSync(deps, resolved.bundleName);
|
|
278
|
+
return { ref: resolved.qualifiedRef, enabled, changed: activation.changed, sync };
|
|
279
|
+
});
|
|
356
280
|
}
|
|
357
281
|
export async function akmTasksHistory(input) {
|
|
358
282
|
const limit = input.limit !== undefined && input.limit > 0 ? input.limit : 50;
|
|
@@ -365,340 +289,288 @@ export async function akmTasksHistory(input) {
|
|
|
365
289
|
}
|
|
366
290
|
/**
|
|
367
291
|
* Reconcile host-local scheduler activation with authored tasks/workflows.
|
|
368
|
-
*
|
|
369
|
-
* •
|
|
370
|
-
*
|
|
371
|
-
* •
|
|
372
|
-
* •
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
292
|
+
*
|
|
293
|
+
* • without a bundle, every enabled configured filesystem/git bundle; with
|
|
294
|
+
* one, only that bundle
|
|
295
|
+
* • only refs listed in `scheduler.enabled` are read and scheduled
|
|
296
|
+
* • a row is installed when missing, rewritten when its rendered definition
|
|
297
|
+
* differs, and removed when its source is gone or no longer enabled
|
|
298
|
+
* • rows another bundle or installation owns are never touched, and a row
|
|
299
|
+
* whose source fails to compile is left as it is
|
|
300
|
+
* • a disabled bundle's rows are removed by an unscoped sync
|
|
376
301
|
*/
|
|
302
|
+
export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
303
|
+
return withSchedulerLock(async () => {
|
|
304
|
+
const { sched, plan, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
305
|
+
const failed = new Set();
|
|
306
|
+
const failures = [...plan.failures];
|
|
307
|
+
for (const operation of plan.operations) {
|
|
308
|
+
try {
|
|
309
|
+
if (operation.kind === "remove")
|
|
310
|
+
await sched.uninstall(operation.nativeId);
|
|
311
|
+
else
|
|
312
|
+
await sched.install(operation.binding, operation.options);
|
|
313
|
+
}
|
|
314
|
+
catch (cause) {
|
|
315
|
+
if (operation.kind === "remove") {
|
|
316
|
+
failed.add(operation.id);
|
|
317
|
+
failures.push({ path: operation.nativeId, reason: errorMessage(cause) });
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
failed.add(operation.binding.id);
|
|
321
|
+
failures.push({
|
|
322
|
+
path: operation.binding.source,
|
|
323
|
+
ref: operation.binding.logicalSource.ref,
|
|
324
|
+
reason: errorMessage(cause),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const applied = (ids) => ids.filter((id) => !failed.has(id));
|
|
330
|
+
return {
|
|
331
|
+
installed: applied(plan.installed),
|
|
332
|
+
updated: applied(plan.updated),
|
|
333
|
+
removed: applied(plan.removed),
|
|
334
|
+
unchanged: [...plan.unchanged],
|
|
335
|
+
skipped: [],
|
|
336
|
+
backend: sched.name,
|
|
337
|
+
failures: failures.map((failure) => ({ ...failure })),
|
|
338
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
|
377
342
|
/**
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* `applySchedulerSyncPlan`. Shared by `akmTasksSync` (applies the plan) and
|
|
381
|
-
* `akmTasksSyncPlan` (#849 `--dry-run`, never applies it) so the two paths
|
|
382
|
-
* can never drift on what "the plan" means. `prepared?.publish`, the one
|
|
383
|
-
* deferred write-producing closure in this pipeline, is returned but never
|
|
384
|
-
* invoked here — only `applySchedulerSyncPlan` may call it.
|
|
343
|
+
* `akm task sync --dry-run` (#849): the exact plan `akmTasksSync` would
|
|
344
|
+
* apply, previewed. It writes nothing: no config, no row.
|
|
385
345
|
*/
|
|
346
|
+
export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
347
|
+
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, { ...options, dryRun: true });
|
|
348
|
+
return renderSchedulerPlanPreview(sched.name, plan.operations, plan.unchanged, plan.failures);
|
|
349
|
+
}
|
|
386
350
|
async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
387
|
-
|
|
351
|
+
let config = loadConfig();
|
|
388
352
|
const sched = deps.backend ?? selectBackend();
|
|
389
|
-
|
|
390
|
-
|
|
353
|
+
const installed = await listInstalledRows(sched);
|
|
354
|
+
// A config that predates `scheduler.enabled` means "keep what is
|
|
355
|
+
// installed": the akm-written rows become the host's choice (written
|
|
356
|
+
// unless this is a dry run) before the desired set, which would otherwise
|
|
357
|
+
// remove them, is computed.
|
|
358
|
+
let enabledRefs = schedulerEnabledRefs(config);
|
|
359
|
+
if (enabledRefs === undefined) {
|
|
360
|
+
enabledRefs = initializeSchedulerChoice(installed, config, { write: options.dryRun !== true });
|
|
361
|
+
if (options.dryRun !== true)
|
|
362
|
+
config = loadConfig();
|
|
391
363
|
}
|
|
392
|
-
const inspection = await sched.inspectBindings({ rebind: options.rebind === true });
|
|
393
|
-
const rawEntries = [...inspection.installed];
|
|
394
|
-
const allEntries = rawEntries.map((entry) => {
|
|
395
|
-
const contextPath = "contextPath" in entry ? entry.contextPath : "";
|
|
396
|
-
// #846: recover the resolved bundle path this entry was installed
|
|
397
|
-
// under from its own scheduler-context descriptor. Any failure (no
|
|
398
|
-
// descriptor, unreadable, corrupt, owned by another user) leaves
|
|
399
|
-
// ownerBundlePath unset — belongsToBundle must never treat that as
|
|
400
|
-
// "mine".
|
|
401
|
-
const ownerBundlePath = contextPath ? resolveInstalledOwnerPath(contextPath) : undefined;
|
|
402
|
-
return {
|
|
403
|
-
...entry,
|
|
404
|
-
...(entry.nativeId !== undefined ? { nativeId: entry.nativeId } : {}),
|
|
405
|
-
...(entry.invocation !== undefined ? { invocation: Object.freeze([...entry.invocation]) } : {}),
|
|
406
|
-
binding: "binding" in entry ? [...entry.binding] : [],
|
|
407
|
-
contextPath,
|
|
408
|
-
...(ownerBundlePath !== undefined ? { ownerBundlePath } : {}),
|
|
409
|
-
};
|
|
410
|
-
});
|
|
411
|
-
const nativeArtifacts = inspection.artifacts;
|
|
412
|
-
const configuredSources = resolveConfiguredSources(config);
|
|
413
364
|
const activeSources = resolveActiveConfiguredSources(config);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const plan = emptySchedulerSyncPlan(inactiveOperations);
|
|
421
|
-
return { sched, plan, sourceSnapshots: Object.freeze([]), prepared: undefined, warnings: [] };
|
|
422
|
-
}
|
|
423
|
-
const selectedNames = sourceNames.length > 0 ? sourceNames : [undefined];
|
|
424
|
-
const enabled = activeSchedulerActivations(config);
|
|
425
|
-
const enabledActivations = new Set(enabled.map((activation) => `${activation.kind}\0${activation.ref}`));
|
|
426
|
-
const preparedSets = [];
|
|
427
|
-
for (const sourceName of selectedNames) {
|
|
428
|
-
const resolved = resolveTaskReadBundle(undefined, sourceName);
|
|
429
|
-
const stashDir = resolved.source.path;
|
|
430
|
-
const syncTarget = sourceName !== undefined && !isPrimaryStashPath(stashDir) ? sourceName : undefined;
|
|
431
|
-
const common = {
|
|
432
|
-
sourceRoot: stashDir,
|
|
433
|
-
adapterId: resolved.source.adapterId ?? detectAdapterId(stashDir),
|
|
434
|
-
bundleName: resolved.source.name,
|
|
435
|
-
...(syncTarget === undefined ? { bundlePath: path.resolve(stashDir) } : {}),
|
|
436
|
-
...(syncTarget ? { bundleTarget: syncTarget } : {}),
|
|
437
|
-
backend: sched.name,
|
|
438
|
-
installed: allEntries,
|
|
439
|
-
nativeArtifacts,
|
|
440
|
-
inspection: Object.freeze({ installed: allEntries, artifacts: nativeArtifacts }),
|
|
441
|
-
enabledActivations,
|
|
442
|
-
rebind: options.rebind === true,
|
|
443
|
-
config,
|
|
444
|
-
resolveAsset: taskProjectionAssetResolver(config, resolved.source.name, stashDir),
|
|
445
|
-
};
|
|
446
|
-
preparedSets.push({
|
|
447
|
-
common,
|
|
448
|
-
preparedSources: await prepareSchedulerSyncSourceSet(common),
|
|
449
|
-
...(syncTarget ? { syncTarget } : {}),
|
|
450
|
-
});
|
|
365
|
+
if (bundleTarget) {
|
|
366
|
+
// Only filesystem and git bundles can carry scheduler state.
|
|
367
|
+
const targetSource = activeSources.find((source) => source.name === bundleTarget);
|
|
368
|
+
if (targetSource && !isWriteCapableSourceKind(targetSource.type)) {
|
|
369
|
+
throw new UsageError(`Bundle "${bundleTarget}" has kind "${targetSource.type}"; task scheduling is only supported for filesystem and git bundles.`, "INVALID_FLAG_VALUE");
|
|
370
|
+
}
|
|
451
371
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
function inactiveBundleRemovalOperations(config, configuredSources, installed, artifacts) {
|
|
372
|
+
const configuredSources = resolveConfiguredSources(config);
|
|
373
|
+
const schedulable = activeSources
|
|
374
|
+
.filter((source) => isWriteCapableSourceKind(source.type))
|
|
375
|
+
.map((source) => source.name);
|
|
376
|
+
// No configured bundle at all: the working stash (e.g. `AKM_BUNDLE_DIR`).
|
|
377
|
+
const selected = bundleTarget
|
|
378
|
+
? [bundleTarget]
|
|
379
|
+
: schedulable.length > 0 || configuredSources.length > 0
|
|
380
|
+
? schedulable
|
|
381
|
+
: [undefined];
|
|
382
|
+
const enabledRefSet = new Set(enabledRefs);
|
|
383
|
+
const desired = [];
|
|
384
|
+
const failures = [];
|
|
385
|
+
const keepRefs = new Set();
|
|
386
|
+
const scopes = [];
|
|
387
|
+
// Each bundle is its own item: one that cannot be read is reported and
|
|
388
|
+
// left untouched while the others reconcile. A scoped sync has only that
|
|
389
|
+
// bundle, so its failure is the whole operation's.
|
|
390
|
+
for (const name of selected) {
|
|
391
|
+
try {
|
|
392
|
+
const resolved = resolveTaskReadBundle(undefined, name);
|
|
393
|
+
const stashDir = resolved.source.path;
|
|
394
|
+
const adapterId = resolved.source.adapterId ?? detectAdapterId(stashDir);
|
|
395
|
+
const compiled = await compileSchedulerSources({
|
|
396
|
+
sourceRoot: stashDir,
|
|
397
|
+
adapterId,
|
|
398
|
+
bundleName: resolved.source.name,
|
|
399
|
+
backend: sched.name,
|
|
400
|
+
config,
|
|
401
|
+
resolveAsset: taskProjectionAssetResolver(config, resolved.source.name, stashDir),
|
|
402
|
+
enabledRefs: enabledRefSet,
|
|
403
|
+
});
|
|
404
|
+
desired.push(...compiled.desired);
|
|
405
|
+
failures.push(...compiled.failures);
|
|
406
|
+
for (const failure of compiled.failures)
|
|
407
|
+
if (failure.ref)
|
|
408
|
+
keepRefs.add(failure.ref);
|
|
409
|
+
scopes.push({ ...bundleScope(resolved.source.name, stashDir), adapterId });
|
|
410
|
+
}
|
|
411
|
+
catch (cause) {
|
|
412
|
+
if (bundleTarget)
|
|
413
|
+
throw cause;
|
|
414
|
+
failures.push({ path: name ?? "(default bundle)", reason: errorMessage(cause) });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// An enabled ref of a synced bundle that produced nothing names a source
|
|
418
|
+
// that is gone or has no schedule; its row, if any, is removed below.
|
|
419
|
+
const represented = new Set([...desired.map((binding) => binding.logicalSource.ref), ...keepRefs]);
|
|
420
|
+
const synced = new Set(scopes.map((scope) => scope.bundleName));
|
|
421
|
+
for (const ref of enabledRefs) {
|
|
422
|
+
const bundle = parseBundleRef(ref).bundle;
|
|
423
|
+
if (bundle !== undefined && synced.has(bundle) && !represented.has(ref)) {
|
|
424
|
+
failures.push({ path: ref, ref, reason: `Enabled ref ${JSON.stringify(ref)} was not found or has no schedule.` });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// A disabled bundle's rows go on an unscoped sync, without reading its content.
|
|
509
428
|
const inactive = new Set(configuredSources.filter((source) => source.enabled === false).map((source) => source.name));
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
429
|
+
const extraRemovals = bundleTarget
|
|
430
|
+
? []
|
|
431
|
+
: installed.filter((row) => {
|
|
432
|
+
const bundle = installedSchedulerBundle(config, row);
|
|
433
|
+
return bundle !== undefined && inactive.has(bundle);
|
|
434
|
+
});
|
|
435
|
+
const runtime = prepareSchedulerRuntime(deps);
|
|
436
|
+
const plan = planSchedulerSync({
|
|
437
|
+
desired,
|
|
438
|
+
installed,
|
|
439
|
+
scopes,
|
|
440
|
+
...(sched.expectedSignature ? { expectedSignature: sched.expectedSignature.bind(sched) } : {}),
|
|
441
|
+
installOptions: runtime.options,
|
|
442
|
+
rebind: options.rebind === true,
|
|
443
|
+
extraRemovals,
|
|
444
|
+
keepRefs,
|
|
519
445
|
});
|
|
446
|
+
const warnings = [];
|
|
447
|
+
const writesLauncher = options.rebind === true
|
|
448
|
+
? plan.operations.some((operation) => operation.kind !== "remove")
|
|
449
|
+
: plan.installed.length > 0;
|
|
450
|
+
if (runtime.via === "checkout" && writesLauncher) {
|
|
451
|
+
warnings.push(`Scheduled tasks now run akm from a source checkout (${runtime.options.binding?.join(" ")}); they run whatever the checkout holds when they fire. Install akm with \`npm install --global akm-cli\` or a standalone release, then run \`akm task sync --rebind\`.`);
|
|
452
|
+
}
|
|
453
|
+
return { sched, plan: { ...plan, failures: [...failures, ...plan.failures] }, warnings };
|
|
520
454
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
455
|
+
/**
|
|
456
|
+
* The launcher and environment rows are written with. The environment
|
|
457
|
+
* follows the current policy on every sync; a row that is already installed
|
|
458
|
+
* keeps its launcher unless `--rebind` (see `installOptionsFor`). An injected
|
|
459
|
+
* backend (tests) renders with its own launcher.
|
|
460
|
+
*/
|
|
461
|
+
function prepareSchedulerRuntime(deps) {
|
|
462
|
+
const environment = scheduledRowEnvironment();
|
|
463
|
+
if (deps.schedulerRuntime) {
|
|
464
|
+
const runtime = deps.schedulerRuntime();
|
|
465
|
+
return { options: { binding: runtime.binding, environment }, ...(runtime.via ? { via: runtime.via } : {}) };
|
|
466
|
+
}
|
|
467
|
+
if (deps.backend)
|
|
468
|
+
return { options: { environment } };
|
|
469
|
+
const invocation = resolveAkmInvocation();
|
|
470
|
+
return { options: { binding: invocation.argv, environment }, via: invocation.via };
|
|
528
471
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
sourcePhysicalIdentity: "inactive",
|
|
535
|
-
sourceDirectoryVersion: "inactive",
|
|
536
|
-
files: Object.freeze([]),
|
|
537
|
-
directoryManifests: Object.freeze([]),
|
|
538
|
-
});
|
|
539
|
-
return Object.freeze({
|
|
540
|
-
desired: Object.freeze([]),
|
|
541
|
-
installed: Object.freeze([]),
|
|
542
|
-
updated: Object.freeze([]),
|
|
543
|
-
removed: Object.freeze(operations.map((operation) => operation.id)),
|
|
544
|
-
unchanged: Object.freeze([]),
|
|
545
|
-
operations: Object.freeze([...operations]),
|
|
546
|
-
sourceSnapshot,
|
|
547
|
-
failures: Object.freeze([]),
|
|
472
|
+
/** One read of the akm-owned rows, each attributed to the bundle path it names (#846). */
|
|
473
|
+
async function listInstalledRows(sched) {
|
|
474
|
+
return (await sched.list()).map((row) => {
|
|
475
|
+
const ownerBundlePath = installedRowBundleDir(row);
|
|
476
|
+
return ownerBundlePath !== undefined ? { ...row, ownerBundlePath } : row;
|
|
548
477
|
});
|
|
549
478
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
479
|
+
/**
|
|
480
|
+
* The `AKM_BUNDLE_DIR` an installed row names: its inline one, or the one in
|
|
481
|
+
* the descriptor a row written before 0.9.17-alpha.7 references. Undefined
|
|
482
|
+
* when it names none, or its descriptor cannot be read — which never means
|
|
483
|
+
* "mine".
|
|
484
|
+
*/
|
|
485
|
+
function installedRowBundleDir(row) {
|
|
486
|
+
if (row.contextPath === undefined)
|
|
487
|
+
return row.environment?.AKM_BUNDLE_DIR;
|
|
488
|
+
try {
|
|
489
|
+
return readLegacySchedulerContext(row.contextPath).AKM_BUNDLE_DIR;
|
|
561
490
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
function assertNoCrossBundleSchedulerCollisions(plans) {
|
|
565
|
-
const owners = new Map();
|
|
566
|
-
for (const binding of plans.flatMap((plan) => plan.desired)) {
|
|
567
|
-
const key = schedulerNativeArtifactKey(schedulerBindingNativeId(binding));
|
|
568
|
-
const existing = owners.get(key);
|
|
569
|
-
if (existing && existing.logicalSource.ref !== binding.logicalSource.ref) {
|
|
570
|
-
throw new UsageError(`Scheduler native id ${JSON.stringify(schedulerBindingNativeId(binding))} is claimed by both ` +
|
|
571
|
-
`${JSON.stringify(existing.logicalSource.ref)} and ${JSON.stringify(binding.logicalSource.ref)}. ` +
|
|
572
|
-
"Rename one task before enabling both.", "RESOURCE_ALREADY_EXISTS");
|
|
573
|
-
}
|
|
574
|
-
owners.set(key, binding);
|
|
491
|
+
catch {
|
|
492
|
+
return undefined;
|
|
575
493
|
}
|
|
576
494
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
? prepared.publish
|
|
581
|
-
: undefined, sourceSnapshots);
|
|
582
|
-
return {
|
|
583
|
-
installed: [...plan.installed],
|
|
584
|
-
updated: [...plan.updated],
|
|
585
|
-
removed: [...plan.removed],
|
|
586
|
-
unchanged: [...plan.unchanged],
|
|
587
|
-
skipped: [],
|
|
588
|
-
backend: sched.name,
|
|
589
|
-
failures: plan.failures.map((failure) => ({ ...failure })),
|
|
590
|
-
...(warnings.length > 0 ? { warnings } : {}),
|
|
591
|
-
};
|
|
495
|
+
/** The primary bundle proves its rows by path (#846); any other bundle by its config name. */
|
|
496
|
+
function bundleScope(bundleName, stashDir) {
|
|
497
|
+
return isPrimaryStashPath(stashDir) ? { bundleName, bundlePath: path.resolve(stashDir) } : { bundleName };
|
|
592
498
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
* point: `prepared.publish`, if any, is dropped on the floor here, never
|
|
601
|
-
* invoked. Zero durable writes, mirroring `akm workflow plan`.
|
|
602
|
-
*/
|
|
603
|
-
export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
604
|
-
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
605
|
-
return renderSchedulerSyncPlanPreview(sched.name, plan);
|
|
499
|
+
function installedSchedulerBundle(config, row) {
|
|
500
|
+
const direct = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
501
|
+
if (direct !== undefined)
|
|
502
|
+
return direct;
|
|
503
|
+
if (row.ownerBundlePath === undefined)
|
|
504
|
+
return undefined;
|
|
505
|
+
return bundleKeyForContentRoot(config, row.ownerBundlePath);
|
|
606
506
|
}
|
|
607
507
|
/**
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
* resolved owner no longer exists on disk (`dead-bundle-path`) is a
|
|
613
|
-
* candidate; `missing-path` (e.g. the akm binary itself moved) is a
|
|
614
|
-
* different failure mode and is intentionally NOT folded in here, per the
|
|
615
|
-
* scoping in #851 — an entry that still resolves to a live bundle is never a
|
|
616
|
-
* candidate, full stop.
|
|
508
|
+
* Why `akm task prune` (#851) would remove an installed row: a row written
|
|
509
|
+
* before 0.9.17-alpha.7 whose descriptor cannot be read (`invalid-context`),
|
|
510
|
+
* or one naming a bundle directory that is gone (`dead-bundle-path`). A row
|
|
511
|
+
* that still resolves to a live bundle is never a candidate.
|
|
617
512
|
*/
|
|
618
513
|
function classifyPruneCandidate(entry) {
|
|
619
|
-
let
|
|
620
|
-
|
|
621
|
-
|
|
514
|
+
let bundleDir;
|
|
515
|
+
if (entry.contextPath !== undefined) {
|
|
516
|
+
try {
|
|
517
|
+
bundleDir = readLegacySchedulerContext(entry.contextPath).AKM_BUNDLE_DIR;
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return "invalid-context";
|
|
521
|
+
}
|
|
522
|
+
// Every descriptor akm wrote names the bundle; one that does not cannot be attributed.
|
|
523
|
+
if (bundleDir === undefined)
|
|
524
|
+
return "invalid-context";
|
|
622
525
|
}
|
|
623
|
-
|
|
624
|
-
|
|
526
|
+
else {
|
|
527
|
+
bundleDir = entry.environment?.AKM_BUNDLE_DIR;
|
|
625
528
|
}
|
|
626
|
-
if (
|
|
529
|
+
if (bundleDir !== undefined && !fs.existsSync(bundleDir))
|
|
627
530
|
return "dead-bundle-path";
|
|
628
531
|
return undefined;
|
|
629
532
|
}
|
|
630
|
-
async function buildTaskPrunePlan(deps = {}, options = {}) {
|
|
631
|
-
const sched = deps.backend ?? selectBackend();
|
|
632
|
-
if (!sched.inspectBindings) {
|
|
633
|
-
throw new ConfigError(`Scheduler backend "${sched.name}" cannot provide one coherent inspection for prune.`, "INVALID_CONFIG_FILE");
|
|
634
|
-
}
|
|
635
|
-
const inspection = await sched.inspectBindings({});
|
|
636
|
-
const candidates = new Map();
|
|
637
|
-
for (const entry of inspection.installed) {
|
|
638
|
-
const reason = classifyPruneCandidate(entry);
|
|
639
|
-
if (reason)
|
|
640
|
-
candidates.set(entry.id, reason);
|
|
641
|
-
}
|
|
642
|
-
const requestedIds = options.id?.filter((id) => id.length > 0) ?? [];
|
|
643
|
-
for (const id of requestedIds) {
|
|
644
|
-
if (!candidates.has(id)) {
|
|
645
|
-
throw new UsageError(`Scheduler binding ${JSON.stringify(id)} is not an orphaned prune candidate ` +
|
|
646
|
-
"(either not installed, or it still resolves to a live bundle) — refusing to prune it.", "INVALID_FLAG_VALUE");
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
const idFilter = requestedIds.length > 0 ? new Set(requestedIds) : undefined;
|
|
650
|
-
const resolved = resolveTaskReadBundle(undefined, undefined);
|
|
651
|
-
const bundleContext = {
|
|
652
|
-
adapterId: resolved.source.adapterId ?? detectAdapterId(resolved.source.path),
|
|
653
|
-
bundleName: resolved.source.name,
|
|
654
|
-
};
|
|
655
|
-
const operations = [];
|
|
656
|
-
for (const entry of inspection.installed) {
|
|
657
|
-
const reason = candidates.get(entry.id);
|
|
658
|
-
if (!reason)
|
|
659
|
-
continue;
|
|
660
|
-
if (idFilter && !idFilter.has(entry.id))
|
|
661
|
-
continue;
|
|
662
|
-
const operation = buildSchedulerRemoveOperation(entry.id, entry, inspection.artifacts, bundleContext);
|
|
663
|
-
operations.push(Object.freeze({ ...operation, reason }));
|
|
664
|
-
}
|
|
665
|
-
return { sched, operations: Object.freeze(operations) };
|
|
666
|
-
}
|
|
667
533
|
/**
|
|
668
|
-
* `akm task prune` (#851): remove installed
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* `akmTasksSyncPlan`'s zero-write guarantee. `--id` (one or more) narrows
|
|
673
|
-
* execution to exactly those bindings; `--yes` alone executes every
|
|
674
|
-
* currently-computed candidate. Both still return the full preview so the
|
|
675
|
-
* plan is never silent about what it did.
|
|
534
|
+
* `akm task prune` (#851): remove installed rows `sync` can never reclaim
|
|
535
|
+
* because they no longer resolve to a live bundle. It scans
|
|
536
|
+
* every installed row, not one bundle's. Without `--yes` it only previews and
|
|
537
|
+
* writes nothing; `--id` narrows it to named candidates.
|
|
676
538
|
*/
|
|
677
539
|
export async function akmTasksPrune(deps = {}, options = {}) {
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
backend: sched.name,
|
|
688
|
-
dryRun: false,
|
|
689
|
-
preview,
|
|
690
|
-
removed: operations.map((operation) => operation.id),
|
|
540
|
+
const prune = async () => {
|
|
541
|
+
const sched = deps.backend ?? selectBackend();
|
|
542
|
+
const operations = pruneOperations(await sched.list(), options.id ?? []);
|
|
543
|
+
const preview = renderSchedulerPlanPreview(sched.name, operations);
|
|
544
|
+
if (!options.yes)
|
|
545
|
+
return { backend: sched.name, dryRun: true, preview, removed: [] };
|
|
546
|
+
for (const operation of operations)
|
|
547
|
+
await sched.uninstall(operation.nativeId);
|
|
548
|
+
return { backend: sched.name, dryRun: false, preview, removed: operations.map((operation) => operation.id) };
|
|
691
549
|
};
|
|
550
|
+
return options.yes ? withSchedulerLock(prune) : prune();
|
|
551
|
+
}
|
|
552
|
+
function pruneOperations(installed, requestedIds) {
|
|
553
|
+
const candidates = installed.flatMap((row) => {
|
|
554
|
+
const reason = classifyPruneCandidate(row);
|
|
555
|
+
return reason ? [{ row, reason }] : [];
|
|
556
|
+
});
|
|
557
|
+
const ids = requestedIds.filter((id) => id.length > 0);
|
|
558
|
+
for (const id of ids) {
|
|
559
|
+
if (!candidates.some(({ row }) => row.id === id)) {
|
|
560
|
+
throw new UsageError(`Scheduler binding ${JSON.stringify(id)} is not an orphaned prune candidate ` +
|
|
561
|
+
"(either not installed, or it still resolves to a live bundle) — refusing to prune it.", "INVALID_FLAG_VALUE");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return candidates
|
|
565
|
+
.filter(({ row }) => ids.length === 0 || ids.includes(row.id))
|
|
566
|
+
.map(({ row, reason }) => ({ kind: "remove", id: row.id, nativeId: installedRowNativeId(row), reason }));
|
|
692
567
|
}
|
|
693
568
|
export async function akmTasksDoctor(deps = {}) {
|
|
694
569
|
const warnings = [];
|
|
695
|
-
let invocation = {
|
|
696
|
-
argv: [],
|
|
697
|
-
via: "unresolved",
|
|
698
|
-
};
|
|
570
|
+
let invocation = { argv: [], via: "unresolved" };
|
|
699
571
|
try {
|
|
700
572
|
const r = (deps.resolveInvocation ?? resolveAkmInvocation)();
|
|
701
|
-
invocation = { argv: r.argv, via: r.via
|
|
573
|
+
invocation = { argv: r.argv, via: r.via };
|
|
702
574
|
}
|
|
703
575
|
catch (err) {
|
|
704
576
|
warnings.push(err instanceof Error ? err.message : String(err));
|
|
@@ -718,7 +590,7 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
718
590
|
warnings.push(`Unable to inspect installed ${backend} definitions: ${error instanceof Error ? error.message : String(error)}`);
|
|
719
591
|
}
|
|
720
592
|
}
|
|
721
|
-
const bindings = groupInstalledBindings(installed
|
|
593
|
+
const bindings = groupInstalledBindings(installed);
|
|
722
594
|
// Report the EFFECTIVE engine view — the same one the runner resolves —
|
|
723
595
|
// so doctor never says "no engine" on an install where tasks actually run.
|
|
724
596
|
const { config } = withEngineFallback(loadConfig());
|
|
@@ -748,7 +620,6 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
748
620
|
defaultStrategy: improveStrategyName,
|
|
749
621
|
enabled: triage.enabled === true,
|
|
750
622
|
applyMode: triage.applyMode ?? "queue",
|
|
751
|
-
policy: triage.policy ?? "personal-stash",
|
|
752
623
|
}
|
|
753
624
|
: undefined;
|
|
754
625
|
return {
|
|
@@ -769,298 +640,11 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
769
640
|
};
|
|
770
641
|
}
|
|
771
642
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
772
|
-
|
|
773
|
-
await applySchedulerTransaction(backend, plan.operations, {
|
|
774
|
-
initialExpectations: plan.operations.map((operation) => operation.expected),
|
|
775
|
-
assertReadSet: () => {
|
|
776
|
-
for (const snapshot of sourceSnapshots)
|
|
777
|
-
assertSchedulerSourceSnapshot(snapshot);
|
|
778
|
-
},
|
|
779
|
-
beforeOperation: (_operation, index) => {
|
|
780
|
-
if (index === 0)
|
|
781
|
-
publish?.();
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
async function applySchedulerTransaction(backend, operations, hooks) {
|
|
786
|
-
if (operations.length === 0) {
|
|
787
|
-
hooks.assertReadSet?.();
|
|
788
|
-
await hooks.afterOperations?.();
|
|
789
|
-
hooks.assertReadSet?.();
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
hooks.assertReadSet?.();
|
|
793
|
-
if (!backend.snapshotBindings || !backend.restoreBindings) {
|
|
794
|
-
throw new ConfigError(`Scheduler backend "${backend.name}" cannot snapshot and restore a whole-set transaction.`, "INVALID_CONFIG_FILE");
|
|
795
|
-
}
|
|
796
|
-
const nativeIds = [
|
|
797
|
-
...new Set(operations.map((operation) => operation.kind === "remove" ? operation.nativeId : schedulerBindingNativeId(operation.binding))),
|
|
798
|
-
];
|
|
799
|
-
const snapshot = await backend.snapshotBindings(nativeIds);
|
|
800
|
-
assertSchedulerTransactionSnapshot(snapshot, nativeIds, hooks.initialExpectations);
|
|
801
|
-
const rollbackExpected = schedulerRollbackExpectations(snapshot, operations);
|
|
802
|
-
hooks.assertReadSet?.();
|
|
803
|
-
try {
|
|
804
|
-
for (const [index, operation] of operations.entries()) {
|
|
805
|
-
hooks.assertReadSet?.();
|
|
806
|
-
await hooks.beforeOperation?.(operation, index);
|
|
807
|
-
hooks.assertReadSet?.();
|
|
808
|
-
if (operation.kind === "remove")
|
|
809
|
-
await backend.uninstall(operation.nativeId, operation.expected);
|
|
810
|
-
else
|
|
811
|
-
await backend.install(operation.binding, operation.options, operation.expected);
|
|
812
|
-
hooks.assertReadSet?.();
|
|
813
|
-
}
|
|
814
|
-
hooks.assertReadSet?.();
|
|
815
|
-
await hooks.afterOperations?.();
|
|
816
|
-
hooks.assertReadSet?.();
|
|
817
|
-
}
|
|
818
|
-
catch (primaryError) {
|
|
819
|
-
let externalRollbackError;
|
|
820
|
-
try {
|
|
821
|
-
await hooks.rollbackExternal?.();
|
|
822
|
-
}
|
|
823
|
-
catch (error) {
|
|
824
|
-
externalRollbackError = error;
|
|
825
|
-
}
|
|
826
|
-
let nativeRollbackError;
|
|
827
|
-
const externalStateAllowsNativeRollback = externalRollbackError !== undefined &&
|
|
828
|
-
hooks.allowNativeRollbackAfterExternalFailure?.(externalRollbackError) === true;
|
|
829
|
-
if (!(externalRollbackError && hooks.suppressNativeRollbackWhenExternalFails && !externalStateAllowsNativeRollback)) {
|
|
830
|
-
try {
|
|
831
|
-
await backend.restoreBindings(snapshot, rollbackExpected);
|
|
832
|
-
}
|
|
833
|
-
catch (error) {
|
|
834
|
-
nativeRollbackError = error;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
const rollbackErrors = [externalRollbackError, nativeRollbackError].filter((error) => error !== undefined);
|
|
838
|
-
if (rollbackErrors.length > 0) {
|
|
839
|
-
throw new AggregateError([primaryError, ...rollbackErrors], `Scheduler transaction failed and rollback was incomplete: ${errorMessage(primaryError)}`);
|
|
840
|
-
}
|
|
841
|
-
throw primaryError;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
function schedulerRollbackExpectations(snapshot, operations) {
|
|
845
|
-
return Object.freeze(snapshot.nativeIds.map((nativeId) => {
|
|
846
|
-
const prior = snapshot.artifacts.find((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === schedulerNativeArtifactKey(nativeId));
|
|
847
|
-
const allowed = [];
|
|
848
|
-
if (prior) {
|
|
849
|
-
if (prior.fingerprint === undefined) {
|
|
850
|
-
throw new ConfigError(`Scheduler backend snapshot for ${JSON.stringify(nativeId)} has no exact fingerprint.`, "INVALID_CONFIG_FILE");
|
|
851
|
-
}
|
|
852
|
-
allowed.push(Object.freeze({
|
|
853
|
-
state: "present",
|
|
854
|
-
...(prior.bindingId !== undefined ? { bindingId: prior.bindingId } : {}),
|
|
855
|
-
...(prior.invocation !== undefined ? { invocation: Object.freeze([...prior.invocation]) } : {}),
|
|
856
|
-
fingerprint: prior.fingerprint,
|
|
857
|
-
}));
|
|
858
|
-
}
|
|
859
|
-
else {
|
|
860
|
-
allowed.push(Object.freeze({ state: "absent" }));
|
|
861
|
-
}
|
|
862
|
-
const matchingOperations = operations.filter((candidate) => {
|
|
863
|
-
const operationNativeId = candidate.kind === "remove" ? candidate.nativeId : schedulerBindingNativeId(candidate.binding);
|
|
864
|
-
return schedulerNativeArtifactKey(operationNativeId) === schedulerNativeArtifactKey(nativeId);
|
|
865
|
-
});
|
|
866
|
-
for (const operation of matchingOperations) {
|
|
867
|
-
if (operation.kind === "remove") {
|
|
868
|
-
if (!allowed.some((state) => state.state === "absent")) {
|
|
869
|
-
allowed.push(Object.freeze({ state: "absent" }));
|
|
870
|
-
}
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
if (operation.resultFingerprint === undefined) {
|
|
874
|
-
throw new ConfigError(`Scheduler backend cannot freeze the post-mutation fingerprint for ${JSON.stringify(nativeId)}.`, "INVALID_CONFIG_FILE");
|
|
875
|
-
}
|
|
876
|
-
allowed.push(Object.freeze({
|
|
877
|
-
state: "present",
|
|
878
|
-
bindingId: operation.binding.id,
|
|
879
|
-
invocation: Object.freeze([...operation.binding.invocation]),
|
|
880
|
-
fingerprint: operation.resultFingerprint,
|
|
881
|
-
}));
|
|
882
|
-
}
|
|
883
|
-
return Object.freeze({ nativeId, allowed: Object.freeze(allowed) });
|
|
884
|
-
}));
|
|
885
|
-
}
|
|
886
|
-
function assertSchedulerTransactionSnapshot(snapshot, nativeIds, initialExpectations) {
|
|
887
|
-
if (!snapshot ||
|
|
888
|
-
!Array.isArray(snapshot.nativeIds) ||
|
|
889
|
-
!Array.isArray(snapshot.artifacts) ||
|
|
890
|
-
nativeIds.some((nativeId) => !snapshot.nativeIds.includes(nativeId))) {
|
|
891
|
-
throw new ConfigError("Scheduler backend returned an incomplete transaction snapshot.", "INVALID_CONFIG_FILE");
|
|
892
|
-
}
|
|
893
|
-
const snapshotKeys = snapshot.nativeIds.map(schedulerNativeArtifactKey);
|
|
894
|
-
const requestedKeys = nativeIds.map(schedulerNativeArtifactKey);
|
|
895
|
-
if (snapshotKeys.length !== requestedKeys.length ||
|
|
896
|
-
new Set(snapshotKeys).size !== snapshotKeys.length ||
|
|
897
|
-
snapshotKeys.some((key) => !requestedKeys.includes(key))) {
|
|
898
|
-
throw new ConfigError("Scheduler backend returned an inexact normalized transaction snapshot set.", "INVALID_CONFIG_FILE");
|
|
899
|
-
}
|
|
900
|
-
for (const expected of initialExpectations) {
|
|
901
|
-
const artifact = assertSchedulerNativeArtifactCardinality(snapshot.artifacts, expected.nativeId, expected.state === "absent" ? 0 : 1);
|
|
902
|
-
assertSchedulerMutationArtifact(artifact, expected);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
async function prepareTaskAddSchedulerTransaction(input) {
|
|
906
|
-
if (!input.sched.inspectBindings) {
|
|
907
|
-
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot provide one coherent inspection for transactional add.`, "INVALID_CONFIG_FILE");
|
|
908
|
-
}
|
|
909
|
-
if (!input.sched.snapshotBindings || !input.sched.restoreBindings || !input.sched.expectedSignature) {
|
|
910
|
-
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot provide exact snapshot, restore, and signature contracts for transactional add.`, "INVALID_CONFIG_FILE");
|
|
911
|
-
}
|
|
912
|
-
const inspection = await input.sched.inspectBindings({ rebind: input.rebind });
|
|
913
|
-
const installedEntries = [...inspection.installed];
|
|
914
|
-
const nativeArtifacts = [...inspection.artifacts];
|
|
915
|
-
const seenNativeKeys = new Set();
|
|
916
|
-
for (const artifact of nativeArtifacts) {
|
|
917
|
-
const key = schedulerNativeArtifactKey(artifact.nativeId);
|
|
918
|
-
if (seenNativeKeys.has(key)) {
|
|
919
|
-
throw new UsageError(`Scheduler inspection has duplicate normalized native artifact ${JSON.stringify(artifact.nativeId)}.`, "RESOURCE_ALREADY_EXISTS");
|
|
920
|
-
}
|
|
921
|
-
seenNativeKeys.add(key);
|
|
922
|
-
}
|
|
923
|
-
for (const binding of input.taskBindings) {
|
|
924
|
-
assertNoForeignSchedule(installedEntries, binding.id, input.ownerTarget);
|
|
925
|
-
}
|
|
926
|
-
const taskEntries = installedEntries.filter((entry) => installedEntryRunsTask(entry, input.id));
|
|
927
|
-
const foreignTaskEntry = taskEntries.find((entry) => !sameBundle(entry.target, input.ownerTarget));
|
|
928
|
-
if (foreignTaskEntry) {
|
|
929
|
-
throw new UsageError(foreignScheduleMessage(input.id, foreignTaskEntry.target), "RESOURCE_ALREADY_EXISTS");
|
|
930
|
-
}
|
|
931
|
-
assertSchedulerNativeArtifactOwnership(input.taskBindings, nativeArtifacts);
|
|
932
|
-
const primary = input.taskBindings[0];
|
|
933
|
-
if (!primary)
|
|
934
|
-
throw new Error("invariant: scheduler transaction has no desired binding");
|
|
935
|
-
const installedEntry = installedEntries.find((entry) => entry.id === primary.id) ?? taskEntries[0];
|
|
936
|
-
const preparedRuntime = installedEntry && !input.rebind
|
|
937
|
-
? {
|
|
938
|
-
options: {
|
|
939
|
-
...input.installOpts,
|
|
940
|
-
binding: Object.freeze([...installedEntry.binding]),
|
|
941
|
-
contextPath: installedEntry.contextPath,
|
|
942
|
-
},
|
|
943
|
-
}
|
|
944
|
-
: prepareSchedulerSyncRuntime(input.installOpts, input.deps, []);
|
|
945
|
-
const runtimeOpts = preparedRuntime.options;
|
|
946
|
-
const removals = taskEntries.map((entry) => {
|
|
947
|
-
const invocation = entry.invocation;
|
|
948
|
-
const nativeId = entry.nativeId ?? schedulerNativeBindingId(entry.id);
|
|
949
|
-
const artifact = assertSchedulerNativeArtifactCardinality(nativeArtifacts, nativeId, 1);
|
|
950
|
-
if (!artifact?.fingerprint || artifact.bindingId !== entry.id) {
|
|
951
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact coherent fingerprint.`, "RESOURCE_ALREADY_EXISTS");
|
|
952
|
-
}
|
|
953
|
-
const logicalSource = primary.logicalSource;
|
|
954
|
-
const ordinal = invocation ? schedulerBindingOrdinal(entry.id, logicalSource, invocation) : undefined;
|
|
955
|
-
if (!invocation || ordinal === undefined) {
|
|
956
|
-
warn(`Installed scheduler binding ${JSON.stringify(entry.id)} (native id ${JSON.stringify(nativeId)}) could not be exactly parsed — likely a hand-edited entry; replacing it without a compare-and-swap guard.`);
|
|
957
|
-
return Object.freeze({
|
|
958
|
-
kind: "remove",
|
|
959
|
-
id: entry.id,
|
|
960
|
-
nativeId,
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
return Object.freeze({
|
|
964
|
-
kind: "remove",
|
|
965
|
-
id: entry.id,
|
|
966
|
-
nativeId,
|
|
967
|
-
expected: Object.freeze({
|
|
968
|
-
bindingId: entry.id,
|
|
969
|
-
nativeId,
|
|
970
|
-
logicalSource,
|
|
971
|
-
ordinal,
|
|
972
|
-
invocation: Object.freeze([...invocation]),
|
|
973
|
-
fingerprint: artifact.fingerprint,
|
|
974
|
-
}),
|
|
975
|
-
});
|
|
976
|
-
});
|
|
977
|
-
const installs = input.taskBindings.map((binding) => {
|
|
978
|
-
const nativeId = schedulerBindingNativeId(binding);
|
|
979
|
-
const resultFingerprint = input.sched.expectedSignature(binding, runtimeOpts);
|
|
980
|
-
if (!resultFingerprint) {
|
|
981
|
-
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot freeze the post-install fingerprint for ${JSON.stringify(binding.id)}.`, "INVALID_CONFIG_FILE");
|
|
982
|
-
}
|
|
983
|
-
return Object.freeze({
|
|
984
|
-
kind: "install",
|
|
985
|
-
binding,
|
|
986
|
-
expected: Object.freeze({
|
|
987
|
-
state: "absent",
|
|
988
|
-
bindingId: binding.id,
|
|
989
|
-
nativeId,
|
|
990
|
-
logicalSource: binding.logicalSource,
|
|
991
|
-
ordinal: binding.ordinal,
|
|
992
|
-
invocation: binding.invocation,
|
|
993
|
-
}),
|
|
994
|
-
resultFingerprint,
|
|
995
|
-
...(runtimeOpts ? { options: runtimeOpts } : {}),
|
|
996
|
-
});
|
|
997
|
-
});
|
|
998
|
-
const initialByKey = new Map();
|
|
999
|
-
for (const removal of removals) {
|
|
1000
|
-
if (removal.kind !== "remove")
|
|
1001
|
-
continue;
|
|
1002
|
-
if (!removal.expected)
|
|
1003
|
-
continue;
|
|
1004
|
-
initialByKey.set(schedulerNativeArtifactKey(removal.nativeId), Object.freeze({ ...removal.expected, state: "present" }));
|
|
1005
|
-
}
|
|
1006
|
-
for (const install of installs) {
|
|
1007
|
-
if (install.kind === "remove")
|
|
1008
|
-
continue;
|
|
1009
|
-
const key = schedulerNativeArtifactKey(schedulerBindingNativeId(install.binding));
|
|
1010
|
-
if (!initialByKey.has(key))
|
|
1011
|
-
initialByKey.set(key, install.expected);
|
|
1012
|
-
}
|
|
1013
|
-
return Object.freeze({
|
|
1014
|
-
runtimeOpts,
|
|
1015
|
-
...(preparedRuntime.publish ? { publishRuntime: preparedRuntime.publish } : {}),
|
|
1016
|
-
operations: Object.freeze([...removals, ...installs]),
|
|
1017
|
-
initialExpectations: Object.freeze([...initialByKey.values()]),
|
|
1018
|
-
publishOperationIndex: removals.length,
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
function prepareSchedulerSyncRuntime(base, deps, warnings, installedBindings = []) {
|
|
1022
|
-
if (deps.backend && !deps.schedulerRuntime)
|
|
1023
|
-
return base ? { options: base } : {};
|
|
1024
|
-
if (deps.schedulerRuntime) {
|
|
1025
|
-
const runtime = deps.schedulerRuntime();
|
|
1026
|
-
warnIneligibleRebind(runtime, warnings, installedBindings);
|
|
1027
|
-
return { options: { ...base, binding: runtime.binding, contextPath: runtime.contextPath } };
|
|
1028
|
-
}
|
|
1029
|
-
const invocation = resolveAndValidateSchedulerInvocation();
|
|
1030
|
-
warnIneligibleRebind(invocation, warnings, installedBindings);
|
|
1031
|
-
const descriptor = schedulerContextDescriptor();
|
|
1032
|
-
const contextPath = schedulerContextPath(descriptor);
|
|
1033
|
-
return {
|
|
1034
|
-
options: { ...base, binding: invocation.binding, contextPath },
|
|
1035
|
-
publish: () => {
|
|
1036
|
-
const written = writeSchedulerContextDescriptor(descriptor);
|
|
1037
|
-
if (written !== contextPath) {
|
|
1038
|
-
throw new ConfigError("Scheduler context descriptor path changed after preflight.", "INVALID_CONFIG_FILE");
|
|
1039
|
-
}
|
|
1040
|
-
},
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
function resolveAndValidateSchedulerInvocation() {
|
|
1044
|
-
const invocation = resolveAkmInvocation();
|
|
1045
|
-
return { binding: invocation.argv, contextPath: "", eligible: invocation.eligible, kind: invocation.kind };
|
|
1046
|
-
}
|
|
1047
|
-
function warnIneligibleRebind(runtime, warnings, installedBindings) {
|
|
1048
|
-
if (runtime.eligible !== false || warnings.length > 0)
|
|
1049
|
-
return;
|
|
1050
|
-
// #868 residue: binding every currently-installed entry to the SAME
|
|
1051
|
-
// invocation it already carries changes nothing — this is the steady
|
|
1052
|
-
// state of an image-baked install re-running `task sync` on a timer.
|
|
1053
|
-
// Only warn when the bind actually moves an entry to a different
|
|
1054
|
-
// invocation.
|
|
1055
|
-
if (installedBindings.length > 0 && installedBindings.every((bound) => sameArgv(bound, runtime.binding)))
|
|
1056
|
-
return;
|
|
1057
|
-
warnings.push(`Scheduled tasks are bound to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
|
|
1058
|
-
}
|
|
1059
|
-
function groupInstalledBindings(entries, invocation) {
|
|
643
|
+
function groupInstalledBindings(entries) {
|
|
1060
644
|
const groups = new Map();
|
|
1061
645
|
for (const entry of entries) {
|
|
1062
646
|
const argv = [...entry.binding];
|
|
1063
|
-
const status = inspectInstalledBinding(entry
|
|
647
|
+
const status = inspectInstalledBinding(entry);
|
|
1064
648
|
const key = JSON.stringify([argv, entry.contextPath, status]);
|
|
1065
649
|
const existing = groups.get(key);
|
|
1066
650
|
if (existing) {
|
|
@@ -1069,192 +653,43 @@ function groupInstalledBindings(entries, invocation) {
|
|
|
1069
653
|
}
|
|
1070
654
|
groups.set(key, {
|
|
1071
655
|
argv,
|
|
1072
|
-
contextPath: entry.contextPath,
|
|
656
|
+
...(entry.contextPath !== undefined ? { contextPath: entry.contextPath } : {}),
|
|
1073
657
|
taskIds: [entry.id],
|
|
1074
658
|
status,
|
|
1075
659
|
});
|
|
1076
660
|
}
|
|
1077
661
|
return [...groups.values()].map((group) => ({ ...group, taskIds: group.taskIds.sort() }));
|
|
1078
662
|
}
|
|
1079
|
-
|
|
1080
|
-
function resolveInstalledOwnerPath(contextPath) {
|
|
1081
|
-
try {
|
|
1082
|
-
return validateSchedulerContextDescriptor(contextPath).environment.AKM_BUNDLE_DIR;
|
|
1083
|
-
}
|
|
1084
|
-
catch {
|
|
1085
|
-
return undefined;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
function inspectInstalledBinding(entry, invocation) {
|
|
663
|
+
function inspectInstalledBinding(entry) {
|
|
1089
664
|
const status = [];
|
|
1090
665
|
const binding = entry.binding;
|
|
1091
|
-
if (
|
|
1092
|
-
binding.some((part) => /(?:^|[\\/])src[\\/]cli\.ts$|(?:^|[\\/])dist[\\/](?:cli\.js|cli-node\.mjs)$/i.test(part) ||
|
|
1093
|
-
(path.isAbsolute(part) && hasGitAncestor(part)))) {
|
|
666
|
+
if (isCheckoutInvocation(binding))
|
|
1094
667
|
status.push("checkout");
|
|
1095
|
-
}
|
|
1096
668
|
if (binding.some((part) => part === "akm" || part === "bun" || part === "node"))
|
|
1097
669
|
status.push("path-selected");
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
670
|
+
if (entry.contextPath !== undefined) {
|
|
671
|
+
try {
|
|
672
|
+
readLegacySchedulerContext(entry.contextPath);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
status.push("invalid-context");
|
|
676
|
+
}
|
|
1103
677
|
}
|
|
1104
|
-
const absolutePaths =
|
|
678
|
+
const absolutePaths = binding.filter((part) => path.isAbsolute(part));
|
|
1105
679
|
if (absolutePaths.some((part) => !fs.existsSync(part)))
|
|
1106
680
|
status.push("missing-path");
|
|
1107
681
|
if (status.length === 0)
|
|
1108
682
|
status.push("ok");
|
|
1109
683
|
return status;
|
|
1110
684
|
}
|
|
1111
|
-
function sameArgv(left, right) {
|
|
1112
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1113
|
-
}
|
|
1114
|
-
function hasGitAncestor(file) {
|
|
1115
|
-
let current;
|
|
1116
|
-
try {
|
|
1117
|
-
current = path.dirname(fs.realpathSync(file));
|
|
1118
|
-
}
|
|
1119
|
-
catch {
|
|
1120
|
-
return false;
|
|
1121
|
-
}
|
|
1122
|
-
for (;;) {
|
|
1123
|
-
if (fs.existsSync(path.join(current, ".git")))
|
|
1124
|
-
return true;
|
|
1125
|
-
const parent = path.dirname(current);
|
|
1126
|
-
if (parent === current)
|
|
1127
|
-
return false;
|
|
1128
|
-
current = parent;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
685
|
function taskAssetRef(id) {
|
|
1132
686
|
return { type: "task", name: id };
|
|
1133
687
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const root = path.resolve(rootInput);
|
|
1137
|
-
const lexicalRelative = path.relative(root, filePath);
|
|
1138
|
-
if (lexicalRelative === "" || lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
1139
|
-
throw new UsageError(`${filePathInput} resolves outside the task source root.`, "PATH_ESCAPE_VIOLATION");
|
|
1140
|
-
}
|
|
1141
|
-
const rootRealPath = fs.realpathSync(root);
|
|
1142
|
-
const rootStat = fs.statSync(rootRealPath, { bigint: true });
|
|
1143
|
-
if (!rootStat.isDirectory()) {
|
|
1144
|
-
throw new UsageError(`${root} is not a task source directory.`, "INVALID_FLAG_VALUE");
|
|
1145
|
-
}
|
|
1146
|
-
const common = { filePath, rootRealPath };
|
|
1147
|
-
let descriptor;
|
|
1148
|
-
try {
|
|
1149
|
-
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
1150
|
-
let before = fs.fstatSync(descriptor, { bigint: true });
|
|
1151
|
-
if (!before.isFile()) {
|
|
1152
|
-
throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
|
|
1153
|
-
}
|
|
1154
|
-
let bytes = fs.readFileSync(descriptor);
|
|
1155
|
-
const torn = !sameTaskSourceStat(before, fs.fstatSync(descriptor, { bigint: true })) ||
|
|
1156
|
-
BigInt(bytes.byteLength) !== before.size;
|
|
1157
|
-
if (torn) {
|
|
1158
|
-
fs.closeSync(descriptor);
|
|
1159
|
-
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
1160
|
-
before = fs.fstatSync(descriptor, { bigint: true });
|
|
1161
|
-
bytes = fs.readFileSync(descriptor);
|
|
1162
|
-
warn(`${filePath} changed while its guarded bytes were read; retried once and proceeding with the latest read (its SHA-256 is re-verified before anything is published).`);
|
|
1163
|
-
}
|
|
1164
|
-
const realPath = fs.realpathSync(filePath);
|
|
1165
|
-
const physicalRelative = path.relative(rootRealPath, realPath);
|
|
1166
|
-
if (physicalRelative === "" || physicalRelative.startsWith("..") || path.isAbsolute(physicalRelative)) {
|
|
1167
|
-
throw new UsageError(`${filePath} resolves outside the task source root.`, "PATH_ESCAPE_VIOLATION");
|
|
1168
|
-
}
|
|
1169
|
-
let content;
|
|
1170
|
-
try {
|
|
1171
|
-
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
1172
|
-
}
|
|
1173
|
-
catch {
|
|
1174
|
-
throw new UsageError(`${filePath} contains invalid UTF-8 bytes.`, "INVALID_FLAG_VALUE");
|
|
1175
|
-
}
|
|
1176
|
-
return Object.freeze({
|
|
1177
|
-
state: "present",
|
|
1178
|
-
...common,
|
|
1179
|
-
realPath,
|
|
1180
|
-
size: bytes.byteLength,
|
|
1181
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1182
|
-
bytesBase64: bytes.toString("base64"),
|
|
1183
|
-
content,
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
catch (cause) {
|
|
1187
|
-
if (cause.code === "ENOENT") {
|
|
1188
|
-
return Object.freeze({ state: "absent", ...common });
|
|
1189
|
-
}
|
|
1190
|
-
if (cause instanceof UsageError)
|
|
1191
|
-
throw cause;
|
|
1192
|
-
throw new UsageError(`${filePath} could not be guarded as a contained regular task source: ${errorMessage(cause)}`, "PATH_ESCAPE_VIOLATION");
|
|
1193
|
-
}
|
|
1194
|
-
finally {
|
|
1195
|
-
if (descriptor !== undefined)
|
|
1196
|
-
fs.closeSync(descriptor);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
// TOCTOU note: this compares CONTENT (state + sha256), not filesystem
|
|
1200
|
-
// identity (inode/mtime/ctime/directory timestamps) — the same split already
|
|
1201
|
-
// applied to the task migrator in 0.9.5. An unrelated touch to the file or
|
|
1202
|
-
// its containing directory must not trip a "changed after planning" refusal;
|
|
1203
|
-
// only a real content change should.
|
|
1204
|
-
function assertTaskSourceExpectation(expected) {
|
|
1205
|
-
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
1206
|
-
if (!sameTaskSourceExpectation(actual, expected)) {
|
|
1207
|
-
throw new UsageError(`Task source ${JSON.stringify(expected.filePath)} changed after transaction planning.`, "RESOURCE_ALREADY_EXISTS");
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
function sameTaskSourceExpectation(left, right) {
|
|
1211
|
-
if (left.state !== right.state || left.filePath !== right.filePath || left.rootRealPath !== right.rootRealPath) {
|
|
1212
|
-
return false;
|
|
1213
|
-
}
|
|
1214
|
-
if (left.state === "absent")
|
|
1215
|
-
return true;
|
|
1216
|
-
return left.sha256 === right.sha256;
|
|
1217
|
-
}
|
|
1218
|
-
function assertTaskSourceRestored(expected) {
|
|
1219
|
-
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
1220
|
-
const restored = actual.state === expected.state &&
|
|
1221
|
-
(actual.state === "absent" ||
|
|
1222
|
-
(expected.state === "present" && actual.sha256 === expected.sha256 && actual.content === expected.content));
|
|
1223
|
-
if (!restored) {
|
|
1224
|
-
throw new UsageError(`Task source ${JSON.stringify(expected.filePath)} could not be restored without replacing a concurrent owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
class TaskSourceRestoredBoundaryError extends Error {
|
|
1228
|
-
cause;
|
|
1229
|
-
constructor(cause) {
|
|
1230
|
-
super(errorMessage(cause));
|
|
1231
|
-
this.name = "TaskSourceRestoredBoundaryError";
|
|
1232
|
-
this.cause = cause;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
function sameTaskSourceStat(left, right) {
|
|
1236
|
-
return (left.dev === right.dev &&
|
|
1237
|
-
left.ino === right.ino &&
|
|
1238
|
-
left.mode === right.mode &&
|
|
1239
|
-
left.size === right.size &&
|
|
1240
|
-
left.mtimeNs === right.mtimeNs &&
|
|
1241
|
-
left.ctimeNs === right.ctimeNs);
|
|
1242
|
-
}
|
|
1243
|
-
function hashTaskSource(source) {
|
|
1244
|
-
return createHash("sha256").update(Buffer.from(source, "utf8")).digest("hex");
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* Resolve the bundle a mutating/run task command targets. Returns the resolved
|
|
1248
|
-
* write/read target, its stash path, and the `--bundle <bundle>` token to embed
|
|
1249
|
-
* in scheduled invocations. The primary bundle uses the target-less form.
|
|
1250
|
-
*/
|
|
1251
|
-
function resolveTaskBundle(target, opts) {
|
|
688
|
+
/** The bundle `task add` writes into: its write target, config, stash path, and name. */
|
|
689
|
+
function resolveTaskBundle(target) {
|
|
1252
690
|
const config = loadConfig();
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
const stashDir = resolved.source.path;
|
|
1256
|
-
const installTarget = isPrimaryStashPath(stashDir) ? undefined : (resolved.selector ?? resolved.source.name);
|
|
1257
|
-
return { resolved, config, stashDir, bundleName: resolved.source.name, installTarget };
|
|
691
|
+
const resolved = prepareWriteTargetForMutation(resolveWriteTarget(config, target, { requireWritable: true }));
|
|
692
|
+
return { resolved, config, stashDir: resolved.source.path, bundleName: resolved.source.name };
|
|
1258
693
|
}
|
|
1259
694
|
function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
|
|
1260
695
|
return async ({ bundle, type, name }) => {
|
|
@@ -1299,8 +734,9 @@ export function resolveTaskReadBundle(refBundle, flagBundle) {
|
|
|
1299
734
|
* New scheduler bindings always carry a canonical `--bundle <owner>` token,
|
|
1300
735
|
* including an env-selected working stash. That stash need not be persisted in
|
|
1301
736
|
* config (CI, one-shot tools, and fresh installs commonly use only
|
|
1302
|
-
* AKM_BUNDLE_DIR), so its scheduled child
|
|
1303
|
-
*
|
|
737
|
+
* AKM_BUNDLE_DIR), so its scheduled child — every row sets AKM_BUNDLE_DIR to
|
|
738
|
+
* the working stash it was synced from — must accept precisely its derived
|
|
739
|
+
* owner name.
|
|
1304
740
|
*
|
|
1305
741
|
* This is intentionally narrower than an unknown-bundle fallback: a configured
|
|
1306
742
|
* source always wins, and an unconfigured selector is accepted only when it is
|
|
@@ -1328,31 +764,9 @@ function isPrimaryStashPath(candidate) {
|
|
|
1328
764
|
}
|
|
1329
765
|
return path.resolve(candidate) === primary;
|
|
1330
766
|
}
|
|
1331
|
-
/** Two bundle attributions match when both are the primary (undefined) or equal names. */
|
|
1332
|
-
function sameBundle(a, b) {
|
|
1333
|
-
return (a ?? undefined) === (b ?? undefined);
|
|
1334
|
-
}
|
|
1335
|
-
function installedEntryRunsTask(entry, id) {
|
|
1336
|
-
const invocation = entry.invocation;
|
|
1337
|
-
return invocation?.[0] === "task" && invocation[1] === "run" && invocation[2] === id;
|
|
1338
|
-
}
|
|
1339
|
-
function foreignScheduleMessage(id, existingTarget) {
|
|
1340
|
-
const where = existingTarget === undefined ? "the default bundle" : `bundle "${existingTarget}"`;
|
|
1341
|
-
return `Task id "${id}" is already scheduled from ${where}; rename the task or disable the existing one first.`;
|
|
1342
|
-
}
|
|
1343
767
|
function errorMessage(cause) {
|
|
1344
768
|
return cause instanceof Error ? cause.message : String(cause);
|
|
1345
769
|
}
|
|
1346
|
-
/**
|
|
1347
|
-
* Refuse to schedule an id already installed from a DIFFERENT bundle. Scheduler
|
|
1348
|
-
* ids are the bare task id (never namespaced), so a single id can be active from
|
|
1349
|
-
* only one bundle at a time — a collision is a hard error, not an auto-rename.
|
|
1350
|
-
*/
|
|
1351
|
-
function assertNoForeignSchedule(entries, id, installTarget) {
|
|
1352
|
-
const foreign = entries.find((entry) => entry.id === id && !sameBundle(entry.target, installTarget));
|
|
1353
|
-
if (foreign)
|
|
1354
|
-
throw new UsageError(foreignScheduleMessage(id, foreign.target), "RESOURCE_ALREADY_EXISTS");
|
|
1355
|
-
}
|
|
1356
770
|
/**
|
|
1357
771
|
* Infer a JSON-Schema-subset `type` keyword from one `--params` value's
|
|
1358
772
|
* runtime shape (spec docs/plans/specs/p4-deletions-closeout.md §3.2.6, row
|