akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +756 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- 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 +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- 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 +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- 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 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- 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 +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- 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/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- 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 +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- 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-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- 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 +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- 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/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- 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 +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -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 +2 -7
- 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/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.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 +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- 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 +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- 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/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 +0 -17
- 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 +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- 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 +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- 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 +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- 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/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +363 -679
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- 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 +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- 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 +87 -406
- 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 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- 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 -21
- 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/config/retired-experimental-keys-shim.js +0 -62
- 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/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- 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/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/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 -78
- 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 {
|
|
43
|
+
import { compileTaskSchedulerBindings, schedulerBindingNativeId, } from "../../tasks/scheduler-binding.js";
|
|
39
44
|
import { schedulerContextDescriptor, schedulerContextPath, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
|
|
40
|
-
import {
|
|
41
|
-
import { renderSchedulerPlanPreview,
|
|
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
144
|
});
|
|
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
|
-
});
|
|
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,272 +289,236 @@ 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, publish, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
305
|
+
if (publish && plan.operations.some((operation) => operation.kind !== "remove"))
|
|
306
|
+
publish();
|
|
307
|
+
const failed = new Set();
|
|
308
|
+
const failures = [...plan.failures];
|
|
309
|
+
for (const operation of plan.operations) {
|
|
310
|
+
try {
|
|
311
|
+
if (operation.kind === "remove")
|
|
312
|
+
await sched.uninstall(operation.nativeId);
|
|
313
|
+
else
|
|
314
|
+
await sched.install(operation.binding, operation.options);
|
|
315
|
+
}
|
|
316
|
+
catch (cause) {
|
|
317
|
+
if (operation.kind === "remove") {
|
|
318
|
+
failed.add(operation.id);
|
|
319
|
+
failures.push({ path: operation.nativeId, reason: errorMessage(cause) });
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
failed.add(operation.binding.id);
|
|
323
|
+
failures.push({
|
|
324
|
+
path: operation.binding.source,
|
|
325
|
+
ref: operation.binding.logicalSource.ref,
|
|
326
|
+
reason: errorMessage(cause),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const applied = (ids) => ids.filter((id) => !failed.has(id));
|
|
332
|
+
return {
|
|
333
|
+
installed: applied(plan.installed),
|
|
334
|
+
updated: applied(plan.updated),
|
|
335
|
+
removed: applied(plan.removed),
|
|
336
|
+
unchanged: [...plan.unchanged],
|
|
337
|
+
skipped: [],
|
|
338
|
+
backend: sched.name,
|
|
339
|
+
failures: failures.map((failure) => ({ ...failure })),
|
|
340
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
}
|
|
377
344
|
/**
|
|
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.
|
|
345
|
+
* `akm task sync --dry-run` (#849): the exact plan `akmTasksSync` would
|
|
346
|
+
* apply, previewed. It writes nothing: no config, no descriptor, no row.
|
|
385
347
|
*/
|
|
348
|
+
export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
349
|
+
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, { ...options, dryRun: true });
|
|
350
|
+
return renderSchedulerPlanPreview(sched.name, plan.operations, plan.unchanged, plan.failures);
|
|
351
|
+
}
|
|
386
352
|
async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
387
|
-
|
|
353
|
+
let config = loadConfig();
|
|
388
354
|
const sched = deps.backend ?? selectBackend();
|
|
389
|
-
|
|
390
|
-
|
|
355
|
+
const installed = await listInstalledRows(sched);
|
|
356
|
+
// A config that predates `scheduler.enabled` means "keep what is
|
|
357
|
+
// installed": the akm-written rows become the host's choice (written
|
|
358
|
+
// unless this is a dry run) before the desired set, which would otherwise
|
|
359
|
+
// remove them, is computed.
|
|
360
|
+
let enabledRefs = schedulerEnabledRefs(config);
|
|
361
|
+
if (enabledRefs === undefined) {
|
|
362
|
+
enabledRefs = initializeSchedulerChoice(installed, config, { write: options.dryRun !== true });
|
|
363
|
+
if (options.dryRun !== true)
|
|
364
|
+
config = loadConfig();
|
|
391
365
|
}
|
|
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
366
|
const activeSources = resolveActiveConfiguredSources(config);
|
|
414
367
|
if (bundleTarget) {
|
|
415
|
-
//
|
|
416
|
-
// other than filesystem/git outright, so a website/npm bundle can never
|
|
417
|
-
// carry scheduler state (akm task enable already fails the same way).
|
|
418
|
-
// Surface that as a clear usage error here instead of letting the
|
|
419
|
-
// write-target resolution below raise a generic ConfigError.
|
|
368
|
+
// Only filesystem and git bundles can carry scheduler state.
|
|
420
369
|
const targetSource = activeSources.find((source) => source.name === bundleTarget);
|
|
421
370
|
if (targetSource && !isWriteCapableSourceKind(targetSource.type)) {
|
|
422
371
|
throw new UsageError(`Bundle "${bundleTarget}" has kind "${targetSource.type}"; task scheduling is only supported for filesystem and git bundles.`, "INVALID_FLAG_VALUE");
|
|
423
372
|
}
|
|
424
373
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
374
|
+
const configuredSources = resolveConfiguredSources(config);
|
|
375
|
+
const schedulable = activeSources
|
|
376
|
+
.filter((source) => isWriteCapableSourceKind(source.type))
|
|
377
|
+
.map((source) => source.name);
|
|
378
|
+
// No configured bundle at all: the working stash (e.g. `AKM_BUNDLE_DIR`).
|
|
379
|
+
const selected = bundleTarget
|
|
430
380
|
? [bundleTarget]
|
|
431
|
-
:
|
|
432
|
-
|
|
381
|
+
: schedulable.length > 0 || configuredSources.length > 0
|
|
382
|
+
? schedulable
|
|
383
|
+
: [undefined];
|
|
384
|
+
const enabledRefSet = new Set(enabledRefs);
|
|
385
|
+
const desired = [];
|
|
386
|
+
const failures = [];
|
|
387
|
+
const keepRefs = new Set();
|
|
388
|
+
const scopes = [];
|
|
389
|
+
// Each bundle is its own item: one that cannot be read is reported and
|
|
390
|
+
// left untouched while the others reconcile. A scoped sync has only that
|
|
391
|
+
// bundle, so its failure is the whole operation's.
|
|
392
|
+
for (const name of selected) {
|
|
393
|
+
try {
|
|
394
|
+
const resolved = resolveTaskReadBundle(undefined, name);
|
|
395
|
+
const stashDir = resolved.source.path;
|
|
396
|
+
const adapterId = resolved.source.adapterId ?? detectAdapterId(stashDir);
|
|
397
|
+
const compiled = await compileSchedulerSources({
|
|
398
|
+
sourceRoot: stashDir,
|
|
399
|
+
adapterId,
|
|
400
|
+
bundleName: resolved.source.name,
|
|
401
|
+
backend: sched.name,
|
|
402
|
+
config,
|
|
403
|
+
resolveAsset: taskProjectionAssetResolver(config, resolved.source.name, stashDir),
|
|
404
|
+
enabledRefs: enabledRefSet,
|
|
405
|
+
});
|
|
406
|
+
desired.push(...compiled.desired);
|
|
407
|
+
failures.push(...compiled.failures);
|
|
408
|
+
for (const failure of compiled.failures)
|
|
409
|
+
if (failure.ref)
|
|
410
|
+
keepRefs.add(failure.ref);
|
|
411
|
+
scopes.push({ ...bundleScope(resolved.source.name, stashDir), adapterId });
|
|
412
|
+
}
|
|
413
|
+
catch (cause) {
|
|
414
|
+
if (bundleTarget)
|
|
415
|
+
throw cause;
|
|
416
|
+
failures.push({ path: name ?? "(default bundle)", reason: errorMessage(cause) });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// An enabled ref of a synced bundle that produced nothing names a source
|
|
420
|
+
// that is gone or has no schedule; its row, if any, is removed below.
|
|
421
|
+
const represented = new Set([...desired.map((binding) => binding.logicalSource.ref), ...keepRefs]);
|
|
422
|
+
const synced = new Set(scopes.map((scope) => scope.bundleName));
|
|
423
|
+
for (const ref of enabledRefs) {
|
|
424
|
+
const bundle = parseBundleRef(ref).bundle;
|
|
425
|
+
if (bundle !== undefined && synced.has(bundle) && !represented.has(ref)) {
|
|
426
|
+
failures.push({ path: ref, ref, reason: `Enabled ref ${JSON.stringify(ref)} was not found or has no schedule.` });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// A disabled bundle's rows go on an unscoped sync, without reading its content.
|
|
430
|
+
const inactive = new Set(configuredSources.filter((source) => source.enabled === false).map((source) => source.name));
|
|
431
|
+
const extraRemovals = bundleTarget
|
|
433
432
|
? []
|
|
434
|
-
:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const plan = emptySchedulerSyncPlan(inactiveOperations);
|
|
438
|
-
return { sched, plan, sourceSnapshots: Object.freeze([]), prepared: undefined, warnings: [] };
|
|
439
|
-
}
|
|
440
|
-
const selectedNames = sourceNames.length > 0 ? sourceNames : [undefined];
|
|
441
|
-
const enabled = activeSchedulerActivations(config);
|
|
442
|
-
const enabledActivations = new Set(enabled.map((activation) => `${activation.kind}\0${activation.ref}`));
|
|
443
|
-
const preparedSets = [];
|
|
444
|
-
for (const sourceName of selectedNames) {
|
|
445
|
-
const resolved = resolveTaskReadBundle(undefined, sourceName);
|
|
446
|
-
const stashDir = resolved.source.path;
|
|
447
|
-
const syncTarget = sourceName !== undefined && !isPrimaryStashPath(stashDir) ? sourceName : undefined;
|
|
448
|
-
const common = {
|
|
449
|
-
sourceRoot: stashDir,
|
|
450
|
-
adapterId: resolved.source.adapterId ?? detectAdapterId(stashDir),
|
|
451
|
-
bundleName: resolved.source.name,
|
|
452
|
-
...(syncTarget === undefined ? { bundlePath: path.resolve(stashDir) } : {}),
|
|
453
|
-
...(syncTarget ? { bundleTarget: syncTarget } : {}),
|
|
454
|
-
backend: sched.name,
|
|
455
|
-
installed: allEntries,
|
|
456
|
-
nativeArtifacts,
|
|
457
|
-
inspection: Object.freeze({ installed: allEntries, artifacts: nativeArtifacts }),
|
|
458
|
-
enabledActivations,
|
|
459
|
-
rebind: options.rebind === true,
|
|
460
|
-
config,
|
|
461
|
-
resolveAsset: taskProjectionAssetResolver(config, resolved.source.name, stashDir),
|
|
462
|
-
};
|
|
463
|
-
preparedSets.push({
|
|
464
|
-
common,
|
|
465
|
-
preparedSources: await prepareSchedulerSyncSourceSet(common),
|
|
466
|
-
...(syncTarget ? { syncTarget } : {}),
|
|
433
|
+
: installed.filter((row) => {
|
|
434
|
+
const bundle = installedSchedulerBundle(config, row);
|
|
435
|
+
return bundle !== undefined && inactive.has(bundle);
|
|
467
436
|
});
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const plans = preparedSets.map(({ common, preparedSources, syncTarget }) => finalizeSchedulerSyncPlan({
|
|
479
|
-
...common,
|
|
480
|
-
...(prepared?.options
|
|
481
|
-
? { installOptions: { ...prepared.options, ...(syncTarget ? { target: syncTarget } : {}) } }
|
|
482
|
-
: syncTarget
|
|
483
|
-
? { installOptions: { target: syncTarget } }
|
|
484
|
-
: {}),
|
|
485
|
-
...(expectedSignature
|
|
486
|
-
? {
|
|
487
|
-
expectedSignature: (binding, install) => expectedSignature(binding, install),
|
|
488
|
-
}
|
|
489
|
-
: {}),
|
|
490
|
-
}, preparedSources));
|
|
491
|
-
assertNoCrossBundleSchedulerCollisions(plans);
|
|
492
|
-
const representedRefs = new Set(plans.flatMap((candidate) => [
|
|
493
|
-
...candidate.desired.map((binding) => binding.logicalSource.ref),
|
|
494
|
-
...candidate.failures.flatMap((failure) => (failure.ref ? [failure.ref] : [])),
|
|
495
|
-
]));
|
|
496
|
-
const selectedBundleNames = new Set(preparedSets.map(({ common }) => common.bundleName));
|
|
497
|
-
const missingActivationFailures = enabled
|
|
498
|
-
.filter((activation) => {
|
|
499
|
-
const bundle = parseBundleRef(activation.ref).bundle;
|
|
500
|
-
return bundle !== undefined && selectedBundleNames.has(bundle) && !representedRefs.has(activation.ref);
|
|
501
|
-
})
|
|
502
|
-
.map((activation) => ({
|
|
503
|
-
path: activation.ref,
|
|
504
|
-
ref: activation.ref,
|
|
505
|
-
reason: `Enabled ${activation.kind} ${JSON.stringify(activation.ref)} was not found or has no schedule.`,
|
|
506
|
-
}));
|
|
507
|
-
const first = plans[0];
|
|
508
|
-
if (!first)
|
|
509
|
-
throw new ConfigError("No configured bundle is available for scheduler sync.", "INVALID_CONFIG_FILE");
|
|
510
|
-
const plan = Object.freeze({
|
|
511
|
-
desired: Object.freeze(plans.flatMap((candidate) => candidate.desired)),
|
|
512
|
-
installed: Object.freeze(plans.flatMap((candidate) => candidate.installed)),
|
|
513
|
-
updated: Object.freeze(plans.flatMap((candidate) => candidate.updated)),
|
|
514
|
-
removed: Object.freeze([
|
|
515
|
-
...plans.flatMap((candidate) => candidate.removed),
|
|
516
|
-
...inactiveOperations.map((operation) => operation.id),
|
|
517
|
-
]),
|
|
518
|
-
unchanged: Object.freeze(plans.flatMap((candidate) => candidate.unchanged)),
|
|
519
|
-
operations: Object.freeze([...plans.flatMap((candidate) => candidate.operations), ...inactiveOperations]),
|
|
520
|
-
sourceSnapshot: first.sourceSnapshot,
|
|
521
|
-
failures: Object.freeze([...plans.flatMap((candidate) => candidate.failures), ...missingActivationFailures]),
|
|
522
|
-
});
|
|
523
|
-
return { sched, plan, sourceSnapshots: plans.map((candidate) => candidate.sourceSnapshot), prepared, warnings };
|
|
524
|
-
}
|
|
525
|
-
function inactiveBundleRemovalOperations(config, configuredSources, installed, artifacts) {
|
|
526
|
-
const inactive = new Set(configuredSources.filter((source) => source.enabled === false).map((source) => source.name));
|
|
527
|
-
if (inactive.size === 0)
|
|
528
|
-
return [];
|
|
529
|
-
return installed
|
|
530
|
-
.map((entry) => ({ entry, bundleName: installedSchedulerBundle(config, entry) }))
|
|
531
|
-
.filter((candidate) => candidate.bundleName !== undefined && inactive.has(candidate.bundleName))
|
|
532
|
-
.sort((left, right) => left.entry.id.localeCompare(right.entry.id))
|
|
533
|
-
.map(({ entry, bundleName }) => {
|
|
534
|
-
const adapterId = bundleComponentConfig(config.bundles?.[bundleName])?.adapter ?? "akm";
|
|
535
|
-
return buildSchedulerRemoveOperation(entry.id, entry, artifacts, { adapterId, bundleName });
|
|
437
|
+
const runtime = prepareSchedulerRuntime(deps);
|
|
438
|
+
const plan = planSchedulerSync({
|
|
439
|
+
desired,
|
|
440
|
+
installed,
|
|
441
|
+
scopes,
|
|
442
|
+
...(sched.expectedSignature ? { expectedSignature: sched.expectedSignature.bind(sched) } : {}),
|
|
443
|
+
...(runtime.options ? { installOptions: runtime.options } : {}),
|
|
444
|
+
rebind: options.rebind === true,
|
|
445
|
+
extraRemovals,
|
|
446
|
+
keepRefs,
|
|
536
447
|
});
|
|
448
|
+
const warnings = [];
|
|
449
|
+
const writesLauncher = options.rebind === true
|
|
450
|
+
? plan.operations.some((operation) => operation.kind !== "remove")
|
|
451
|
+
: plan.installed.length > 0;
|
|
452
|
+
if (runtime.via === "checkout" && writesLauncher) {
|
|
453
|
+
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\`.`);
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
sched,
|
|
457
|
+
plan: { ...plan, failures: [...failures, ...plan.failures] },
|
|
458
|
+
...(runtime.publish ? { publish: runtime.publish } : {}),
|
|
459
|
+
warnings,
|
|
460
|
+
};
|
|
537
461
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
462
|
+
/**
|
|
463
|
+
* The launcher and descriptor rows are written with. The descriptor follows
|
|
464
|
+
* the current policy on every sync; a row that is already installed keeps its
|
|
465
|
+
* launcher unless `--rebind` (see `installOptionsFor`). An injected backend
|
|
466
|
+
* (tests) renders with its own defaults.
|
|
467
|
+
*/
|
|
468
|
+
function prepareSchedulerRuntime(deps) {
|
|
469
|
+
if (deps.schedulerRuntime) {
|
|
470
|
+
const runtime = deps.schedulerRuntime();
|
|
471
|
+
return {
|
|
472
|
+
options: { binding: runtime.binding, contextPath: runtime.contextPath },
|
|
473
|
+
...(runtime.via ? { via: runtime.via } : {}),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (deps.backend)
|
|
477
|
+
return {};
|
|
478
|
+
const descriptor = schedulerContextDescriptor();
|
|
479
|
+
const invocation = resolveAkmInvocation();
|
|
480
|
+
return {
|
|
481
|
+
options: { binding: invocation.argv, contextPath: schedulerContextPath(descriptor) },
|
|
482
|
+
// The content-addressed descriptor is written once, before the first row that references it.
|
|
483
|
+
publish: () => {
|
|
484
|
+
writeSchedulerContextDescriptor(descriptor);
|
|
485
|
+
},
|
|
486
|
+
via: invocation.via,
|
|
487
|
+
};
|
|
545
488
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
sourcePhysicalIdentity: "inactive",
|
|
552
|
-
sourceDirectoryVersion: "inactive",
|
|
553
|
-
files: Object.freeze([]),
|
|
554
|
-
directoryManifests: Object.freeze([]),
|
|
555
|
-
});
|
|
556
|
-
return Object.freeze({
|
|
557
|
-
desired: Object.freeze([]),
|
|
558
|
-
installed: Object.freeze([]),
|
|
559
|
-
updated: Object.freeze([]),
|
|
560
|
-
removed: Object.freeze(operations.map((operation) => operation.id)),
|
|
561
|
-
unchanged: Object.freeze([]),
|
|
562
|
-
operations: Object.freeze([...operations]),
|
|
563
|
-
sourceSnapshot,
|
|
564
|
-
failures: Object.freeze([]),
|
|
489
|
+
/** One read of the akm-owned rows, each attributed to the bundle path its own descriptor names (#846). */
|
|
490
|
+
async function listInstalledRows(sched) {
|
|
491
|
+
return (await sched.list()).map((row) => {
|
|
492
|
+
const ownerBundlePath = row.contextPath ? resolveInstalledOwnerPath(row.contextPath) : undefined;
|
|
493
|
+
return ownerBundlePath !== undefined ? { ...row, ownerBundlePath } : row;
|
|
565
494
|
});
|
|
566
495
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
return
|
|
571
|
-
if (invocation[0] === "workflow" && invocation[1] === "run" && invocation[2]) {
|
|
572
|
-
try {
|
|
573
|
-
return parseBundleRef(invocation[2]).bundle;
|
|
574
|
-
}
|
|
575
|
-
catch {
|
|
576
|
-
return undefined;
|
|
577
|
-
}
|
|
496
|
+
/** Best-effort recovery of an installed binding's owning bundle path (#846). */
|
|
497
|
+
function resolveInstalledOwnerPath(contextPath) {
|
|
498
|
+
try {
|
|
499
|
+
return validateSchedulerContextDescriptor(contextPath).environment.AKM_BUNDLE_DIR;
|
|
578
500
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
function assertNoCrossBundleSchedulerCollisions(plans) {
|
|
582
|
-
const owners = new Map();
|
|
583
|
-
for (const binding of plans.flatMap((plan) => plan.desired)) {
|
|
584
|
-
const key = schedulerNativeArtifactKey(schedulerBindingNativeId(binding));
|
|
585
|
-
const existing = owners.get(key);
|
|
586
|
-
if (existing && existing.logicalSource.ref !== binding.logicalSource.ref) {
|
|
587
|
-
throw new UsageError(`Scheduler native id ${JSON.stringify(schedulerBindingNativeId(binding))} is claimed by both ` +
|
|
588
|
-
`${JSON.stringify(existing.logicalSource.ref)} and ${JSON.stringify(binding.logicalSource.ref)}. ` +
|
|
589
|
-
"Rename one task before enabling both.", "RESOURCE_ALREADY_EXISTS");
|
|
590
|
-
}
|
|
591
|
-
owners.set(key, binding);
|
|
501
|
+
catch {
|
|
502
|
+
return undefined;
|
|
592
503
|
}
|
|
593
504
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
? prepared.publish
|
|
598
|
-
: undefined, sourceSnapshots);
|
|
599
|
-
return {
|
|
600
|
-
installed: [...plan.installed],
|
|
601
|
-
updated: [...plan.updated],
|
|
602
|
-
removed: [...plan.removed],
|
|
603
|
-
unchanged: [...plan.unchanged],
|
|
604
|
-
skipped: [],
|
|
605
|
-
backend: sched.name,
|
|
606
|
-
failures: plan.failures.map((failure) => ({ ...failure })),
|
|
607
|
-
...(warnings.length > 0 ? { warnings } : {}),
|
|
608
|
-
};
|
|
505
|
+
/** The primary bundle proves its rows by path (#846); any other bundle by its config name. */
|
|
506
|
+
function bundleScope(bundleName, stashDir) {
|
|
507
|
+
return isPrimaryStashPath(stashDir) ? { bundleName, bundlePath: path.resolve(stashDir) } : { bundleName };
|
|
609
508
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
* point: `prepared.publish`, if any, is dropped on the floor here, never
|
|
618
|
-
* invoked. Zero durable writes, mirroring `akm workflow plan`.
|
|
619
|
-
*/
|
|
620
|
-
export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
621
|
-
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
622
|
-
return renderSchedulerSyncPlanPreview(sched.name, plan);
|
|
509
|
+
function installedSchedulerBundle(config, row) {
|
|
510
|
+
const direct = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
511
|
+
if (direct !== undefined)
|
|
512
|
+
return direct;
|
|
513
|
+
if (row.ownerBundlePath === undefined)
|
|
514
|
+
return undefined;
|
|
515
|
+
return bundleKeyForContentRoot(config, row.ownerBundlePath);
|
|
623
516
|
}
|
|
624
517
|
/**
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
* resolved owner no longer exists on disk (`dead-bundle-path`) is a
|
|
630
|
-
* candidate; `missing-path` (e.g. the akm binary itself moved) is a
|
|
631
|
-
* different failure mode and is intentionally NOT folded in here, per the
|
|
632
|
-
* scoping in #851 — an entry that still resolves to a live bundle is never a
|
|
633
|
-
* candidate, full stop.
|
|
518
|
+
* Why `akm task prune` (#851) would remove an installed row: its own
|
|
519
|
+
* descriptor does not load (`invalid-context`) or names a bundle directory
|
|
520
|
+
* that is gone (`dead-bundle-path`). A row that still resolves to a live
|
|
521
|
+
* bundle is never a candidate.
|
|
634
522
|
*/
|
|
635
523
|
function classifyPruneCandidate(entry) {
|
|
636
524
|
let ownerBundlePath;
|
|
@@ -644,78 +532,47 @@ function classifyPruneCandidate(entry) {
|
|
|
644
532
|
return "dead-bundle-path";
|
|
645
533
|
return undefined;
|
|
646
534
|
}
|
|
647
|
-
async function buildTaskPrunePlan(deps = {}, options = {}) {
|
|
648
|
-
const sched = deps.backend ?? selectBackend();
|
|
649
|
-
if (!sched.inspectBindings) {
|
|
650
|
-
throw new ConfigError(`Scheduler backend "${sched.name}" cannot provide one coherent inspection for prune.`, "INVALID_CONFIG_FILE");
|
|
651
|
-
}
|
|
652
|
-
const inspection = await sched.inspectBindings({});
|
|
653
|
-
const candidates = new Map();
|
|
654
|
-
for (const entry of inspection.installed) {
|
|
655
|
-
const reason = classifyPruneCandidate(entry);
|
|
656
|
-
if (reason)
|
|
657
|
-
candidates.set(entry.id, reason);
|
|
658
|
-
}
|
|
659
|
-
const requestedIds = options.id?.filter((id) => id.length > 0) ?? [];
|
|
660
|
-
for (const id of requestedIds) {
|
|
661
|
-
if (!candidates.has(id)) {
|
|
662
|
-
throw new UsageError(`Scheduler binding ${JSON.stringify(id)} is not an orphaned prune candidate ` +
|
|
663
|
-
"(either not installed, or it still resolves to a live bundle) — refusing to prune it.", "INVALID_FLAG_VALUE");
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
const idFilter = requestedIds.length > 0 ? new Set(requestedIds) : undefined;
|
|
667
|
-
const resolved = resolveTaskReadBundle(undefined, undefined);
|
|
668
|
-
const bundleContext = {
|
|
669
|
-
adapterId: resolved.source.adapterId ?? detectAdapterId(resolved.source.path),
|
|
670
|
-
bundleName: resolved.source.name,
|
|
671
|
-
};
|
|
672
|
-
const operations = [];
|
|
673
|
-
for (const entry of inspection.installed) {
|
|
674
|
-
const reason = candidates.get(entry.id);
|
|
675
|
-
if (!reason)
|
|
676
|
-
continue;
|
|
677
|
-
if (idFilter && !idFilter.has(entry.id))
|
|
678
|
-
continue;
|
|
679
|
-
const operation = buildSchedulerRemoveOperation(entry.id, entry, inspection.artifacts, bundleContext);
|
|
680
|
-
operations.push(Object.freeze({ ...operation, reason }));
|
|
681
|
-
}
|
|
682
|
-
return { sched, operations: Object.freeze(operations) };
|
|
683
|
-
}
|
|
684
535
|
/**
|
|
685
|
-
* `akm task prune` (#851): remove installed
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* `akmTasksSyncPlan`'s zero-write guarantee. `--id` (one or more) narrows
|
|
690
|
-
* execution to exactly those bindings; `--yes` alone executes every
|
|
691
|
-
* currently-computed candidate. Both still return the full preview so the
|
|
692
|
-
* plan is never silent about what it did.
|
|
536
|
+
* `akm task prune` (#851): remove installed rows `sync` can never reclaim
|
|
537
|
+
* because their descriptor no longer resolves to a live bundle. It scans
|
|
538
|
+
* every installed row, not one bundle's. Without `--yes` it only previews and
|
|
539
|
+
* writes nothing; `--id` narrows it to named candidates.
|
|
693
540
|
*/
|
|
694
541
|
export async function akmTasksPrune(deps = {}, options = {}) {
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
backend: sched.name,
|
|
705
|
-
dryRun: false,
|
|
706
|
-
preview,
|
|
707
|
-
removed: operations.map((operation) => operation.id),
|
|
542
|
+
const prune = async () => {
|
|
543
|
+
const sched = deps.backend ?? selectBackend();
|
|
544
|
+
const operations = pruneOperations(await sched.list(), options.id ?? []);
|
|
545
|
+
const preview = renderSchedulerPlanPreview(sched.name, operations);
|
|
546
|
+
if (!options.yes)
|
|
547
|
+
return { backend: sched.name, dryRun: true, preview, removed: [] };
|
|
548
|
+
for (const operation of operations)
|
|
549
|
+
await sched.uninstall(operation.nativeId);
|
|
550
|
+
return { backend: sched.name, dryRun: false, preview, removed: operations.map((operation) => operation.id) };
|
|
708
551
|
};
|
|
552
|
+
return options.yes ? withSchedulerLock(prune) : prune();
|
|
553
|
+
}
|
|
554
|
+
function pruneOperations(installed, requestedIds) {
|
|
555
|
+
const candidates = installed.flatMap((row) => {
|
|
556
|
+
const reason = classifyPruneCandidate(row);
|
|
557
|
+
return reason ? [{ row, reason }] : [];
|
|
558
|
+
});
|
|
559
|
+
const ids = requestedIds.filter((id) => id.length > 0);
|
|
560
|
+
for (const id of ids) {
|
|
561
|
+
if (!candidates.some(({ row }) => row.id === id)) {
|
|
562
|
+
throw new UsageError(`Scheduler binding ${JSON.stringify(id)} is not an orphaned prune candidate ` +
|
|
563
|
+
"(either not installed, or it still resolves to a live bundle) — refusing to prune it.", "INVALID_FLAG_VALUE");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return candidates
|
|
567
|
+
.filter(({ row }) => ids.length === 0 || ids.includes(row.id))
|
|
568
|
+
.map(({ row, reason }) => ({ kind: "remove", id: row.id, nativeId: installedRowNativeId(row), reason }));
|
|
709
569
|
}
|
|
710
570
|
export async function akmTasksDoctor(deps = {}) {
|
|
711
571
|
const warnings = [];
|
|
712
|
-
let invocation = {
|
|
713
|
-
argv: [],
|
|
714
|
-
via: "unresolved",
|
|
715
|
-
};
|
|
572
|
+
let invocation = { argv: [], via: "unresolved" };
|
|
716
573
|
try {
|
|
717
574
|
const r = (deps.resolveInvocation ?? resolveAkmInvocation)();
|
|
718
|
-
invocation = { argv: r.argv, via: r.via
|
|
575
|
+
invocation = { argv: r.argv, via: r.via };
|
|
719
576
|
}
|
|
720
577
|
catch (err) {
|
|
721
578
|
warnings.push(err instanceof Error ? err.message : String(err));
|
|
@@ -735,7 +592,7 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
735
592
|
warnings.push(`Unable to inspect installed ${backend} definitions: ${error instanceof Error ? error.message : String(error)}`);
|
|
736
593
|
}
|
|
737
594
|
}
|
|
738
|
-
const bindings = groupInstalledBindings(installed
|
|
595
|
+
const bindings = groupInstalledBindings(installed);
|
|
739
596
|
// Report the EFFECTIVE engine view — the same one the runner resolves —
|
|
740
597
|
// so doctor never says "no engine" on an install where tasks actually run.
|
|
741
598
|
const { config } = withEngineFallback(loadConfig());
|
|
@@ -765,7 +622,6 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
765
622
|
defaultStrategy: improveStrategyName,
|
|
766
623
|
enabled: triage.enabled === true,
|
|
767
624
|
applyMode: triage.applyMode ?? "queue",
|
|
768
|
-
policy: triage.policy ?? "personal-stash",
|
|
769
625
|
}
|
|
770
626
|
: undefined;
|
|
771
627
|
return {
|
|
@@ -786,298 +642,11 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
786
642
|
};
|
|
787
643
|
}
|
|
788
644
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
789
|
-
|
|
790
|
-
await applySchedulerTransaction(backend, plan.operations, {
|
|
791
|
-
initialExpectations: plan.operations.map((operation) => operation.expected),
|
|
792
|
-
assertReadSet: () => {
|
|
793
|
-
for (const snapshot of sourceSnapshots)
|
|
794
|
-
assertSchedulerSourceSnapshot(snapshot);
|
|
795
|
-
},
|
|
796
|
-
beforeOperation: (_operation, index) => {
|
|
797
|
-
if (index === 0)
|
|
798
|
-
publish?.();
|
|
799
|
-
},
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
async function applySchedulerTransaction(backend, operations, hooks) {
|
|
803
|
-
if (operations.length === 0) {
|
|
804
|
-
hooks.assertReadSet?.();
|
|
805
|
-
await hooks.afterOperations?.();
|
|
806
|
-
hooks.assertReadSet?.();
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
hooks.assertReadSet?.();
|
|
810
|
-
if (!backend.snapshotBindings || !backend.restoreBindings) {
|
|
811
|
-
throw new ConfigError(`Scheduler backend "${backend.name}" cannot snapshot and restore a whole-set transaction.`, "INVALID_CONFIG_FILE");
|
|
812
|
-
}
|
|
813
|
-
const nativeIds = [
|
|
814
|
-
...new Set(operations.map((operation) => operation.kind === "remove" ? operation.nativeId : schedulerBindingNativeId(operation.binding))),
|
|
815
|
-
];
|
|
816
|
-
const snapshot = await backend.snapshotBindings(nativeIds);
|
|
817
|
-
assertSchedulerTransactionSnapshot(snapshot, nativeIds, hooks.initialExpectations);
|
|
818
|
-
const rollbackExpected = schedulerRollbackExpectations(snapshot, operations);
|
|
819
|
-
hooks.assertReadSet?.();
|
|
820
|
-
try {
|
|
821
|
-
for (const [index, operation] of operations.entries()) {
|
|
822
|
-
hooks.assertReadSet?.();
|
|
823
|
-
await hooks.beforeOperation?.(operation, index);
|
|
824
|
-
hooks.assertReadSet?.();
|
|
825
|
-
if (operation.kind === "remove")
|
|
826
|
-
await backend.uninstall(operation.nativeId, operation.expected);
|
|
827
|
-
else
|
|
828
|
-
await backend.install(operation.binding, operation.options, operation.expected);
|
|
829
|
-
hooks.assertReadSet?.();
|
|
830
|
-
}
|
|
831
|
-
hooks.assertReadSet?.();
|
|
832
|
-
await hooks.afterOperations?.();
|
|
833
|
-
hooks.assertReadSet?.();
|
|
834
|
-
}
|
|
835
|
-
catch (primaryError) {
|
|
836
|
-
let externalRollbackError;
|
|
837
|
-
try {
|
|
838
|
-
await hooks.rollbackExternal?.();
|
|
839
|
-
}
|
|
840
|
-
catch (error) {
|
|
841
|
-
externalRollbackError = error;
|
|
842
|
-
}
|
|
843
|
-
let nativeRollbackError;
|
|
844
|
-
const externalStateAllowsNativeRollback = externalRollbackError !== undefined &&
|
|
845
|
-
hooks.allowNativeRollbackAfterExternalFailure?.(externalRollbackError) === true;
|
|
846
|
-
if (!(externalRollbackError && hooks.suppressNativeRollbackWhenExternalFails && !externalStateAllowsNativeRollback)) {
|
|
847
|
-
try {
|
|
848
|
-
await backend.restoreBindings(snapshot, rollbackExpected);
|
|
849
|
-
}
|
|
850
|
-
catch (error) {
|
|
851
|
-
nativeRollbackError = error;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
const rollbackErrors = [externalRollbackError, nativeRollbackError].filter((error) => error !== undefined);
|
|
855
|
-
if (rollbackErrors.length > 0) {
|
|
856
|
-
throw new AggregateError([primaryError, ...rollbackErrors], `Scheduler transaction failed and rollback was incomplete: ${errorMessage(primaryError)}`);
|
|
857
|
-
}
|
|
858
|
-
throw primaryError;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
function schedulerRollbackExpectations(snapshot, operations) {
|
|
862
|
-
return Object.freeze(snapshot.nativeIds.map((nativeId) => {
|
|
863
|
-
const prior = snapshot.artifacts.find((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === schedulerNativeArtifactKey(nativeId));
|
|
864
|
-
const allowed = [];
|
|
865
|
-
if (prior) {
|
|
866
|
-
if (prior.fingerprint === undefined) {
|
|
867
|
-
throw new ConfigError(`Scheduler backend snapshot for ${JSON.stringify(nativeId)} has no exact fingerprint.`, "INVALID_CONFIG_FILE");
|
|
868
|
-
}
|
|
869
|
-
allowed.push(Object.freeze({
|
|
870
|
-
state: "present",
|
|
871
|
-
...(prior.bindingId !== undefined ? { bindingId: prior.bindingId } : {}),
|
|
872
|
-
...(prior.invocation !== undefined ? { invocation: Object.freeze([...prior.invocation]) } : {}),
|
|
873
|
-
fingerprint: prior.fingerprint,
|
|
874
|
-
}));
|
|
875
|
-
}
|
|
876
|
-
else {
|
|
877
|
-
allowed.push(Object.freeze({ state: "absent" }));
|
|
878
|
-
}
|
|
879
|
-
const matchingOperations = operations.filter((candidate) => {
|
|
880
|
-
const operationNativeId = candidate.kind === "remove" ? candidate.nativeId : schedulerBindingNativeId(candidate.binding);
|
|
881
|
-
return schedulerNativeArtifactKey(operationNativeId) === schedulerNativeArtifactKey(nativeId);
|
|
882
|
-
});
|
|
883
|
-
for (const operation of matchingOperations) {
|
|
884
|
-
if (operation.kind === "remove") {
|
|
885
|
-
if (!allowed.some((state) => state.state === "absent")) {
|
|
886
|
-
allowed.push(Object.freeze({ state: "absent" }));
|
|
887
|
-
}
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
if (operation.resultFingerprint === undefined) {
|
|
891
|
-
throw new ConfigError(`Scheduler backend cannot freeze the post-mutation fingerprint for ${JSON.stringify(nativeId)}.`, "INVALID_CONFIG_FILE");
|
|
892
|
-
}
|
|
893
|
-
allowed.push(Object.freeze({
|
|
894
|
-
state: "present",
|
|
895
|
-
bindingId: operation.binding.id,
|
|
896
|
-
invocation: Object.freeze([...operation.binding.invocation]),
|
|
897
|
-
fingerprint: operation.resultFingerprint,
|
|
898
|
-
}));
|
|
899
|
-
}
|
|
900
|
-
return Object.freeze({ nativeId, allowed: Object.freeze(allowed) });
|
|
901
|
-
}));
|
|
902
|
-
}
|
|
903
|
-
function assertSchedulerTransactionSnapshot(snapshot, nativeIds, initialExpectations) {
|
|
904
|
-
if (!snapshot ||
|
|
905
|
-
!Array.isArray(snapshot.nativeIds) ||
|
|
906
|
-
!Array.isArray(snapshot.artifacts) ||
|
|
907
|
-
nativeIds.some((nativeId) => !snapshot.nativeIds.includes(nativeId))) {
|
|
908
|
-
throw new ConfigError("Scheduler backend returned an incomplete transaction snapshot.", "INVALID_CONFIG_FILE");
|
|
909
|
-
}
|
|
910
|
-
const snapshotKeys = snapshot.nativeIds.map(schedulerNativeArtifactKey);
|
|
911
|
-
const requestedKeys = nativeIds.map(schedulerNativeArtifactKey);
|
|
912
|
-
if (snapshotKeys.length !== requestedKeys.length ||
|
|
913
|
-
new Set(snapshotKeys).size !== snapshotKeys.length ||
|
|
914
|
-
snapshotKeys.some((key) => !requestedKeys.includes(key))) {
|
|
915
|
-
throw new ConfigError("Scheduler backend returned an inexact normalized transaction snapshot set.", "INVALID_CONFIG_FILE");
|
|
916
|
-
}
|
|
917
|
-
for (const expected of initialExpectations) {
|
|
918
|
-
const artifact = assertSchedulerNativeArtifactCardinality(snapshot.artifacts, expected.nativeId, expected.state === "absent" ? 0 : 1);
|
|
919
|
-
assertSchedulerMutationArtifact(artifact, expected);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
async function prepareTaskAddSchedulerTransaction(input) {
|
|
923
|
-
if (!input.sched.inspectBindings) {
|
|
924
|
-
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot provide one coherent inspection for transactional add.`, "INVALID_CONFIG_FILE");
|
|
925
|
-
}
|
|
926
|
-
if (!input.sched.snapshotBindings || !input.sched.restoreBindings || !input.sched.expectedSignature) {
|
|
927
|
-
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot provide exact snapshot, restore, and signature contracts for transactional add.`, "INVALID_CONFIG_FILE");
|
|
928
|
-
}
|
|
929
|
-
const inspection = await input.sched.inspectBindings({ rebind: input.rebind });
|
|
930
|
-
const installedEntries = [...inspection.installed];
|
|
931
|
-
const nativeArtifacts = [...inspection.artifacts];
|
|
932
|
-
const seenNativeKeys = new Set();
|
|
933
|
-
for (const artifact of nativeArtifacts) {
|
|
934
|
-
const key = schedulerNativeArtifactKey(artifact.nativeId);
|
|
935
|
-
if (seenNativeKeys.has(key)) {
|
|
936
|
-
throw new UsageError(`Scheduler inspection has duplicate normalized native artifact ${JSON.stringify(artifact.nativeId)}.`, "RESOURCE_ALREADY_EXISTS");
|
|
937
|
-
}
|
|
938
|
-
seenNativeKeys.add(key);
|
|
939
|
-
}
|
|
940
|
-
for (const binding of input.taskBindings) {
|
|
941
|
-
assertNoForeignSchedule(installedEntries, binding.id, input.ownerTarget);
|
|
942
|
-
}
|
|
943
|
-
const taskEntries = installedEntries.filter((entry) => installedEntryRunsTask(entry, input.id));
|
|
944
|
-
const foreignTaskEntry = taskEntries.find((entry) => !sameBundle(entry.target, input.ownerTarget));
|
|
945
|
-
if (foreignTaskEntry) {
|
|
946
|
-
throw new UsageError(foreignScheduleMessage(input.id, foreignTaskEntry.target), "RESOURCE_ALREADY_EXISTS");
|
|
947
|
-
}
|
|
948
|
-
assertSchedulerNativeArtifactOwnership(input.taskBindings, nativeArtifacts);
|
|
949
|
-
const primary = input.taskBindings[0];
|
|
950
|
-
if (!primary)
|
|
951
|
-
throw new Error("invariant: scheduler transaction has no desired binding");
|
|
952
|
-
const installedEntry = installedEntries.find((entry) => entry.id === primary.id) ?? taskEntries[0];
|
|
953
|
-
const preparedRuntime = installedEntry && !input.rebind
|
|
954
|
-
? {
|
|
955
|
-
options: {
|
|
956
|
-
...input.installOpts,
|
|
957
|
-
binding: Object.freeze([...installedEntry.binding]),
|
|
958
|
-
contextPath: installedEntry.contextPath,
|
|
959
|
-
},
|
|
960
|
-
}
|
|
961
|
-
: prepareSchedulerSyncRuntime(input.installOpts, input.deps, []);
|
|
962
|
-
const runtimeOpts = preparedRuntime.options;
|
|
963
|
-
const removals = taskEntries.map((entry) => {
|
|
964
|
-
const invocation = entry.invocation;
|
|
965
|
-
const nativeId = entry.nativeId ?? schedulerNativeBindingId(entry.id);
|
|
966
|
-
const artifact = assertSchedulerNativeArtifactCardinality(nativeArtifacts, nativeId, 1);
|
|
967
|
-
if (!artifact?.fingerprint || artifact.bindingId !== entry.id) {
|
|
968
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact coherent fingerprint.`, "RESOURCE_ALREADY_EXISTS");
|
|
969
|
-
}
|
|
970
|
-
const logicalSource = primary.logicalSource;
|
|
971
|
-
const ordinal = invocation ? schedulerBindingOrdinal(entry.id, logicalSource, invocation) : undefined;
|
|
972
|
-
if (!invocation || ordinal === undefined) {
|
|
973
|
-
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.`);
|
|
974
|
-
return Object.freeze({
|
|
975
|
-
kind: "remove",
|
|
976
|
-
id: entry.id,
|
|
977
|
-
nativeId,
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
return Object.freeze({
|
|
981
|
-
kind: "remove",
|
|
982
|
-
id: entry.id,
|
|
983
|
-
nativeId,
|
|
984
|
-
expected: Object.freeze({
|
|
985
|
-
bindingId: entry.id,
|
|
986
|
-
nativeId,
|
|
987
|
-
logicalSource,
|
|
988
|
-
ordinal,
|
|
989
|
-
invocation: Object.freeze([...invocation]),
|
|
990
|
-
fingerprint: artifact.fingerprint,
|
|
991
|
-
}),
|
|
992
|
-
});
|
|
993
|
-
});
|
|
994
|
-
const installs = input.taskBindings.map((binding) => {
|
|
995
|
-
const nativeId = schedulerBindingNativeId(binding);
|
|
996
|
-
const resultFingerprint = input.sched.expectedSignature(binding, runtimeOpts);
|
|
997
|
-
if (!resultFingerprint) {
|
|
998
|
-
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot freeze the post-install fingerprint for ${JSON.stringify(binding.id)}.`, "INVALID_CONFIG_FILE");
|
|
999
|
-
}
|
|
1000
|
-
return Object.freeze({
|
|
1001
|
-
kind: "install",
|
|
1002
|
-
binding,
|
|
1003
|
-
expected: Object.freeze({
|
|
1004
|
-
state: "absent",
|
|
1005
|
-
bindingId: binding.id,
|
|
1006
|
-
nativeId,
|
|
1007
|
-
logicalSource: binding.logicalSource,
|
|
1008
|
-
ordinal: binding.ordinal,
|
|
1009
|
-
invocation: binding.invocation,
|
|
1010
|
-
}),
|
|
1011
|
-
resultFingerprint,
|
|
1012
|
-
...(runtimeOpts ? { options: runtimeOpts } : {}),
|
|
1013
|
-
});
|
|
1014
|
-
});
|
|
1015
|
-
const initialByKey = new Map();
|
|
1016
|
-
for (const removal of removals) {
|
|
1017
|
-
if (removal.kind !== "remove")
|
|
1018
|
-
continue;
|
|
1019
|
-
if (!removal.expected)
|
|
1020
|
-
continue;
|
|
1021
|
-
initialByKey.set(schedulerNativeArtifactKey(removal.nativeId), Object.freeze({ ...removal.expected, state: "present" }));
|
|
1022
|
-
}
|
|
1023
|
-
for (const install of installs) {
|
|
1024
|
-
if (install.kind === "remove")
|
|
1025
|
-
continue;
|
|
1026
|
-
const key = schedulerNativeArtifactKey(schedulerBindingNativeId(install.binding));
|
|
1027
|
-
if (!initialByKey.has(key))
|
|
1028
|
-
initialByKey.set(key, install.expected);
|
|
1029
|
-
}
|
|
1030
|
-
return Object.freeze({
|
|
1031
|
-
runtimeOpts,
|
|
1032
|
-
...(preparedRuntime.publish ? { publishRuntime: preparedRuntime.publish } : {}),
|
|
1033
|
-
operations: Object.freeze([...removals, ...installs]),
|
|
1034
|
-
initialExpectations: Object.freeze([...initialByKey.values()]),
|
|
1035
|
-
publishOperationIndex: removals.length,
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
function prepareSchedulerSyncRuntime(base, deps, warnings, installedBindings = []) {
|
|
1039
|
-
if (deps.backend && !deps.schedulerRuntime)
|
|
1040
|
-
return base ? { options: base } : {};
|
|
1041
|
-
if (deps.schedulerRuntime) {
|
|
1042
|
-
const runtime = deps.schedulerRuntime();
|
|
1043
|
-
warnIneligibleRebind(runtime, warnings, installedBindings);
|
|
1044
|
-
return { options: { ...base, binding: runtime.binding, contextPath: runtime.contextPath } };
|
|
1045
|
-
}
|
|
1046
|
-
const invocation = resolveAndValidateSchedulerInvocation();
|
|
1047
|
-
warnIneligibleRebind(invocation, warnings, installedBindings);
|
|
1048
|
-
const descriptor = schedulerContextDescriptor();
|
|
1049
|
-
const contextPath = schedulerContextPath(descriptor);
|
|
1050
|
-
return {
|
|
1051
|
-
options: { ...base, binding: invocation.binding, contextPath },
|
|
1052
|
-
publish: () => {
|
|
1053
|
-
const written = writeSchedulerContextDescriptor(descriptor);
|
|
1054
|
-
if (written !== contextPath) {
|
|
1055
|
-
throw new ConfigError("Scheduler context descriptor path changed after preflight.", "INVALID_CONFIG_FILE");
|
|
1056
|
-
}
|
|
1057
|
-
},
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
function resolveAndValidateSchedulerInvocation() {
|
|
1061
|
-
const invocation = resolveAkmInvocation();
|
|
1062
|
-
return { binding: invocation.argv, contextPath: "", eligible: invocation.eligible, kind: invocation.kind };
|
|
1063
|
-
}
|
|
1064
|
-
function warnIneligibleRebind(runtime, warnings, installedBindings) {
|
|
1065
|
-
if (runtime.eligible !== false || warnings.length > 0)
|
|
1066
|
-
return;
|
|
1067
|
-
// #868 residue: binding every currently-installed entry to the SAME
|
|
1068
|
-
// invocation it already carries changes nothing — this is the steady
|
|
1069
|
-
// state of an image-baked install re-running `task sync` on a timer.
|
|
1070
|
-
// Only warn when the bind actually moves an entry to a different
|
|
1071
|
-
// invocation.
|
|
1072
|
-
if (installedBindings.length > 0 && installedBindings.every((bound) => sameArgv(bound, runtime.binding)))
|
|
1073
|
-
return;
|
|
1074
|
-
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\`.`);
|
|
1075
|
-
}
|
|
1076
|
-
function groupInstalledBindings(entries, invocation) {
|
|
645
|
+
function groupInstalledBindings(entries) {
|
|
1077
646
|
const groups = new Map();
|
|
1078
647
|
for (const entry of entries) {
|
|
1079
648
|
const argv = [...entry.binding];
|
|
1080
|
-
const status = inspectInstalledBinding(entry
|
|
649
|
+
const status = inspectInstalledBinding(entry);
|
|
1081
650
|
const key = JSON.stringify([argv, entry.contextPath, status]);
|
|
1082
651
|
const existing = groups.get(key);
|
|
1083
652
|
if (existing) {
|
|
@@ -1093,23 +662,11 @@ function groupInstalledBindings(entries, invocation) {
|
|
|
1093
662
|
}
|
|
1094
663
|
return [...groups.values()].map((group) => ({ ...group, taskIds: group.taskIds.sort() }));
|
|
1095
664
|
}
|
|
1096
|
-
|
|
1097
|
-
function resolveInstalledOwnerPath(contextPath) {
|
|
1098
|
-
try {
|
|
1099
|
-
return validateSchedulerContextDescriptor(contextPath).environment.AKM_BUNDLE_DIR;
|
|
1100
|
-
}
|
|
1101
|
-
catch {
|
|
1102
|
-
return undefined;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
function inspectInstalledBinding(entry, invocation) {
|
|
665
|
+
function inspectInstalledBinding(entry) {
|
|
1106
666
|
const status = [];
|
|
1107
667
|
const binding = entry.binding;
|
|
1108
|
-
if (
|
|
1109
|
-
binding.some((part) => /(?:^|[\\/])src[\\/]cli\.ts$|(?:^|[\\/])dist[\\/](?:cli\.js|cli-node\.mjs)$/i.test(part) ||
|
|
1110
|
-
(path.isAbsolute(part) && hasGitAncestor(part)))) {
|
|
668
|
+
if (isCheckoutInvocation(binding))
|
|
1111
669
|
status.push("checkout");
|
|
1112
|
-
}
|
|
1113
670
|
if (binding.some((part) => part === "akm" || part === "bun" || part === "node"))
|
|
1114
671
|
status.push("path-selected");
|
|
1115
672
|
try {
|
|
@@ -1125,153 +682,14 @@ function inspectInstalledBinding(entry, invocation) {
|
|
|
1125
682
|
status.push("ok");
|
|
1126
683
|
return status;
|
|
1127
684
|
}
|
|
1128
|
-
function sameArgv(left, right) {
|
|
1129
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1130
|
-
}
|
|
1131
|
-
function hasGitAncestor(file) {
|
|
1132
|
-
let current;
|
|
1133
|
-
try {
|
|
1134
|
-
current = path.dirname(fs.realpathSync(file));
|
|
1135
|
-
}
|
|
1136
|
-
catch {
|
|
1137
|
-
return false;
|
|
1138
|
-
}
|
|
1139
|
-
for (;;) {
|
|
1140
|
-
if (fs.existsSync(path.join(current, ".git")))
|
|
1141
|
-
return true;
|
|
1142
|
-
const parent = path.dirname(current);
|
|
1143
|
-
if (parent === current)
|
|
1144
|
-
return false;
|
|
1145
|
-
current = parent;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
685
|
function taskAssetRef(id) {
|
|
1149
686
|
return { type: "task", name: id };
|
|
1150
687
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const root = path.resolve(rootInput);
|
|
1154
|
-
const lexicalRelative = path.relative(root, filePath);
|
|
1155
|
-
if (lexicalRelative === "" || lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
1156
|
-
throw new UsageError(`${filePathInput} resolves outside the task source root.`, "PATH_ESCAPE_VIOLATION");
|
|
1157
|
-
}
|
|
1158
|
-
const rootRealPath = fs.realpathSync(root);
|
|
1159
|
-
const rootStat = fs.statSync(rootRealPath, { bigint: true });
|
|
1160
|
-
if (!rootStat.isDirectory()) {
|
|
1161
|
-
throw new UsageError(`${root} is not a task source directory.`, "INVALID_FLAG_VALUE");
|
|
1162
|
-
}
|
|
1163
|
-
const common = { filePath, rootRealPath };
|
|
1164
|
-
let descriptor;
|
|
1165
|
-
try {
|
|
1166
|
-
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
1167
|
-
let before = fs.fstatSync(descriptor, { bigint: true });
|
|
1168
|
-
if (!before.isFile()) {
|
|
1169
|
-
throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
|
|
1170
|
-
}
|
|
1171
|
-
let bytes = fs.readFileSync(descriptor);
|
|
1172
|
-
const torn = !sameTaskSourceStat(before, fs.fstatSync(descriptor, { bigint: true })) ||
|
|
1173
|
-
BigInt(bytes.byteLength) !== before.size;
|
|
1174
|
-
if (torn) {
|
|
1175
|
-
fs.closeSync(descriptor);
|
|
1176
|
-
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
1177
|
-
before = fs.fstatSync(descriptor, { bigint: true });
|
|
1178
|
-
bytes = fs.readFileSync(descriptor);
|
|
1179
|
-
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).`);
|
|
1180
|
-
}
|
|
1181
|
-
const realPath = fs.realpathSync(filePath);
|
|
1182
|
-
const physicalRelative = path.relative(rootRealPath, realPath);
|
|
1183
|
-
if (physicalRelative === "" || physicalRelative.startsWith("..") || path.isAbsolute(physicalRelative)) {
|
|
1184
|
-
throw new UsageError(`${filePath} resolves outside the task source root.`, "PATH_ESCAPE_VIOLATION");
|
|
1185
|
-
}
|
|
1186
|
-
let content;
|
|
1187
|
-
try {
|
|
1188
|
-
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
1189
|
-
}
|
|
1190
|
-
catch {
|
|
1191
|
-
throw new UsageError(`${filePath} contains invalid UTF-8 bytes.`, "INVALID_FLAG_VALUE");
|
|
1192
|
-
}
|
|
1193
|
-
return Object.freeze({
|
|
1194
|
-
state: "present",
|
|
1195
|
-
...common,
|
|
1196
|
-
realPath,
|
|
1197
|
-
size: bytes.byteLength,
|
|
1198
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1199
|
-
bytesBase64: bytes.toString("base64"),
|
|
1200
|
-
content,
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
catch (cause) {
|
|
1204
|
-
if (cause.code === "ENOENT") {
|
|
1205
|
-
return Object.freeze({ state: "absent", ...common });
|
|
1206
|
-
}
|
|
1207
|
-
if (cause instanceof UsageError)
|
|
1208
|
-
throw cause;
|
|
1209
|
-
throw new UsageError(`${filePath} could not be guarded as a contained regular task source: ${errorMessage(cause)}`, "PATH_ESCAPE_VIOLATION");
|
|
1210
|
-
}
|
|
1211
|
-
finally {
|
|
1212
|
-
if (descriptor !== undefined)
|
|
1213
|
-
fs.closeSync(descriptor);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
// TOCTOU note: this compares CONTENT (state + sha256), not filesystem
|
|
1217
|
-
// identity (inode/mtime/ctime/directory timestamps) — the same split already
|
|
1218
|
-
// applied to the task migrator in 0.9.5. An unrelated touch to the file or
|
|
1219
|
-
// its containing directory must not trip a "changed after planning" refusal;
|
|
1220
|
-
// only a real content change should.
|
|
1221
|
-
function assertTaskSourceExpectation(expected) {
|
|
1222
|
-
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
1223
|
-
if (!sameTaskSourceExpectation(actual, expected)) {
|
|
1224
|
-
throw new UsageError(`Task source ${JSON.stringify(expected.filePath)} changed after transaction planning.`, "RESOURCE_ALREADY_EXISTS");
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
function sameTaskSourceExpectation(left, right) {
|
|
1228
|
-
if (left.state !== right.state || left.filePath !== right.filePath || left.rootRealPath !== right.rootRealPath) {
|
|
1229
|
-
return false;
|
|
1230
|
-
}
|
|
1231
|
-
if (left.state === "absent")
|
|
1232
|
-
return true;
|
|
1233
|
-
return left.sha256 === right.sha256;
|
|
1234
|
-
}
|
|
1235
|
-
function assertTaskSourceRestored(expected) {
|
|
1236
|
-
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
1237
|
-
const restored = actual.state === expected.state &&
|
|
1238
|
-
(actual.state === "absent" ||
|
|
1239
|
-
(expected.state === "present" && actual.sha256 === expected.sha256 && actual.content === expected.content));
|
|
1240
|
-
if (!restored) {
|
|
1241
|
-
throw new UsageError(`Task source ${JSON.stringify(expected.filePath)} could not be restored without replacing a concurrent owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
class TaskSourceRestoredBoundaryError extends Error {
|
|
1245
|
-
cause;
|
|
1246
|
-
constructor(cause) {
|
|
1247
|
-
super(errorMessage(cause));
|
|
1248
|
-
this.name = "TaskSourceRestoredBoundaryError";
|
|
1249
|
-
this.cause = cause;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
function sameTaskSourceStat(left, right) {
|
|
1253
|
-
return (left.dev === right.dev &&
|
|
1254
|
-
left.ino === right.ino &&
|
|
1255
|
-
left.mode === right.mode &&
|
|
1256
|
-
left.size === right.size &&
|
|
1257
|
-
left.mtimeNs === right.mtimeNs &&
|
|
1258
|
-
left.ctimeNs === right.ctimeNs);
|
|
1259
|
-
}
|
|
1260
|
-
function hashTaskSource(source) {
|
|
1261
|
-
return createHash("sha256").update(Buffer.from(source, "utf8")).digest("hex");
|
|
1262
|
-
}
|
|
1263
|
-
/**
|
|
1264
|
-
* Resolve the bundle a mutating/run task command targets. Returns the resolved
|
|
1265
|
-
* write/read target, its stash path, and the `--bundle <bundle>` token to embed
|
|
1266
|
-
* in scheduled invocations. The primary bundle uses the target-less form.
|
|
1267
|
-
*/
|
|
1268
|
-
function resolveTaskBundle(target, opts) {
|
|
688
|
+
/** The bundle `task add` writes into: its write target, config, stash path, and name. */
|
|
689
|
+
function resolveTaskBundle(target) {
|
|
1269
690
|
const config = loadConfig();
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
const stashDir = resolved.source.path;
|
|
1273
|
-
const installTarget = isPrimaryStashPath(stashDir) ? undefined : (resolved.selector ?? resolved.source.name);
|
|
1274
|
-
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 };
|
|
1275
693
|
}
|
|
1276
694
|
function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
|
|
1277
695
|
return async ({ bundle, type, name }) => {
|
|
@@ -1345,31 +763,9 @@ function isPrimaryStashPath(candidate) {
|
|
|
1345
763
|
}
|
|
1346
764
|
return path.resolve(candidate) === primary;
|
|
1347
765
|
}
|
|
1348
|
-
/** Two bundle attributions match when both are the primary (undefined) or equal names. */
|
|
1349
|
-
function sameBundle(a, b) {
|
|
1350
|
-
return (a ?? undefined) === (b ?? undefined);
|
|
1351
|
-
}
|
|
1352
|
-
function installedEntryRunsTask(entry, id) {
|
|
1353
|
-
const invocation = entry.invocation;
|
|
1354
|
-
return invocation?.[0] === "task" && invocation[1] === "run" && invocation[2] === id;
|
|
1355
|
-
}
|
|
1356
|
-
function foreignScheduleMessage(id, existingTarget) {
|
|
1357
|
-
const where = existingTarget === undefined ? "the default bundle" : `bundle "${existingTarget}"`;
|
|
1358
|
-
return `Task id "${id}" is already scheduled from ${where}; rename the task or disable the existing one first.`;
|
|
1359
|
-
}
|
|
1360
766
|
function errorMessage(cause) {
|
|
1361
767
|
return cause instanceof Error ? cause.message : String(cause);
|
|
1362
768
|
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Refuse to schedule an id already installed from a DIFFERENT bundle. Scheduler
|
|
1365
|
-
* ids are the bare task id (never namespaced), so a single id can be active from
|
|
1366
|
-
* only one bundle at a time — a collision is a hard error, not an auto-rename.
|
|
1367
|
-
*/
|
|
1368
|
-
function assertNoForeignSchedule(entries, id, installTarget) {
|
|
1369
|
-
const foreign = entries.find((entry) => entry.id === id && !sameBundle(entry.target, installTarget));
|
|
1370
|
-
if (foreign)
|
|
1371
|
-
throw new UsageError(foreignScheduleMessage(id, foreign.target), "RESOURCE_ALREADY_EXISTS");
|
|
1372
|
-
}
|
|
1373
769
|
/**
|
|
1374
770
|
* Infer a JSON-Schema-subset `type` keyword from one `--params` value's
|
|
1375
771
|
* runtime shape (spec docs/plans/specs/p4-deletions-closeout.md §3.2.6, row
|