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.
@@ -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.5",
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.5"
28
+ "version": "1.9.8"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.9.5",
3
+ "version": "1.9.8",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
@@ -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
- || !Number.isFinite(value.provider.max_budget_usd)
181
- || value.provider.max_budget_usd <= 0
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
- max_budget_usd: metadata.provider.max_budget_usd,
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 (maxBudget === undefined || permissionMode === undefined) {
303
- throw new Error("Claude run binding is missing launch budget or permission mode");
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
- max_budget_usd: requirePositiveNumber(opts.maxBudget ?? 5, "--max-budget"),
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: 5)
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
- ? parseFloat(runValues["max-budget"])
593
- : undefined,
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
- throw new CanonicalRunServiceError("unsupported", "worker stop authority is unavailable", { cause: error });
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
- let lastError = null;
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
- lastError = new Error("Windows execution supervisor is not durably ready");
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
- lastError = error;
1601
- break;
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
- const message = lastError instanceof Error
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
- sendControl(record, request) {
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 LocalExecutionAuthorityError("unsupported", "execution supervisor control timed out"));
2643
- }, this.controlTimeoutMs);
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
- rejectPromise(new LocalExecutionAuthorityError("unsupported", "execution supervisor control failed", { cause: error }));
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
- socket.destroy(new Error("execution control response too large"));
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
- try {
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
- /** Max budget per agent in USD (default 10) */
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
- lines.push(`MAX_BUDGET="${options.maxBudgetPerAgent}"`);
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
- lines.push(' --max-budget-usd "$MAX_BUDGET" \\');
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 ?? 10,
481
+ maxBudgetPerAgent: options?.maxBudgetPerAgent,
475
482
  outputFormat: options?.outputFormat ?? "json",
476
483
  permissionMode: options?.permissionMode ?? "default",
477
484
  logDir: options?.logDir ?? ".",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.9.5",
3
+ "version": "1.9.8",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",