@margay/ccl-core 1.5.0-alpha.0 → 1.5.1-alpha.2

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 (94) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +95197 -74935
  3. package/dist/sdk/background.d.ts +48 -0
  4. package/dist/sdk/background.js +87 -0
  5. package/dist/sdk/backgroundRunTypes.d.ts +101 -0
  6. package/dist/sdk/backgroundRunTypes.js +1 -0
  7. package/dist/sdk/commandAdapter.d.ts +77 -0
  8. package/dist/sdk/commandAdapter.js +180 -0
  9. package/dist/sdk/commandProcess.d.ts +27 -0
  10. package/dist/sdk/commandProcess.js +130 -0
  11. package/dist/sdk/executionGuidance.d.ts +7 -0
  12. package/dist/sdk/executionGuidance.js +37 -0
  13. package/dist/sdk/index.d.ts +16 -1
  14. package/dist/sdk/index.js +10 -0
  15. package/dist/sdk/inputProvenance.d.ts +13 -0
  16. package/dist/sdk/inputProvenance.js +26 -0
  17. package/dist/sdk/mcpServer.d.ts +9 -5
  18. package/dist/sdk/mcpServer.js +51 -9
  19. package/dist/sdk/operations/command.d.ts +24 -0
  20. package/dist/sdk/operations/command.js +48 -0
  21. package/dist/sdk/operations/host.d.ts +3 -0
  22. package/dist/sdk/operations/host.js +376 -0
  23. package/dist/sdk/operations/json.d.ts +6 -0
  24. package/dist/sdk/operations/json.js +24 -0
  25. package/dist/sdk/operations/reference.d.ts +15 -0
  26. package/dist/sdk/operations/reference.js +19 -0
  27. package/dist/sdk/operations/store.d.ts +26 -0
  28. package/dist/sdk/operations/store.js +300 -0
  29. package/dist/sdk/operations/types.d.ts +171 -0
  30. package/dist/sdk/operations/types.js +8 -0
  31. package/dist/sdk/pendingApprovalIdentity.d.ts +35 -0
  32. package/dist/sdk/pendingApprovalIdentity.js +27 -0
  33. package/dist/sdk/permissionDelegation.d.ts +19 -0
  34. package/dist/sdk/permissionDelegation.js +11 -0
  35. package/dist/sdk/query.d.ts +4 -2
  36. package/dist/sdk/query.js +236 -56
  37. package/dist/sdk/queryInput.js +7 -0
  38. package/dist/sdk/queryLifecycle.d.ts +19 -0
  39. package/dist/sdk/queryLifecycle.js +93 -0
  40. package/dist/sdk/queryLifecycleTypes.d.ts +160 -0
  41. package/dist/sdk/queryLifecycleTypes.js +47 -0
  42. package/dist/sdk/runtimeResources.d.ts +197 -0
  43. package/dist/sdk/runtimeResources.js +36 -0
  44. package/dist/sdk/scheduledOccurrenceTypes.d.ts +103 -0
  45. package/dist/sdk/scheduledOccurrenceTypes.js +12 -0
  46. package/dist/sdk/session.d.ts +4 -3
  47. package/dist/sdk/session.js +19 -14
  48. package/dist/sdk/types.d.ts +21 -2
  49. package/docs/README.md +5 -0
  50. package/docs/background-runs.md +229 -0
  51. package/docs/ccl-duo-agent.md +1 -4
  52. package/docs/ccl-ide-usage.md +7 -70
  53. package/docs/cli-installation.md +37 -0
  54. package/docs/command-tool-recovery.md +62 -0
  55. package/docs/dev-quickstart.md +29 -7
  56. package/docs/error-recovery-usage.md +30 -2
  57. package/docs/features/business-facts.md +2 -4
  58. package/docs/hook-input-provenance.md +120 -0
  59. package/docs/index.md +30 -96
  60. package/docs/local-analytics-quickstart.md +11 -28
  61. package/docs/mcp-tool-recovery.md +82 -0
  62. package/docs/operations-client-integration.md +1 -7
  63. package/docs/peer-development-review.md +0 -1
  64. package/docs/releases/1.5.0-alpha.3.md +17 -0
  65. package/docs/releases/1.5.0-alpha.4.md +16 -0
  66. package/docs/releases/1.5.0-alpha.5.md +26 -0
  67. package/docs/releases/1.5.0-alpha.6.md +15 -0
  68. package/docs/releases/1.5.1-alpha.1.md +11 -0
  69. package/docs/releases/1.5.1-alpha.2.md +17 -0
  70. package/docs/releases/1.5.1-alpha.md +22 -0
  71. package/docs/runtime-resources.md +137 -0
  72. package/docs/self-hosted-product-events.md +6 -35
  73. package/docs/shared-skill-resources.md +50 -0
  74. package/examples/background-runs/agents/background-reviewer.md +6 -0
  75. package/examples/background-runs/agents/background-worker.md +6 -0
  76. package/examples/background-runs/worker-reviewer.mjs +42 -0
  77. package/examples/command-recovery/durable-server.mjs +44 -0
  78. package/examples/command-recovery/receipt-command.mjs +40 -0
  79. package/examples/runtime-resources/catalog.ts +68 -0
  80. package/package.json +25 -14
  81. package/sdk/API.md +236 -1
  82. package/sdk/README.md +6 -0
  83. package/docs/agent-owned-error-recovery-ux-2026-10-04.md +0 -146
  84. package/docs/analytics-capabilities-2026-10-08.md +0 -223
  85. package/docs/releases/1.4.5-rc.1.md +0 -39
  86. package/docs/releases/1.4.5-rc.10.md +0 -50
  87. package/docs/releases/1.4.5-rc.2.md +0 -26
  88. package/docs/releases/1.4.5-rc.3.md +0 -9
  89. package/docs/releases/1.4.5-rc.6.md +0 -11
  90. package/docs/releases/1.4.5-rc.7.md +0 -14
  91. package/docs/releases/1.4.5-rc.8.md +0 -18
  92. package/docs/releases/1.4.5-rc.9.md +0 -22
  93. package/docs/releases/1.4.6-beta.0.md +0 -54
  94. package/docs/releases/1.5.0-alpha.0.md +0 -44
@@ -0,0 +1,48 @@
1
+ import type { BackgroundRun, BackgroundRunAction, BackgroundRunApproval, BackgroundRunEvents, BackgroundRunLookup, BackgroundRunSelector, BackgroundRunSpec, BackgroundRunSource } from './backgroundRunTypes.js';
2
+ import type { OccurrenceCancelRequest, OccurrenceApprovalRequest, OccurrenceApprovalReceipt, OccurrenceLookup, OccurrenceEvents, ScheduledOccurrence } from './scheduledOccurrenceTypes.js';
3
+ export type BackgroundClientOptions = {
4
+ cwd: string;
5
+ cliPath: string;
6
+ execPath?: string;
7
+ launch?: {
8
+ model?: string;
9
+ settings?: string;
10
+ extraArgs?: string[];
11
+ };
12
+ env?: NodeJS.ProcessEnv;
13
+ timeoutMs?: number;
14
+ };
15
+ export declare class BackgroundClientError extends Error {
16
+ readonly code: string;
17
+ constructor(code: string, message: string);
18
+ }
19
+ /** Durable runs do not retain JS callbacks or in-process SDK MCP servers. Use
20
+ * persistent stdio/HTTP tools configured through launch instead. */
21
+ export declare function createBackgroundClient(options: BackgroundClientOptions): {
22
+ getOccurrence: (request: {
23
+ occurrenceId: string;
24
+ }) => Promise<OccurrenceLookup>;
25
+ listOccurrences: (request?: {
26
+ taskId?: string;
27
+ sourceSessionId?: string;
28
+ }) => Promise<ScheduledOccurrence[]>;
29
+ readOccurrenceEvents: (request: {
30
+ occurrenceId: string;
31
+ afterSequence?: number;
32
+ limit?: number;
33
+ }) => Promise<OccurrenceEvents>;
34
+ cancelOccurrence: (request: OccurrenceCancelRequest) => Promise<ScheduledOccurrence>;
35
+ approveOccurrence: (request: OccurrenceApprovalRequest) => Promise<OccurrenceApprovalReceipt>;
36
+ submitRun: (request: {
37
+ requestKey: string;
38
+ spec: BackgroundRunSpec;
39
+ source?: BackgroundRunSource;
40
+ }) => Promise<BackgroundRun>;
41
+ getRun: (request: BackgroundRunSelector) => Promise<BackgroundRunLookup>;
42
+ readRunEvents: (request: BackgroundRunSelector & {
43
+ cursor?: string;
44
+ limit?: number;
45
+ }) => Promise<BackgroundRunEvents>;
46
+ cancelRun: (request: BackgroundRunAction) => Promise<BackgroundRun>;
47
+ approveRun: (request: BackgroundRunApproval) => Promise<BackgroundRun>;
48
+ };
@@ -0,0 +1,87 @@
1
+ import { spawn } from 'node:child_process';
2
+ export class BackgroundClientError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = 'BackgroundClientError';
8
+ }
9
+ }
10
+ /** Durable runs do not retain JS callbacks or in-process SDK MCP servers. Use
11
+ * persistent stdio/HTTP tools configured through launch instead. */
12
+ export function createBackgroundClient(options) {
13
+ for (const key of Object.keys(options)) {
14
+ if (!['cwd', 'cliPath', 'execPath', 'launch', 'env', 'timeoutMs'].includes(key))
15
+ throw new BackgroundClientError('unsupported_option', `Background execution cannot preserve option ${key}; use durable service configuration`);
16
+ }
17
+ async function invoke(command, request) {
18
+ // Serialization can throw (for example for a cyclic input); do it before
19
+ // allocating a subprocess that would otherwise wait forever for stdin.
20
+ const input = JSON.stringify(request);
21
+ const retryAdvice = command.includes('occurrence') ? 'Read the original occurrence and approvalDecisions. Retry approval only with the same actionId and payload.' : 'Query the original requestKey; do not submit a new key.';
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(options.execPath ?? process.execPath, [options.cliPath, 'daemon', command, '--cwd', options.cwd, '--request-stdin'], {
24
+ cwd: options.cwd, env: options.env ?? process.env, stdio: ['pipe', 'pipe', 'pipe'],
25
+ });
26
+ // A UTF-8 character may straddle pipe chunks. Decode incrementally so
27
+ // saved event text is not silently replaced with U+FFFD on readback.
28
+ child.stdout.setEncoding('utf8');
29
+ child.stderr.setEncoding('utf8');
30
+ let stdout = '', stderr = '', settled = false;
31
+ let forceStop;
32
+ const finish = (error, value) => {
33
+ if (settled)
34
+ return;
35
+ settled = true;
36
+ clearTimeout(timer);
37
+ if (error && child.exitCode === null && child.signalCode === null) {
38
+ child.kill('SIGTERM');
39
+ forceStop = setTimeout(() => child.kill('SIGKILL'), 1000);
40
+ forceStop.unref();
41
+ }
42
+ if (error)
43
+ reject(error);
44
+ else
45
+ resolve(value);
46
+ };
47
+ const timer = setTimeout(() => {
48
+ child.kill('SIGTERM');
49
+ finish(new BackgroundClientError('ack_unknown', `Background acknowledgement timed out. ${retryAdvice}`));
50
+ }, options.timeoutMs ?? 45_000);
51
+ child.on('error', error => finish(error));
52
+ child.stdin.on('error', error => finish(error));
53
+ child.stdout.on('data', chunk => { stdout += chunk; if (Buffer.byteLength(stdout) > 32 * 1024 * 1024) {
54
+ child.kill('SIGTERM');
55
+ finish(new BackgroundClientError('response_too_large', `Response exceeds 32 MiB. ${command === 'events' ? 'Use limit: 1; if one complete event still exceeds 32 MiB, ' : ''}read the original result using the CLI daemon ${command === 'events' ? 'events' : 'get'} command redirected to a task-owned file. Do not resubmit with a new key.`));
56
+ } });
57
+ child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-4096); });
58
+ child.on('close', () => {
59
+ if (forceStop)
60
+ clearTimeout(forceStop);
61
+ try {
62
+ const reply = JSON.parse(stdout);
63
+ if (reply.ok !== true)
64
+ finish(new BackgroundClientError(reply.code ?? 'request_failed', reply.error ?? 'Background request failed'));
65
+ else
66
+ finish(undefined, reply.result);
67
+ }
68
+ catch {
69
+ finish(new BackgroundClientError('invalid_response', `No complete background response. ${retryAdvice} ${stderr}`));
70
+ }
71
+ });
72
+ child.stdin.end(input);
73
+ });
74
+ }
75
+ return {
76
+ getOccurrence: (request) => invoke('get-occurrence', request),
77
+ listOccurrences: (request = {}) => invoke('list-occurrences', request),
78
+ readOccurrenceEvents: (request) => invoke('occurrence-events', request),
79
+ cancelOccurrence: (request) => invoke('cancel-occurrence', request),
80
+ approveOccurrence: (request) => invoke('approve-occurrence', request),
81
+ submitRun: (request) => invoke('submit', { ...request, launch: options.launch }),
82
+ getRun: (request) => invoke('get', request),
83
+ readRunEvents: (request) => invoke('events', request),
84
+ cancelRun: (request) => invoke('cancel-run', request),
85
+ approveRun: (request) => invoke('approve-run', request),
86
+ };
87
+ }
@@ -0,0 +1,101 @@
1
+ /** Local daemon submissions. These records describe execution, not business acceptance. */
2
+ export type BackgroundRunSpec = {
3
+ prompt: string;
4
+ systemPrompt?: string;
5
+ model?: string;
6
+ maxTurns?: number;
7
+ agent?: string;
8
+ /** Optional restriction; omission preserves the existing default permission mode. */
9
+ permissionMode?: 'plan';
10
+ };
11
+ export type BackgroundRun = {
12
+ runId: string;
13
+ requestKey: string;
14
+ scope: string;
15
+ spec: BackgroundRunSpec;
16
+ specHash: string;
17
+ configId: string;
18
+ source?: BackgroundRunSource;
19
+ launchRecovery?: BackgroundLaunchRecovery;
20
+ dispatchBlockedReason?: 'context_unavailable';
21
+ acceptedAt: number;
22
+ updatedAt: number;
23
+ state: 'queued' | 'starting' | 'running' | 'waiting_permission' | 'succeeded' | 'completed_with_issues' | 'failed' | 'interrupted' | 'cancelled';
24
+ sessionId: string;
25
+ generation: string;
26
+ revision: number;
27
+ cancellation: 'none' | 'requested';
28
+ permissions: BackgroundPermission[];
29
+ operationReferences: import('./operations/reference.js').OperationReference[];
30
+ result?: string;
31
+ error?: string;
32
+ toolErrors: unknown[];
33
+ deniedToolUseIds: string[];
34
+ /** Latest captured request diagnostic, not the current run state or proof that
35
+ * earlier tools did not execute. Inspect state/events/operationReferences too. */
36
+ latestDiagnostic?: import('./errorDiagnostics.js').RecoveryDiagnostic;
37
+ };
38
+ /** Source descriptors only. Credential values never belong in durable facts. */
39
+ export type BackgroundLaunchRecovery = {
40
+ version: 1;
41
+ launchDigest: string;
42
+ configurationFiles: string[];
43
+ credentialEnvKeys: string[];
44
+ inlineCredentialPaths: string[];
45
+ };
46
+ export type BackgroundRunSource = {
47
+ sessionId: string;
48
+ assistantId?: string;
49
+ toolUseId?: string;
50
+ parentRunId?: string;
51
+ };
52
+ export type BackgroundPermission = {
53
+ toolUseId: string;
54
+ toolName: string;
55
+ input: unknown;
56
+ assistantId: string;
57
+ decision?: 'allow' | 'deny';
58
+ };
59
+ export type BackgroundRunEvent = {
60
+ runId: string;
61
+ generation: string;
62
+ sequence: number;
63
+ at: number;
64
+ kind: string;
65
+ snapshot: BackgroundRun;
66
+ data?: unknown;
67
+ };
68
+ export type BackgroundRunEvents = {
69
+ events: BackgroundRunEvent[];
70
+ nextCursor: string;
71
+ hasMore: boolean;
72
+ };
73
+ export type BackgroundRunAction = {
74
+ runId: string;
75
+ expectedRevision: number;
76
+ actionId: string;
77
+ };
78
+ export type BackgroundRunApproval = BackgroundRunAction & {
79
+ toolUseId: string;
80
+ decision: 'allow' | 'deny';
81
+ };
82
+ export type BackgroundRunLookup = {
83
+ status: 'not_found';
84
+ ownerAvailable: boolean;
85
+ } | {
86
+ status: 'found';
87
+ snapshot: BackgroundRun;
88
+ observation: {
89
+ checkedAt: number;
90
+ ownerAvailable: boolean;
91
+ executorLock: 'held' | 'free' | 'unknown';
92
+ externalLiveness: 'unknown';
93
+ };
94
+ allowedActions: Array<'get' | 'events' | 'cancel' | 'approve'>;
95
+ dispatchBlockedReason?: 'configuration_changed' | 'owner_unavailable' | 'context_unavailable';
96
+ };
97
+ export type BackgroundRunSelector = {
98
+ requestKey: string;
99
+ } | {
100
+ runId: string;
101
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { type CommandProcessResult } from './commandProcess.js';
2
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type CommandOperation = Readonly<{
6
+ operationId: string;
7
+ requestKey: string;
8
+ adapterId: string;
9
+ adapterVersion: string;
10
+ targetIdentity: string;
11
+ bindingDigest: string;
12
+ inputDigest: string;
13
+ input: JsonValue;
14
+ createdAt: number;
15
+ }>;
16
+ export type CommandOutcome = {
17
+ status: 'applied';
18
+ receipt: JsonValue;
19
+ result?: JsonValue;
20
+ } | {
21
+ status: 'in_progress';
22
+ handle: string;
23
+ } | {
24
+ status: 'unknown';
25
+ reason: string;
26
+ } | {
27
+ status: 'unsupported';
28
+ reason: string;
29
+ };
30
+ export type CommandExecution = Readonly<{
31
+ operation: CommandOperation;
32
+ attemptId: string;
33
+ process: CommandProcessResult | null;
34
+ outcome: CommandOutcome;
35
+ }>;
36
+ export type CommandDefinition = {
37
+ cmd: string;
38
+ args?: readonly string[];
39
+ cwd?: string;
40
+ env?: Record<string, string>;
41
+ timeoutMs: number;
42
+ maxOutputBytes?: number;
43
+ inheritEnv?: boolean;
44
+ };
45
+ export type CommandAdapterSpec = {
46
+ adapterId: string;
47
+ adapterVersion: string;
48
+ targetIdentity: string;
49
+ execute: CommandDefinition;
50
+ query?: CommandDefinition;
51
+ /** Trusted external service guarantee, never a model-supplied capability. */
52
+ idempotency?: {
53
+ scope: string;
54
+ retentionMs: number;
55
+ };
56
+ /** Check the real account/version/target before recovery, not merely config strings. */
57
+ verifyIdentity?: (operation: CommandOperation) => Promise<boolean>;
58
+ };
59
+ export interface CommandAdapter {
60
+ prepare(input: JsonValue, options?: {
61
+ requestKey?: string;
62
+ }): CommandOperation;
63
+ execute(operation: CommandOperation, options?: {
64
+ signal?: AbortSignal;
65
+ }): Promise<CommandExecution>;
66
+ queryOutcome(operation: CommandOperation, options?: {
67
+ signal?: AbortSignal;
68
+ }): Promise<CommandExecution>;
69
+ retry(operation: CommandOperation, options: {
70
+ signal?: AbortSignal;
71
+ authorize: (operation: CommandOperation) => Promise<boolean>;
72
+ }): Promise<CommandExecution>;
73
+ }
74
+ /** Internal host bridge. Not exported by sdk/index: only verified durable records may enter. */
75
+ export declare function restoreCommandOperation(adapter: CommandAdapter, operation: CommandOperation, previouslyDispatched: boolean): CommandOperation;
76
+ /** Live command adapter. Durable ownership and rehydration are provided separately by the host. */
77
+ export declare function createCommandAdapter(spec: CommandAdapterSpec): CommandAdapter;
@@ -0,0 +1,180 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { runCommandProcess } from './commandProcess.js';
3
+ function canonical(value) {
4
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
5
+ return JSON.stringify(value);
6
+ if (typeof value === 'number' && Number.isFinite(value))
7
+ return JSON.stringify(value);
8
+ if (Array.isArray(value))
9
+ return `[${value.map(canonical).join(',')}]`;
10
+ if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
11
+ return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
12
+ }
13
+ throw new Error('Expected a finite JSON value');
14
+ }
15
+ function freeze(value) {
16
+ if (value && typeof value === 'object') {
17
+ for (const v of Object.values(value))
18
+ freeze(v);
19
+ Object.freeze(value);
20
+ }
21
+ return value;
22
+ }
23
+ function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
24
+ const restoreHandlers = new WeakMap();
25
+ /** Internal host bridge. Not exported by sdk/index: only verified durable records may enter. */
26
+ export function restoreCommandOperation(adapter, operation, previouslyDispatched) {
27
+ const restore = restoreHandlers.get(adapter);
28
+ if (!restore)
29
+ throw new Error('Unknown command adapter instance');
30
+ return restore(operation, previouslyDispatched);
31
+ }
32
+ /** Live command adapter. Durable ownership and rehydration are provided separately by the host. */
33
+ export function createCommandAdapter(spec) {
34
+ for (const s of [spec.adapterId, spec.adapterVersion, spec.targetIdentity]) {
35
+ if (!s)
36
+ throw new Error('Adapter identity must be nonempty');
37
+ }
38
+ const verifyIdentity = spec.verifyIdentity;
39
+ const snapshot = (command) => {
40
+ if (!command.cmd || !Number.isFinite(command.timeoutMs) || command.timeoutMs <= 0 || command.timeoutMs > 2_147_483_647)
41
+ throw new Error('Invalid command configuration');
42
+ if (command.maxOutputBytes !== undefined && (!Number.isSafeInteger(command.maxOutputBytes) || command.maxOutputBytes < 1))
43
+ throw new Error('Invalid output limit');
44
+ return freeze({ cmd: command.cmd, args: [...(command.args ?? [])], cwd: command.cwd ?? process.cwd(),
45
+ env: { ...command.env }, inheritEnv: command.inheritEnv !== false, timeoutMs: command.timeoutMs, maxOutputBytes: command.maxOutputBytes ?? 8 * 1024 * 1024 });
46
+ };
47
+ const execute = snapshot(spec.execute), query = spec.query ? snapshot(spec.query) : undefined;
48
+ const idempotency = spec.idempotency ? freeze({ ...spec.idempotency }) : undefined;
49
+ if (idempotency && (!idempotency.scope || !Number.isFinite(idempotency.retentionMs) || idempotency.retentionMs <= 0))
50
+ throw new Error('Invalid idempotency contract');
51
+ const identity = { adapterId: spec.adapterId, adapterVersion: spec.adapterVersion, targetIdentity: spec.targetIdentity };
52
+ const binding = () => digest({ ...identity, execute: { ...execute, env: execute.inheritEnv ? { ...process.env, ...execute.env } : execute.env },
53
+ query: query ? { ...query, env: query.inheritEnv ? { ...process.env, ...query.env } : query.env } : null, idempotency: idempotency ?? null });
54
+ const bindingDigest = binding();
55
+ const states = new WeakMap();
56
+ const keys = new Map();
57
+ function stateFor(operation) {
58
+ const state = states.get(operation);
59
+ if (!state)
60
+ throw new Error('Operation is not owned by this adapter instance');
61
+ if (binding() !== state.bindingDigest)
62
+ throw new Error('Command environment binding changed; verify identity before recovery');
63
+ return state;
64
+ }
65
+ function result(operation, outcome, process = null, attemptId = randomUUID()) {
66
+ return freeze({ operation, attemptId, process, outcome });
67
+ }
68
+ async function run(operation, command, signal) {
69
+ const state = stateFor(operation);
70
+ const attemptId = randomUUID();
71
+ const envelope = { schemaVersion: 1, ...operation, attemptId };
72
+ const processResult = await runCommandProcess({ ...command, input: JSON.stringify(envelope), signal });
73
+ let outcome = { status: 'unknown', reason: 'Command did not return a valid bound receipt' };
74
+ if (processResult.exit_code === 0 && !processResult.error && !processResult.cancelled && !processResult.timed_out) {
75
+ try {
76
+ const reply = JSON.parse(processResult.output);
77
+ const matches = reply.schemaVersion === 1 && ['operationId', 'requestKey', 'adapterId', 'adapterVersion', 'targetIdentity', 'inputDigest', 'bindingDigest'].every(k => reply[k] === envelope[k]);
78
+ if (matches) {
79
+ if (reply.status === 'applied' && reply.receipt !== undefined && reply.receipt !== null) {
80
+ canonical(reply.receipt);
81
+ if (reply.result !== undefined)
82
+ canonical(reply.result);
83
+ outcome = { status: 'applied', receipt: reply.receipt, ...(reply.result !== undefined ? { result: reply.result } : {}) };
84
+ }
85
+ else if (reply.status === 'in_progress' && typeof reply.handle === 'string' && reply.handle)
86
+ outcome = { status: 'in_progress', handle: reply.handle };
87
+ else if (reply.status === 'unknown' && typeof reply.reason === 'string')
88
+ outcome = { status: 'unknown', reason: reply.reason };
89
+ }
90
+ }
91
+ catch { /* Invalid/truncated output never proves an effect. */ }
92
+ }
93
+ const execution = result(operation, outcome, processResult, attemptId);
94
+ if (outcome.status === 'applied')
95
+ state.accepted ??= execution;
96
+ return state.accepted ?? execution;
97
+ }
98
+ const adapter = {
99
+ prepare(input, options = {}) {
100
+ const text = canonical(input), inputDigest = digest(input);
101
+ const requestKey = options.requestKey ?? randomUUID();
102
+ if (!requestKey)
103
+ throw new Error('requestKey must be nonempty');
104
+ const existing = keys.get(requestKey);
105
+ if (existing) {
106
+ if (existing.inputDigest !== inputDigest)
107
+ throw new Error('requestKey is already bound to different input');
108
+ stateFor(existing);
109
+ return existing;
110
+ }
111
+ if (binding() !== bindingDigest)
112
+ throw new Error('Command environment binding changed');
113
+ const operation = freeze({ operationId: randomUUID(), requestKey, ...identity, bindingDigest,
114
+ inputDigest, input: JSON.parse(text), createdAt: Date.now() });
115
+ states.set(operation, { active: false, bindingDigest });
116
+ keys.set(requestKey, operation);
117
+ return operation;
118
+ },
119
+ execute(operation, options = {}) {
120
+ const state = stateFor(operation);
121
+ if (state.first)
122
+ return state.first;
123
+ state.active = true;
124
+ state.first = run(operation, execute, options.signal).finally(() => { state.active = false; });
125
+ return state.first;
126
+ },
127
+ async queryOutcome(operation, options = {}) {
128
+ const state = stateFor(operation);
129
+ if (!state.first)
130
+ throw new Error('Operation has not been submitted');
131
+ if (state.accepted)
132
+ return state.accepted;
133
+ if (!query)
134
+ return result(operation, { status: 'unsupported', reason: 'No query command configured' });
135
+ if (verifyIdentity && !await verifyIdentity(operation))
136
+ return result(operation, { status: 'unknown', reason: 'External target identity could not be verified' });
137
+ return run(operation, query, options.signal);
138
+ },
139
+ async retry(operation, options) {
140
+ const state = stateFor(operation);
141
+ if (!state.first)
142
+ throw new Error('Use execute for an operation that has not been submitted');
143
+ if (state.accepted)
144
+ return state.accepted;
145
+ if (state.active)
146
+ throw new Error('Operation execution or recovery is already active');
147
+ const eligible = () => idempotency && Date.now() - operation.createdAt < idempotency.retentionMs;
148
+ if (!eligible() || !verifyIdentity)
149
+ throw new Error('Retry requires valid idempotency and external identity verification');
150
+ state.active = true;
151
+ try {
152
+ if (!await options.authorize(operation))
153
+ throw new Error('Retry permission denied');
154
+ if (!await verifyIdentity(operation))
155
+ throw new Error('External target identity changed');
156
+ stateFor(operation);
157
+ if (state.accepted)
158
+ return state.accepted;
159
+ if (!eligible())
160
+ throw new Error('Idempotency retention expired during authorization');
161
+ return await run(operation, execute, options.signal);
162
+ }
163
+ finally {
164
+ state.active = false;
165
+ }
166
+ },
167
+ };
168
+ restoreHandlers.set(adapter, (record, previouslyDispatched) => {
169
+ if (record.adapterId !== identity.adapterId || record.adapterVersion !== identity.adapterVersion || record.targetIdentity !== identity.targetIdentity || digest(record.input) !== record.inputDigest || !record.operationId || !record.requestKey || !Number.isFinite(record.createdAt))
170
+ throw new Error('Invalid restored command operation binding');
171
+ const operation = freeze(JSON.parse(canonical(record)));
172
+ const state = { active: false, bindingDigest: binding() };
173
+ if (previouslyDispatched)
174
+ state.first = Promise.resolve(result(operation, { status: 'unknown', reason: 'Original command was dispatched; recovering its durable history' }));
175
+ states.set(operation, state);
176
+ keys.set(operation.requestKey, operation);
177
+ return operation;
178
+ });
179
+ return adapter;
180
+ }
@@ -0,0 +1,27 @@
1
+ export type CommandProcessInput = {
2
+ cmd: string;
3
+ args: readonly string[];
4
+ timeoutMs: number;
5
+ input?: string;
6
+ env?: NodeJS.ProcessEnv;
7
+ inheritEnv?: boolean;
8
+ cwd?: string;
9
+ signal?: AbortSignal;
10
+ maxOutputBytes?: number;
11
+ };
12
+ export type CommandProcessResult = {
13
+ output: string;
14
+ stderr: string;
15
+ exit_code: number;
16
+ duration_ms: number;
17
+ timed_out: boolean;
18
+ cancelled: boolean;
19
+ started: boolean;
20
+ pid?: number;
21
+ error?: string;
22
+ dispatch: 'not_dispatched' | 'dispatched';
23
+ effect: 'not_dispatched' | 'unknown';
24
+ cancellation: 'not_requested' | 'requested';
25
+ };
26
+ /** Run exactly once, without a shell. Local termination never proves remote effects stopped. */
27
+ export declare function runCommandProcess(opts: CommandProcessInput): Promise<CommandProcessResult>;
@@ -0,0 +1,130 @@
1
+ import { spawn } from 'node:child_process';
2
+ /** Run exactly once, without a shell. Local termination never proves remote effects stopped. */
3
+ export function runCommandProcess(opts) {
4
+ if (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs <= 0 || opts.timeoutMs > 2_147_483_647) {
5
+ throw new Error('timeoutMs must be positive and within the timer range');
6
+ }
7
+ const limit = opts.maxOutputBytes ?? 8 * 1024 * 1024;
8
+ if (!Number.isSafeInteger(limit) || limit < 1)
9
+ throw new Error('maxOutputBytes must be a positive integer');
10
+ const start = Date.now();
11
+ return new Promise(resolve => {
12
+ const stdout = [], stderr = [];
13
+ let bytes = 0, started = false, settled = false, stopping = false;
14
+ let timedOut = false, cancelled = false, error;
15
+ let exitCode = null;
16
+ let timeout;
17
+ let escalation;
18
+ let deadline;
19
+ let drain;
20
+ let child;
21
+ const kill = (signal) => {
22
+ if (!started || !child?.pid)
23
+ return;
24
+ try {
25
+ // detached gives this invocation its own process group, never the host's group.
26
+ if (process.platform !== 'win32')
27
+ process.kill(-child.pid, signal);
28
+ else if (signal === 'SIGKILL') {
29
+ const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
30
+ killer.on('error', () => { try {
31
+ child?.kill(signal);
32
+ }
33
+ catch { } });
34
+ killer.unref();
35
+ }
36
+ else
37
+ child.kill(signal);
38
+ }
39
+ catch (e) {
40
+ if (e.code !== 'ESRCH')
41
+ error ??= 'Unable to terminate the owned process group';
42
+ }
43
+ };
44
+ const finish = () => {
45
+ if (settled)
46
+ return;
47
+ settled = true;
48
+ clearTimeout(timeout);
49
+ clearTimeout(escalation);
50
+ clearTimeout(deadline);
51
+ clearTimeout(drain);
52
+ opts.signal?.removeEventListener('abort', abort);
53
+ child?.stdin?.destroy();
54
+ child?.stdout?.destroy();
55
+ child?.stderr?.destroy();
56
+ resolve({
57
+ output: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'),
58
+ exit_code: timedOut ? 124 : cancelled ? 130 : error ? (exitCode || 1) : (exitCode ?? 1),
59
+ duration_ms: Date.now() - start, timed_out: timedOut, cancelled, started,
60
+ ...(child?.pid ? { pid: child.pid } : {}), ...(error ? { error } : {}),
61
+ dispatch: started ? 'dispatched' : 'not_dispatched', effect: started ? 'unknown' : 'not_dispatched',
62
+ cancellation: timedOut || cancelled || stopping ? 'requested' : 'not_requested',
63
+ });
64
+ };
65
+ const stop = () => {
66
+ if (stopping || settled)
67
+ return;
68
+ stopping = true;
69
+ clearTimeout(timeout);
70
+ clearTimeout(drain);
71
+ kill('SIGTERM');
72
+ // Keep escalation even if the leader closes first: its descendants may ignore TERM.
73
+ escalation = setTimeout(() => { kill('SIGKILL'); deadline = setTimeout(finish, 50); }, 250);
74
+ };
75
+ const abort = () => { cancelled = true; stop(); };
76
+ if (opts.signal?.aborted) {
77
+ cancelled = true;
78
+ finish();
79
+ return;
80
+ }
81
+ try {
82
+ child = spawn(opts.cmd, [...opts.args], {
83
+ cwd: opts.cwd, env: opts.inheritEnv === false ? { ...opts.env } : { ...process.env, ...opts.env },
84
+ stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32', windowsHide: true,
85
+ });
86
+ }
87
+ catch (e) {
88
+ error = e.message;
89
+ finish();
90
+ return;
91
+ }
92
+ child.on('spawn', () => { started = true; if (stopping)
93
+ kill('SIGTERM'); });
94
+ child.on('error', e => { error = e.message; if (started)
95
+ stop();
96
+ else
97
+ finish(); });
98
+ const collect = (target, data) => {
99
+ const keep = Math.min(data.length, Math.max(0, limit - bytes));
100
+ if (keep)
101
+ target.push(data.subarray(0, keep));
102
+ bytes += data.length;
103
+ if (bytes > limit) {
104
+ error ??= 'Command output limit exceeded; output is incomplete';
105
+ stop();
106
+ }
107
+ };
108
+ child.stdout?.on('data', data => collect(stdout, data));
109
+ child.stderr?.on('data', data => collect(stderr, data));
110
+ child.stdin?.on('error', e => { error ??= `Command stdin failed: ${e.message}`; stop(); });
111
+ child.on('exit', code => {
112
+ exitCode = code;
113
+ // A descendant may retain a pipe after the leader exits.
114
+ drain = setTimeout(() => { error ??= 'Command descendants retained output pipes'; stop(); }, 100);
115
+ });
116
+ child.on('close', code => {
117
+ exitCode = code;
118
+ if (!stopping) {
119
+ // A foreground invocation must not leave detached work in its owned group.
120
+ kill('SIGKILL');
121
+ finish();
122
+ }
123
+ });
124
+ opts.signal?.addEventListener('abort', abort, { once: true });
125
+ if (opts.signal?.aborted)
126
+ abort();
127
+ timeout = setTimeout(() => { timedOut = true; stop(); }, opts.timeoutMs);
128
+ child.stdin?.end(opts.input ?? '');
129
+ });
130
+ }
@@ -0,0 +1,7 @@
1
+ export declare const TOOL_EXECUTION_GUIDANCE = "Start from the requested outcome and the paths, tool descriptions, and usage instructions already supplied. When an available tool or CLI has enough information to perform the requested operation, call it through an authorized tool directly. Using a documented command does not itself require reading its implementation or requesting help first. Consult targeted help or source when required details are missing, an actual result needs diagnosis, or the task asks you to inspect or change the implementation.\n\nYou can request multiple tool calls in a single response. Group necessary calls when their inputs are already known and they have no result dependencies or conflicting side effects. The host determines which calls can execute concurrently. If a call needs an earlier result, or the operations could interfere, wait for the earlier result before requesting the dependent call. After a batch returns, use its results together to determine what remains unresolved. Do not add unnecessary calls just to form a batch, or repeat a completed lookup unless missing information, changed state, or necessary verification warrants it.\n\nFor investigation or design, gather the evidence needed to support the requested conclusions. A successful query may leave relevant documentation or implementation questions unresolved. Distinguish facts established by the evidence, information you have not checked, and absence confirmed within a checked scope. A field missing from a query response does not establish that documentation or another capability does not exist. Report unresolved information as unknown when further investigation is outside the requested scope.\n\nKeep exploration focused on the questions needed for the requested outcome; a lookup or execution task is not automatically a codebase investigation. Once that outcome and its necessary verification are complete, report them without extending the search to unrelated files or capabilities.";
2
+ /**
3
+ * Shared CLI/SDK execution guidance for the tools exposed by the caller.
4
+ * Pure text assembly: does not load tools, grant permissions, or alter persona.
5
+ * Pass canonical tool names from the effective pool, not display aliases.
6
+ */
7
+ export declare function getExecutionGuidance(toolNames: readonly string[]): string;