intentdna 1.9.3 → 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.
@@ -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;
@@ -19,9 +19,34 @@ export class PushDriverIndeterminateAttachError extends PushDriverError {
19
19
  this.name = "PushDriverIndeterminateAttachError";
20
20
  }
21
21
  }
22
+ export class PushDriverIndeterminateSubmissionError extends PushDriverError {
23
+ disposition = "retain_attempt";
24
+ constructor(attemptId, cause) {
25
+ super("submission_indeterminate", `attempt ${attemptId} submission disposition is indeterminate; the attempt was retained`, { cause });
26
+ this.name = "PushDriverIndeterminateSubmissionError";
27
+ }
28
+ }
22
29
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
23
30
  const DEFAULT_WAIT_POLL_MS = 50;
24
31
  const NO_ATTEMPT_ERROR = Symbol("no_attempt_error");
32
+ function cancellableDelay(milliseconds) {
33
+ if (milliseconds <= 0) {
34
+ return { promise: Promise.resolve(), cancel: () => undefined };
35
+ }
36
+ let timer;
37
+ const promise = new Promise((resolve) => {
38
+ timer = setTimeout(resolve, milliseconds);
39
+ });
40
+ return {
41
+ promise,
42
+ cancel: () => {
43
+ if (timer !== undefined) {
44
+ clearTimeout(timer);
45
+ timer = undefined;
46
+ }
47
+ },
48
+ };
49
+ }
25
50
  const DEFAULT_CAPABILITIES = {
26
51
  worktree_isolation: true,
27
52
  parallelism: true,
@@ -448,13 +473,16 @@ export class PushDriver {
448
473
  const schedule = (response) => {
449
474
  if (stopped)
450
475
  return;
476
+ const heartbeatAt = Date.parse(response.heartbeat_at);
451
477
  const claimExpiresAt = Date.parse(response.claim_expires_at);
452
478
  const executionDeadline = Date.parse(response.execution_deadline_at);
453
479
  if (claimExpiresAt === executionDeadline)
454
480
  return;
455
- const leaseWindow = claimExpiresAt - Date.parse(response.heartbeat_at);
481
+ const leaseWindow = claimExpiresAt - heartbeatAt;
456
482
  const delay = Math.max(1, Math.min(this.heartbeatIntervalMs, Math.floor(leaseWindow / 2)));
457
483
  timer = setTimeout(() => {
484
+ if (stopped)
485
+ return;
458
486
  inFlight = heartbeat().then((next) => schedule(next), async (error) => {
459
487
  failureError = error;
460
488
  await this.emit({
@@ -469,7 +497,9 @@ export class PushDriver {
469
497
  }, delay);
470
498
  timer.unref?.();
471
499
  };
472
- schedule(await heartbeat());
500
+ const initial = await heartbeat();
501
+ const drainDeadline = cancellableDelay(Math.max(0, Date.parse(initial.execution_deadline_at) - Date.parse(initial.heartbeat_at)));
502
+ schedule(initial);
473
503
  return {
474
504
  failure,
475
505
  race: (operation) => (failureError === NO_ATTEMPT_ERROR
@@ -479,10 +509,111 @@ export class PushDriver {
479
509
  stopped = true;
480
510
  if (timer !== undefined)
481
511
  clearTimeout(timer);
482
- await inFlight;
512
+ try {
513
+ await Promise.race([inFlight, drainDeadline.promise]);
514
+ }
515
+ finally {
516
+ drainDeadline.cancel();
517
+ void inFlight.catch(() => undefined);
518
+ }
483
519
  },
484
520
  };
485
521
  }
522
+ submissionIsCommitted(view, lease) {
523
+ return view.results.some((result) => result.attempt_id === lease.attempt_id);
524
+ }
525
+ submissionIsDefinitivelyFenced(view, lease) {
526
+ const attempt = view.attempts.find((candidate) => candidate.attempt_id === lease.attempt_id);
527
+ const latestClaim = view.claims
528
+ .filter((candidate) => candidate.attempt_id === lease.attempt_id)
529
+ .sort((left, right) => right.claim_epoch - left.claim_epoch)[0];
530
+ return attempt?.phase === "terminal"
531
+ || view.run.terminal !== null
532
+ || view.run.status === "cancelling"
533
+ || (latestClaim !== undefined && (latestClaim.claim_epoch !== lease.claim_epoch
534
+ || latestClaim.owner_id !== lease.owner_id));
535
+ }
536
+ async reconcileSubmission(lease, rawSubmission, initialError, reconciliationDeadline) {
537
+ let lastInspectionError = initialError;
538
+ let acceptingEvents = true;
539
+ let waiter = null;
540
+ const queued = [];
541
+ const pushEvent = (event) => {
542
+ if (!acceptingEvents)
543
+ return;
544
+ if (waiter !== null) {
545
+ const resolve = waiter;
546
+ waiter = null;
547
+ resolve(event);
548
+ }
549
+ else {
550
+ queued.push(event);
551
+ }
552
+ };
553
+ const nextEvent = () => {
554
+ const event = queued.shift();
555
+ if (event !== undefined)
556
+ return Promise.resolve(event);
557
+ return new Promise((resolve) => { waiter = resolve; });
558
+ };
559
+ void rawSubmission.then((outcome) => {
560
+ pushEvent({ kind: "raw", outcome });
561
+ });
562
+ const deadline = cancellableDelay(Math.max(0, reconciliationDeadline - Date.now()));
563
+ void deadline.promise.then(() => pushEvent({ kind: "deadline" }));
564
+ const poll = { current: null };
565
+ let inspectionPending = false;
566
+ const inspect = () => {
567
+ if (inspectionPending)
568
+ return;
569
+ inspectionPending = true;
570
+ void this.service.inspect(lease.run_id).then((view) => pushEvent({ kind: "inspected", view }), (error) => pushEvent({ kind: "inspection_rejected", error }));
571
+ };
572
+ const schedulePoll = () => {
573
+ poll.current?.cancel();
574
+ poll.current = cancellableDelay(Math.min(DEFAULT_WAIT_POLL_MS, Math.max(0, reconciliationDeadline - Date.now())));
575
+ void poll.current.promise.then(() => pushEvent({ kind: "poll" }));
576
+ };
577
+ try {
578
+ inspect();
579
+ for (;;) {
580
+ const event = await nextEvent();
581
+ if (event.kind === "deadline") {
582
+ throw new PushDriverIndeterminateSubmissionError(lease.attempt_id, new AggregateError([initialError, lastInspectionError], "submission could not be reconciled before its deadline"));
583
+ }
584
+ if (event.kind === "raw") {
585
+ if (event.outcome.kind === "submitted")
586
+ return event.outcome;
587
+ lastInspectionError = new AggregateError([lastInspectionError, event.outcome.error], "submission transport rejected without authoritative disposition");
588
+ if (!inspectionPending && poll.current === null)
589
+ schedulePoll();
590
+ continue;
591
+ }
592
+ if (event.kind === "poll") {
593
+ poll.current = null;
594
+ inspect();
595
+ continue;
596
+ }
597
+ inspectionPending = false;
598
+ if (event.kind === "inspection_rejected") {
599
+ lastInspectionError = event.error;
600
+ }
601
+ else if (this.submissionIsCommitted(event.view, lease)) {
602
+ return { kind: "committed", run: event.view };
603
+ }
604
+ else if (this.submissionIsDefinitivelyFenced(event.view, lease)) {
605
+ return { kind: "fenced", run: event.view };
606
+ }
607
+ schedulePoll();
608
+ }
609
+ }
610
+ finally {
611
+ acceptingEvents = false;
612
+ waiter = null;
613
+ poll.current?.cancel();
614
+ deadline.cancel();
615
+ }
616
+ }
486
617
  attachIsDefinitivelyFenced(view, lease, workerSessionId) {
487
618
  const attempt = view.attempts.find((candidate) => candidate.attempt_id === lease.attempt_id);
488
619
  const claims = view.claims
@@ -736,13 +867,49 @@ export class PushDriver {
736
867
  const attestation = await heartbeat.race(Promise.resolve(authority.attest(attestationRequest)));
737
868
  evidenceRefs = [assertCallerAttestation(authority, attestationRequest, attestation)];
738
869
  }
739
- const response = await heartbeat.race(this.service.submitAttempt({
870
+ const rawSubmission = this.service.submitAttempt({
740
871
  lease_token: lease.claim_token,
741
872
  owner_id: lease.owner_id,
742
873
  submission_key: submissionKey,
743
874
  execution: submissionExecution,
744
875
  ...(evidenceRefs === undefined ? {} : { evidence_refs: evidenceRefs }),
745
- }));
876
+ }).then((value) => ({ kind: "submitted", value }), (error) => ({ kind: "rejected", error }));
877
+ let submission = null;
878
+ let submissionRaceError = NO_ATTEMPT_ERROR;
879
+ try {
880
+ submission = await heartbeat.race(rawSubmission);
881
+ }
882
+ catch (error) {
883
+ submissionRaceError = error;
884
+ }
885
+ const reconciliationRequired = submissionRaceError !== NO_ATTEMPT_ERROR
886
+ || submission?.kind === "rejected";
887
+ if (reconciliationRequired) {
888
+ let reconciliationCause;
889
+ if (submissionRaceError !== NO_ATTEMPT_ERROR) {
890
+ reconciliationCause = submissionRaceError;
891
+ }
892
+ else if (submission?.kind === "rejected") {
893
+ reconciliationCause = submission.error;
894
+ }
895
+ else {
896
+ throw new PushDriverError("invalid_service_response", `submission reconciliation has no cause for attempt ${lease.attempt_id}`);
897
+ }
898
+ await heartbeat.stop();
899
+ heartbeat = null;
900
+ const reconciled = await this.reconcileSubmission(lease, rawSubmission, reconciliationCause, Date.parse(lease.execution_deadline_at) + node.attempt_policy.cancellation_grace_ms);
901
+ if (reconciled.kind === "committed" || reconciled.kind === "fenced") {
902
+ return reconciled.run;
903
+ }
904
+ submission = reconciled;
905
+ }
906
+ if (submission === null) {
907
+ throw new PushDriverError("invalid_service_response", `submission reconciliation returned no disposition for attempt ${lease.attempt_id}`);
908
+ }
909
+ if (submission.kind === "rejected") {
910
+ throw new PushDriverError("invalid_service_response", `submission rejection was not reconciled for attempt ${lease.attempt_id}`);
911
+ }
912
+ const response = submission.value;
746
913
  if (response.replayed) {
747
914
  await this.emit({
748
915
  type: "submission_replayed",
@@ -929,6 +1096,10 @@ export class PushDriver {
929
1096
  const attemptError = firstAttemptError === NO_ATTEMPT_ERROR
930
1097
  ? completion.error
931
1098
  : firstAttemptError;
1099
+ if (attemptError instanceof PushDriverIndeterminateSubmissionError) {
1100
+ abandonActiveWaits();
1101
+ throw attemptError;
1102
+ }
932
1103
  if (attemptError instanceof PushDriverIndeterminateAttachError) {
933
1104
  const siblings = [...active.values()];
934
1105
  active.clear();
@@ -420,7 +420,8 @@ function assertVerifierExecutionReceipt(value, verifierId, result, label) {
420
420
  "execution_authority",
421
421
  ], label);
422
422
  if (value.schema_version !== "intentdna.verifier_execution_receipt.v1"
423
- || value.evidence_mode !== "historical_execution") {
423
+ || (value.evidence_mode !== "historical_execution"
424
+ && value.evidence_mode !== "historical_execution_with_unbound_external_dependencies")) {
424
425
  throw new ResultStoreError("invalid_result", `${label} has an unsupported receipt contract`);
425
426
  }
426
427
  assertCanonicalNonEmptyString(value.verifier_id, `${label}.verifier_id`);
@@ -577,7 +578,10 @@ function assertWorkspaceObservation(value, evidenceDigest, label, artifactCaptur
577
578
  "byte_count",
578
579
  "git",
579
580
  ], `${label}.workspace_observation`);
580
- if (value.schema_version !== "intentdna.workspace_observation.v2") {
581
+ if (value.schema_version !== "intentdna.workspace_observation.v2"
582
+ && value.schema_version !== "intentdna.workspace_observation.v3"
583
+ && value.schema_version !== "intentdna.workspace_observation.v4"
584
+ && value.schema_version !== "intentdna.workspace_observation.v5") {
581
585
  throw new ResultStoreError("invalid_result", `${label}.workspace_observation.schema_version is unsupported`);
582
586
  }
583
587
  assertCanonicalNonEmptyString(value.workspace_id, `${label}.workspace_observation.workspace_id`);
@@ -593,14 +597,74 @@ function assertWorkspaceObservation(value, evidenceDigest, label, artifactCaptur
593
597
  throw new ResultStoreError("invalid_result", `${label}.workspace_observation.observed_digest does not match its evidence binding`);
594
598
  }
595
599
  assertRecord(value.scope, `${label}.workspace_observation.scope`);
596
- assertExactKeys(value.scope, ["kind", "relative_root", "excluded_relative_paths"], `${label}.workspace_observation.scope`);
600
+ const hasOpaqueDependencies = value.schema_version === "intentdna.workspace_observation.v4"
601
+ || value.schema_version === "intentdna.workspace_observation.v5";
602
+ const legacyExcludedRelativePaths = [".dna/runtime", ".dna/worktrees/runtime"];
603
+ const codegraphExcludedRelativePaths = [...legacyExcludedRelativePaths, ".codegraph"];
604
+ const canonicalExcludedRelativePaths = Array.isArray(value.scope.excluded_relative_paths)
605
+ ? canonicalizeJson(value.scope.excluded_relative_paths)
606
+ : null;
607
+ const exclusionsAreCanonical = value.schema_version === "intentdna.workspace_observation.v4"
608
+ // Pre-release v4 records exist with both shapes because the version was reused during dogfood.
609
+ ? canonicalExcludedRelativePaths === canonicalizeJson(legacyExcludedRelativePaths)
610
+ || canonicalExcludedRelativePaths === canonicalizeJson(codegraphExcludedRelativePaths)
611
+ : canonicalExcludedRelativePaths === canonicalizeJson(legacyExcludedRelativePaths);
612
+ assertExactKeys(value.scope, hasOpaqueDependencies
613
+ ? ["kind", "relative_root", "excluded_relative_paths", "opaque_external_dependencies"]
614
+ : ["kind", "relative_root", "excluded_relative_paths"], `${label}.workspace_observation.scope`);
597
615
  if (value.scope.kind !== "attempt_workspace"
598
616
  || value.scope.relative_root !== "."
599
617
  || !Array.isArray(value.scope.excluded_relative_paths)
600
- || canonicalizeJson(value.scope.excluded_relative_paths)
601
- !== canonicalizeJson([".dna/runtime", ".dna/worktrees/runtime"])) {
618
+ || !exclusionsAreCanonical) {
602
619
  throw new ResultStoreError("invalid_result", `${label}.workspace_observation.scope is not canonical`);
603
620
  }
621
+ const opaqueDependencies = hasOpaqueDependencies
622
+ ? value.scope.opaque_external_dependencies
623
+ : [];
624
+ if (!Array.isArray(opaqueDependencies)) {
625
+ throw new ResultStoreError("invalid_result", `${label}.workspace_observation.scope.opaque_external_dependencies must be an array`);
626
+ }
627
+ let previousOpaquePath = null;
628
+ for (let index = 0; index < opaqueDependencies.length; index += 1) {
629
+ const dependency = opaqueDependencies[index];
630
+ const dependencyLabel = `${label}.workspace_observation.scope.opaque_external_dependencies[${index}]`;
631
+ assertRecord(dependency, dependencyLabel);
632
+ assertExactKeys(dependency, [
633
+ "kind",
634
+ "relative_path",
635
+ "link_target_digest",
636
+ "resolved_target_path_digest",
637
+ "git_ignore_evidence_digest",
638
+ ], dependencyLabel);
639
+ assertCanonicalNonEmptyString(dependency.relative_path, `${dependencyLabel}.relative_path`);
640
+ const relativePath = dependency.relative_path;
641
+ if (dependency.kind !== "gitignored_symlink_target"
642
+ || relativePath === "."
643
+ || relativePath.startsWith("/")
644
+ || relativePath.split("/").includes("..")
645
+ || (previousOpaquePath !== null
646
+ && Buffer.compare(Buffer.from(previousOpaquePath, "utf8"), Buffer.from(relativePath, "utf8")) >= 0)) {
647
+ throw new ResultStoreError("invalid_result", `${dependencyLabel} is not canonical`);
648
+ }
649
+ for (const field of [
650
+ "link_target_digest",
651
+ "resolved_target_path_digest",
652
+ "git_ignore_evidence_digest",
653
+ ]) {
654
+ if (!SHA256_DIGEST_PATTERN.test(dependency[field])) {
655
+ throw new ResultStoreError("invalid_result", `${dependencyLabel}.${field} must be a lowercase sha256 digest`);
656
+ }
657
+ }
658
+ previousOpaquePath = relativePath;
659
+ }
660
+ if (executionReceipt !== null) {
661
+ const expectedMode = opaqueDependencies.length === 0
662
+ ? "historical_execution"
663
+ : "historical_execution_with_unbound_external_dependencies";
664
+ if (executionReceipt.evidence_mode !== expectedMode) {
665
+ throw new ResultStoreError("invalid_result", `${label}.execution_receipt.evidence_mode does not match its workspace observation scope`);
666
+ }
667
+ }
604
668
  for (const field of ["entry_count", "byte_count"]) {
605
669
  if (!Number.isSafeInteger(value[field]) || value[field] < 0) {
606
670
  throw new ResultStoreError("invalid_result", `${label}.workspace_observation.${field} must be a non-negative safe integer`);
@@ -683,15 +683,16 @@ export interface CanonicalWorkerNonlaunchAttestation {
683
683
  readonly attestation_digest: CanonicalResultDigest;
684
684
  readonly payload: CanonicalWorkerNonlaunchAttestationPayload;
685
685
  }
686
- export declare const CANONICAL_WORKSPACE_OBSERVATION_SCHEMA_VERSION: "intentdna.workspace_observation.v2";
687
- export interface CanonicalWorkspaceObservation {
688
- readonly schema_version: typeof CANONICAL_WORKSPACE_OBSERVATION_SCHEMA_VERSION;
686
+ export declare const CANONICAL_WORKSPACE_OBSERVATION_SCHEMA_VERSION: "intentdna.workspace_observation.v5";
687
+ export interface CanonicalOpaqueExternalDependency {
688
+ readonly kind: "gitignored_symlink_target";
689
+ readonly relative_path: string;
690
+ readonly link_target_digest: CanonicalResultDigest;
691
+ readonly resolved_target_path_digest: CanonicalResultDigest;
692
+ readonly git_ignore_evidence_digest: CanonicalResultDigest;
693
+ }
694
+ interface CanonicalWorkspaceObservationBase {
689
695
  readonly workspace_id: string;
690
- readonly scope: {
691
- readonly kind: "attempt_workspace";
692
- readonly relative_root: ".";
693
- readonly excluded_relative_paths: readonly [".dna/runtime", ".dna/worktrees/runtime"];
694
- };
695
696
  readonly observed_digest: CanonicalResultDigest;
696
697
  readonly entry_count: number;
697
698
  readonly byte_count: number;
@@ -703,10 +704,34 @@ export interface CanonicalWorkspaceObservation {
703
704
  readonly local_config_digest: CanonicalResultDigest;
704
705
  } | null;
705
706
  }
707
+ interface CanonicalWorkspaceObservationScopeV2 {
708
+ readonly kind: "attempt_workspace";
709
+ readonly relative_root: ".";
710
+ readonly excluded_relative_paths: readonly [".dna/runtime", ".dna/worktrees/runtime"];
711
+ readonly opaque_external_dependencies?: never;
712
+ }
713
+ interface CanonicalWorkspaceObservationScopeV4 {
714
+ readonly kind: "attempt_workspace";
715
+ readonly relative_root: ".";
716
+ readonly excluded_relative_paths: readonly [".dna/runtime", ".dna/worktrees/runtime"] | readonly [".dna/runtime", ".dna/worktrees/runtime", ".codegraph"];
717
+ readonly opaque_external_dependencies: readonly CanonicalOpaqueExternalDependency[];
718
+ }
719
+ export type CanonicalWorkspaceObservation = CanonicalWorkspaceObservationBase & ({
720
+ readonly schema_version: "intentdna.workspace_observation.v2" | "intentdna.workspace_observation.v3";
721
+ readonly scope: CanonicalWorkspaceObservationScopeV2;
722
+ } | {
723
+ readonly schema_version: "intentdna.workspace_observation.v4";
724
+ readonly scope: CanonicalWorkspaceObservationScopeV4;
725
+ } | {
726
+ readonly schema_version: typeof CANONICAL_WORKSPACE_OBSERVATION_SCHEMA_VERSION;
727
+ readonly scope: CanonicalWorkspaceObservationScopeV4 & {
728
+ readonly excluded_relative_paths: readonly [".dna/runtime", ".dna/worktrees/runtime"];
729
+ };
730
+ });
706
731
  export declare const CANONICAL_VERIFIER_EXECUTION_RECEIPT_SCHEMA_VERSION: "intentdna.verifier_execution_receipt.v1";
707
732
  export interface CanonicalVerifierExecutionReceipt {
708
733
  readonly schema_version: typeof CANONICAL_VERIFIER_EXECUTION_RECEIPT_SCHEMA_VERSION;
709
- readonly evidence_mode: "historical_execution";
734
+ readonly evidence_mode: "historical_execution" | "historical_execution_with_unbound_external_dependencies";
710
735
  readonly plan_id: CanonicalResultDigest;
711
736
  readonly verifier_id: string;
712
737
  readonly verifier_definition_digest: CanonicalResultDigest;
@@ -115,5 +115,5 @@ export const CANONICAL_ATTEMPT_STOP_INTENT_SCHEMA_VERSION = "intentdna.attempt_s
115
115
  export const CANONICAL_WORKER_STOP_RECEIPT_SCHEMA_VERSION = "intentdna.worker_stop_receipt.v1";
116
116
  export const CANONICAL_WORKER_ALREADY_STOPPED_EVIDENCE_SCHEMA_VERSION = "intentdna.worker_already_stopped_evidence.v1";
117
117
  export const CANONICAL_WORKER_NONLAUNCH_ATTESTATION_SCHEMA_VERSION = "intentdna.worker_nonlaunch_attestation.v1";
118
- export const CANONICAL_WORKSPACE_OBSERVATION_SCHEMA_VERSION = "intentdna.workspace_observation.v2";
118
+ export const CANONICAL_WORKSPACE_OBSERVATION_SCHEMA_VERSION = "intentdna.workspace_observation.v5";
119
119
  export const CANONICAL_VERIFIER_EXECUTION_RECEIPT_SCHEMA_VERSION = "intentdna.verifier_execution_receipt.v1";
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { ProviderTerminatedError } from "./execution-provider.js";
2
3
  import { runProcessTree, } from "./process-tree.js";
3
4
  function eventId() {
4
5
  return randomUUID();
@@ -164,12 +165,19 @@ export async function executeWorkerAttempt(packet, provider, options = {}) {
164
165
  });
165
166
  }
166
167
  catch (error) {
167
- outcome = {
168
- kind: "malformed_result",
169
- exit_code: processResult.status,
170
- signal: null,
171
- error: error instanceof Error ? error.message : String(error),
172
- };
168
+ outcome = error instanceof ProviderTerminatedError
169
+ ? {
170
+ kind: "cancelled",
171
+ exit_code: processResult.status,
172
+ signal: null,
173
+ reason: error.message,
174
+ }
175
+ : {
176
+ kind: "malformed_result",
177
+ exit_code: processResult.status,
178
+ signal: null,
179
+ error: error instanceof Error ? error.message : String(error),
180
+ };
173
181
  }
174
182
  }
175
183
  if (outcome === null) {
@@ -11,6 +11,8 @@ export interface WorkspaceObservationLimits {
11
11
  }
12
12
  export interface WorkspaceObservationHooks {
13
13
  after_directory_read?(relativePath: string): Promise<void> | void;
14
+ after_metadata_read?(relativePath: string): Promise<void> | void;
15
+ after_file_read?(relativePath: string): Promise<void> | void;
14
16
  }
15
17
  export interface ObserveWorkspaceRequest {
16
18
  readonly workspace_root: string;
@@ -26,19 +28,32 @@ export interface GitWorkspaceSnapshot {
26
28
  readonly local_config_digest: `sha256:${string}`;
27
29
  }
28
30
  export interface WorkspaceObservation {
29
- readonly schema_version: "intentdna.workspace_observation.v2";
31
+ readonly schema_version: "intentdna.workspace_observation.v5";
30
32
  readonly observed_digest: `sha256:${string}`;
31
33
  readonly entry_count: number;
32
34
  readonly byte_count: number;
33
- readonly excluded_relative_paths: readonly [".dna/runtime", ".dna/worktrees/runtime"];
35
+ readonly excluded_relative_paths: readonly [
36
+ ".dna/runtime",
37
+ ".dna/worktrees/runtime"
38
+ ];
39
+ readonly opaque_external_dependencies: readonly OpaqueExternalDependency[];
34
40
  readonly git: GitWorkspaceSnapshot | null;
35
41
  }
42
+ export interface OpaqueExternalDependency {
43
+ readonly kind: "gitignored_symlink_target";
44
+ readonly relative_path: string;
45
+ readonly link_target_digest: `sha256:${string}`;
46
+ readonly resolved_target_path_digest: `sha256:${string}`;
47
+ readonly git_ignore_evidence_digest: `sha256:${string}`;
48
+ }
36
49
  export declare const DEFAULT_WORKSPACE_OBSERVATION_LIMITS: WorkspaceObservationLimits;
37
50
  export declare const WORKSPACE_OBSERVATION_EXCLUDED_PATHS: readonly [".dna/runtime", ".dna/worktrees/runtime"];
38
51
  export declare function workspaceObservationPolicy(limits?: Partial<WorkspaceObservationLimits>): {
39
- readonly schema_version: "intentdna.workspace_observation_policy.v1";
52
+ readonly schema_version: "intentdna.workspace_observation_policy.v4";
40
53
  readonly limits: WorkspaceObservationLimits;
41
54
  readonly excluded_relative_paths: typeof WORKSPACE_OBSERVATION_EXCLUDED_PATHS;
42
55
  readonly git_observation: "head_branch_status_local_config";
56
+ readonly symlink_observation: "reject_unignored_record_gitignored_target_as_opaque";
57
+ readonly special_file_observation: "metadata_only_no_open";
43
58
  };
44
59
  export declare function observeWorkspace(request: ObserveWorkspaceRequest): Promise<WorkspaceObservation>;