@fusionkit/handoff 0.1.0

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.
@@ -0,0 +1,69 @@
1
+ import type { CheckpointTier, DisclosureMode } from "@fusionkit/protocol";
2
+ import type { RuntimeTarget } from "./targets.js";
3
+ import type { Trigger } from "./triggers.js";
4
+ /**
5
+ * Client-side continuation policy. This is the SDK's own fail-closed gate,
6
+ * evaluated before anything moves; the plane's org policy is still
7
+ * evaluated independently at contract time. Both must pass.
8
+ */
9
+ export type ContinuationPolicy = {
10
+ kind: "continuation-policy";
11
+ id: "local-first";
12
+ /** Pools work may continue to. Undefined means any pool. */
13
+ allowPools?: string[];
14
+ /** Pools that are always denied, evaluated before the allowlist. */
15
+ denyPools?: string[];
16
+ maxSpendUsd?: number;
17
+ maxDurationMin?: number;
18
+ /** Ceiling for `parallel(...)` fan-out. */
19
+ maxParallelRuns: number;
20
+ disclosure: DisclosureMode;
21
+ /**
22
+ * Conditions under which `h.needs(target)` reports that work should
23
+ * continue. Empty or absent means "needs" reduces to "allowed by policy".
24
+ */
25
+ continueWhen?: Trigger[];
26
+ };
27
+ export type LocalFirstOptions = {
28
+ allowPools?: string[];
29
+ denyPools?: string[];
30
+ maxSpendUsd?: number;
31
+ maxDurationMin?: number;
32
+ maxParallelRuns?: number;
33
+ disclosure?: DisclosureMode;
34
+ continueWhen?: Trigger[];
35
+ };
36
+ /**
37
+ * localFirst() defaults: a modest fan-out ceiling and the most conservative
38
+ * disclosure mode. Orgs override per policy via LocalFirstOptions.
39
+ */
40
+ export declare const DEFAULT_MAX_PARALLEL_RUNS = 4;
41
+ export declare const DEFAULT_DISCLOSURE: "minimal-context";
42
+ /**
43
+ * Local-first: work stays local until the app explicitly continues it,
44
+ * and continuation is allowed only within the configured bounds.
45
+ */
46
+ export declare function localFirst(options?: LocalFirstOptions): ContinuationPolicy;
47
+ /** Deterministic, explainable continuation-planning outcome. */
48
+ export type PlanningDecision = {
49
+ decision: "continue" | "deny";
50
+ target: RuntimeTarget;
51
+ tier: CheckpointTier;
52
+ disclosure: DisclosureMode;
53
+ reasons: string[];
54
+ };
55
+ export type PlanInput = {
56
+ target: RuntimeTarget;
57
+ secrets: string[];
58
+ budget: {
59
+ maxSpendUsd?: number;
60
+ maxDurationMin?: number;
61
+ };
62
+ parallelism: number;
63
+ };
64
+ /**
65
+ * The v1 planner is deterministic policy logic, not a model. Every
66
+ * decision carries human-readable reasons so the trace can explain why
67
+ * the runtime boundary changed (or refused to).
68
+ */
69
+ export declare function planContinuation(policy: ContinuationPolicy, input: PlanInput): PlanningDecision;
package/dist/policy.js ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * localFirst() defaults: a modest fan-out ceiling and the most conservative
3
+ * disclosure mode. Orgs override per policy via LocalFirstOptions.
4
+ */
5
+ export const DEFAULT_MAX_PARALLEL_RUNS = 4;
6
+ export const DEFAULT_DISCLOSURE = "minimal-context";
7
+ /**
8
+ * Local-first: work stays local until the app explicitly continues it,
9
+ * and continuation is allowed only within the configured bounds.
10
+ */
11
+ export function localFirst(options = {}) {
12
+ return {
13
+ kind: "continuation-policy",
14
+ id: "local-first",
15
+ ...(options.allowPools ? { allowPools: options.allowPools } : {}),
16
+ ...(options.denyPools ? { denyPools: options.denyPools } : {}),
17
+ ...(options.maxSpendUsd !== undefined
18
+ ? { maxSpendUsd: options.maxSpendUsd }
19
+ : {}),
20
+ ...(options.maxDurationMin !== undefined
21
+ ? { maxDurationMin: options.maxDurationMin }
22
+ : {}),
23
+ maxParallelRuns: options.maxParallelRuns ?? DEFAULT_MAX_PARALLEL_RUNS,
24
+ disclosure: options.disclosure ?? DEFAULT_DISCLOSURE,
25
+ ...(options.continueWhen ? { continueWhen: options.continueWhen } : {})
26
+ };
27
+ }
28
+ /**
29
+ * The v1 planner is deterministic policy logic, not a model. Every
30
+ * decision carries human-readable reasons so the trace can explain why
31
+ * the runtime boundary changed (or refused to).
32
+ */
33
+ export function planContinuation(policy, input) {
34
+ const denials = [];
35
+ const pool = input.target.pool;
36
+ if (policy.denyPools?.includes(pool)) {
37
+ denials.push(`pool "${pool}" is denied by continuation policy`);
38
+ }
39
+ if (policy.allowPools && !policy.allowPools.includes(pool)) {
40
+ denials.push(`pool "${pool}" is not in the continuation allowlist`);
41
+ }
42
+ // An omitted budget field means "inherit the policy ceiling": the request
43
+ // is still bounded, because the plane enforces budget ceilings again at
44
+ // contract issuance and the runner applies a hard session timeout. Only
45
+ // an explicit value above the ceiling is a denial.
46
+ if (policy.maxSpendUsd !== undefined &&
47
+ (input.budget.maxSpendUsd ?? 0) > policy.maxSpendUsd) {
48
+ denials.push(`requested budget $${input.budget.maxSpendUsd} exceeds policy ceiling $${policy.maxSpendUsd}`);
49
+ }
50
+ if (policy.maxDurationMin !== undefined &&
51
+ (input.budget.maxDurationMin ?? 0) > policy.maxDurationMin) {
52
+ denials.push(`requested duration ${input.budget.maxDurationMin}m exceeds policy ceiling ${policy.maxDurationMin}m`);
53
+ }
54
+ if (input.parallelism > policy.maxParallelRuns) {
55
+ denials.push(`requested ${input.parallelism} parallel runs exceeds policy ceiling ${policy.maxParallelRuns}`);
56
+ }
57
+ if (denials.length > 0) {
58
+ return {
59
+ decision: "deny",
60
+ target: input.target,
61
+ tier: "workspace",
62
+ disclosure: policy.disclosure,
63
+ reasons: denials
64
+ };
65
+ }
66
+ return {
67
+ decision: "continue",
68
+ target: input.target,
69
+ tier: "workspace",
70
+ disclosure: policy.disclosure,
71
+ reasons: [
72
+ `pool "${pool}" is allowed`,
73
+ `disclosure mode "${policy.disclosure}"`,
74
+ input.secrets.length > 0
75
+ ? `secret release requested: ${input.secrets.join(", ")} (subject to org policy)`
76
+ : "no secrets requested"
77
+ ]
78
+ };
79
+ }
@@ -0,0 +1,57 @@
1
+ import type { ReceiptBundle } from "@fusionkit/protocol";
2
+ import { PlaneClient } from "@fusionkit/sdk";
3
+ import { HandoffRun } from "./run.js";
4
+ /**
5
+ * Typed review strategies for choosing among fan-out attempts.
6
+ * Deterministic and explainable, like the planner.
7
+ */
8
+ export type ReviewStrategy = {
9
+ kind: "review-strategy";
10
+ id: "smallest-diff" | "first-completed" | "tests-pass-smallest-diff";
11
+ };
12
+ export declare const reviewStrategies: {
13
+ /** Prefer the completed attempt with the smallest output diff. */
14
+ smallestDiff(): ReviewStrategy;
15
+ /** Prefer the completed attempt that finished first. */
16
+ firstCompleted(): ReviewStrategy;
17
+ /**
18
+ * The spec's flagship strategy: among attempts whose harness exited
19
+ * cleanly (the "tests pass" signal at the session boundary), prefer the
20
+ * smallest output diff.
21
+ */
22
+ testsPassSmallestDiff(): ReviewStrategy;
23
+ };
24
+ /** Deterministic, evidence-derived comparison data for one attempt. */
25
+ export type Scorecard = {
26
+ status: ReceiptBundle["receipt"]["status"];
27
+ exitCode?: number;
28
+ diffBytes: number;
29
+ filesChanged: number;
30
+ durationMs: number;
31
+ eventCount: number;
32
+ blockedEgressAttempts: number;
33
+ secretsReleased: number;
34
+ };
35
+ export type ReviewedRun = {
36
+ run: HandoffRun;
37
+ bundle: ReceiptBundle;
38
+ scorecard: Scorecard;
39
+ /** Kept for convenience; equals scorecard.diffBytes. */
40
+ diffBytes: number;
41
+ endedAt: string;
42
+ };
43
+ export type ReviewResult = {
44
+ chosen: ReviewedRun;
45
+ candidates: ReviewedRun[];
46
+ strategy: ReviewStrategy;
47
+ reason: string;
48
+ };
49
+ /**
50
+ * Build the deterministic, evidence-derived scorecard for one run from its
51
+ * receipt bundle and output diff size. Exposed so adapters that judge runs
52
+ * outside the fan-out review path (e.g. the swarm tools, where a cloud model
53
+ * judges each worker) consume the exact same deterministic evidence the
54
+ * review strategies do, rather than reconstructing it.
55
+ */
56
+ export declare function scorecardFor(bundle: ReceiptBundle, diffBytes: number): Scorecard;
57
+ export declare function reviewRuns(client: PlaneClient, runs: HandoffRun[], strategy: ReviewStrategy): Promise<ReviewResult>;
package/dist/review.js ADDED
@@ -0,0 +1,110 @@
1
+ export const reviewStrategies = {
2
+ /** Prefer the completed attempt with the smallest output diff. */
3
+ smallestDiff() {
4
+ return { kind: "review-strategy", id: "smallest-diff" };
5
+ },
6
+ /** Prefer the completed attempt that finished first. */
7
+ firstCompleted() {
8
+ return { kind: "review-strategy", id: "first-completed" };
9
+ },
10
+ /**
11
+ * The spec's flagship strategy: among attempts whose harness exited
12
+ * cleanly (the "tests pass" signal at the session boundary), prefer the
13
+ * smallest output diff.
14
+ */
15
+ testsPassSmallestDiff() {
16
+ return { kind: "review-strategy", id: "tests-pass-smallest-diff" };
17
+ }
18
+ };
19
+ /**
20
+ * Build the deterministic, evidence-derived scorecard for one run from its
21
+ * receipt bundle and output diff size. Exposed so adapters that judge runs
22
+ * outside the fan-out review path (e.g. the swarm tools, where a cloud model
23
+ * judges each worker) consume the exact same deterministic evidence the
24
+ * review strategies do, rather than reconstructing it.
25
+ */
26
+ export function scorecardFor(bundle, diffBytes) {
27
+ const { receipt, events } = bundle;
28
+ // Distinct files, not raw event count: a file touched repeatedly during a
29
+ // session still counts once. exitCode is the session's final command —
30
+ // the run's overall outcome by harness convention; per-command results
31
+ // remain available in the event chain.
32
+ const changedPaths = new Set();
33
+ let exitCode;
34
+ for (const entry of events) {
35
+ if (entry.event.type === "file.changed")
36
+ changedPaths.add(entry.event.path);
37
+ if (entry.event.type === "command.executed")
38
+ exitCode = entry.event.exitCode;
39
+ }
40
+ const filesChanged = changedPaths.size;
41
+ return {
42
+ status: receipt.status,
43
+ ...(exitCode !== undefined ? { exitCode } : {}),
44
+ diffBytes,
45
+ filesChanged,
46
+ durationMs: new Date(receipt.endedAt).getTime() - new Date(receipt.startedAt).getTime(),
47
+ eventCount: receipt.eventCount,
48
+ blockedEgressAttempts: receipt.networkAccessed.filter((record) => record.decision === "blocked").length,
49
+ secretsReleased: receipt.secretsReleased.length
50
+ };
51
+ }
52
+ export async function reviewRuns(client, runs, strategy) {
53
+ const candidates = [];
54
+ const skipped = [];
55
+ for (const run of runs) {
56
+ const status = await run.status();
57
+ if (status !== "completed") {
58
+ skipped.push({ runId: run.runId, status });
59
+ continue;
60
+ }
61
+ const bundle = await run.receipt();
62
+ const diffHash = bundle.receipt.workspaceOut.diffHash;
63
+ const diffBytes = diffHash ? (await client.getBlob(diffHash)).length : 0;
64
+ candidates.push({
65
+ run,
66
+ bundle,
67
+ scorecard: scorecardFor(bundle, diffBytes),
68
+ diffBytes,
69
+ endedAt: bundle.receipt.endedAt
70
+ });
71
+ }
72
+ if (candidates.length === 0) {
73
+ const detail = skipped
74
+ .map((entry) => `${entry.runId}: ${entry.status}`)
75
+ .join(", ");
76
+ throw new Error(`no completed runs to review${detail ? ` (${detail})` : ""}`);
77
+ }
78
+ let chosen;
79
+ let reason;
80
+ switch (strategy.id) {
81
+ case "smallest-diff": {
82
+ chosen = candidates.reduce((a, b) => (b.diffBytes < a.diffBytes ? b : a));
83
+ reason = `smallest output diff (${chosen.diffBytes} bytes) among ${candidates.length} completed attempt(s)`;
84
+ break;
85
+ }
86
+ case "first-completed": {
87
+ chosen = candidates.reduce((a, b) => new Date(b.endedAt).getTime() < new Date(a.endedAt).getTime() ? b : a);
88
+ reason = `first attempt to complete (${chosen.endedAt}) among ${candidates.length} completed attempt(s)`;
89
+ break;
90
+ }
91
+ case "tests-pass-smallest-diff": {
92
+ // This strategy's definition of "tests pass" IS harness exit 0 — that
93
+ // is the session-boundary signal the spec names. Agents with custom
94
+ // success semantics (multiple commands, non-zero success codes) pick
95
+ // their winner directly from candidates' scorecards/events instead.
96
+ const passing = candidates.filter((candidate) => candidate.scorecard.exitCode === 0);
97
+ if (passing.length === 0) {
98
+ throw new Error("no attempt passed (harness exit 0) to review");
99
+ }
100
+ chosen = passing.reduce((a, b) => (b.diffBytes < a.diffBytes ? b : a));
101
+ reason = `harness exited 0 and smallest output diff (${chosen.diffBytes} bytes) among ${passing.length} passing attempt(s)`;
102
+ break;
103
+ }
104
+ default: {
105
+ const exhausted = strategy.id;
106
+ throw new Error(`unknown review strategy: ${String(exhausted)}`);
107
+ }
108
+ }
109
+ return { chosen, candidates, strategy, reason };
110
+ }
@@ -0,0 +1,76 @@
1
+ import type { ActorRef, BundleVerification, ExecutionSpec, ReceiptBundle, RunStatus, SessionIsolation } from "@fusionkit/protocol";
2
+ import type { PlaneClient } from "@fusionkit/sdk";
3
+ import type { PullResult } from "@fusionkit/workspace";
4
+ import type { Handoff } from "./handoff.js";
5
+ import type { ContinuationPolicy } from "./policy.js";
6
+ import type { HandoffRun } from "./run.js";
7
+ import type { RuntimeTarget } from "./targets.js";
8
+ /**
9
+ * The one configuration shape for adapters that run the "command" harness
10
+ * over governed sessions (the AI SDK remote tools and the compute surface
11
+ * extend this rather than redeclaring it).
12
+ */
13
+ export type CommandHarnessConfig = {
14
+ /** Local git workspace whose state the governed session materializes. */
15
+ workspace: string;
16
+ plane: PlaneClient | {
17
+ url: string;
18
+ adminToken: string;
19
+ };
20
+ /** Runner pool that executes the commands. */
21
+ pool: string;
22
+ actor?: ActorRef;
23
+ policy?: ContinuationPolicy;
24
+ secrets?: string[];
25
+ allowHosts?: string[];
26
+ allowUntracked?: string[];
27
+ /** Requested session isolation for command runs. Defaults to "process". */
28
+ session?: SessionIsolation;
29
+ /** Per-command wait ceiling. Defaults to 5 minutes. */
30
+ timeoutMs?: number;
31
+ };
32
+ /**
33
+ * Build the continuation context every command-harness adapter shares:
34
+ * a `handoff(...)` bound to the command agent, with the optional fields
35
+ * spread only when present.
36
+ */
37
+ export declare function createCommandContext(config: CommandHarnessConfig): Handoff;
38
+ export type GovernedCommandOptions = {
39
+ command: string;
40
+ target: RuntimeTarget;
41
+ reason: string;
42
+ timeoutMs: number;
43
+ pullResults?: boolean;
44
+ execution?: ExecutionSpec;
45
+ session?: SessionIsolation;
46
+ };
47
+ export type GovernedCommandResult = {
48
+ run: HandoffRun;
49
+ status: RunStatus;
50
+ output: string;
51
+ exitCode: number | undefined;
52
+ receiptBundle: ReceiptBundle;
53
+ verification: BundleVerification;
54
+ pullResult?: PullResult;
55
+ };
56
+ /**
57
+ * The receipt-backed evidence record adapters keep for every governed
58
+ * command: run id, terminal status, contract hash, and the offline
59
+ * verification verdict. Adapter-specific fields (tool name, sandbox id)
60
+ * are intersected on by the caller.
61
+ */
62
+ export type GovernedRunRecord = {
63
+ command: string;
64
+ runId: string;
65
+ status: RunStatus;
66
+ exitCode?: number;
67
+ contractHash: string;
68
+ /** Actual runner isolation recorded in the receipt. */
69
+ isolation?: SessionIsolation;
70
+ /** Offline verification result of the receipt bundle for this command. */
71
+ receiptVerified: boolean;
72
+ pullMode?: PullResult["mode"];
73
+ };
74
+ /** Distill a governed command result into its evidence record. */
75
+ export declare function toGovernedRunRecord(command: string, result: GovernedCommandResult): GovernedRunRecord;
76
+ export declare function executeGovernedCommand(context: Handoff, options: GovernedCommandOptions): Promise<GovernedCommandResult>;
@@ -0,0 +1,71 @@
1
+ import { verifyReceiptBundle } from "@fusionkit/protocol";
2
+ import { agents } from "./agents.js";
3
+ import { handoff } from "./handoff.js";
4
+ /**
5
+ * Build the continuation context every command-harness adapter shares:
6
+ * a `handoff(...)` bound to the command agent, with the optional fields
7
+ * spread only when present.
8
+ */
9
+ export function createCommandContext(config) {
10
+ return handoff({
11
+ workspace: config.workspace,
12
+ plane: config.plane,
13
+ agent: agents.command(),
14
+ ...(config.actor ? { actor: config.actor } : {}),
15
+ ...(config.policy ? { policy: config.policy } : {}),
16
+ ...(config.secrets ? { secrets: config.secrets } : {}),
17
+ ...(config.allowHosts ? { allowHosts: config.allowHosts } : {}),
18
+ ...(config.allowUntracked ? { allowUntracked: config.allowUntracked } : {})
19
+ });
20
+ }
21
+ /** Distill a governed command result into its evidence record. */
22
+ export function toGovernedRunRecord(command, result) {
23
+ return {
24
+ command,
25
+ runId: result.run.runId,
26
+ status: result.status,
27
+ ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
28
+ contractHash: result.receiptBundle.receipt.contractHash,
29
+ ...(result.receiptBundle.receipt.runner.isolation
30
+ ? { isolation: result.receiptBundle.receipt.runner.isolation }
31
+ : {}),
32
+ receiptVerified: result.verification.ok,
33
+ ...(result.pullResult ? { pullMode: result.pullResult.mode } : {})
34
+ };
35
+ }
36
+ export async function executeGovernedCommand(context, options) {
37
+ const execution = options.execution ?? {
38
+ kind: "shell",
39
+ script: options.command
40
+ };
41
+ const run = await context.continueIn(options.target, {
42
+ task: options.command,
43
+ agent: agents.command(),
44
+ reason: options.reason,
45
+ execution,
46
+ ...(options.session ? { session: options.session } : {})
47
+ });
48
+ const outcome = await run.wait({ timeoutMs: options.timeoutMs });
49
+ if (outcome.status === "awaiting_approval") {
50
+ throw new Error(`run ${run.runId} is blocked on consent (${outcome.consentRequirements.join("; ")}); ` +
51
+ `approve it with: warrant approve ${run.runId}`);
52
+ }
53
+ const [output, exitCode, receiptBundle] = await Promise.all([
54
+ run.sessionLog(),
55
+ run.commandExitCode(),
56
+ run.receipt()
57
+ ]);
58
+ const verification = verifyReceiptBundle(receiptBundle);
59
+ const pullResult = options.pullResults === true && outcome.status === "completed"
60
+ ? await run.pull()
61
+ : undefined;
62
+ return {
63
+ run,
64
+ status: outcome.status,
65
+ output,
66
+ exitCode,
67
+ receiptBundle,
68
+ verification,
69
+ ...(pullResult ? { pullResult } : {})
70
+ };
71
+ }
package/dist/run.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import type { ActorRef, ChainedEvent, CheckpointTier, HandoffEnvelope, ReceiptBundle, RunStatus } from "@fusionkit/protocol";
2
+ import { PlaneClient } from "@fusionkit/sdk";
3
+ import type { PullResult } from "@fusionkit/workspace";
4
+ import type { IsolationStrategy } from "./isolation.js";
5
+ import type { RuntimeTarget } from "./targets.js";
6
+ export type WaitOptions = {
7
+ timeoutMs?: number;
8
+ pollMs?: number;
9
+ };
10
+ export type WaitOutcome = {
11
+ status: RunStatus;
12
+ /** Present when the run is blocked on a human decision. */
13
+ consentRequirements: string[];
14
+ };
15
+ /**
16
+ * A continuation that became a governed run. Wraps the plane API with the
17
+ * operations a continuation caller needs: wait, approve, receipt, pull.
18
+ */
19
+ export declare class HandoffRun {
20
+ readonly runId: string;
21
+ readonly target: RuntimeTarget;
22
+ readonly envelope: HandoffEnvelope;
23
+ readonly envelopeHash: string;
24
+ /** Human-readable planner explanation for why this continuation ran. */
25
+ readonly explanation: string;
26
+ /** Isolation strategy applied at pull time. */
27
+ readonly isolate?: IsolationStrategy;
28
+ private readonly client;
29
+ private readonly actor;
30
+ private readonly workspaceDir;
31
+ private readonly onTerminal;
32
+ private readonly onPulled;
33
+ constructor(input: {
34
+ runId: string;
35
+ target: RuntimeTarget;
36
+ envelope: HandoffEnvelope;
37
+ envelopeHash: string;
38
+ explanation?: string;
39
+ isolate?: IsolationStrategy;
40
+ client: PlaneClient;
41
+ actor: ActorRef;
42
+ workspaceDir: string;
43
+ onTerminal: (runId: string, status: RunStatus) => void;
44
+ onPulled: (runId: string, mode: PullResult["mode"]) => void;
45
+ });
46
+ /** The checkpoint tier this continuation carried. */
47
+ get tier(): CheckpointTier;
48
+ /** Deep link to this run in the control panel. */
49
+ get url(): string;
50
+ /** Where the signed evidence lives: bundle download via the CLI. */
51
+ get auditUrl(): string;
52
+ status(): Promise<RunStatus>;
53
+ events(): Promise<ChainedEvent[]>;
54
+ /**
55
+ * Poll until the run is terminal or blocked on consent. Consent is a
56
+ * human decision; the SDK surfaces it instead of spinning forever.
57
+ */
58
+ wait(options?: WaitOptions): Promise<WaitOutcome>;
59
+ /**
60
+ * The session's combined stdout/stderr, fetched by the content hash
61
+ * recorded in the event chain. Empty when the session produced no output.
62
+ */
63
+ sessionLog(): Promise<string>;
64
+ /**
65
+ * Exit code of the session's final harness command (the run's overall
66
+ * outcome by convention). Sessions that execute multiple commands surface
67
+ * each one as its own command.executed entry in events() for callers that
68
+ * need per-command results.
69
+ */
70
+ commandExitCode(): Promise<number | undefined>;
71
+ /** Grant required consent as the given actor (defaults to the context actor). */
72
+ approve(actor?: ActorRef): Promise<RunStatus>;
73
+ /** Cancel the run if it has not been claimed by a runner yet. */
74
+ cancel(actor?: ActorRef): Promise<RunStatus>;
75
+ /** The signed, offline-verifiable receipt bundle. */
76
+ receipt(): Promise<ReceiptBundle>;
77
+ /**
78
+ * Divergence-safe pull of the run's output into the local workspace:
79
+ * applied in place when the workspace is clean at the contract base ref,
80
+ * otherwise materialized on a dedicated branch. A `branch()` isolation
81
+ * strategy (set here or at continueIn/parallel time) always lands on a
82
+ * branch and never touches the working tree.
83
+ */
84
+ pull(options?: {
85
+ repoDir?: string;
86
+ isolate?: IsolationStrategy;
87
+ }): Promise<PullResult>;
88
+ }