intentdna 1.9.5 → 1.9.8
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/run.js +13 -10
- package/dist/cli/index.js +4 -4
- package/dist/runtime/canonical-run-service.js +8 -1
- package/dist/runtime/local-execution-authority.d.ts +2 -0
- package/dist/runtime/local-execution-authority.js +101 -33
- package/dist/runtime/local-windows-execution-supervisor-script.js +5 -3
- package/dist/runtime/workflow-runner.d.ts +1 -1
- package/dist/runtime/workflow-runner.js +10 -3
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "intentdna",
|
|
11
11
|
"description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
|
|
12
|
-
"version": "1.9.
|
|
12
|
+
"version": "1.9.8",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Samuel"
|
|
15
15
|
},
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"version": "1.9.
|
|
28
|
+
"version": "1.9.8"
|
|
29
29
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -170,15 +170,15 @@ function readCanonicalMetadata(value) {
|
|
|
170
170
|
|| !hasExactKeys(value.provider, [
|
|
171
171
|
"kind",
|
|
172
172
|
"executable",
|
|
173
|
-
"max_budget_usd",
|
|
173
|
+
...(value.provider.max_budget_usd === undefined ? [] : ["max_budget_usd"]),
|
|
174
174
|
"permission_mode",
|
|
175
175
|
])
|
|
176
176
|
|| (value.provider.kind !== "claude" && value.provider.kind !== "codex")
|
|
177
177
|
|| (value.provider.executable !== null && (typeof value.provider.executable !== "string"
|
|
178
178
|
|| value.provider.executable.length === 0))
|
|
179
|
-
|| typeof value.provider.max_budget_usd !== "number"
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
|| (value.provider.max_budget_usd !== undefined && (typeof value.provider.max_budget_usd !== "number"
|
|
180
|
+
|| !Number.isFinite(value.provider.max_budget_usd)
|
|
181
|
+
|| value.provider.max_budget_usd <= 0))
|
|
182
182
|
|| typeof value.provider.permission_mode !== "string"
|
|
183
183
|
|| value.provider.permission_mode.length === 0
|
|
184
184
|
|| typeof value.max_concurrency !== "number"
|
|
@@ -220,7 +220,9 @@ function providerConfiguration(metadata) {
|
|
|
220
220
|
...(metadata.provider.executable === null
|
|
221
221
|
? {}
|
|
222
222
|
: { executable: metadata.provider.executable }),
|
|
223
|
-
|
|
223
|
+
...(metadata.provider.max_budget_usd === undefined
|
|
224
|
+
? {}
|
|
225
|
+
: { max_budget_usd: metadata.provider.max_budget_usd }),
|
|
224
226
|
permission_mode: metadata.provider.permission_mode,
|
|
225
227
|
},
|
|
226
228
|
credential_references: [],
|
|
@@ -299,8 +301,8 @@ function createProvider(binding) {
|
|
|
299
301
|
}
|
|
300
302
|
const maxBudget = configuration.options.max_budget_usd;
|
|
301
303
|
const permissionMode = configuration.options.permission_mode;
|
|
302
|
-
if (
|
|
303
|
-
throw new Error("Claude run binding is missing launch
|
|
304
|
+
if (permissionMode === undefined) {
|
|
305
|
+
throw new Error("Claude run binding is missing launch permission mode");
|
|
304
306
|
}
|
|
305
307
|
const mcpConfigPath = findProjectMcpConfig(binding.workspace_root);
|
|
306
308
|
return createClaudeExecutionProvider({
|
|
@@ -308,8 +310,7 @@ function createProvider(binding) {
|
|
|
308
310
|
agentName: (packet) => `dna-${toKebabCase(packet.role)}`,
|
|
309
311
|
mcpConfigPath,
|
|
310
312
|
extraArgs: [
|
|
311
|
-
"--max-budget-usd",
|
|
312
|
-
String(maxBudget),
|
|
313
|
+
...(maxBudget === undefined ? [] : ["--max-budget-usd", String(maxBudget)]),
|
|
313
314
|
"--permission-mode",
|
|
314
315
|
permissionMode,
|
|
315
316
|
],
|
|
@@ -425,7 +426,9 @@ function startMetadata(opts, projectDirectory) {
|
|
|
425
426
|
provider: {
|
|
426
427
|
kind: provider,
|
|
427
428
|
executable: opts.providerExecutable === undefined ? null : requireText(opts.providerExecutable, "--provider-executable"),
|
|
428
|
-
|
|
429
|
+
...(opts.maxBudget === undefined
|
|
430
|
+
? {}
|
|
431
|
+
: { max_budget_usd: requirePositiveNumber(opts.maxBudget, "--max-budget") }),
|
|
429
432
|
// Durable workers have no interactive prompt available for tool approvals.
|
|
430
433
|
// Keep an explicitly supplied mode unchanged, but make the unattended
|
|
431
434
|
// default writable so required artifacts can be produced.
|
package/dist/cli/index.js
CHANGED
|
@@ -310,7 +310,7 @@ Options:
|
|
|
310
310
|
--max-concurrency <n> Maximum simultaneously active Steps (default: 1)
|
|
311
311
|
--timeout-ms <n> Per-attempt timeout in milliseconds
|
|
312
312
|
--cancellation-grace-ms <n> Process-tree cancellation grace period (default: 5000)
|
|
313
|
-
--max-budget <usd> Claude budget per Step attempt (default
|
|
313
|
+
--max-budget <usd> Optional Claude budget per Step attempt (no budget by default)
|
|
314
314
|
--permission-mode <mode> Claude permission mode (default: bypassPermissions for unattended runs)
|
|
315
315
|
--agents <dir> Project agent output directory (default: .claude/agents)
|
|
316
316
|
--reason <text> Cancellation or closure reason
|
|
@@ -588,9 +588,9 @@ async function main() {
|
|
|
588
588
|
context: runValues.context,
|
|
589
589
|
vars: Object.keys(vars).length > 0 ? vars : undefined,
|
|
590
590
|
dryRun: runValues["dry-run"],
|
|
591
|
-
maxBudget: runValues["max-budget"]
|
|
592
|
-
?
|
|
593
|
-
:
|
|
591
|
+
maxBudget: runValues["max-budget"] === undefined
|
|
592
|
+
? undefined
|
|
593
|
+
: Number(runValues["max-budget"]),
|
|
594
594
|
logDir: runValues["log-dir"],
|
|
595
595
|
permissionMode: runValues["permission-mode"],
|
|
596
596
|
agentsDir: runValues.agents,
|
|
@@ -3560,7 +3560,14 @@ export class CanonicalRunService {
|
|
|
3560
3560
|
resolution = await this.executionAuthority.requestStop(stopRequest);
|
|
3561
3561
|
}
|
|
3562
3562
|
catch (error) {
|
|
3563
|
-
|
|
3563
|
+
const details = error !== null && typeof error === "object"
|
|
3564
|
+
? error
|
|
3565
|
+
: {};
|
|
3566
|
+
const diagnostic = [
|
|
3567
|
+
"control_timeout", "control_connection", "control_empty_response",
|
|
3568
|
+
"identity_conflict", "corrupt_state", "invalid_configuration", "unsupported",
|
|
3569
|
+
].find((code) => code === details.diagnostic_code || code === details.code);
|
|
3570
|
+
throw new CanonicalRunServiceError("unsupported", `worker stop authority is unavailable${diagnostic === undefined ? "" : ` (${diagnostic})`}`, { cause: error });
|
|
3564
3571
|
}
|
|
3565
3572
|
const matches = resolution?.kind === "stopped_by_request"
|
|
3566
3573
|
? resolution.authority !== undefined
|
|
@@ -185,7 +185,9 @@ export declare class LocalAttemptExecutionAuthority implements AttemptExecutionA
|
|
|
185
185
|
private executeSupervised;
|
|
186
186
|
private observeRecoveredExecution;
|
|
187
187
|
private recordNormalizedCompletion;
|
|
188
|
+
private recoverControlCompletion;
|
|
188
189
|
private sendControl;
|
|
190
|
+
private sendControlOnce;
|
|
189
191
|
private issueReceipt;
|
|
190
192
|
private issueAlreadyStoppedEvidence;
|
|
191
193
|
withStoppedExecution<T>(request: AttemptExecutionAuthorityRequest, operation: (authority: StoppedExecutionAuthority) => Promise<T>): Promise<T>;
|
|
@@ -31,6 +31,13 @@ export class LocalExecutionAuthorityError extends Error {
|
|
|
31
31
|
this.code = code;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
class LocalExecutionControlTransportError extends LocalExecutionAuthorityError {
|
|
35
|
+
diagnostic_code;
|
|
36
|
+
constructor(diagnostic_code, message, options) {
|
|
37
|
+
super("unsupported", message, options);
|
|
38
|
+
this.diagnostic_code = diagnostic_code;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
34
41
|
function durableLockOwnersEqual(left, right) {
|
|
35
42
|
return left !== null && canonicalizeJson(left) === canonicalizeJson(right);
|
|
36
43
|
}
|
|
@@ -1561,22 +1568,24 @@ export class LocalAttemptExecutionAuthority {
|
|
|
1561
1568
|
if (initial.backend.kind !== "windows_job" || initial.raw_completion !== null)
|
|
1562
1569
|
return initial;
|
|
1563
1570
|
await this.ensureWindowsBackendTrusted(initial, path);
|
|
1564
|
-
|
|
1571
|
+
const deadline = Date.now() + this.controlTimeoutMs;
|
|
1565
1572
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1566
1573
|
const before = this.readRecord(path);
|
|
1567
1574
|
if (before.raw_completion !== null || before.phase === "unsupported")
|
|
1568
1575
|
return before;
|
|
1569
1576
|
if (before.phase !== "running" || before.supervisor.pid === null || before.supervisor.ready_at === null) {
|
|
1570
|
-
|
|
1571
|
-
break;
|
|
1577
|
+
throw new LocalExecutionControlTransportError("control_connection", "Windows execution supervisor is not durably ready");
|
|
1572
1578
|
}
|
|
1573
1579
|
try {
|
|
1574
|
-
const response = await this.sendControl(before, { action: "probe" });
|
|
1580
|
+
const response = await this.sendControl(before, { action: "probe" }, deadline);
|
|
1575
1581
|
const after = this.readRecord(path);
|
|
1576
1582
|
if (after.raw_completion !== null || after.phase === "unsupported")
|
|
1577
1583
|
return after;
|
|
1578
|
-
if (after.revision !== before.revision)
|
|
1584
|
+
if (after.revision !== before.revision) {
|
|
1585
|
+
if (Date.now() >= deadline)
|
|
1586
|
+
break;
|
|
1579
1587
|
continue;
|
|
1588
|
+
}
|
|
1580
1589
|
const identity = response.identity;
|
|
1581
1590
|
const expected = {
|
|
1582
1591
|
record_revision: after.revision,
|
|
@@ -1597,14 +1606,15 @@ export class LocalAttemptExecutionAuthority {
|
|
|
1597
1606
|
return after;
|
|
1598
1607
|
}
|
|
1599
1608
|
catch (error) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1609
|
+
if (error instanceof LocalExecutionControlTransportError)
|
|
1610
|
+
throw error;
|
|
1611
|
+
const message = error instanceof Error
|
|
1612
|
+
? error.message
|
|
1613
|
+
: "Windows execution supervisor identity could not be authenticated";
|
|
1614
|
+
return this.failWindowsAuthentication(path, message, error);
|
|
1602
1615
|
}
|
|
1603
1616
|
}
|
|
1604
|
-
|
|
1605
|
-
? lastError.message
|
|
1606
|
-
: "Windows execution supervisor identity could not be authenticated";
|
|
1607
|
-
return this.failWindowsAuthentication(path, message, lastError);
|
|
1617
|
+
throw new LocalExecutionControlTransportError("control_timeout", "Windows execution supervisor identity changed during control authentication");
|
|
1608
1618
|
}
|
|
1609
1619
|
readRecoveryRecord(request) {
|
|
1610
1620
|
const identity = recoveryIdentity(request);
|
|
@@ -2619,7 +2629,64 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2619
2629
|
};
|
|
2620
2630
|
});
|
|
2621
2631
|
}
|
|
2622
|
-
|
|
2632
|
+
recoverControlCompletion(record, request) {
|
|
2633
|
+
const durable = this.readRecord(this.recordPath(record.identity));
|
|
2634
|
+
if (canonicalizeJson(durable.identity) !== canonicalizeJson(record.identity)
|
|
2635
|
+
|| durable.identity_digest !== record.identity_digest
|
|
2636
|
+
|| durable.execution_instance_id !== record.execution_instance_id
|
|
2637
|
+
|| durable.authority_instance_id !== record.authority_instance_id
|
|
2638
|
+
|| durable.challenge !== record.challenge
|
|
2639
|
+
|| canonicalizeJson(durable.backend) !== canonicalizeJson(record.backend)
|
|
2640
|
+
|| canonicalizeJson(durable.supervisor) !== canonicalizeJson(record.supervisor)
|
|
2641
|
+
|| durable.revision < record.revision) {
|
|
2642
|
+
throw new LocalExecutionAuthorityError("identity_conflict", "durable control recovery identity does not match");
|
|
2643
|
+
}
|
|
2644
|
+
if (durable.phase === "unsupported") {
|
|
2645
|
+
throw new LocalExecutionAuthorityError("unsupported", "execution supervisor evidence is unavailable");
|
|
2646
|
+
}
|
|
2647
|
+
if (request.action === "stop") {
|
|
2648
|
+
if (request.stop_idempotency_key !== undefined) {
|
|
2649
|
+
if (durable.stop_fact?.kind === "canonical_stop"
|
|
2650
|
+
&& (durable.stop_fact.stop_idempotency_key !== request.stop_idempotency_key
|
|
2651
|
+
|| durable.stop_fact.requested_at !== request.requested_at
|
|
2652
|
+
|| canonicalizeJson(durable.stop_fact.requested_outcome) !== canonicalizeJson(request.requested_outcome))) {
|
|
2653
|
+
throw new LocalExecutionAuthorityError("identity_conflict", "durable canonical stop conflicts with the request");
|
|
2654
|
+
}
|
|
2655
|
+
// Natural completion can win the race; requestStop issues distinct already-stopped evidence.
|
|
2656
|
+
}
|
|
2657
|
+
else if (durable.stop_fact !== null
|
|
2658
|
+
&& (durable.stop_fact.kind !== "local_cancel" || durable.stop_fact.reason !== (request.reason ?? null))) {
|
|
2659
|
+
throw new LocalExecutionAuthorityError("identity_conflict", "durable local cancellation conflicts with the request");
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
if (durable.raw_completion === null)
|
|
2663
|
+
return null;
|
|
2664
|
+
if (durable.raw_completion.process_tree_drained !== true) {
|
|
2665
|
+
throw new LocalExecutionAuthorityError("corrupt_state", "trusted process-tree drain evidence is unavailable");
|
|
2666
|
+
}
|
|
2667
|
+
return { ok: true, raw_completion: durable.raw_completion };
|
|
2668
|
+
}
|
|
2669
|
+
async sendControl(record, request, deadline = Date.now() + this.controlTimeoutMs) {
|
|
2670
|
+
let lastError;
|
|
2671
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2672
|
+
const remaining = deadline - Date.now();
|
|
2673
|
+
if (remaining <= 0)
|
|
2674
|
+
break;
|
|
2675
|
+
try {
|
|
2676
|
+
return await this.sendControlOnce(record, request, Math.max(1, Math.floor(remaining / (3 - attempt))));
|
|
2677
|
+
}
|
|
2678
|
+
catch (error) {
|
|
2679
|
+
if (!(error instanceof LocalExecutionControlTransportError))
|
|
2680
|
+
throw error;
|
|
2681
|
+
const completed = this.recoverControlCompletion(record, request);
|
|
2682
|
+
if (completed !== null)
|
|
2683
|
+
return completed;
|
|
2684
|
+
lastError = error;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
throw lastError ?? new LocalExecutionControlTransportError("control_timeout", "execution supervisor control timed out");
|
|
2688
|
+
}
|
|
2689
|
+
sendControlOnce(record, request, timeoutMs) {
|
|
2623
2690
|
const socketPath = record.supervisor.socket_path;
|
|
2624
2691
|
if (record.backend.kind !== "windows_job") {
|
|
2625
2692
|
let metadata;
|
|
@@ -2627,6 +2694,9 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2627
2694
|
metadata = lstatSync(socketPath);
|
|
2628
2695
|
}
|
|
2629
2696
|
catch (error) {
|
|
2697
|
+
if (isErrno(error, "ENOENT") || isErrno(error, "ENOTDIR")) {
|
|
2698
|
+
throw new LocalExecutionControlTransportError("control_connection", "execution supervisor control socket is unavailable", { cause: error });
|
|
2699
|
+
}
|
|
2630
2700
|
throw new LocalExecutionAuthorityError("unsupported", "execution supervisor control socket is unavailable", { cause: error });
|
|
2631
2701
|
}
|
|
2632
2702
|
if (!metadata.isSocket() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0) {
|
|
@@ -2639,42 +2709,32 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2639
2709
|
let response = "";
|
|
2640
2710
|
const timer = setTimeout(() => {
|
|
2641
2711
|
socket.destroy();
|
|
2642
|
-
rejectPromise(new
|
|
2643
|
-
},
|
|
2712
|
+
rejectPromise(new LocalExecutionControlTransportError("control_timeout", "execution supervisor control timed out"));
|
|
2713
|
+
}, timeoutMs);
|
|
2644
2714
|
timer.unref?.();
|
|
2645
2715
|
const fail = (error) => {
|
|
2646
2716
|
clearTimeout(timer);
|
|
2647
|
-
|
|
2717
|
+
const transient = ["ECONNRESET", "ECONNREFUSED", "ECONNABORTED", "EPIPE", "ENOENT", "ETIMEDOUT"]
|
|
2718
|
+
.some((code) => isErrno(error, code));
|
|
2719
|
+
rejectPromise(transient
|
|
2720
|
+
? new LocalExecutionControlTransportError("control_connection", "execution supervisor control failed", { cause: error })
|
|
2721
|
+
: new LocalExecutionAuthorityError("unsupported", "execution supervisor control failed", { cause: error }));
|
|
2648
2722
|
};
|
|
2649
2723
|
socket.once("error", fail);
|
|
2650
2724
|
socket.setEncoding("utf8");
|
|
2651
2725
|
socket.on("data", (chunk) => {
|
|
2652
2726
|
response += chunk;
|
|
2653
2727
|
if (Buffer.byteLength(response, "utf8") > MAX_CONTROL_RESPONSE_BYTES) {
|
|
2654
|
-
|
|
2728
|
+
clearTimeout(timer);
|
|
2729
|
+
rejectPromise(new LocalExecutionAuthorityError("unsupported", "execution control response too large"));
|
|
2730
|
+
socket.destroy();
|
|
2655
2731
|
}
|
|
2656
2732
|
});
|
|
2657
2733
|
socket.once("end", () => {
|
|
2658
2734
|
clearTimeout(timer);
|
|
2659
2735
|
try {
|
|
2660
2736
|
if (response.trim().length === 0) {
|
|
2661
|
-
|
|
2662
|
-
const durable = this.readRecord(this.recordPath(record.identity));
|
|
2663
|
-
const matchesStop = durable.raw_completion !== null
|
|
2664
|
-
&& (request.stop_idempotency_key === undefined
|
|
2665
|
-
|| (durable.stop_fact?.kind === "canonical_stop"
|
|
2666
|
-
&& durable.stop_fact.stop_idempotency_key === request.stop_idempotency_key
|
|
2667
|
-
&& canonicalizeJson(durable.stop_fact.requested_outcome)
|
|
2668
|
-
=== canonicalizeJson(request.requested_outcome)));
|
|
2669
|
-
if (matchesStop) {
|
|
2670
|
-
resolvePromise({ ok: true, raw_completion: durable.raw_completion });
|
|
2671
|
-
return;
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
catch {
|
|
2675
|
-
// The control response remains authoritative unless durable completion proves the stop.
|
|
2676
|
-
}
|
|
2677
|
-
throw new LocalExecutionAuthorityError("unsupported", "execution supervisor control response was empty");
|
|
2737
|
+
throw new LocalExecutionControlTransportError("control_empty_response", "execution supervisor control response was empty");
|
|
2678
2738
|
}
|
|
2679
2739
|
let responseText = response;
|
|
2680
2740
|
if (framedControl) {
|
|
@@ -2909,6 +2969,9 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2909
2969
|
record = await this.authenticateWindowsSupervisor(record, path);
|
|
2910
2970
|
}
|
|
2911
2971
|
}
|
|
2972
|
+
if (record.phase === "unsupported") {
|
|
2973
|
+
throw new LocalExecutionAuthorityError("unsupported", "execution supervisor evidence is unavailable");
|
|
2974
|
+
}
|
|
2912
2975
|
if (record.raw_completion === null) {
|
|
2913
2976
|
await this.sendControl(record, {
|
|
2914
2977
|
action: "stop",
|
|
@@ -2918,9 +2981,14 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2918
2981
|
});
|
|
2919
2982
|
record = this.readRecord(path);
|
|
2920
2983
|
}
|
|
2984
|
+
if (record.raw_completion !== null && record.stop_fact?.kind !== "canonical_stop") {
|
|
2985
|
+
const evidence = await this.issueAlreadyStoppedEvidence(path, request);
|
|
2986
|
+
return { kind: "already_stopped", drain_evidence: evidence };
|
|
2987
|
+
}
|
|
2921
2988
|
if (record.raw_completion === null
|
|
2922
2989
|
|| record.stop_fact?.kind !== "canonical_stop"
|
|
2923
2990
|
|| record.stop_fact.stop_idempotency_key !== request.stop_idempotency_key
|
|
2991
|
+
|| record.stop_fact.requested_at !== request.requested_at
|
|
2924
2992
|
|| canonicalizeJson(record.stop_fact.requested_outcome) !== canonicalizeJson(request.requested_outcome)) {
|
|
2925
2993
|
throw new LocalExecutionAuthorityError("unsupported", "canonical stop evidence was not durably published");
|
|
2926
2994
|
}
|
|
@@ -508,17 +508,18 @@ async function main() {
|
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
async function registerStop(kind, detail) {
|
|
511
|
-
if (rawCompletion !== null || pendingCompletionFact !== null) {
|
|
512
|
-
throw controlConflict("execution already completed before the stop request");
|
|
513
|
-
}
|
|
514
511
|
if (stopFact !== null) {
|
|
512
|
+
if (kind === "canonical_stop" && stopFact.kind !== "canonical_stop") return;
|
|
515
513
|
const same = stopFact.kind === kind && (kind !== "canonical_stop" || (
|
|
516
514
|
stopFact.stop_idempotency_key === detail.stop_idempotency_key
|
|
515
|
+
&& stopFact.requested_at === detail.requested_at
|
|
517
516
|
&& canonical(stopFact.requested_outcome) === canonical(detail.requested_outcome)
|
|
518
517
|
));
|
|
519
518
|
if (!same) throw controlConflict("execution already has a different terminal fact");
|
|
520
519
|
return;
|
|
521
520
|
}
|
|
521
|
+
// An authenticated stop can race natural completion; preserve the original completion.
|
|
522
|
+
if (rawCompletion !== null || pendingCompletionFact !== null) return;
|
|
522
523
|
if (kind === "canonical_stop" && canonicalStopMode(detail.requested_outcome) === null) {
|
|
523
524
|
throw controlConflict("canonical stop outcome is unsupported by Windows execution");
|
|
524
525
|
}
|
|
@@ -533,6 +534,7 @@ async function main() {
|
|
|
533
534
|
const next = await persist({ stop_fact: stopFact });
|
|
534
535
|
if (next.phase !== "running" || canonical(next.stop_fact) !== canonical(stopFact)) {
|
|
535
536
|
stopFact = null;
|
|
537
|
+
if (next.phase !== "unsupported" && next.raw_completion !== null && next.stop_fact === null) return;
|
|
536
538
|
throw controlConflict("execution completion won before the stop request");
|
|
537
539
|
}
|
|
538
540
|
sendTerminateIfReleased();
|
|
@@ -19,7 +19,7 @@ import type { WorkflowPlan } from "../schema/types.js";
|
|
|
19
19
|
export interface WorkflowShellOptions {
|
|
20
20
|
/** Agent name prefix (default "dna-") */
|
|
21
21
|
agentPrefix?: string;
|
|
22
|
-
/**
|
|
22
|
+
/** Optional max budget per agent in USD; no budget unless explicitly set */
|
|
23
23
|
maxBudgetPerAgent?: number;
|
|
24
24
|
/** Output format for agent results (default "json") */
|
|
25
25
|
outputFormat?: "json" | "text";
|
|
@@ -111,7 +111,9 @@ function generateHeader(plan, options) {
|
|
|
111
111
|
lines.push("");
|
|
112
112
|
// Variables
|
|
113
113
|
lines.push("# Configuration");
|
|
114
|
-
|
|
114
|
+
if (options.maxBudgetPerAgent !== undefined) {
|
|
115
|
+
lines.push(`MAX_BUDGET="${options.maxBudgetPerAgent}"`);
|
|
116
|
+
}
|
|
115
117
|
lines.push(`LOG_DIR="${options.logDir}"`);
|
|
116
118
|
lines.push('mkdir -p "$LOG_DIR"');
|
|
117
119
|
lines.push("");
|
|
@@ -142,7 +144,9 @@ function generateRunAgentFn(options) {
|
|
|
142
144
|
lines.push(' echo "$prompt" | claude -p \\');
|
|
143
145
|
lines.push(' --agent "$agent" \\');
|
|
144
146
|
lines.push(` --output-format ${options.outputFormat} \\`);
|
|
145
|
-
|
|
147
|
+
if (options.maxBudgetPerAgent !== undefined) {
|
|
148
|
+
lines.push(' --max-budget-usd "$MAX_BUDGET" \\');
|
|
149
|
+
}
|
|
146
150
|
lines.push(` --permission-mode ${options.permissionMode} \\`);
|
|
147
151
|
lines.push(' --session-id "$(uuidgen | tr \'[:upper:]\' \'[:lower:]\')" \\');
|
|
148
152
|
lines.push(' --no-session-persistence \\');
|
|
@@ -469,9 +473,12 @@ function generateFailureChecks(plan) {
|
|
|
469
473
|
* RunController through `dna run` for canonical execution.
|
|
470
474
|
*/
|
|
471
475
|
export function compileWorkflowToShell(plan, options) {
|
|
476
|
+
if (options?.maxBudgetPerAgent !== undefined && (!Number.isFinite(options.maxBudgetPerAgent) || options.maxBudgetPerAgent <= 0)) {
|
|
477
|
+
throw new TypeError("maxBudgetPerAgent must be a positive finite number");
|
|
478
|
+
}
|
|
472
479
|
const opts = {
|
|
473
480
|
agentPrefix: options?.agentPrefix ?? "dna-",
|
|
474
|
-
maxBudgetPerAgent: options?.maxBudgetPerAgent
|
|
481
|
+
maxBudgetPerAgent: options?.maxBudgetPerAgent,
|
|
475
482
|
outputFormat: options?.outputFormat ?? "json",
|
|
476
483
|
permissionMode: options?.permissionMode ?? "default",
|
|
477
484
|
logDir: options?.logDir ?? ".",
|