intentdna 1.9.4 → 1.9.5

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.4",
12
+ "version": "1.9.5",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.9.4"
28
+ "version": "1.9.5"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
@@ -1,5 +1,6 @@
1
1
  /** Intent DNA - canonical dna run CLI transport. */
2
2
  import { createHash } from "node:crypto";
3
+ import { lstatSync } from "node:fs";
3
4
  import { join, resolve } from "node:path";
4
5
  import { toKebabCase } from "../../runtime/agent-md.js";
5
6
  import { CanonicalRunApplication, } from "../../runtime/canonical-run-application.js";
@@ -301,9 +302,11 @@ function createProvider(binding) {
301
302
  if (maxBudget === undefined || permissionMode === undefined) {
302
303
  throw new Error("Claude run binding is missing launch budget or permission mode");
303
304
  }
305
+ const mcpConfigPath = findProjectMcpConfig(binding.workspace_root);
304
306
  return createClaudeExecutionProvider({
305
307
  executable,
306
308
  agentName: (packet) => `dna-${toKebabCase(packet.role)}`,
309
+ mcpConfigPath,
307
310
  extraArgs: [
308
311
  "--max-budget-usd",
309
312
  String(maxBudget),
@@ -312,6 +315,22 @@ function createProvider(binding) {
312
315
  ],
313
316
  });
314
317
  }
318
+ /**
319
+ * Resolve only regular, project-owned MCP config files. A missing config is
320
+ * valid for projects that do not use IntentDNA's MCP transport.
321
+ */
322
+ function findProjectMcpConfig(projectRoot) {
323
+ const candidate = join(projectRoot, ".mcp.json");
324
+ try {
325
+ if (lstatSync(candidate).isFile())
326
+ return candidate;
327
+ }
328
+ catch {
329
+ // A missing or unreadable optional config is handled by Claude's normal
330
+ // project discovery; do not make provider construction fail.
331
+ }
332
+ return undefined;
333
+ }
315
334
  function createDriver(runtime, view, metadata, projectDirectory) {
316
335
  assertRunComposition(view, metadata, projectDirectory);
317
336
  runtime.execution_authority.assertExecutionSupported();
@@ -386,6 +405,8 @@ function startMetadata(opts, projectDirectory) {
386
405
  : { kind: "workflow", key: requireText(opts.workflowName, "--workflow") };
387
406
  const taskId = requireText(opts.taskId, "--task");
388
407
  const timeoutMs = opts.timeoutMs == null ? null : requireNonNegativeInteger(opts.timeoutMs, "--timeout-ms");
408
+ const provider = opts.provider ?? "claude";
409
+ const defaultPermissionMode = provider === "claude" ? "bypassPermissions" : "default";
389
410
  return {
390
411
  schema_version: "intentdna.canonical_run.v3",
391
412
  project_directory: projectDirectory,
@@ -402,10 +423,13 @@ function startMetadata(opts, projectDirectory) {
402
423
  },
403
424
  agents_directory: opts.agentsDir ? resolve(projectDirectory, opts.agentsDir) : join(projectDirectory, ".claude", "agents"),
404
425
  provider: {
405
- kind: opts.provider ?? "claude",
426
+ kind: provider,
406
427
  executable: opts.providerExecutable === undefined ? null : requireText(opts.providerExecutable, "--provider-executable"),
407
428
  max_budget_usd: requirePositiveNumber(opts.maxBudget ?? 5, "--max-budget"),
408
- permission_mode: requireText(opts.permissionMode ?? "default", "--permission-mode"),
429
+ // Durable workers have no interactive prompt available for tool approvals.
430
+ // Keep an explicitly supplied mode unchanged, but make the unattended
431
+ // default writable so required artifacts can be produced.
432
+ permission_mode: requireText(opts.permissionMode ?? defaultPermissionMode, "--permission-mode"),
409
433
  },
410
434
  max_concurrency: requirePositiveInteger(opts.maxConcurrency ?? 1, "--max-concurrency"),
411
435
  timeout_ms: timeoutMs,
package/dist/cli/index.js CHANGED
@@ -311,7 +311,7 @@ Options:
311
311
  --timeout-ms <n> Per-attempt timeout in milliseconds
312
312
  --cancellation-grace-ms <n> Process-tree cancellation grace period (default: 5000)
313
313
  --max-budget <usd> Claude budget per Step attempt (default: 5)
314
- --permission-mode <mode> Claude permission mode (default: default)
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
317
317
  --json Print lifecycle output as JSON
@@ -94,6 +94,12 @@ function geneToToolFilters(name, gene) {
94
94
  /**
95
95
  * Extract pre-execution gates from threshold and sense codons.
96
96
  */
97
+ function gateEnforcement(condition) {
98
+ return /^personal_data_exposure\s*==\s*false$/.test(condition)
99
+ || /^tool_name\s+in\s+\[[^\]]+\]$/.test(condition)
100
+ ? "runtime"
101
+ : "prompt_only";
102
+ }
97
103
  function geneToGates(name, gene) {
98
104
  if (gene.expression_level === 0)
99
105
  return [];
@@ -105,6 +111,7 @@ function geneToGates(name, gene) {
105
111
  action: "block",
106
112
  message: `DNA threshold violation: ${codon.condition} (gene: ${name})`,
107
113
  source_gene: name,
114
+ enforcement: gateEnforcement(codon.condition),
108
115
  origin: codon.origin,
109
116
  });
110
117
  }
@@ -114,6 +121,7 @@ function geneToGates(name, gene) {
114
121
  action: "escalate",
115
122
  message: `DNA requires human approval for: ${codon.signal} (gene: ${name})`,
116
123
  source_gene: name,
124
+ enforcement: "prompt_only",
117
125
  });
118
126
  }
119
127
  if (codon.type === "sense" && codon.response === "block") {
@@ -122,6 +130,7 @@ function geneToGates(name, gene) {
122
130
  action: "block",
123
131
  message: `DNA blocks action on signal: ${codon.signal} (gene: ${name})`,
124
132
  source_gene: name,
133
+ enforcement: "prompt_only",
125
134
  });
126
135
  }
127
136
  }
package/dist/hooks/cli.js CHANGED
@@ -625,6 +625,24 @@ export async function runHookEvent(options) {
625
625
  await writeRuntimeDecision(mismatch, { output: mismatch, trace: { matched_rule: "workflow_runtime_revision", step_id: wfState?.current_step } }, wfState);
626
626
  return finish(mismatch);
627
627
  }
628
+ if (isCanonicalWorkflowState(wfState)) {
629
+ const stopOutput = silentOutput();
630
+ const traceId = randomUUID();
631
+ appendTrace(projectDir, {
632
+ trace_id: traceId,
633
+ event: "Stop",
634
+ workflow: wfState.workflow,
635
+ step: wfState.current_step,
636
+ decision: "allow",
637
+ duration_ms: 0,
638
+ timestamp: new Date().toISOString(),
639
+ }, sessionId).catch(() => { });
640
+ await writeRuntimeDecision(stopOutput, {
641
+ output: stopOutput,
642
+ trace: { matched_rule: "workflow_boundary", step_id: wfState.current_step },
643
+ }, wfState, traceId);
644
+ return finish(stopOutput);
645
+ }
628
646
  let stopArtifactFacts = [];
629
647
  if (wfState?.active) {
630
648
  const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
@@ -550,11 +550,18 @@ function enforceToolFilters(filters, input) {
550
550
  }
551
551
  function enforceGates(gates, input, strictBoundary = false) {
552
552
  for (const gate of gates) {
553
+ if (gate.enforcement === "prompt_only")
554
+ continue;
553
555
  const matches = evaluateGateCondition(gate.condition, input);
554
556
  if (matches === null) {
557
+ // Older compiled IR did not declare enforcement. Provenance-backed
558
+ // thresholds and bare semantic signals were compiler-generated prompt
559
+ // constraints, so preserve that behavior during the schema transition.
560
+ if (gate.origin !== undefined || /^\w+$/.test(gate.condition))
561
+ continue;
555
562
  if (!strictBoundary)
556
563
  continue;
557
- const detail = `Gate condition '${gate.condition}' could not be evaluated.`;
564
+ const detail = `Runtime gate condition '${gate.condition}' could not be evaluated.`;
558
565
  if (gate.action === "block") {
559
566
  return blockOutput(`[Intent DNA] gate_evaluation: ${detail}`);
560
567
  }
@@ -107,6 +107,7 @@ async function observeAttemptWorkspace(workspaceRoot, workspaceId) {
107
107
  kind: "attempt_workspace",
108
108
  relative_root: ".",
109
109
  excluded_relative_paths: observation.excluded_relative_paths,
110
+ opaque_external_dependencies: observation.opaque_external_dependencies,
110
111
  },
111
112
  observed_digest: observation.observed_digest,
112
113
  entry_count: observation.entry_count,
@@ -691,7 +692,9 @@ class ContractVerifier {
691
692
  }
692
693
  const receipt = {
693
694
  schema_version: CANONICAL_VERIFIER_EXECUTION_RECEIPT_SCHEMA_VERSION,
694
- evidence_mode: "historical_execution",
695
+ evidence_mode: (observation.scope.opaque_external_dependencies?.length ?? 0) === 0
696
+ ? "historical_execution"
697
+ : "historical_execution_with_unbound_external_dependencies",
695
698
  plan_id: request.plan.plan_id,
696
699
  verifier_id: result.verifier_id,
697
700
  verifier_definition_digest: digest(commandExecution.verifier),
@@ -51,7 +51,7 @@ export function compileForClaude(ir) {
51
51
  }
52
52
  // Add gate descriptions to prompt (so LLM is aware of constraints)
53
53
  if (ir.pre_execution_gates.length > 0) {
54
- promptLines.push("## Hard Constraints (will be enforced by system)");
54
+ promptLines.push("## Policy Constraints");
55
55
  for (const gate of ir.pre_execution_gates) {
56
56
  promptLines.push(`- ${gate.message}`);
57
57
  }
@@ -59,7 +59,9 @@ export function compileForClaude(ir) {
59
59
  }
60
60
  return {
61
61
  system_prompt_addition: promptLines.join("\n"),
62
- pre_tool_rules: ir.pre_execution_gates.map((g) => ({
62
+ pre_tool_rules: ir.pre_execution_gates
63
+ .filter((g) => g.enforcement !== "prompt_only")
64
+ .map((g) => ({
63
65
  condition: g.condition,
64
66
  action: g.action,
65
67
  message: g.message,
@@ -988,7 +988,7 @@ function textMatchesCondition(text, condition) {
988
988
  }
989
989
  function matchingPreExecutionGate(ir, input) {
990
990
  const subject = [input.tool_name, input.command, input.resource_ref, JSON.stringify(input.payload)].filter(Boolean).join(" ");
991
- return ir.pre_execution_gates.find((gate) => textMatchesCondition(subject, gate.condition));
991
+ return ir.pre_execution_gates.find((gate) => gate.enforcement !== "prompt_only" && textMatchesCondition(subject, gate.condition));
992
992
  }
993
993
  function codexHookOutputForGate(gate) {
994
994
  if (gate.action === "block") {
@@ -32,6 +32,11 @@ export interface WorkerExecution {
32
32
  export declare class MalformedProviderResultError extends Error {
33
33
  constructor(message: string);
34
34
  }
35
+ export type ProviderTerminalReason = "hook_stopped" | "stop_hook_prevented";
36
+ export declare class ProviderTerminatedError extends Error {
37
+ readonly reason: ProviderTerminalReason;
38
+ constructor(reason: ProviderTerminalReason, message: string);
39
+ }
35
40
  /**
36
41
  * Decode the provider's final assistant text into the declared output contract.
37
42
  * A single text output may use the assistant text directly. Other contracts use
@@ -4,6 +4,14 @@ export class MalformedProviderResultError extends Error {
4
4
  this.name = "MalformedProviderResultError";
5
5
  }
6
6
  }
7
+ export class ProviderTerminatedError extends Error {
8
+ reason;
9
+ constructor(reason, message) {
10
+ super(message);
11
+ this.name = "ProviderTerminatedError";
12
+ this.reason = reason;
13
+ }
14
+ }
7
15
  function isRecord(value) {
8
16
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
17
  }
@@ -25,8 +25,8 @@ export { CanonicalAttemptOutcomeValidationError, canonicalAttemptOutcomeStatus,
25
25
  export type { CanonicalAttemptOutcomeStatus, ValidatedCanonicalAttemptOutcome, } from "./canonical-attempt-outcome.js";
26
26
  export { HandoffResolutionError, HandoffResolver, } from "./handoff-resolver.js";
27
27
  export type { ReferenceHandoffRequest, QuoteHandoffRequest, StructuredHandoffRequest, NoHandoffRequest, HandoffRequest, StructuredValueValidator, HandoffResolverOptions, HandoffResolutionErrorCode, } from "./handoff-resolver.js";
28
- export { MalformedProviderResultError, decodeDeclaredOutputs, } from "./execution-provider.js";
29
- export type { ProviderLaunchSpec, ProviderParseContext, ParsedProviderResult, ExecutionProvider, WorkerExecution, } from "./execution-provider.js";
28
+ export { MalformedProviderResultError, ProviderTerminatedError, decodeDeclaredOutputs, } from "./execution-provider.js";
29
+ export type { ProviderLaunchSpec, ProviderParseContext, ParsedProviderResult, ExecutionProvider, ProviderTerminalReason, WorkerExecution, } from "./execution-provider.js";
30
30
  export { createClaudeExecutionProvider } from "./providers/claude.js";
31
31
  export type { ClaudeExecutionProviderOptions, } from "./providers/claude.js";
32
32
  export { createCodexExecutionProvider } from "./providers/codex.js";
@@ -13,7 +13,7 @@ export { RUN_STORE_SCHEMA_VERSION, LEGACY_RUN_STORE_SCHEMA_VERSION, CANONICAL_RU
13
13
  export { ResultStoreError, ImmutableResultStore, ImmutableCanonicalResultStore, } from "./result-store.js";
14
14
  export { CanonicalAttemptOutcomeValidationError, canonicalAttemptOutcomeStatus, validateCanonicalAttemptOutcome, } from "./canonical-attempt-outcome.js";
15
15
  export { HandoffResolutionError, HandoffResolver, } from "./handoff-resolver.js";
16
- export { MalformedProviderResultError, decodeDeclaredOutputs, } from "./execution-provider.js";
16
+ export { MalformedProviderResultError, ProviderTerminatedError, decodeDeclaredOutputs, } from "./execution-provider.js";
17
17
  export { createClaudeExecutionProvider } from "./providers/claude.js";
18
18
  export { createCodexExecutionProvider } from "./providers/codex.js";
19
19
  export { runProcessTree, runProcessTreeWithTimeout, } from "./process-tree.js";
@@ -662,6 +662,20 @@ function nonlaunchSourceEvidenceDigest(record) {
662
662
  });
663
663
  }
664
664
  function alreadyStoppedSourceEvidenceDigest(record) {
665
+ if (record.stop_receipt !== null) {
666
+ return digest({
667
+ schema_version: "intentdna.local_already_stopped_source_evidence.v2",
668
+ identity_digest: record.identity_digest,
669
+ execution_instance_id: record.execution_instance_id,
670
+ authority_instance_id: record.authority_instance_id,
671
+ backend: record.backend,
672
+ supervisor: record.supervisor,
673
+ provider: record.provider,
674
+ raw_completion: record.raw_completion,
675
+ normalized_process_outcome: record.normalized_process_outcome,
676
+ stop_receipt_digest: record.stop_receipt.receipt_digest,
677
+ });
678
+ }
665
679
  return digest({
666
680
  schema_version: "intentdna.local_already_stopped_source_evidence.v1",
667
681
  identity_digest: record.identity_digest,
@@ -674,6 +688,17 @@ function alreadyStoppedSourceEvidenceDigest(record) {
674
688
  normalized_process_outcome: record.normalized_process_outcome,
675
689
  });
676
690
  }
691
+ function alreadyStoppedObservedOutcome(record) {
692
+ if (record.stop_receipt !== null)
693
+ return record.stop_receipt.payload.terminal_outcome;
694
+ if (record.normalized_process_outcome !== null) {
695
+ return terminalOutcomeToCanonical(record.normalized_process_outcome);
696
+ }
697
+ if (record.raw_completion !== null) {
698
+ return terminalOutcomeToCanonical(rawCompletionToOutcome(record.raw_completion));
699
+ }
700
+ throw new LocalExecutionAuthorityError("corrupt_state", "already-stopped evidence has no trusted completion outcome");
701
+ }
677
702
  function stopReceiptSourceEvidenceDigest(record) {
678
703
  return digest({
679
704
  identity_digest: record.identity_digest,
@@ -868,7 +893,7 @@ function parseRecord(value) {
868
893
  || record.already_stopped_evidence.payload.started_at !== record.identity.started_at
869
894
  || record.already_stopped_evidence.payload.completed_at !== record.raw_completion.completed_at
870
895
  || canonicalizeJson(record.already_stopped_evidence.payload.observed_outcome)
871
- !== canonicalizeJson(terminalOutcomeToCanonical(rawCompletionToOutcome(record.raw_completion)))
896
+ !== canonicalizeJson(alreadyStoppedObservedOutcome(record))
872
897
  || record.already_stopped_evidence.payload.process_tree_drained !== true
873
898
  || !isCanonicalDigest(record.already_stopped_evidence.payload.stop_idempotency_key)
874
899
  || !isCanonicalTimestamp(record.already_stopped_evidence.payload.stop_requested_at)
@@ -2698,6 +2723,9 @@ export class LocalAttemptExecutionAuthority {
2698
2723
  async issueReceipt(path, requestedOutcome, stoppedAt, expectedReceipt) {
2699
2724
  return this.withLock(path, async () => {
2700
2725
  const current = this.readRecord(path);
2726
+ if (current.already_stopped_evidence !== null) {
2727
+ throw new LocalExecutionAuthorityError("identity_conflict", "already-stopped evidence already owns the durable terminal transition");
2728
+ }
2701
2729
  if (current.stop_receipt !== null) {
2702
2730
  if (canonicalizeJson(current.stop_receipt.payload.terminal_outcome) !== canonicalizeJson(requestedOutcome)
2703
2731
  || current.stop_receipt.payload.stopped_at !== stoppedAt
@@ -2766,13 +2794,14 @@ export class LocalAttemptExecutionAuthority {
2766
2794
  if (request.expected_receipt !== null) {
2767
2795
  throw new LocalExecutionAuthorityError("identity_conflict", "expected stop receipt cannot match a naturally completed execution");
2768
2796
  }
2769
- if (current.stop_receipt !== null || current.stop_fact?.kind === "canonical_stop") {
2797
+ if (current.stop_fact?.kind === "canonical_stop") {
2770
2798
  throw new LocalExecutionAuthorityError("identity_conflict", "canonical stop already owns the durable terminal transition");
2771
2799
  }
2772
2800
  if (current.raw_completion === null) {
2773
2801
  throw new LocalExecutionAuthorityError("unsupported", "trusted natural completion evidence is unavailable");
2774
2802
  }
2775
- const fencingEpoch = await this.nextFencingEpoch();
2803
+ const fencingEpoch = current.stop_receipt?.payload.fencing_epoch
2804
+ ?? await this.nextFencingEpoch();
2776
2805
  const evidence = createWorkerAlreadyStoppedEvidence({
2777
2806
  schema_version: "intentdna.worker_already_stopped_evidence.v1",
2778
2807
  run_id: current.identity.run_id,
@@ -2787,7 +2816,7 @@ export class LocalAttemptExecutionAuthority {
2787
2816
  fencing_epoch: fencingEpoch,
2788
2817
  started_at: current.identity.started_at,
2789
2818
  completed_at: current.raw_completion.completed_at,
2790
- observed_outcome: terminalOutcomeToCanonical(rawCompletionToOutcome(current.raw_completion)),
2819
+ observed_outcome: alreadyStoppedObservedOutcome(current),
2791
2820
  process_tree_drained: true,
2792
2821
  stop_idempotency_key: request.stop_idempotency_key,
2793
2822
  stop_requested_at: request.requested_at,
@@ -2795,6 +2824,8 @@ export class LocalAttemptExecutionAuthority {
2795
2824
  stop_requested_outcome: request.requested_outcome,
2796
2825
  source_evidence_digest: alreadyStoppedSourceEvidenceDigest(current),
2797
2826
  });
2827
+ if (current.stop_receipt !== null)
2828
+ return evidence;
2798
2829
  const updated = {
2799
2830
  ...current,
2800
2831
  phase: "receipted",
@@ -2859,6 +2890,10 @@ export class LocalAttemptExecutionAuthority {
2859
2890
  return { kind: "already_stopped", drain_evidence: evidence };
2860
2891
  }
2861
2892
  if (record.stop_receipt !== null) {
2893
+ if (record.stop_fact?.kind !== "canonical_stop") {
2894
+ const evidence = await this.issueAlreadyStoppedEvidence(path, request);
2895
+ return { kind: "already_stopped", drain_evidence: evidence };
2896
+ }
2862
2897
  const authority = await this.issueReceipt(path, request.requested_outcome, record.stop_receipt.payload.stopped_at, request.expected_receipt);
2863
2898
  return { kind: "stopped_by_request", authority };
2864
2899
  }
@@ -3,6 +3,11 @@ import type { StepPacket } from "../run-contracts.js";
3
3
  export interface ClaudeExecutionProviderOptions {
4
4
  readonly executable?: string;
5
5
  readonly agentName?: (packet: StepPacket) => string;
6
+ /**
7
+ * Explicit project MCP configuration to load for each standalone attempt.
8
+ * A function keeps the launch spec bound to the packet's exact workspace.
9
+ */
10
+ readonly mcpConfigPath?: string | ((packet: StepPacket) => string | null | undefined);
6
11
  readonly extraArgs?: readonly string[];
7
12
  readonly env?: NodeJS.ProcessEnv;
8
13
  }
@@ -1,11 +1,57 @@
1
- import { decodeDeclaredOutputs, MalformedProviderResultError, } from "../execution-provider.js";
1
+ import { decodeDeclaredOutputs, MalformedProviderResultError, ProviderTerminatedError, } from "../execution-provider.js";
2
2
  function isRecord(value) {
3
3
  return typeof value === "object" && value !== null && !Array.isArray(value);
4
4
  }
5
+ const CLAUDE_HOOK_TERMINAL_REASONS = new Set([
6
+ "hook_stopped",
7
+ "stop_hook_prevented",
8
+ ]);
9
+ const CLAUDE_DIAGNOSTIC_TOOL_NAMES = new Set([
10
+ "Bash",
11
+ "Edit",
12
+ "Glob",
13
+ "Grep",
14
+ "NotebookEdit",
15
+ "Read",
16
+ "Task",
17
+ "WebFetch",
18
+ "WebSearch",
19
+ "Write",
20
+ ]);
21
+ function summarizePermissionDenials(value) {
22
+ if (!Array.isArray(value))
23
+ return null;
24
+ const toolNames = [...new Set(value.flatMap((denial) => (isRecord(denial)
25
+ && typeof denial.tool_name === "string"
26
+ && CLAUDE_DIAGNOSTIC_TOOL_NAMES.has(denial.tool_name)
27
+ ? [denial.tool_name]
28
+ : [])))].slice(0, 3);
29
+ const count = value.length > 999 ? "999+" : String(value.length);
30
+ return toolNames.length > 0
31
+ ? `permission_denials=${count}; denied_tools=${toolNames.join(",")}`
32
+ : `permission_denials=${count}`;
33
+ }
34
+ function standaloneClaudePrompt(packet) {
35
+ const workingDirectory = JSON.stringify(packet.workspace.working_directory);
36
+ return [
37
+ "IntentDNA runtime workspace contract:",
38
+ `- The exact, authoritative process working directory for this attempt is ${workingDirectory}.`,
39
+ "- Use paths relative to that working directory for repository-local file access whenever possible.",
40
+ "- Never infer, reconstruct, rename, or guess an absolute repository path from a session name, project name, transcript location, or encoded directory name.",
41
+ `- If an absolute repository path is required, copy ${workingDirectory} exactly instead of reconstructing it.`,
42
+ "- This is a standalone attempt. Do not search for or read Claude Code session transcripts, including files under ~/.claude/projects, to recover task context.",
43
+ "",
44
+ "Task:",
45
+ packet.standalone_prompt,
46
+ ].join("\n");
47
+ }
5
48
  export function createClaudeExecutionProvider(options = {}) {
6
49
  return {
7
50
  name: "claude",
8
51
  createLaunch(packet) {
52
+ const mcpConfigPath = typeof options.mcpConfigPath === "function"
53
+ ? options.mcpConfigPath(packet)
54
+ : options.mcpConfigPath;
9
55
  return {
10
56
  command: options.executable ?? "claude",
11
57
  args: [
@@ -17,9 +63,12 @@ export function createClaudeExecutionProvider(options = {}) {
17
63
  "--session-id",
18
64
  packet.worker_session_id,
19
65
  "--no-session-persistence",
66
+ ...(mcpConfigPath === undefined || mcpConfigPath === null
67
+ ? []
68
+ : ["--mcp-config", mcpConfigPath]),
20
69
  ...(options.extraArgs ?? []),
21
70
  ],
22
- stdin: packet.standalone_prompt,
71
+ stdin: standaloneClaudePrompt(packet),
23
72
  cwd: packet.workspace.working_directory,
24
73
  ...(options.env ? { env: options.env } : {}),
25
74
  };
@@ -35,6 +84,16 @@ export function createClaudeExecutionProvider(options = {}) {
35
84
  if (!isRecord(value)) {
36
85
  throw new MalformedProviderResultError("Claude output must be a JSON object");
37
86
  }
87
+ const hookTerminalReason = typeof value.terminal_reason === "string"
88
+ && CLAUDE_HOOK_TERMINAL_REASONS.has(value.terminal_reason)
89
+ ? value.terminal_reason
90
+ : null;
91
+ if (hookTerminalReason !== null) {
92
+ const denials = summarizePermissionDenials(value.permission_denials);
93
+ throw new ProviderTerminatedError(hookTerminalReason, denials
94
+ ? `Claude terminal_reason=${hookTerminalReason}; ${denials}`
95
+ : `Claude terminal_reason=${hookTerminalReason}`);
96
+ }
38
97
  if (value.is_error === true) {
39
98
  throw new MalformedProviderResultError(typeof value.result === "string"
40
99
  ? value.result
@@ -4,7 +4,7 @@ import type { ExecutionProvider } from "./execution-provider.js";
4
4
  import type { CanonicalAttemptOutcome, CanonicalEvidenceRef, CanonicalResultDigest, RunId, StepOutput, StepPacket, TerminalOutcome, WorkerSessionId } from "./run-contracts.js";
5
5
  import { type RunBindingPayload } from "./run-binding.js";
6
6
  import { executeWorkerAttempt, type WorkerExecutionAuthorityBinding, type WorkerExecutionWithEvents, type WorkerExecutorOptions } from "./worker-executor.js";
7
- export type PushDriverErrorCode = "invalid_configuration" | "invalid_service_response" | "invalid_execution_result" | "invalid_provider_output" | "unsupported_output_contract" | "unsupported" | "attempt_relaunch" | "attach_indeterminate" | "authority_preparation_abandon_failed" | "driver_failure_settlement_failed" | "wait_stopped";
7
+ export type PushDriverErrorCode = "invalid_configuration" | "invalid_service_response" | "invalid_execution_result" | "invalid_provider_output" | "unsupported_output_contract" | "unsupported" | "attempt_relaunch" | "attach_indeterminate" | "submission_indeterminate" | "authority_preparation_abandon_failed" | "driver_failure_settlement_failed" | "wait_stopped";
8
8
  export declare class PushDriverError extends Error {
9
9
  readonly code: PushDriverErrorCode;
10
10
  constructor(code: PushDriverErrorCode, message: string, options?: ErrorOptions);
@@ -13,6 +13,10 @@ export declare class PushDriverIndeterminateAttachError extends PushDriverError
13
13
  readonly disposition: "retain_preparation";
14
14
  constructor(attemptId: string, cause: unknown);
15
15
  }
16
+ export declare class PushDriverIndeterminateSubmissionError extends PushDriverError {
17
+ readonly disposition: "retain_attempt";
18
+ constructor(attemptId: string, cause: unknown);
19
+ }
16
20
  export interface PushDriverService {
17
21
  inspect(runId: RunId): Promise<RunView>;
18
22
  resume(runId: RunId): Promise<RunView>;
@@ -175,6 +179,9 @@ export declare class PushDriver {
175
179
  private attemptError;
176
180
  private assertHeartbeatLease;
177
181
  private startHeartbeat;
182
+ private submissionIsCommitted;
183
+ private submissionIsDefinitivelyFenced;
184
+ private reconcileSubmission;
178
185
  private attachIsDefinitivelyFenced;
179
186
  private abandonPreparation;
180
187
  private reconcilePreparedAttach;