akm-cli 0.9.31 → 0.10.26101002-alpha
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 +265 -0
- package/STABILITY.md +79 -53
- package/dist/api-node.mjs +20 -0
- package/dist/api.d.ts +23 -0
- package/dist/api.js +45 -0
- package/dist/assets/hints/cli-hints-full.md +13 -27
- package/dist/assets/hints/cli-hints-short.md +1 -1
- package/dist/assets/improve-strategies/default.json +3 -3
- package/dist/assets/prompts/consolidate-pair.md +9 -7
- package/dist/cli/shared.js +31 -48
- package/dist/cli/unknown-flags.js +6 -43
- package/dist/cli.js +8 -34
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/completions.js +1 -2
- package/dist/commands/config-cli.js +5 -28
- package/dist/commands/env/env-cli.js +5 -5
- package/dist/commands/env/secret-cli.js +2 -2
- package/dist/commands/health/checks.js +41 -16
- package/dist/commands/health/plugin-staleness.js +52 -22
- package/dist/commands/improve/autonomy-gate.js +5 -3
- package/dist/commands/improve/consolidate/pair-pass.js +7 -1
- package/dist/commands/improve/consolidate.js +6 -35
- package/dist/commands/improve/distill/lesson-update.js +91 -0
- package/dist/commands/improve/distill-guards.js +1 -43
- package/dist/commands/improve/distill.js +140 -20
- package/dist/commands/improve/eligibility.js +14 -1
- package/dist/commands/improve/extract-cli.js +1 -1
- package/dist/commands/improve/improve-cli.js +87 -102
- package/dist/commands/improve/improve-strategies.js +8 -12
- package/dist/commands/improve/improve.js +2 -2
- package/dist/commands/improve/loop-stages.js +0 -1
- package/dist/commands/improve/planner.js +0 -1
- package/dist/commands/improve/preparation.js +40 -99
- package/dist/commands/improve/proactive-maintenance.js +2 -2
- package/dist/commands/improve/reflect-noise.js +3 -55
- package/dist/commands/improve/reflect.js +3 -8
- package/dist/commands/improve/retrieval-scope.js +2 -2
- package/dist/commands/improve/salience.js +2 -9
- package/dist/commands/improve/stage.js +14 -9
- package/dist/commands/lint/index.js +5 -20
- package/dist/commands/observability-cli.js +3 -9
- package/dist/commands/proposal/drain.js +39 -6
- package/dist/commands/proposal/proposal-cli.js +11 -31
- package/dist/commands/proposal/repository.js +3 -3
- package/dist/commands/read/knowledge.js +3 -3
- package/dist/commands/read/remember-cli.js +0 -13
- package/dist/commands/read/search-cli.js +29 -97
- package/dist/commands/read/search.js +0 -11
- package/dist/commands/read/show.js +0 -19
- package/dist/commands/sources/plugin-upgrade.js +164 -101
- package/dist/commands/sources/sources-cli.js +1 -15
- package/dist/commands/sources/stash-cli.js +4 -12
- package/dist/commands/tasks/tasks-cli.js +4 -115
- package/dist/commands/workflow/plan.js +11 -33
- package/dist/commands/workflow-cli.js +2 -7
- package/dist/core/adapter/adapter-ids.js +1 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
- package/dist/core/adapter/adapters/akm-adapter.js +9 -48
- package/dist/core/adapter/adapters/akm-lint.js +10 -26
- package/dist/core/adapter/adapters/akm-metadata.js +2 -72
- package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
- package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
- package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
- package/dist/core/adapter/recognize-match.js +3 -4
- package/dist/core/common.js +18 -2
- package/dist/core/config/config-schema.js +12 -15
- package/dist/core/config/config-types.js +6 -15
- package/dist/core/config/config-walker.js +6 -23
- package/dist/core/config/config.js +17 -80
- package/dist/core/config/engine-semantics.js +7 -3
- package/dist/core/config/schema/engines.js +7 -7
- package/dist/core/config/schema/improve-processes.js +8 -61
- package/dist/core/config/schema/improve.js +2 -21
- package/dist/core/config/schema/index-config.js +6 -39
- package/dist/core/env-secret-ref.js +2 -45
- package/dist/core/errors.js +1 -2
- package/dist/core/extra-params.js +2 -86
- package/dist/core/improve-result.js +11 -1
- package/dist/core/mutation-target.js +2 -2
- package/dist/core/recognition-util.js +4 -5
- package/dist/core/warn.js +21 -4
- package/dist/core/write-source.js +3 -3
- package/dist/execution/target-ref.js +1 -2
- package/dist/indexer/search/db-search.js +4 -19
- package/dist/indexer/search/fts-query.js +0 -19
- package/dist/integrations/agent/engine-resolution.js +5 -2
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/profiles.js +41 -98
- package/dist/integrations/agent/request-lowering.js +3 -3
- package/dist/integrations/harnesses/aider/index.js +5 -0
- package/dist/integrations/harnesses/amazonq/index.js +1 -0
- package/dist/integrations/harnesses/claude/config-import.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +5 -0
- package/dist/integrations/harnesses/codex/index.js +1 -0
- package/dist/integrations/harnesses/copilot/index.js +1 -0
- package/dist/integrations/harnesses/gemini/index.js +1 -0
- package/dist/integrations/harnesses/index.js +18 -39
- package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
- package/dist/integrations/harnesses/opencode/config-import.js +121 -52
- package/dist/integrations/harnesses/opencode/index.js +22 -0
- package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
- package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
- package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
- package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
- package/dist/integrations/harnesses/opencode/session-log.js +104 -115
- package/dist/integrations/harnesses/opencode/version.js +98 -0
- package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
- package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
- package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +228 -0
- package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
- package/dist/integrations/harnesses/openhands/index.js +1 -0
- package/dist/integrations/harnesses/pi/index.js +1 -0
- package/dist/integrations/harnesses/shared.js +2 -0
- package/dist/integrations/harnesses/types.js +2 -0
- package/dist/llm/client.js +41 -11
- package/dist/output/cli-hints.js +1 -1
- package/dist/output/context.js +10 -19
- package/dist/output/format-exempt.js +5 -8
- package/dist/output/shapes/helpers.js +1 -22
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/shapes.js +3 -31
- package/dist/output/stdout.js +5 -1
- package/dist/output/text/command-format.js +6 -1
- package/dist/output/text/workflow-format.js +4 -22
- package/dist/scripts/akm-migrate-node.js +13049 -14013
- package/dist/scripts/akm-migrate.js +16316 -17276
- package/dist/setup/detect.js +10 -4
- package/dist/setup/harness-config-import.js +5 -14
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +12 -13
- package/dist/storage/repositories/task-history-repository.js +0 -5
- package/dist/storage/repositories/workflow-runs-repository.js +5 -15
- package/dist/tasks/embedded.js +2 -1
- package/dist/tasks/run/task-history.js +0 -2
- package/dist/tasks/schedule-gates.js +39 -0
- package/dist/tasks/scheduler-binding.js +0 -16
- package/dist/tasks/scheduler-sync.js +3 -97
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/task-source-v4.js +2 -12
- package/dist/tasks/task-run-reserved-flags.js +2 -31
- package/dist/workflows/authoring/authoring.js +6 -6
- package/dist/workflows/compile.js +8 -65
- package/dist/workflows/exec/child-workflow.js +22 -3
- package/dist/workflows/exec/native-executor.js +12 -147
- package/dist/workflows/exec/run-workflow.js +31 -103
- package/dist/workflows/exec/step-work.js +9 -178
- package/dist/workflows/exec/unit-dispatch.js +0 -10
- package/dist/workflows/freeze/environment.js +3 -44
- package/dist/workflows/freeze/freeze.js +8 -41
- package/dist/workflows/freeze/targets/child-workflow.js +27 -63
- package/dist/workflows/freeze/targets/command.js +2 -16
- package/dist/workflows/freeze/targets/shell.js +0 -2
- package/dist/workflows/freeze/task-bindings.js +2 -28
- package/dist/workflows/ir/params.js +12 -2
- package/dist/workflows/parser.js +82 -232
- package/dist/workflows/plan.js +1 -1
- package/dist/workflows/renderer.js +10 -19
- package/dist/workflows/runtime/run-plan.js +11 -53
- package/dist/workflows/runtime/runs.js +4 -27
- package/dist/workflows/source-files.js +12 -16
- package/dist/workflows/source-semantics.js +1 -93
- package/docs/README.md +1 -1
- package/docs/agents/README.md +3 -3
- package/docs/agents/agent-install.md +1 -1
- package/docs/architecture/adapters.md +17 -31
- package/docs/architecture/architecture.md +4 -5
- package/docs/architecture/improvement.md +52 -21
- package/docs/architecture/internals/improve-workflow.md +15 -8
- package/docs/architecture/internals/indexing.md +1 -1
- package/docs/architecture/internals/search.md +1 -2
- package/docs/architecture/internals/storage-locations.md +5 -5
- package/docs/architecture/persisted-data-compat.md +13 -4
- package/docs/architecture/specs/0.9.0-decisions.md +6 -2
- package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
- package/docs/architecture/specs/workflow-format-unification.md +1 -6
- package/docs/architecture/testing/manual-testing-checklist.md +20 -29
- package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
- package/docs/architecture/testing/testing-workflow.md +1 -1
- package/docs/architecture/workflow-engine.md +5 -56
- package/docs/guides/author-workflows.md +39 -69
- package/docs/guides/capture-knowledge.md +1 -1
- package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
- package/docs/guides/discover-and-load.md +4 -4
- package/docs/guides/getting-started.md +1 -1
- package/docs/guides/improve-the-library.md +7 -7
- package/docs/guides/recipes/headless-install.md +2 -2
- package/docs/guides/run-workflows.md +1 -1
- package/docs/guides/use-with-any-agent.md +49 -4
- package/docs/integration/bundling-akm.md +7 -6
- package/docs/maintainers/curate-workmap.md +1 -2
- package/docs/maintainers/release-checklist.md +30 -1
- package/docs/posts/akm-command-tour.md +3 -3
- package/docs/product-surface.md +1 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/api.md +65 -0
- package/docs/reference/asset-types.md +3 -5
- package/docs/reference/bundle-types.md +2 -4
- package/docs/reference/cli.md +129 -171
- package/docs/reference/configuration.md +84 -19
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/env-and-secrets.md +2 -2
- package/docs/reference/supported-formats.md +3 -4
- package/docs/reference/tasks.md +12 -19
- package/docs/reference/website-sources.md +1 -1
- package/docs/reference/workflow-schema.md +89 -317
- package/docs/reference/workflows.md +20 -48
- package/package.json +12 -2
- package/schemas/akm-config.json +2 -243
- package/schemas/akm-task.json +2 -3
- package/schemas/akm-workflow.json +47 -84
- package/dist/assets/improve-strategies/catchup.json +0 -41
- package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
- package/dist/assets/improve-strategies/quick.json +0 -17
- package/dist/assets/improve-strategies/reflect-distill.json +0 -29
- package/dist/assets/improve-strategies/thorough.json +0 -49
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
- package/dist/cli/retired-commands.js +0 -118
- package/dist/commands/tasks/validate.js +0 -176
- package/dist/core/config/legacy-source-shape-shim.js +0 -117
- package/dist/integrations/harnesses/ids.js +0 -28
- package/dist/tasks/source/task-source-v3-frozen.js +0 -424
- package/dist/tasks/source/task-to-v4.js +0 -863
- package/dist/workflows/exec/worktree.js +0 -372
- package/dist/workflows/freeze/child-output-references.js +0 -83
- package/dist/workflows/freeze/identity.js +0 -22
- package/dist/workflows/freeze/targets/script.js +0 -50
- package/dist/workflows/freeze/targets/task.js +0 -124
- package/dist/workflows/github-yaml.js +0 -506
- package/dist/workflows/runtime/run-outputs.js +0 -80
|
@@ -1,863 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Pure, byte-producing task-source migration planner: a v2, v3, or v4 task
|
|
6
|
-
* file straight to task source v4 (spec docs/plans/specs/p2b-input-bindings.md
|
|
7
|
-
* §1.3, §1.7 C-N1, §5). One planner, one outcome per file — the former
|
|
8
|
-
* two-generation chain (legacy task to v3, then v3 to v4, composed by
|
|
9
|
-
* `scripts/akm-migrate/migrate/task-files.ts`) is gone: a v2 file is read
|
|
10
|
-
* once and converted directly, with no intermediate v3 file ever written to
|
|
11
|
-
* disk or reported as its own outcome.
|
|
12
|
-
*
|
|
13
|
-
* A v3 document — real, or the v3-shape record a v2 file converts to in
|
|
14
|
-
* memory — is read as a raw record by a vendored bounded-YAML reader, never
|
|
15
|
-
* the typed `parseTaskV3Yaml`, which would normalize away exactly the value
|
|
16
|
-
* bytes this migrator must preserve (a duration string like "5m", or a bare
|
|
17
|
-
* numeric `timeout`, would be converted to milliseconds by the real parser).
|
|
18
|
-
* The one exception is a pre-validation gate: every v3-versioned document —
|
|
19
|
-
* real, or freshly built from v2 — is first checked against the REAL typed
|
|
20
|
-
* `parseTaskV3Yaml`, exactly as the prior two-generation chain did at each of
|
|
21
|
-
* its hops, so every blocked reason only the typed parser catches (an
|
|
22
|
-
* escaping `working-directory` symlink, a GitHub-expression schedule, an
|
|
23
|
-
* invalid builtin-command `with:` shape, and so on) still blocks here, with
|
|
24
|
-
* the same reason. The OUTPUT side is validated through the REAL
|
|
25
|
-
* `parseTaskSourceV4` before a "changed" outcome is ever handed back (C-N1,
|
|
26
|
-
* B-71).
|
|
27
|
-
*
|
|
28
|
-
* `inputs:` is never invented — the migrator translates structure, not
|
|
29
|
-
* intent (spec §5.3).
|
|
30
|
-
*/
|
|
31
|
-
import crypto from "node:crypto";
|
|
32
|
-
import path from "node:path";
|
|
33
|
-
import { isMap, isSeq, LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
|
|
34
|
-
import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
35
|
-
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
36
|
-
import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_TIMEOUT_MS } from "../../workflows/resource-limits.js";
|
|
37
|
-
import { validateTaskId } from "../task-id.js";
|
|
38
|
-
import { assertBoundedTaskYamlDocument, TASK_V3_MAX_REDACT_NAMES } from "./bounded-document.js";
|
|
39
|
-
import { classifyTaskV3Uses, parseTaskV3Yaml } from "./task-source-v3-frozen.js";
|
|
40
|
-
import { parseTaskSourceV4 } from "./task-source-v4.js";
|
|
41
|
-
// ── v3 grammar (real v3 input, and the v3-shape record a v2 file builds) ────
|
|
42
|
-
/** The closed v3 top-level key set (`src/tasks/source-v3.ts`'s own, vendored — not exported there). */
|
|
43
|
-
const V3_TOP_LEVEL_KEYS = new Set([
|
|
44
|
-
"version",
|
|
45
|
-
"name",
|
|
46
|
-
"uses",
|
|
47
|
-
"run",
|
|
48
|
-
"with",
|
|
49
|
-
"env",
|
|
50
|
-
"shell",
|
|
51
|
-
"working-directory",
|
|
52
|
-
"akm",
|
|
53
|
-
"on",
|
|
54
|
-
]);
|
|
55
|
-
/** The closed v3 `akm.*` key set, vendored from `src/tasks/source-v3.ts`. */
|
|
56
|
-
const V3_AKM_KEYS = new Set([
|
|
57
|
-
"schedule",
|
|
58
|
-
"enabled",
|
|
59
|
-
"description",
|
|
60
|
-
"when_to_use",
|
|
61
|
-
"tags",
|
|
62
|
-
"agent",
|
|
63
|
-
"engine",
|
|
64
|
-
"model",
|
|
65
|
-
"inference",
|
|
66
|
-
"outputSchema",
|
|
67
|
-
"tools",
|
|
68
|
-
"timeout",
|
|
69
|
-
"redact",
|
|
70
|
-
"maxSteps",
|
|
71
|
-
"maxRetries",
|
|
72
|
-
]);
|
|
73
|
-
/** The closed v3 `on.*` key set, vendored from `src/tasks/source-v3.ts`. */
|
|
74
|
-
const V3_ON_KEYS = new Set(["schedule", "workflow_dispatch"]);
|
|
75
|
-
/** `akm.*` keys hoisted verbatim to the identical top-level v4 key (schedule/enabled handled separately). */
|
|
76
|
-
const AKM_HOIST_KEYS = [
|
|
77
|
-
"description",
|
|
78
|
-
"when_to_use",
|
|
79
|
-
"tags",
|
|
80
|
-
"agent",
|
|
81
|
-
"engine",
|
|
82
|
-
"model",
|
|
83
|
-
"inference",
|
|
84
|
-
"tools",
|
|
85
|
-
"timeout",
|
|
86
|
-
"redact",
|
|
87
|
-
"maxSteps",
|
|
88
|
-
"maxRetries",
|
|
89
|
-
];
|
|
90
|
-
// ── v2 grammar ────────────────────────────────────────────────────────────────
|
|
91
|
-
const V2_KEYS = new Set([
|
|
92
|
-
"version",
|
|
93
|
-
"name",
|
|
94
|
-
"description",
|
|
95
|
-
"when_to_use",
|
|
96
|
-
"tags",
|
|
97
|
-
"schedule",
|
|
98
|
-
"enabled",
|
|
99
|
-
"workflow",
|
|
100
|
-
"prompt",
|
|
101
|
-
"command",
|
|
102
|
-
"params",
|
|
103
|
-
"engine",
|
|
104
|
-
"model",
|
|
105
|
-
"timeoutMs",
|
|
106
|
-
"maxSteps",
|
|
107
|
-
"maxRetries",
|
|
108
|
-
"llm",
|
|
109
|
-
"redact",
|
|
110
|
-
]);
|
|
111
|
-
const V2_SHARED_KEYS = new Set([
|
|
112
|
-
"version",
|
|
113
|
-
"name",
|
|
114
|
-
"description",
|
|
115
|
-
"when_to_use",
|
|
116
|
-
"tags",
|
|
117
|
-
"schedule",
|
|
118
|
-
"enabled",
|
|
119
|
-
"redact",
|
|
120
|
-
]);
|
|
121
|
-
const V2_LLM_KEYS = new Set([
|
|
122
|
-
"temperature",
|
|
123
|
-
"maxTokens",
|
|
124
|
-
"supportsJsonSchema",
|
|
125
|
-
"extraParams",
|
|
126
|
-
"contextLength",
|
|
127
|
-
"enableThinking",
|
|
128
|
-
"reasoningEffort",
|
|
129
|
-
]);
|
|
130
|
-
const SAFE_V2_COMMAND_TOKEN = /^[A-Za-z0-9_./:=+,-]+$/;
|
|
131
|
-
const SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
132
|
-
/**
|
|
133
|
-
* V2 executed argv directly, while v3 `run:` enters a host shell. An explicit
|
|
134
|
-
* path bypasses shell aliases/builtins; `akm` is the one bare executable whose
|
|
135
|
-
* v3 runtime resolution is contractually pinned to the current installation.
|
|
136
|
-
*/
|
|
137
|
-
function shellStableV2Executable(executable) {
|
|
138
|
-
return executable === "akm" || executable.includes("/");
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* `env NAME=value... cmd args...` is env(1) itself resolving and exec'ing
|
|
142
|
-
* `cmd` via its own PATH search — that lookup happens inside env's execvp()
|
|
143
|
-
* regardless of whether env was launched by direct execve (v2) or by a host
|
|
144
|
-
* shell (v3 `run:`). The shell-vs-argv divergence `shellStableV2Executable`
|
|
145
|
-
* guards against (bare names shadowed by shell aliases/builtins/functions)
|
|
146
|
-
* therefore does not apply to whatever env ultimately invokes, so skip past
|
|
147
|
-
* a leading `env` and its `NAME=value` assignments to find the real target.
|
|
148
|
-
* Returns the original tokens, unchanged, when there is no such target
|
|
149
|
-
* (e.g. `env` with nothing after its assignments).
|
|
150
|
-
*/
|
|
151
|
-
function skipEnvAssignmentPrefix(tokens) {
|
|
152
|
-
if (tokens[0] !== "env")
|
|
153
|
-
return { tokens, envWrapped: false };
|
|
154
|
-
let index = 1;
|
|
155
|
-
while (index < tokens.length && SHELL_ASSIGNMENT_WORD.test(tokens[index]))
|
|
156
|
-
index += 1;
|
|
157
|
-
if (index >= tokens.length)
|
|
158
|
-
return { tokens, envWrapped: false };
|
|
159
|
-
return { tokens: tokens.slice(index), envWrapped: true };
|
|
160
|
-
}
|
|
161
|
-
const KNOWN_PROMPT_REF_FAMILIES = new Set([
|
|
162
|
-
"agents",
|
|
163
|
-
"commands",
|
|
164
|
-
"env",
|
|
165
|
-
"facts",
|
|
166
|
-
"instructions",
|
|
167
|
-
"knowledge",
|
|
168
|
-
"lessons",
|
|
169
|
-
"memories",
|
|
170
|
-
"scripts",
|
|
171
|
-
"secrets",
|
|
172
|
-
"sessions",
|
|
173
|
-
"skills",
|
|
174
|
-
"tasks",
|
|
175
|
-
"workflows",
|
|
176
|
-
]);
|
|
177
|
-
/**
|
|
178
|
-
* #902: the one blocker with an unambiguous remedy. The sibling shell-safety
|
|
179
|
-
* reasons need a case-by-case judgement and stay reason-only.
|
|
180
|
-
*/
|
|
181
|
-
const ARGV_ARRAY_BLOCK_DETAIL = "Manual conversion required: an array `command:` has no safe v3 `run:` string. Rewrite it by hand as " +
|
|
182
|
-
"`run:` (string) plus `shell:` — see docs/migration/v0.9.1-to-v0.9.2.md for the full v2 to v4 field mapping.";
|
|
183
|
-
// ── shared helpers ────────────────────────────────────────────────────────────
|
|
184
|
-
function hash(bytes) {
|
|
185
|
-
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
186
|
-
}
|
|
187
|
-
function causeMessage(cause) {
|
|
188
|
-
return cause instanceof Error ? cause.message : String(cause);
|
|
189
|
-
}
|
|
190
|
-
function base(input) {
|
|
191
|
-
return {
|
|
192
|
-
filePath: input.filePath,
|
|
193
|
-
before: Buffer.from(input.bytes),
|
|
194
|
-
beforeHash: hash(input.bytes),
|
|
195
|
-
mode: input.mode,
|
|
196
|
-
writable: input.writable,
|
|
197
|
-
...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
|
|
198
|
-
...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
function blocked(input, reason, detail) {
|
|
202
|
-
return Object.freeze({ status: "blocked", ...base(input), reason, ...(detail ? { detail } : {}) });
|
|
203
|
-
}
|
|
204
|
-
function plainRecord(value, label) {
|
|
205
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
206
|
-
throw new Error(`${label} must be a mapping`);
|
|
207
|
-
}
|
|
208
|
-
const prototype = Object.getPrototypeOf(value);
|
|
209
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
210
|
-
throw new Error(`${label} must use a plain or null prototype`);
|
|
211
|
-
}
|
|
212
|
-
return value;
|
|
213
|
-
}
|
|
214
|
-
function exactString(value, label, nonempty = false) {
|
|
215
|
-
if (typeof value !== "string" || (nonempty && value.trim().length === 0)) {
|
|
216
|
-
throw new Error(`${label} must be ${nonempty ? "a non-empty " : "a "}string`);
|
|
217
|
-
}
|
|
218
|
-
return value;
|
|
219
|
-
}
|
|
220
|
-
function optionalString(value, label) {
|
|
221
|
-
if (value === undefined || value === null)
|
|
222
|
-
return undefined;
|
|
223
|
-
return exactString(value, label);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Bounded-YAML raw-record reader shared by every version and every
|
|
227
|
-
* generation (v2 grammar, real v3, and the v3-shape record a v2 file
|
|
228
|
-
* builds): reading the RAW decoded record — rather than a typed parser —
|
|
229
|
-
* keeps every field's original value bytes (a duration string, a bare
|
|
230
|
-
* millisecond integer, an env value's exact type) intact for verbatim
|
|
231
|
-
* re-emission; a typed parse would normalize several of these away (C-N1).
|
|
232
|
-
* Which grammar applies to the result is entirely up to the caller, decided
|
|
233
|
-
* after `data.version` is known.
|
|
234
|
-
*/
|
|
235
|
-
function parseRawTaskYaml(input) {
|
|
236
|
-
let source;
|
|
237
|
-
try {
|
|
238
|
-
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
throw new Error("task YAML contains invalid UTF-8 bytes");
|
|
242
|
-
}
|
|
243
|
-
const lineCounter = new LineCounter();
|
|
244
|
-
let document;
|
|
245
|
-
try {
|
|
246
|
-
document = parseDocument(source, { lineCounter, uniqueKeys: true });
|
|
247
|
-
}
|
|
248
|
-
catch (cause) {
|
|
249
|
-
throw new Error(`invalid YAML: ${causeMessage(cause)}`);
|
|
250
|
-
}
|
|
251
|
-
const [parseError] = document.errors;
|
|
252
|
-
if (parseError)
|
|
253
|
-
throw new Error(`invalid YAML: ${parseError.message.split("\n")[0]}`);
|
|
254
|
-
const [parseWarning] = document.warnings;
|
|
255
|
-
if (parseWarning)
|
|
256
|
-
throw new Error(`unsupported YAML construct: ${parseWarning.message}`);
|
|
257
|
-
assertBoundedTaskYamlDocument(document, {
|
|
258
|
-
filePath: input.filePath,
|
|
259
|
-
sourceLabel: "task migration source",
|
|
260
|
-
lineCounter,
|
|
261
|
-
});
|
|
262
|
-
return { data: plainRecord(document.toJS({ maxAliasCount: 0 }), "task YAML"), source };
|
|
263
|
-
}
|
|
264
|
-
/** Convert one already-validated v3 raw record (real or v2-derived) to final task source v4 bytes. */
|
|
265
|
-
function planV3DataToV4(input, data) {
|
|
266
|
-
const unknownTop = Object.keys(data).filter((key) => !V3_TOP_LEVEL_KEYS.has(key));
|
|
267
|
-
if (unknownTop.length > 0) {
|
|
268
|
-
return blocked(input, "invalid-v3-task", `unknown v3 field(s): ${unknownTop.join(", ")}`);
|
|
269
|
-
}
|
|
270
|
-
const hasUses = Object.hasOwn(data, "uses");
|
|
271
|
-
const hasRun = Object.hasOwn(data, "run");
|
|
272
|
-
if (hasUses === hasRun) {
|
|
273
|
-
return blocked(input, "invalid-v3-task", "requires exactly one executable selector: uses or run");
|
|
274
|
-
}
|
|
275
|
-
let akm;
|
|
276
|
-
if (Object.hasOwn(data, "akm")) {
|
|
277
|
-
try {
|
|
278
|
-
akm = plainRecord(data.akm, "akm");
|
|
279
|
-
}
|
|
280
|
-
catch (cause) {
|
|
281
|
-
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
282
|
-
}
|
|
283
|
-
const unknownAkm = Object.keys(akm).filter((key) => !V3_AKM_KEYS.has(key));
|
|
284
|
-
if (unknownAkm.length > 0) {
|
|
285
|
-
return blocked(input, "unrecognized-akm-member", `akm has unknown field(s): ${unknownAkm.join(", ")}`);
|
|
286
|
-
}
|
|
287
|
-
if (Object.hasOwn(akm, "enabled") && typeof akm.enabled !== "boolean") {
|
|
288
|
-
return blocked(input, "invalid-v3-task", "akm.enabled must be a boolean");
|
|
289
|
-
}
|
|
290
|
-
if (Object.hasOwn(akm, "schedule") && typeof akm.schedule !== "string") {
|
|
291
|
-
return blocked(input, "invalid-v3-task", "akm.schedule must be a string");
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const hasOn = Object.hasOwn(data, "on");
|
|
295
|
-
let onRecord;
|
|
296
|
-
if (hasOn) {
|
|
297
|
-
try {
|
|
298
|
-
onRecord = plainRecord(data.on, "on");
|
|
299
|
-
}
|
|
300
|
-
catch (cause) {
|
|
301
|
-
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
302
|
-
}
|
|
303
|
-
// Mirrors the frozen v3 reader's own `parseOn` gates exactly
|
|
304
|
-
// (task-source-v3-frozen.ts:395-427): an empty `on: {}` declares no
|
|
305
|
-
// trigger at all, and `on.workflow_dispatch` accepts only null or an
|
|
306
|
-
// empty mapping (inputs are unsupported in v3). The frozen parser
|
|
307
|
-
// rejects both shapes outright, so this migrator must too — translating
|
|
308
|
-
// a document the v3 oracle would refuse to parse into runnable v4 bytes
|
|
309
|
-
// would launder invalid input into a valid, schedule-less task.
|
|
310
|
-
if (Object.keys(onRecord).length === 0) {
|
|
311
|
-
return blocked(input, "invalid-v3-task", "on must declare schedule and/or workflow_dispatch.");
|
|
312
|
-
}
|
|
313
|
-
const unknownOn = Object.keys(onRecord).filter((key) => !V3_ON_KEYS.has(key));
|
|
314
|
-
if (unknownOn.length > 0) {
|
|
315
|
-
return blocked(input, "invalid-v3-task", `on has unknown field(s): ${unknownOn.join(", ")}`);
|
|
316
|
-
}
|
|
317
|
-
if (Object.hasOwn(onRecord, "workflow_dispatch") && onRecord.workflow_dispatch !== null) {
|
|
318
|
-
let dispatchMapping;
|
|
319
|
-
try {
|
|
320
|
-
dispatchMapping = plainRecord(onRecord.workflow_dispatch, "on.workflow_dispatch");
|
|
321
|
-
}
|
|
322
|
-
catch (cause) {
|
|
323
|
-
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
324
|
-
}
|
|
325
|
-
if (Object.keys(dispatchMapping).length > 0) {
|
|
326
|
-
return blocked(input, "invalid-v3-task", "on.workflow_dispatch must be null or an empty mapping; inputs are unsupported.");
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
const hasAkmSchedule = akm !== undefined && Object.hasOwn(akm, "schedule");
|
|
331
|
-
if (hasAkmSchedule && hasOn) {
|
|
332
|
-
return blocked(input, "ambiguous-scheduling-source", "declares both akm.schedule and on:; task v3 requires exactly one scheduling source and the migrator will not guess which one wins.");
|
|
333
|
-
}
|
|
334
|
-
if (!hasAkmSchedule && !hasOn) {
|
|
335
|
-
return blocked(input, "invalid-v3-task", "requires exactly one scheduling source: akm.schedule or on.");
|
|
336
|
-
}
|
|
337
|
-
const hasWith = Object.hasOwn(data, "with");
|
|
338
|
-
let usesTarget;
|
|
339
|
-
if (hasUses) {
|
|
340
|
-
let usesValue;
|
|
341
|
-
try {
|
|
342
|
-
usesValue = exactString(data.uses, "uses", true);
|
|
343
|
-
}
|
|
344
|
-
catch (cause) {
|
|
345
|
-
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
346
|
-
}
|
|
347
|
-
try {
|
|
348
|
-
usesTarget = classifyTaskV3Uses(usesValue);
|
|
349
|
-
}
|
|
350
|
-
catch (cause) {
|
|
351
|
-
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
352
|
-
}
|
|
353
|
-
if (usesTarget.kind === "github-action") {
|
|
354
|
-
return blocked(input, "github-action-target-removed", `"${usesValue}" is a github-action target; the github-action uses: variant was removed in task source v4. Use commands/, scripts/, workflows/, or akm/command instead.`);
|
|
355
|
-
}
|
|
356
|
-
if (hasWith && usesTarget.kind !== "builtin-command") {
|
|
357
|
-
return blocked(input, "with-on-non-command-target", `a with: block on "${usesValue}" (a non-akm/command target) has no task source v4 equivalent; task-call inputs are declared and bound separately.`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
else if (hasWith) {
|
|
361
|
-
return blocked(input, "invalid-v3-task", "with is legal only with uses");
|
|
362
|
-
}
|
|
363
|
-
if (!input.writable || input.onDiskWritable === false) {
|
|
364
|
-
return blocked(input, "read-only-source", !input.writable ? "the owning source is not writable" : "the source file or publication directory is read-only");
|
|
365
|
-
}
|
|
366
|
-
let scheduleField;
|
|
367
|
-
// Several independent translation facts can need reporting on the SAME
|
|
368
|
-
// file (a manual-only trigger AND a dropped output schema, say), so
|
|
369
|
-
// notices accumulate and are joined into the single `notice` string the
|
|
370
|
-
// outcome carries.
|
|
371
|
-
const notices = [];
|
|
372
|
-
if (hasAkmSchedule) {
|
|
373
|
-
const cron = akm.schedule;
|
|
374
|
-
scheduleField = cron;
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
const rawSchedule = onRecord !== undefined && Object.hasOwn(onRecord, "schedule") ? onRecord.schedule : undefined;
|
|
378
|
-
if (rawSchedule !== undefined) {
|
|
379
|
-
if (!Array.isArray(rawSchedule) || rawSchedule.length === 0) {
|
|
380
|
-
return blocked(input, "invalid-v3-task", "on.schedule must be a non-empty list of {cron} records");
|
|
381
|
-
}
|
|
382
|
-
const crons = [];
|
|
383
|
-
for (const entry of rawSchedule) {
|
|
384
|
-
let record;
|
|
385
|
-
try {
|
|
386
|
-
record = plainRecord(entry, "on.schedule[]");
|
|
387
|
-
}
|
|
388
|
-
catch (cause) {
|
|
389
|
-
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
390
|
-
}
|
|
391
|
-
const keys = Object.keys(record);
|
|
392
|
-
if (keys.length !== 1 || keys[0] !== "cron" || typeof record.cron !== "string" || record.cron.length === 0) {
|
|
393
|
-
return blocked(input, "invalid-v3-task", "each on.schedule entry must be exactly {cron: <non-empty string>}");
|
|
394
|
-
}
|
|
395
|
-
crons.push(record.cron);
|
|
396
|
-
}
|
|
397
|
-
scheduleField = crons.map((cron) => ({ cron }));
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
notices.push("schedule: is absent from the migrated document — the source's only trigger was on.workflow_dispatch (manual dispatch); task source v4 tasks are always runnable manually via `akm task run`, so no schedule: entry was emitted.");
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const out = { version: 4 };
|
|
404
|
-
if (Object.hasOwn(data, "name"))
|
|
405
|
-
out.name = data.name;
|
|
406
|
-
if (hasUses)
|
|
407
|
-
out.uses = data.uses;
|
|
408
|
-
else
|
|
409
|
-
out.run = data.run;
|
|
410
|
-
if (Object.hasOwn(data, "shell"))
|
|
411
|
-
out.shell = data.shell;
|
|
412
|
-
if (hasWith)
|
|
413
|
-
out.with = data.with;
|
|
414
|
-
if (Object.hasOwn(data, "env"))
|
|
415
|
-
out.env = data.env;
|
|
416
|
-
if (Object.hasOwn(data, "working-directory"))
|
|
417
|
-
out["working-directory"] = data["working-directory"];
|
|
418
|
-
if (scheduleField !== undefined)
|
|
419
|
-
out.schedule = scheduleField;
|
|
420
|
-
if (akm) {
|
|
421
|
-
for (const key of AKM_HOIST_KEYS) {
|
|
422
|
-
if (Object.hasOwn(akm, key))
|
|
423
|
-
out[key] = akm[key];
|
|
424
|
-
}
|
|
425
|
-
// v3's `akm.outputSchema: null` means "no schema" (accepted verbatim by
|
|
426
|
-
// the frozen v3 reader, task-source-v3-frozen.ts:256-258); v4's
|
|
427
|
-
// `output:` has no null form (parseOutputSchema always requires a
|
|
428
|
-
// mapping). Omitting the key is the faithful v4 equivalent of an
|
|
429
|
-
// explicit v3 null — emitting `output: null` would fail the real
|
|
430
|
-
// parseTaskSourceV4 validation below and block the whole file.
|
|
431
|
-
if (Object.hasOwn(akm, "outputSchema") && akm.outputSchema !== null) {
|
|
432
|
-
// v4 accepts `output:` ONLY on a command target — `uses: commands/<ref>`
|
|
433
|
-
// or `uses: akm/command` (src/tasks/source/task-source-v4.ts's
|
|
434
|
-
// `targetConsumesOutputSchema`). v3 enforced no such rule: the frozen v3
|
|
435
|
-
// reader accepts `akm.outputSchema` on ANY target kind
|
|
436
|
-
// (task-source-v3-frozen.ts:256-263), and on `run:`/`uses: scripts/`/
|
|
437
|
-
// `uses: workflows/` it was equally inert there — nothing ever consumed
|
|
438
|
-
// it. Hoisting it unconditionally would therefore emit bytes the real
|
|
439
|
-
// parseTaskSourceV4 below rejects, blocking a valid, previously-runnable
|
|
440
|
-
// v3 file from `akm migrate apply`
|
|
441
|
-
// (scripts/akm-migrate/migrate/task-files.ts). Dropping an
|
|
442
|
-
// already-inert field and SAYING SO is the faithful translation, and
|
|
443
|
-
// keeps spec row B-66 / §5.3's `changed` guarantee intact.
|
|
444
|
-
if (usesTarget !== undefined && (usesTarget.kind === "command" || usesTarget.kind === "builtin-command")) {
|
|
445
|
-
out.output = akm.outputSchema;
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
const targetLabel = usesTarget === undefined ? "a run: target" : `the "${usesTarget.ref}" target`;
|
|
449
|
-
notices.push(`akm.outputSchema was dropped rather than hoisted to output: — task source v4 accepts output: only with a command target (uses: commands/<ref> or uses: akm/command), and ${targetLabel} never consumed the schema in v3 either, so nothing enforceable was lost.`);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
const afterYaml = stringifyYaml(out);
|
|
454
|
-
const after = Buffer.from(afterYaml, "utf8");
|
|
455
|
-
try {
|
|
456
|
-
parseTaskSourceV4({
|
|
457
|
-
yaml: afterYaml,
|
|
458
|
-
filePath: input.filePath,
|
|
459
|
-
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
catch (cause) {
|
|
463
|
-
return blocked(input, "generated-v4-validation-failed", causeMessage(cause));
|
|
464
|
-
}
|
|
465
|
-
const notice = notices.join(" ");
|
|
466
|
-
return Object.freeze({
|
|
467
|
-
status: "changed",
|
|
468
|
-
...base(input),
|
|
469
|
-
reason: "task-converted",
|
|
470
|
-
after,
|
|
471
|
-
afterHash: hash(after),
|
|
472
|
-
...(notice ? { notice } : {}),
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
// ── v2 → v3-shape record ───────────────────────────────────────────────────
|
|
476
|
-
function validateCommonV2(data) {
|
|
477
|
-
const unknown = Object.keys(data).filter((key) => !V2_KEYS.has(key));
|
|
478
|
-
if (unknown.length > 0)
|
|
479
|
-
throw new Error(`unknown v2 field(s): ${unknown.join(", ")}`);
|
|
480
|
-
exactString(data.schedule, "schedule", true);
|
|
481
|
-
if (data.enabled !== undefined && typeof data.enabled !== "boolean")
|
|
482
|
-
throw new Error("enabled must be a boolean");
|
|
483
|
-
for (const key of ["name", "description", "when_to_use"])
|
|
484
|
-
optionalString(data[key], key);
|
|
485
|
-
if (data.tags !== undefined && data.tags !== null) {
|
|
486
|
-
if (!Array.isArray(data.tags) || data.tags.some((entry) => typeof entry !== "string" || entry.length === 0)) {
|
|
487
|
-
throw new Error("tags must be an array of non-empty strings");
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (data.timeoutMs !== undefined && data.timeoutMs !== null) {
|
|
491
|
-
if (!Number.isInteger(data.timeoutMs) ||
|
|
492
|
-
data.timeoutMs < 1 ||
|
|
493
|
-
data.timeoutMs > WORKFLOW_MAX_TIMEOUT_MS) {
|
|
494
|
-
throw new Error(`timeoutMs must be null or an integer from 1 through ${WORKFLOW_MAX_TIMEOUT_MS}`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
if (data.redact !== undefined && data.redact !== null) {
|
|
498
|
-
if (!Array.isArray(data.redact) ||
|
|
499
|
-
data.redact.length > TASK_V3_MAX_REDACT_NAMES ||
|
|
500
|
-
data.redact.some((entry) => typeof entry !== "string" || !WORKFLOW_ENV_VAR_NAME_PATTERN.test(entry))) {
|
|
501
|
-
throw new Error("redact must contain only bounded environment variable names");
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
function validateTargetFields(data, allowed) {
|
|
506
|
-
const targetFields = new Set([...allowed, "workflow", "prompt", "command"]);
|
|
507
|
-
const invalid = Object.keys(data).filter((key) => !V2_SHARED_KEYS.has(key) && !targetFields.has(key));
|
|
508
|
-
if (invalid.length > 0)
|
|
509
|
-
throw new Error(`field(s) not valid for this target: ${invalid.join(", ")}`);
|
|
510
|
-
}
|
|
511
|
-
function validateV2Llm(value) {
|
|
512
|
-
if (value === undefined)
|
|
513
|
-
return undefined;
|
|
514
|
-
const llm = plainRecord(value, "llm");
|
|
515
|
-
const unknown = Object.keys(llm).filter((key) => !V2_LLM_KEYS.has(key));
|
|
516
|
-
if (unknown.length > 0)
|
|
517
|
-
throw new Error(`llm has unknown field(s): ${unknown.join(", ")}`);
|
|
518
|
-
if (llm.temperature !== undefined && (typeof llm.temperature !== "number" || !Number.isFinite(llm.temperature))) {
|
|
519
|
-
throw new Error("llm.temperature must be a finite number");
|
|
520
|
-
}
|
|
521
|
-
for (const key of ["maxTokens", "contextLength"]) {
|
|
522
|
-
if (llm[key] !== undefined && (!Number.isInteger(llm[key]) || llm[key] <= 0)) {
|
|
523
|
-
throw new Error(`llm.${key} must be a positive integer`);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
for (const key of ["supportsJsonSchema", "enableThinking"]) {
|
|
527
|
-
if (llm[key] !== undefined && typeof llm[key] !== "boolean")
|
|
528
|
-
throw new Error(`llm.${key} must be a boolean`);
|
|
529
|
-
}
|
|
530
|
-
if (llm.reasoningEffort !== undefined && (typeof llm.reasoningEffort !== "string" || !llm.reasoningEffort.trim())) {
|
|
531
|
-
throw new Error("llm.reasoningEffort must be a non-empty string");
|
|
532
|
-
}
|
|
533
|
-
if (llm.extraParams !== undefined) {
|
|
534
|
-
const issue = validateExtraParams(llm.extraParams)[0];
|
|
535
|
-
if (issue)
|
|
536
|
-
throw new Error(formatExtraParamsIssue("llm.extraParams", issue));
|
|
537
|
-
}
|
|
538
|
-
return llm;
|
|
539
|
-
}
|
|
540
|
-
function commonAkm(data) {
|
|
541
|
-
const akm = {
|
|
542
|
-
schedule: exactString(data.schedule, "schedule", true),
|
|
543
|
-
enabled: data.enabled === undefined ? true : data.enabled,
|
|
544
|
-
};
|
|
545
|
-
for (const key of ["description", "when_to_use", "tags"]) {
|
|
546
|
-
if (data[key] !== undefined && data[key] !== null)
|
|
547
|
-
akm[key] = data[key];
|
|
548
|
-
}
|
|
549
|
-
return akm;
|
|
550
|
-
}
|
|
551
|
-
function addRuntimeOverrides(data, akm) {
|
|
552
|
-
for (const key of ["engine", "model"]) {
|
|
553
|
-
const value = optionalString(data[key], key);
|
|
554
|
-
if (value)
|
|
555
|
-
akm[key] = value;
|
|
556
|
-
}
|
|
557
|
-
const llm = validateV2Llm(data.llm);
|
|
558
|
-
if (llm !== undefined)
|
|
559
|
-
akm.inference = llm;
|
|
560
|
-
if (data.timeoutMs !== undefined)
|
|
561
|
-
akm.timeout = data.timeoutMs;
|
|
562
|
-
if (data.redact !== undefined && data.redact !== null) {
|
|
563
|
-
akm.redact = [...new Set(data.redact)];
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
function addSharedNonPromptOverrides(data, akm) {
|
|
567
|
-
if (data.timeoutMs !== undefined)
|
|
568
|
-
akm.timeout = data.timeoutMs;
|
|
569
|
-
if (data.redact !== undefined && data.redact !== null) {
|
|
570
|
-
akm.redact = [...new Set(data.redact)];
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
function promptSourceKind(raw) {
|
|
574
|
-
const trimmed = raw.trim();
|
|
575
|
-
if (trimmed.startsWith("./") ||
|
|
576
|
-
trimmed.startsWith("../") ||
|
|
577
|
-
path.isAbsolute(trimmed) ||
|
|
578
|
-
/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
579
|
-
return "file";
|
|
580
|
-
}
|
|
581
|
-
try {
|
|
582
|
-
const parsed = parseBundleRef(trimmed);
|
|
583
|
-
const family = parsed.conceptId.split("/", 1)[0] ?? "";
|
|
584
|
-
if (bundleRefToString(parsed) !== trimmed || !parsed.conceptId.includes("/")) {
|
|
585
|
-
return "inline";
|
|
586
|
-
}
|
|
587
|
-
if (!KNOWN_PROMPT_REF_FAMILIES.has(family))
|
|
588
|
-
return parsed.bundle === undefined ? "inline" : "other-ref";
|
|
589
|
-
if (family === "agents")
|
|
590
|
-
return "agent";
|
|
591
|
-
if (family === "commands")
|
|
592
|
-
return "command";
|
|
593
|
-
return "other-ref";
|
|
594
|
-
}
|
|
595
|
-
catch {
|
|
596
|
-
return "inline";
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Convert one already-normalized legacy record to a v3-shape record, in
|
|
601
|
-
* memory — never written to disk, never reported as its own outcome. Returns
|
|
602
|
-
* a blocked reason string in place of the record when the v2 document has no
|
|
603
|
-
* safe v4 representation.
|
|
604
|
-
*/
|
|
605
|
-
function migratedObject(data) {
|
|
606
|
-
validateCommonV2(data);
|
|
607
|
-
const targets = ["workflow", "prompt", "command"].filter((key) => Object.hasOwn(data, key) && data[key] !== null && data[key] !== "");
|
|
608
|
-
if (targets.length !== 1)
|
|
609
|
-
throw new Error("v2 task must declare exactly one of workflow, prompt, or command");
|
|
610
|
-
const output = { version: 3 };
|
|
611
|
-
if (data.name !== undefined && data.name !== null)
|
|
612
|
-
output.name = data.name;
|
|
613
|
-
const akm = commonAkm(data);
|
|
614
|
-
if (targets[0] === "workflow") {
|
|
615
|
-
validateTargetFields(data, ["params", "timeoutMs", "maxSteps", "maxRetries"]);
|
|
616
|
-
const ref = exactString(data.workflow, "workflow", true).trim();
|
|
617
|
-
let target;
|
|
618
|
-
try {
|
|
619
|
-
target = classifyTaskV3Uses(ref);
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
throw new Error("workflow is not a canonical v3 asset ref");
|
|
623
|
-
}
|
|
624
|
-
if (target.kind !== "workflow")
|
|
625
|
-
throw new Error("workflow target is not a workflows/ ref");
|
|
626
|
-
output.uses = ref;
|
|
627
|
-
if (data.params !== undefined && data.params !== null) {
|
|
628
|
-
const params = plainRecord(data.params, "params");
|
|
629
|
-
output.with = params;
|
|
630
|
-
}
|
|
631
|
-
if (data.maxSteps !== undefined && data.maxSteps !== null) {
|
|
632
|
-
if (!Number.isSafeInteger(data.maxSteps) || data.maxSteps < 1)
|
|
633
|
-
throw new Error("maxSteps must be positive");
|
|
634
|
-
akm.maxSteps = data.maxSteps;
|
|
635
|
-
}
|
|
636
|
-
if (data.maxRetries !== undefined && data.maxRetries !== null) {
|
|
637
|
-
if (!Number.isSafeInteger(data.maxRetries) || data.maxRetries < 0) {
|
|
638
|
-
throw new Error("maxRetries must be a non-negative integer");
|
|
639
|
-
}
|
|
640
|
-
akm.maxRetries = data.maxRetries;
|
|
641
|
-
}
|
|
642
|
-
addSharedNonPromptOverrides(data, akm);
|
|
643
|
-
}
|
|
644
|
-
else if (targets[0] === "prompt") {
|
|
645
|
-
validateTargetFields(data, ["engine", "model", "timeoutMs", "llm"]);
|
|
646
|
-
const prompt = exactString(data.prompt, "prompt", true).trim();
|
|
647
|
-
const kind = promptSourceKind(prompt);
|
|
648
|
-
if (kind === "file")
|
|
649
|
-
return "dynamic-file-read-cannot-be-inlined-without-changing-semantics";
|
|
650
|
-
if (kind === "agent")
|
|
651
|
-
return "agent-ref-has-persona-but-no-command-work";
|
|
652
|
-
if (kind === "other-ref")
|
|
653
|
-
return "non-command-asset-has-no-v3-command-ref-equivalent";
|
|
654
|
-
if (kind === "command")
|
|
655
|
-
output.uses = prompt;
|
|
656
|
-
else {
|
|
657
|
-
output.uses = "akm/command";
|
|
658
|
-
output.with = { content: prompt };
|
|
659
|
-
}
|
|
660
|
-
addRuntimeOverrides(data, akm);
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
validateTargetFields(data, ["timeoutMs"]);
|
|
664
|
-
if (Array.isArray(data.command))
|
|
665
|
-
return "argv-array-has-no-portable-shell-string";
|
|
666
|
-
const command = exactString(data.command, "command", true).trim();
|
|
667
|
-
if (/['"\\]/.test(command))
|
|
668
|
-
return "shell-quoting-changes-v2-whitespace-split-semantics";
|
|
669
|
-
const tokens = command.split(/\s+/).filter(Boolean);
|
|
670
|
-
if (tokens.length === 0 || tokens.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
|
|
671
|
-
return "shell-operators-change-v2-literal-argv-semantics";
|
|
672
|
-
}
|
|
673
|
-
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens);
|
|
674
|
-
const executable = targetTokens[0];
|
|
675
|
-
if (SHELL_ASSIGNMENT_WORD.test(executable) || (!envWrapped && !shellStableV2Executable(executable))) {
|
|
676
|
-
return "shell-command-resolution-changes-v2-literal-argv-semantics";
|
|
677
|
-
}
|
|
678
|
-
output.run = tokens.join(" ");
|
|
679
|
-
addSharedNonPromptOverrides(data, akm);
|
|
680
|
-
}
|
|
681
|
-
output.akm = akm;
|
|
682
|
-
return output;
|
|
683
|
-
}
|
|
684
|
-
function isReason(value) {
|
|
685
|
-
return typeof value === "string";
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* v2 straight to v4: build the v3-shape record in memory, validate it
|
|
689
|
-
* through the REAL typed v3 parser exactly as the prior v2-to-v3 generation
|
|
690
|
-
* did (the same safety net, now inline), then hoist it to v4 through the
|
|
691
|
-
* same `planV3DataToV4` every real v3 file goes through. `before`/
|
|
692
|
-
* `beforeHash` on the result are always the original v2 bytes (`base(input)`,
|
|
693
|
-
* computed from the untouched `input`).
|
|
694
|
-
*/
|
|
695
|
-
function planV2DataToV4(input, data) {
|
|
696
|
-
if (!input.writable || input.onDiskWritable === false) {
|
|
697
|
-
return blocked(input, "read-only-source", !input.writable ? "the owning source is not writable" : "the source file or publication directory is read-only");
|
|
698
|
-
}
|
|
699
|
-
try {
|
|
700
|
-
validateTaskId(path.basename(input.filePath, ".yml"));
|
|
701
|
-
}
|
|
702
|
-
catch (cause) {
|
|
703
|
-
return blocked(input, "invalid-v2-task", causeMessage(cause));
|
|
704
|
-
}
|
|
705
|
-
let migrated;
|
|
706
|
-
try {
|
|
707
|
-
migrated = migratedObject(data);
|
|
708
|
-
}
|
|
709
|
-
catch (cause) {
|
|
710
|
-
return blocked(input, "invalid-v2-task", causeMessage(cause));
|
|
711
|
-
}
|
|
712
|
-
if (isReason(migrated)) {
|
|
713
|
-
const detail = migrated === "argv-array-has-no-portable-shell-string" ? ARGV_ARRAY_BLOCK_DETAIL : undefined;
|
|
714
|
-
return blocked(input, migrated, detail);
|
|
715
|
-
}
|
|
716
|
-
const v3Yaml = stringifyYaml(migrated);
|
|
717
|
-
try {
|
|
718
|
-
parseTaskV3Yaml({
|
|
719
|
-
yaml: v3Yaml,
|
|
720
|
-
filePath: input.filePath,
|
|
721
|
-
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
catch (cause) {
|
|
725
|
-
return blocked(input, "generated-v3-validation-failed", causeMessage(cause));
|
|
726
|
-
}
|
|
727
|
-
let v3Data;
|
|
728
|
-
try {
|
|
729
|
-
({ data: v3Data } = parseRawTaskYaml({ ...input, bytes: Buffer.from(v3Yaml, "utf8") }));
|
|
730
|
-
}
|
|
731
|
-
catch (cause) {
|
|
732
|
-
return blocked(input, "invalid-task-yaml", causeMessage(cause));
|
|
733
|
-
}
|
|
734
|
-
const v4Outcome = planV3DataToV4(input, v3Data);
|
|
735
|
-
return v4Outcome.status === "changed" ? { ...v4Outcome, reason: "task-converted" } : v4Outcome;
|
|
736
|
-
}
|
|
737
|
-
// ── v4 (cleanup only) ───────────────────────────────────────────────────────
|
|
738
|
-
/** A v4 document: strip 0.9.15's retired per-schedule `enabled`, if present; otherwise already current. */
|
|
739
|
-
function planV4Cleanup(input, source) {
|
|
740
|
-
const document = parseDocument(source, { uniqueKeys: true });
|
|
741
|
-
const schedule = document.get("schedule", true);
|
|
742
|
-
let removed = false;
|
|
743
|
-
if (isSeq(schedule)) {
|
|
744
|
-
for (const entry of schedule.items) {
|
|
745
|
-
if (!isMap(entry) || !entry.has("enabled"))
|
|
746
|
-
continue;
|
|
747
|
-
entry.delete("enabled");
|
|
748
|
-
removed = true;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
if (removed) {
|
|
752
|
-
if (!input.writable || input.onDiskWritable === false) {
|
|
753
|
-
return blocked(input, "read-only-source", !input.writable ? "the owning source is not writable" : "the source file or publication directory is read-only");
|
|
754
|
-
}
|
|
755
|
-
const after = Buffer.from(document.toString(), "utf8");
|
|
756
|
-
try {
|
|
757
|
-
parseTaskSourceV4({
|
|
758
|
-
yaml: after.toString("utf8"),
|
|
759
|
-
filePath: input.filePath,
|
|
760
|
-
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
catch (cause) {
|
|
764
|
-
return blocked(input, "generated-v4-validation-failed", causeMessage(cause));
|
|
765
|
-
}
|
|
766
|
-
return Object.freeze({
|
|
767
|
-
status: "changed",
|
|
768
|
-
...base(input),
|
|
769
|
-
reason: "source-enablement-removed",
|
|
770
|
-
after,
|
|
771
|
-
afterHash: hash(after),
|
|
772
|
-
notice: "Removed source-owned schedule enablement; scheduler activation is now host-local config.",
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
try {
|
|
776
|
-
parseTaskSourceV4({
|
|
777
|
-
yaml: source,
|
|
778
|
-
filePath: input.filePath,
|
|
779
|
-
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
780
|
-
});
|
|
781
|
-
return Object.freeze({ status: "skipped", ...base(input), reason: "already-v4" });
|
|
782
|
-
}
|
|
783
|
-
catch (cause) {
|
|
784
|
-
return blocked(input, "invalid-v4-task", causeMessage(cause));
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
// ── dispatcher ───────────────────────────────────────────────────────────────
|
|
788
|
-
/** Plan exactly one source file — v2, v3, or v4 — straight to v4, without touching disk. */
|
|
789
|
-
export function planTaskToV4File(input) {
|
|
790
|
-
let data;
|
|
791
|
-
let source;
|
|
792
|
-
try {
|
|
793
|
-
({ data, source } = parseRawTaskYaml(input));
|
|
794
|
-
}
|
|
795
|
-
catch (cause) {
|
|
796
|
-
return blocked(input, "invalid-task-yaml", causeMessage(cause));
|
|
797
|
-
}
|
|
798
|
-
if (data.version === 4) {
|
|
799
|
-
return planV4Cleanup(input, source);
|
|
800
|
-
}
|
|
801
|
-
if (data.version === 3) {
|
|
802
|
-
try {
|
|
803
|
-
parseTaskV3Yaml({
|
|
804
|
-
yaml: source,
|
|
805
|
-
filePath: input.filePath,
|
|
806
|
-
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
catch (cause) {
|
|
810
|
-
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
811
|
-
}
|
|
812
|
-
return planV3DataToV4(input, data);
|
|
813
|
-
}
|
|
814
|
-
if (data.version === 2) {
|
|
815
|
-
return planV2DataToV4(input, data);
|
|
816
|
-
}
|
|
817
|
-
return blocked(input, "unsupported-task-version", `expected version 2, 3, or 4, got ${String(data.version)}`);
|
|
818
|
-
}
|
|
819
|
-
function generationFor(files) {
|
|
820
|
-
const digest = crypto.createHash("sha256");
|
|
821
|
-
digest.update("akm-task-to-v4-plan-v1\0");
|
|
822
|
-
for (const file of files) {
|
|
823
|
-
digest.update(file.filePath);
|
|
824
|
-
digest.update("\0");
|
|
825
|
-
digest.update(file.status);
|
|
826
|
-
digest.update("\0");
|
|
827
|
-
digest.update(file.reason);
|
|
828
|
-
digest.update("\0");
|
|
829
|
-
digest.update(String(file.mode));
|
|
830
|
-
digest.update("\0");
|
|
831
|
-
digest.update(file.writable ? "writable" : "read-only");
|
|
832
|
-
digest.update("\0");
|
|
833
|
-
digest.update(file.onDiskWritable === false ? "disk-read-only" : "disk-writable-or-unspecified");
|
|
834
|
-
digest.update("\0");
|
|
835
|
-
if (file.containmentRoot)
|
|
836
|
-
digest.update(file.containmentRoot);
|
|
837
|
-
digest.update("\0");
|
|
838
|
-
digest.update(file.beforeHash);
|
|
839
|
-
digest.update("\0");
|
|
840
|
-
if (file.status === "changed")
|
|
841
|
-
digest.update(file.afterHash);
|
|
842
|
-
digest.update("\0");
|
|
843
|
-
if (file.detail)
|
|
844
|
-
digest.update(file.detail);
|
|
845
|
-
digest.update("\0");
|
|
846
|
-
if (file.status === "changed" && file.notice)
|
|
847
|
-
digest.update(file.notice);
|
|
848
|
-
digest.update("\0");
|
|
849
|
-
}
|
|
850
|
-
return digest.digest("hex");
|
|
851
|
-
}
|
|
852
|
-
/** Build/fingerprint a plan from already-derived immutable outcomes. */
|
|
853
|
-
export function taskToV4PlanFromOutcomes(outcomes) {
|
|
854
|
-
const files = [...outcomes].sort((left, right) => left.filePath < right.filePath ? -1 : left.filePath > right.filePath ? 1 : 0);
|
|
855
|
-
for (let index = 1; index < files.length; index += 1) {
|
|
856
|
-
const previous = files[index - 1];
|
|
857
|
-
const current = files[index];
|
|
858
|
-
if (previous && current && path.resolve(previous.filePath) === path.resolve(current.filePath)) {
|
|
859
|
-
throw new Error(`duplicate task migration file path: ${current.filePath}`);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
return Object.freeze({ schemaVersion: 1, generation: generationFor(files), files: Object.freeze(files) });
|
|
863
|
-
}
|