@the-open-engine/zeroshot 6.10.0 → 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.
- package/cli/index.js +16 -0
- package/cluster-hooks/block-ask-user-question.py +2 -3
- package/cluster-hooks/block-dangerous-git.py +2 -3
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +42 -1
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +2 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +6 -2
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +13 -6
- package/src/agent/agent-task-executor.js +526 -313
- package/src/agent-cli-provider/adapters/claude.ts +46 -1
- package/src/agent-cli-provider/contract-options.ts +6 -0
- package/src/agent-cli-provider/types.ts +9 -6
- package/src/claude-task-runner.js +153 -44
- package/src/task-spawn-cleanup-ownership.js +82 -0
- package/src/worktree-claude-config.js +193 -85
- package/task-lib/attachable-watcher.js +93 -267
- package/task-lib/command-spec-cleanup.js +219 -0
- package/task-lib/commands/clean.js +35 -5
- package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
- package/task-lib/commands/kill.js +117 -4
- package/task-lib/commands/status.js +1 -0
- package/task-lib/runner.js +61 -4
- package/task-lib/store.js +68 -6
- package/task-lib/watcher-output-runtime.js +284 -0
- package/task-lib/watcher.js +124 -257
|
@@ -3,127 +3,202 @@ const os = require('os');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
const { resolveWorktreeRoot } = require('./worktree-tooling-env');
|
|
6
|
-
const { provisionClaudeCredentials } = require('./claude-credentials');
|
|
7
6
|
|
|
8
7
|
const CLAUDE_DIRNAME = '.claude';
|
|
9
|
-
const SETTINGS_BASENAME = 'settings.json';
|
|
10
8
|
const MCP_BASENAME = '.mcp.json';
|
|
9
|
+
const SETTINGS_BASENAME = 'settings.json';
|
|
10
|
+
const OVERLAY_PREFIX = 'zeroshot-claude-settings-';
|
|
11
|
+
const CLAUDE_SETTINGS_ENV = 'ZEROSHOT_CLAUDE_SETTINGS_FILE';
|
|
12
|
+
const CLAUDE_MCP_CONFIG_ENV = 'ZEROSHOT_CLAUDE_MCP_CONFIG_FILE';
|
|
13
|
+
const ASK_USER_HOOK = 'block-ask-user-question.py';
|
|
14
|
+
const DANGEROUS_GIT_HOOK = 'block-dangerous-git.py';
|
|
11
15
|
|
|
12
|
-
function isPlainObject(value) {
|
|
13
|
-
return value && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
-
}
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
-
if (!fs.existsSync(
|
|
18
|
-
return
|
|
17
|
+
function readSettings(settingsPath) {
|
|
18
|
+
if (!fs.existsSync(settingsPath)) {
|
|
19
|
+
return {};
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
try {
|
|
22
|
-
return JSON.parse(fs.readFileSync(
|
|
23
|
-
} catch {
|
|
24
|
-
|
|
23
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Could not parse Claude settings overlay ${settingsPath}: ${error.message}`);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const merged = [];
|
|
32
|
-
|
|
33
|
-
for (const entry of [...baseValue, ...overrideValue]) {
|
|
34
|
-
const key = JSON.stringify(entry);
|
|
35
|
-
if (seen.has(key)) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
seen.add(key);
|
|
39
|
-
merged.push(entry);
|
|
40
|
-
}
|
|
29
|
+
function writeSettings(settingsPath, settings) {
|
|
30
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
31
|
+
}
|
|
41
32
|
|
|
42
|
-
|
|
33
|
+
function requireTargetClaudeDir(targetClaudeDir) {
|
|
34
|
+
if (typeof targetClaudeDir !== 'string' || !targetClaudeDir) {
|
|
35
|
+
throw new Error('Claude safety hooks require an explicit per-run settings directory.');
|
|
36
|
+
}
|
|
37
|
+
if (!isClaudeSettingsOverlayDirectory(targetClaudeDir)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Claude safety hooks require a Zeroshot-owned Claude settings overlay: ${targetClaudeDir}`
|
|
40
|
+
);
|
|
43
41
|
}
|
|
42
|
+
return targetClaudeDir;
|
|
43
|
+
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return merged;
|
|
45
|
+
function copyHookScript(targetClaudeDir, hookScriptName) {
|
|
46
|
+
const hooksDir = path.join(targetClaudeDir, 'hooks');
|
|
47
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const sourcePath = path.join(__dirname, '..', 'cluster-hooks', hookScriptName);
|
|
50
|
+
if (!fs.existsSync(sourcePath)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Claude safety hook ${hookScriptName} is missing from the Zeroshot installation.`
|
|
53
|
+
);
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
const destinationPath = path.join(hooksDir, hookScriptName);
|
|
57
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
58
|
+
fs.chmodSync(destinationPath, 0o755);
|
|
59
|
+
return destinationPath;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
+
function ensurePreToolUseHooks(settings) {
|
|
63
|
+
settings.hooks ||= {};
|
|
64
|
+
settings.hooks.PreToolUse ||= [];
|
|
65
|
+
return settings.hooks.PreToolUse;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
+
function ensureAskUserQuestionHook(targetClaudeDir) {
|
|
69
|
+
const overlayDir = requireTargetClaudeDir(targetClaudeDir);
|
|
70
|
+
|
|
71
|
+
const hookScriptPath = copyHookScript(overlayDir, ASK_USER_HOOK);
|
|
72
|
+
const settingsPath = path.join(overlayDir, SETTINGS_BASENAME);
|
|
73
|
+
const settings = readSettings(settingsPath);
|
|
74
|
+
const hooks = ensurePreToolUseHooks(settings);
|
|
75
|
+
const hasHook = hooks.some(
|
|
76
|
+
(entry) =>
|
|
77
|
+
entry.matcher === 'AskUserQuestion' ||
|
|
78
|
+
entry.hooks?.some((hook) => hook.command?.includes(ASK_USER_HOOK))
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!hasHook) {
|
|
82
|
+
hooks.push({
|
|
83
|
+
matcher: 'AskUserQuestion',
|
|
84
|
+
hooks: [{ type: 'command', command: hookScriptPath }],
|
|
85
|
+
});
|
|
86
|
+
writeSettings(settingsPath, settings);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
function ensureDangerousGitHook(targetClaudeDir) {
|
|
92
|
+
const overlayDir = requireTargetClaudeDir(targetClaudeDir);
|
|
93
|
+
|
|
94
|
+
const hookScriptPath = copyHookScript(overlayDir, DANGEROUS_GIT_HOOK);
|
|
95
|
+
const settingsPath = path.join(overlayDir, SETTINGS_BASENAME);
|
|
96
|
+
const settings = readSettings(settingsPath);
|
|
97
|
+
const hooks = ensurePreToolUseHooks(settings);
|
|
98
|
+
const hasHook = hooks.some(
|
|
99
|
+
(entry) =>
|
|
100
|
+
entry.matcher === 'Bash' &&
|
|
101
|
+
entry.hooks?.some((hook) => hook.command?.includes(DANGEROUS_GIT_HOOK))
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!hasHook) {
|
|
105
|
+
hooks.push({
|
|
106
|
+
matcher: 'Bash',
|
|
107
|
+
hooks: [{ type: 'command', command: hookScriptPath }],
|
|
108
|
+
});
|
|
109
|
+
writeSettings(settingsPath, settings);
|
|
71
110
|
}
|
|
72
111
|
|
|
73
|
-
return {
|
|
74
|
-
configDir,
|
|
75
|
-
settingsPath,
|
|
76
|
-
mcpPath,
|
|
77
|
-
};
|
|
78
112
|
}
|
|
79
113
|
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
function prepareClaudeSettingsOverlay(options = {}) {
|
|
115
|
+
const overlayDir = fs.mkdtempSync(path.join(os.tmpdir(), OVERLAY_PREFIX));
|
|
116
|
+
fs.chmodSync(overlayDir, 0o700);
|
|
117
|
+
try {
|
|
118
|
+
ensureAskUserQuestionHook(overlayDir);
|
|
119
|
+
if (options.includeDangerousGit) {
|
|
120
|
+
ensureDangerousGitHook(overlayDir);
|
|
121
|
+
}
|
|
122
|
+
return path.join(overlayDir, SETTINGS_BASENAME);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
fs.rmSync(overlayDir, { recursive: true, force: true });
|
|
125
|
+
throw error;
|
|
84
126
|
}
|
|
127
|
+
}
|
|
85
128
|
|
|
86
|
-
|
|
87
|
-
if (!
|
|
88
|
-
return
|
|
129
|
+
function cleanupClaudeSettingsOverlay(settingsPath) {
|
|
130
|
+
if (!isCanonicalClaudeSettingsOverlayPath(settingsPath)) {
|
|
131
|
+
return false;
|
|
89
132
|
}
|
|
90
133
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
134
|
+
const overlayDir = path.dirname(settingsPath);
|
|
135
|
+
if (!fs.existsSync(overlayDir)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (!isClaudeSettingsOverlayPath(settingsPath)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
95
141
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
142
|
+
fs.rmSync(overlayDir, { recursive: true, force: true });
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
99
145
|
|
|
100
|
-
|
|
146
|
+
function isCanonicalClaudeSettingsOverlayPath(settingsPath) {
|
|
147
|
+
if (typeof settingsPath !== 'string' || !settingsPath) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const resolvedSettingsPath = path.resolve(settingsPath);
|
|
151
|
+
const overlayDir = path.dirname(resolvedSettingsPath);
|
|
152
|
+
return (
|
|
153
|
+
resolvedSettingsPath === settingsPath &&
|
|
154
|
+
path.basename(resolvedSettingsPath) === SETTINGS_BASENAME &&
|
|
155
|
+
path.dirname(overlayDir) === path.resolve(os.tmpdir()) &&
|
|
156
|
+
path.basename(overlayDir).startsWith(OVERLAY_PREFIX)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
101
159
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (Object.keys(mergedSettings).length > 0) {
|
|
106
|
-
fs.writeFileSync(
|
|
107
|
-
path.join(overlayDir, SETTINGS_BASENAME),
|
|
108
|
-
JSON.stringify(mergedSettings, null, 2)
|
|
109
|
-
);
|
|
160
|
+
function isClaudeSettingsOverlayPath(settingsPath, platform = process.platform) {
|
|
161
|
+
if (!isCanonicalClaudeSettingsOverlayPath(settingsPath)) {
|
|
162
|
+
return false;
|
|
110
163
|
}
|
|
111
164
|
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
fs.
|
|
165
|
+
const resolvedSettingsPath = path.resolve(settingsPath);
|
|
166
|
+
const overlayDir = path.dirname(resolvedSettingsPath);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const stat = fs.lstatSync(overlayDir);
|
|
170
|
+
const ownedByProcess =
|
|
171
|
+
typeof process.getuid !== 'function' || stat.uid === process.getuid();
|
|
172
|
+
const privateMode = platform === 'win32' || (stat.mode & 0o777) === 0o700;
|
|
173
|
+
return stat.isDirectory() && !stat.isSymbolicLink() && ownedByProcess && privateMode;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
117
176
|
}
|
|
177
|
+
}
|
|
118
178
|
|
|
119
|
-
|
|
179
|
+
function isCanonicalClaudeSettingsOverlayDirectory(overlayDir) {
|
|
180
|
+
if (
|
|
181
|
+
typeof overlayDir !== 'string' ||
|
|
182
|
+
!overlayDir ||
|
|
183
|
+
path.resolve(overlayDir) !== overlayDir
|
|
184
|
+
) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return isCanonicalClaudeSettingsOverlayPath(path.join(overlayDir, SETTINGS_BASENAME));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isClaudeSettingsOverlayDirectory(overlayDir) {
|
|
191
|
+
if (typeof overlayDir !== 'string' || !overlayDir) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return isClaudeSettingsOverlayPath(path.join(overlayDir, SETTINGS_BASENAME));
|
|
120
195
|
}
|
|
121
196
|
|
|
122
197
|
/**
|
|
123
|
-
* Resolve the repo's
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
198
|
+
* Resolve the repo's MCP config for a given worktree/cwd. The project-root
|
|
199
|
+
* `.mcp.json` is Claude's current convention. `.claude/.mcp.json` remains a
|
|
200
|
+
* deliberate compatibility fallback for repositories created by Zeroshot's
|
|
201
|
+
* earlier worktree integration.
|
|
127
202
|
*/
|
|
128
203
|
function resolveRepoMcpConfigPath(options = {}) {
|
|
129
204
|
const worktreeRoot = resolveWorktreeRoot(options.worktreePath || options.cwd);
|
|
@@ -131,11 +206,44 @@ function resolveRepoMcpConfigPath(options = {}) {
|
|
|
131
206
|
return null;
|
|
132
207
|
}
|
|
133
208
|
|
|
134
|
-
const
|
|
135
|
-
|
|
209
|
+
const candidates = [
|
|
210
|
+
path.join(worktreeRoot, MCP_BASENAME),
|
|
211
|
+
path.join(worktreeRoot, CLAUDE_DIRNAME, MCP_BASENAME),
|
|
212
|
+
];
|
|
213
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveContainerMcpConfigPath(options = {}) {
|
|
217
|
+
const worktreeRoot = resolveWorktreeRoot(options.worktreePath || options.cwd);
|
|
218
|
+
const hostConfigPath = resolveRepoMcpConfigPath(options);
|
|
219
|
+
if (!worktreeRoot || !hostConfigPath) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const relativePath = path.relative(worktreeRoot, hostConfigPath);
|
|
224
|
+
if (
|
|
225
|
+
!relativePath ||
|
|
226
|
+
path.isAbsolute(relativePath) ||
|
|
227
|
+
relativePath === '..' ||
|
|
228
|
+
relativePath.startsWith(`..${path.sep}`)
|
|
229
|
+
) {
|
|
230
|
+
throw new Error(`Repository MCP config is outside the mounted workspace: ${hostConfigPath}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return path.posix.join('/workspace', relativePath.split(path.sep).join('/'));
|
|
136
234
|
}
|
|
137
235
|
|
|
138
236
|
module.exports = {
|
|
139
|
-
|
|
237
|
+
CLAUDE_MCP_CONFIG_ENV,
|
|
238
|
+
CLAUDE_SETTINGS_ENV,
|
|
239
|
+
cleanupClaudeSettingsOverlay,
|
|
240
|
+
ensureAskUserQuestionHook,
|
|
241
|
+
ensureDangerousGitHook,
|
|
242
|
+
isCanonicalClaudeSettingsOverlayDirectory,
|
|
243
|
+
isCanonicalClaudeSettingsOverlayPath,
|
|
244
|
+
isClaudeSettingsOverlayDirectory,
|
|
245
|
+
isClaudeSettingsOverlayPath,
|
|
246
|
+
prepareClaudeSettingsOverlay,
|
|
247
|
+
resolveContainerMcpConfigPath,
|
|
140
248
|
resolveRepoMcpConfigPath,
|
|
141
249
|
};
|