akm-cli 0.9.11 → 0.9.12

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 (127) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/STABILITY.md +6 -1
  3. package/dist/assets/hints/cli-hints-full.md +1 -1
  4. package/dist/assets/improve-strategies/consolidate.json +1 -1
  5. package/dist/assets/improve-strategies/default.json +1 -1
  6. package/dist/assets/improve-strategies/thorough.json +1 -2
  7. package/dist/cli/shared.js +16 -4
  8. package/dist/cli.js +15 -13
  9. package/dist/commands/agent/agent-dispatch.js +8 -0
  10. package/dist/commands/command/execution-source-loader.js +25 -22
  11. package/dist/commands/command/portable-template.js +4 -26
  12. package/dist/commands/config-cli.js +10 -4
  13. package/dist/commands/env/env-binding.js +10 -3
  14. package/dist/commands/env/env-cli.js +7 -0
  15. package/dist/commands/env/secret-cli.js +15 -4
  16. package/dist/commands/health/checks.js +186 -71
  17. package/dist/commands/health.js +16 -4
  18. package/dist/commands/improve/distill/quality-gate.js +2 -2
  19. package/dist/commands/improve/distill.js +28 -12
  20. package/dist/commands/improve/execution.js +1 -2
  21. package/dist/commands/improve/extract.js +82 -56
  22. package/dist/commands/improve/improve-strategies.js +26 -8
  23. package/dist/commands/improve/improve.js +13 -0
  24. package/dist/commands/improve/preparation.js +9 -6
  25. package/dist/commands/improve/reflect.js +61 -77
  26. package/dist/commands/lint/index.js +3 -1
  27. package/dist/commands/migrate-cli.js +6 -4
  28. package/dist/commands/proposal/drain-policies.js +22 -2
  29. package/dist/commands/proposal/repository.js +4 -4
  30. package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
  31. package/dist/commands/proposal/validators/proposals.js +10 -19
  32. package/dist/commands/read/show.js +42 -31
  33. package/dist/commands/registry-cli.js +4 -2
  34. package/dist/commands/sources/init.js +4 -8
  35. package/dist/commands/sources/self-update.js +2 -2
  36. package/dist/commands/sources/source-clone.js +5 -7
  37. package/dist/commands/sources/sources-cli.js +3 -5
  38. package/dist/commands/tasks/tasks-cli.js +4 -12
  39. package/dist/commands/tasks/tasks.js +38 -35
  40. package/dist/commands/workflow-cli.js +17 -15
  41. package/dist/core/activation-policy.js +31 -3
  42. package/dist/core/adapter/execution-source.js +39 -11
  43. package/dist/core/asset/stash-meta.js +7 -41
  44. package/dist/core/common.js +8 -17
  45. package/dist/core/config/config-schema.js +3 -23
  46. package/dist/core/config/config-walker.js +50 -4
  47. package/dist/core/config/config.js +21 -5
  48. package/dist/core/config/legacy-source-shape-shim.js +79 -0
  49. package/dist/core/config/schema/embedding.js +2 -2
  50. package/dist/core/config/schema/engines.js +2 -2
  51. package/dist/core/config/schema/index-config.js +19 -21
  52. package/dist/core/config/schema/primitives.js +21 -10
  53. package/dist/core/config/schema/sources-bundles.js +1 -6
  54. package/dist/core/errors.js +2 -3
  55. package/dist/core/improve-types.js +17 -0
  56. package/dist/core/json-schema.js +1 -11
  57. package/dist/core/maintenance-barrier.js +17 -2
  58. package/dist/core/paths.js +12 -15
  59. package/dist/core/state/migrations.js +28 -0
  60. package/dist/core/state-db.js +28 -1
  61. package/dist/core/write-source.js +6 -6
  62. package/dist/indexer/bundle-identity-guard.js +3 -0
  63. package/dist/indexer/ensure-index.js +5 -0
  64. package/dist/indexer/indexer.js +11 -3
  65. package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
  66. package/dist/indexer/passes/metadata.js +16 -5
  67. package/dist/indexer/search/search-fields.js +1 -30
  68. package/dist/integrations/agent/engine-resolution.js +15 -1
  69. package/dist/integrations/agent/model-map.js +16 -10
  70. package/dist/integrations/agent/prompts.js +13 -6
  71. package/dist/integrations/lockfile.js +22 -7
  72. package/dist/llm/client.js +16 -0
  73. package/dist/llm/index-passes.js +3 -2
  74. package/dist/output/shapes/passthrough.js +9 -3
  75. package/dist/output/shapes.js +5 -3
  76. package/dist/output/text/workflow-format.js +8 -1
  77. package/dist/scripts/akm-migrate-node.js +1461 -1267
  78. package/dist/scripts/akm-migrate.js +1460 -1266
  79. package/dist/setup/setup.js +14 -21
  80. package/dist/sources/include.js +150 -20
  81. package/dist/sources/providers/git-install.js +14 -12
  82. package/dist/sources/providers/git-provider.js +3 -3
  83. package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
  84. package/dist/sources/website-url.js +12 -4
  85. package/dist/storage/engines/sqlite-migrations.js +40 -10
  86. package/dist/storage/like-pattern.js +7 -0
  87. package/dist/storage/repositories/extract-sessions-repository.js +23 -0
  88. package/dist/storage/repositories/index-connection.js +27 -10
  89. package/dist/storage/repositories/index-entry-schema.js +19 -2
  90. package/dist/storage/repositories/index-schema.js +30 -9
  91. package/dist/storage/repositories/proposals-repository.js +2 -1
  92. package/dist/storage/repositories/task-history-repository.js +14 -7
  93. package/dist/storage/repositories/workflow-runs-repository.js +15 -1
  94. package/dist/storage/sqlite-read-snapshot.js +11 -9
  95. package/dist/tasks/backends/cron.js +34 -5
  96. package/dist/tasks/backends/launchd.js +23 -26
  97. package/dist/tasks/backends/schtasks.js +50 -3
  98. package/dist/tasks/frozen-script.js +2 -0
  99. package/dist/tasks/prepare/prepare.js +2 -7
  100. package/dist/tasks/prepare/script-capture.js +38 -6
  101. package/dist/tasks/schedule.js +154 -13
  102. package/dist/tasks/source/task-source-v3-frozen.js +0 -1
  103. package/dist/tasks/source/task-source-v4.js +0 -1
  104. package/dist/workflows/exec/child-workflow.js +2 -3
  105. package/dist/workflows/exec/exec-unit.js +3 -4
  106. package/dist/workflows/exec/run-workflow.js +19 -10
  107. package/dist/workflows/exec/step-work.js +35 -56
  108. package/dist/workflows/freeze/resolve-steps.js +19 -11
  109. package/dist/workflows/freeze/source-freeze.js +7 -0
  110. package/dist/workflows/freeze/targets/child-workflow.js +12 -18
  111. package/dist/workflows/freeze/targets/command.js +14 -2
  112. package/dist/workflows/ir/environment-v4.js +4 -2
  113. package/dist/workflows/ir/freeze-v4.js +2 -5
  114. package/dist/workflows/ir/plan-hash.js +0 -3
  115. package/dist/workflows/ir/schema-v4.js +14 -9
  116. package/dist/workflows/ir/schema.js +1 -3
  117. package/dist/workflows/resource-limits.js +35 -48
  118. package/dist/workflows/runtime/plan-classifier.js +89 -41
  119. package/dist/workflows/runtime/run-outputs.js +1 -21
  120. package/dist/workflows/runtime/runs.js +76 -150
  121. package/dist/workflows/source-files.js +28 -54
  122. package/dist/workflows/source-ir/program.js +2 -2
  123. package/dist/workflows/source-ir/semantics.js +5 -23
  124. package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
  125. package/docs/reference/cli.md +74 -17
  126. package/package.json +1 -1
  127. package/schemas/akm-config.json +5 -10
@@ -28,10 +28,11 @@
28
28
  import { createHash } from "node:crypto";
29
29
  import { parseBundleRef } from "../../../core/asset/asset-ref.js";
30
30
  import { UsageError } from "../../../core/errors.js";
31
+ import { warn } from "../../../core/warn.js";
31
32
  import { GuardedExecutionSourceCollector } from "../../../execution/guarded-source.js";
32
33
  import { workflowParamContract } from "../../ir/params.js";
33
34
  import { canonicalJson, canonicalPlanJson } from "../../ir/plan-hash.js";
34
- import { utf8Bytes, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES } from "../../resource-limits.js";
35
+ import { utf8Bytes } from "../../resource-limits.js";
35
36
  import { loadWorkflowAsset } from "../../runtime/workflow-asset-loader.js";
36
37
  import { resolveOwnedAsset } from "../environment.js";
37
38
  import { declaredParamNames, earlierStepIds } from "../step-values.js";
@@ -62,15 +63,14 @@ function childWorkflowContentHash(fields) {
62
63
  * `freezeEnvironment`): literal `env:` values and `unit: {env: [...]}` refs.
63
64
  * An absent/empty `env:` is not authored and stays valid.
64
65
  */
65
- function assertNoStepEnvironment(stepId, childRef, source) {
66
+ function warnIfStepEnvironment(stepId, childRef, source) {
66
67
  const hasLiteralEnv = Object.keys(source.env ?? {}).length > 0;
67
68
  const hasEnvRefs = (source.unit?.env ?? []).length > 0;
68
69
  if (!hasLiteralEnv && !hasEnvRefs)
69
70
  return;
70
- throw new UsageError(`Workflow step ${stepId} cannot pass env: while composing ${childRef}: a child run carries its own frozen ` +
71
- `environment inside its own plan, so a parent-level env: on the composing step cannot be honored. Remove ` +
72
- `env: from this step, or move it into ${childRef}'s own source.`, "COMPOSITION_INVALID", "Remove the env: (or unit: env:) block from this step, or set those variables inside the child workflow's " +
73
- "own source — a composing step's environment is never delivered into a child run.");
71
+ warn(`Workflow step ${stepId} declares env: while composing ${childRef}: a child run carries its own frozen ` +
72
+ `environment inside its own plan, so this composing step's env: (or unit: env:) is not delivered into it ` +
73
+ `and has no effect. Set those variables inside ${childRef}'s own source instead.`);
74
74
  }
75
75
  /** A workflow entry of a composition `refPath` — the only entries a cycle can close through (§4.5: a task target can never itself be a task, so no task->task chain exists to close one). */
76
76
  function isWorkflowRef(ref) {
@@ -98,14 +98,8 @@ function assertNoCompositionCycle(stepId, childRef, refPath) {
98
98
  * total exactly as it was, matching every other freeze-time failure's
99
99
  * no-partial-effect shape.
100
100
  */
101
- function chargeEmbeddedBudget(stepId, childRef, childPlanBytes, budget) {
102
- const projected = budget.embeddedBytes + childPlanBytes;
103
- if (projected > WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES) {
104
- throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: the embedded child plans would total ${projected} ` +
105
- `bytes, over the ${WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES}-byte limit for one workflow run.`, "COMPOSITION_INVALID", "Reduce the number or size of workflows composed into this run — split the work across separate " +
106
- "top-level runs, or trim the composed children's own plans.");
107
- }
108
- budget.embeddedBytes = projected;
101
+ function chargeEmbeddedBudget(_stepId, _childRef, childPlanBytes, budget) {
102
+ budget.embeddedBytes += childPlanBytes;
109
103
  }
110
104
  export async function childWorkflowDispatch(input) {
111
105
  const { source, baseUnit, childRefInput, context, via, taskRef, authoredInputs } = input;
@@ -115,10 +109,10 @@ export async function childWorkflowDispatch(input) {
115
109
  const owned = await resolveOwnedAsset(childRefInput, "workflow", context);
116
110
  const childAsset = await loadWorkflowAsset(owned.ref);
117
111
  const childRef = childAsset.ref;
118
- // Code-review finding: an authored env: on the composing step has no
119
- // path to reach the child run and must reject, not vanish (see
120
- // assertNoStepEnvironment's doc comment).
121
- assertNoStepEnvironment(source.id, childRef, source);
112
+ // Issue 10: an authored env: on the composing step has no path to reach
113
+ // the child run warn rather than vanish silently (see
114
+ // warnIfStepEnvironment's doc comment).
115
+ warnIfStepEnvironment(source.id, childRef, source);
122
116
  // §4.2 step 2: the composition cycle check, before any child compilation.
123
117
  assertNoCompositionCycle(source.id, childRef, context.composition.refPath);
124
118
  const childDepth = context.composition.depth + 1;
@@ -2,7 +2,9 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { createHash } from "node:crypto";
5
+ import { parseBuiltinCommandAction } from "../../../commands/command/builtin-action.js";
5
6
  import { prepareCommandInvocation } from "../../../commands/command/command-execution.js";
7
+ import { PORTABLE_ARGUMENTS_PLACEHOLDER } from "../../../commands/command/portable-template.js";
6
8
  import { captureFrozenDirectoryIdentity } from "../../../execution/directory-identity.js";
7
9
  import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
8
10
  import { canonicalResolvedExecutionRequest, } from "../../../execution/resolved-request.js";
@@ -13,15 +15,25 @@ import { prepareInlineExecution } from "../../../integrations/agent/inline-execu
13
15
  import { freezeEnvironment, guardedExecutionSource } from "../environment.js";
14
16
  import { gitIdentity } from "../identity.js";
15
17
  import { durableRequest, executionUnitValues, executionValues, targetConcurrency, } from "../step-values.js";
18
+ function inlineWorkflowCommandAction(action, commandMode) {
19
+ if (commandMode !== "portable-template")
20
+ return action;
21
+ const parsed = parseBuiltinCommandAction(action);
22
+ if (parsed.kind !== "inline")
23
+ return action;
24
+ return { content: parsed.content.split(PORTABLE_ARGUMENTS_PLACEHOLDER).join(parsed.arguments ?? "") };
25
+ }
16
26
  export async function commandDispatch(source, baseUnit, action, context) {
17
27
  const prepared = await prepareCommandInvocation({
18
- action,
28
+ action: inlineWorkflowCommandAction(action, source.commandMode),
19
29
  config: context.config,
20
30
  invocationKind: "workflow",
21
31
  ...(context.sourceIr.defaults
22
32
  ? { invocationDefaults: executionUnitValues(context.sourceIr.defaults, context.asset.sourcePath) }
23
33
  : {}),
24
- ...(source.commandMode === "literal" ? { inlineContentMode: "literal" } : {}),
34
+ ...(source.commandMode === "literal" || source.commandMode === "portable-template"
35
+ ? { inlineContentMode: "literal" }
36
+ : {}),
25
37
  current: executionValues(source, context.asset.sourcePath),
26
38
  sourceLoader: (ref, kind) => guardedExecutionSource(ref, kind, context),
27
39
  });
@@ -16,6 +16,7 @@ import dotenv from "dotenv";
16
16
  import { assetPathForName } from "../../core/asset/asset-placement.js";
17
17
  import { isWithin } from "../../core/common.js";
18
18
  import { NotFoundError, UsageError } from "../../core/errors.js";
19
+ import { warn } from "../../core/warn.js";
19
20
  import { captureGuardedExecutionSource, GuardedExecutionSourceCollector } from "../../execution/guarded-source.js";
20
21
  const SECRET_TOKEN_RE = /\$\{secret:([A-Za-z0-9_./-]+)\}/g;
21
22
  const ENVIRONMENT_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -33,7 +34,7 @@ export function freezeWorkflowEnvironment(refs, options) {
33
34
  throw new UsageError(`Workflow env ref ${JSON.stringify(inputRef)} did not resolve to a canonical fully-qualified owner.`, "WORKFLOW_SOURCE_INVALID");
34
35
  }
35
36
  if (logical.has(resolved.ref)) {
36
- throw new UsageError(`Workflow environment contains duplicate ref ${resolved.ref}.`, "WORKFLOW_SOURCE_INVALID");
37
+ continue;
37
38
  }
38
39
  logical.add(resolved.ref);
39
40
  trackParentDirectories(collector, resolved.root, resolved.path);
@@ -48,7 +49,8 @@ export function freezeWorkflowEnvironment(refs, options) {
48
49
  const physicalKey = `${captured.containmentPhysicalIdentity}\0${captured.physicalIdentity}`;
49
50
  const alias = physical.get(physicalKey);
50
51
  if (alias !== undefined && alias !== resolved.ref) {
51
- throw new UsageError(`${resolved.ref} aliases the same physical environment source as ${alias} under a different logical owner.`, "RESOURCE_ALREADY_EXISTS");
52
+ warn(`Workflow env ref ${resolved.ref} aliases the same physical environment source as ${alias} under a different logical owner. Using ${alias}.`);
53
+ continue;
52
54
  }
53
55
  physical.set(physicalKey, resolved.ref);
54
56
  const parsed = dotenv.parse(Buffer.from(captured.bytesBase64, "base64"));
@@ -51,11 +51,8 @@ export async function compileResolveFreezeWorkflowV4(asset, config, options = {}
51
51
  throw new UsageError(compiled.errors.map((error) => `${asset.path}:${error.line}: ${error.message}`).join("\n"), code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined);
52
52
  }
53
53
  const steps = compiled.plan.steps.map((step) => {
54
- const frozenJudge = step.gate.criteria.length === 0 ? null : resolved.judges.get(step.stepId);
55
- if (step.gate.criteria.length > 0 && !frozenJudge) {
56
- throw new Error(`resolved workflow judge missing for step ${step.stepId}`);
57
- }
58
- const gate = Object.freeze({ ...step.gate, maxLoops: step.gate.maxLoops ?? 1, frozenJudge: frozenJudge ?? null });
54
+ const frozenJudge = step.gate.criteria.length === 0 ? null : (resolved.judges.get(step.stepId) ?? null);
55
+ const gate = Object.freeze({ ...step.gate, maxLoops: step.gate.maxLoops ?? 1, frozenJudge });
59
56
  if (!step.root) {
60
57
  const { root: _root, ...withoutRoot } = step;
61
58
  return Object.freeze({ ...withoutRoot, gate });
@@ -13,7 +13,6 @@
13
13
  * Pure module: no IO beyond node:crypto, no engine imports.
14
14
  */
15
15
  import { createHash } from "node:crypto";
16
- import { utf8Bytes, WORKFLOW_MAX_PLAN_BYTES } from "../resource-limits.js";
17
16
  import { decodeWorkflowPlanV4, WORKFLOW_IR_V5_VERSION } from "./schema-v4.js";
18
17
  /** sha256 hex of the canonical (recursively sorted-keys) JSON of the plan. */
19
18
  export function computePlanHash(plan) {
@@ -29,8 +28,6 @@ export function canonicalJson(value) {
29
28
  }
30
29
  /** Decode, require stored canonical bytes, then verify the stored SHA-256. */
31
30
  export function decodeCanonicalPlan(runId, planJson, planHash, expectedVersion) {
32
- if (utf8Bytes(planJson) > WORKFLOW_MAX_PLAN_BYTES)
33
- throw new Error(`Workflow run ${runId} frozen plan exceeds the 2 MiB resource limit.`);
34
31
  let parsed;
35
32
  try {
36
33
  parsed = JSON.parse(planJson);
@@ -19,7 +19,7 @@ import { decodeExecutionSourceIdentity } from "../../execution/source.js";
19
19
  import { decodeFrozenRunnerSpec } from "../../integrations/agent/execution-lowering.js";
20
20
  import { parseReference } from "../program/expressions.js";
21
21
  import { PROGRAM_PARAM_NAME_PATTERN } from "../program/schema.js";
22
- import { utf8Bytes, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES } from "../resource-limits.js";
22
+ import { utf8Bytes } from "../resource-limits.js";
23
23
  import { decodeWorkflowExecSpec, validateWorkflowPlanStructure, } from "./schema.js";
24
24
  export const WORKFLOW_IR_V5_VERSION = 5;
25
25
  /**
@@ -152,8 +152,18 @@ function decodeGateV4(value, stepId, requiredSources) {
152
152
  frozenJudge: null,
153
153
  });
154
154
  }
155
- if (!Object.hasOwn(gate, "frozenJudge") || gate.frozenJudge === null) {
156
- fail(`gate ${stepId} with criteria requires a frozen judge target`);
155
+ if (!Object.hasOwn(gate, "frozenJudge")) {
156
+ fail(`gate ${stepId} with criteria is missing its frozenJudge field`);
157
+ }
158
+ if (gate.frozenJudge === null) {
159
+ return Object.freeze({
160
+ kind: "gate",
161
+ id: gate.id,
162
+ stepId,
163
+ criteria,
164
+ maxLoops: gate.maxLoops,
165
+ frozenJudge: null,
166
+ });
157
167
  }
158
168
  const identity = {
159
169
  kind: "unit",
@@ -239,12 +249,7 @@ function decodeChildWorkflowTarget(target, unit, depth, budget) {
239
249
  if (actualPlanHash !== planHash) {
240
250
  fail(`unit ${unit.id} child workflow embedded plan does not match its frozen planHash`);
241
251
  }
242
- const projectedBytes = budget.embeddedBytes + utf8Bytes(embeddedPlanJson);
243
- if (projectedBytes > WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES) {
244
- fail(`unit ${unit.id} child workflow ${target.ref} embedded plans total ${projectedBytes} bytes, over the ` +
245
- `${WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES}-byte limit`);
246
- }
247
- budget.embeddedBytes = projectedBytes;
252
+ budget.embeddedBytes += utf8Bytes(embeddedPlanJson);
248
253
  return Object.freeze({
249
254
  kind: "child-workflow",
250
255
  ref: target.ref,
@@ -5,7 +5,7 @@ import { isContainedRelativePath, isRecord } from "../../core/common.js";
5
5
  import { UsageError } from "../../core/errors.js";
6
6
  import { parseReference } from "../program/expressions.js";
7
7
  import { PROGRAM_PARAM_NAME_PATTERN, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN } from "../program/schema.js";
8
- import { jsonBytes, utf8Bytes, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_PLAN_BYTES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_TIMEOUT_MS, } from "../resource-limits.js";
8
+ import { jsonBytes, utf8Bytes, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_TIMEOUT_MS, } from "../resource-limits.js";
9
9
  // Shared dispatch-significant bounds now live in `../resource-limits` so the
10
10
  // parser, the published JSON Schema, and this decoder enforce identical
11
11
  // values. Re-exported here for existing importers (e.g. `commands/workflow-cli.ts`).
@@ -18,8 +18,6 @@ export function validateWorkflowPlanStructure(input, options) {
18
18
  fail(`irVersion must be ${options.expectedVersion}`);
19
19
  }
20
20
  assertJson(input);
21
- if (jsonBytes(input) > WORKFLOW_MAX_PLAN_BYTES)
22
- fail("plan exceeds the 2 MiB resource limit");
23
21
  const plan = input;
24
22
  assertKeys(input, ["irVersion", "title", "params", "paramSchemas", "budget", "execution", "steps", ...(options.planExtraKeys ?? [])], "plan");
25
23
  assertString(plan.title, "title");
@@ -2,7 +2,6 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { EXECUTION_MAX_TIMEOUT_MS } from "../execution/limits.js";
5
- export const WORKFLOW_MAX_PLAN_BYTES = 2 * 1024 * 1024;
6
5
  export const WORKFLOW_MAX_SOURCE_BYTES = 1024 * 1024;
7
6
  export const WORKFLOW_MAX_INSTRUCTION_BYTES = 256 * 1024;
8
7
  export const WORKFLOW_MAX_SCHEMA_BYTES = 256 * 1024;
@@ -73,24 +72,22 @@ export const DEFAULT_EXEC_TIMEOUT_MS = 600_000;
73
72
  * corrupt every downstream reference. That is the residual failure the cap
74
73
  * genuinely justifies.
75
74
  *
76
- * 8 MiB is deliberately generous the whole-row evidence cap
77
- * ({@link WORKFLOW_MAX_EVIDENCE_JSON_BYTES}), so any output that could survive
78
- * persistence intact fits many times over, and an ordinary full test/build log
79
- * is nowhere near it.
75
+ * 8 MiB is deliberately generous, so any output a step is likely to produce
76
+ * fits many times over and an ordinary full test/build log is nowhere near
77
+ * it.
80
78
  */
81
79
  export const WORKFLOW_MAX_EXEC_OUTPUT_BYTES = 8 * 1024 * 1024;
82
80
  /**
83
81
  * Marker stamped on an exec artifact that was RETAINED ONLY IN PART because the
84
82
  * command wrote past {@link WORKFLOW_MAX_EXEC_OUTPUT_BYTES}.
85
83
  *
86
- * Deliberately ugly and unique, exactly like `WORKFLOW_EVIDENCE_TRUNCATED_MARKER`
87
- * (`runtime/runs.ts`) the same idiom for the same reason: a truncated value
88
- * must NEVER be mistakable for a complete one by a downstream
89
- * `steps.<id>.output` reference, by a gate judge, by `akm workflow status`, or
90
- * by a human reading the row. The artifact is TEXT here rather than a JSON
91
- * value, so the marker is appended as a trailing block instead of replacing the
92
- * value with an envelope: the retained prefix is still genuinely useful (it is
93
- * the head of a real log), and the block says exactly how much is missing.
84
+ * Deliberately ugly and unique: a truncated value must NEVER be mistakable
85
+ * for a complete one by a downstream `steps.<id>.output` reference, by a gate
86
+ * judge, by `akm workflow status`, or by a human reading the row. The
87
+ * artifact is TEXT here rather than a JSON value, so the marker is appended
88
+ * as a trailing block instead of replacing the value with an envelope: the
89
+ * retained prefix is still genuinely useful (it is the head of a real log),
90
+ * and the block says exactly how much is missing.
94
91
  */
95
92
  export const WORKFLOW_EXEC_OUTPUT_TRUNCATED_MARKER = "__akm_exec_output_truncated__";
96
93
  // Per-var: Win32 `SetEnvironmentVariable` caps one variable at 32 767 UTF-16
@@ -143,27 +140,17 @@ export function clip(text, max) {
143
140
  return text.length > max ? `${text.slice(0, max)}…` : text;
144
141
  }
145
142
  // ── Persistence bounds ───────────────────────────────────────────────────────
146
- /**
147
- * Max serialized size of one `workflow_run_steps.evidence_json` row value.
148
- *
149
- * The promoted step artifact (`evidence.output`) is deliberately NOT clipped
150
- * when it is built gates judge the full artifact and downstream
151
- * `steps.<id>.output` references need it intact but a `collect` reducer
152
- * over an unbounded fan-out (each unit contributing up to a full unit
153
- * result) would otherwise write an unbounded blob into a single SQLite row.
154
- * Persistence is therefore bounded here, at the write boundary, by
155
- * `clipStepEvidenceForPersistence` (`runtime/runs.ts`), which replaces
156
- * oversized values with an explicitly-marked truncation envelope rather than
157
- * silently shortening them.
158
- *
159
- * 1 MiB is deliberately generous: it is 4× the per-instruction cap and half the
160
- * whole-plan cap, so no realistic authored workflow reaches it, while a runaway
161
- * fan-out is still bounded to something SQLite and `akm workflow status` can
162
- * handle.
163
- */
164
- export const WORKFLOW_MAX_EVIDENCE_JSON_BYTES = 1024 * 1024;
165
- /** Chars of the original value retained (as a marked preview) in a truncation envelope. */
166
- export const WORKFLOW_EVIDENCE_TRUNCATION_PREVIEW_CHARS = 1000;
143
+ //
144
+ // `workflow_run_steps.evidence_json` is persisted WHOLE and unclipped. It used
145
+ // to be capped at 1 MiB, past which the row was replaced by a marked
146
+ // truncation envelope but the run still looked successful, and the NEXT
147
+ // invocation (a resume, or any later step referencing the artifact) failed
148
+ // permanently: the value was gone from the only place a resumed run can read
149
+ // it from, so every prior paid step had to be re-run from scratch. SQLite has
150
+ // no practical row-size problem here (its own ceiling is ~1 GB), so there is
151
+ // nothing this cap protected that a full write does not already handle
152
+ // correctly. `JSON.stringify` throwing on genuinely unserializable evidence
153
+ // is unchanged.
167
154
  export function utf8Bytes(value) {
168
155
  return Buffer.byteLength(value, "utf8");
169
156
  }
@@ -173,21 +160,21 @@ export function jsonBytes(value) {
173
160
  // ── Recursive child-workflow composition bounds (spec docs/plans/specs/
174
161
  // ── p3a-plan-v5-child-freeze.md §4.5, A-N6) ──────────────────────────────────
175
162
  //
176
- // Enforced ONCE, at freeze, before publication, in
177
- // `src/workflows/freeze/targets/child-workflow.ts` — the ONE resolver both the
178
- // direct `uses: workflows/<ref>` form and the task-wrapped form route through
179
- // — and re-enforced as a corruption gate whenever a parent plan is DECODED
180
- // (`src/workflows/ir/schema-v4.ts`'s recursive `decodeChildWorkflowTarget`).
181
- // Full design history, including the rejected alternative for the byte cap:
182
- // docs/architecture/decisions/0007-workflow-composition-bounds.md.
183
- //
184
163
  // Composition DEPTH is unbounded — `assertNoCompositionCycle` (freeze/targets/
185
164
  // child-workflow.ts) already makes infinite composition mathematically
186
165
  // impossible, so a depth ceiling on top of it only bounded how many
187
166
  // legitimately distinct workflows an author could nest.
188
- /**
189
- * Max AGGREGATE canonical-JSON bytes of every embedded child plan in ONE root
190
- * freeze (the sum across the whole composition tree, not per child).
191
- * Deliberately HALF of {@link WORKFLOW_MAX_PLAN_BYTES} — see ADR 0007.
192
- */
193
- export const WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES = 1024 * 1024;
167
+ //
168
+ // This section used to also cap the AGGREGATE canonical-JSON bytes of every
169
+ // embedded child plan in one root freeze (`WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES`,
170
+ // enforced in `freeze/targets/child-workflow.ts` and re-checked on every
171
+ // decode in `ir/schema-v4.ts`'s `decodeChildWorkflowTarget`), alongside a
172
+ // standalone cap on any one frozen plan's own bytes
173
+ // (`WORKFLOW_MAX_PLAN_BYTES`, enforced in `ir/plan-hash.ts` and
174
+ // `ir/schema.ts`). A large plan is not a wrong plan: composing several
175
+ // substantial workflows together is a legitimate, deliberate authoring
176
+ // choice, and SQLite has no practical row-size problem here (its own
177
+ // ceiling is ~1 GB) — there was nothing left for either cap to protect that
178
+ // `planHash`/`contentHash` verification does not already cover. Both caps
179
+ // are removed; the hash/canonical-JSON integrity checks they sat next to
180
+ // are unchanged.
@@ -2,6 +2,7 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { UsageError } from "../../core/errors.js";
5
+ import { warnOnce } from "../../core/warn.js";
5
6
  import { decodeCanonicalPlan } from "../ir/plan-hash.js";
6
7
  import { WORKFLOW_IR_V5_VERSION } from "../ir/schema-v4.js";
7
8
  /** Validate that a live run carries exactly the current frozen-plan format. */
@@ -21,31 +22,33 @@ export function classifyWorkflowRunPlan(row) {
21
22
  support: "unsupported-version",
22
23
  irVersion: row.plan_ir_version,
23
24
  // §3.2's exact complete-or-abandon policy string (A-N2): pre-irVersion-5
24
- // plans keep status/list/abandon working but can no longer execute.
25
- error: `Workflow run ${runId} was frozen as workflow plan irVersion ${row.plan_ir_version}; pre-irVersion-5 ` +
26
- `plans cannot execute after the 0.9.2 upgrade. Complete them before upgrading, or run ` +
27
- `'akm workflow abandon ${runId}' and start a new run from the authored workflow. ` +
28
- `'akm workflow status' and 'akm workflow list' still work on this run.`,
29
- };
30
- }
31
- if (row.plan_ir_version !== WORKFLOW_IR_V5_VERSION) {
32
- return {
33
- support: "corrupt-plan",
34
- irVersion: null,
35
- error: `Workflow run ${runId} does not declare a supported workflow IR version.`,
25
+ // plans keep status/list/abandon working but can no longer execute. A
26
+ // version ABOVE the current one (#919) is a distinct situation never
27
+ // an "upgrade" problem so it gets its own text below rather than
28
+ // being folded into the pre-5 wording.
29
+ error: row.plan_ir_version < WORKFLOW_IR_V5_VERSION
30
+ ? // Issue 8: leads with the remedy available to a user who has
31
+ `Workflow run ${runId} was frozen as workflow plan irVersion ${row.plan_ir_version}; pre-irVersion-5 ` +
32
+ `plans cannot execute after the 0.9.2 upgrade. Run 'akm workflow abandon ${runId}' and start a new ` +
33
+ `run from the authored workflow to continue. 'akm workflow status' and 'akm workflow list' still ` +
34
+ `work on this run.`
35
+ : `Workflow run ${runId} was frozen with workflow plan irVersion ${row.plan_ir_version}, which this akm ` +
36
+ `(irVersion ${WORKFLOW_IR_V5_VERSION}) does not understand; it was probably written by a newer akm. ` +
37
+ `Complete it with that akm version, or run 'akm workflow abandon ${runId}' and start a new run from ` +
38
+ `the authored workflow. 'akm workflow status' and 'akm workflow list' still work on this run.`,
36
39
  };
37
40
  }
38
41
  try {
39
42
  return {
40
43
  support: "supported",
41
- irVersion: row.plan_ir_version,
44
+ irVersion: WORKFLOW_IR_V5_VERSION,
42
45
  plan: decodeCanonicalPlan(runId, row.plan_json, row.plan_hash, row.plan_ir_version),
43
46
  };
44
47
  }
45
48
  catch (cause) {
46
49
  return {
47
50
  support: "corrupt-plan",
48
- irVersion: row.plan_ir_version,
51
+ irVersion: row.plan_ir_version ?? null,
49
52
  error: cause instanceof Error ? cause.message : String(cause),
50
53
  };
51
54
  }
@@ -80,37 +83,82 @@ export function frozenStepRows(plan) {
80
83
  sequenceIndex: step.sequenceIndex,
81
84
  }));
82
85
  }
83
- /** Verify the durable spine still agrees with the decoded/hash-verified plan before any mutation. */
84
- export function assertWorkflowSpineMatchesPlan(plan, run, rows) {
85
- const expected = frozenStepRows(plan);
86
+ /**
87
+ * Verify the durable spine's STEP IDENTITY still agrees with the
88
+ * decoded/hash-verified plan: the same number of steps, and the same set of
89
+ * step ids. A published plan's own step ids are fixed forever at freeze
90
+ * time, so a mismatch here means the row set itself is wrong — genuine
91
+ * corruption, not something a later akm release could have caused by
92
+ * changing how a field is FORMATTED (that is
93
+ * {@link reconcileWorkflowSpineWithPlan}'s concern, issue 7).
94
+ */
95
+ function assertSpineIdentityMatchesPlan(runId, expected, rows) {
86
96
  if (rows.length !== expected.length)
87
- corruptSpine(run.id, "step count differs from the frozen plan");
88
- for (let index = 0; index < expected.length; index++) {
89
- const actual = rows[index];
90
- const planned = expected[index];
91
- // The length check above (corruptSpine returns `never`) guarantees both are
92
- // present; the guard narrows them and preserves the "missing row" message.
93
- if (!actual || !planned) {
94
- corruptSpine(run.id, `step row ${index} differs from the frozen plan (missing row)`);
95
- }
96
- if (actual.step_id !== planned.stepId ||
97
- actual.step_title !== planned.stepTitle ||
98
- actual.instructions !== planned.instructions ||
99
- actual.completion_json !== planned.completionJson ||
100
- actual.sequence_index !== planned.sequenceIndex) {
101
- const fields = [
102
- actual.step_id !== planned.stepId ? "step_id" : "",
103
- actual.step_title !== planned.stepTitle ? "step_title" : "",
104
- actual.instructions !== planned.instructions ? "instructions" : "",
105
- actual.completion_json !== planned.completionJson ? "completion_json" : "",
106
- actual.sequence_index !== planned.sequenceIndex ? "sequence_index" : "",
107
- ].filter(Boolean);
108
- corruptSpine(run.id, `step row ${index} differs from the frozen plan (${fields.join(", ")})`);
97
+ corruptSpine(runId, "step count differs from the frozen plan");
98
+ const expectedIds = new Set(expected.map((step) => step.stepId));
99
+ for (const row of rows) {
100
+ if (!expectedIds.has(row.step_id))
101
+ corruptSpine(runId, `step "${row.step_id}" is not in the frozen plan`);
102
+ }
103
+ }
104
+ /**
105
+ * Reconcile the durable spine's PURE-DERIVATION fields (title, instructions,
106
+ * completion criteria, sequence position) against the plan (issue 7).
107
+ *
108
+ * These are recomputed from the plan by {@link frozenStepRows} on every
109
+ * read; a later akm release changing how one of them is FORMATTED from the
110
+ * SAME plan data used to mark every in-flight run from the previous release
111
+ * "corrupt" the moment anything (`akm workflow status`, `resume`, a step
112
+ * completion) touched it. A mismatch here is warned about, once per run,
113
+ * rather than blocking the caller — never step identity (row count, which
114
+ * step ids exist), which stays a hard failure in
115
+ * {@link assertSpineIdentityMatchesPlan} because the plan cannot have
116
+ * produced a different step id for an already-frozen run.
117
+ *
118
+ * The durable row not the plan is what a driving agent actually acts
119
+ * on: `getNextWorkflowStep` reads `instructions` straight off this row
120
+ * (`toWorkflowRunStepState`/`projectNextResult` in `runtime/runs.ts`), and
121
+ * `exec/run-workflow.ts`/`exec/step-work.ts` dispatch from that result. So
122
+ * warning and proceeding with the STORED row (rather than rewriting it to
123
+ * match the plan's current formatting) is the conservative choice: the
124
+ * durable spine is the contract this run has been executing against since
125
+ * freeze, and a newer akm formatting the same plan data differently should
126
+ * not retroactively change an in-flight run's instructions out from under
127
+ * it mid-execution. This module has no write path for that anyway
128
+ * (`workflow_run_steps` is owned by
129
+ * `storage/repositories/workflow-runs-repository.ts`) — and it should not
130
+ * gain one for this purpose. Step
131
+ * identity (row count, which step ids exist) is the part that would
132
+ * actually desynchronize execution from the plan, which is exactly why
133
+ * {@link assertSpineIdentityMatchesPlan} keeps that a hard failure.
134
+ */
135
+ export function reconcileWorkflowSpineWithPlan(plan, run, rows) {
136
+ const expected = frozenStepRows(plan);
137
+ assertSpineIdentityMatchesPlan(run.id, expected, rows);
138
+ const expectedById = new Map(expected.map((step) => [step.stepId, step]));
139
+ const drifted = [];
140
+ for (const row of rows) {
141
+ const planned = expectedById.get(row.step_id);
142
+ if (!planned)
143
+ continue; // unreachable after assertSpineIdentityMatchesPlan; kept defensive.
144
+ if (row.step_title !== planned.stepTitle ||
145
+ row.instructions !== planned.instructions ||
146
+ row.completion_json !== planned.completionJson ||
147
+ row.sequence_index !== planned.sequenceIndex) {
148
+ drifted.push(row.step_id);
109
149
  }
110
150
  }
111
- if (run.current_step_id !== null && !expected.some((step) => step.stepId === run.current_step_id))
112
- corruptSpine(run.id, `current step ${run.current_step_id} is not in the frozen plan`);
151
+ if (drifted.length > 0) {
152
+ warnOnce(`workflow-spine-drift:${run.id}`, `Workflow run ${run.id}: durable step row(s) [${drifted.join(", ")}] no longer match the frozen plan's ` +
153
+ "title/instructions/completion-criteria/sequence derivation (an akm upgrade likely changed how one of " +
154
+ "these is formatted from the same plan data). Continuing with the stored row(s) as-is — this run keeps " +
155
+ "executing against the instructions it was frozen with, rather than having them rewritten mid-flight.");
156
+ }
157
+ }
158
+ export function assertRunStatusMatchesSpine(run, rows) {
113
159
  const current = run.current_step_id ? rows.find((row) => row.step_id === run.current_step_id) : undefined;
160
+ if (run.current_step_id !== null && !current)
161
+ corruptSpine(run.id, `current step ${run.current_step_id} is not in the frozen plan`);
114
162
  if (run.status === "active") {
115
163
  const firstPending = rows.find((row) => row.status === "pending");
116
164
  if (!current || current.status !== "pending" || firstPending?.step_id !== current.step_id)
@@ -19,13 +19,6 @@
19
19
  */
20
20
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
21
21
  import { parseReference, resolveReferenceString } from "../program/expressions.js";
22
- /** Mirrors `runs.ts`'s `WORKFLOW_EVIDENCE_TRUNCATED_MARKER` byte-for-byte — see this file's header for why it is reproduced, not imported. */
23
- const EVIDENCE_TRUNCATED_MARKER = "__akm_evidence_truncated__";
24
- function isTruncatedEvidenceValue(value) {
25
- return (typeof value === "object" &&
26
- value !== null &&
27
- value[EVIDENCE_TRUNCATED_MARKER] === true);
28
- }
29
22
  /** Project a step artifact out of its persisted evidence — mirrors `exec/step-work.ts`'s `projectStepOutput`. */
30
23
  function projectStepOutput(evidence) {
31
24
  return Object.hasOwn(evidence, "output") ? evidence.output : evidence;
@@ -60,22 +53,11 @@ export function resolveWorkflowRunOutputs(plan, steps) {
60
53
  errors.push(`output "${name}": "${declaration.from}" is not a valid step-output reference.`);
61
54
  continue;
62
55
  }
63
- const rootValue = Object.hasOwn(stepOutputs, parsed.expr.stepId) ? stepOutputs[parsed.expr.stepId] : undefined;
64
- if (isTruncatedEvidenceValue(rootValue)) {
65
- errors.push(`output "${name}" reads step "${parsed.expr.stepId}"'s artifact, which was truncated — it exceeded the ` +
66
- `evidence persistence cap and was not stored.`);
67
- continue;
68
- }
69
56
  const resolved = resolveReferenceString(declaration.from, scope);
70
57
  if (!resolved.ok) {
71
58
  errors.push(`output "${name}": ${resolved.error.message}`);
72
59
  continue;
73
60
  }
74
- if (isTruncatedEvidenceValue(resolved.value)) {
75
- errors.push(`output "${name}" reads step "${parsed.expr.stepId}"'s artifact, which was truncated — it exceeded the ` +
76
- `evidence persistence cap and was not stored.`);
77
- continue;
78
- }
79
61
  if (declaration.schema) {
80
62
  const schemaErrors = validateJsonSchemaSubset(resolved.value, declaration.schema);
81
63
  if (schemaErrors.length > 0) {
@@ -86,9 +68,7 @@ export function resolveWorkflowRunOutputs(plan, steps) {
86
68
  }
87
69
  outputs[name] = resolved.value;
88
70
  }
89
- if (errors.length > 0)
90
- return { ok: false, errors };
91
- return { ok: true, outputs };
71
+ return { outputs, errors };
92
72
  }
93
73
  /**
94
74
  * What a completed run EXPORTS: the resolved declared outputs, or