akm-cli 0.9.1 → 0.9.2-alpha.2
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 +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -8,48 +8,38 @@
|
|
|
8
8
|
* `src/cli.ts` wraps these in citty `defineCommand`s and shapes their return
|
|
9
9
|
* values via `output()`.
|
|
10
10
|
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
import fs from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import { stringify as yamlStringify } from "yaml";
|
|
15
|
+
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
14
16
|
import { assetPathForName } from "../../core/asset/asset-placement.js";
|
|
15
|
-
import {
|
|
17
|
+
import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
18
|
+
import { conceptIdFromTypeName, isFullRefInput } from "../../core/asset/resolve-ref.js";
|
|
16
19
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
17
20
|
import { loadConfig } from "../../core/config/config.js";
|
|
21
|
+
import { resolveConfiguredSources } from "../../core/config/config-sources.js";
|
|
18
22
|
import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../core/config/experimental.js";
|
|
19
23
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
20
24
|
import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
|
|
21
|
-
import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
25
|
+
import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
22
26
|
import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
|
|
27
|
+
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
23
28
|
import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
|
|
24
|
-
import { parseTaskDocument } from "../../tasks/parser.js";
|
|
25
29
|
import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
|
|
26
|
-
import { exitCodeForStatus,
|
|
30
|
+
import { exitCodeForStatus, readTaskHistory, runTask } from "../../tasks/runner.js";
|
|
31
|
+
import { prepareTaskV3Execution } from "../../tasks/runtime-v3.js";
|
|
27
32
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
33
|
+
import { assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "../../tasks/scheduler-binding.js";
|
|
34
|
+
import { schedulerContextDescriptor, schedulerContextPath, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
|
|
35
|
+
import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
|
|
36
|
+
import { parseTaskV3Yaml, TASK_V3_MAX_SOURCE_BYTES } from "../../tasks/source-v3.js";
|
|
37
|
+
import { normaliseTaskConceptId, normaliseTaskId } from "../../tasks/task-id.js";
|
|
31
38
|
import { applyAutonomyGate, configuredDirectAutonomyLanes, describeGatedLanes } from "../improve/autonomy-gate.js";
|
|
32
39
|
import { resolveImproveStrategy } from "../improve/improve-strategies.js";
|
|
33
40
|
export async function akmTasksAdd(input, deps = {}) {
|
|
34
41
|
const id = normaliseTaskId(input.id);
|
|
35
|
-
|
|
36
|
-
input.command !== null &&
|
|
37
|
-
!(typeof input.command === "string" && input.command.trim() === "") &&
|
|
38
|
-
!(Array.isArray(input.command) && input.command.length === 0);
|
|
39
|
-
const targetCount = [Boolean(input.workflow), Boolean(input.prompt), hasCommand].filter(Boolean).length;
|
|
40
|
-
if (targetCount !== 1) {
|
|
41
|
-
throw new UsageError("Pass exactly one of --workflow <ref>, --prompt <asset-ref|./file.md|text>, or --command <shell-command>.", "INVALID_FLAG_VALUE");
|
|
42
|
-
}
|
|
43
|
-
// `--timeout-ms` IS valid on a workflow task: it is the whole-run bound the
|
|
44
|
-
// task runner turns into an abort signal (issue 11), the same one
|
|
45
|
-
// `akm workflow run --timeout` applies interactively. Engine and model stay
|
|
46
|
-
// prompt-only — a workflow's engines come from its frozen plan.
|
|
47
|
-
if (input.workflow && (input.engine !== undefined || input.model !== undefined)) {
|
|
48
|
-
throw new UsageError("Workflow tasks accept --params and --timeout-ms; engine and model are prompt-task fields.", "INVALID_FLAG_VALUE");
|
|
49
|
-
}
|
|
50
|
-
if (hasCommand && (input.engine !== undefined || input.model !== undefined)) {
|
|
51
|
-
throw new UsageError("Command tasks accept --timeout-ms but not --engine or --model.", "INVALID_FLAG_VALUE");
|
|
52
|
-
}
|
|
42
|
+
assertTaskAddTargetShape(input);
|
|
53
43
|
// Validate the schedule for the active backend before writing anything.
|
|
54
44
|
// WI-9.10e: the injected backend (tests) carries its own name, so derive it
|
|
55
45
|
// from `deps.backend` when present — retiring the `_setBackendsForTests` seam.
|
|
@@ -64,14 +54,13 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
64
54
|
if (!isWithin(assetPath, typeRoot)) {
|
|
65
55
|
throw new UsageError(`Resolved task path escapes the stash: "${id}".`, "PATH_ESCAPE_VIOLATION");
|
|
66
56
|
}
|
|
67
|
-
|
|
68
|
-
// (see the tasks dir rule in src/indexer/walk/matchers.ts). A leftover
|
|
69
|
-
// `<id>.md` still names the same task, so creating `<id>.yml` beside it
|
|
70
|
-
// must collide loudly rather than silently minting a duplicate.
|
|
71
|
-
const legacyAssetPath = path.join(typeRoot, `${id}.md`);
|
|
72
|
-
if ((fs.existsSync(assetPath) || fs.existsSync(legacyAssetPath)) && !input.force) {
|
|
57
|
+
if (fs.existsSync(assetPath) && !input.force) {
|
|
73
58
|
throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or delete its file and run \`akm task sync\` first.`, "RESOURCE_ALREADY_EXISTS");
|
|
74
59
|
}
|
|
60
|
+
const sourceExpectation = captureTaskSourceExpectation(assetPath, stashDir);
|
|
61
|
+
if (sourceExpectation.state === "present" && !input.force) {
|
|
62
|
+
throw new UsageError(`Task "${id}" appeared while add was preparing. Pass --force only after reviewing the current owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
63
|
+
}
|
|
75
64
|
const yaml = renderTaskYaml({
|
|
76
65
|
id,
|
|
77
66
|
schedule: input.schedule,
|
|
@@ -88,152 +77,165 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
88
77
|
tags: input.tags,
|
|
89
78
|
enabled: input.disabled !== true,
|
|
90
79
|
});
|
|
91
|
-
const task =
|
|
92
|
-
|
|
80
|
+
const task = parseTaskV3Yaml({ yaml, filePath: assetPath, workspaceRoot: stashDir });
|
|
81
|
+
const qualifiedRef = makeBundleRef(bundle.bundleName, `tasks/${id}`);
|
|
82
|
+
await prepareTaskV3Execution(task, {
|
|
83
|
+
taskId: id,
|
|
84
|
+
taskRef: qualifiedRef,
|
|
85
|
+
bundleName: bundle.bundleName,
|
|
86
|
+
bundleRoot: stashDir,
|
|
87
|
+
config: bundle.config,
|
|
88
|
+
resolveAsset: taskProjectionAssetResolver(bundle.config, bundle.bundleName, stashDir),
|
|
89
|
+
});
|
|
90
|
+
const taskBindings = compileTaskSchedulerBindings({
|
|
91
|
+
id,
|
|
92
|
+
qualifiedRef,
|
|
93
|
+
...(bundle.installTarget ? { bundleTarget: bundle.installTarget } : {}),
|
|
94
|
+
enabled: task.akm?.enabled !== false,
|
|
95
|
+
schedules: task.triggers.schedules,
|
|
96
|
+
});
|
|
97
|
+
const taskBinding = taskBindings[0];
|
|
98
|
+
if (!taskBinding)
|
|
99
|
+
throw new UsageError(`Task "${id}" has no schedulable trigger.`, "INVALID_FLAG_VALUE");
|
|
93
100
|
const ref = taskAssetRef(id);
|
|
94
|
-
const previousYaml = fs.existsSync(assetPath) ? fs.readFileSync(assetPath, "utf8") : undefined;
|
|
95
|
-
let previousTask;
|
|
96
|
-
let previousTaskError;
|
|
97
|
-
if (previousYaml !== undefined) {
|
|
98
|
-
try {
|
|
99
|
-
previousTask = parseTaskDocument({ yaml: previousYaml, filePath: assetPath, id });
|
|
100
|
-
}
|
|
101
|
-
catch (err) {
|
|
102
|
-
previousTaskError = err;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
101
|
const sched = deps.backend ?? selectBackend();
|
|
106
102
|
const writeAsset = deps.writeAsset ?? writeAssetToSource;
|
|
107
103
|
const deleteAsset = deps.deleteAsset ?? deleteAssetFromSource;
|
|
108
104
|
const commitBoundary = deps.commitBoundary ?? commitWriteTargetBoundary;
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
const transaction = await prepareTaskAddSchedulerTransaction({
|
|
106
|
+
id,
|
|
107
|
+
installTarget: bundle.installTarget,
|
|
108
|
+
ownerTarget: bundle.bundleName,
|
|
109
|
+
installOpts,
|
|
110
|
+
taskBindings,
|
|
111
|
+
sched,
|
|
112
|
+
deps,
|
|
113
|
+
rebind: input.rebind === true,
|
|
114
|
+
});
|
|
115
|
+
let sourceMutationReceipt;
|
|
116
|
+
let sourcePublished = false;
|
|
117
|
+
let sourcePublicationAttempted = false;
|
|
118
|
+
const publishSource = async () => {
|
|
119
|
+
assertTaskSourceExpectation(sourceExpectation);
|
|
120
|
+
sourcePublicationAttempted = true;
|
|
118
121
|
await writeAsset(writeTarget.source, writeTarget.config, ref, yaml);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
await restoreTaskSourceBytes(writeAsset, writeTarget.source, writeTarget.config, ref, assetPath, previousYaml);
|
|
136
|
-
sourceRestored = true;
|
|
122
|
+
const publishedSource = captureTaskSourceExpectation(assetPath, stashDir);
|
|
123
|
+
if (publishedSource.state !== "present" || publishedSource.sha256 !== hashTaskSource(yaml)) {
|
|
124
|
+
throw new UsageError(`Task source ${JSON.stringify(assetPath)} changed during publication.`, "RESOURCE_ALREADY_EXISTS");
|
|
125
|
+
}
|
|
126
|
+
sourceMutationReceipt = publishedSource;
|
|
127
|
+
sourcePublished = true;
|
|
128
|
+
transaction.publishRuntime?.();
|
|
129
|
+
};
|
|
130
|
+
await applySchedulerTransaction(sched, transaction.operations, {
|
|
131
|
+
initialExpectations: transaction.initialExpectations,
|
|
132
|
+
assertReadSet: () => {
|
|
133
|
+
if (sourcePublished) {
|
|
134
|
+
if (!sourceMutationReceipt) {
|
|
135
|
+
throw new ConfigError("Published task source lost its transaction receipt.", "INVALID_CONFIG_FILE");
|
|
137
136
|
}
|
|
137
|
+
assertTaskSourceExpectation(sourceMutationReceipt);
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (installSucceeded && !wasInstalled) {
|
|
144
|
-
try {
|
|
145
|
-
await sched.uninstall(id);
|
|
139
|
+
else {
|
|
140
|
+
assertTaskSourceExpectation(sourceExpectation);
|
|
146
141
|
}
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
},
|
|
143
|
+
beforeOperation: async (_operation, index) => {
|
|
144
|
+
if (index === transaction.publishOperationIndex && !sourcePublished)
|
|
145
|
+
await publishSource();
|
|
146
|
+
},
|
|
147
|
+
afterOperations: () => commitBoundary(writeTarget, `Update tasks/${id}`),
|
|
148
|
+
rollbackExternal: async () => {
|
|
149
|
+
if (!sourcePublicationAttempted)
|
|
150
|
+
return;
|
|
151
|
+
if (!sourceMutationReceipt) {
|
|
152
|
+
const current = captureTaskSourceExpectation(assetPath, stashDir);
|
|
153
|
+
if (sameTaskSourceExpectation(current, sourceExpectation))
|
|
154
|
+
return;
|
|
155
|
+
if (current.state !== "present" || current.sha256 !== hashTaskSource(yaml)) {
|
|
156
|
+
throw new UsageError(`Task source ${JSON.stringify(assetPath)} has an unowned publication state; refusing rollback over a possible concurrent owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
157
|
+
}
|
|
158
|
+
sourceMutationReceipt = current;
|
|
149
159
|
}
|
|
150
|
-
|
|
151
|
-
else if (installSucceeded && previousTask) {
|
|
160
|
+
assertTaskSourceExpectation(sourceMutationReceipt);
|
|
152
161
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
catch (rollbackError) {
|
|
156
|
-
rollbackErrors.push(rollbackError);
|
|
157
|
-
try {
|
|
158
|
-
if (typeof sched.setEnabled !== "function") {
|
|
159
|
-
throw new Error(`Scheduler backend "${sched.name}" cannot disable task "${id}".`);
|
|
160
|
-
}
|
|
161
|
-
await sched.setEnabled(id, false);
|
|
162
|
+
if (sourceExpectation.state === "absent") {
|
|
163
|
+
await deleteAsset(writeTarget.source, writeTarget.config, ref);
|
|
162
164
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
else {
|
|
166
|
+
await writeAsset(writeTarget.source, writeTarget.config, ref, sourceExpectation.content);
|
|
167
|
+
const providerRestored = captureTaskSourceExpectation(assetPath, stashDir);
|
|
168
|
+
if (providerRestored.state !== "present" || providerRestored.sha256 !== sourceExpectation.sha256) {
|
|
169
|
+
// Provider writers conventionally normalize a trailing newline.
|
|
170
|
+
// Rollback is byte-exact, so finish the already-owned restore with
|
|
171
|
+
// the frozen bytes before checking the physical source state.
|
|
172
|
+
fs.writeFileSync(assetPath, Buffer.from(sourceExpectation.bytesBase64, "base64"));
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
else if (installSucceeded && wasInstalled) {
|
|
175
|
-
rollbackErrors.push(previousTaskError ?? new Error(`Prior task "${id}" could not be restored.`));
|
|
176
|
-
}
|
|
177
|
-
if (sourceRestored) {
|
|
178
|
-
try {
|
|
175
|
+
assertTaskSourceRestored(sourceExpectation);
|
|
179
176
|
commitBoundary(writeTarget, `Restore tasks/${id}`);
|
|
180
177
|
}
|
|
181
|
-
catch (
|
|
182
|
-
|
|
178
|
+
catch (cause) {
|
|
179
|
+
// A write/commit seam may report failure after it has already restored
|
|
180
|
+
// the exact source bytes. Prove that state before allowing native
|
|
181
|
+
// rollback; otherwise preserve the possible concurrent source owner.
|
|
182
|
+
try {
|
|
183
|
+
assertTaskSourceRestored(sourceExpectation);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
throw cause;
|
|
187
|
+
}
|
|
188
|
+
throw new TaskSourceRestoredBoundaryError(cause);
|
|
183
189
|
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
throw err;
|
|
190
|
-
}
|
|
190
|
+
},
|
|
191
|
+
suppressNativeRollbackWhenExternalFails: true,
|
|
192
|
+
allowNativeRollbackAfterExternalFailure: (error) => error instanceof TaskSourceRestoredBoundaryError,
|
|
193
|
+
});
|
|
191
194
|
return {
|
|
192
195
|
id,
|
|
193
196
|
ref: conceptIdFromTypeName("task", id),
|
|
194
197
|
path: assetPath,
|
|
195
198
|
bundleDir: stashDir,
|
|
196
|
-
schedule:
|
|
197
|
-
enabled:
|
|
199
|
+
schedule: taskBinding.cron,
|
|
200
|
+
enabled: taskBinding.enabled,
|
|
198
201
|
backend,
|
|
199
202
|
target: task.target,
|
|
200
203
|
};
|
|
201
204
|
}
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
taskId: INVALID_TASK_ATTEMPT_ID,
|
|
211
|
-
reason: "invalid_task_id",
|
|
212
|
-
failure,
|
|
213
|
-
startedAt,
|
|
214
|
-
});
|
|
215
|
-
throw failure;
|
|
205
|
+
function assertTaskAddTargetShape(input) {
|
|
206
|
+
const hasCommand = input.command !== undefined &&
|
|
207
|
+
input.command !== null &&
|
|
208
|
+
!(typeof input.command === "string" && input.command.trim() === "") &&
|
|
209
|
+
!(Array.isArray(input.command) && input.command.length === 0);
|
|
210
|
+
const targetCount = [Boolean(input.workflow), Boolean(input.prompt), hasCommand].filter(Boolean).length;
|
|
211
|
+
if (targetCount !== 1) {
|
|
212
|
+
throw new UsageError("Pass exactly one of --workflow <ref>, --prompt <inline-text>, or --command <shell-command>.", "INVALID_FLAG_VALUE");
|
|
216
213
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
? resolveWriteTarget(loadConfig(), options.target, { requireWritable: false }).source.path
|
|
225
|
-
: resolveStashDir();
|
|
226
|
-
}
|
|
227
|
-
catch (failure) {
|
|
228
|
-
recordTaskAttemptFailure({
|
|
229
|
-
taskId: normalised,
|
|
230
|
-
reason: "task_load_failed",
|
|
231
|
-
failure,
|
|
232
|
-
startedAt,
|
|
233
|
-
});
|
|
234
|
-
throw failure;
|
|
214
|
+
// `--timeout-ms` is the workflow's whole-run bound. Engine and model stay
|
|
215
|
+
// prompt-only because workflow engines come from the frozen plan.
|
|
216
|
+
if (input.workflow && (input.engine !== undefined || input.model !== undefined)) {
|
|
217
|
+
throw new UsageError("Workflow tasks accept --params and --timeout-ms; engine and model are prompt-task fields.", "INVALID_FLAG_VALUE");
|
|
218
|
+
}
|
|
219
|
+
if (hasCommand && (input.engine !== undefined || input.model !== undefined)) {
|
|
220
|
+
throw new UsageError("Command tasks accept --timeout-ms but not --engine or --model.", "INVALID_FLAG_VALUE");
|
|
235
221
|
}
|
|
236
|
-
|
|
222
|
+
if (input.prompt !== undefined)
|
|
223
|
+
assertInlineTaskPrompt(input.prompt);
|
|
224
|
+
}
|
|
225
|
+
export async function akmTasksRun(id, options = {}) {
|
|
226
|
+
const parsed = parseTaskRef(id);
|
|
227
|
+
const bundle = resolveTaskReadBundle(parsed.bundle, options.target);
|
|
228
|
+
const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
|
|
229
|
+
const resolvedId = taskIdForAdapter(parsed.id, adapterId);
|
|
230
|
+
const runOptions = {
|
|
231
|
+
stashDir: bundle.source.path,
|
|
232
|
+
bundleName: bundle.source.name,
|
|
233
|
+
adapterId,
|
|
234
|
+
scheduled: options.scheduled === true,
|
|
235
|
+
};
|
|
236
|
+
// The runner owns the prepare-before-reserve boundary. Invalid source,
|
|
237
|
+
// projectability, and resolver failures therefore create no history row.
|
|
238
|
+
const result = await runTask(resolvedId, runOptions);
|
|
237
239
|
const exitCode = result.status === "failed" && result.target.kind === "command" && result.detail?.exitCode === 78
|
|
238
240
|
? 78
|
|
239
241
|
: exitCodeForStatus(result.status);
|
|
@@ -245,14 +247,18 @@ export async function akmTasksRun(id, options = {}) {
|
|
|
245
247
|
}
|
|
246
248
|
export async function akmTasksHistory(input) {
|
|
247
249
|
const limit = input.limit !== undefined && input.limit > 0 ? input.limit : 50;
|
|
248
|
-
const
|
|
250
|
+
const parsed = input.id ? parseTaskRef(input.id) : undefined;
|
|
251
|
+
const bundle = resolveTaskReadBundle(parsed?.bundle, input.target);
|
|
252
|
+
const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
|
|
253
|
+
const id = parsed ? taskIdForAdapter(parsed.id, adapterId) : undefined;
|
|
249
254
|
// History rows are keyed by task id in state.db, not per bundle.
|
|
250
255
|
return { rows: readTaskHistory({ id, limit }) };
|
|
251
256
|
}
|
|
252
257
|
/**
|
|
253
258
|
* Reconcile the on-disk task files of ONE bundle with the OS scheduler.
|
|
254
|
-
* •
|
|
255
|
-
*
|
|
259
|
+
* • compile and runtime-project the complete desired task/workflow set;
|
|
260
|
+
* one invalid source rejects the sync before descriptor/backend mutation
|
|
261
|
+
* • install missing bindings only after that whole-set preflight succeeds
|
|
256
262
|
* • reinstall tasks whose schedule or enabled state changed in the .yml
|
|
257
263
|
* (drift detected by comparing the backend's installed signature against
|
|
258
264
|
* the signature the current definition would produce)
|
|
@@ -268,101 +274,65 @@ export async function akmTasksHistory(input) {
|
|
|
268
274
|
* (or `sync --bundle` on a bundle whose task files are already present).
|
|
269
275
|
*/
|
|
270
276
|
export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
271
|
-
const
|
|
272
|
-
|
|
277
|
+
const resolved = resolveTaskReadBundle(undefined, bundleTarget);
|
|
278
|
+
const config = loadConfig();
|
|
279
|
+
const stashDir = resolved.source.path;
|
|
273
280
|
const syncTarget = bundleTarget !== undefined && !isPrimaryStashPath(stashDir) ? bundleTarget : undefined;
|
|
274
|
-
const typeRoot = path.join(stashDir, "tasks");
|
|
275
|
-
const fileIds = fs.existsSync(typeRoot)
|
|
276
|
-
? fs
|
|
277
|
-
.readdirSync(typeRoot)
|
|
278
|
-
.filter((f) => f.endsWith(".yml"))
|
|
279
|
-
.map((f) => f.slice(0, -4))
|
|
280
|
-
: [];
|
|
281
281
|
const sched = deps.backend ?? selectBackend();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
282
|
+
if (!sched.inspectBindings) {
|
|
283
|
+
throw new ConfigError(`Scheduler backend "${sched.name}" cannot provide one coherent inspection for transactional sync.`, "INVALID_CONFIG_FILE");
|
|
284
|
+
}
|
|
285
|
+
const inspection = await sched.inspectBindings({ rebind: options.rebind === true });
|
|
286
|
+
const rawEntries = [...inspection.installed];
|
|
287
|
+
const allEntries = rawEntries.map((entry) => ({
|
|
288
|
+
...entry,
|
|
289
|
+
...(entry.nativeId !== undefined ? { nativeId: entry.nativeId } : {}),
|
|
290
|
+
...(entry.invocation !== undefined ? { invocation: Object.freeze([...entry.invocation]) } : {}),
|
|
291
|
+
binding: "binding" in entry ? [...entry.binding] : [],
|
|
292
|
+
contextPath: "contextPath" in entry ? entry.contextPath : "",
|
|
293
|
+
}));
|
|
294
|
+
const nativeArtifacts = inspection.artifacts;
|
|
295
|
+
const common = {
|
|
296
|
+
sourceRoot: stashDir,
|
|
297
|
+
adapterId: resolved.source.adapterId ?? detectAdapterId(stashDir),
|
|
298
|
+
bundleName: resolved.source.name,
|
|
299
|
+
...(syncTarget ? { bundleTarget: syncTarget } : {}),
|
|
300
|
+
backend: sched.name,
|
|
301
|
+
installed: allEntries,
|
|
302
|
+
nativeArtifacts,
|
|
303
|
+
inspection: Object.freeze({ installed: allEntries, artifacts: nativeArtifacts }),
|
|
304
|
+
rebind: options.rebind === true,
|
|
305
|
+
config,
|
|
306
|
+
resolveAsset: taskProjectionAssetResolver(config, resolved.source.name, stashDir),
|
|
307
|
+
};
|
|
308
|
+
// Pass one validates every desired source, ownership domain, schedule, and
|
|
309
|
+
// installed-id collision before runtime descriptor preparation is possible.
|
|
310
|
+
const preparedSources = await prepareSchedulerSyncSourceSet(common);
|
|
311
|
+
const preflight = finalizeSchedulerSyncPlan(common, preparedSources);
|
|
293
312
|
const warnings = [];
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (foreign)
|
|
306
|
-
throw new UsageError(foreignScheduleMessage(id, foreign.target), "RESOURCE_ALREADY_EXISTS");
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
309
|
-
skipped.push({ id, reason: err instanceof Error ? err.message : String(err) });
|
|
310
|
-
if (present.has(id)) {
|
|
311
|
-
try {
|
|
312
|
-
await sched.setEnabled(id, false);
|
|
313
|
-
}
|
|
314
|
-
catch (disableError) {
|
|
315
|
-
try {
|
|
316
|
-
await sched.uninstall(id);
|
|
317
|
-
}
|
|
318
|
-
catch (uninstallError) {
|
|
319
|
-
throw new AggregateError([err, disableError, uninstallError], `Task "${id}" is invalid and its installed scheduler entry could not be disabled or removed.`);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
if (!present.has(id)) {
|
|
326
|
-
try {
|
|
327
|
-
const runtimeOpts = schedulerInstallOptions(installOpts, undefined, deps, options.rebind === true, `create scheduler entry for task "${id}"`, warnings);
|
|
328
|
-
await sched.install(task, runtimeOpts);
|
|
329
|
-
installed.push(id);
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
skipped.push({ id, reason: error instanceof Error ? error.message : String(error) });
|
|
313
|
+
const expectedSignature = sched.expectedSignature?.bind(sched);
|
|
314
|
+
const needsRuntime = preflight.operations.some((operation) => operation.kind !== "remove" && operation.options?.binding === undefined);
|
|
315
|
+
const prepared = needsRuntime
|
|
316
|
+
? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps, options.rebind === true, "reconcile native scheduler bindings", warnings)
|
|
317
|
+
: undefined;
|
|
318
|
+
const plan = finalizeSchedulerSyncPlan({
|
|
319
|
+
...common,
|
|
320
|
+
...(prepared?.options ? { installOptions: prepared.options } : {}),
|
|
321
|
+
...(expectedSignature
|
|
322
|
+
? {
|
|
323
|
+
expectedSignature: (binding, install) => expectedSignature(binding, install),
|
|
333
324
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// didn't record one), reinstall unconditionally — install() is idempotent,
|
|
340
|
-
// so the cost is one crontab write and correctness is guaranteed.
|
|
341
|
-
const installedEntry = present.get(id);
|
|
342
|
-
const runtimeOpts = schedulerInstallOptions(installOpts, options.rebind ? undefined : installedEntry, deps, options.rebind === true, `rebind scheduler entry for task "${id}"`, warnings);
|
|
343
|
-
const installedSig = installedEntry.signature;
|
|
344
|
-
const expectedSig = sched.expectedSignature?.(task, runtimeOpts);
|
|
345
|
-
if (installedSig !== undefined && expectedSig !== undefined && installedSig === expectedSig) {
|
|
346
|
-
unchanged.push(id);
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
await sched.install(task, runtimeOpts);
|
|
350
|
-
updated.push(id);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
const removed = [];
|
|
354
|
-
for (const installedId of present.keys()) {
|
|
355
|
-
if (!fileIds.includes(installedId)) {
|
|
356
|
-
await sched.uninstall(installedId);
|
|
357
|
-
removed.push(installedId);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
325
|
+
: {}),
|
|
326
|
+
}, preparedSources);
|
|
327
|
+
await applySchedulerSyncPlan(sched, plan, prepared?.publish && plan.operations.some((operation) => operation.kind !== "remove")
|
|
328
|
+
? prepared.publish
|
|
329
|
+
: undefined);
|
|
360
330
|
return {
|
|
361
|
-
installed,
|
|
362
|
-
updated,
|
|
363
|
-
removed,
|
|
364
|
-
unchanged,
|
|
365
|
-
skipped,
|
|
331
|
+
installed: [...plan.installed],
|
|
332
|
+
updated: [...plan.updated],
|
|
333
|
+
removed: [...plan.removed],
|
|
334
|
+
unchanged: [...plan.unchanged],
|
|
335
|
+
skipped: [],
|
|
366
336
|
backend: sched.name,
|
|
367
337
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
368
338
|
};
|
|
@@ -446,24 +416,281 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
446
416
|
};
|
|
447
417
|
}
|
|
448
418
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
419
|
+
async function applySchedulerSyncPlan(backend, plan, publish) {
|
|
420
|
+
await applySchedulerTransaction(backend, plan.operations, {
|
|
421
|
+
initialExpectations: plan.operations.map((operation) => operation.expected),
|
|
422
|
+
assertReadSet: () => assertSchedulerSourceSnapshot(plan.sourceSnapshot),
|
|
423
|
+
beforeOperation: (_operation, index) => {
|
|
424
|
+
if (index === 0)
|
|
425
|
+
publish?.();
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
async function applySchedulerTransaction(backend, operations, hooks) {
|
|
430
|
+
if (operations.length === 0) {
|
|
431
|
+
hooks.assertReadSet?.();
|
|
432
|
+
await hooks.afterOperations?.();
|
|
433
|
+
hooks.assertReadSet?.();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
hooks.assertReadSet?.();
|
|
437
|
+
if (!backend.snapshotBindings || !backend.restoreBindings) {
|
|
438
|
+
throw new ConfigError(`Scheduler backend "${backend.name}" cannot snapshot and restore a whole-set transaction.`, "INVALID_CONFIG_FILE");
|
|
456
439
|
}
|
|
457
|
-
|
|
440
|
+
const nativeIds = [
|
|
441
|
+
...new Set(operations.map((operation) => operation.kind === "remove" ? operation.nativeId : schedulerBindingNativeId(operation.binding))),
|
|
442
|
+
];
|
|
443
|
+
const snapshot = await backend.snapshotBindings(nativeIds);
|
|
444
|
+
assertSchedulerTransactionSnapshot(snapshot, nativeIds, hooks.initialExpectations);
|
|
445
|
+
const rollbackExpected = schedulerRollbackExpectations(snapshot, operations);
|
|
446
|
+
hooks.assertReadSet?.();
|
|
447
|
+
try {
|
|
448
|
+
for (const [index, operation] of operations.entries()) {
|
|
449
|
+
hooks.assertReadSet?.();
|
|
450
|
+
await hooks.beforeOperation?.(operation, index);
|
|
451
|
+
hooks.assertReadSet?.();
|
|
452
|
+
if (operation.kind === "remove")
|
|
453
|
+
await backend.uninstall(operation.nativeId, operation.expected);
|
|
454
|
+
else
|
|
455
|
+
await backend.install(operation.binding, operation.options, operation.expected);
|
|
456
|
+
hooks.assertReadSet?.();
|
|
457
|
+
}
|
|
458
|
+
hooks.assertReadSet?.();
|
|
459
|
+
await hooks.afterOperations?.();
|
|
460
|
+
hooks.assertReadSet?.();
|
|
461
|
+
}
|
|
462
|
+
catch (primaryError) {
|
|
463
|
+
let externalRollbackError;
|
|
464
|
+
try {
|
|
465
|
+
await hooks.rollbackExternal?.();
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
externalRollbackError = error;
|
|
469
|
+
}
|
|
470
|
+
let nativeRollbackError;
|
|
471
|
+
const externalStateAllowsNativeRollback = externalRollbackError !== undefined &&
|
|
472
|
+
hooks.allowNativeRollbackAfterExternalFailure?.(externalRollbackError) === true;
|
|
473
|
+
if (!(externalRollbackError && hooks.suppressNativeRollbackWhenExternalFails && !externalStateAllowsNativeRollback)) {
|
|
474
|
+
try {
|
|
475
|
+
await backend.restoreBindings(snapshot, rollbackExpected);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
nativeRollbackError = error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const rollbackErrors = [externalRollbackError, nativeRollbackError].filter((error) => error !== undefined);
|
|
482
|
+
if (rollbackErrors.length > 0) {
|
|
483
|
+
throw new AggregateError([primaryError, ...rollbackErrors], `Scheduler transaction failed and rollback was incomplete: ${errorMessage(primaryError)}`);
|
|
484
|
+
}
|
|
485
|
+
throw primaryError;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function schedulerRollbackExpectations(snapshot, operations) {
|
|
489
|
+
return Object.freeze(snapshot.nativeIds.map((nativeId) => {
|
|
490
|
+
const prior = snapshot.artifacts.find((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === schedulerNativeArtifactKey(nativeId));
|
|
491
|
+
const allowed = [];
|
|
492
|
+
if (prior) {
|
|
493
|
+
if (prior.fingerprint === undefined) {
|
|
494
|
+
throw new ConfigError(`Scheduler backend snapshot for ${JSON.stringify(nativeId)} has no exact fingerprint.`, "INVALID_CONFIG_FILE");
|
|
495
|
+
}
|
|
496
|
+
allowed.push(Object.freeze({
|
|
497
|
+
state: "present",
|
|
498
|
+
...(prior.bindingId !== undefined ? { bindingId: prior.bindingId } : {}),
|
|
499
|
+
...(prior.invocation !== undefined ? { invocation: Object.freeze([...prior.invocation]) } : {}),
|
|
500
|
+
fingerprint: prior.fingerprint,
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
allowed.push(Object.freeze({ state: "absent" }));
|
|
505
|
+
}
|
|
506
|
+
const matchingOperations = operations.filter((candidate) => {
|
|
507
|
+
const operationNativeId = candidate.kind === "remove" ? candidate.nativeId : schedulerBindingNativeId(candidate.binding);
|
|
508
|
+
return schedulerNativeArtifactKey(operationNativeId) === schedulerNativeArtifactKey(nativeId);
|
|
509
|
+
});
|
|
510
|
+
for (const operation of matchingOperations) {
|
|
511
|
+
if (operation.kind === "remove") {
|
|
512
|
+
if (!allowed.some((state) => state.state === "absent")) {
|
|
513
|
+
allowed.push(Object.freeze({ state: "absent" }));
|
|
514
|
+
}
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (operation.resultFingerprint === undefined) {
|
|
518
|
+
throw new ConfigError(`Scheduler backend cannot freeze the post-mutation fingerprint for ${JSON.stringify(nativeId)}.`, "INVALID_CONFIG_FILE");
|
|
519
|
+
}
|
|
520
|
+
allowed.push(Object.freeze({
|
|
521
|
+
state: "present",
|
|
522
|
+
bindingId: operation.binding.id,
|
|
523
|
+
invocation: Object.freeze([...operation.binding.invocation]),
|
|
524
|
+
fingerprint: operation.resultFingerprint,
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
return Object.freeze({ nativeId, allowed: Object.freeze(allowed) });
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
function assertSchedulerTransactionSnapshot(snapshot, nativeIds, initialExpectations) {
|
|
531
|
+
if (!snapshot ||
|
|
532
|
+
!Array.isArray(snapshot.nativeIds) ||
|
|
533
|
+
!Array.isArray(snapshot.artifacts) ||
|
|
534
|
+
nativeIds.some((nativeId) => !snapshot.nativeIds.includes(nativeId))) {
|
|
535
|
+
throw new ConfigError("Scheduler backend returned an incomplete transaction snapshot.", "INVALID_CONFIG_FILE");
|
|
536
|
+
}
|
|
537
|
+
const snapshotKeys = snapshot.nativeIds.map(schedulerNativeArtifactKey);
|
|
538
|
+
const requestedKeys = nativeIds.map(schedulerNativeArtifactKey);
|
|
539
|
+
if (snapshotKeys.length !== requestedKeys.length ||
|
|
540
|
+
new Set(snapshotKeys).size !== snapshotKeys.length ||
|
|
541
|
+
snapshotKeys.some((key) => !requestedKeys.includes(key))) {
|
|
542
|
+
throw new ConfigError("Scheduler backend returned an inexact normalized transaction snapshot set.", "INVALID_CONFIG_FILE");
|
|
543
|
+
}
|
|
544
|
+
for (const expected of initialExpectations) {
|
|
545
|
+
const artifact = assertSchedulerNativeArtifactCardinality(snapshot.artifacts, expected.nativeId, expected.state === "absent" ? 0 : 1);
|
|
546
|
+
assertSchedulerMutationArtifact(artifact, expected);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function prepareTaskAddSchedulerTransaction(input) {
|
|
550
|
+
if (!input.sched.inspectBindings) {
|
|
551
|
+
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot provide one coherent inspection for transactional add.`, "INVALID_CONFIG_FILE");
|
|
552
|
+
}
|
|
553
|
+
if (!input.sched.snapshotBindings || !input.sched.restoreBindings || !input.sched.expectedSignature) {
|
|
554
|
+
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot provide exact snapshot, restore, and signature contracts for transactional add.`, "INVALID_CONFIG_FILE");
|
|
555
|
+
}
|
|
556
|
+
const inspection = await input.sched.inspectBindings({ rebind: input.rebind });
|
|
557
|
+
const installedEntries = [...inspection.installed];
|
|
558
|
+
const nativeArtifacts = [...inspection.artifacts];
|
|
559
|
+
const seenNativeKeys = new Set();
|
|
560
|
+
for (const artifact of nativeArtifacts) {
|
|
561
|
+
const key = schedulerNativeArtifactKey(artifact.nativeId);
|
|
562
|
+
if (seenNativeKeys.has(key)) {
|
|
563
|
+
throw new UsageError(`Scheduler inspection has duplicate normalized native artifact ${JSON.stringify(artifact.nativeId)}.`, "RESOURCE_ALREADY_EXISTS");
|
|
564
|
+
}
|
|
565
|
+
seenNativeKeys.add(key);
|
|
566
|
+
}
|
|
567
|
+
for (const binding of input.taskBindings) {
|
|
568
|
+
assertNoForeignSchedule(installedEntries, binding.id, input.ownerTarget);
|
|
569
|
+
}
|
|
570
|
+
const taskEntries = installedEntries.filter((entry) => installedEntryRunsTask(entry, input.id));
|
|
571
|
+
const foreignTaskEntry = taskEntries.find((entry) => !sameBundle(entry.target, input.ownerTarget));
|
|
572
|
+
if (foreignTaskEntry) {
|
|
573
|
+
throw new UsageError(foreignScheduleMessage(input.id, foreignTaskEntry.target), "RESOURCE_ALREADY_EXISTS");
|
|
574
|
+
}
|
|
575
|
+
assertSchedulerNativeArtifactOwnership(input.taskBindings, nativeArtifacts);
|
|
576
|
+
const primary = input.taskBindings[0];
|
|
577
|
+
if (!primary)
|
|
578
|
+
throw new Error("invariant: scheduler transaction has no desired binding");
|
|
579
|
+
const installedEntry = installedEntries.find((entry) => entry.id === primary.id) ?? taskEntries[0];
|
|
580
|
+
const preparedRuntime = installedEntry && !input.rebind
|
|
581
|
+
? {
|
|
582
|
+
options: {
|
|
583
|
+
...input.installOpts,
|
|
584
|
+
binding: Object.freeze([...installedEntry.binding]),
|
|
585
|
+
contextPath: installedEntry.contextPath,
|
|
586
|
+
},
|
|
587
|
+
}
|
|
588
|
+
: prepareSchedulerSyncRuntime(input.installOpts, input.deps, input.rebind, `create scheduler entry for task "${input.id}"`, []);
|
|
589
|
+
const runtimeOpts = preparedRuntime.options;
|
|
590
|
+
const removals = taskEntries.map((entry) => {
|
|
591
|
+
const invocation = entry.invocation;
|
|
592
|
+
if (!invocation) {
|
|
593
|
+
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact parsed owner; refusing replacement.`, "RESOURCE_ALREADY_EXISTS");
|
|
594
|
+
}
|
|
595
|
+
const nativeId = entry.nativeId ?? schedulerNativeBindingId(entry.id);
|
|
596
|
+
const artifact = assertSchedulerNativeArtifactCardinality(nativeArtifacts, nativeId, 1);
|
|
597
|
+
if (!artifact?.fingerprint || artifact.bindingId !== entry.id) {
|
|
598
|
+
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact coherent fingerprint.`, "RESOURCE_ALREADY_EXISTS");
|
|
599
|
+
}
|
|
600
|
+
const logicalSource = primary.logicalSource;
|
|
601
|
+
const ordinal = schedulerBindingOrdinal(entry.id, logicalSource, invocation);
|
|
602
|
+
if (ordinal === undefined) {
|
|
603
|
+
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact schedule ordinal; refusing replacement.`, "RESOURCE_ALREADY_EXISTS");
|
|
604
|
+
}
|
|
605
|
+
return Object.freeze({
|
|
606
|
+
kind: "remove",
|
|
607
|
+
id: entry.id,
|
|
608
|
+
nativeId,
|
|
609
|
+
expected: Object.freeze({
|
|
610
|
+
bindingId: entry.id,
|
|
611
|
+
nativeId,
|
|
612
|
+
logicalSource,
|
|
613
|
+
ordinal,
|
|
614
|
+
invocation: Object.freeze([...invocation]),
|
|
615
|
+
fingerprint: artifact.fingerprint,
|
|
616
|
+
}),
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
const installs = input.taskBindings.map((binding) => {
|
|
620
|
+
const nativeId = schedulerBindingNativeId(binding);
|
|
621
|
+
const resultFingerprint = input.sched.expectedSignature(binding, runtimeOpts);
|
|
622
|
+
if (!resultFingerprint) {
|
|
623
|
+
throw new ConfigError(`Scheduler backend "${input.sched.name}" cannot freeze the post-install fingerprint for ${JSON.stringify(binding.id)}.`, "INVALID_CONFIG_FILE");
|
|
624
|
+
}
|
|
625
|
+
return Object.freeze({
|
|
626
|
+
kind: "install",
|
|
627
|
+
binding,
|
|
628
|
+
expected: Object.freeze({
|
|
629
|
+
state: "absent",
|
|
630
|
+
bindingId: binding.id,
|
|
631
|
+
nativeId,
|
|
632
|
+
logicalSource: binding.logicalSource,
|
|
633
|
+
ordinal: binding.ordinal,
|
|
634
|
+
invocation: binding.invocation,
|
|
635
|
+
}),
|
|
636
|
+
resultFingerprint,
|
|
637
|
+
...(runtimeOpts ? { options: runtimeOpts } : {}),
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
const initialByKey = new Map();
|
|
641
|
+
for (const removal of removals) {
|
|
642
|
+
if (removal.kind !== "remove")
|
|
643
|
+
continue;
|
|
644
|
+
initialByKey.set(schedulerNativeArtifactKey(removal.nativeId), Object.freeze({ ...removal.expected, state: "present" }));
|
|
645
|
+
}
|
|
646
|
+
for (const install of installs) {
|
|
647
|
+
if (install.kind === "remove")
|
|
648
|
+
continue;
|
|
649
|
+
const key = schedulerNativeArtifactKey(schedulerBindingNativeId(install.binding));
|
|
650
|
+
if (!initialByKey.has(key))
|
|
651
|
+
initialByKey.set(key, install.expected);
|
|
652
|
+
}
|
|
653
|
+
return Object.freeze({
|
|
654
|
+
runtimeOpts,
|
|
655
|
+
...(preparedRuntime.publish ? { publishRuntime: preparedRuntime.publish } : {}),
|
|
656
|
+
operations: Object.freeze([...removals, ...installs]),
|
|
657
|
+
initialExpectations: Object.freeze([...initialByKey.values()]),
|
|
658
|
+
publishOperationIndex: removals.length,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
function prepareSchedulerSyncRuntime(base, deps, explicitRebind, operation, warnings) {
|
|
458
662
|
if (deps.backend && !deps.schedulerRuntime)
|
|
459
|
-
return base;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
warnings.push(`--rebind bound scheduled tasks 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\`.`);
|
|
663
|
+
return base ? { options: base } : {};
|
|
664
|
+
if (deps.schedulerRuntime) {
|
|
665
|
+
const runtime = deps.schedulerRuntime();
|
|
666
|
+
warnIneligibleRebind(runtime, explicitRebind, warnings);
|
|
667
|
+
return { options: { ...base, binding: runtime.binding, contextPath: runtime.contextPath } };
|
|
465
668
|
}
|
|
466
|
-
|
|
669
|
+
const invocation = resolveAndValidateSchedulerInvocation(explicitRebind, operation);
|
|
670
|
+
warnIneligibleRebind(invocation, explicitRebind, warnings);
|
|
671
|
+
const descriptor = schedulerContextDescriptor();
|
|
672
|
+
const contextPath = schedulerContextPath(descriptor);
|
|
673
|
+
return {
|
|
674
|
+
options: { ...base, binding: invocation.binding, contextPath },
|
|
675
|
+
publish: () => {
|
|
676
|
+
const written = writeSchedulerContextDescriptor(descriptor);
|
|
677
|
+
if (written !== contextPath) {
|
|
678
|
+
throw new ConfigError("Scheduler context descriptor path changed after preflight.", "INVALID_CONFIG_FILE");
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function resolveAndValidateSchedulerInvocation(explicitRebind, operation) {
|
|
684
|
+
const invocation = resolveAkmInvocation();
|
|
685
|
+
if (!invocation.eligible && !explicitRebind) {
|
|
686
|
+
throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
|
|
687
|
+
}
|
|
688
|
+
return { binding: invocation.argv, contextPath: "", eligible: invocation.eligible, kind: invocation.kind };
|
|
689
|
+
}
|
|
690
|
+
function warnIneligibleRebind(runtime, explicitRebind, warnings) {
|
|
691
|
+
if (!explicitRebind || runtime.eligible !== false || warnings.length > 0)
|
|
692
|
+
return;
|
|
693
|
+
warnings.push(`--rebind bound scheduled tasks 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\`.`);
|
|
467
694
|
}
|
|
468
695
|
export function prepareSchedulerRuntime(explicitRebind, operation, deps = {}) {
|
|
469
696
|
const invocation = (deps.resolveInvocation ?? resolveAkmInvocation)();
|
|
@@ -476,7 +703,7 @@ export function prepareSchedulerRuntime(explicitRebind, operation, deps = {}) {
|
|
|
476
703
|
function groupInstalledBindings(entries, invocation) {
|
|
477
704
|
const groups = new Map();
|
|
478
705
|
for (const entry of entries) {
|
|
479
|
-
const argv = entry.binding;
|
|
706
|
+
const argv = [...entry.binding];
|
|
480
707
|
const status = inspectInstalledBinding(entry, invocation);
|
|
481
708
|
const key = JSON.stringify([argv, entry.contextPath, status]);
|
|
482
709
|
const existing = groups.get(key);
|
|
@@ -539,10 +766,119 @@ function hasGitAncestor(file) {
|
|
|
539
766
|
function taskAssetRef(id) {
|
|
540
767
|
return { type: "task", name: id };
|
|
541
768
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
769
|
+
function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
770
|
+
const filePath = path.resolve(filePathInput);
|
|
771
|
+
const root = path.resolve(rootInput);
|
|
772
|
+
const lexicalRelative = path.relative(root, filePath);
|
|
773
|
+
if (lexicalRelative === "" || lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
774
|
+
throw new UsageError(`${filePathInput} resolves outside the task source root.`, "PATH_ESCAPE_VIOLATION");
|
|
775
|
+
}
|
|
776
|
+
const rootRealPath = fs.realpathSync(root);
|
|
777
|
+
const rootStat = fs.statSync(rootRealPath, { bigint: true });
|
|
778
|
+
if (!rootStat.isDirectory()) {
|
|
779
|
+
throw new UsageError(`${root} is not a task source directory.`, "INVALID_FLAG_VALUE");
|
|
780
|
+
}
|
|
781
|
+
const common = {
|
|
782
|
+
filePath,
|
|
783
|
+
rootRealPath,
|
|
784
|
+
rootPhysicalIdentity: rootStat.ino === 0n ? `path:${rootRealPath}` : `inode:${rootStat.dev}:${rootStat.ino}`,
|
|
785
|
+
rootMtimeNs: String(rootStat.mtimeNs),
|
|
786
|
+
rootCtimeNs: String(rootStat.ctimeNs),
|
|
787
|
+
};
|
|
788
|
+
let descriptor;
|
|
789
|
+
try {
|
|
790
|
+
const noFollow = "O_NOFOLLOW" in fs.constants ? fs.constants.O_NOFOLLOW : 0;
|
|
791
|
+
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
|
792
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
793
|
+
if (!before.isFile()) {
|
|
794
|
+
throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
|
|
795
|
+
}
|
|
796
|
+
if (before.size > BigInt(TASK_V3_MAX_SOURCE_BYTES)) {
|
|
797
|
+
throw new UsageError(`${filePath} exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) task source limit.`, "INVALID_FLAG_VALUE");
|
|
798
|
+
}
|
|
799
|
+
const bytes = fs.readFileSync(descriptor);
|
|
800
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
801
|
+
if (!sameTaskSourceStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
|
|
802
|
+
throw new UsageError(`${filePath} changed while its guarded bytes were read.`, "RESOURCE_ALREADY_EXISTS");
|
|
803
|
+
}
|
|
804
|
+
const realPath = fs.realpathSync(filePath);
|
|
805
|
+
const physicalRelative = path.relative(rootRealPath, realPath);
|
|
806
|
+
if (physicalRelative === "" || physicalRelative.startsWith("..") || path.isAbsolute(physicalRelative)) {
|
|
807
|
+
throw new UsageError(`${filePath} resolves outside the task source root.`, "PATH_ESCAPE_VIOLATION");
|
|
808
|
+
}
|
|
809
|
+
let content;
|
|
810
|
+
try {
|
|
811
|
+
content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
throw new UsageError(`${filePath} contains invalid UTF-8 bytes.`, "INVALID_FLAG_VALUE");
|
|
815
|
+
}
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
state: "present",
|
|
818
|
+
...common,
|
|
819
|
+
realPath,
|
|
820
|
+
physicalIdentity: before.ino === 0n ? `path:${realPath}` : `inode:${before.dev}:${before.ino}`,
|
|
821
|
+
size: bytes.byteLength,
|
|
822
|
+
mtimeNs: String(before.mtimeNs),
|
|
823
|
+
ctimeNs: String(before.ctimeNs),
|
|
824
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
825
|
+
bytesBase64: bytes.toString("base64"),
|
|
826
|
+
content,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
catch (cause) {
|
|
830
|
+
if (cause.code === "ENOENT") {
|
|
831
|
+
return Object.freeze({ state: "absent", ...common });
|
|
832
|
+
}
|
|
833
|
+
if (cause instanceof UsageError)
|
|
834
|
+
throw cause;
|
|
835
|
+
if (cause.code === "ELOOP") {
|
|
836
|
+
throw new UsageError(`${filePath} must not be a symbolic task source.`, "RESOURCE_ALREADY_EXISTS");
|
|
837
|
+
}
|
|
838
|
+
throw new UsageError(`${filePath} could not be guarded as a contained regular task source: ${errorMessage(cause)}`, "PATH_ESCAPE_VIOLATION");
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
if (descriptor !== undefined)
|
|
842
|
+
fs.closeSync(descriptor);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function assertTaskSourceExpectation(expected) {
|
|
846
|
+
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
847
|
+
if (!sameTaskSourceExpectation(actual, expected)) {
|
|
848
|
+
throw new UsageError(`Task source ${JSON.stringify(expected.filePath)} changed after transaction planning.`, "RESOURCE_ALREADY_EXISTS");
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function sameTaskSourceExpectation(left, right) {
|
|
852
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
853
|
+
}
|
|
854
|
+
function assertTaskSourceRestored(expected) {
|
|
855
|
+
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
856
|
+
const restored = actual.state === expected.state &&
|
|
857
|
+
actual.rootPhysicalIdentity === expected.rootPhysicalIdentity &&
|
|
858
|
+
(actual.state === "absent" ||
|
|
859
|
+
(expected.state === "present" && actual.sha256 === expected.sha256 && actual.content === expected.content));
|
|
860
|
+
if (!restored) {
|
|
861
|
+
throw new UsageError(`Task source ${JSON.stringify(expected.filePath)} could not be restored without replacing a concurrent owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
class TaskSourceRestoredBoundaryError extends Error {
|
|
865
|
+
cause;
|
|
866
|
+
constructor(cause) {
|
|
867
|
+
super(errorMessage(cause));
|
|
868
|
+
this.name = "TaskSourceRestoredBoundaryError";
|
|
869
|
+
this.cause = cause;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function sameTaskSourceStat(left, right) {
|
|
873
|
+
return (left.dev === right.dev &&
|
|
874
|
+
left.ino === right.ino &&
|
|
875
|
+
left.mode === right.mode &&
|
|
876
|
+
left.size === right.size &&
|
|
877
|
+
left.mtimeNs === right.mtimeNs &&
|
|
878
|
+
left.ctimeNs === right.ctimeNs);
|
|
879
|
+
}
|
|
880
|
+
function hashTaskSource(source) {
|
|
881
|
+
return createHash("sha256").update(Buffer.from(source, "utf8")).digest("hex");
|
|
546
882
|
}
|
|
547
883
|
/**
|
|
548
884
|
* Resolve the bundle a mutating/run task command targets. Returns the resolved
|
|
@@ -550,20 +886,66 @@ async function restoreTaskSourceBytes(writeAsset, source, config, ref, filePath,
|
|
|
550
886
|
* in scheduled invocations. The primary bundle uses the target-less form.
|
|
551
887
|
*/
|
|
552
888
|
function resolveTaskBundle(target, opts) {
|
|
553
|
-
const
|
|
889
|
+
const config = loadConfig();
|
|
890
|
+
const selected = resolveWriteTarget(config, target, { requireWritable: opts.requireWritable });
|
|
554
891
|
const resolved = opts.requireWritable ? prepareWriteTargetForMutation(selected) : selected;
|
|
555
892
|
const stashDir = resolved.source.path;
|
|
556
893
|
const installTarget = isPrimaryStashPath(stashDir) ? undefined : (resolved.selector ?? resolved.source.name);
|
|
557
|
-
return { resolved, stashDir, installTarget };
|
|
894
|
+
return { resolved, config, stashDir, bundleName: resolved.source.name, installTarget };
|
|
895
|
+
}
|
|
896
|
+
function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
|
|
897
|
+
return async ({ bundle, type, name }) => {
|
|
898
|
+
if (bundle === bundleName) {
|
|
899
|
+
return { file: await resolveAssetPath(bundleRoot, type, name), bundleRoot };
|
|
900
|
+
}
|
|
901
|
+
const target = resolveWriteTarget(config, bundle, { requireWritable: false });
|
|
902
|
+
return {
|
|
903
|
+
file: await resolveAssetPath(target.source.path, type, name),
|
|
904
|
+
bundleRoot: target.source.path,
|
|
905
|
+
};
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function resolveTaskReadBundle(refBundle, flagBundle) {
|
|
909
|
+
if (refBundle && flagBundle && refBundle !== flagBundle) {
|
|
910
|
+
throw new UsageError(`Task ref selects bundle ${JSON.stringify(refBundle)}, but --bundle selects ${JSON.stringify(flagBundle)}.`, "INVALID_FLAG_VALUE");
|
|
911
|
+
}
|
|
912
|
+
const selector = flagBundle ?? refBundle;
|
|
913
|
+
const config = loadConfig();
|
|
914
|
+
let resolved;
|
|
915
|
+
if (!selector) {
|
|
916
|
+
resolved = resolveWorkingStashTarget(config, { requireWritable: false });
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
const configured = resolveConfiguredSources(config).some((source) => source.name === selector);
|
|
920
|
+
const implicit = configured ? undefined : resolveImplicitScheduledBundleTarget(config, selector);
|
|
921
|
+
resolved = implicit ?? resolveWriteTarget(config, selector, { requireWritable: false });
|
|
922
|
+
}
|
|
923
|
+
if (refBundle && resolved.source.name !== refBundle) {
|
|
924
|
+
throw new UsageError(`Task ref bundle ${JSON.stringify(refBundle)} does not match the resolved source.`, "INVALID_FLAG_VALUE");
|
|
925
|
+
}
|
|
926
|
+
return resolved;
|
|
558
927
|
}
|
|
559
928
|
/**
|
|
560
|
-
*
|
|
561
|
-
*
|
|
929
|
+
* New scheduler bindings always carry a canonical `--bundle <owner>` token,
|
|
930
|
+
* including an env-selected working stash. That stash need not be persisted in
|
|
931
|
+
* config (CI, one-shot tools, and fresh installs commonly use only
|
|
932
|
+
* AKM_BUNDLE_DIR), so its scheduled child must accept precisely its derived
|
|
933
|
+
* owner name after the scheduler context restores the environment.
|
|
934
|
+
*
|
|
935
|
+
* This is intentionally narrower than an unknown-bundle fallback: a configured
|
|
936
|
+
* source always wins, and an unconfigured selector is accepted only when it is
|
|
937
|
+
* exactly the current env-selected working stash identity.
|
|
562
938
|
*/
|
|
563
|
-
function
|
|
564
|
-
if (
|
|
565
|
-
return
|
|
566
|
-
|
|
939
|
+
function resolveImplicitScheduledBundleTarget(config, selector) {
|
|
940
|
+
if (!process.env.AKM_BUNDLE_DIR?.trim())
|
|
941
|
+
return undefined;
|
|
942
|
+
try {
|
|
943
|
+
const working = resolveWorkingStashTarget(config, { requireWritable: false });
|
|
944
|
+
return working.source.name === selector ? working : undefined;
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
return undefined;
|
|
948
|
+
}
|
|
567
949
|
}
|
|
568
950
|
/** True when `candidate` resolves to the same directory as the primary stash. */
|
|
569
951
|
function isPrimaryStashPath(candidate) {
|
|
@@ -580,10 +962,17 @@ function isPrimaryStashPath(candidate) {
|
|
|
580
962
|
function sameBundle(a, b) {
|
|
581
963
|
return (a ?? undefined) === (b ?? undefined);
|
|
582
964
|
}
|
|
965
|
+
function installedEntryRunsTask(entry, id) {
|
|
966
|
+
const invocation = entry.invocation;
|
|
967
|
+
return invocation?.[0] === "task" && invocation[1] === "run" && invocation[2] === id;
|
|
968
|
+
}
|
|
583
969
|
function foreignScheduleMessage(id, existingTarget) {
|
|
584
970
|
const where = existingTarget === undefined ? "the default bundle" : `bundle "${existingTarget}"`;
|
|
585
971
|
return `Task id "${id}" is already scheduled from ${where}; rename the task or disable the existing one first.`;
|
|
586
972
|
}
|
|
973
|
+
function errorMessage(cause) {
|
|
974
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
975
|
+
}
|
|
587
976
|
/**
|
|
588
977
|
* Refuse to schedule an id already installed from a DIFFERENT bundle. Scheduler
|
|
589
978
|
* ids are the bare task id (never namespaced), so a single id can be active from
|
|
@@ -595,42 +984,44 @@ function assertNoForeignSchedule(entries, id, installTarget) {
|
|
|
595
984
|
throw new UsageError(foreignScheduleMessage(id, foreign.target), "RESOURCE_ALREADY_EXISTS");
|
|
596
985
|
}
|
|
597
986
|
function renderTaskYaml(input) {
|
|
598
|
-
const obj = { version:
|
|
987
|
+
const obj = { version: 3 };
|
|
599
988
|
if (input.workflow) {
|
|
600
|
-
obj.
|
|
601
|
-
if (input.params)
|
|
602
|
-
obj.
|
|
603
|
-
}
|
|
604
|
-
if (input.timeoutMs !== undefined)
|
|
605
|
-
obj.timeoutMs = input.timeoutMs;
|
|
989
|
+
obj.uses = input.workflow;
|
|
990
|
+
if (input.params)
|
|
991
|
+
obj.with = parseJsonObjectArg(input.params);
|
|
606
992
|
}
|
|
607
993
|
else if (input.prompt) {
|
|
608
|
-
obj.
|
|
609
|
-
|
|
610
|
-
obj.engine = input.engine;
|
|
611
|
-
if (input.model)
|
|
612
|
-
obj.model = input.model;
|
|
613
|
-
if (input.timeoutMs !== undefined)
|
|
614
|
-
obj.timeoutMs = input.timeoutMs;
|
|
994
|
+
obj.uses = "akm/command";
|
|
995
|
+
obj.with = { content: input.prompt };
|
|
615
996
|
}
|
|
616
997
|
else if (input.command !== undefined) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
obj.
|
|
621
|
-
if (input.timeoutMs !== undefined)
|
|
622
|
-
obj.timeoutMs = input.timeoutMs;
|
|
998
|
+
if (Array.isArray(input.command)) {
|
|
999
|
+
throw new UsageError("Task v3 --command accepts one shell string; argv arrays require manual migration.", "INVALID_FLAG_VALUE");
|
|
1000
|
+
}
|
|
1001
|
+
obj.run = input.command;
|
|
623
1002
|
}
|
|
624
1003
|
if (input.name)
|
|
625
1004
|
obj.name = input.name;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1005
|
+
obj.akm = {
|
|
1006
|
+
schedule: input.schedule,
|
|
1007
|
+
enabled: input.enabled,
|
|
1008
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
1009
|
+
...(input.when_to_use !== undefined ? { when_to_use: input.when_to_use } : {}),
|
|
1010
|
+
...(input.tags && input.tags.length > 0 ? { tags: input.tags } : {}),
|
|
1011
|
+
...(input.engine !== undefined ? { engine: input.engine } : {}),
|
|
1012
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
1013
|
+
...(input.timeoutMs !== undefined ? { timeout: input.timeoutMs } : {}),
|
|
1014
|
+
};
|
|
632
1015
|
return yamlStringify(obj);
|
|
633
1016
|
}
|
|
1017
|
+
function assertInlineTaskPrompt(input) {
|
|
1018
|
+
const value = input.trim();
|
|
1019
|
+
const pathShaped = /^(?:\.{1,2}[\\/]|~[\\/]|[\\/]|[A-Za-z]:[\\/])/.test(value) ||
|
|
1020
|
+
(!/\s/.test(value) && /[\\/]/.test(value) && path.extname(value) !== "");
|
|
1021
|
+
if (!isFullRefInput(value) && !pathShaped)
|
|
1022
|
+
return;
|
|
1023
|
+
throw new UsageError("--prompt accepts inline text only; asset refs and file paths are not prompt content. Use --workflow or an authored command ref where appropriate.", "INVALID_FLAG_VALUE");
|
|
1024
|
+
}
|
|
634
1025
|
function parseJsonObjectArg(raw) {
|
|
635
1026
|
let parsed;
|
|
636
1027
|
try {
|
|
@@ -645,7 +1036,7 @@ function parseJsonObjectArg(raw) {
|
|
|
645
1036
|
return parsed;
|
|
646
1037
|
}
|
|
647
1038
|
/**
|
|
648
|
-
* Toggle the `enabled:` value in a task YAML file
|
|
1039
|
+
* Toggle the v3 `akm.enabled:` value in a task YAML file without a full
|
|
649
1040
|
* parse/render round-trip (which would reformat the file). Appends the key
|
|
650
1041
|
* if absent.
|
|
651
1042
|
*
|
|
@@ -653,40 +1044,68 @@ function parseJsonObjectArg(raw) {
|
|
|
653
1044
|
* case-sensitive matching (YAML keys are case-sensitive).
|
|
654
1045
|
*/
|
|
655
1046
|
export function setEnabledInYaml(yaml, enabled) {
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
if (
|
|
659
|
-
return yaml.
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1047
|
+
const lines = yaml.replace(/\r\n/g, "\n").split("\n");
|
|
1048
|
+
const akmLine = lines.findIndex((line) => /^akm:\s*(?:#.*)?$/.test(line));
|
|
1049
|
+
if (akmLine < 0)
|
|
1050
|
+
return `${yaml.trimEnd()}\nakm:\n enabled: ${enabled}\n`;
|
|
1051
|
+
let insertAt = akmLine + 1;
|
|
1052
|
+
for (let index = akmLine + 1; index < lines.length; index += 1) {
|
|
1053
|
+
const line = lines[index];
|
|
1054
|
+
if (line === undefined)
|
|
1055
|
+
break;
|
|
1056
|
+
if (line !== "" && !/^[ \t]/.test(line))
|
|
1057
|
+
break;
|
|
1058
|
+
insertAt = index + 1;
|
|
1059
|
+
const match = line.match(/^([ \t]+enabled:\s*)([^\s#\r\n][^\r\n]*?)(\s*(?:#[^\r\n]*))?$/);
|
|
1060
|
+
if (match) {
|
|
1061
|
+
lines[index] = `${match[1]}${enabled}${match[3] ?? ""}`;
|
|
1062
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
1063
|
+
}
|
|
1064
|
+
const bare = line.match(/^([ \t]+enabled:)\s*$/);
|
|
1065
|
+
if (bare) {
|
|
1066
|
+
lines[index] = `${bare[1]} ${enabled}`;
|
|
1067
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
1068
|
+
}
|
|
665
1069
|
}
|
|
666
|
-
|
|
1070
|
+
lines.splice(insertAt, 0, ` enabled: ${enabled}`);
|
|
1071
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
667
1072
|
}
|
|
668
1073
|
// Re-exported so tests can verify the validator path directly.
|
|
669
1074
|
// Re-export error classes consumed by callers that want to instanceof-check.
|
|
670
1075
|
// Re-export this so the CLI can decide what process exit code to use after
|
|
671
1076
|
// `akm task run` completes.
|
|
672
|
-
export { ConfigError, exitCodeForStatus, NotFoundError,
|
|
673
|
-
//
|
|
1077
|
+
export { ConfigError, exitCodeForStatus, NotFoundError, UsageError };
|
|
1078
|
+
// Parse only the bundle/ref syntax here. Whether a concept is a task is an
|
|
1079
|
+
// adapter-specific decision made after resolving the selected bundle.
|
|
674
1080
|
export function parseTaskRef(input) {
|
|
675
1081
|
const trimmed = input.trim();
|
|
676
|
-
// Canonical conceptId form: `[bundle//]tasks/<id>`. A `/` unambiguously marks
|
|
677
|
-
// it — a bare task id can never contain `/` (`validateTaskId` forbids it) — so
|
|
678
|
-
// route it through the shared parser, which strips any bundle prefix and maps
|
|
679
|
-
// the `tasks/` stash-subdir back to the `task` type in one place.
|
|
680
1082
|
if (trimmed.includes("/")) {
|
|
681
1083
|
try {
|
|
682
|
-
const parsed =
|
|
683
|
-
if (parsed.
|
|
684
|
-
|
|
1084
|
+
const parsed = parseBundleRef(trimmed);
|
|
1085
|
+
if (parsed.fragment !== undefined)
|
|
1086
|
+
throw new Error("task refs do not accept fragments");
|
|
1087
|
+
return {
|
|
1088
|
+
id: normaliseTaskConceptId(parsed.conceptId),
|
|
1089
|
+
...(parsed.bundle ? { bundle: parsed.bundle } : {}),
|
|
1090
|
+
};
|
|
685
1091
|
}
|
|
686
1092
|
catch {
|
|
687
1093
|
// fall through to the shared error below
|
|
688
1094
|
}
|
|
689
|
-
throw new UsageError(`Expected a task
|
|
1095
|
+
throw new UsageError(`Expected a syntactically valid task concept id, got "${input}".`, "INVALID_FLAG_VALUE");
|
|
690
1096
|
}
|
|
691
1097
|
return { id: normaliseTaskId(trimmed) };
|
|
692
1098
|
}
|
|
1099
|
+
function taskIdForAdapter(parsedId, adapterId) {
|
|
1100
|
+
if (adapterId === "akm-task")
|
|
1101
|
+
return normaliseTaskConceptId(parsedId);
|
|
1102
|
+
if (adapterId === "akm") {
|
|
1103
|
+
if (!parsedId.includes("/"))
|
|
1104
|
+
return normaliseTaskId(parsedId);
|
|
1105
|
+
if (parsedId.startsWith("tasks/") && !parsedId.slice("tasks/".length).includes("/")) {
|
|
1106
|
+
return normaliseTaskId(parsedId.slice("tasks/".length));
|
|
1107
|
+
}
|
|
1108
|
+
throw new UsageError(`The native akm adapter accepts only a bare task id or tasks/<id>, got "${parsedId}".`, "INVALID_FLAG_VALUE");
|
|
1109
|
+
}
|
|
1110
|
+
throw new UsageError(`Bundle adapter "${adapterId}" does not define task runtime identity.`, "INVALID_FLAG_VALUE");
|
|
1111
|
+
}
|