akm-cli 0.9.5 → 0.9.7

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 (130) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/dist/assets/hints/cli-hints-full.md +3 -3
  3. package/dist/assets/improve-strategies/catchup.json +40 -11
  4. package/dist/assets/improve-strategies/thorough.json +45 -7
  5. package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
  6. package/dist/commands/agent/contribute-cli.js +11 -0
  7. package/dist/commands/env/env-cli.js +1 -2
  8. package/dist/commands/env/secret-cli.js +1 -5
  9. package/dist/commands/feedback-cli.js +0 -4
  10. package/dist/commands/health/checks.js +0 -32
  11. package/dist/commands/health/surfaces.js +2 -2
  12. package/dist/commands/health.js +6 -15
  13. package/dist/commands/improve/autonomy-gate.js +1 -1
  14. package/dist/commands/improve/consolidate.js +25 -26
  15. package/dist/commands/improve/distill.js +2 -2
  16. package/dist/commands/improve/extract.js +8 -1
  17. package/dist/commands/improve/improve-cli.js +1 -1
  18. package/dist/commands/improve/improve-strategies.js +0 -4
  19. package/dist/commands/improve/improve.js +3 -5
  20. package/dist/commands/improve/locks.js +13 -5
  21. package/dist/commands/improve/loop-stages.js +8 -9
  22. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  23. package/dist/commands/improve/memory/memory-improve.js +2 -1
  24. package/dist/commands/improve/outcome-loop.js +0 -6
  25. package/dist/commands/improve/preparation.js +1 -1
  26. package/dist/commands/improve/reflect.js +21 -10
  27. package/dist/commands/lint/base-linter.js +141 -18
  28. package/dist/commands/lint/index.js +21 -10
  29. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  30. package/dist/commands/read/curate.js +47 -0
  31. package/dist/commands/read/search-cli.js +24 -1
  32. package/dist/commands/sources/bundle-config-ops.js +9 -4
  33. package/dist/commands/sources/info.js +14 -10
  34. package/dist/commands/tasks/tasks.js +12 -16
  35. package/dist/commands/workflow-cli.js +2 -2
  36. package/dist/core/asset/asset-placement.js +13 -2
  37. package/dist/core/asset/frontmatter.js +116 -0
  38. package/dist/core/asset/memory-archive.js +97 -0
  39. package/dist/core/common.js +61 -24
  40. package/dist/core/config/config-io.js +2 -2
  41. package/dist/core/config/config-sources.js +32 -2
  42. package/dist/core/config/config.js +1 -1
  43. package/dist/core/config/engine-semantics.js +0 -2
  44. package/dist/core/config/schema/engines.js +9 -7
  45. package/dist/core/config/schema/primitives.js +0 -5
  46. package/dist/core/config/schema/search.js +2 -1
  47. package/dist/core/file-lock.js +2 -1
  48. package/dist/core/maintenance-barrier.js +2 -14
  49. package/dist/core/paths.js +0 -3
  50. package/dist/core/redaction.js +2 -2
  51. package/dist/core/spawn-env.js +8 -12
  52. package/dist/core/state/migrations.js +1 -12
  53. package/dist/core/state-db.js +9 -27
  54. package/dist/core/write-source.js +12 -19
  55. package/dist/execution/directory-identity.js +36 -10
  56. package/dist/indexer/graph/graph-boost.js +0 -4
  57. package/dist/indexer/index-writer-lock.js +43 -24
  58. package/dist/indexer/index-written-assets.js +5 -6
  59. package/dist/indexer/indexer.js +2 -39
  60. package/dist/indexer/materialize-embeddings.js +85 -41
  61. package/dist/indexer/search/db-search.js +15 -48
  62. package/dist/indexer/search/ranking-contributors.js +0 -25
  63. package/dist/indexer/search/ranking.js +3 -13
  64. package/dist/integrations/agent/builder-shared.js +0 -25
  65. package/dist/integrations/agent/model-map.js +2 -60
  66. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  67. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  68. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  69. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  70. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  71. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  72. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  73. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  74. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  75. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  76. package/dist/integrations/session-logs/index.js +0 -9
  77. package/dist/llm/client.js +75 -42
  78. package/dist/llm/embedder.js +7 -3
  79. package/dist/llm/embedders/remote.js +141 -42
  80. package/dist/registry/network.js +5 -37
  81. package/dist/runtime.js +2 -10
  82. package/dist/scripts/akm-migrate-node.js +73 -133
  83. package/dist/scripts/akm-migrate.js +73 -133
  84. package/dist/setup/engine-config.js +2 -5
  85. package/dist/setup/registry-stash-loader.js +0 -8
  86. package/dist/setup/setup.js +9 -46
  87. package/dist/setup/steps/connection-shared.js +10 -13
  88. package/dist/sources/providers/git-install.js +1 -1
  89. package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
  90. package/dist/storage/engines/sqlite-migrations.js +20 -1
  91. package/dist/storage/repositories/index-connection.js +45 -3
  92. package/dist/storage/repositories/index-entries-repository.js +0 -15
  93. package/dist/tasks/backends/cron.js +49 -9
  94. package/dist/tasks/backends/launchd.js +15 -20
  95. package/dist/tasks/backends/schtasks.js +18 -8
  96. package/dist/tasks/resolve-akm-bin.js +17 -2
  97. package/dist/tasks/run/run-native-task.js +8 -6
  98. package/dist/tasks/scheduler-invocation.js +8 -1
  99. package/dist/tasks/source/bounded-document.js +2 -4
  100. package/dist/tasks/source/parse-task-source.js +23 -9
  101. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  102. package/dist/tasks/source/task-source-v4.js +5 -10
  103. package/dist/tasks/source/task-to-v3.js +5 -10
  104. package/dist/tasks/source/task-to-v4.js +1 -4
  105. package/dist/tasks/source-v3.js +6 -6
  106. package/dist/workflows/exec/native-executor.js +21 -31
  107. package/dist/workflows/exec/run-workflow.js +5 -6
  108. package/dist/workflows/exec/scheduler.js +3 -19
  109. package/dist/workflows/exec/step-work.js +1 -4
  110. package/dist/workflows/exec/unit-dispatch.js +2 -2
  111. package/dist/workflows/exec/worktree.js +1 -13
  112. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  113. package/dist/workflows/ir/plan-hash.js +4 -6
  114. package/dist/workflows/ir/schema-v4.js +0 -12
  115. package/dist/workflows/ir/schema.js +20 -31
  116. package/dist/workflows/parser.js +11 -52
  117. package/dist/workflows/renderer.js +2 -3
  118. package/dist/workflows/resource-limits.js +11 -41
  119. package/dist/workflows/runtime/runs.js +3 -4
  120. package/dist/workflows/source-ir/schema.js +14 -30
  121. package/dist/workflows/validator.js +1 -7
  122. package/docs/reference/cli.md +5 -1
  123. package/docs/reference/configuration.md +1 -1
  124. package/package.json +1 -1
  125. package/schemas/akm-config.json +4 -23
  126. package/schemas/akm-task.json +1 -2
  127. package/schemas/akm-workflow.json +1 -13
  128. package/dist/assets/improve-strategies/frequent.json +0 -15
  129. package/dist/assets/improve-strategies/memory-focus.json +0 -15
  130. package/dist/indexer/search/semantic-status.js +0 -142
@@ -10,7 +10,11 @@
10
10
  * Resolution order:
11
11
  *
12
12
  * 1. `process.execPath` alone for a Bun standalone executable.
13
- * 2. Absolute Node plus the public `dist/akm` package launcher.
13
+ * 2. Absolute Node plus the public `dist/akm` package launcher — eligible
14
+ * when it is the active npm global install, or when this process
15
+ * cannot write to the directory containing it (a read-only mount, e.g.
16
+ * an image-baked install, gives the same "won't change out from under
17
+ * the scheduler" guarantee npm-global ownership does).
14
18
  * 3. Absolute runtime plus the source/build CLI entry, classified as a
15
19
  * checkout that requires explicit `--rebind` for scheduler writes.
16
20
  *
@@ -51,12 +55,13 @@ export function resolveAkmInvocation(options = {}) {
51
55
  }
52
56
  }
53
57
  const npmGlobal = !checkout && packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot);
58
+ const readOnlyInstall = !checkout && !npmGlobal && !(options.isPathWritable ?? isPathWritable)(path.dirname(launcherPath));
54
59
  const kind = checkout ? "checkout" : npmGlobal ? "npm" : "package-local";
55
60
  return {
56
61
  argv: [absoluteInvocationPath(nodePath), absoluteInvocationPath(launcherPath)],
57
62
  via: kind,
58
63
  kind,
59
- eligible: npmGlobal,
64
+ eligible: npmGlobal || readOnlyInstall,
60
65
  };
61
66
  }
62
67
  const checkoutEntry = resolveCheckoutEntry(options.cliEntryUrl ?? import.meta.url, runtime, mainPath);
@@ -117,6 +122,16 @@ function isCheckoutLauncher(file) {
117
122
  const packageRoot = path.dirname(path.dirname(launcher));
118
123
  return fs.existsSync(path.join(packageRoot, ".git"));
119
124
  }
125
+ /** Whether this process can write to `dir` — false also covers a read-only mount. */
126
+ function isPathWritable(dir) {
127
+ try {
128
+ fs.accessSync(dir, fs.constants.W_OK);
129
+ return true;
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
120
135
  function packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot) {
121
136
  if (!npmGlobalRoot)
122
137
  return false;
@@ -20,7 +20,7 @@
20
20
  import path from "node:path";
21
21
  import { assertNever } from "../../core/assert.js";
22
22
  import { runManagedSubprocess, streamCaptureFailure } from "../../core/subprocess.js";
23
- import { assertFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
23
+ import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
24
24
  import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } from "../frozen-script.js";
25
25
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
26
26
  import { finishAttempt } from "./attempt-lifecycle.js";
@@ -146,11 +146,13 @@ export async function runNativeTask(input) {
146
146
  const dbLines = [{ line: header }];
147
147
  let exitCode = null;
148
148
  try {
149
- // The projector froze both canonical paths and filesystem identities before
150
- // history mutation. Re-resolve the authored root/cwd immediately before
151
- // spawn so a symlink, ancestor, bundle-root, or directory/file swap cannot
152
- // redirect execution outside that physical workspace.
153
- assertFrozenDirectoryIdentity(task.cwdIdentity);
149
+ // Re-resolve the authored root/cwd immediately before spawn so a
150
+ // symlink, ancestor, bundle-root, or directory/file swap cannot redirect
151
+ // execution outside that physical workspace. This is a live containment
152
+ // check, not an identity comparison — it does not care whether the
153
+ // underlying device/inode changed (e.g. a remount), only whether the
154
+ // resolved cwd still lives inside its root.
155
+ assertFrozenDirectoryContained(task.cwdIdentity);
154
156
  if (task.kind === "script") {
155
157
  materialized = materializeFrozenScript(task);
156
158
  cmd = frozenScriptCommand(task, materialized.file);
@@ -189,7 +189,14 @@ export function parseScheduledBindingArgv(argv) {
189
189
  ...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
190
190
  };
191
191
  }
192
- function parsePublicSchedulerInvocation(invocation) {
192
+ /**
193
+ * Parse just the public `task run …` / `workflow run …` tail, with no
194
+ * `--scheduler-context` wrapper. Exported so a backend can recognize a
195
+ * pre-`--scheduler-context` invocation still sitting inside akm's own
196
+ * ownership-marked block (see `extractCronInvocation` in
197
+ * `src/tasks/backends/cron.ts`) without re-implementing this grammar.
198
+ */
199
+ export function parsePublicSchedulerInvocation(invocation) {
193
200
  if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
194
201
  try {
195
202
  if (normaliseTaskConceptId(invocation[2]) !== invocation[2])
@@ -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 {
@@ -17,7 +17,7 @@
17
17
  * | root `version` | outcome |
18
18
  * |------------------------|-----------------------------------------------------------------|
19
19
  * | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
20
- * | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) — the shim removes friction for the deterministic case, it never hides a real problem |
20
+ * | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape — the file needs a human decision, not a re-run), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED` naming the specific blocked reason (issue #869) — the shim removes friction for the deterministic case, it never hides a real problem |
21
21
  * | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
22
22
  * | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
23
23
  *
@@ -57,6 +57,18 @@ export function peekTaskSourceVersion(root) {
57
57
  return typeof value === "number" ? value : undefined;
58
58
  }
59
59
  const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
60
+ /**
61
+ * Thrown only when the deterministic conversion itself could not produce a
62
+ * task source v4 document — a case where a person must decide the intended
63
+ * behavior (e.g. an ambiguous shell command), not one the migrator can just
64
+ * be re-run to fix. `reason`/`detail` are the SAME blocked outcome
65
+ * `akm migrate status`/`apply` reports for this file, so the message names
66
+ * the actual decision instead of pointing at a command that will report the
67
+ * identical block.
68
+ */
69
+ function unmigratableVersionError(filePath, version, reason, detail) {
70
+ return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
71
+ }
60
72
  function unsupportedVersionError(filePath, version) {
61
73
  return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
62
74
  }
@@ -64,9 +76,10 @@ function unsupportedVersionError(filePath, version) {
64
76
  * Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
65
77
  * chained v2->v3->v4) migration planner(s) — never touches disk, never
66
78
  * writes the file, never re-reads it from disk. Returns the produced v4
67
- * YAML text, or `undefined` when the deterministic conversion cannot
68
- * proceed (an unmigratable v2/v3 shape) — the caller falls back to the same
69
- * hard error this gate threw before the shim existed.
79
+ * YAML text, or the blocked reason/detail when the deterministic conversion
80
+ * cannot proceed (an unmigratable v2/v3 shape) — the caller falls back to
81
+ * the same hard error this gate threw before the shim existed, now naming
82
+ * that reason.
70
83
  */
71
84
  function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
72
85
  const bytes = Buffer.from(yaml, "utf8");
@@ -89,12 +102,12 @@ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
89
102
  else {
90
103
  const v3Outcome = planTaskToV3File(baseInput);
91
104
  if (v3Outcome.status !== "changed")
92
- return undefined;
105
+ return { reason: v3Outcome.reason, detail: v3Outcome.detail };
93
106
  v3Bytes = v3Outcome.after;
94
107
  }
95
108
  const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
96
109
  if (v4Outcome.status !== "changed")
97
- return undefined;
110
+ return { reason: v4Outcome.reason, detail: v4Outcome.detail };
98
111
  return v4Outcome.after.toString("utf8");
99
112
  }
100
113
  /** Parse task source YAML, routing per the terminal table above. */
@@ -103,16 +116,17 @@ export function parseTaskSource(input) {
103
116
  const version = peekTaskSourceVersion(root);
104
117
  if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
105
118
  if (version === 2 || version === 3) {
106
- const v4Yaml = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
107
- if (v4Yaml !== undefined) {
119
+ const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
120
+ if (typeof shimmed === "string") {
108
121
  const v4 = parseTaskSourceV4({
109
- yaml: v4Yaml,
122
+ yaml: shimmed,
110
123
  filePath: input.filePath,
111
124
  ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
112
125
  });
113
126
  warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
114
127
  return Object.freeze({ version: 4, v4 });
115
128
  }
129
+ throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
116
130
  }
117
131
  throw unsupportedVersionError(input.filePath, version);
118
132
  }
@@ -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([
@@ -135,9 +135,6 @@ function optionalString(value, label) {
135
135
  return exactString(value, label);
136
136
  }
137
137
  function parseLegacyTaskYaml(input) {
138
- if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
139
- throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
140
- }
141
138
  let source;
142
139
  try {
143
140
  source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
@@ -189,7 +186,7 @@ function validateCommonV2(data) {
189
186
  }
190
187
  if (data.redact !== undefined && data.redact !== null) {
191
188
  if (!Array.isArray(data.redact) ||
192
- data.redact.length > WORKFLOW_MAX_EXEC_PASS_ENV ||
189
+ data.redact.length > TASK_V3_MAX_REDACT_NAMES ||
193
190
  data.redact.some((entry) => typeof entry !== "string" || !WORKFLOW_ENV_VAR_NAME_PATTERN.test(entry))) {
194
191
  throw new Error("redact must contain only bounded environment variable names");
195
192
  }
@@ -321,10 +318,8 @@ function migratedObject(data) {
321
318
  akm.maxSteps = data.maxSteps;
322
319
  }
323
320
  if (data.maxRetries !== undefined && data.maxRetries !== null) {
324
- if (!Number.isSafeInteger(data.maxRetries) ||
325
- data.maxRetries < 0 ||
326
- data.maxRetries > WORKFLOW_MAX_RETRIES) {
327
- 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");
328
323
  }
329
324
  akm.maxRetries = data.maxRetries;
330
325
  }
@@ -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). */
@@ -114,9 +114,6 @@ function exactString(value, label, nonempty = false) {
114
114
  * away (C-N1).
115
115
  */
116
116
  function parseV3RawYaml(input) {
117
- if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
118
- throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
119
- }
120
117
  let source;
121
118
  try {
122
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
@@ -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`