@the-open-engine/zeroshot 6.9.1 → 6.10.1

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 (65) hide show
  1. package/cli/commands/inspect.js +1 -0
  2. package/cli/index.js +17 -1
  3. package/cluster-hooks/block-ask-user-question.py +2 -3
  4. package/cluster-hooks/block-dangerous-git.py +2 -3
  5. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/claude.js +57 -3
  7. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/codex.js +32 -2
  10. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/common.js +1 -1
  12. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  13. package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
  14. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/adapters/index.js +8 -0
  16. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  17. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/contract-options.js +3 -0
  19. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  20. package/lib/agent-cli-provider/index.d.ts +1 -1
  21. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  22. package/lib/agent-cli-provider/index.js +2 -1
  23. package/lib/agent-cli-provider/index.js.map +1 -1
  24. package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
  25. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  26. package/lib/agent-cli-provider/provider-registry.js +3 -0
  27. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  28. package/lib/agent-cli-provider/types.d.ts +10 -2
  29. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  30. package/lib/agent-cli-provider/types.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/agent/agent-context-builder.js +90 -4
  33. package/src/agent/agent-context-sources.js +20 -2
  34. package/src/agent/agent-lifecycle.js +43 -8
  35. package/src/agent/agent-task-executor.js +557 -314
  36. package/src/agent/guidance-queue.js +29 -7
  37. package/src/agent/provider-session.js +277 -0
  38. package/src/agent-cli-provider/adapters/claude.ts +64 -4
  39. package/src/agent-cli-provider/adapters/codex.ts +47 -3
  40. package/src/agent-cli-provider/adapters/common.ts +1 -1
  41. package/src/agent-cli-provider/adapters/index.ts +10 -0
  42. package/src/agent-cli-provider/contract-options.ts +7 -0
  43. package/src/agent-cli-provider/index.ts +1 -0
  44. package/src/agent-cli-provider/provider-registry.ts +13 -1
  45. package/src/agent-cli-provider/types.ts +13 -6
  46. package/src/agent-wrapper.js +44 -8
  47. package/src/claude-task-runner.js +153 -44
  48. package/src/ledger-sequence.js +63 -0
  49. package/src/ledger.js +114 -29
  50. package/src/orchestrator.js +40 -2
  51. package/src/task-spawn-cleanup-ownership.js +82 -0
  52. package/src/worktree-claude-config.js +193 -85
  53. package/task-lib/attachable-watcher.js +101 -253
  54. package/task-lib/command-spec-cleanup.js +219 -0
  55. package/task-lib/commands/clean.js +35 -5
  56. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  57. package/task-lib/commands/kill.js +117 -4
  58. package/task-lib/commands/resume.js +29 -6
  59. package/task-lib/commands/status.js +4 -0
  60. package/task-lib/provider-helper-runtime.js +1 -0
  61. package/task-lib/provider-session-capture.js +138 -0
  62. package/task-lib/runner.js +70 -5
  63. package/task-lib/store.js +126 -12
  64. package/task-lib/watcher-output-runtime.js +284 -0
  65. package/task-lib/watcher.js +131 -242
@@ -1,6 +1,7 @@
1
1
  import { unlinkSync, existsSync } from 'fs';
2
2
  import chalk from 'chalk';
3
3
  import { loadTasks, saveTasks } from '../store.js';
4
+ import { createCommandSpecCleanup } from '../command-spec-cleanup.js';
4
5
 
5
6
  export function cleanTasks(options = {}) {
6
7
  const tasks = loadTasks();
@@ -11,6 +12,8 @@ export function cleanTasks(options = {}) {
11
12
  return;
12
13
  }
13
14
 
15
+ let cleanupFailed = false;
16
+ let removedCount = 0;
14
17
  const toRemove = [];
15
18
 
16
19
  for (const task of taskList) {
@@ -33,18 +36,45 @@ export function cleanTasks(options = {}) {
33
36
  console.log(chalk.dim(`Removing ${toRemove.length} task(s)...\n`));
34
37
 
35
38
  for (const task of toRemove) {
36
- // Delete log file
39
+ if (task.commandCleanup) {
40
+ if (task.status === 'running') {
41
+ cleanupFailed = true;
42
+ console.log(
43
+ chalk.yellow(` Retained: ${task.id} [running] (live command cleanup ownership)`)
44
+ );
45
+ continue;
46
+ }
47
+ let recovered = false;
48
+ try {
49
+ const cleanup = createCommandSpecCleanup(task.commandCleanup, (cleanupPath, error) => {
50
+ console.log(chalk.yellow(`Warning: failed to clean up ${cleanupPath}: ${error.message}`));
51
+ });
52
+ recovered = cleanup.runSync();
53
+ } catch (error) {
54
+ console.log(
55
+ chalk.yellow(`Warning: failed to validate cleanup for task ${task.id}: ${error.message}`)
56
+ );
57
+ }
58
+ if (!recovered) {
59
+ cleanupFailed = true;
60
+ console.log(
61
+ chalk.yellow(` Retained: ${task.id} [${task.status}] (command cleanup pending)`)
62
+ );
63
+ continue;
64
+ }
65
+ task.commandCleanup = null;
66
+ }
37
67
  if (task.logFile && existsSync(task.logFile)) {
38
68
  unlinkSync(task.logFile);
39
69
  }
40
70
 
41
- // Remove from tasks
42
- delete tasks[task.id];
43
-
44
71
  console.log(chalk.dim(` Removed: ${task.id} [${task.status}]`));
72
+ delete tasks[task.id];
73
+ removedCount++;
45
74
  }
46
75
 
47
76
  saveTasks(tasks);
48
77
 
49
- console.log(chalk.green(`\n✓ Cleaned ${toRemove.length} task(s)`));
78
+ console.log(chalk.green(`\n✓ Cleaned ${removedCount} task(s)`));
79
+ if (cleanupFailed) process.exitCode = 1;
50
80
  }
@@ -0,0 +1,10 @@
1
+ import { getTaskBySpawnOwnershipToken } from '../store.js';
2
+
3
+ export function getTaskIdBySpawnToken(token) {
4
+ const task = getTaskBySpawnOwnershipToken(token);
5
+ if (!task) {
6
+ process.exitCode = 2;
7
+ return;
8
+ }
9
+ process.stdout.write(`${task.id}\n`);
10
+ }
@@ -1,6 +1,75 @@
1
1
  import chalk from 'chalk';
2
- import { getTask, updateTask } from '../store.js';
3
- import { terminateProcess } from '../runner.js';
2
+ import { getTask, requestTaskCancellation, updateTask } from '../store.js';
3
+ import { createCommandSpecCleanup } from '../command-spec-cleanup.js';
4
+ import { terminateProcess } from '../process-termination.js';
5
+
6
+ async function cleanupTerminatedTask(task) {
7
+ if (!task.commandCleanup) return {};
8
+ try {
9
+ const cleanup = createCommandSpecCleanup(task.commandCleanup, (cleanupPath, error) => {
10
+ console.log(chalk.yellow(`Warning: failed to clean up ${cleanupPath}: ${error.message}`));
11
+ });
12
+ return (await cleanup.run()) ? { commandCleanup: null } : {};
13
+ } catch (error) {
14
+ console.log(
15
+ chalk.yellow(`Warning: failed to validate persisted command cleanup: ${error.message}`)
16
+ );
17
+ return {};
18
+ }
19
+ }
20
+
21
+ async function retryTerminalTaskCleanup(taskId, task) {
22
+ if (!task.commandCleanup) return true;
23
+ const cleanupUpdate = await cleanupTerminatedTask(task);
24
+ if (cleanupUpdate.commandCleanup === null) {
25
+ updateTask(taskId, cleanupUpdate);
26
+ console.log(chalk.green(`✓ Recovered pending command cleanup for task ${taskId}`));
27
+ return true;
28
+ }
29
+ console.log(chalk.yellow(`Warning: command cleanup remains pending for task ${taskId}`));
30
+ process.exitCode = 1;
31
+ return false;
32
+ }
33
+
34
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
35
+
36
+ function sleep(ms) {
37
+ return new Promise((resolve) => setTimeout(resolve, ms));
38
+ }
39
+
40
+ async function waitForStartupCancellation(taskId, options) {
41
+ const timeoutMs = options.startupCancelTimeoutMs ?? 8000;
42
+ const pollMs = options.startupCancelPollMs ?? options.pollMs ?? 25;
43
+ const deadline = Date.now() + timeoutMs;
44
+
45
+ while (Date.now() <= deadline) {
46
+ const current = getTask(taskId);
47
+ if (!current) break;
48
+ if (TERMINAL_STATUSES.has(current.status)) {
49
+ await retryTerminalTaskCleanup(taskId, current);
50
+ if (!getTask(taskId)?.commandCleanup) {
51
+ console.log(chalk.green(`✓ Cancelled task ${taskId} before provider startup completed`));
52
+ return true;
53
+ }
54
+ }
55
+ if (Number.isInteger(current.pid) && current.pid > 0) {
56
+ await killTaskCommand(taskId, options);
57
+ const terminal = getTask(taskId);
58
+ return Boolean(
59
+ terminal && TERMINAL_STATUSES.has(terminal.status) && !terminal.commandCleanup
60
+ );
61
+ }
62
+ await sleep(pollMs);
63
+ }
64
+
65
+ console.log(
66
+ chalk.yellow(
67
+ `Cancellation for task ${taskId} remains pending; provider termination and cleanup were not confirmed`
68
+ )
69
+ );
70
+ process.exitCode = 1;
71
+ return false;
72
+ }
4
73
 
5
74
  export async function killTaskCommand(taskId, options = {}) {
6
75
  const task = getTask(taskId);
@@ -11,30 +80,68 @@ export async function killTaskCommand(taskId, options = {}) {
11
80
  }
12
81
 
13
82
  if (task.status !== 'running') {
83
+ await retryTerminalTaskCleanup(taskId, task);
14
84
  console.log(chalk.yellow(`Task is not running (status: ${task.status})`));
15
85
  return;
16
86
  }
17
87
 
88
+ if (!Number.isInteger(task.pid) || task.pid <= 0) {
89
+ requestTaskCancellation(taskId);
90
+ console.log(
91
+ chalk.yellow(
92
+ `Task ${taskId} has not published a provider PID; persisted cancellation is pending`
93
+ )
94
+ );
95
+ await waitForStartupCancellation(taskId, options);
96
+ return;
97
+ }
98
+
99
+ const platform = options.platform || process.platform;
100
+ const terminate = options.terminateProcessFn || terminateProcess;
101
+ const processOptions = { ...options };
102
+ delete processOptions.platform;
103
+ delete processOptions.terminateProcessFn;
104
+
18
105
  const terminationOptions = {
19
- ...options,
106
+ ...processOptions,
20
107
  processGroupId: task.processGroupId,
21
108
  terminationStrategy: task.terminationStrategy || 'process',
22
109
  };
23
110
 
24
- const result = await terminateProcess(task.pid, terminationOptions);
111
+ const result = await terminate(task.pid, terminationOptions);
25
112
 
26
113
  if (result.terminated && result.alreadyDead) {
114
+ if (platform === 'win32' && task.terminationStrategy === 'process-tree') {
115
+ console.log(
116
+ chalk.yellow(
117
+ `Warning: Windows task root ${task.pid} is gone but descendant termination is unverified; preserving cleanup ownership`
118
+ )
119
+ );
120
+ updateTask(taskId, {
121
+ error: 'Windows process-tree termination could not be confirmed after root exit',
122
+ });
123
+ process.exitCode = 1;
124
+ return;
125
+ }
27
126
  console.log(chalk.yellow('Process already dead, updating status...'));
127
+ const cleanupUpdate = await cleanupTerminatedTask(task);
28
128
  updateTask(taskId, {
29
129
  status: 'stale',
30
130
  pid: null,
31
131
  processGroupId: null,
32
132
  error: 'Process died unexpectedly',
133
+ cancelRequested: false,
134
+ ...cleanupUpdate,
33
135
  });
136
+ if (getTask(taskId)?.commandCleanup) {
137
+ console.log(chalk.yellow(`Warning: command cleanup remains pending for task ${taskId}`));
138
+ process.exitCode = 1;
139
+ }
34
140
  return;
35
141
  }
36
142
 
37
143
  if (result.terminated) {
144
+ const cleanupUpdate = await cleanupTerminatedTask(task);
38
145
  const suffix = result.escalated ? ' after SIGKILL escalation' : ' with SIGTERM';
39
146
  console.log(chalk.green(`✓ Killed task ${taskId} (PID: ${task.pid})${suffix}`));
40
147
  if (result.degraded) {
@@ -46,7 +153,13 @@ export async function killTaskCommand(taskId, options = {}) {
46
153
  processGroupId: null,
47
154
  exitCode: result.escalated ? 137 : 143,
48
155
  error: result.escalated ? 'Killed by user after SIGKILL escalation' : 'Killed by user',
156
+ cancelRequested: false,
157
+ ...cleanupUpdate,
49
158
  });
159
+ if (getTask(taskId)?.commandCleanup) {
160
+ console.log(chalk.yellow(`Warning: command cleanup remains pending for task ${taskId}`));
161
+ process.exitCode = 1;
162
+ }
50
163
  } else {
51
164
  console.log(chalk.red(`Failed to kill task ${taskId}`));
52
165
  updateTask(taskId, {
@@ -1,7 +1,35 @@
1
1
  import chalk from 'chalk';
2
+ import { createRequire } from 'module';
2
3
  import { getTask } from '../store.js';
3
4
  import { spawnTask } from '../runner.js';
4
5
 
6
+ const require = createRequire(import.meta.url);
7
+ const { providerSupportsCapability } = require('../../lib/provider-names.js');
8
+
9
+ export function buildResumeTaskOptions(task) {
10
+ if (!providerSupportsCapability(task.provider, 'sessionResume')) {
11
+ throw new Error(`Provider ${task.provider} does not support safe session resume.`);
12
+ }
13
+ if (
14
+ task.requestedResumeSessionId &&
15
+ (task.status !== 'completed' || task.resumeIdentityVerified !== true)
16
+ ) {
17
+ throw new Error(
18
+ `Task ${task.id} did not durably verify its requested provider session; refusing resume.`
19
+ );
20
+ }
21
+ if (!task.sessionId) {
22
+ throw new Error(
23
+ `Task ${task.id} has no captured provider session ID; refusing cwd-wide continuation.`
24
+ );
25
+ }
26
+ return {
27
+ cwd: task.cwd,
28
+ resume: task.sessionId,
29
+ provider: task.provider,
30
+ };
31
+ }
32
+
5
33
  export async function resumeTask(taskId, newPrompt) {
6
34
  const task = getTask(taskId);
7
35
 
@@ -23,12 +51,7 @@ export async function resumeTask(taskId, newPrompt) {
23
51
  console.log(chalk.dim(`Original prompt: ${task.prompt}`));
24
52
  console.log(chalk.dim(`Resume prompt: ${prompt}`));
25
53
 
26
- const newTask = await spawnTask(prompt, {
27
- cwd: task.cwd,
28
- continue: true, // Use --continue to load most recent session in that directory
29
- sessionId: task.sessionId,
30
- provider: task.provider,
31
- });
54
+ const newTask = await spawnTask(prompt, buildResumeTaskOptions(task));
32
55
 
33
56
  console.log(chalk.green(`\n✓ Resumed as new task: ${chalk.cyan(newTask.id)}`));
34
57
  console.log(chalk.dim(` PID: ${newTask.pid}`));
@@ -41,6 +41,10 @@ export function showStatus(taskId) {
41
41
  console.log(`${chalk.dim('PID:')} ${task.pid || 'N/A'}`);
42
42
  console.log(`${chalk.dim('Exit Code:')} ${task.exitCode ?? 'N/A'}`);
43
43
  console.log(`${chalk.dim('Session:')} ${task.sessionId || 'N/A'}`);
44
+ console.log(`${chalk.dim('Cleanup:')} ${task.commandCleanup ? 'pending' : 'complete'}`);
45
+ if (task.requestedResumeSessionId) {
46
+ console.log(`${chalk.dim('Requested:')} ${task.requestedResumeSessionId}`);
47
+ }
44
48
  console.log(`${chalk.dim('Log File:')} ${task.logFile}`);
45
49
 
46
50
  console.log(`\n${chalk.dim('Prompt:')}`);
@@ -19,6 +19,7 @@ export const {
19
19
  classifyProviderError,
20
20
  detectProviderFatalError,
21
21
  detectProviderStreamingModeError,
22
+ extractProviderSessionId,
22
23
  findProviderRegistryEntry,
23
24
  getProviderAdapter,
24
25
  getProviderRegistryEntry,
@@ -0,0 +1,138 @@
1
+ import { extractProviderSessionId } from './provider-helper-runtime.js';
2
+
3
+ /**
4
+ * Capture a provider-owned session ID from one complete output line.
5
+ *
6
+ * The caller owns persistence so watcher variants can share parsing without
7
+ * importing each other's process lifecycle. Repeated IDs are idempotent; any
8
+ * differing ID makes the capture permanently ambiguous.
9
+ */
10
+ export function captureProviderSessionLine({
11
+ providerName,
12
+ line,
13
+ currentSessionId = null,
14
+ observedSessionIds = new Set(currentSessionId ? [currentSessionId] : []),
15
+ sessionIdConflict = false,
16
+ onCapture = () => {},
17
+ onConflict = () => {},
18
+ }) {
19
+ const sessionId = extractProviderSessionId(providerName, line);
20
+ if (!sessionId) {
21
+ return { currentSessionId, sessionIdConflict };
22
+ }
23
+
24
+ observedSessionIds.add(sessionId);
25
+ if (sessionIdConflict || observedSessionIds.size > 1) {
26
+ if (!sessionIdConflict) {
27
+ onConflict([...observedSessionIds]);
28
+ }
29
+ return { currentSessionId: null, sessionIdConflict: true };
30
+ }
31
+
32
+ if (sessionId === currentSessionId) {
33
+ return { currentSessionId, sessionIdConflict: false };
34
+ }
35
+ onCapture(sessionId);
36
+ return { currentSessionId: sessionId, sessionIdConflict: false };
37
+ }
38
+
39
+ function providerSessionCompletionError({
40
+ requestedSessionId,
41
+ currentSessionId,
42
+ sessionIdConflict,
43
+ persistenceError,
44
+ }) {
45
+ if (persistenceError) {
46
+ return `Provider session identity could not be persisted: ${persistenceError.message}`;
47
+ }
48
+
49
+ const requested = requestedSessionId?.trim() || null;
50
+ if (!requested) {
51
+ return null;
52
+ }
53
+ if (sessionIdConflict) {
54
+ return 'Provider continuation emitted conflicting session identities';
55
+ }
56
+
57
+ const captured = currentSessionId?.trim() || null;
58
+ if (captured === requested) {
59
+ return null;
60
+ }
61
+ return captured
62
+ ? 'Provider continuation returned a different session identity'
63
+ : 'Provider continuation did not confirm the requested session identity';
64
+ }
65
+
66
+ export function createProviderSessionCapture({
67
+ providerName,
68
+ taskId,
69
+ updateTask,
70
+ log,
71
+ requestedSessionId = null,
72
+ initialSessionId = null,
73
+ initialSessionIdConflict = false,
74
+ }) {
75
+ let currentSessionId = initialSessionId;
76
+ let sessionIdConflict = initialSessionIdConflict;
77
+ let persistenceError = null;
78
+ const observedSessionIds = new Set(initialSessionId ? [initialSessionId] : []);
79
+
80
+ function persist(update) {
81
+ try {
82
+ updateTask(taskId, update);
83
+ } catch (error) {
84
+ persistenceError ||= error;
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ function captureLine(line) {
90
+ try {
91
+ ({ currentSessionId, sessionIdConflict } = captureProviderSessionLine({
92
+ providerName,
93
+ line,
94
+ currentSessionId,
95
+ observedSessionIds,
96
+ sessionIdConflict,
97
+ onCapture: (sessionId) => {
98
+ // Advance memory before persistence so a failed write can never leave
99
+ // this watcher believing the earlier identity is still trustworthy.
100
+ currentSessionId = sessionId;
101
+ persist({ sessionId });
102
+ },
103
+ onConflict: () => {
104
+ currentSessionId = null;
105
+ sessionIdConflict = true;
106
+ persist({
107
+ sessionId: null,
108
+ sessionIdConflict: true,
109
+ resumeIdentityVerified: false,
110
+ });
111
+ },
112
+ }));
113
+ } catch (error) {
114
+ log(`[${Date.now()}][SESSION] Failed to persist provider session: ${error.message}\n`);
115
+ }
116
+ }
117
+
118
+ function getCompletionError() {
119
+ return providerSessionCompletionError({
120
+ requestedSessionId,
121
+ currentSessionId,
122
+ sessionIdConflict,
123
+ persistenceError,
124
+ });
125
+ }
126
+
127
+ return {
128
+ captureLine,
129
+ getCompletionError,
130
+ getCompletionUpdate: (resolvedCode) => {
131
+ const error = getCompletionError();
132
+ return {
133
+ resumeIdentityVerified: resolvedCode === 0 && !error,
134
+ ...(error ? { sessionId: null, sessionIdConflict: true } : {}),
135
+ };
136
+ },
137
+ };
138
+ }
@@ -12,6 +12,12 @@ const {
12
12
  ISOLATED_SETTINGS_FILE_MARKER,
13
13
  LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV,
14
14
  } = require('../src/task-run-model-args.js');
15
+ const {
16
+ CLAUDE_MCP_CONFIG_ENV,
17
+ CLAUDE_SETTINGS_ENV,
18
+ isClaudeSettingsOverlayPath,
19
+ } = require('../src/worktree-claude-config');
20
+ const { TASK_SPAWN_OWNERSHIP_TOKEN_ENV } = require('../src/task-spawn-cleanup-ownership');
15
21
  export {
16
22
  isOwnedProcessTreeRunning,
17
23
  isProcessRunning,
@@ -37,7 +43,7 @@ export function spawnTask(prompt, options = {}) {
37
43
  });
38
44
  const providerName = prepared.adapter.id;
39
45
  const modelSpec = prepared.options.modelSpec;
40
- const commandSpec = prepared.commandSpec;
46
+ const commandSpec = attachClaudeOverlayCleanup(prepared.commandSpec, providerName);
41
47
 
42
48
  const task = buildTaskRecord({
43
49
  id,
@@ -47,6 +53,7 @@ export function spawnTask(prompt, options = {}) {
47
53
  logFile,
48
54
  providerName,
49
55
  modelSpec,
56
+ commandSpec,
50
57
  });
51
58
 
52
59
  addTask(task);
@@ -116,14 +123,24 @@ function buildProviderOptions(options, runtime, modelSelection) {
116
123
  autoApprove: true,
117
124
  ...(modelSelection === undefined ? {} : { modelSpec: modelSelection.modelSpec }),
118
125
  ...mcpConfigOption(options),
126
+ ...claudeSettingsFileOption(),
119
127
  ...(options.resume ? { resumeSessionId: options.resume } : {}),
120
128
  ...(options.continue ? { continueSession: true } : {}),
121
129
  };
122
130
  }
123
131
 
132
+ function claudeSettingsFileOption() {
133
+ const settingsPath = process.env[CLAUDE_SETTINGS_ENV]?.trim();
134
+ return settingsPath ? { claudeSettingsFile: settingsPath } : {};
135
+ }
136
+
124
137
  function mcpConfigOption(options) {
125
- const entries = options.mcpConfig;
126
- if (!Array.isArray(entries) || entries.length === 0) return {};
138
+ const entries = Array.isArray(options.mcpConfig) ? [...options.mcpConfig] : [];
139
+ const claudeMcpConfigPath = process.env[CLAUDE_MCP_CONFIG_ENV]?.trim();
140
+ if (claudeMcpConfigPath && !entries.includes(claudeMcpConfigPath)) {
141
+ entries.push(claudeMcpConfigPath);
142
+ }
143
+ if (entries.length === 0) return {};
127
144
  return { mcpConfig: entries };
128
145
  }
129
146
 
@@ -155,7 +172,37 @@ function providerLevelSelection(options) {
155
172
  return { modelSpec };
156
173
  }
157
174
 
158
- function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, modelSpec }) {
175
+ export function attachClaudeOverlayCleanup(commandSpec, providerName) {
176
+ if (providerName !== 'claude') return commandSpec;
177
+ const settingsPath = process.env[CLAUDE_SETTINGS_ENV]?.trim();
178
+ if (!isClaudeSettingsOverlayPath(settingsPath)) return commandSpec;
179
+
180
+ const overlayDir = dirname(settingsPath);
181
+ return {
182
+ ...commandSpec,
183
+ cleanup: [...(commandSpec.cleanup || []), overlayDir],
184
+ cleanupMetadata: [
185
+ ...(commandSpec.cleanupMetadata || []),
186
+ {
187
+ kind: 'temp-directory',
188
+ provider: 'claude',
189
+ path: overlayDir,
190
+ reason: 'settings-overlay',
191
+ },
192
+ ],
193
+ };
194
+ }
195
+
196
+ export function buildTaskRecord({
197
+ id,
198
+ prompt,
199
+ cwd,
200
+ options,
201
+ logFile,
202
+ providerName,
203
+ modelSpec,
204
+ commandSpec = {},
205
+ }) {
159
206
  return {
160
207
  id,
161
208
  prompt: prompt.slice(0, 200) + (prompt.length > 200 ? '...' : ''),
@@ -163,7 +210,15 @@ function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, mode
163
210
  cwd,
164
211
  status: 'running',
165
212
  pid: null,
166
- sessionId: options.resume || options.sessionId || null,
213
+ // Only watcher-observed provider output may populate sessionId. A requested
214
+ // resume ID is diagnostic input, not proof the resumed provider emitted or
215
+ // accepted that session identity.
216
+ sessionId: null,
217
+ sessionIdConflict: false,
218
+ requestedResumeSessionId: options.resume || null,
219
+ // Resumed tasks start fail-closed. Only the watcher terminal transaction
220
+ // may prove that the requested identity completed without conflict.
221
+ resumeIdentityVerified: !options.resume,
167
222
  logFile,
168
223
  createdAt: new Date().toISOString(),
169
224
  updatedAt: new Date().toISOString(),
@@ -178,6 +233,15 @@ function buildTaskRecord({ id, prompt, cwd, options, logFile, providerName, mode
178
233
  attachable: false,
179
234
  processGroupId: null,
180
235
  terminationStrategy: null,
236
+ cancelRequested: false,
237
+ spawnOwnershipToken: process.env[TASK_SPAWN_OWNERSHIP_TOKEN_ENV] || null,
238
+ commandCleanup:
239
+ commandSpec.cleanup?.length > 0
240
+ ? {
241
+ cleanup: commandSpec.cleanup,
242
+ cleanupMetadata: commandSpec.cleanupMetadata || [],
243
+ }
244
+ : null,
181
245
  };
182
246
  }
183
247
 
@@ -236,6 +300,7 @@ function spawnWatcher({ watcherScript, id, cwd, logFile, finalArgs, watcherConfi
236
300
  export function buildWatcherEnv(sourceEnv = process.env) {
237
301
  const watcherEnv = { ...sourceEnv };
238
302
  delete watcherEnv[LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV];
303
+ delete watcherEnv[TASK_SPAWN_OWNERSHIP_TOKEN_ENV];
239
304
  if (watcherEnv[ISOLATED_SETTINGS_FILE_MARKER] === '1') {
240
305
  delete watcherEnv[ISOLATED_SETTINGS_FILE_ENV];
241
306
  delete watcherEnv[ISOLATED_SETTINGS_FILE_MARKER];