akm-cli 0.9.5 → 0.9.6

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 (105) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/dist/commands/env/env-cli.js +1 -2
  3. package/dist/commands/env/secret-cli.js +1 -5
  4. package/dist/commands/feedback-cli.js +0 -4
  5. package/dist/commands/health/checks.js +0 -32
  6. package/dist/commands/health/surfaces.js +2 -2
  7. package/dist/commands/health.js +6 -15
  8. package/dist/commands/improve/autonomy-gate.js +1 -1
  9. package/dist/commands/improve/consolidate.js +25 -26
  10. package/dist/commands/improve/distill.js +2 -2
  11. package/dist/commands/improve/extract.js +8 -1
  12. package/dist/commands/improve/improve.js +3 -5
  13. package/dist/commands/improve/locks.js +13 -5
  14. package/dist/commands/improve/loop-stages.js +8 -9
  15. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  16. package/dist/commands/improve/outcome-loop.js +0 -6
  17. package/dist/commands/improve/reflect.js +20 -9
  18. package/dist/commands/lint/index.js +4 -6
  19. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  20. package/dist/commands/sources/bundle-config-ops.js +9 -4
  21. package/dist/commands/sources/info.js +14 -10
  22. package/dist/commands/tasks/tasks.js +12 -16
  23. package/dist/commands/workflow-cli.js +2 -2
  24. package/dist/core/common.js +61 -24
  25. package/dist/core/config/config-io.js +2 -2
  26. package/dist/core/config/config-sources.js +32 -2
  27. package/dist/core/config/config.js +1 -1
  28. package/dist/core/config/schema/engines.js +9 -7
  29. package/dist/core/config/schema/primitives.js +0 -5
  30. package/dist/core/config/schema/search.js +2 -1
  31. package/dist/core/file-lock.js +2 -1
  32. package/dist/core/maintenance-barrier.js +2 -14
  33. package/dist/core/paths.js +0 -3
  34. package/dist/core/redaction.js +2 -2
  35. package/dist/core/spawn-env.js +8 -12
  36. package/dist/core/state/migrations.js +1 -12
  37. package/dist/core/state-db.js +9 -27
  38. package/dist/core/write-source.js +12 -19
  39. package/dist/execution/directory-identity.js +36 -10
  40. package/dist/indexer/graph/graph-boost.js +0 -4
  41. package/dist/indexer/index-writer-lock.js +43 -24
  42. package/dist/indexer/index-written-assets.js +5 -6
  43. package/dist/indexer/indexer.js +2 -39
  44. package/dist/indexer/materialize-embeddings.js +85 -41
  45. package/dist/indexer/search/db-search.js +15 -48
  46. package/dist/indexer/search/ranking-contributors.js +0 -25
  47. package/dist/indexer/search/ranking.js +3 -13
  48. package/dist/integrations/agent/builder-shared.js +0 -25
  49. package/dist/integrations/agent/model-map.js +2 -60
  50. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  51. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  52. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  53. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  54. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  55. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  56. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  57. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  58. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  59. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  60. package/dist/integrations/session-logs/index.js +0 -9
  61. package/dist/llm/client.js +75 -42
  62. package/dist/llm/embedder.js +7 -3
  63. package/dist/llm/embedders/remote.js +141 -42
  64. package/dist/registry/network.js +5 -37
  65. package/dist/runtime.js +2 -10
  66. package/dist/scripts/akm-migrate-node.js +60 -126
  67. package/dist/scripts/akm-migrate.js +60 -126
  68. package/dist/setup/engine-config.js +2 -5
  69. package/dist/setup/registry-stash-loader.js +0 -8
  70. package/dist/setup/setup.js +9 -46
  71. package/dist/setup/steps/connection-shared.js +10 -13
  72. package/dist/sources/providers/git-install.js +1 -1
  73. package/dist/storage/engines/sqlite-migrations.js +20 -1
  74. package/dist/storage/repositories/index-entries-repository.js +0 -15
  75. package/dist/tasks/backends/launchd.js +15 -20
  76. package/dist/tasks/backends/schtasks.js +18 -8
  77. package/dist/tasks/run/run-native-task.js +8 -6
  78. package/dist/tasks/source/bounded-document.js +2 -4
  79. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  80. package/dist/tasks/source/task-source-v4.js +5 -10
  81. package/dist/tasks/source/task-to-v3.js +5 -10
  82. package/dist/tasks/source/task-to-v4.js +1 -4
  83. package/dist/tasks/source-v3.js +6 -6
  84. package/dist/workflows/exec/native-executor.js +21 -31
  85. package/dist/workflows/exec/run-workflow.js +5 -6
  86. package/dist/workflows/exec/scheduler.js +3 -19
  87. package/dist/workflows/exec/step-work.js +1 -4
  88. package/dist/workflows/exec/unit-dispatch.js +2 -2
  89. package/dist/workflows/exec/worktree.js +1 -13
  90. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  91. package/dist/workflows/ir/plan-hash.js +4 -6
  92. package/dist/workflows/ir/schema-v4.js +0 -12
  93. package/dist/workflows/ir/schema.js +20 -31
  94. package/dist/workflows/parser.js +11 -52
  95. package/dist/workflows/renderer.js +2 -3
  96. package/dist/workflows/resource-limits.js +11 -41
  97. package/dist/workflows/runtime/runs.js +3 -4
  98. package/dist/workflows/source-ir/schema.js +14 -30
  99. package/dist/workflows/validator.js +1 -7
  100. package/docs/reference/configuration.md +1 -1
  101. package/package.json +1 -1
  102. package/schemas/akm-config.json +4 -23
  103. package/schemas/akm-task.json +1 -2
  104. package/schemas/akm-workflow.json +1 -13
  105. package/dist/indexer/search/semantic-status.js +0 -142
@@ -115,7 +115,7 @@ import { appendEvent } from "../../core/events.js";
115
115
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
116
116
  import { runStructured } from "../../core/structured.js";
117
117
  import { warn } from "../../core/warn.js";
118
- import { assertFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
118
+ import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
119
119
  import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
120
120
  import { withWorkflowRunsConnection, withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
121
121
  import { materializeFrozenWorkflowEnvironment } from "../ir/environment-v4.js";
@@ -132,7 +132,7 @@ import { collectWorkflowDispatchSensitiveValues, redactUnitOutcome } from "./dis
132
132
  // and the process-outcome → failure-reason mapping.
133
133
  import { runExecUnit } from "./exec-unit.js";
134
134
  import { mergeLoweringNotices } from "./lowering-notices.js";
135
- import { LIFETIME_UNIT_CAP, scheduleUnits, UnitCapExceededError } from "./scheduler.js";
135
+ import { scheduleUnits } from "./scheduler.js";
136
136
  // Shared step semantics — the ONE implementation consumed by the engine
137
137
  // (this module + run-workflow.ts) on both the fresh-execution and the resume
138
138
  // path. This module dispatches; step-work.ts owns the pure decisions.
@@ -142,13 +142,13 @@ import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } fro
142
142
  import { enqueueUnitWrite } from "./unit-writer.js";
143
143
  import { assertGitWorkTree, cleanupUnitWorktree, createUnitWorktree } from "./worktree.js";
144
144
  /**
145
- * Mutable per-step dispatch budget: the lifetime unit cap PLUS the declared
146
- * run-level budget ceilings (`budget.max_units` / `budget.max_tokens`,
147
- * addendum R2). Consumed once per journaled dispatch attempt (including
148
- * retries); durable-row reuses never touch it — the peer-review fix that
149
- * keeps large partially-completed fan-outs resumable instead of tripping the
150
- * cap on `journaled + items`. Token usage accumulates per actual dispatch on
151
- * top of the journal-seeded run total (reused rows' tokens are already in the
145
+ * Mutable per-step dispatch budget: the declared run-level budget ceilings
146
+ * (`budget.max_units` / `budget.max_tokens`, addendum R2). Consumed once per
147
+ * journaled dispatch attempt (including retries); durable-row reuses never
148
+ * touch it — the peer-review fix that keeps large partially-completed
149
+ * fan-outs resumable instead of tripping a pre-batch check on
150
+ * `journaled + items`. Token usage accumulates per actual dispatch on top of
151
+ * the journal-seeded run total (reused rows' tokens are already in the
152
152
  * seed). Check-and-increment is synchronous, so concurrent units cannot race
153
153
  * it; crossing a declared ceiling fires `onExceeded` ONCE (the executor's
154
154
  * chained AbortController), aborting pending and in-flight dispatches.
@@ -157,8 +157,6 @@ class DispatchBudget {
157
157
  used;
158
158
  /** Run-total tokens: journal-seeded input + this step's dispatch usage. */
159
159
  tokens;
160
- /** Set (once) when a dispatch was refused; the step fails with this message. */
161
- capMessage;
162
160
  /** Set (once) when a declared budget ceiling was hit; the step fails hard with it. */
163
161
  budgetMessage;
164
162
  maxUnits;
@@ -185,10 +183,6 @@ class DispatchBudget {
185
183
  `against the workflow's declared budget.max_tokens of ${this.maxTokens} — refusing further dispatch.`);
186
184
  return false;
187
185
  }
188
- if (this.used >= LIFETIME_UNIT_CAP) {
189
- this.capMessage ??= new UnitCapExceededError(LIFETIME_UNIT_CAP).message;
190
- return false;
191
- }
192
186
  this.used++;
193
187
  return true;
194
188
  }
@@ -343,7 +337,7 @@ async function prepareStepDispatchPrerequisites(input) {
343
337
  const target = workUnits[0]?.frozenTarget;
344
338
  const frozenCwd = target && "cwdIdentity" in target ? target.cwdIdentity : undefined;
345
339
  if (frozenCwd)
346
- assertFrozenDirectoryIdentity(frozenCwd);
340
+ assertFrozenDirectoryContained(frozenCwd);
347
341
  const base = frozenCwd?.realRoot ?? ctx.workDir ?? process.cwd();
348
342
  const preflightWorktree = ctx.preflightWorktree ?? assertGitWorkTree;
349
343
  const gitError = preflightWorktree(base);
@@ -464,15 +458,12 @@ async function executeStepPlanInConnection(plan, ctx) {
464
458
  // journal-write failures must not erase notices already observed from real
465
459
  // dispatches; durable row reuses naturally contribute none.
466
460
  const notices = mergeLoweringNotices(...outcomes.map((outcome) => outcome?.notices));
467
- // Declared budget ceilings and the lifetime cap are hard backstops: a step
468
- // that hit one FAILS regardless of on_error policy (a capped run must never
469
- // quietly pass its gate). The budget message names WHICH ceiling tripped.
461
+ // A declared budget ceiling is a hard backstop: a step that hit one FAILS
462
+ // regardless of on_error policy (a capped run must never quietly pass its
463
+ // gate). The budget message names WHICH ceiling tripped.
470
464
  if (budget.budgetMessage) {
471
465
  return { ...failedStep(budget.used, budget.budgetMessage, notices), tokensUsed: budget.tokens };
472
466
  }
473
- if (budget.capMessage) {
474
- return { ...failedStep(budget.used, budget.capMessage, notices), tokensUsed: budget.tokens };
475
- }
476
467
  const units = outcomes.map((outcome, index) => outcome ?? {
477
468
  unitId: workUnits[index].unitId,
478
469
  ok: false,
@@ -597,17 +588,16 @@ async function runUnit(input) {
597
588
  });
598
589
  }
599
590
  const attemptId = attemptIdFor(attempt);
600
- // Lifetime cap + declared budget ceilings, consumed per ACTUAL dispatch
601
- // (reuses above returned before reaching here). Refusal fails this unit
602
- // without journaling a row — nothing was dispatched — and the sticky
603
- // capMessage/budgetMessage fails the step.
591
+ // Declared budget ceilings, consumed per ACTUAL dispatch (reuses above
592
+ // returned before reaching here). Refusal fails this unit without
593
+ // journaling a row — nothing was dispatched — and the sticky
594
+ // budgetMessage fails the step.
604
595
  if (!input.budget.tryConsume()) {
605
- const budgetHit = input.budget.budgetMessage !== undefined;
606
596
  return (outcome ?? {
607
597
  unitId,
608
598
  ok: false,
609
- failureReason: budgetHit ? "budget_exceeded" : "unit_cap_exceeded",
610
- error: input.budget.budgetMessage ?? input.budget.capMessage ?? "lifetime unit cap exceeded",
599
+ failureReason: "budget_exceeded",
600
+ error: input.budget.budgetMessage ?? "budget exceeded",
611
601
  });
612
602
  }
613
603
  outcome = await dispatchJournaledAttempt({
@@ -1055,7 +1045,7 @@ async function dispatchUnit(request, dispatcher) {
1055
1045
  export const defaultUnitDispatcher = async (request, feedback) => {
1056
1046
  const frozenTarget = request.frozenTarget;
1057
1047
  if (frozenTarget.kind === "script") {
1058
- assertFrozenDirectoryIdentity(frozenTarget.cwdIdentity);
1048
+ assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
1059
1049
  if (frozenTarget.executable) {
1060
1050
  assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
1061
1051
  }
@@ -1099,7 +1089,7 @@ export const defaultUnitDispatcher = async (request, feedback) => {
1099
1089
  }
1100
1090
  if (frozenTarget.kind === "shell") {
1101
1091
  if (frozenTarget.cwdIdentity)
1102
- assertFrozenDirectoryIdentity(frozenTarget.cwdIdentity);
1092
+ assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
1103
1093
  if (frozenTarget.executable) {
1104
1094
  assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
1105
1095
  }
@@ -332,12 +332,11 @@ function workflowSummaryJudge(options, stepPlan, signal, owner) {
332
332
  return frozenSummaryJudge(stepPlan.gate.frozenJudge, signal, options.dispatcher ?? defaultUnitDispatcher, owner, options.eventSource);
333
333
  }
334
334
  /**
335
- * Seed the lifetime unit cap AND the budget ceilings from the journal so
336
- * both are truly per-RUN: a resumed or re-invoked run must not restart the
337
- * runaway backstop — or a declared `budget` — at zero. The append-only attempt
338
- * journal is authoritative: dispatch attempts count against
339
- * `budget.max_units`, and their known tokens count against
340
- * `budget.max_tokens`. Durable result reuse is free.
335
+ * Seed the declared budget ceilings from the journal so they are truly
336
+ * per-RUN: a resumed or re-invoked run must not restart a declared `budget`
337
+ * at zero. The append-only attempt journal is authoritative: dispatch
338
+ * attempts count against `budget.max_units`, and their known tokens count
339
+ * against `budget.max_tokens`. Durable result reuse is free.
341
340
  *
342
341
  * Gate-evaluation rows (`phase = "gate"`, journaled by the completion-gate
343
342
  * judge) are EXCLUDED from the seed: the live path never consumes
@@ -15,17 +15,9 @@
15
15
  * - Cooperative cancellation via AbortSignal (workers stop claiming items;
16
16
  * the same signal is passed into each dispatch so in-flight units can be
17
17
  * preempted too).
18
- *
19
- * The lifetime unit cap ({@link LIFETIME_UNIT_CAP}) is DECLARED here but
20
- * enforced per actual dispatch by the native executor: a pre-batch check
21
- * (`journaled + items.length`) counted durable-row REUSES as new dispatches,
22
- * which made any partially-completed fan-out with more than ~cap/2 journaled
23
- * units impossible to resume (peer review R1). Only work that really
24
- * dispatches consumes the cap.
25
18
  */
26
19
  import { concurrentMap } from "../../core/concurrent.js";
27
20
  import { cpuDerivedUnitConcurrency, workflowMaxConcurrency } from "../concurrency-policy.js";
28
- import { WORKFLOW_MAX_MAP_EXPANSION } from "../resource-limits.js";
29
21
  export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENCY_CEILING, } from "../concurrency-policy.js";
30
22
  /**
31
23
  * Hard ceiling on an EXPLICIT `workflow.maxConcurrency`. A user value above
@@ -49,21 +41,13 @@ export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENC
49
41
  export function maxUnitConcurrency(cpuCount, configured) {
50
42
  return workflowMaxConcurrency(configured, cpuCount);
51
43
  }
52
- /** Lifetime unit cap per run — a runaway-loop backstop, far above real use. */
53
- export const LIFETIME_UNIT_CAP = WORKFLOW_MAX_MAP_EXPANSION;
54
- export class UnitCapExceededError extends Error {
55
- constructor(cap) {
56
- super(`workflow run exceeded the lifetime unit cap (${cap}). Aborting dispatch — check for a runaway fan-out.`);
57
- this.name = "UnitCapExceededError";
58
- }
59
- }
60
44
  /**
61
45
  * Run `dispatch` over `items` under the engine concurrency caps. Individual
62
46
  * failures do not cancel siblings (allSettled semantics from `concurrentMap`);
63
47
  * a slot whose dispatch threw or that was never claimed after an abort stays
64
- * `undefined` in the result array. The lifetime unit cap is NOT checked here
65
- * — the executor consumes it per actual dispatch, so durable-row reuses on
66
- * resume stay free.
48
+ * `undefined` in the result array. A declared `budget.max_units` ceiling is
49
+ * NOT checked here — the executor consumes it per actual dispatch, so
50
+ * durable-row reuses on resume stay free.
67
51
  */
68
52
  export async function scheduleUnits(items, dispatch, options = {}) {
69
53
  const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, options.maxConcurrency ?? Number.POSITIVE_INFINITY, options.llmConcurrency ?? Number.POSITIVE_INFINITY, options.hostConcurrency ?? cpuDerivedUnitConcurrency()));
@@ -22,7 +22,7 @@ import { canonicalInputJson, validateInputs } from "../../execution/input-contra
22
22
  import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
23
23
  import { canonicalJson as canonicalJsonString } from "../ir/plan-hash.js";
24
24
  import { parseReference, resolveReferenceString, } from "../program/expressions.js";
25
- import { clip, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
25
+ import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
26
26
  import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
27
27
  import { GATE_EVALUATION_PHASE } from "../runtime/unit-phases.js";
28
28
  import { parseJudgeVerdict } from "../validate-summary.js";
@@ -60,9 +60,6 @@ const GATE_ARTIFACT_CLIP = 4_000;
60
60
  * deterministically, before dispatch.
61
61
  */
62
62
  function validateFanOutItems(stepId, items) {
63
- if (items.length > WORKFLOW_MAX_MAP_EXPANSION) {
64
- return `Step "${stepId}" fan-out expands to ${items.length} units, exceeding the ${WORKFLOW_MAX_MAP_EXPANSION}-unit resource limit.`;
65
- }
66
63
  const nullIndex = items.findIndex((item) => item === null || item === undefined);
67
64
  if (nullIndex !== -1) {
68
65
  return (`Step "${stepId}" fan-out list contains a null item (index ${nullIndex}). ` +
@@ -2,7 +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 { ConfigError } from "../../core/errors.js";
5
- import { assertFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
5
+ import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
6
6
  import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
7
7
  import { canonicalResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
8
8
  import { dispatchLoweredExecutionRequest, lowerResolvedExecutionRequestWithRunner, } from "../../integrations/agent/execution-lowering.js";
@@ -11,7 +11,7 @@ import { getHarness } from "../../integrations/harnesses/index.js";
11
11
  export function prepareWorkflowExecution(request, prompt = request.prompt) {
12
12
  const target = request.frozenTarget;
13
13
  if (target.cwdIdentity)
14
- assertFrozenDirectoryIdentity(target.cwdIdentity);
14
+ assertFrozenDirectoryContained(target.cwdIdentity);
15
15
  if (target.executable)
16
16
  assertFrozenExecutableIdentity(target.executable, `unit ${request.unitId} executable`);
17
17
  const wire = JSON.parse(canonicalResolvedExecutionRequest(target.request));
@@ -67,7 +67,7 @@ import { spawnSync } from "node:child_process";
67
67
  import fsp from "node:fs/promises";
68
68
  import os from "node:os";
69
69
  import path from "node:path";
70
- import { isWithinAsync, safeRealpathAsync } from "../../core/common.js";
70
+ import { isProcessAlive, isWithinAsync, safeRealpathAsync } from "../../core/common.js";
71
71
  import { serializeByKey } from "../../core/concurrent.js";
72
72
  import { runManagedSubprocess } from "../../core/subprocess.js";
73
73
  import { warn } from "../../core/warn.js";
@@ -417,18 +417,6 @@ async function isWorktreeLeaseLive(candidate) {
417
417
  return false;
418
418
  return isProcessAlive(lease.pid);
419
419
  }
420
- /** `kill(pid, 0)` liveness probe. EPERM proves the process exists but is not ours. */
421
- function isProcessAlive(pid) {
422
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
423
- return false;
424
- try {
425
- process.kill(pid, 0);
426
- return true;
427
- }
428
- catch (err) {
429
- return err.code === "EPERM";
430
- }
431
- }
432
420
  // ── Garbage collection ──────────────────────────────────────────────────────
433
421
  /**
434
422
  * Drop the run-scoped root once its last unit worktree is gone. `rmdir`
@@ -31,7 +31,7 @@ import { UsageError } from "../../../core/errors.js";
31
31
  import { GuardedExecutionSourceCollector } from "../../../execution/guarded-source.js";
32
32
  import { workflowParamContract } from "../../ir/params.js";
33
33
  import { canonicalJson, canonicalPlanJson } from "../../ir/plan-hash.js";
34
- import { utf8Bytes, WORKFLOW_MAX_COMPOSITION_DEPTH, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES, } from "../../resource-limits.js";
34
+ import { utf8Bytes, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES } from "../../resource-limits.js";
35
35
  import { loadWorkflowAsset } from "../../runtime/workflow-asset-loader.js";
36
36
  import { resolveOwnedAsset } from "../environment.js";
37
37
  import { declaredParamNames, earlierStepIds } from "../step-values.js";
@@ -91,13 +91,6 @@ function assertNoCompositionCycle(stepId, childRef, refPath) {
91
91
  `Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", "Break the cycle: remove or redirect one of the compositions in the path above so no workflow ends up " +
92
92
  "composing itself, directly or through intermediates.");
93
93
  }
94
- function assertCompositionDepthAllowed(stepId, childRef, childDepth, refPath) {
95
- if (childDepth <= WORKFLOW_MAX_COMPOSITION_DEPTH)
96
- return;
97
- throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: workflow composition is limited to ` +
98
- `${WORKFLOW_MAX_COMPOSITION_DEPTH} levels. Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", `Flatten the composition chain to ${WORKFLOW_MAX_COMPOSITION_DEPTH} levels or fewer — inline one of the ` +
99
- "intermediate workflows, or restructure the chain so fewer child compositions are nested.");
100
- }
101
94
  /**
102
95
  * Add the child's embedded plan bytes to the shared, tree-wide budget
103
96
  * (A-N6) and fail before publication if the AGGREGATE crosses the cap.
@@ -126,10 +119,9 @@ export async function childWorkflowDispatch(input) {
126
119
  // path to reach the child run and must reject, not vanish (see
127
120
  // assertNoStepEnvironment's doc comment).
128
121
  assertNoStepEnvironment(source.id, childRef, source);
129
- // §4.2 steps 2-3: the composition bounds, before any child compilation.
122
+ // §4.2 step 2: the composition cycle check, before any child compilation.
130
123
  assertNoCompositionCycle(source.id, childRef, context.composition.refPath);
131
124
  const childDepth = context.composition.depth + 1;
132
- assertCompositionDepthAllowed(source.id, childRef, childDepth, context.composition.refPath);
133
125
  // §4.2 step 4: freeze the child COMPLETELY (compile -> validate -> freeze),
134
126
  // with its OWN fresh collector (A-N7) so its plan is a pure function of its
135
127
  // own source, and the SAME mutable budget object so the aggregate bound
@@ -13,7 +13,7 @@
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_JSON_DEPTH, WORKFLOW_MAX_PLAN_BYTES } from "../resource-limits.js";
16
+ import { utf8Bytes, WORKFLOW_MAX_PLAN_BYTES } from "../resource-limits.js";
17
17
  import { decodeWorkflowPlanV4, WORKFLOW_IR_V5_VERSION } from "./schema-v4.js";
18
18
  /** sha256 hex of the canonical (recursively sorted-keys) JSON of the plan. */
19
19
  export function computePlanHash(plan) {
@@ -53,9 +53,7 @@ export function decodeCanonicalPlan(runId, planJson, planHash, expectedVersion)
53
53
  throw new Error(`Workflow run ${runId} has noncanonical frozen plan JSON.`);
54
54
  return plan;
55
55
  }
56
- function sortKeys(value, depth = 0) {
57
- if (depth > WORKFLOW_MAX_JSON_DEPTH)
58
- throw new TypeError("JSON value exceeds maximum depth");
56
+ function sortKeys(value) {
59
57
  if (value === null || typeof value === "string" || typeof value === "boolean")
60
58
  return value;
61
59
  if (typeof value === "number") {
@@ -64,11 +62,11 @@ function sortKeys(value, depth = 0) {
64
62
  return value;
65
63
  }
66
64
  if (Array.isArray(value))
67
- return value.map((item) => sortKeys(item, depth + 1));
65
+ return value.map((item) => sortKeys(item));
68
66
  if (value && typeof value === "object") {
69
67
  return Object.fromEntries(Object.entries(value)
70
68
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
71
- .map(([k, v]) => [k, sortKeys(v, depth + 1)]));
69
+ .map(([k, v]) => [k, sortKeys(v)]));
72
70
  }
73
71
  throw new TypeError("JSON value contains a non-JSON value");
74
72
  }
@@ -182,15 +182,6 @@ function decodeFrozenTarget(value, unit, environment, requiredSources, depth, bu
182
182
  return decodeChildWorkflowTarget(target, unit, depth, budget);
183
183
  fail(`unit ${unit.id} frozenTarget has unsupported kind ${String(target.kind)}`);
184
184
  }
185
- /**
186
- * Composition-depth corruption bound (spec §3.6 step 6, §4.5, row A-23).
187
- * Mirrors `WORKFLOW_MAX_COMPOSITION_DEPTH` (= 8), which Lane B adds to
188
- * `src/workflows/resource-limits.ts` in a later commit as the freeze-time
189
- * `COMPOSITION_INVALID` gate. Reproduced here — the decode-time corruption
190
- * gate — rather than imported across the lane boundary, matching
191
- * `tests/workflows/plan-v5-schema.test.ts`'s own header comment.
192
- */
193
- const CHILD_WORKFLOW_DECODE_MAX_DEPTH = 8;
194
185
  /**
195
186
  * Decode a `kind: "child-workflow"` frozen target (§3.5): the embedded
196
187
  * COMPLETE child plan, re-verified against its own `planHash` and this
@@ -241,9 +232,6 @@ function decodeChildWorkflowTarget(target, unit, depth, budget) {
241
232
  fail(`unit ${unit.id} child workflow contentHash does not match its frozen dispatch`);
242
233
  }
243
234
  const childDepth = depth + 1;
244
- if (childDepth > CHILD_WORKFLOW_DECODE_MAX_DEPTH) {
245
- fail(`unit ${unit.id} child workflow ${target.ref} exceeds the max composition depth of ${CHILD_WORKFLOW_DECODE_MAX_DEPTH}`);
246
- }
247
235
  const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, {}, childDepth, budget);
248
236
  const embeddedPlanJson = canonicalJsonLocal(frozenPlan);
249
237
  const actualPlanHash = sha256(embeddedPlanJson);
@@ -5,12 +5,11 @@ import { isContainedRelativePath } 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_EXEC_ARG_BYTES, WORKFLOW_MAX_EXEC_ARGV, WORKFLOW_MAX_EXEC_CWD_LENGTH, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_GATE_LOOPS, WORKFLOW_MAX_INPUTS, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_JSON_DEPTH, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_PLAN_BYTES, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_STEPS, 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_PLAN_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`).
12
- export { WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_GATE_LOOPS, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_TIMEOUT_MS };
13
- export const WORKFLOW_MAX_UNITS = WORKFLOW_MAX_MAP_EXPANSION;
12
+ export { WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_TIMEOUT_MS };
14
13
  const MAX_LIST_ITEMS = 1024;
15
14
  const MAX_STRING_LENGTH = 1_000_000;
16
15
  /** Validate the current durable execution graph. */
@@ -33,8 +32,8 @@ export function validateWorkflowPlanStructure(input, options, hooks = {}) {
33
32
  fail(`execution.maxConcurrency must be an integer from 1 through ${WORKFLOW_MAX_CONCURRENCY}`);
34
33
  }
35
34
  assertKeys(plan.execution, ["maxConcurrency"], "execution");
36
- if (!Array.isArray(plan.steps) || plan.steps.length === 0 || plan.steps.length > WORKFLOW_MAX_STEPS)
37
- fail("steps must contain 1 through 256 entries");
35
+ if (!Array.isArray(plan.steps) || plan.steps.length === 0)
36
+ fail("steps must be a non-empty array");
38
37
  const stepIds = new Set();
39
38
  const nodeIds = new Set();
40
39
  for (let index = 0; index < plan.steps.length; index++) {
@@ -126,7 +125,7 @@ function validateNode(node, stepId, nodeIds, unitExtraKeys) {
126
125
  validateSchema(node.schema, `unit ${node.id} schema`);
127
126
  validateRetry(node.retry, node.id);
128
127
  validateStringArray(node.env, `unit ${node.id} env`, MAX_LIST_ITEMS, true);
129
- validateStringArray(node.inputs, `unit ${node.id} inputs`, WORKFLOW_MAX_INPUTS, true);
128
+ validateStringArray(node.inputs, `unit ${node.id} inputs`, Infinity, true);
130
129
  validateSource(node.source, `unit ${node.id} source`);
131
130
  }
132
131
  /**
@@ -143,15 +142,11 @@ function validateExecSpec(value, label) {
143
142
  const command = value.command;
144
143
  if (!Array.isArray(command) ||
145
144
  command.length === 0 ||
146
- command.length > WORKFLOW_MAX_EXEC_ARGV ||
147
- !command.every((arg) => typeof arg === "string" && arg.length > 0 && utf8Bytes(arg) <= WORKFLOW_MAX_EXEC_ARG_BYTES)) {
148
- fail(`${label}.command must be an argv array of 1 through ${WORKFLOW_MAX_EXEC_ARGV} bounded non-empty strings`);
145
+ !command.every((arg) => typeof arg === "string" && arg.length > 0)) {
146
+ fail(`${label}.command must be an argv array of non-empty strings`);
149
147
  }
150
148
  if (value.cwd !== undefined) {
151
- if (typeof value.cwd !== "string" ||
152
- value.cwd.length === 0 ||
153
- value.cwd.length > WORKFLOW_MAX_EXEC_CWD_LENGTH ||
154
- !isContainedRelativePath(value.cwd)) {
149
+ if (typeof value.cwd !== "string" || value.cwd.length === 0 || !isContainedRelativePath(value.cwd)) {
155
150
  fail(`${label}.cwd must be a relative path contained in the unit working directory`);
156
151
  }
157
152
  }
@@ -183,11 +178,10 @@ function validateExecEnvScope(value, label) {
183
178
  const passEnv = value.passEnv;
184
179
  if (!Array.isArray(passEnv) ||
185
180
  passEnv.length === 0 ||
186
- passEnv.length > WORKFLOW_MAX_EXEC_PASS_ENV ||
187
181
  !passEnv.every((name) => typeof name === "string" && WORKFLOW_ENV_VAR_NAME_PATTERN.test(name)) ||
188
182
  new Set(passEnv).size !== passEnv.length) {
189
- fail(`${label}.passEnv must be 1 through ${WORKFLOW_MAX_EXEC_PASS_ENV} distinct environment variable names ` +
190
- `matching ${WORKFLOW_ENV_VAR_NAME_PATTERN.source}`);
183
+ fail(`${label}.passEnv must be a non-empty list of distinct environment variable names matching ` +
184
+ `${WORKFLOW_ENV_VAR_NAME_PATTERN.source}`);
191
185
  }
192
186
  }
193
187
  function validateGate(gate, stepId, nodeIds, gateExtraKeys) {
@@ -199,8 +193,7 @@ function validateGate(gate, stepId, nodeIds, gateExtraKeys) {
199
193
  gate.criteria.length > MAX_LIST_ITEMS ||
200
194
  !gate.criteria.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_STRING_LENGTH) ||
201
195
  !Number.isInteger(gate.maxLoops) ||
202
- gate.maxLoops < 1 ||
203
- gate.maxLoops > WORKFLOW_MAX_GATE_LOOPS)
196
+ gate.maxLoops < 1)
204
197
  fail(`gate for step ${stepId} is invalid`);
205
198
  if (nodeIds.has(gate.id))
206
199
  fail(`gate id ${gate.id} collides with a node`);
@@ -213,7 +206,6 @@ function validateRoute(route, stepId) {
213
206
  !route.input ||
214
207
  !isRecord(route.when) ||
215
208
  Object.keys(route.when).length === 0 ||
216
- Object.keys(route.when).length > WORKFLOW_MAX_ROUTE_BRANCHES ||
217
209
  !Object.keys(route.when).every((match) => match.length > 0 && match.length <= MAX_STRING_LENGTH) ||
218
210
  !Object.values(route.when).every((target) => typeof target === "string" && target))
219
211
  fail(`route for step ${stepId} is invalid`);
@@ -226,9 +218,7 @@ function assertKeys(value, allowed, label) {
226
218
  if (!allowed.includes(key))
227
219
  fail(`${label} contains unknown key ${key}`);
228
220
  }
229
- function assertJson(value, depth = 0) {
230
- if (depth > WORKFLOW_MAX_JSON_DEPTH)
231
- fail("plan exceeds JSON depth limit of 64");
221
+ function assertJson(value) {
232
222
  if (value === null || typeof value === "boolean")
233
223
  return;
234
224
  if (typeof value === "string") {
@@ -243,23 +233,23 @@ function assertJson(value, depth = 0) {
243
233
  }
244
234
  if (Array.isArray(value)) {
245
235
  for (const item of value)
246
- assertJson(item, depth + 1);
236
+ assertJson(item);
247
237
  return;
248
238
  }
249
239
  if (isRecord(value)) {
250
240
  for (const item of Object.values(value))
251
- assertJson(item, depth + 1);
241
+ assertJson(item);
252
242
  return;
253
243
  }
254
244
  fail("plan contains a non-JSON value");
255
245
  }
256
246
  function validateParams(params, schemas) {
257
- validateStringArray(params, "params", WORKFLOW_MAX_PARAMS, true);
247
+ validateStringArray(params, "params", Infinity, true);
258
248
  if (params?.some((name) => !PROGRAM_PARAM_NAME_PATTERN.test(name)))
259
249
  fail("params contains an invalid name");
260
250
  if (schemas !== undefined) {
261
- if (!isRecord(schemas) || Object.keys(schemas).length > WORKFLOW_MAX_PARAMS)
262
- fail("paramSchemas must be a bounded object");
251
+ if (!isRecord(schemas))
252
+ fail("paramSchemas must be an object");
263
253
  for (const [name, schema] of Object.entries(schemas)) {
264
254
  if (!PROGRAM_PARAM_NAME_PATTERN.test(name))
265
255
  fail(`paramSchemas.${name} is invalid`);
@@ -284,9 +274,8 @@ function validateBudget(budget) {
284
274
  fail("budget must declare a ceiling");
285
275
  validateOptionalPositiveInteger(budget.maxTokens, "budget.maxTokens");
286
276
  const maxUnits = budget.maxUnits;
287
- if (maxUnits !== undefined &&
288
- (!Number.isSafeInteger(maxUnits) || maxUnits < 1 || maxUnits > WORKFLOW_MAX_UNITS))
289
- fail(`budget.maxUnits must be an integer from 1 through ${WORKFLOW_MAX_UNITS}`);
277
+ if (maxUnits !== undefined && (!Number.isSafeInteger(maxUnits) || maxUnits < 1))
278
+ fail("budget.maxUnits must be a positive integer");
290
279
  }
291
280
  function validateRetry(retry, nodeId) {
292
281
  if (retry === undefined)
@@ -294,7 +283,7 @@ function validateRetry(retry, nodeId) {
294
283
  if (!isRecord(retry))
295
284
  fail(`unit ${nodeId} retry must be an object`);
296
285
  assertKeys(retry, ["max", "on"], `unit ${nodeId} retry`);
297
- if (!Number.isSafeInteger(retry.max) || retry.max < 0 || retry.max > WORKFLOW_MAX_RETRIES)
286
+ if (!Number.isSafeInteger(retry.max) || retry.max < 0)
298
287
  fail(`unit ${nodeId} retry.max is invalid`);
299
288
  validateStringArray(retry.on, `unit ${nodeId} retry.on`, PROGRAM_RETRY_REASONS.length, true);
300
289
  if (retry.on === undefined ||