akm-cli 0.9.2-alpha.4 → 0.9.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 +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
* File-private helpers prepareTaskV3Execution depends on, moved body-intact
|
|
6
|
+
* out of the pre-P1b src/tasks/runtime-v3.ts (spec
|
|
7
|
+
* docs/plans/specs/p1b-model-extraction.md §4.1, Lane B / D4 module map).
|
|
8
|
+
* None of these was exported at head either; only the subset prepare.ts's
|
|
9
|
+
* moved function body calls is exported here.
|
|
10
|
+
*/
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
13
|
+
import { COMPOSITION_INVALID_MULTI_JOB_HINT, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
14
|
+
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
15
|
+
import { isPortableExecutionAgentSelector } from "../../execution/source.js";
|
|
16
|
+
import { requireAuthorizedExecutionPlan } from "../../integrations/agent/execution-cascade.js";
|
|
17
|
+
import { lowerResolvedExecutionRequest } from "../../integrations/agent/execution-lowering.js";
|
|
18
|
+
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
19
|
+
import { detectSecretShapedParams } from "../../workflows/exec/param-secrets.js";
|
|
20
|
+
import { compileWorkflowPlan } from "../../workflows/ir/compile.js";
|
|
21
|
+
import { compileWorkflowSource } from "../../workflows/source-ir/compile.js";
|
|
22
|
+
import { isInferredSecretName } from "../log-redaction.js";
|
|
23
|
+
import { SCHEDULED_TASK_CONTEXT_KEYS } from "../scheduler-invocation.js";
|
|
24
|
+
function own(value, key) {
|
|
25
|
+
return value !== undefined && Object.hasOwn(value, key);
|
|
26
|
+
}
|
|
27
|
+
export function environmentSnapshot(environment) {
|
|
28
|
+
const out = Object.create(null);
|
|
29
|
+
for (const key of Object.keys(environment ?? {}).sort()) {
|
|
30
|
+
const raw = environment?.[key];
|
|
31
|
+
if (raw === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
const value = String(raw);
|
|
34
|
+
if (isInferredSecretName(key) || detectSecretShapedParams({ [key]: value }).length > 0) {
|
|
35
|
+
throw new UsageError(`Task env.${key} is a secret-shaped literal env value. Store credentials in a secret/env binding rather than durable task source.`, "TASK_SOURCE_INVALID");
|
|
36
|
+
}
|
|
37
|
+
Object.defineProperty(out, key, { value, enumerable: true, configurable: false, writable: false });
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze(out);
|
|
40
|
+
}
|
|
41
|
+
/** Merge source env with the authoritative, closed scheduler directory context. */
|
|
42
|
+
export function commandEnvironmentSnapshot(environment, schedulerContext) {
|
|
43
|
+
const out = Object.create(null);
|
|
44
|
+
for (const key of Object.keys(environment)) {
|
|
45
|
+
Object.defineProperty(out, key, { value: environment[key], enumerable: true, configurable: true, writable: true });
|
|
46
|
+
}
|
|
47
|
+
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
48
|
+
const value = schedulerContext?.[key];
|
|
49
|
+
if (!value)
|
|
50
|
+
continue;
|
|
51
|
+
Object.defineProperty(out, key, { value, enumerable: true, configurable: true, writable: true });
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze(out);
|
|
54
|
+
}
|
|
55
|
+
function normalizeTimeout(value) {
|
|
56
|
+
if (value === undefined || value === null || typeof value === "number")
|
|
57
|
+
return value;
|
|
58
|
+
const parsed = parseDuration(value, DURATION_UNITS);
|
|
59
|
+
if (parsed === null)
|
|
60
|
+
throw new UsageError(`Invalid task timeout ${JSON.stringify(value)}.`, "TASK_SOURCE_INVALID");
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
export function qualifyOwnedRef(ref, context) {
|
|
64
|
+
const parsed = parseBundleRef(ref);
|
|
65
|
+
const bundle = parsed.bundle ?? context.bundleName;
|
|
66
|
+
return { parsed, qualified: makeBundleRef(bundle, parsed.conceptId) };
|
|
67
|
+
}
|
|
68
|
+
export function currentExecutionValues(document, context, environment) {
|
|
69
|
+
const akm = document.akm;
|
|
70
|
+
const agent = akm?.agent;
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
...(own(akm, "agent")
|
|
73
|
+
? {
|
|
74
|
+
agent: typeof agent === "string" && isPortableExecutionAgentSelector(agent)
|
|
75
|
+
? qualifyOwnedRef(agent, context).qualified
|
|
76
|
+
: agent,
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
...(own(akm, "engine") ? { engine: akm?.engine } : {}),
|
|
80
|
+
...(own(akm, "model") ? { model: akm?.model } : {}),
|
|
81
|
+
...(own(akm, "inference") ? { inference: akm?.inference } : {}),
|
|
82
|
+
...(own(akm, "outputSchema") ? { outputSchema: akm?.outputSchema } : {}),
|
|
83
|
+
...(own(akm, "tools") ? { tools: akm?.tools } : {}),
|
|
84
|
+
...(own(akm, "timeout") ? { timeout: akm?.timeout } : {}),
|
|
85
|
+
workspace: context.bundleRoot,
|
|
86
|
+
environment,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function base(document, context, environment) {
|
|
90
|
+
const timeoutMs = normalizeTimeout(document.akm?.timeout);
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
taskId: context.taskId,
|
|
93
|
+
taskRef: context.taskRef,
|
|
94
|
+
environment,
|
|
95
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
96
|
+
redact: Object.freeze([...(document.akm?.redact ?? [])]),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export async function resolvedOwnedAsset(qualified, type, context) {
|
|
100
|
+
const parsed = parseBundleRef(qualified);
|
|
101
|
+
const prefix = `${type}s/`;
|
|
102
|
+
const name = parsed.conceptId.slice(prefix.length);
|
|
103
|
+
if (context.resolveAsset) {
|
|
104
|
+
const resolved = await context.resolveAsset({ bundle: parsed.bundle, type, name, ref: qualified });
|
|
105
|
+
return typeof resolved === "string"
|
|
106
|
+
? Object.freeze({ file: resolved, bundleRoot: context.bundleRoot })
|
|
107
|
+
: Object.freeze({ file: resolved.file, bundleRoot: resolved.bundleRoot });
|
|
108
|
+
}
|
|
109
|
+
if (parsed.bundle !== context.bundleName) {
|
|
110
|
+
throw new NotFoundError(`Task target ${JSON.stringify(qualified)} names bundle ${JSON.stringify(parsed.bundle)}, but no bundle resolver was provided.`, "ASSET_NOT_FOUND");
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
file: await resolveAssetPath(context.bundleRoot, type, name),
|
|
114
|
+
bundleRoot: context.bundleRoot,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function defaultTaskShell(platform) {
|
|
118
|
+
return platform === "win32" ? "powershell" : "sh";
|
|
119
|
+
}
|
|
120
|
+
export function validatePreparedCommand(invocation, context) {
|
|
121
|
+
const request = requireAuthorizedExecutionPlan(invocation.plan);
|
|
122
|
+
if (!request.engine.name) {
|
|
123
|
+
throw new ConfigError(`Task ${JSON.stringify(context.taskRef)} has no resolved execution engine. Configure defaults.engine or akm.engine before running it.`, "INVALID_CONFIG_FILE");
|
|
124
|
+
}
|
|
125
|
+
// Lowering is pure. Running it before the durable-attempt boundary proves
|
|
126
|
+
// the selected target is transport-projectable; dispatch repeats the same
|
|
127
|
+
// deterministic projection from this frozen request/config snapshot.
|
|
128
|
+
lowerResolvedExecutionRequest(request, invocation.config);
|
|
129
|
+
return invocation;
|
|
130
|
+
}
|
|
131
|
+
export function validateWorkflowRuntimeSource(file, workspaceRoot, readFile) {
|
|
132
|
+
const source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFile(file, workspaceRoot));
|
|
133
|
+
const compiled = compileWorkflowSource(source, { path: file, workspaceRoot });
|
|
134
|
+
if (!compiled.ok) {
|
|
135
|
+
// P4-N2's mapping (docs/plans/specs/p4-deletions-closeout.md §3.3.4): a
|
|
136
|
+
// task-wrapped workflow target's own multi-job source is a composition
|
|
137
|
+
// failure, same as freezing it directly would be; any other compile
|
|
138
|
+
// failure is WORKFLOW_SOURCE_INVALID.
|
|
139
|
+
const detail = compiled.errors.map((error) => `${error.path}:${error.line}: ${error.message}`).join("; ");
|
|
140
|
+
const isMultiJob = compiled.errors.length === 1 && compiled.errors[0]?.code === "multi-job-unsupported";
|
|
141
|
+
const code = isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID";
|
|
142
|
+
throw new UsageError(`Task workflow target is not projectable: ${detail}`, code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined);
|
|
143
|
+
}
|
|
144
|
+
const planned = compileWorkflowPlan(compiled.ir, path.basename(file, path.extname(file)));
|
|
145
|
+
if (!planned.ok) {
|
|
146
|
+
// Same mapping applied for consistency; `compiled.ir` is already
|
|
147
|
+
// guaranteed exactly one job here, so this arm always resolves to
|
|
148
|
+
// WORKFLOW_SOURCE_INVALID in practice.
|
|
149
|
+
const detail = planned.errors.map((error) => `${file}:${error.line}: ${error.message}`).join("; ");
|
|
150
|
+
const isMultiJob = planned.errors.length === 1 && planned.errors[0]?.code === "multi-job-unsupported";
|
|
151
|
+
const code = isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID";
|
|
152
|
+
throw new UsageError(`Task workflow target is not projectable: ${detail}`, code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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 task runtime projection.
|
|
6
|
+
*
|
|
7
|
+
* This is the only bridge from an already-parsed, already-projected task
|
|
8
|
+
* source document (the `PreparableTaskDocument` seam, P4-N4) into executable
|
|
9
|
+
* work. It performs all source/config/asset reads before the task runner
|
|
10
|
+
* reserves a durable attempt and returns immutable snapshots. In particular,
|
|
11
|
+
* script work contains frozen bytes and their digest, never a path that can
|
|
12
|
+
* be reread when a delayed or resumed dispatch begins.
|
|
13
|
+
*
|
|
14
|
+
* `prepareTaskV3Execution`'s three production callers are
|
|
15
|
+
* `src/tasks/run/load-task.ts`, `src/tasks/scheduler-sync.ts`, and
|
|
16
|
+
* `src/workflows/freeze/targets/task.ts`'s `taskDispatch`. The name and the
|
|
17
|
+
* `TaskV3*` type family it takes stay (spec docs/plans/specs/p4-deletions-closeout.md
|
|
18
|
+
* §0, R-R1) — only task source v4 ever reaches this function now, always via
|
|
19
|
+
* `src/tasks/source/project-v4.ts`'s `projectTaskSourceV4()`.
|
|
20
|
+
*/
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import { prepareCommandInvocation } from "../../commands/command/command-execution.js";
|
|
23
|
+
import { UsageError } from "../../core/errors.js";
|
|
24
|
+
import { base, commandEnvironmentSnapshot, currentExecutionValues, defaultTaskShell, environmentSnapshot, qualifyOwnedRef, resolvedOwnedAsset, validatePreparedCommand, validateWorkflowRuntimeSource, } from "./prepare-support.js";
|
|
25
|
+
import { captureDirectoryIdentity, captureScriptTarget } from "./script-capture.js";
|
|
26
|
+
/** Project one canonical task-v3 source into immutable executable work. */
|
|
27
|
+
export async function prepareTaskV3Execution(document, context) {
|
|
28
|
+
const environment = environmentSnapshot(document.env);
|
|
29
|
+
const commandEnvironment = commandEnvironmentSnapshot(environment, context.schedulerContext);
|
|
30
|
+
const common = base(document, context, environment);
|
|
31
|
+
if (document.target.kind === "run") {
|
|
32
|
+
const cwdIdentity = captureDirectoryIdentity(context.bundleRoot, document.target.workingDirectory);
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
...common,
|
|
35
|
+
kind: "shell",
|
|
36
|
+
command: document.target.run,
|
|
37
|
+
shell: document.target.shell ?? defaultTaskShell(context.platform ?? process.platform),
|
|
38
|
+
cwd: cwdIdentity.realCwd,
|
|
39
|
+
cwdIdentity,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const target = document.target.uses;
|
|
43
|
+
if (target.kind === "builtin-command") {
|
|
44
|
+
const command = document.target.command;
|
|
45
|
+
if (!command)
|
|
46
|
+
throw new Error("invariant: parsed built-in command target has no command action");
|
|
47
|
+
const action = command.kind === "stored"
|
|
48
|
+
? {
|
|
49
|
+
ref: qualifyOwnedRef(command.ref, context).qualified,
|
|
50
|
+
...(command.arguments !== undefined ? { arguments: command.arguments } : {}),
|
|
51
|
+
}
|
|
52
|
+
: { content: command.content, ...(command.arguments !== undefined ? { arguments: command.arguments } : {}) };
|
|
53
|
+
const invocation = validatePreparedCommand(await (context.prepareCommand ?? prepareCommandInvocation)({
|
|
54
|
+
action,
|
|
55
|
+
config: context.config,
|
|
56
|
+
invocationKind: "task",
|
|
57
|
+
current: currentExecutionValues(document, context, commandEnvironment),
|
|
58
|
+
...(context.commandSourceLoader ? { sourceLoader: context.commandSourceLoader } : {}),
|
|
59
|
+
}), context);
|
|
60
|
+
return Object.freeze({ ...common, kind: "command", invocation });
|
|
61
|
+
}
|
|
62
|
+
const { qualified } = qualifyOwnedRef(target.ref, context);
|
|
63
|
+
if (target.kind === "command") {
|
|
64
|
+
// Unreachable from any parsed source: task source v4 accepts with: only
|
|
65
|
+
// on uses: akm/command. Kept as a seam invariant — this function takes a
|
|
66
|
+
// structurally-typed document.
|
|
67
|
+
if (document.target.with !== undefined) {
|
|
68
|
+
throw new UsageError("Command refs do not accept with; use akm/command with {ref, arguments} for portable arguments.", "COMPOSITION_INVALID");
|
|
69
|
+
}
|
|
70
|
+
const invocation = validatePreparedCommand(await (context.prepareCommand ?? prepareCommandInvocation)({
|
|
71
|
+
action: { ref: qualified },
|
|
72
|
+
config: context.config,
|
|
73
|
+
invocationKind: "task",
|
|
74
|
+
current: currentExecutionValues(document, context, commandEnvironment),
|
|
75
|
+
...(context.commandSourceLoader ? { sourceLoader: context.commandSourceLoader } : {}),
|
|
76
|
+
}), context);
|
|
77
|
+
return Object.freeze({ ...common, kind: "command", invocation });
|
|
78
|
+
}
|
|
79
|
+
if (target.kind === "workflow") {
|
|
80
|
+
// Stays reachable — task source v4 still has a top-level env: (P4-N4).
|
|
81
|
+
if (Object.keys(environment).length > 0) {
|
|
82
|
+
// P4 (docs/plans/specs/p4-deletions-closeout.md §5.5, row P-04): PRESERVED,
|
|
83
|
+
// not re-coded — tests/integration/tasks-with-classification-characterization.test.ts's
|
|
84
|
+
// P-04 block pins this exact code (CONVERT, not FLIP, per §7.2 F-A2.8:
|
|
85
|
+
// "the P-04 block ... stays reachable and stays pinned"). §5.2's target
|
|
86
|
+
// table predicted all 3 of this file's remaining sites → COMPOSITION_INVALID;
|
|
87
|
+
// this is the recorded deviation for the one site a preservation gate blocks.
|
|
88
|
+
throw new UsageError("Task workflow env cannot be consumed by the durable workflow runtime in 0.9.2; remove env or use a command target.", "INVALID_FLAG_VALUE");
|
|
89
|
+
}
|
|
90
|
+
const resolved = await resolvedOwnedAsset(qualified, "workflow", context);
|
|
91
|
+
validateWorkflowRuntimeSource(resolved.file, resolved.bundleRoot, context.readFile ?? ((targetPath) => fs.readFileSync(targetPath)));
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
...common,
|
|
94
|
+
kind: "workflow",
|
|
95
|
+
ref: qualified,
|
|
96
|
+
params: Object.freeze({ ...(document.target.with ?? {}) }),
|
|
97
|
+
...(document.akm?.maxSteps !== undefined ? { maxSteps: document.akm.maxSteps } : {}),
|
|
98
|
+
...(document.akm?.maxRetries !== undefined ? { maxRetries: document.akm.maxRetries } : {}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Unreachable from any parsed source: task source v4 accepts with: only on
|
|
102
|
+
// uses: akm/command. Kept as a seam invariant — this function takes a
|
|
103
|
+
// structurally-typed document.
|
|
104
|
+
if (document.target.with !== undefined) {
|
|
105
|
+
throw new UsageError("Script refs do not accept with.", "COMPOSITION_INVALID");
|
|
106
|
+
}
|
|
107
|
+
const resolved = await resolvedOwnedAsset(qualified, "script", context);
|
|
108
|
+
// Shared with prepare-script-target.ts's prepareScriptTarget() (spec §4.3):
|
|
109
|
+
// one implementation of the byte/interpreter capture, not two.
|
|
110
|
+
const captured = captureScriptTarget(qualified, resolved.file, resolved.bundleRoot, context.readFile ?? ((targetPath) => fs.readFileSync(targetPath)));
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
...common,
|
|
113
|
+
kind: "script",
|
|
114
|
+
sourceRef: qualified,
|
|
115
|
+
...captured,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
* The one shared implementation of frozen-script byte/interpreter capture
|
|
6
|
+
* (spec docs/plans/specs/p1b-model-extraction.md §4.3). Both prepare.ts's
|
|
7
|
+
* script arm (the moved prepareTaskV3Execution) and
|
|
8
|
+
* prepare-script-target.ts's typed prepareScriptTarget() call
|
|
9
|
+
* captureScriptTarget() below, so the two never drift into two copies of the
|
|
10
|
+
* same capture logic. scriptInterpreter() and captureDirectoryIdentity() are
|
|
11
|
+
* moved body-intact out of the pre-P1b src/tasks/runtime-v3.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { UsageError } from "../../core/errors.js";
|
|
16
|
+
import { captureFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
|
|
17
|
+
import { isBunStandaloneMain } from "../resolve-akm-bin.js";
|
|
18
|
+
const SCRIPT_INTERPRETERS = Object.freeze({
|
|
19
|
+
".sh": "sh",
|
|
20
|
+
".ts": "bun",
|
|
21
|
+
".js": "bun",
|
|
22
|
+
".ps1": "powershell",
|
|
23
|
+
".cmd": "cmd",
|
|
24
|
+
".bat": "cmd",
|
|
25
|
+
".py": "python",
|
|
26
|
+
".rb": "ruby",
|
|
27
|
+
".go": "go",
|
|
28
|
+
".pl": "perl",
|
|
29
|
+
".php": "php",
|
|
30
|
+
".lua": "lua",
|
|
31
|
+
".r": "rscript",
|
|
32
|
+
".swift": "swift",
|
|
33
|
+
".kt": "kotlin",
|
|
34
|
+
".kts": "kotlin",
|
|
35
|
+
});
|
|
36
|
+
export function scriptInterpreter(extension, ref) {
|
|
37
|
+
const interpreter = SCRIPT_INTERPRETERS[extension];
|
|
38
|
+
if (!interpreter) {
|
|
39
|
+
throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} has no closed runtime interpreter for extension ${JSON.stringify(extension)}.`, "TASK_TARGET_UNSUPPORTED");
|
|
40
|
+
}
|
|
41
|
+
if (interpreter !== "bun")
|
|
42
|
+
return interpreter;
|
|
43
|
+
if (!process.versions.bun) {
|
|
44
|
+
throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} requires Bun for ${extension} execution, but this runtime cannot provide it.`, "TASK_TARGET_UNSUPPORTED");
|
|
45
|
+
}
|
|
46
|
+
return isBunStandaloneMain() ? "bun-standalone" : "bun";
|
|
47
|
+
}
|
|
48
|
+
export function captureDirectoryIdentity(bundleRoot, workingDirectory) {
|
|
49
|
+
try {
|
|
50
|
+
return captureFrozenDirectoryIdentity(bundleRoot, workingDirectory);
|
|
51
|
+
}
|
|
52
|
+
catch (cause) {
|
|
53
|
+
if (cause instanceof UsageError)
|
|
54
|
+
throw cause;
|
|
55
|
+
throw new UsageError(`Task working directory ${JSON.stringify(workingDirectory ?? ".")} cannot be physically verified: ${cause instanceof Error ? cause.message : String(cause)}`, "TASK_SOURCE_INVALID");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read a script's bytes off disk (or the caller's own read seam) and freeze
|
|
60
|
+
* the interpreter/digest/directory-identity shape every script dispatch
|
|
61
|
+
* needs. `ref` is used only for interpreter-selection error messages — it is
|
|
62
|
+
* the script's own qualified ref, never a synthetic task ref. `bundleRoot` is
|
|
63
|
+
* where directory identity is captured from (one level above the script file
|
|
64
|
+
* itself, matching prepareTaskV3Execution's historical behavior).
|
|
65
|
+
*/
|
|
66
|
+
export function captureScriptTarget(ref, file, bundleRoot, readFile) {
|
|
67
|
+
const extension = path.extname(file).toLowerCase();
|
|
68
|
+
const raw = readFile(file, bundleRoot);
|
|
69
|
+
const bytes = Uint8Array.from(raw);
|
|
70
|
+
const cwdIdentity = captureDirectoryIdentity(bundleRoot);
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
interpreter: scriptInterpreter(extension, ref),
|
|
73
|
+
extension,
|
|
74
|
+
bytesBase64: Buffer.from(bytes).toString("base64"),
|
|
75
|
+
byteLength: bytes.byteLength,
|
|
76
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
77
|
+
cwd: cwdIdentity.realCwd,
|
|
78
|
+
cwdIdentity,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
* The task-attempt reservation/finalization lifecycle: `reserveTaskAttempt`
|
|
6
|
+
* (called before any dispatch), `finishAttempt` (the monotonic finish-time
|
|
7
|
+
* clamp every arm's result uses), and `recordTaskAttemptFailure` (the
|
|
8
|
+
* catch-all for a dispatch that threw after an attempt was reserved).
|
|
9
|
+
*
|
|
10
|
+
* Moved from src/tasks/runner.ts (spec docs/plans/specs/p1b-model-extraction.md
|
|
11
|
+
* §5.1, §9, runner.ts:970-1069). `reserveTaskAttempt` and `finishAttempt`
|
|
12
|
+
* were module-private at head; both are exported here because run-task.ts and
|
|
13
|
+
* every dispatch arm (run-native-task.ts, run-workflow-task.ts,
|
|
14
|
+
* run-command-task.ts), now separate files, call them.
|
|
15
|
+
*
|
|
16
|
+
* F-4 (P1a advisory, spec §5.5): `SAFE_TASK_ATTEMPT_ERROR_CODES` gains
|
|
17
|
+
* `TASK_SOURCE_INVALID` and `COMPOSITION_INVALID` — see the comment on the
|
|
18
|
+
* set for the verified reachability path. `SAFE_TASK_ATTEMPT_ERROR_CODES` and
|
|
19
|
+
* `safeTaskAttemptErrorCode` stay unexported: their membership is observed
|
|
20
|
+
* the same way production code observes it, through
|
|
21
|
+
* `recordTaskAttemptFailure`'s one externally-visible effect (the stored
|
|
22
|
+
* `detail.error` code).
|
|
23
|
+
*
|
|
24
|
+
* P4 (docs/plans/specs/p4-deletions-closeout.md §4.1, row B-55): the set
|
|
25
|
+
* additionally gains `TARGET_REF_INVALID`, `WORKFLOW_SOURCE_INVALID`,
|
|
26
|
+
* `INPUT_BINDING_INVALID`, and `TASK_TARGET_UNSUPPORTED` — every code P4's
|
|
27
|
+
* `INVALID_FLAG_VALUE` re-coding sweep (spec §5.2) makes reachable from the
|
|
28
|
+
* same post-reservation dispatch catch. `TARGET_REF_INVALID`
|
|
29
|
+
* (`execution/target-ref.ts`) and `INPUT_BINDING_INVALID`
|
|
30
|
+
* (`workflows/freeze/task-bindings.ts`) already reach it today through the
|
|
31
|
+
* same WORKFLOW arm as `COMPOSITION_INVALID` above — freezing a composed
|
|
32
|
+
* child (a `tasks/<ref>` or `workflows/<ref>` step) during dispatch can
|
|
33
|
+
* classify a malformed `uses:` or an invalid `with:` binding after the
|
|
34
|
+
* attempt is reserved. `WORKFLOW_SOURCE_INVALID` and `TASK_TARGET_UNSUPPORTED`
|
|
35
|
+
* get their first live throw sites in this same phase (the freeze wrapper's
|
|
36
|
+
* P4-N2 mapping, and `prepare/script-capture.ts`'s interpreter rejections,
|
|
37
|
+
* respectively) — added here ahead of / alongside that wiring rather than
|
|
38
|
+
* split across two commits, since an unreachable member of an allowlist Set
|
|
39
|
+
* is inert, not a defect.
|
|
40
|
+
*/
|
|
41
|
+
import { AkmError, rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
42
|
+
import { withStateDb } from "../../core/state-db.js";
|
|
43
|
+
import { reserveTaskHistoryAttempt } from "../../storage/repositories/task-history-repository.js";
|
|
44
|
+
import { validateTaskId } from "../task-id.js";
|
|
45
|
+
import { appendHistory } from "./task-history.js";
|
|
46
|
+
import { persistRunLog, resolveTaskLogPath } from "./task-log.js";
|
|
47
|
+
export const INVALID_TASK_ATTEMPT_ID = "_invalid-task-id";
|
|
48
|
+
/** Reserve a collision-free identity through state.db's existing unique index. */
|
|
49
|
+
export function reserveTaskAttempt(taskId, requestedStartedAt) {
|
|
50
|
+
try {
|
|
51
|
+
return withStateDb((db) => {
|
|
52
|
+
for (let offsetMs = 0;; offsetMs++) {
|
|
53
|
+
const startedAt = new Date(requestedStartedAt.getTime() + offsetMs);
|
|
54
|
+
const reserved = reserveTaskHistoryAttempt(db, {
|
|
55
|
+
task_id: taskId,
|
|
56
|
+
status: "active",
|
|
57
|
+
started_at: startedAt.toISOString(),
|
|
58
|
+
completed_at: null,
|
|
59
|
+
failed_at: null,
|
|
60
|
+
log_path: null,
|
|
61
|
+
target_kind: null,
|
|
62
|
+
target_ref: null,
|
|
63
|
+
metadata_json: JSON.stringify({ metadataVersion: 2, durationMs: 0, detail: null }),
|
|
64
|
+
});
|
|
65
|
+
if (reserved)
|
|
66
|
+
return { startedAt, historyReserved: true };
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
rethrowIfTestIsolationError(error);
|
|
72
|
+
// Attempt recording cannot prevent or replace task execution.
|
|
73
|
+
return { startedAt: requestedStartedAt, historyReserved: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Clamp a finish observation to never precede its own start (a mocked clock can otherwise report a negative duration). */
|
|
77
|
+
export function finishAttempt(startedAt, observedFinishedAt) {
|
|
78
|
+
return observedFinishedAt.getTime() < startedAt.getTime() ? new Date(startedAt) : observedFinishedAt;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Error codes safe to surface verbatim in a task-history `detail.error`
|
|
82
|
+
* (rather than the generic `"INTERNAL"`) — user-actionable configuration and
|
|
83
|
+
* usage failures, never an unclassified internal error that might leak
|
|
84
|
+
* implementation detail.
|
|
85
|
+
*
|
|
86
|
+
* F-4 (P1a advisory, spec §5.5): `TASK_SOURCE_INVALID` and
|
|
87
|
+
* `COMPOSITION_INVALID` join the allowlist. Verified reachability: the only
|
|
88
|
+
* caller of `recordTaskAttemptFailure` inside the runner is the
|
|
89
|
+
* post-reservation catch in run-task.ts; task-source parsing happens BEFORE
|
|
90
|
+
* reservation (load-task.ts), so the direct parse path never reaches it.
|
|
91
|
+
* Both codes reach it through the WORKFLOW arm instead: a workflow task whose
|
|
92
|
+
* plan freezes a `tasks/<ref>` step raises `TASK_SOURCE_INVALID` (via
|
|
93
|
+
* `taskDispatch`'s `parseTaskSource`/`parseTaskSourceV4Document` —
|
|
94
|
+
* `src/workflows/freeze/targets/task.ts`; `parseTaskV3Yaml` no longer exists
|
|
95
|
+
* in `src`, P4 deleted task v3 acceptance, spec
|
|
96
|
+
* docs/plans/specs/p4-deletions-closeout.md §3.2) or `COMPOSITION_INVALID`
|
|
97
|
+
* (the P1a with-rejection guard) DURING dispatch, after the attempt was
|
|
98
|
+
* already reserved. Before this widening, both were recorded as
|
|
99
|
+
* `"INTERNAL"`.
|
|
100
|
+
*/
|
|
101
|
+
const SAFE_TASK_ATTEMPT_ERROR_CODES = new Set([
|
|
102
|
+
"CONFIG_DIR_UNRESOLVABLE",
|
|
103
|
+
"STASH_DIR_NOT_FOUND",
|
|
104
|
+
"STASH_DIR_NOT_A_DIRECTORY",
|
|
105
|
+
"STASH_DIR_UNREADABLE",
|
|
106
|
+
"LLM_NOT_CONFIGURED",
|
|
107
|
+
"INVALID_CONFIG_FILE",
|
|
108
|
+
"UNSUPPORTED_CONFIG_VERSION",
|
|
109
|
+
"TEST_ISOLATION_MISSING",
|
|
110
|
+
"INVALID_FLAG_VALUE",
|
|
111
|
+
"MISSING_REQUIRED_ARGUMENT",
|
|
112
|
+
"PATH_ESCAPE_VIOLATION",
|
|
113
|
+
"TASK_SCHEMA_VERSION_UNSUPPORTED",
|
|
114
|
+
"ASSET_NOT_FOUND",
|
|
115
|
+
"WORKFLOW_NOT_FOUND",
|
|
116
|
+
"FILE_NOT_FOUND",
|
|
117
|
+
"TASK_SOURCE_INVALID",
|
|
118
|
+
"COMPOSITION_INVALID",
|
|
119
|
+
"TARGET_REF_INVALID",
|
|
120
|
+
"WORKFLOW_SOURCE_INVALID",
|
|
121
|
+
"INPUT_BINDING_INVALID",
|
|
122
|
+
"TASK_TARGET_UNSUPPORTED",
|
|
123
|
+
]);
|
|
124
|
+
function safeTaskAttemptErrorCode(failure) {
|
|
125
|
+
if (failure instanceof AkmError && SAFE_TASK_ATTEMPT_ERROR_CODES.has(failure.code))
|
|
126
|
+
return failure.code;
|
|
127
|
+
return "INTERNAL";
|
|
128
|
+
}
|
|
129
|
+
export function recordTaskAttemptFailure(input) {
|
|
130
|
+
let taskId = input.taskId;
|
|
131
|
+
try {
|
|
132
|
+
validateTaskId(taskId);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
taskId = INVALID_TASK_ATTEMPT_ID;
|
|
136
|
+
}
|
|
137
|
+
const attempt = input.historyReserved === undefined
|
|
138
|
+
? reserveTaskAttempt(taskId, input.startedAt)
|
|
139
|
+
: { startedAt: input.startedAt, historyReserved: input.historyReserved };
|
|
140
|
+
const finishedAt = finishAttempt(attempt.startedAt, input.finishedAt ?? new Date());
|
|
141
|
+
const startedAtIso = attempt.startedAt.toISOString();
|
|
142
|
+
const finishedAtIso = finishedAt.toISOString();
|
|
143
|
+
const errorCode = safeTaskAttemptErrorCode(input.failure);
|
|
144
|
+
const logPath = resolveTaskLogPath(input.logDir, taskId, startedAtIso);
|
|
145
|
+
const line = `[akm task] status=failed reason=${input.reason} code=${errorCode}`;
|
|
146
|
+
const result = {
|
|
147
|
+
id: taskId,
|
|
148
|
+
status: "failed",
|
|
149
|
+
startedAt: startedAtIso,
|
|
150
|
+
finishedAt: finishedAtIso,
|
|
151
|
+
durationMs: Math.max(0, finishedAt.getTime() - attempt.startedAt.getTime()),
|
|
152
|
+
log: logPath,
|
|
153
|
+
target: { kind: "unknown" },
|
|
154
|
+
detail: { reason: input.reason, error: errorCode },
|
|
155
|
+
};
|
|
156
|
+
persistRunLog({
|
|
157
|
+
taskId,
|
|
158
|
+
startedAtIso,
|
|
159
|
+
finishedAtIso,
|
|
160
|
+
logPath,
|
|
161
|
+
fileText: `${line}\n`,
|
|
162
|
+
dbLines: [{ level: "error", line }],
|
|
163
|
+
});
|
|
164
|
+
appendHistory(result, attempt.historyReserved);
|
|
165
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
* Load and project one task asset into immutable executable work: id
|
|
6
|
+
* validation, adapter detection, owner resolution, source read, strict task
|
|
7
|
+
* source parsing, config selection, bundle-name resolution, and the
|
|
8
|
+
* `prepareTaskV3Execution` call. Everything here is non-mutating — run-task.ts
|
|
9
|
+
* reserves a durable attempt only after this resolves.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
13
|
+
import { makeBundleRef } from "../../core/asset/asset-ref.js";
|
|
14
|
+
import { loadConfig } from "../../core/config/config.js";
|
|
15
|
+
import { NotFoundError } from "../../core/errors.js";
|
|
16
|
+
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
|
+
import { applyInputDefaults, materializeInputFlags, validateInputs, } from "../../execution/input-contract.js";
|
|
18
|
+
import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
|
|
19
|
+
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
20
|
+
import { prepareTaskV3Execution } from "../prepare/prepare.js";
|
|
21
|
+
import { scheduledTaskContextEnv } from "../scheduler-invocation.js";
|
|
22
|
+
import { parseTaskSource } from "../source/parse-task-source.js";
|
|
23
|
+
import { projectTaskSourceV4 } from "../source/project-v4.js";
|
|
24
|
+
import { TASK_INPUT_DIAGNOSTICS } from "../source/task-input-diagnostics.js";
|
|
25
|
+
import { validateTaskConceptId, validateTaskId } from "../task-id.js";
|
|
26
|
+
/**
|
|
27
|
+
* F-3 (spec §5.4): the literal bundle-name fallback, hoisted to a named
|
|
28
|
+
* constant — the VALUE does not change (it is user-visible data, R-09's
|
|
29
|
+
* observable behavior). The now-deleted `runner.ts:173`'s (P4) bare
|
|
30
|
+
* `"stash"` became this at the P1b runner.ts split.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_BUNDLE_NAME = "stash";
|
|
33
|
+
const CONFIG_FREE_TASK_RUNTIME = Object.freeze({
|
|
34
|
+
configVersion: "0.9.0",
|
|
35
|
+
semanticSearchMode: "off",
|
|
36
|
+
});
|
|
37
|
+
/** Resolve, parse, and project one task-v3 asset into a frozen, executable projection. */
|
|
38
|
+
export async function loadPreparedTask(id, options) {
|
|
39
|
+
const bundleDir = options.bundleDir;
|
|
40
|
+
const adapterId = options.adapterId ?? detectAdapterId(bundleDir);
|
|
41
|
+
if (adapterId === "akm-task")
|
|
42
|
+
validateTaskConceptId(id);
|
|
43
|
+
else
|
|
44
|
+
validateTaskId(id);
|
|
45
|
+
const taskConceptId = adapterId === "akm" ? `tasks/${id}` : id;
|
|
46
|
+
const owner = resolveAdapterConceptOwner(bundleDir, adapterId, taskConceptId);
|
|
47
|
+
if (!owner) {
|
|
48
|
+
throw new NotFoundError(`Task ${JSON.stringify(id)} was not found in the configured ${JSON.stringify(adapterId)} component.`, "ASSET_NOT_FOUND");
|
|
49
|
+
}
|
|
50
|
+
const filePath = owner.path;
|
|
51
|
+
const yaml = fs.readFileSync(filePath, "utf8");
|
|
52
|
+
const parsed = parseTaskSource({ yaml, filePath, workspaceRoot: bundleDir });
|
|
53
|
+
const source = projectTaskSourceV4(parsed.v4);
|
|
54
|
+
const requiresCommandConfig = source.target.kind === "uses" &&
|
|
55
|
+
(source.target.uses.kind === "builtin-command" || source.target.uses.kind === "command");
|
|
56
|
+
const config = requiresCommandConfig ? loadConfig() : CONFIG_FREE_TASK_RUNTIME;
|
|
57
|
+
const bundleName = options.bundleName ?? config.defaultBundle ?? DEFAULT_BUNDLE_NAME;
|
|
58
|
+
// P2a Lane C, Stage 2 (spec docs/plans/specs/p2a-task-source-v4.md §5.1):
|
|
59
|
+
// materialize akm task run's raw input flags (Stage 1,
|
|
60
|
+
// src/commands/tasks/tasks-cli.ts) against the task's own declared
|
|
61
|
+
// inputs: contract (empty when the document declares none, so any input
|
|
62
|
+
// flag on such a task fails UNKNOWN_FLAG — there is nothing declared to
|
|
63
|
+
// match against).
|
|
64
|
+
// materializeInputFlags already validates the flag-supplied values; a
|
|
65
|
+
// required input satisfied only by its own default (never supplied as a
|
|
66
|
+
// flag) needs the SEPARATE validateInputs call below, run after defaults
|
|
67
|
+
// are applied — materializeInputFlags returns {} immediately for zero
|
|
68
|
+
// flags, before ever checking `required` (spec §5.1, input-contract.ts's
|
|
69
|
+
// own header).
|
|
70
|
+
const inputContract = parsed.v4.inputs ?? {};
|
|
71
|
+
const materializedInputs = materializeInputFlags(inputContract, options.inputFlags ?? [], TASK_INPUT_DIAGNOSTICS);
|
|
72
|
+
const defaultedInputs = applyInputDefaults(inputContract, materializedInputs);
|
|
73
|
+
const requiredErrors = validateInputs(inputContract, defaultedInputs);
|
|
74
|
+
if (requiredErrors.length > 0)
|
|
75
|
+
throw TASK_INPUT_DIAGNOSTICS.contractViolation(requiredErrors);
|
|
76
|
+
const inputBindings = Object.entries(defaultedInputs).map(([name, value]) => Object.freeze({ kind: "literal", name, value }));
|
|
77
|
+
if (options.captureTaskInvocation) {
|
|
78
|
+
const invocation = Object.freeze({
|
|
79
|
+
taskRef: makeBundleRef(bundleName, taskConceptId),
|
|
80
|
+
caller: Object.freeze({ kind: "cli" }),
|
|
81
|
+
...(inputBindings.length > 0 ? { inputs: Object.freeze(inputBindings) } : {}),
|
|
82
|
+
});
|
|
83
|
+
options.captureTaskInvocation(invocation);
|
|
84
|
+
}
|
|
85
|
+
// P2b Lane B (spec docs/plans/specs/p2b-input-bindings.md §4.3, B-40): a
|
|
86
|
+
// task source v4 document's own declared `inputs:` deliver into a
|
|
87
|
+
// `uses: workflows/<ref>` target's child-run params through the EXISTING
|
|
88
|
+
// with-> params path (prepare.ts's workflow branch already reads
|
|
89
|
+
// `document.target.with`) — task source v4 never authors `with:` on a
|
|
90
|
+
// workflow target itself (task-source-v4.ts's own parser accepts `with:`
|
|
91
|
+
// only on `uses: akm/command`, source/task-source-v4.ts:329-333), so this
|
|
92
|
+
// override is purely additive for a workflow-target task.
|
|
93
|
+
const deliverySource = source.target.kind === "uses" && source.target.uses.kind === "workflow"
|
|
94
|
+
? { ...source, target: { ...source.target, with: defaultedInputs } }
|
|
95
|
+
: source;
|
|
96
|
+
return prepareTaskV3Execution(deliverySource, {
|
|
97
|
+
taskId: id,
|
|
98
|
+
taskRef: makeBundleRef(bundleName, taskConceptId),
|
|
99
|
+
bundleName,
|
|
100
|
+
bundleRoot: bundleDir,
|
|
101
|
+
config,
|
|
102
|
+
// Agent profiles build child env from an allowlist, so freeze the closed
|
|
103
|
+
// scheduler-restored AKM directory context before command preparation.
|
|
104
|
+
...(options.scheduled ? { schedulerContext: scheduledTaskContextEnv() } : {}),
|
|
105
|
+
resolveAsset: async ({ bundle, type, name }) => {
|
|
106
|
+
if (bundle === bundleName) {
|
|
107
|
+
return { file: await resolveAssetPath(bundleDir, type, name), bundleRoot: bundleDir };
|
|
108
|
+
}
|
|
109
|
+
const resolutionConfig = requiresCommandConfig ? config : loadConfig();
|
|
110
|
+
const resolvedBundle = resolveWriteTarget(resolutionConfig, bundle, { requireWritable: false });
|
|
111
|
+
return {
|
|
112
|
+
file: await resolveAssetPath(resolvedBundle.source.path, type, name),
|
|
113
|
+
bundleRoot: resolvedBundle.source.path,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
* Build the default task-run provenance context: `eventSource` is always
|
|
6
|
+
* `"task"` (D5-N1); `scheduled` carries the caller's own scheduled flag.
|
|
7
|
+
*/
|
|
8
|
+
export function createExecutionProvenanceContext(scheduled) {
|
|
9
|
+
return Object.freeze({ eventSource: "task", scheduled });
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the effective provenance for one `runTask()` call: an explicit
|
|
13
|
+
* `RunTaskOptions.provenance` always wins; absent, this is the default
|
|
14
|
+
* context (§5.2 "Threading") — what keeps every pre-P1b caller (and any
|
|
15
|
+
* future in-repo caller that never sets `provenance`) byte-equivalent to
|
|
16
|
+
* today's behavior.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveProvenanceContext(explicit, scheduled) {
|
|
19
|
+
return explicit ?? createExecutionProvenanceContext(scheduled);
|
|
20
|
+
}
|