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.
- package/CHANGELOG.md +265 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/reflect.js +20 -9
- package/dist/commands/lint/index.js +4 -6
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +101 -18
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +7 -7
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/improve-result.js +35 -14
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/output/shapes/passthrough.js +17 -5
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +180 -238
- package/dist/scripts/akm-migrate.js +180 -238
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/storage/repositories/proposals-repository.js +32 -6
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +29 -22
- package/dist/tasks/source/task-to-v4.js +1 -14
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/cli.md +25 -3
- package/docs/reference/configuration.md +28 -7
- package/docs/reference/tasks.md +10 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- 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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 >
|
|
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
|
-
|
|
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
|
|
352
|
-
|
|
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
|
|
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);
|
package/dist/tasks/source-v3.js
CHANGED
|
@@ -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
|
|
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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
export { TASK_V3_MAX_SCHEDULES
|
|
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 {
|
|
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 {
|
|
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
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
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
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
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:
|
|
610
|
-
error: input.budget.budgetMessage ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* `budget.
|
|
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
|