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.
Files changed (143) hide show
  1. package/CHANGELOG.md +493 -0
  2. package/STABILITY.md +23 -5
  3. package/dist/assets/hints/cli-hints-full.md +12 -7
  4. package/dist/assets/tasks/core/extract.yml +3 -5
  5. package/dist/assets/tasks/core/improve.yml +3 -5
  6. package/dist/assets/tasks/core/index-refresh.yml +3 -5
  7. package/dist/assets/tasks/core/sync.yml +3 -5
  8. package/dist/assets/tasks/core/version-check.yml +3 -5
  9. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
  10. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
  12. package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
  13. package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
  14. package/dist/cli/unknown-flags.js +12 -1
  15. package/dist/cli.js +8 -1
  16. package/dist/commands/command/command-execution.js +23 -2
  17. package/dist/commands/health/improve-metrics.js +38 -0
  18. package/dist/commands/health/windows.js +8 -4
  19. package/dist/commands/health.js +8 -4
  20. package/dist/commands/lint/index.js +1 -1
  21. package/dist/commands/migrate-cli.js +130 -24
  22. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  23. package/dist/commands/tasks/explain.js +304 -0
  24. package/dist/commands/tasks/tasks-cli.js +185 -3
  25. package/dist/commands/tasks/tasks.js +265 -52
  26. package/dist/commands/workflow/plan.js +159 -0
  27. package/dist/commands/workflow-cli.js +94 -2
  28. package/dist/core/activation-policy.js +2 -12
  29. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  30. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  31. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  32. package/dist/core/errors.js +45 -0
  33. package/dist/core/json-schema.js +15 -5
  34. package/dist/core/state/migrations.js +57 -0
  35. package/dist/core/state-db.js +16 -14
  36. package/dist/core/subprocess.js +47 -13
  37. package/dist/execution/guarded-source.js +44 -0
  38. package/dist/execution/input-contract.js +250 -0
  39. package/dist/execution/target-ref.js +63 -0
  40. package/dist/indexer/usage/usage-events.js +14 -3
  41. package/dist/integrations/agent/execution-lowering.js +12 -1
  42. package/dist/output/shapes/passthrough.js +2 -0
  43. package/dist/output/text/helpers.js +1 -1
  44. package/dist/output/text/migrate.js +12 -3
  45. package/dist/output/text/workflow-format.js +192 -10
  46. package/dist/output/text/workflow.js +2 -1
  47. package/dist/runtime.js +1 -0
  48. package/dist/scripts/akm-migrate-node.js +11838 -10118
  49. package/dist/scripts/akm-migrate.js +11828 -10117
  50. package/dist/setup/steps/tasks.js +34 -17
  51. package/dist/storage/repositories/task-history-repository.js +5 -1
  52. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  53. package/dist/tasks/backends/launchd.js +31 -84
  54. package/dist/tasks/embedded.js +13 -7
  55. package/dist/tasks/model/invocation.js +4 -0
  56. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  57. package/dist/tasks/prepare/prepare-support.js +154 -0
  58. package/dist/tasks/prepare/prepare.js +117 -0
  59. package/dist/tasks/prepare/prepared-execution.js +4 -0
  60. package/dist/tasks/prepare/script-capture.js +80 -0
  61. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  62. package/dist/tasks/run/load-task.js +117 -0
  63. package/dist/tasks/run/provenance.js +20 -0
  64. package/dist/tasks/run/run-command-task.js +92 -0
  65. package/dist/tasks/run/run-native-task.js +222 -0
  66. package/dist/tasks/run/run-task.js +99 -0
  67. package/dist/tasks/run/run-workflow-task.js +222 -0
  68. package/dist/tasks/run/task-history.js +134 -0
  69. package/dist/tasks/run/task-log.js +179 -0
  70. package/dist/tasks/run/task-result.js +19 -0
  71. package/dist/tasks/scheduler-binding.js +66 -2
  72. package/dist/tasks/scheduler-invocation.js +63 -3
  73. package/dist/tasks/scheduler-sync.js +77 -14
  74. package/dist/tasks/source/bounded-document.js +455 -0
  75. package/dist/tasks/source/parse-task-source.js +59 -0
  76. package/dist/tasks/source/project-v4.js +62 -0
  77. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  78. package/dist/tasks/source/task-source-v4.js +626 -0
  79. package/dist/tasks/source-v3.js +10 -733
  80. package/dist/tasks/task-run-reserved-flags.js +79 -0
  81. package/dist/workflows/authoring/authoring.js +17 -8
  82. package/dist/workflows/exec/child-invocation.js +34 -0
  83. package/dist/workflows/exec/child-workflow.js +370 -0
  84. package/dist/workflows/exec/exec-unit.js +50 -170
  85. package/dist/workflows/exec/frozen-judge.js +19 -2
  86. package/dist/workflows/exec/native-executor.js +49 -27
  87. package/dist/workflows/exec/param-secrets.js +12 -0
  88. package/dist/workflows/exec/run-workflow.js +48 -59
  89. package/dist/workflows/exec/step-work.js +222 -80
  90. package/dist/workflows/exec/unit-dispatch.js +72 -0
  91. package/dist/workflows/freeze/child-output-references.js +94 -0
  92. package/dist/workflows/freeze/environment.js +174 -0
  93. package/dist/workflows/freeze/identity.js +22 -0
  94. package/dist/workflows/freeze/resolve-steps.js +78 -0
  95. package/dist/workflows/freeze/source-freeze.js +57 -0
  96. package/dist/workflows/freeze/step-values.js +68 -0
  97. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  98. package/dist/workflows/freeze/targets/command.js +81 -0
  99. package/dist/workflows/freeze/targets/script.js +57 -0
  100. package/dist/workflows/freeze/targets/shell.js +31 -0
  101. package/dist/workflows/freeze/targets/task.js +179 -0
  102. package/dist/workflows/freeze/task-bindings.js +180 -0
  103. package/dist/workflows/ir/compile.js +59 -11
  104. package/dist/workflows/ir/environment-v4.js +3 -3
  105. package/dist/workflows/ir/freeze-v4.js +41 -7
  106. package/dist/workflows/ir/params.js +58 -131
  107. package/dist/workflows/ir/plan-hash.js +3 -3
  108. package/dist/workflows/ir/schema-v4.js +246 -17
  109. package/dist/workflows/parser.js +74 -2
  110. package/dist/workflows/program/schema.js +5 -2
  111. package/dist/workflows/resource-limits.js +20 -0
  112. package/dist/workflows/runtime/plan-classifier.js +24 -7
  113. package/dist/workflows/runtime/run-outputs.js +103 -0
  114. package/dist/workflows/runtime/runs.js +114 -9
  115. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  116. package/dist/workflows/source-files.js +5 -5
  117. package/dist/workflows/source-ir/compare.js +17 -0
  118. package/dist/workflows/source-ir/compile.js +7 -3
  119. package/dist/workflows/source-ir/github-yaml.js +64 -17
  120. package/dist/workflows/source-ir/schema.js +69 -21
  121. package/dist/workflows/source-ir/semantics.js +7 -25
  122. package/dist/workflows/source-ir/triggers.js +79 -0
  123. package/dist/workflows/source-ir/uses.js +33 -7
  124. package/docs/migration/README.md +1 -1
  125. package/docs/migration/release-notes/0.9.2.md +87 -11
  126. package/docs/migration/release-notes/README.md +3 -2
  127. package/docs/migration/v0.8-to-v0.9.md +13 -11
  128. package/docs/migration/v0.9.0-troubleshooting.md +20 -13
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  130. package/docs/reference/README.md +1 -1
  131. package/docs/reference/cli.md +140 -46
  132. package/docs/reference/configuration.md +6 -5
  133. package/docs/reference/supported-formats.md +9 -5
  134. package/docs/reference/tasks.md +338 -75
  135. package/docs/reference/workflow-schema.md +290 -16
  136. package/docs/reference/workflows.md +57 -7
  137. package/package.json +1 -1
  138. package/schemas/akm-task.json +173 -118
  139. package/schemas/akm-workflow.json +28 -0
  140. package/dist/tasks/runner.js +0 -941
  141. package/dist/tasks/runtime-v3.js +0 -281
  142. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  143. 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,4 @@
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
+ export {};
@@ -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
+ }