intentdna 1.8.6 → 1.8.7

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.
Files changed (60) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +1 -0
  4. package/dist/cli/commands/run-lifecycle.d.ts +73 -0
  5. package/dist/cli/commands/run-lifecycle.js +240 -0
  6. package/dist/cli/commands/run.d.ts +22 -40
  7. package/dist/cli/commands/run.js +674 -392
  8. package/dist/cli/index.js +87 -2
  9. package/dist/compiler/workflow.js +3 -2
  10. package/dist/hooks/cli.d.ts +1 -2
  11. package/dist/hooks/cli.js +119 -80
  12. package/dist/hooks/enforce.d.ts +2 -0
  13. package/dist/hooks/enforce.js +56 -27
  14. package/dist/hooks/enforcement-boundary.d.ts +13 -0
  15. package/dist/hooks/enforcement-boundary.js +33 -0
  16. package/dist/hooks/index.d.ts +3 -2
  17. package/dist/hooks/index.js +3 -2
  18. package/dist/hooks/protocol.d.ts +12 -4
  19. package/dist/hooks/protocol.js +20 -14
  20. package/dist/hooks/schema.d.ts +2 -1
  21. package/dist/hooks/schema.js +6 -2
  22. package/dist/hooks/state-manager.d.ts +5 -5
  23. package/dist/hooks/state-manager.js +26 -24
  24. package/dist/hooks/state.d.ts +19 -3
  25. package/dist/hooks/state.js +327 -80
  26. package/dist/mcp/index.js +0 -0
  27. package/dist/runtime/diagnosis-contract-verifier.d.ts +11 -0
  28. package/dist/runtime/diagnosis-contract-verifier.js +417 -0
  29. package/dist/runtime/execution-provider.d.ts +40 -0
  30. package/dist/runtime/execution-provider.js +138 -0
  31. package/dist/runtime/handoff-resolver.d.ts +61 -0
  32. package/dist/runtime/handoff-resolver.js +167 -0
  33. package/dist/runtime/index.d.ts +24 -0
  34. package/dist/runtime/index.js +13 -0
  35. package/dist/runtime/process-tree.d.ts +47 -0
  36. package/dist/runtime/process-tree.js +402 -0
  37. package/dist/runtime/providers/claude.d.ts +9 -0
  38. package/dist/runtime/providers/claude.js +64 -0
  39. package/dist/runtime/providers/codex.d.ts +8 -0
  40. package/dist/runtime/providers/codex.js +72 -0
  41. package/dist/runtime/result-store.d.ts +32 -0
  42. package/dist/runtime/result-store.js +130 -0
  43. package/dist/runtime/run-contracts.d.ts +290 -0
  44. package/dist/runtime/run-contracts.js +58 -0
  45. package/dist/runtime/run-controller.d.ts +149 -0
  46. package/dist/runtime/run-controller.js +1108 -0
  47. package/dist/runtime/run-store.d.ts +96 -0
  48. package/dist/runtime/run-store.js +725 -0
  49. package/dist/runtime/worker-executor.d.ts +19 -0
  50. package/dist/runtime/worker-executor.js +194 -0
  51. package/dist/runtime/workflow-plan-adapter.d.ts +26 -0
  52. package/dist/runtime/workflow-plan-adapter.js +416 -0
  53. package/dist/runtime/workflow-runner.d.ts +15 -3
  54. package/dist/runtime/workflow-runner.js +13 -1
  55. package/dist/runtime/workspace-isolation.d.ts +103 -0
  56. package/dist/runtime/workspace-isolation.js +373 -0
  57. package/dist/schema/types.d.ts +1 -0
  58. package/dist/schema/validate.js +64 -6
  59. package/dist/schema/yaml-parser.js +7 -2
  60. package/package.json +1 -1
@@ -0,0 +1,167 @@
1
+ export class HandoffResolutionError extends Error {
2
+ code;
3
+ binding_id;
4
+ result_id;
5
+ constructor(code, message, bindingId, resultId, options) {
6
+ super(message, options);
7
+ this.name = "HandoffResolutionError";
8
+ this.code = code;
9
+ this.binding_id = bindingId;
10
+ this.result_id = resultId;
11
+ }
12
+ }
13
+ function noHandoff(request, reason) {
14
+ return {
15
+ binding_id: request.binding_id,
16
+ input_name: request.input_name,
17
+ required: false,
18
+ description: request.description,
19
+ mode: "none",
20
+ reason,
21
+ };
22
+ }
23
+ function requireRequestIdentity(request) {
24
+ const sourceIsInvalid = request.mode !== "none" &&
25
+ (!request.source.result_id ||
26
+ !request.source.step_id ||
27
+ !request.source.output_name);
28
+ if (!request.binding_id || !request.input_name || sourceIsInvalid) {
29
+ throw new HandoffResolutionError("invalid_request", "binding_id, input_name, and any declared source identities must be non-empty", request.binding_id, request.mode === "none" ? null : request.source.result_id);
30
+ }
31
+ }
32
+ function findOutput(outputs, source) {
33
+ return outputs.find((output) => output.name === source.output_name) ?? null;
34
+ }
35
+ /**
36
+ * Resolves only declared values from immutable committed results. It has no
37
+ * provider-session, transcript, Hook-state, filesystem, or summarization input.
38
+ */
39
+ export class HandoffResolver {
40
+ results;
41
+ structuredValidator;
42
+ constructor(results, options = {}) {
43
+ this.results = results;
44
+ this.structuredValidator = options.validate_structured_value;
45
+ }
46
+ async resolve(runId, request) {
47
+ requireRequestIdentity(request);
48
+ if (request.mode === "none") {
49
+ return noHandoff(request, request.reason);
50
+ }
51
+ const result = await this.results.read(runId, request.source.result_id);
52
+ if (!result) {
53
+ if (!request.required) {
54
+ return noHandoff(request, "optional upstream result is not committed");
55
+ }
56
+ throw new HandoffResolutionError("required_input_uncommitted", `required input ${request.input_name} needs committed result ${request.source.result_id}`, request.binding_id, request.source.result_id);
57
+ }
58
+ if (result.step_id !== request.source.step_id) {
59
+ throw new HandoffResolutionError("source_identity_mismatch", `result ${result.result_id} belongs to step ${result.step_id}, not ${request.source.step_id}`, request.binding_id, request.source.result_id);
60
+ }
61
+ const output = findOutput(result.outputs, request.source);
62
+ if (!output) {
63
+ if (!request.required) {
64
+ return noHandoff(request, "optional upstream output is absent");
65
+ }
66
+ throw new HandoffResolutionError("source_output_missing", `required output ${request.source.output_name} is absent from result ${result.result_id}`, request.binding_id, request.source.result_id);
67
+ }
68
+ if (request.mode === "reference") {
69
+ if (output.kind !== "reference") {
70
+ throw this.kindMismatch(request, output);
71
+ }
72
+ return {
73
+ binding_id: request.binding_id,
74
+ input_name: request.input_name,
75
+ required: request.required,
76
+ description: request.description,
77
+ mode: "reference",
78
+ source: request.source,
79
+ reference: output.reference,
80
+ };
81
+ }
82
+ if (request.mode === "quote") {
83
+ if (output.kind !== "text") {
84
+ throw this.kindMismatch(request, output);
85
+ }
86
+ const selection = request.selection;
87
+ if (selection === null) {
88
+ return {
89
+ binding_id: request.binding_id,
90
+ input_name: request.input_name,
91
+ required: request.required,
92
+ description: request.description,
93
+ mode: "quote",
94
+ source: request.source,
95
+ exact_text: output.text,
96
+ selection: { start: null, end: null, selector: null },
97
+ };
98
+ }
99
+ if (!Number.isSafeInteger(selection.start) ||
100
+ !Number.isSafeInteger(selection.end) ||
101
+ selection.start < 0 ||
102
+ selection.end < selection.start ||
103
+ selection.end > output.text.length) {
104
+ throw new HandoffResolutionError("invalid_quote_selection", `quote range ${selection.start}:${selection.end} is invalid for output length ${output.text.length}`, request.binding_id, request.source.result_id);
105
+ }
106
+ return {
107
+ binding_id: request.binding_id,
108
+ input_name: request.input_name,
109
+ required: request.required,
110
+ description: request.description,
111
+ mode: "quote",
112
+ source: request.source,
113
+ exact_text: output.text.slice(selection.start, selection.end),
114
+ selection,
115
+ };
116
+ }
117
+ if (output.kind !== "structured") {
118
+ throw this.kindMismatch(request, output);
119
+ }
120
+ if (request.schema_ref !== null &&
121
+ output.schema_ref !== request.schema_ref) {
122
+ throw new HandoffResolutionError("structured_schema_mismatch", `output schema ${String(output.schema_ref)} does not match input schema ${request.schema_ref}`, request.binding_id, request.source.result_id);
123
+ }
124
+ const schemaRef = request.schema_ref ?? output.schema_ref;
125
+ if (schemaRef !== null) {
126
+ if (!this.structuredValidator) {
127
+ throw new HandoffResolutionError("structured_schema_validator_missing", `structured handoff ${request.binding_id} requires validator ${schemaRef}`, request.binding_id, request.source.result_id);
128
+ }
129
+ try {
130
+ await this.structuredValidator(schemaRef, output.value);
131
+ }
132
+ catch (error) {
133
+ throw new HandoffResolutionError("structured_value_invalid", `structured output ${request.source.output_name} failed schema ${schemaRef}`, request.binding_id, request.source.result_id, { cause: error });
134
+ }
135
+ }
136
+ return {
137
+ binding_id: request.binding_id,
138
+ input_name: request.input_name,
139
+ required: request.required,
140
+ description: request.description,
141
+ mode: "structured",
142
+ source: request.source,
143
+ value: output.value,
144
+ schema_ref: schemaRef,
145
+ };
146
+ }
147
+ async resolveAll(runId, requests) {
148
+ const bindingIds = new Set();
149
+ const inputNames = new Set();
150
+ for (const request of requests) {
151
+ if (bindingIds.has(request.binding_id) ||
152
+ inputNames.has(request.input_name)) {
153
+ throw new HandoffResolutionError("duplicate_binding", `duplicate binding_id or input_name for ${request.binding_id}`, request.binding_id, request.mode === "none" ? null : request.source.result_id);
154
+ }
155
+ bindingIds.add(request.binding_id);
156
+ inputNames.add(request.input_name);
157
+ }
158
+ const bindings = [];
159
+ for (const request of requests) {
160
+ bindings.push(await this.resolve(runId, request));
161
+ }
162
+ return bindings;
163
+ }
164
+ kindMismatch(request, output) {
165
+ return new HandoffResolutionError("source_output_kind_mismatch", `handoff ${request.binding_id} requested ${request.mode}, but output ${output.name} is ${output.kind}`, request.binding_id, request.source.result_id);
166
+ }
167
+ }
@@ -15,3 +15,27 @@ export { createClaudeCoreSyncTargetPlan, executeClaudeCoreSyncTargetPlan, format
15
15
  export type { ClaudeCoreAdapterCapabilityReport, ClaudeCoreCapabilityReportEntry, ClaudeCorePlannedArtifact, ClaudeCoreSyncTargetPlan, ClaudeCoreSyncTargetPlanOptions, ClaudeSettingsHookRegistration, ClaudeSyncMode, ExecuteClaudeCoreSyncTargetPlanResult, } from "./claude-sync-target.js";
16
16
  export type { AdapterCapabilityReport, CapabilityReportEntry, CapabilitySupportStatus, EvidenceClassification, EvidencePlan, PlannedArtifact, SyncTarget, SyncTargetMode, SyncTargetPlan, } from "./sync-target-plan.js";
17
17
  export { formatSyncTargetPlanDiagnostics, trackedOutputsForSyncTargetPlan, } from "./sync-target-plan.js";
18
+ export { TASK_STATE_VALUES, TASK_STATE_TRANSITIONS, RETRY_BACKOFF_VALUES as RUNTIME_RETRY_BACKOFF_VALUES, TERMINAL_OUTCOME_KIND_VALUES, RUNTIME_EVENT_TYPE_VALUES, } from "./run-contracts.js";
19
+ export type { RuntimeId, RunId, StepId, AttemptId, WorkerSessionId, ResultId, EventId, ClaimToken, JsonValue, TaskKey, AttemptIdentity, TaskState, TerminalTaskState, TaskRecord, TaskLease, TaskClaim, RetryBackoff as RuntimeRetryBackoff, RetryPolicy as RuntimeRetryPolicy, RetryDecision, AttemptPhase, AttemptRecord, HandoffSource, ReferenceHandoffBinding, QuoteHandoffBinding, StructuredHandoffBinding, NoHandoffBinding, HandoffBinding, StepOutputKind, DeclaredStepOutput, StepOutput, StepPacket, TerminalOutcomeKind, TerminalOutcome, ProviderEvent, ProviderExecutionResult, CommittedStepResult, RuntimeEventType, RuntimeEvent, } from "./run-contracts.js";
20
+ export { RUN_STORE_SCHEMA_VERSION, RUN_STATUS_VALUES, RunStoreError, DurableRunStore, } from "./run-store.js";
21
+ export type { RunStatus, TerminalRunStatus, RunCancellationIntent, RunTerminalRecord, RunRecord, StoredTaskClaim, RunStoreSnapshot, CreateRunStoreRecord, RunStoreUpdate, RunStoreUpdateResult, DurableRunStoreOptions, RunStoreErrorCode, } from "./run-store.js";
22
+ export { ResultStoreError, ImmutableResultStore, } from "./result-store.js";
23
+ export type { ResultStoreErrorCode, CommitStepResultRequest, CommitStepResultResponse, } from "./result-store.js";
24
+ export { HandoffResolutionError, HandoffResolver, } from "./handoff-resolver.js";
25
+ export type { ReferenceHandoffRequest, QuoteHandoffRequest, StructuredHandoffRequest, NoHandoffRequest, HandoffRequest, StructuredValueValidator, HandoffResolverOptions, HandoffResolutionErrorCode, } from "./handoff-resolver.js";
26
+ export { MalformedProviderResultError, decodeDeclaredOutputs, } from "./execution-provider.js";
27
+ export type { ProviderLaunchSpec, ProviderParseContext, ParsedProviderResult, ExecutionProvider, WorkerExecution, } from "./execution-provider.js";
28
+ export { createClaudeExecutionProvider } from "./providers/claude.js";
29
+ export type { ClaudeExecutionProviderOptions, } from "./providers/claude.js";
30
+ export { createCodexExecutionProvider } from "./providers/codex.js";
31
+ export type { CodexExecutionProviderOptions, } from "./providers/codex.js";
32
+ export { runProcessTree, runProcessTreeWithTimeout, } from "./process-tree.js";
33
+ export type { ProcessTerminationReason, ProcessTreeRunOptions, ProcessTreeRunResult, } from "./process-tree.js";
34
+ export { executeWorkerAttempt } from "./worker-executor.js";
35
+ export type { WorkerExecutorOptions, WorkerExecutionWithEvents, } from "./worker-executor.js";
36
+ export { RunControllerError, RunController, } from "./run-controller.js";
37
+ export type { ControllerReferenceHandoff, ControllerQuoteHandoff, ControllerStructuredHandoff, ControllerNoHandoff, ControllerHandoffDeclaration, MaterializeStepPacketInput, ControllerStepDefinition, ReconciledAttemptState, AttemptReconciler, RunControllerOptions, CreateControllerRunInput, ClaimedStep, RunControllerErrorCode, } from "./run-controller.js";
38
+ export { WorkflowPlanAdapterError, adaptWorkflowPlan, } from "./workflow-plan-adapter.js";
39
+ export type { WorkflowPlanAdapterErrorCode, WorkflowPlanAdapterOptions, } from "./workflow-plan-adapter.js";
40
+ export { WorkspaceIsolationError, planAttemptWorkspace, allocateAttemptWorkspace, inspectAttemptWorkspace, applyWorkspaceLifecycleDecision, defaultWorktreeRoot, describeWorkspace, } from "./workspace-isolation.js";
41
+ export type { WorkspaceIsolation, WorkspaceIsolationErrorCode, AttemptWorkspaceRequest, SharedWorkspacePlan, IsolatedWorkspacePlan, AttemptWorkspacePlan, SharedWorkspaceAllocation, IsolatedWorkspaceAllocation, AttemptWorkspaceAllocation, WorkspaceInspection, WorkspacePreservationReason, WorkspaceLifecycleDecision, WorkspaceLifecycleResult, } from "./workspace-isolation.js";
@@ -7,3 +7,16 @@ export { CODEX_ADAPTER_MINIMUM_VERSION, CODEX_NATIVE_HOOK_EVENTS, appendCodexRun
7
7
  export { codexSyncDispatcherCommand, createCodexSyncTargetPlan, executeCodexSyncTargetPlan, formatCodexSyncTargetPlanSummary, removeCodexSyncTargetArtifacts, } from "./codex-sync-target.js";
8
8
  export { createClaudeCoreSyncTargetPlan, executeClaudeCoreSyncTargetPlan, formatClaudeCoreSyncTargetPlanDryRun, removeLegacyBashHooks, } from "./claude-sync-target.js";
9
9
  export { formatSyncTargetPlanDiagnostics, trackedOutputsForSyncTargetPlan, } from "./sync-target-plan.js";
10
+ // Foundation Runtime canonical execution surface.
11
+ export { TASK_STATE_VALUES, TASK_STATE_TRANSITIONS, RETRY_BACKOFF_VALUES as RUNTIME_RETRY_BACKOFF_VALUES, TERMINAL_OUTCOME_KIND_VALUES, RUNTIME_EVENT_TYPE_VALUES, } from "./run-contracts.js";
12
+ export { RUN_STORE_SCHEMA_VERSION, RUN_STATUS_VALUES, RunStoreError, DurableRunStore, } from "./run-store.js";
13
+ export { ResultStoreError, ImmutableResultStore, } from "./result-store.js";
14
+ export { HandoffResolutionError, HandoffResolver, } from "./handoff-resolver.js";
15
+ export { MalformedProviderResultError, decodeDeclaredOutputs, } from "./execution-provider.js";
16
+ export { createClaudeExecutionProvider } from "./providers/claude.js";
17
+ export { createCodexExecutionProvider } from "./providers/codex.js";
18
+ export { runProcessTree, runProcessTreeWithTimeout, } from "./process-tree.js";
19
+ export { executeWorkerAttempt } from "./worker-executor.js";
20
+ export { RunControllerError, RunController, } from "./run-controller.js";
21
+ export { WorkflowPlanAdapterError, adaptWorkflowPlan, } from "./workflow-plan-adapter.js";
22
+ export { WorkspaceIsolationError, planAttemptWorkspace, allocateAttemptWorkspace, inspectAttemptWorkspace, applyWorkspaceLifecycleDecision, defaultWorktreeRoot, describeWorkspace, } from "./workspace-isolation.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Bounded adaptation of oh-my-codex v0.20.3 process-tree behavior.
3
+ *
4
+ * The local delta adds external cancellation, distinct termination reasons,
5
+ * explicit cancellation grace, stdin, and observation callbacks. Process-group
6
+ * ownership, timeout escalation, parent-exit cleanup, output/process limits, and
7
+ * residual descendant cleanup remain centralized here.
8
+ */
9
+ import { spawn } from "node:child_process";
10
+ export type ProcessTerminationReason = "timeout" | "cancellation" | "process_limit" | "output_limit";
11
+ export interface ProcessTreeRunOptions {
12
+ readonly cwd?: string;
13
+ readonly env?: NodeJS.ProcessEnv;
14
+ readonly stdin?: string;
15
+ readonly encoding?: BufferEncoding;
16
+ readonly timeoutMs?: number;
17
+ readonly cancellationSignal?: AbortSignal;
18
+ readonly cancellationReason?: string | null;
19
+ readonly killSignal?: NodeJS.Signals;
20
+ readonly sigkillGraceMs?: number;
21
+ readonly cancellationGraceMs?: number;
22
+ readonly maxOutputBytes?: number;
23
+ readonly maxProcessCount?: number;
24
+ readonly processLimitPollMs?: number;
25
+ readonly cleanupOnParentExit?: boolean;
26
+ readonly platform?: NodeJS.Platform;
27
+ readonly spawnImpl?: typeof spawn;
28
+ readonly onProcessStart?: (processId: number) => void;
29
+ readonly onStdout?: (text: string) => void;
30
+ readonly onStderr?: (text: string) => void;
31
+ readonly onTerminationRequested?: (reason: ProcessTerminationReason) => void;
32
+ }
33
+ export interface ProcessTreeRunResult {
34
+ readonly stdout: string;
35
+ readonly stderr: string;
36
+ readonly status: number | null;
37
+ readonly signal: NodeJS.Signals | null;
38
+ readonly timedOut: boolean;
39
+ readonly cancelled: boolean;
40
+ readonly cancellationReason: string | null;
41
+ readonly processLimitExceeded: boolean;
42
+ readonly outputLimitExceeded: boolean;
43
+ readonly terminationReason: ProcessTerminationReason | null;
44
+ readonly error?: NodeJS.ErrnoException;
45
+ }
46
+ export declare function runProcessTree(command: string, args: readonly string[], options?: ProcessTreeRunOptions): Promise<ProcessTreeRunResult>;
47
+ export declare const runProcessTreeWithTimeout: typeof runProcessTree;
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Bounded adaptation of oh-my-codex v0.20.3 process-tree behavior.
3
+ *
4
+ * The local delta adds external cancellation, distinct termination reasons,
5
+ * explicit cancellation grace, stdin, and observation callbacks. Process-group
6
+ * ownership, timeout escalation, parent-exit cleanup, output/process limits, and
7
+ * residual descendant cleanup remain centralized here.
8
+ */
9
+ import { spawn, spawnSync, } from "node:child_process";
10
+ import { readdirSync, readFileSync, statSync } from "node:fs";
11
+ import { basename, delimiter, extname, join, } from "node:path";
12
+ const DEFAULT_TERMINATION_GRACE_MS = 1_000;
13
+ const DEFAULT_PROCESS_LIMIT_POLL_MS = 100;
14
+ const WINDOWS_DIRECT_EXTENSIONS = new Set([".com", ".exe"]);
15
+ const WINDOWS_CMD_EXTENSIONS = new Set([".bat", ".cmd"]);
16
+ const WINDOWS_DEFAULT_PATHEXT = [".exe", ".com", ".cmd", ".bat", ".ps1"];
17
+ function positiveInteger(value) {
18
+ if (value === undefined || !Number.isFinite(value) || value <= 0)
19
+ return undefined;
20
+ return Math.floor(value);
21
+ }
22
+ function cancellationReason(signal, configured) {
23
+ if (configured !== undefined && configured !== null)
24
+ return configured;
25
+ const reason = signal?.reason;
26
+ if (typeof reason === "string")
27
+ return reason;
28
+ if (reason instanceof Error)
29
+ return reason.message;
30
+ return reason === undefined || reason === null ? null : String(reason);
31
+ }
32
+ function nonNegativeInteger(value) {
33
+ if (value === undefined || !Number.isFinite(value) || value < 0)
34
+ return undefined;
35
+ return Math.floor(value);
36
+ }
37
+ function fileExists(path) {
38
+ try {
39
+ return statSync(path).isFile();
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function resolveWindowsCommand(command, env) {
46
+ const hasPath = /^[A-Za-z]:/.test(command) || /[\\/]/.test(command);
47
+ const rawExtensions = String(env.PATHEXT ?? "").trim();
48
+ const extensions = rawExtensions === ""
49
+ ? WINDOWS_DEFAULT_PATHEXT
50
+ : [...new Set([
51
+ ...WINDOWS_DEFAULT_PATHEXT,
52
+ ...rawExtensions.split(";").map((value) => value.trim().toLowerCase()).filter(Boolean),
53
+ ])];
54
+ const directories = hasPath
55
+ ? [""]
56
+ : String(env.Path ?? env.PATH ?? "").split(delimiter).filter(Boolean);
57
+ const extension = extname(command);
58
+ for (const directory of directories) {
59
+ const base = directory === "" ? command : join(directory, command);
60
+ const candidates = extension ? [base] : [...extensions.map((item) => `${base}${item}`), base];
61
+ for (const candidate of candidates) {
62
+ if (fileExists(candidate))
63
+ return candidate;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+ function commandSpec(command, args, platform, env) {
69
+ if (platform !== "win32")
70
+ return { command, args: [...args] };
71
+ const resolved = resolveWindowsCommand(command, env);
72
+ if (!resolved)
73
+ return { command, args: [...args] };
74
+ const extension = extname(resolved).toLowerCase();
75
+ if (WINDOWS_CMD_EXTENSIONS.has(extension)) {
76
+ const quote = (value) => `"${value.replace(/"/g, "\"\"")}"`;
77
+ const commandLine = [resolved, ...args].map(quote).join(" ");
78
+ return {
79
+ command: env.ComSpec || "cmd.exe",
80
+ args: ["/d", "/s", "/c", `"${commandLine}"`],
81
+ windowsVerbatimArguments: true,
82
+ };
83
+ }
84
+ if (extension === ".ps1") {
85
+ return {
86
+ command: "powershell.exe",
87
+ args: [
88
+ "-NoLogo",
89
+ "-NoProfile",
90
+ "-ExecutionPolicy",
91
+ "Bypass",
92
+ "-File",
93
+ resolved,
94
+ ...args,
95
+ ],
96
+ };
97
+ }
98
+ if (WINDOWS_DIRECT_EXTENSIONS.has(extension) || basename(resolved) !== "") {
99
+ return { command: resolved, args: [...args] };
100
+ }
101
+ return { command, args: [...args] };
102
+ }
103
+ function terminateProcessTree(child, platform, signal) {
104
+ if (child.pid === undefined)
105
+ return;
106
+ if (platform === "win32") {
107
+ const force = signal === "SIGKILL" ? ["/F"] : [];
108
+ const result = spawnSync("taskkill", ["/PID", String(child.pid), "/T", ...force], { windowsHide: true, stdio: "ignore" });
109
+ if (!result.error && result.status === 0)
110
+ return;
111
+ }
112
+ try {
113
+ if (platform === "win32") {
114
+ child.kill(signal);
115
+ }
116
+ else {
117
+ process.kill(-child.pid, signal);
118
+ }
119
+ }
120
+ catch (error) {
121
+ const code = error.code;
122
+ if (code === "ESRCH")
123
+ return;
124
+ try {
125
+ child.kill(signal);
126
+ }
127
+ catch (fallbackError) {
128
+ if (fallbackError.code !== "ESRCH") {
129
+ throw fallbackError;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ function readLinuxProcessTable() {
135
+ try {
136
+ const table = new Map();
137
+ for (const entry of readdirSync("/proc", { withFileTypes: true })) {
138
+ if (!entry.isDirectory() || !/^\d+$/.test(entry.name))
139
+ continue;
140
+ let stat;
141
+ try {
142
+ stat = readFileSync(`/proc/${entry.name}/stat`, "utf-8");
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ const closeParen = stat.lastIndexOf(")");
148
+ if (closeParen < 0)
149
+ continue;
150
+ const fields = stat.slice(closeParen + 2).split(" ");
151
+ const pid = Number.parseInt(entry.name, 10);
152
+ const parentPid = Number.parseInt(fields[1] ?? "", 10);
153
+ if (Number.isFinite(parentPid))
154
+ table.set(pid, parentPid);
155
+ }
156
+ return table;
157
+ }
158
+ catch {
159
+ return undefined;
160
+ }
161
+ }
162
+ function countLinuxDescendants(rootPid) {
163
+ const table = readLinuxProcessTable();
164
+ if (!table)
165
+ return undefined;
166
+ const children = new Map();
167
+ for (const [pid, parentPid] of table) {
168
+ const values = children.get(parentPid) ?? [];
169
+ values.push(pid);
170
+ children.set(parentPid, values);
171
+ }
172
+ let count = 0;
173
+ const pending = [...(children.get(rootPid) ?? [])];
174
+ while (pending.length > 0) {
175
+ const pid = pending.pop();
176
+ if (pid === undefined)
177
+ continue;
178
+ count += 1;
179
+ pending.push(...(children.get(pid) ?? []));
180
+ }
181
+ return count;
182
+ }
183
+ export function runProcessTree(command, args, options = {}) {
184
+ const cancellationSignal = options.cancellationSignal;
185
+ if (cancellationSignal?.aborted) {
186
+ options.onTerminationRequested?.("cancellation");
187
+ return Promise.resolve({
188
+ stdout: "",
189
+ stderr: "",
190
+ status: null,
191
+ signal: null,
192
+ timedOut: false,
193
+ cancelled: true,
194
+ cancellationReason: cancellationReason(cancellationSignal, options.cancellationReason),
195
+ processLimitExceeded: false,
196
+ outputLimitExceeded: false,
197
+ terminationReason: "cancellation",
198
+ });
199
+ }
200
+ const platform = options.platform ?? process.platform;
201
+ const env = options.env ?? process.env;
202
+ const spec = commandSpec(command, args, platform, env);
203
+ const spawnImpl = options.spawnImpl ?? spawn;
204
+ const timeoutMs = positiveInteger(options.timeoutMs);
205
+ const killSignal = options.killSignal ?? "SIGTERM";
206
+ const timeoutGraceMs = nonNegativeInteger(options.sigkillGraceMs)
207
+ ?? DEFAULT_TERMINATION_GRACE_MS;
208
+ const cancellationGraceMs = nonNegativeInteger(options.cancellationGraceMs)
209
+ ?? DEFAULT_TERMINATION_GRACE_MS;
210
+ const processLimitPollMs = positiveInteger(options.processLimitPollMs)
211
+ ?? DEFAULT_PROCESS_LIMIT_POLL_MS;
212
+ const maxOutputBytes = positiveInteger(options.maxOutputBytes);
213
+ const maxProcessCount = positiveInteger(options.maxProcessCount);
214
+ const encoding = options.encoding ?? "utf-8";
215
+ return new Promise((resolve) => {
216
+ let stdout = "";
217
+ let stderr = "";
218
+ let timedOut = false;
219
+ let cancelled = false;
220
+ let processLimitExceeded = false;
221
+ let outputLimitExceeded = false;
222
+ let terminationReason = null;
223
+ let settled = false;
224
+ let timeoutTimer;
225
+ let forceKillTimer;
226
+ let processLimitTimer;
227
+ const spawnOptions = {
228
+ cwd: options.cwd,
229
+ env,
230
+ detached: platform !== "win32",
231
+ stdio: ["pipe", "pipe", "pipe"],
232
+ windowsHide: true,
233
+ windowsVerbatimArguments: spec.windowsVerbatimArguments,
234
+ };
235
+ let child;
236
+ try {
237
+ child = spawnImpl(spec.command, [...spec.args], spawnOptions);
238
+ }
239
+ catch (error) {
240
+ resolve({
241
+ stdout,
242
+ stderr,
243
+ status: null,
244
+ signal: null,
245
+ timedOut: false,
246
+ cancelled: false,
247
+ cancellationReason: null,
248
+ processLimitExceeded: false,
249
+ outputLimitExceeded: false,
250
+ terminationReason: null,
251
+ error: error,
252
+ });
253
+ return;
254
+ }
255
+ const requestTermination = (reason, graceMs) => {
256
+ if (settled || terminationReason !== null)
257
+ return;
258
+ terminationReason = reason;
259
+ options.onTerminationRequested?.(reason);
260
+ terminateProcessTree(child, platform, killSignal);
261
+ if (killSignal !== "SIGKILL") {
262
+ forceKillTimer = setTimeout(() => {
263
+ if (!settled)
264
+ terminateProcessTree(child, platform, "SIGKILL");
265
+ }, graceMs);
266
+ forceKillTimer.unref?.();
267
+ }
268
+ };
269
+ const parentCleanupHandler = (signal) => {
270
+ const parentSignal = typeof signal === "string"
271
+ ? signal
272
+ : killSignal;
273
+ terminateProcessTree(child, platform, parentSignal);
274
+ };
275
+ const cleanupSignals = ["SIGINT", "SIGTERM", "SIGHUP"];
276
+ if (options.cleanupOnParentExit) {
277
+ for (const signal of cleanupSignals)
278
+ process.once(signal, parentCleanupHandler);
279
+ process.once("beforeExit", parentCleanupHandler);
280
+ process.once("exit", parentCleanupHandler);
281
+ }
282
+ const removeParentHandlers = () => {
283
+ if (!options.cleanupOnParentExit)
284
+ return;
285
+ for (const signal of cleanupSignals)
286
+ process.off(signal, parentCleanupHandler);
287
+ process.off("beforeExit", parentCleanupHandler);
288
+ process.off("exit", parentCleanupHandler);
289
+ };
290
+ const cancellationHandler = () => {
291
+ if (settled || terminationReason !== null)
292
+ return;
293
+ cancelled = true;
294
+ requestTermination("cancellation", cancellationGraceMs);
295
+ };
296
+ cancellationSignal?.addEventListener("abort", cancellationHandler, { once: true });
297
+ const finish = (status, signal, error) => {
298
+ if (settled)
299
+ return;
300
+ settled = true;
301
+ if (timeoutTimer)
302
+ clearTimeout(timeoutTimer);
303
+ if (forceKillTimer)
304
+ clearTimeout(forceKillTimer);
305
+ if (processLimitTimer)
306
+ clearInterval(processLimitTimer);
307
+ cancellationSignal?.removeEventListener("abort", cancellationHandler);
308
+ removeParentHandlers();
309
+ resolve({
310
+ stdout,
311
+ stderr,
312
+ status,
313
+ signal,
314
+ timedOut,
315
+ cancelled,
316
+ cancellationReason: cancelled
317
+ ? cancellationReason(cancellationSignal, options.cancellationReason)
318
+ : null,
319
+ processLimitExceeded,
320
+ outputLimitExceeded,
321
+ terminationReason,
322
+ ...(error ? { error } : {}),
323
+ });
324
+ };
325
+ const appendOutput = (current, chunk, observer) => {
326
+ if (outputLimitExceeded)
327
+ return current;
328
+ let accepted = chunk;
329
+ if (maxOutputBytes !== undefined) {
330
+ const currentBytes = Buffer.byteLength(current, encoding);
331
+ const remaining = Math.max(0, maxOutputBytes - currentBytes);
332
+ if (Buffer.byteLength(chunk, encoding) > remaining) {
333
+ outputLimitExceeded = true;
334
+ accepted = Buffer.from(chunk, encoding)
335
+ .subarray(0, remaining)
336
+ .toString(encoding);
337
+ requestTermination("output_limit", timeoutGraceMs);
338
+ }
339
+ }
340
+ if (accepted !== "")
341
+ observer?.(accepted);
342
+ return current + accepted;
343
+ };
344
+ if (child.pid !== undefined)
345
+ options.onProcessStart?.(child.pid);
346
+ child.stdout?.setEncoding(encoding);
347
+ child.stderr?.setEncoding(encoding);
348
+ child.stdout?.on("data", (chunk) => {
349
+ stdout = appendOutput(stdout, chunk, options.onStdout);
350
+ });
351
+ child.stderr?.on("data", (chunk) => {
352
+ stderr = appendOutput(stderr, chunk, options.onStderr);
353
+ });
354
+ child.stdin?.on("error", () => {
355
+ // EPIPE is expected when a provider exits before consuming its prompt.
356
+ });
357
+ child.stdin?.end(options.stdin);
358
+ child.on("error", (error) => {
359
+ finish(null, null, error);
360
+ });
361
+ child.on("exit", () => {
362
+ if (platform === "win32")
363
+ return;
364
+ terminateProcessTree(child, platform, killSignal);
365
+ const residualGraceMs = terminationReason === "cancellation"
366
+ ? cancellationGraceMs
367
+ : timeoutGraceMs;
368
+ const residualTimer = setTimeout(() => {
369
+ terminateProcessTree(child, platform, "SIGKILL");
370
+ }, residualGraceMs);
371
+ residualTimer.unref?.();
372
+ });
373
+ child.on("close", (status, signal) => {
374
+ finish(status, signal);
375
+ });
376
+ if (cancellationSignal?.aborted)
377
+ cancellationHandler();
378
+ if (timeoutMs !== undefined) {
379
+ timeoutTimer = setTimeout(() => {
380
+ if (settled || terminationReason !== null)
381
+ return;
382
+ timedOut = true;
383
+ requestTermination("timeout", timeoutGraceMs);
384
+ }, timeoutMs);
385
+ timeoutTimer.unref?.();
386
+ }
387
+ if (platform === "linux" && maxProcessCount !== undefined) {
388
+ processLimitTimer = setInterval(() => {
389
+ if (settled || terminationReason !== null || child.pid === undefined)
390
+ return;
391
+ const descendantCount = countLinuxDescendants(child.pid);
392
+ if (descendantCount !== undefined
393
+ && descendantCount + 1 > maxProcessCount) {
394
+ processLimitExceeded = true;
395
+ requestTermination("process_limit", timeoutGraceMs);
396
+ }
397
+ }, processLimitPollMs);
398
+ processLimitTimer.unref?.();
399
+ }
400
+ });
401
+ }
402
+ export const runProcessTreeWithTimeout = runProcessTree;
@@ -0,0 +1,9 @@
1
+ import type { ExecutionProvider } from "../execution-provider.js";
2
+ import type { StepPacket } from "../run-contracts.js";
3
+ export interface ClaudeExecutionProviderOptions {
4
+ readonly executable?: string;
5
+ readonly agentName?: (packet: StepPacket) => string;
6
+ readonly extraArgs?: readonly string[];
7
+ readonly env?: NodeJS.ProcessEnv;
8
+ }
9
+ export declare function createClaudeExecutionProvider(options?: ClaudeExecutionProviderOptions): ExecutionProvider;