akm-cli 0.9.4 → 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 (123) hide show
  1. package/CHANGELOG.md +265 -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/anti-collapse.js +4 -91
  9. package/dist/commands/improve/autonomy-gate.js +1 -1
  10. package/dist/commands/improve/consolidate.js +25 -26
  11. package/dist/commands/improve/distill.js +2 -2
  12. package/dist/commands/improve/extract.js +8 -1
  13. package/dist/commands/improve/improve.js +3 -5
  14. package/dist/commands/improve/locks.js +13 -5
  15. package/dist/commands/improve/loop-stages.js +8 -9
  16. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  17. package/dist/commands/improve/outcome-loop.js +0 -6
  18. package/dist/commands/improve/reflect.js +20 -9
  19. package/dist/commands/lint/index.js +4 -6
  20. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  21. package/dist/commands/proposal/validators/proposal-validators.js +12 -0
  22. package/dist/commands/read/search.js +14 -24
  23. package/dist/commands/sources/bundle-config-ops.js +9 -4
  24. package/dist/commands/sources/info.js +14 -10
  25. package/dist/commands/tasks/tasks-cli.js +56 -3
  26. package/dist/commands/tasks/tasks.js +101 -18
  27. package/dist/commands/workflow-cli.js +2 -2
  28. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  29. package/dist/core/common.js +61 -24
  30. package/dist/core/config/config-io.js +2 -2
  31. package/dist/core/config/config-sources.js +32 -2
  32. package/dist/core/config/config-version-shim.js +101 -0
  33. package/dist/core/config/config.js +7 -7
  34. package/dist/core/config/schema/engines.js +9 -7
  35. package/dist/core/config/schema/primitives.js +0 -5
  36. package/dist/core/config/schema/search.js +2 -1
  37. package/dist/core/file-lock.js +2 -1
  38. package/dist/core/improve-result.js +35 -14
  39. package/dist/core/maintenance-barrier.js +2 -14
  40. package/dist/core/paths.js +0 -3
  41. package/dist/core/redaction.js +2 -2
  42. package/dist/core/spawn-env.js +8 -12
  43. package/dist/core/state/migrations.js +1 -12
  44. package/dist/core/state-db.js +9 -27
  45. package/dist/core/write-source.js +12 -19
  46. package/dist/execution/directory-identity.js +36 -10
  47. package/dist/execution/guarded-source.js +0 -10
  48. package/dist/indexer/graph/graph-boost.js +0 -4
  49. package/dist/indexer/index-writer-lock.js +43 -24
  50. package/dist/indexer/index-written-assets.js +5 -6
  51. package/dist/indexer/indexer.js +2 -39
  52. package/dist/indexer/materialize-embeddings.js +85 -41
  53. package/dist/indexer/passes/metadata.js +12 -4
  54. package/dist/indexer/scan/doc-to-entry.js +2 -0
  55. package/dist/indexer/search/db-search.js +15 -48
  56. package/dist/indexer/search/ranking-contributors.js +0 -25
  57. package/dist/indexer/search/ranking.js +3 -13
  58. package/dist/indexer/search/search-fields.js +16 -1
  59. package/dist/integrations/agent/builder-shared.js +0 -25
  60. package/dist/integrations/agent/model-map.js +2 -60
  61. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  62. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  63. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  64. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  65. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  66. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  67. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  68. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  69. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  70. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  71. package/dist/integrations/session-logs/index.js +0 -9
  72. package/dist/llm/client.js +75 -42
  73. package/dist/llm/embedder.js +7 -3
  74. package/dist/llm/embedders/remote.js +141 -42
  75. package/dist/output/shapes/passthrough.js +17 -5
  76. package/dist/registry/network.js +5 -37
  77. package/dist/runtime.js +2 -10
  78. package/dist/scripts/akm-migrate-node.js +180 -238
  79. package/dist/scripts/akm-migrate.js +180 -238
  80. package/dist/setup/engine-config.js +2 -5
  81. package/dist/setup/registry-stash-loader.js +0 -8
  82. package/dist/setup/setup.js +9 -46
  83. package/dist/setup/steps/connection-shared.js +10 -13
  84. package/dist/sources/providers/git-install.js +1 -1
  85. package/dist/storage/engines/sqlite-migrations.js +20 -1
  86. package/dist/storage/repositories/index-entries-repository.js +0 -15
  87. package/dist/storage/repositories/proposals-repository.js +32 -6
  88. package/dist/tasks/backends/launchd.js +15 -20
  89. package/dist/tasks/backends/schtasks.js +18 -8
  90. package/dist/tasks/run/run-native-task.js +8 -6
  91. package/dist/tasks/scheduler-binding.js +15 -5
  92. package/dist/tasks/scheduler-sync-preview.js +4 -2
  93. package/dist/tasks/scheduler-sync.js +77 -42
  94. package/dist/tasks/source/bounded-document.js +2 -4
  95. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  96. package/dist/tasks/source/task-source-v4.js +5 -10
  97. package/dist/tasks/source/task-to-v3.js +29 -22
  98. package/dist/tasks/source/task-to-v4.js +1 -14
  99. package/dist/tasks/source-v3.js +6 -6
  100. package/dist/workflows/exec/native-executor.js +21 -31
  101. package/dist/workflows/exec/run-workflow.js +5 -6
  102. package/dist/workflows/exec/scheduler.js +3 -19
  103. package/dist/workflows/exec/step-work.js +1 -4
  104. package/dist/workflows/exec/unit-dispatch.js +2 -2
  105. package/dist/workflows/exec/worktree.js +1 -13
  106. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  107. package/dist/workflows/ir/plan-hash.js +4 -6
  108. package/dist/workflows/ir/schema-v4.js +0 -12
  109. package/dist/workflows/ir/schema.js +20 -31
  110. package/dist/workflows/parser.js +11 -52
  111. package/dist/workflows/renderer.js +2 -3
  112. package/dist/workflows/resource-limits.js +11 -41
  113. package/dist/workflows/runtime/runs.js +3 -4
  114. package/dist/workflows/source-ir/schema.js +14 -30
  115. package/dist/workflows/validator.js +1 -7
  116. package/docs/reference/cli.md +25 -3
  117. package/docs/reference/configuration.md +28 -7
  118. package/docs/reference/tasks.md +10 -0
  119. package/package.json +1 -1
  120. package/schemas/akm-config.json +4 -23
  121. package/schemas/akm-task.json +1 -2
  122. package/schemas/akm-workflow.json +1 -13
  123. package/dist/indexer/search/semantic-status.js +0 -142
@@ -17,7 +17,7 @@ import { WorkflowSourceCollisionError, WorkflowSourceNameError, WorkflowSourceRe
17
17
  import { compileWorkflowSource } from "../workflows/source-ir/compile.js";
18
18
  import { prepareTaskV3Execution } from "./prepare/prepare.js";
19
19
  import { parseSchedule } from "./schedule.js";
20
- import { assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "./scheduler-binding.js";
20
+ import { assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeArtifactOwner, schedulerNativeBindingId, } from "./scheduler-binding.js";
21
21
  import { parseTaskSource } from "./source/parse-task-source.js";
22
22
  import { projectTaskSourceV4 } from "./source/project-v4.js";
23
23
  import { taskSourceErrorDetail } from "./source-v3.js";
@@ -47,6 +47,7 @@ export async function prepareSchedulerSyncSourceSet(input) {
47
47
  desired: compiled.desired,
48
48
  sourceSnapshot,
49
49
  executableWorkflows: compiled.executableWorkflows,
50
+ failures: compiled.failures,
50
51
  });
51
52
  }
52
53
  export function finalizeSchedulerSyncPlan(input, prepared) {
@@ -108,35 +109,7 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
108
109
  .sort(compareCodePoints);
109
110
  for (const id of removed) {
110
111
  const current = present.get(id);
111
- if (!current?.invocation) {
112
- throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
113
- }
114
- const nativeId = exactInstalledNativeId(id, current, inspection.artifacts);
115
- const artifact = inspection.artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
116
- const priorFingerprint = current.signature ?? artifact?.fingerprint;
117
- if (!artifact || priorFingerprint === undefined) {
118
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
119
- }
120
- const logicalSource = installedLogicalSource(current.invocation, coherentInput);
121
- const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
122
- if (ordinal === undefined) {
123
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
124
- }
125
- operations.push(Object.freeze({
126
- kind: "remove",
127
- id,
128
- nativeId,
129
- expected: freezeRemovalExpectation({
130
- state: "present",
131
- bindingId: id,
132
- nativeId,
133
- logicalSource,
134
- ordinal,
135
- invocation: current.invocation,
136
- fingerprint: priorFingerprint,
137
- }),
138
- ...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
139
- }));
112
+ operations.push(buildSchedulerRemoveOperation(id, current, inspection.artifacts, coherentInput));
140
113
  }
141
114
  return Object.freeze({
142
115
  desired,
@@ -146,6 +119,49 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
146
119
  unchanged: Object.freeze(unchanged),
147
120
  operations: Object.freeze(operations),
148
121
  sourceSnapshot: prepared.sourceSnapshot,
122
+ failures: prepared.failures,
123
+ });
124
+ }
125
+ /**
126
+ * Build the exact removal operation for one installed binding: same
127
+ * exact-native-fingerprint / ordinal-attribution safety checks
128
+ * `finalizeSchedulerSyncPlan`'s remove loop always applied, factored out so
129
+ * `akm task prune` (#851) can build removal operations for entries
130
+ * `belongsToBundle` structurally can't see (unresolvable ownership) without
131
+ * re-deriving — or weakening — this logic. Throws the same `UsageError`s a
132
+ * sync removal would on an inexact match; callers computing prune candidates
133
+ * should only pass entries they've already independently confirmed are safe
134
+ * to remove.
135
+ */
136
+ export function buildSchedulerRemoveOperation(id, current, artifacts, input) {
137
+ if (!current?.invocation) {
138
+ throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
139
+ }
140
+ const nativeId = exactInstalledNativeId(id, current, artifacts);
141
+ const artifact = artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
142
+ const priorFingerprint = current.signature ?? artifact?.fingerprint;
143
+ if (!artifact || priorFingerprint === undefined) {
144
+ throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
145
+ }
146
+ const logicalSource = installedLogicalSource(current.invocation, input);
147
+ const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
148
+ if (ordinal === undefined) {
149
+ throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
150
+ }
151
+ return Object.freeze({
152
+ kind: "remove",
153
+ id,
154
+ nativeId,
155
+ expected: freezeRemovalExpectation({
156
+ state: "present",
157
+ bindingId: id,
158
+ nativeId,
159
+ logicalSource,
160
+ ordinal,
161
+ invocation: current.invocation,
162
+ fingerprint: priorFingerprint,
163
+ }),
164
+ ...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
149
165
  });
150
166
  }
151
167
  function exactInstalledNativeId(logicalId, current, artifacts) {
@@ -190,10 +206,17 @@ export function assertSchedulerNativeArtifactOwnership(desired, installed) {
190
206
  const wanted = desiredByKey.get(key);
191
207
  if (!wanted)
192
208
  continue;
193
- if (artifact.nativeId !== schedulerBindingNativeId(wanted) ||
194
- artifact.bindingId !== wanted.id ||
195
- artifact.invocation === undefined ||
196
- !sameInvocation(artifact.invocation, wanted.invocation)) {
209
+ // Re-derive ownership from the artifact's own invocation content (not the
210
+ // caller-supplied `bindingId` label) so a proven owner whose invocation
211
+ // no longer matches the desired shape is an UPDATE, not a refusal — that
212
+ // reconciliation happens below in finalizeSchedulerSyncPlan. An artifact
213
+ // whose invocation content does not actually prove it belongs to
214
+ // `wanted` (unproven, malformed, or a different logical owner) is still
215
+ // a genuine collision.
216
+ const provenBindingId = artifact.invocation !== undefined
217
+ ? schedulerNativeArtifactOwner(artifact.nativeId, artifact.invocation)?.logicalId
218
+ : undefined;
219
+ if (artifact.nativeId !== schedulerBindingNativeId(wanted) || provenBindingId !== wanted.id) {
197
220
  throw nativeArtifactCollision(desiredArtifact(wanted), artifact);
198
221
  }
199
222
  }
@@ -252,12 +275,17 @@ async function compileDesiredSourceSet(input, collector) {
252
275
  const failures = [];
253
276
  await compileTaskSources(input, collector, bindings, failures);
254
277
  await compileWorkflowSources(input, collector, bindings, executableWorkflows, failures);
255
- if (failures.length > 0) {
256
- throw new UsageError(`Scheduler sync rejected the desired source set before mutation:\n${failures.map((failure) => `- ${failure}`).join("\n")}`, "TASK_SOURCE_INVALID");
257
- }
278
+ // Degrade, don't reject (#867): one source that fails to parse/prepare no
279
+ // longer poisons the whole desired set it is dropped from `desired` and
280
+ // reported here instead, so every OTHER task/workflow still reconciles.
281
+ // Genuinely cross-cutting integrity violations (duplicate ids, native
282
+ // artifact ownership conflicts, an incoherent backend inspection) are
283
+ // asserted separately in `finalizeSchedulerSyncPlan` and still hard-fail
284
+ // the whole sync — this only relaxes the per-source parse/prepare gate.
258
285
  return Object.freeze({
259
286
  desired: Object.freeze(bindings),
260
287
  executableWorkflows: Object.freeze(executableWorkflows.sort((left, right) => compareCodePoints(left.ref, right.ref))),
288
+ failures: Object.freeze(failures.sort((left, right) => compareCodePoints(left.path, right.path))),
261
289
  });
262
290
  }
263
291
  async function compileTaskSources(input, collector, out, failures) {
@@ -269,6 +297,7 @@ async function compileTaskSources(input, collector, out, failures) {
269
297
  const relative = guarded.relativePath;
270
298
  const conceptId = relative.slice(0, -4);
271
299
  const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
300
+ const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
272
301
  try {
273
302
  const physicalIdentity = guarded.physicalIdentity;
274
303
  const priorOwner = physicalOwners.get(physicalIdentity);
@@ -356,7 +385,7 @@ async function compileTaskSources(input, collector, out, failures) {
356
385
  }
357
386
  }
358
387
  catch (cause) {
359
- failures.push(taskFailure(sourcePath, cause));
388
+ failures.push(taskFailure(sourcePath, qualifiedRefForFailure, cause));
360
389
  }
361
390
  }
362
391
  }
@@ -365,6 +394,8 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
365
394
  return;
366
395
  const lookups = enumerateWorkflowLookups(input, collector, failures);
367
396
  for (const [canonicalName, sources] of lookups) {
397
+ const failurePath = sources[0]?.sourcePath ?? canonicalName;
398
+ const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
368
399
  try {
369
400
  if (sources.length > 1) {
370
401
  throw new WorkflowSourceCollisionError(input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName, sources.map((source) => source.relativePath));
@@ -427,7 +458,7 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
427
458
  }
428
459
  }
429
460
  catch (cause) {
430
- failures.push(errorMessage(cause));
461
+ failures.push(workflowFailure(failurePath, failureRef, cause));
431
462
  }
432
463
  }
433
464
  }
@@ -456,7 +487,7 @@ function enumerateWorkflowLookups(input, collector, failures) {
456
487
  const stem = authoredName.slice(0, -extension.length).toLowerCase();
457
488
  const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
458
489
  if (nestedSuffix) {
459
- failures.push(errorMessage(new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
490
+ failures.push(workflowFailure(sourcePath, undefined, new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
460
491
  continue;
461
492
  }
462
493
  const canonicalName = canonicalizeWorkflowName(authoredName);
@@ -529,9 +560,13 @@ function assertUniqueInstalledIds(installed) {
529
560
  seen.add(binding.id);
530
561
  }
531
562
  }
532
- function taskFailure(file, cause) {
563
+ function taskFailure(file, ref, cause) {
533
564
  const detail = taskSourceErrorDetail(cause);
534
- return detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
565
+ const reason = detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
566
+ return Object.freeze({ path: file, ref, reason });
567
+ }
568
+ function workflowFailure(file, ref, cause) {
569
+ return Object.freeze({ path: file, ...(ref ? { ref } : {}), reason: errorMessage(cause) });
535
570
  }
536
571
  function errorMessage(cause) {
537
572
  return cause instanceof Error ? cause.message : String(cause);
@@ -53,13 +53,14 @@ import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
53
53
  import { snapshotStrictRecord } from "../../execution/record.js";
54
54
  import { WORKFLOW_ENV_VAR_NAME_PATTERN } from "../../workflows/resource-limits.js";
55
55
  // ── Resource bounds (D2-N4: owned here, re-exported by source-v3.ts) ───────
56
- export const TASK_V3_MAX_SOURCE_BYTES = 1024 * 1024;
57
56
  export const TASK_V3_MAX_JSON_DEPTH = 64;
58
57
  export const TASK_V3_MAX_JSON_NODES = 10_000;
59
58
  export const TASK_V3_MAX_COLLECTION_ITEMS = 1024;
60
59
  export const TASK_V3_MAX_OBJECT_KEYS = 256;
61
60
  export const TASK_V3_MAX_STRING_BYTES = 256 * 1024;
62
61
  export const TASK_V3_MAX_SCHEDULES = 64;
62
+ /** Max entries in a task's `redact:` env-name list. */
63
+ export const TASK_V3_MAX_REDACT_NAMES = 32;
63
64
  export function own(value, key) {
64
65
  return Object.hasOwn(value, key);
65
66
  }
@@ -414,9 +415,6 @@ export function readBoundedTaskSourceYaml(input, options) {
414
415
  if (typeof input.yaml !== "string") {
415
416
  throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source must be a string.`, "TASK_SOURCE_INVALID");
416
417
  }
417
- if (utf8Bytes(input.yaml) > TASK_V3_MAX_SOURCE_BYTES) {
418
- throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) resource limit.`, "TASK_SOURCE_INVALID");
419
- }
420
418
  const lineCounter = new LineCounter();
421
419
  let document;
422
420
  try {
@@ -42,8 +42,8 @@ import { parseBuiltinCommandAction } from "../../commands/command/builtin-action
42
42
  import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
43
43
  import { UsageError } from "../../core/errors.js";
44
44
  import { checkJsonSchemaDefinition } from "../../core/json-schema.js";
45
- import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_RETRIES, } from "../../workflows/resource-limits.js";
46
- import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
45
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN } from "../../workflows/resource-limits.js";
46
+ import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_REDACT_NAMES, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
47
47
  export const TASK_V3_SCHEMA_VERSION = 3;
48
48
  /** Closed authoring vocabulary. Arbitrary GitHub `{0}` shell templates are not accepted. */
49
49
  export const TASK_V3_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
@@ -146,7 +146,7 @@ function parseAkm(value, ctx) {
146
146
  out.timeout = parseTimeout(input.timeout, ctx);
147
147
  if (own(input, "redact")) {
148
148
  const names = parseStringArray(input.redact, ctx, ["akm", "redact"], {
149
- max: WORKFLOW_MAX_EXEC_PASS_ENV,
149
+ max: TASK_V3_MAX_REDACT_NAMES,
150
150
  pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
151
151
  });
152
152
  if (new Set(names).size !== names.length)
@@ -160,10 +160,8 @@ function parseAkm(value, ctx) {
160
160
  out.maxSteps = input.maxSteps;
161
161
  }
162
162
  if (own(input, "maxRetries")) {
163
- if (!Number.isSafeInteger(input.maxRetries) ||
164
- input.maxRetries < 0 ||
165
- input.maxRetries > WORKFLOW_MAX_RETRIES) {
166
- sourceError(ctx, ["akm", "maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
163
+ if (!Number.isSafeInteger(input.maxRetries) || input.maxRetries < 0) {
164
+ sourceError(ctx, ["akm", "maxRetries"], "must be a non-negative safe integer.");
167
165
  }
168
166
  out.maxRetries = input.maxRetries;
169
167
  }
@@ -42,10 +42,10 @@ import { applyInputDefaults, INPUT_NAME_PATTERN, validateInputs, } from "../../e
42
42
  import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
43
43
  import { classifyTargetRef } from "../../execution/target-ref.js";
44
44
  import { detectSecretShapedParams } from "../../workflows/exec/param-secrets.js";
45
- import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_SCHEMA_BYTES, } from "../../workflows/resource-limits.js";
45
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_SCHEMA_BYTES } from "../../workflows/resource-limits.js";
46
46
  import { TASK_V3_HOST_SHELLS, TASK_V3_MAX_SCHEDULES } from "../source-v3.js";
47
47
  import { TASK_RUN_RESERVED_FLAG_NAMES, TASK_RUN_SELF_DIAGNOSED_FLAGS } from "../task-run-reserved-flags.js";
48
- import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, utf8Bytes, validateWorkingDirectory, } from "./bounded-document.js";
48
+ import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_REDACT_NAMES, utf8Bytes, validateWorkingDirectory, } from "./bounded-document.js";
49
49
  // ── Closed constants (D1, D2-N3, D2-N7) ─────────────────────────────────────
50
50
  export const TASK_SOURCE_V4_VERSION = 4;
51
51
  /** The exact, closed top-level key set (D2-N7) — `akm` and `on` are deliberately absent. */
@@ -298,7 +298,7 @@ function parseExecutionControls(input, ctx) {
298
298
  out.timeout = parseTimeoutTopLevel(input.timeout, ctx);
299
299
  if (own(input, "redact")) {
300
300
  const names = parseStringArray(input.redact, ctx, ["redact"], {
301
- max: WORKFLOW_MAX_EXEC_PASS_ENV,
301
+ max: TASK_V3_MAX_REDACT_NAMES,
302
302
  pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
303
303
  });
304
304
  if (new Set(names).size !== names.length)
@@ -312,10 +312,8 @@ function parseExecutionControls(input, ctx) {
312
312
  out.maxSteps = input.maxSteps;
313
313
  }
314
314
  if (own(input, "maxRetries")) {
315
- if (!Number.isSafeInteger(input.maxRetries) ||
316
- input.maxRetries < 0 ||
317
- input.maxRetries > WORKFLOW_MAX_RETRIES) {
318
- sourceError(ctx, ["maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
315
+ if (!Number.isSafeInteger(input.maxRetries) || input.maxRetries < 0) {
316
+ sourceError(ctx, ["maxRetries"], "must be a non-negative safe integer.");
319
317
  }
320
318
  out.maxRetries = input.maxRetries;
321
319
  }
@@ -376,9 +374,6 @@ function parseInputDeclaration(name, raw, ctx) {
376
374
  function parseInputDeclarations(value, ctx) {
377
375
  const input = asRecord(value, ctx, ["inputs"]);
378
376
  const names = Object.keys(input);
379
- if (names.length > WORKFLOW_MAX_PARAMS) {
380
- sourceError(ctx, ["inputs"], `accepts at most ${WORKFLOW_MAX_PARAMS} declared inputs.`);
381
- }
382
377
  const result = {};
383
378
  for (const name of names) {
384
379
  if (!INPUT_NAME_PATTERN.test(name)) {
@@ -7,9 +7,9 @@ import path from "node:path";
7
7
  import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
8
8
  import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
9
9
  import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
10
- import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_TIMEOUT_MS, } from "../../workflows/resource-limits.js";
10
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_TIMEOUT_MS } from "../../workflows/resource-limits.js";
11
11
  import { validateTaskId } from "../task-id.js";
12
- import { assertBoundedTaskYamlDocument, TASK_V3_MAX_SOURCE_BYTES } from "./bounded-document.js";
12
+ import { assertBoundedTaskYamlDocument, TASK_V3_MAX_REDACT_NAMES } from "./bounded-document.js";
13
13
  import { classifyTaskV3Uses, parseTaskV3Yaml } from "./task-source-v3-frozen.js";
14
14
  import { parseTaskSourceV4 } from "./task-source-v4.js";
15
15
  const V2_KEYS = new Set([
@@ -61,6 +61,27 @@ const SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
61
61
  function shellStableV2Executable(executable) {
62
62
  return executable === "akm" || executable.includes("/");
63
63
  }
64
+ /**
65
+ * `env NAME=value... cmd args...` is env(1) itself resolving and exec'ing
66
+ * `cmd` via its own PATH search — that lookup happens inside env's execvp()
67
+ * regardless of whether env was launched by direct execve (v2) or by a host
68
+ * shell (v3 `run:`). The shell-vs-argv divergence `shellStableV2Executable`
69
+ * guards against (bare names shadowed by shell aliases/builtins/functions)
70
+ * therefore does not apply to whatever env ultimately invokes, so skip past
71
+ * a leading `env` and its `NAME=value` assignments to find the real target.
72
+ * Returns the original tokens, unchanged, when there is no such target
73
+ * (e.g. `env` with nothing after its assignments).
74
+ */
75
+ function skipEnvAssignmentPrefix(tokens) {
76
+ if (tokens[0] !== "env")
77
+ return { tokens, envWrapped: false };
78
+ let index = 1;
79
+ while (index < tokens.length && SHELL_ASSIGNMENT_WORD.test(tokens[index]))
80
+ index += 1;
81
+ if (index >= tokens.length)
82
+ return { tokens, envWrapped: false };
83
+ return { tokens: tokens.slice(index), envWrapped: true };
84
+ }
64
85
  const KNOWN_PROMPT_REF_FAMILIES = new Set([
65
86
  "agents",
66
87
  "commands",
@@ -81,15 +102,6 @@ function hash(bytes) {
81
102
  return crypto.createHash("sha256").update(bytes).digest("hex");
82
103
  }
83
104
  function base(input) {
84
- const inspectionIdentity = input.inspectionIdentity
85
- ? Object.freeze({
86
- file: Object.freeze({ ...input.inspectionIdentity.file }),
87
- root: Object.freeze({ ...input.inspectionIdentity.root }),
88
- ...(input.inspectionIdentity.bundleRoot
89
- ? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) }
90
- : {}),
91
- })
92
- : undefined;
93
105
  return {
94
106
  filePath: input.filePath,
95
107
  before: Buffer.from(input.bytes),
@@ -98,7 +110,6 @@ function base(input) {
98
110
  writable: input.writable,
99
111
  ...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
100
112
  ...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
101
- ...(inspectionIdentity ? { inspectionIdentity } : {}),
102
113
  };
103
114
  }
104
115
  function blocked(input, reason, detail) {
@@ -124,9 +135,6 @@ function optionalString(value, label) {
124
135
  return exactString(value, label);
125
136
  }
126
137
  function parseLegacyTaskYaml(input) {
127
- if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
128
- throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
129
- }
130
138
  let source;
131
139
  try {
132
140
  source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
@@ -178,7 +186,7 @@ function validateCommonV2(data) {
178
186
  }
179
187
  if (data.redact !== undefined && data.redact !== null) {
180
188
  if (!Array.isArray(data.redact) ||
181
- data.redact.length > WORKFLOW_MAX_EXEC_PASS_ENV ||
189
+ data.redact.length > TASK_V3_MAX_REDACT_NAMES ||
182
190
  data.redact.some((entry) => typeof entry !== "string" || !WORKFLOW_ENV_VAR_NAME_PATTERN.test(entry))) {
183
191
  throw new Error("redact must contain only bounded environment variable names");
184
192
  }
@@ -310,10 +318,8 @@ function migratedObject(data) {
310
318
  akm.maxSteps = data.maxSteps;
311
319
  }
312
320
  if (data.maxRetries !== undefined && data.maxRetries !== null) {
313
- if (!Number.isSafeInteger(data.maxRetries) ||
314
- data.maxRetries < 0 ||
315
- data.maxRetries > WORKFLOW_MAX_RETRIES) {
316
- throw new Error(`maxRetries must be from 0 through ${WORKFLOW_MAX_RETRIES}`);
321
+ if (!Number.isSafeInteger(data.maxRetries) || data.maxRetries < 0) {
322
+ throw new Error("maxRetries must be a non-negative integer");
317
323
  }
318
324
  akm.maxRetries = data.maxRetries;
319
325
  }
@@ -348,8 +354,9 @@ function migratedObject(data) {
348
354
  if (tokens.length === 0 || tokens.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
349
355
  return "shell-operators-change-v2-literal-argv-semantics";
350
356
  }
351
- const executable = tokens[0];
352
- if (SHELL_ASSIGNMENT_WORD.test(executable) || !shellStableV2Executable(executable)) {
357
+ const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens);
358
+ const executable = targetTokens[0];
359
+ if (SHELL_ASSIGNMENT_WORD.test(executable) || (!envWrapped && !shellStableV2Executable(executable))) {
353
360
  return "shell-command-resolution-changes-v2-literal-argv-semantics";
354
361
  }
355
362
  output.run = tokens.join(" ");
@@ -18,7 +18,7 @@
18
18
  import crypto from "node:crypto";
19
19
  import path from "node:path";
20
20
  import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
21
- import { assertBoundedTaskYamlDocument, TASK_V3_MAX_SOURCE_BYTES } from "./bounded-document.js";
21
+ import { assertBoundedTaskYamlDocument } from "./bounded-document.js";
22
22
  import { classifyTaskV3Uses } from "./task-source-v3-frozen.js";
23
23
  import { parseTaskSourceV4 } from "./task-source-v4.js";
24
24
  /** The closed v3 top-level key set (`src/tasks/source-v3.ts`'s own, vendored — not exported there). */
@@ -76,15 +76,6 @@ function causeMessage(cause) {
76
76
  return cause instanceof Error ? cause.message : String(cause);
77
77
  }
78
78
  function base(input) {
79
- const inspectionIdentity = input.inspectionIdentity
80
- ? Object.freeze({
81
- file: Object.freeze({ ...input.inspectionIdentity.file }),
82
- root: Object.freeze({ ...input.inspectionIdentity.root }),
83
- ...(input.inspectionIdentity.bundleRoot
84
- ? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) }
85
- : {}),
86
- })
87
- : undefined;
88
79
  return {
89
80
  filePath: input.filePath,
90
81
  before: Buffer.from(input.bytes),
@@ -93,7 +84,6 @@ function base(input) {
93
84
  writable: input.writable,
94
85
  ...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
95
86
  ...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
96
- ...(inspectionIdentity ? { inspectionIdentity } : {}),
97
87
  };
98
88
  }
99
89
  function blocked(input, reason, detail) {
@@ -124,9 +114,6 @@ function exactString(value, label, nonempty = false) {
124
114
  * away (C-N1).
125
115
  */
126
116
  function parseV3RawYaml(input) {
127
- if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
128
- throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
129
- }
130
117
  let source;
131
118
  try {
132
119
  source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
@@ -1,13 +1,13 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
- import { TASK_V3_MAX_SCHEDULES, TASK_V3_MAX_SOURCE_BYTES } from "./source/bounded-document.js";
4
+ import { TASK_V3_MAX_SCHEDULES } from "./source/bounded-document.js";
5
5
  // D2-N4 (spec docs/plans/specs/p2a-task-source-v4.md §3.1, §9): re-exported
6
- // at their EXISTING names so no importer changes. Only the two names a
7
- // surviving `src` consumer still imports (task-source-v4.ts,
8
- // commands/tasks/tasks.ts) — every other bounded-document re-export this
9
- // file used to carry was dropped in P4 §3.2.3.
10
- export { TASK_V3_MAX_SCHEDULES, TASK_V3_MAX_SOURCE_BYTES };
6
+ // at its EXISTING name so no importer changes. The one name a surviving
7
+ // `src` consumer still imports (task-source-v4.ts) — every other
8
+ // bounded-document re-export this file used to carry was dropped in P4
9
+ // §3.2.3.
10
+ export { TASK_V3_MAX_SCHEDULES };
11
11
  export const TASK_EXTENSION = ".yml";
12
12
  export const TASK_NEAR_MISS_EXTENSION = ".yaml";
13
13
  /** Explain why the near-miss `.yaml` spelling is not a task source. */
@@ -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