@ran-sh/dsh-crew 0.5.1 → 0.5.3
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/README.md +93 -112
- package/README.zh.md +93 -111
- package/agents/ds-worker.md +5 -4
- package/codex/AGENTS.md +92 -0
- package/codex/agents/ds-worker.toml +1 -1
- package/docs/installation.md +63 -0
- package/docs/job-contracts.md +12 -4
- package/docs/readiness-matrix.md +6 -2
- package/docs/ui-surfaces.md +1 -2
- package/lib/client.js +41 -4
- package/official-web-bridge/lib/client.js +41 -4
- package/package.json +6 -1
- package/scripts/setup.mjs +127 -42
- package/src/client/host-readiness.mjs +2 -1
- package/src/client/index.tsx +29 -15
- package/src/config-readiness.mjs +50 -10
- package/src/hub/index.mjs +42 -15
- package/src/install/install-legacy.mjs +65 -11
- package/src/install/install.mjs +3 -1
- package/src/install/npx-lifecycle.mjs +55 -19
- package/src/install/windows-startup.mjs +115 -0
- package/src/install/zcode.mjs +316 -0
- package/src/orchestrator.mjs +53 -0
- package/src/readiness-matrix.mjs +4 -3
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +38 -34
- package/src/workflow-runtime.mjs +27 -27
- package/src/workflow.mjs +63 -5
- package/windows/start-dsh-crew.cmd +57 -0
- package/windows/start-dsh-crew.ps1 +326 -0
- package/windows/start-dsh-crew.vbs +9 -0
- package/zcode/AGENTS.md +26 -0
- package/zcode/agents/ds-reviewer.md +36 -0
- package/zcode/agents/ds-worker.md +39 -0
- package/zcode/commands/dsh-config.md +6 -0
- package/zcode/commands/dsh-status.md +4 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// ZCode host integration. This module is deliberately file-only: it never
|
|
2
|
+
// launches ZCode, mutates credentials, or overwrites an unowned MCP server.
|
|
3
|
+
// The generated files make ZCode dispatch through the same dsh-crew MCP server
|
|
4
|
+
// used by Codex and Claude while keeping ZCode's native config precedence.
|
|
5
|
+
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
|
|
12
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
|
+
const HOST = 'zcode';
|
|
14
|
+
const SERVER = 'dsh-crew';
|
|
15
|
+
const POLICY_START = '<!-- DSH CREW MANAGED ZCODE POLICY:START -->';
|
|
16
|
+
const POLICY_END = '<!-- DSH CREW MANAGED ZCODE POLICY:END -->';
|
|
17
|
+
const OWNERSHIP_FILE = ({ home = homedir() } = {}) => join(home, '.config', 'dsh-crew', 'integrations', 'zcode.json');
|
|
18
|
+
|
|
19
|
+
// Keep the allowlist explicit. ZCode rejects wildcard tool permissions, and a
|
|
20
|
+
// future server tool is not silently exposed until a template intentionally
|
|
21
|
+
// opts into it.
|
|
22
|
+
export const ZCODE_MCP_TOOLS = Object.freeze([
|
|
23
|
+
'dsh_run_worker',
|
|
24
|
+
'dsh_spawn_worker',
|
|
25
|
+
'dsh_worker_status',
|
|
26
|
+
'dsh_worker_result',
|
|
27
|
+
'dsh_worker_cancel',
|
|
28
|
+
'dsh_worker_config',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function readText(file) {
|
|
32
|
+
try { return readFileSync(file, 'utf8'); } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readJson(file, fallback = null) {
|
|
36
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function contentHash(text) {
|
|
40
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeJson(file, value) {
|
|
44
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
45
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function backup(file) {
|
|
49
|
+
if (!existsSync(file)) return null;
|
|
50
|
+
const path = `${file}.dsh-crew-backup-${Date.now()}`;
|
|
51
|
+
copyFileSync(file, path);
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizePath(value) {
|
|
56
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
57
|
+
try {
|
|
58
|
+
const path = resolve(value);
|
|
59
|
+
return process.platform === 'win32' ? path.toLowerCase() : path;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serverTarget(server) {
|
|
64
|
+
if (!server || typeof server !== 'object') return null;
|
|
65
|
+
if (server.command !== 'node' || !Array.isArray(server.args) || typeof server.args[0] !== 'string') return null;
|
|
66
|
+
return normalizePath(server.args[0]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nativeServers(config) {
|
|
70
|
+
const servers = config?.mcp?.servers;
|
|
71
|
+
return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sharedServers(config) {
|
|
75
|
+
const servers = config?.mcpServers;
|
|
76
|
+
return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function configSource({ home = homedir() } = {}) {
|
|
80
|
+
const nativeFile = join(home, '.zcode', 'cli', 'config.json');
|
|
81
|
+
const sharedFile = join(home, '.agents', 'mcp.json');
|
|
82
|
+
const native = readJson(nativeFile, {});
|
|
83
|
+
const shared = readJson(sharedFile, {});
|
|
84
|
+
const nativeMap = nativeServers(native);
|
|
85
|
+
const sharedMap = sharedServers(shared);
|
|
86
|
+
// ZCode shadows .agents/mcp.json whenever its native config has at least
|
|
87
|
+
// one server. If native is empty, the shared file is the least-surprising
|
|
88
|
+
// compatibility path for users who already manage MCP centrally.
|
|
89
|
+
if (Object.keys(nativeMap).length > 0 || Object.keys(sharedMap).length === 0) {
|
|
90
|
+
return { file: nativeFile, kind: 'native', config: native, servers: nativeMap };
|
|
91
|
+
}
|
|
92
|
+
return { file: sharedFile, kind: 'shared', config: shared, servers: sharedMap };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveZCodeMcpTarget({ home = homedir() } = {}) {
|
|
96
|
+
const source = configSource({ home });
|
|
97
|
+
return serverTarget(source.servers[SERVER]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectedTarget({ root = ROOT } = {}) {
|
|
101
|
+
return resolve(join(root, 'src', 'server.mjs'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function managedPolicyBlock(root) {
|
|
105
|
+
const template = readText(join(root, 'zcode', 'AGENTS.md'))?.trim();
|
|
106
|
+
return template ? `${POLICY_START}\n${template}\n${POLICY_END}` : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function installPolicy({ home, root }) {
|
|
110
|
+
const file = join(home, '.zcode', 'AGENTS.md');
|
|
111
|
+
const block = managedPolicyBlock(root);
|
|
112
|
+
if (!block) return { ok: false, code: 'ZCODE_POLICY_TEMPLATE_MISSING' };
|
|
113
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
114
|
+
const current = readText(file) ?? '';
|
|
115
|
+
const escapedStart = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
+
const escapedEnd = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
117
|
+
const managed = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`, 'm');
|
|
118
|
+
const next = managed.test(current)
|
|
119
|
+
? current.replace(managed, block)
|
|
120
|
+
: `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
|
|
121
|
+
if (next !== current) { backup(file); writeFileSync(file, next); }
|
|
122
|
+
return { ok: true, file };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function removePolicy({ home }) {
|
|
126
|
+
const file = join(home, '.zcode', 'AGENTS.md');
|
|
127
|
+
const current = readText(file);
|
|
128
|
+
if (typeof current !== 'string') return false;
|
|
129
|
+
const start = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
130
|
+
const end = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
131
|
+
const managed = new RegExp(`(?:\\r?\\n){0,2}${start}[\\s\\S]*?${end}(?:\\r?\\n)?`, 'm');
|
|
132
|
+
if (!managed.test(current)) return false;
|
|
133
|
+
const next = current.replace(managed, '').trimEnd();
|
|
134
|
+
backup(file);
|
|
135
|
+
if (next.trim()) writeFileSync(file, `${next}\n`);
|
|
136
|
+
else rmSync(file, { force: true });
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ownership({ home = homedir() } = {}) {
|
|
141
|
+
const data = readJson(OWNERSHIP_FILE({ home }), null);
|
|
142
|
+
return data && typeof data === 'object' && data.host === HOST ? data : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeOwnership({ home, configFile, configKind, target, files = [] }) {
|
|
146
|
+
writeJson(OWNERSHIP_FILE({ home }), {
|
|
147
|
+
schema_version: 1,
|
|
148
|
+
host: HOST,
|
|
149
|
+
server: SERVER,
|
|
150
|
+
config_file: configFile,
|
|
151
|
+
config_kind: configKind,
|
|
152
|
+
target,
|
|
153
|
+
files,
|
|
154
|
+
managed_at: new Date().toISOString(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function templateFiles({ home, root }) {
|
|
159
|
+
return [
|
|
160
|
+
['agents', 'ds-worker.md'],
|
|
161
|
+
['agents', 'ds-reviewer.md'],
|
|
162
|
+
['commands', 'dsh-config.md'],
|
|
163
|
+
['commands', 'dsh-status.md'],
|
|
164
|
+
].map(([dir, file]) => ({ source: join(root, 'zcode', dir, file), dest: join(home, '.zcode', dir, file) }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function installTemplates({ home, root, priorFiles = [] }) {
|
|
168
|
+
const actions = [];
|
|
169
|
+
const records = [];
|
|
170
|
+
for (const { source, dest } of templateFiles({ home, root })) {
|
|
171
|
+
if (!existsSync(source)) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source };
|
|
172
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
173
|
+
const before = readText(dest);
|
|
174
|
+
const rendered = readFileSync(source, 'utf8');
|
|
175
|
+
const previous = priorFiles.find((entry) => entry?.path === dest);
|
|
176
|
+
let backupFile = previous?.backup ?? null;
|
|
177
|
+
if (before !== rendered) {
|
|
178
|
+
if (before !== null && !backupFile) backupFile = backup(dest);
|
|
179
|
+
writeFileSync(dest, rendered);
|
|
180
|
+
}
|
|
181
|
+
actions.push(dest);
|
|
182
|
+
records.push({
|
|
183
|
+
path: dest,
|
|
184
|
+
preexisting: previous?.preexisting === true || (previous === undefined && before !== null),
|
|
185
|
+
...(backupFile ? { backup: backupFile } : {}),
|
|
186
|
+
managed_sha256: contentHash(rendered),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, actions, records };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function updateMcp({ home, root }) {
|
|
193
|
+
const source = configSource({ home });
|
|
194
|
+
const target = expectedTarget({ root });
|
|
195
|
+
const current = source.servers[SERVER];
|
|
196
|
+
const currentTarget = serverTarget(current);
|
|
197
|
+
const owned = ownership({ home });
|
|
198
|
+
if (current && currentTarget !== target) {
|
|
199
|
+
// Only an exact previously-owned entry may be repaired. Any foreign
|
|
200
|
+
// command, even if it is also named dsh-crew, is a hard collision.
|
|
201
|
+
if (!(owned && owned.config_file === source.file && normalizePath(owned.target) === currentTarget)) {
|
|
202
|
+
return { ok: false, code: 'ZCODE_MCP_COLLISION', config_file: source.file };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const next = { ...source.config };
|
|
207
|
+
if (source.kind === 'native') next.mcp = { ...(next.mcp ?? {}), servers: { ...source.servers, [SERVER]: { command: 'node', args: [target] } } };
|
|
208
|
+
else next.mcpServers = { ...source.servers, [SERVER]: { command: 'node', args: [target] } };
|
|
209
|
+
const changed = JSON.stringify(next) !== JSON.stringify(source.config);
|
|
210
|
+
if (changed) { backup(source.file); writeJson(source.file, next); }
|
|
211
|
+
return { ok: true, changed, config_file: source.file, config_kind: source.kind, target };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function installZCode({ home = homedir(), root = ROOT } = {}) {
|
|
215
|
+
const templates = templateFiles({ home, root });
|
|
216
|
+
const missing = templates.find(({ source }) => !existsSync(source));
|
|
217
|
+
if (missing) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source: missing.source };
|
|
218
|
+
const priorFiles = ownership({ home })?.files ?? [];
|
|
219
|
+
// Check for collisions before writing any other integration surface so a
|
|
220
|
+
// failed install is transaction-like and leaves user files untouched.
|
|
221
|
+
const mcp = updateMcp({ home, root });
|
|
222
|
+
if (!mcp.ok) return mcp;
|
|
223
|
+
const policy = installPolicy({ home, root });
|
|
224
|
+
if (!policy.ok) return policy;
|
|
225
|
+
const installed = installTemplates({ home, root, priorFiles });
|
|
226
|
+
if (!installed.ok) return installed;
|
|
227
|
+
writeOwnership({
|
|
228
|
+
home,
|
|
229
|
+
configFile: mcp.config_file,
|
|
230
|
+
configKind: mcp.config_kind,
|
|
231
|
+
target: mcp.target,
|
|
232
|
+
files: installed.records,
|
|
233
|
+
});
|
|
234
|
+
return { ok: true, ...mcp, policy_file: policy.file, files: installed.actions };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
|
|
238
|
+
const expected = normalizePath(expectedTarget({ root }));
|
|
239
|
+
const source = configSource({ home });
|
|
240
|
+
const configured = serverTarget(source.servers[SERVER]);
|
|
241
|
+
const owned = ownership({ home });
|
|
242
|
+
const components = {
|
|
243
|
+
mcp: configured === expected,
|
|
244
|
+
policy: typeof readText(join(home, '.zcode', 'AGENTS.md')) === 'string' && readText(join(home, '.zcode', 'AGENTS.md')).includes(POLICY_START),
|
|
245
|
+
worker_agent: existsSync(join(home, '.zcode', 'agents', 'ds-worker.md')),
|
|
246
|
+
reviewer_agent: existsSync(join(home, '.zcode', 'agents', 'ds-reviewer.md')),
|
|
247
|
+
config_prompt: existsSync(join(home, '.zcode', 'commands', 'dsh-config.md')),
|
|
248
|
+
status_prompt: existsSync(join(home, '.zcode', 'commands', 'dsh-status.md')),
|
|
249
|
+
ownership: !!owned && owned.config_file === source.file && normalizePath(owned.target) === expected,
|
|
250
|
+
};
|
|
251
|
+
return { components, source, expected, configured, owned };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function zcodeStatus({ home = homedir(), root = ROOT } = {}) {
|
|
255
|
+
const { components, source, expected, configured } = zcodeComponents({ home, root });
|
|
256
|
+
const missing = Object.entries(components).filter(([, value]) => !value).map(([key]) => key);
|
|
257
|
+
const installed = Object.values(components).some(Boolean);
|
|
258
|
+
return {
|
|
259
|
+
installed,
|
|
260
|
+
ready: missing.length === 0,
|
|
261
|
+
components,
|
|
262
|
+
missing,
|
|
263
|
+
config_file: source.file,
|
|
264
|
+
config_kind: source.kind,
|
|
265
|
+
target: configured ?? expected,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function uninstallZCode({ home = homedir() } = {}) {
|
|
270
|
+
const actions = [];
|
|
271
|
+
const owned = ownership({ home });
|
|
272
|
+
if (owned) {
|
|
273
|
+
const file = owned.config_file;
|
|
274
|
+
const config = readJson(file, null);
|
|
275
|
+
if (config && typeof config === 'object') {
|
|
276
|
+
const kind = owned.config_kind === 'shared' ? 'shared' : 'native';
|
|
277
|
+
const servers = kind === 'shared' ? sharedServers(config) : nativeServers(config);
|
|
278
|
+
const current = servers[SERVER];
|
|
279
|
+
if (normalizePath(owned.target) === serverTarget(current)) {
|
|
280
|
+
const next = { ...config };
|
|
281
|
+
if (kind === 'shared') {
|
|
282
|
+
next.mcpServers = { ...servers }; delete next.mcpServers[SERVER];
|
|
283
|
+
if (Object.keys(next.mcpServers).length === 0) delete next.mcpServers;
|
|
284
|
+
} else {
|
|
285
|
+
next.mcp = { ...(next.mcp ?? {}), servers: { ...servers } }; delete next.mcp.servers[SERVER];
|
|
286
|
+
if (Object.keys(next.mcp.servers).length === 0) delete next.mcp.servers;
|
|
287
|
+
if (Object.keys(next.mcp).length === 0) delete next.mcp;
|
|
288
|
+
}
|
|
289
|
+
backup(file); writeJson(file, next); actions.push(`mcp: removed ${SERVER} from ${file}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
rmSync(OWNERSHIP_FILE({ home }), { force: true });
|
|
293
|
+
}
|
|
294
|
+
if (removePolicy({ home })) actions.push('policy: removed managed ZCode block');
|
|
295
|
+
const managedFiles = Array.isArray(owned?.files) && owned.files.length
|
|
296
|
+
? owned.files
|
|
297
|
+
: ['agents/ds-worker.md', 'agents/ds-reviewer.md', 'commands/dsh-config.md', 'commands/dsh-status.md']
|
|
298
|
+
.map((rel) => ({ path: join(home, '.zcode', rel) }));
|
|
299
|
+
for (const entry of managedFiles) {
|
|
300
|
+
const file = entry?.path;
|
|
301
|
+
if (!file || !existsSync(file)) continue;
|
|
302
|
+
const current = readText(file);
|
|
303
|
+
const unchanged = !entry.managed_sha256 || (current !== null && contentHash(current) === entry.managed_sha256);
|
|
304
|
+
if (!unchanged) continue;
|
|
305
|
+
if (entry.preexisting && entry.backup && existsSync(entry.backup)) {
|
|
306
|
+
copyFileSync(entry.backup, file);
|
|
307
|
+
actions.push(`restored: ${file}`);
|
|
308
|
+
} else {
|
|
309
|
+
rmSync(file, { force: true });
|
|
310
|
+
actions.push(`removed: ${file}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, actions };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function zcodeOwnershipFile({ home = homedir() } = {}) { return OWNERSHIP_FILE({ home }); }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
function explicitSource(value) {
|
|
4
|
+
if (typeof value !== 'string') return null;
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
return /^[a-z0-9][a-z0-9._-]{0,63}$/.test(normalized) ? normalized : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function classifyParent(value) {
|
|
10
|
+
const firstLine = String(value ?? '').split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
11
|
+
if (!firstLine) return 'unknown';
|
|
12
|
+
const command = firstLine.split(/[\\/]/).pop().toLowerCase().replace(/\.exe$/, '');
|
|
13
|
+
if (command.includes('claude')) return 'claude-code';
|
|
14
|
+
if (command.includes('codex')) return 'codex';
|
|
15
|
+
if (command.includes('zcode')) return 'zcode';
|
|
16
|
+
return /^[a-z0-9][a-z0-9._-]{0,63}$/.test(command) ? command : 'unknown';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readParentProcess({ platform, pid }) {
|
|
20
|
+
const options = {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
timeout: 2_000,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
};
|
|
26
|
+
if (platform === 'win32') {
|
|
27
|
+
return execFileSync('powershell.exe', [
|
|
28
|
+
'-NoLogo',
|
|
29
|
+
'-NoProfile',
|
|
30
|
+
'-NonInteractive',
|
|
31
|
+
'-Command',
|
|
32
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").Name`,
|
|
33
|
+
], options);
|
|
34
|
+
}
|
|
35
|
+
return execFileSync('ps', ['-o', 'comm=', '-p', String(pid)], options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function detectOrchestrator({
|
|
39
|
+
env = process.env,
|
|
40
|
+
platform = process.platform,
|
|
41
|
+
parentPid = process.ppid,
|
|
42
|
+
readParent = readParentProcess,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const explicit = explicitSource(env.DSH_ORCHESTRATOR);
|
|
45
|
+
if (explicit) return explicit;
|
|
46
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return 'claude-code';
|
|
47
|
+
if (!Number.isInteger(parentPid) || parentPid < 1) return 'unknown';
|
|
48
|
+
try {
|
|
49
|
+
return classifyParent(readParent({ platform, pid: parentPid }));
|
|
50
|
+
} catch {
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/readiness-matrix.mjs
CHANGED
|
@@ -26,9 +26,10 @@ const TARGET_ROWS = Object.freeze([
|
|
|
26
26
|
['linux_deterministic', 'ci'],
|
|
27
27
|
['windows_regressions', 'ci'],
|
|
28
28
|
['macos_smoke', 'ci'],
|
|
29
|
-
['hub_compatibility', 'live-runtime'],
|
|
30
|
-
['provider_catalog', 'live-runtime'],
|
|
31
|
-
['
|
|
29
|
+
['hub_compatibility', 'live-runtime'],
|
|
30
|
+
['provider_catalog', 'live-runtime'],
|
|
31
|
+
['model_execution', 'real-execution'],
|
|
32
|
+
['deepseek_flash', 'real-execution'],
|
|
32
33
|
['deepseek_pro', 'real-execution'],
|
|
33
34
|
['opencode_go_mimo_qwen', 'real-execution'],
|
|
34
35
|
['reviewer_pipeline', 'real-execution'],
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Keep this module pure and dependency-free so Hub, MCP and tests all use the
|
|
9
9
|
// exact same compatibility rules.
|
|
10
10
|
|
|
11
|
-
export const RUNTIME_VERSION = '0.5.
|
|
11
|
+
export const RUNTIME_VERSION = '0.5.3';
|
|
12
12
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
13
13
|
|
|
14
14
|
export const HUB_CAPABILITIES = Object.freeze([
|
package/src/server.mjs
CHANGED
|
@@ -26,8 +26,9 @@ import { buildReviewTask } from './information-flow.mjs';
|
|
|
26
26
|
import { projectWorkflowView } from './job-contracts.mjs';
|
|
27
27
|
import { loadRoleProfiles, resolveRoleProfile } from './role-profiles.mjs';
|
|
28
28
|
import { loadWorkspaceContexts, resolveWorkspaceContext, buildWorkspaceTask, addContextReferences, isSafeBranchName } from './workspace-context.mjs';
|
|
29
|
-
import { buildExtensionContract } from './extension-contract.mjs';
|
|
30
|
-
import { assessWorkspaceReadiness } from './workspace-readiness.mjs';
|
|
29
|
+
import { buildExtensionContract } from './extension-contract.mjs';
|
|
30
|
+
import { assessWorkspaceReadiness } from './workspace-readiness.mjs';
|
|
31
|
+
import { detectOrchestrator } from './orchestrator.mjs';
|
|
31
32
|
|
|
32
33
|
const server = new McpServer({ name: 'dsh-crew', version: RUNTIME_VERSION });
|
|
33
34
|
|
|
@@ -44,10 +45,11 @@ const workspaceSchema = z.object({
|
|
|
44
45
|
branch: z.string().refine(isSafeBranchName, 'invalid git branch name').optional(),
|
|
45
46
|
worktree: z.enum(['auto', 'existing', 'none']).optional(),
|
|
46
47
|
}).optional().describe('Per-job workspace overrides. auto isolates coding Workers; existing/none use the supplied workspace.');
|
|
47
|
-
const constraintsSchema = z.object({
|
|
48
|
-
timeout_seconds: z.number().int().positive().max(7200).optional(),
|
|
49
|
-
allow_fallback: z.boolean().optional(),
|
|
50
|
-
|
|
48
|
+
const constraintsSchema = z.object({
|
|
49
|
+
timeout_seconds: z.number().int().positive().max(7200).optional(),
|
|
50
|
+
allow_fallback: z.boolean().optional(),
|
|
51
|
+
allow_no_changes: z.boolean().optional().describe('Explicitly allow a verified read-only or analysis job to succeed with zero workspace changes.'),
|
|
52
|
+
}).optional().describe('Per-job constraints override profile and session defaults.');
|
|
51
53
|
|
|
52
54
|
// Session-level configuration. This MCP server process lives exactly as long
|
|
53
55
|
// as one Claude Code / Codex session, so plain memory IS session scope.
|
|
@@ -123,19 +125,7 @@ function text(obj) {
|
|
|
123
125
|
return { content: [{ type: 'text', text: typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2) }] };
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return 'claude-code';
|
|
128
|
-
try {
|
|
129
|
-
const { execSync } = require('node:child_process');
|
|
130
|
-
const comm = execSync(`ps -o comm= -p ${process.ppid}`, { encoding: 'utf8' }).trim().toLowerCase();
|
|
131
|
-
if (comm.includes('claude')) return 'claude-code';
|
|
132
|
-
if (comm.includes('codex')) return 'codex';
|
|
133
|
-
return comm.split('/').pop() || 'unknown';
|
|
134
|
-
} catch { return 'unknown'; }
|
|
135
|
-
}
|
|
136
|
-
import { createRequire } from 'node:module';
|
|
137
|
-
const require = createRequire(import.meta.url);
|
|
138
|
-
const ORCHESTRATOR = detectOrchestrator();
|
|
128
|
+
const ORCHESTRATOR = detectOrchestrator();
|
|
139
129
|
|
|
140
130
|
function policyRejection(decision) {
|
|
141
131
|
return text({
|
|
@@ -232,9 +222,10 @@ function prepareDispatch({ task, role, tier, legacy_tier, effort, cwd, timeout_s
|
|
|
232
222
|
profile_id: resolvedProfile.profile_id,
|
|
233
223
|
requested_isolation: requestedIsolation,
|
|
234
224
|
workspace_branch: workspaceOverride?.branch ?? withRefs.context?.default_branch ?? null,
|
|
235
|
-
timeout_seconds: effectiveTimeout,
|
|
236
|
-
allow_fallback: constraints?.allow_fallback ?? profileValue.fallback,
|
|
237
|
-
|
|
225
|
+
timeout_seconds: effectiveTimeout,
|
|
226
|
+
allow_fallback: constraints?.allow_fallback ?? profileValue.fallback,
|
|
227
|
+
allow_no_changes: constraints?.allow_no_changes === true,
|
|
228
|
+
routing: profileValue.routing,
|
|
238
229
|
review_strictness: profileValue.review_strictness,
|
|
239
230
|
workspace_context: withRefs.context,
|
|
240
231
|
},
|
|
@@ -333,10 +324,12 @@ async function buildConfigReport() {
|
|
|
333
324
|
const hubCompatibility = await hubStatus({ force: true });
|
|
334
325
|
let effectiveWorkerProvider = null;
|
|
335
326
|
let effectiveWorkerSelection = { flash: null, pro: null };
|
|
336
|
-
let providerResolutionError;
|
|
337
|
-
let providerCatalogChecked = false;
|
|
338
|
-
let providerCatalogBody = null;
|
|
339
|
-
|
|
327
|
+
let providerResolutionError;
|
|
328
|
+
let providerCatalogChecked = false;
|
|
329
|
+
let providerCatalogBody = null;
|
|
330
|
+
let hubJobsChecked = false;
|
|
331
|
+
let hubJobsBody = null;
|
|
332
|
+
const workerProviderMode = globalConfig.worker_provider_mode ?? 'deepseek-official';
|
|
340
333
|
if (workerProviderMode === 'deepseek-official') {
|
|
341
334
|
effectiveWorkerSelection = {
|
|
342
335
|
flash: { provider: 'deepseek-official', model: 'deepseek-v4-flash', source: 'legacy-strict' },
|
|
@@ -366,16 +359,27 @@ async function buildConfigReport() {
|
|
|
366
359
|
providerCatalogChecked = true;
|
|
367
360
|
providerResolutionError = err?.message ?? String(err);
|
|
368
361
|
}
|
|
369
|
-
} else if (hubCompatibility.reachable) {
|
|
370
|
-
providerResolutionError = hubCompatibilityMessage(hubCompatibility);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
362
|
+
} else if (hubCompatibility.reachable) {
|
|
363
|
+
providerResolutionError = hubCompatibilityMessage(hubCompatibility);
|
|
364
|
+
}
|
|
365
|
+
if (hubCompatibility.compatible) {
|
|
366
|
+
try {
|
|
367
|
+
hubJobsChecked = true;
|
|
368
|
+
const jobsRes = await fetch(`${globalConfig.hub_url}/_dsh/dsh-crew/jobs`, { signal: AbortSignal.timeout(800) });
|
|
369
|
+
hubJobsBody = await jobsRes.json();
|
|
370
|
+
} catch {
|
|
371
|
+
hubJobsChecked = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
effectiveWorkerProvider = effectiveWorkerSelection.flash?.provider ?? null;
|
|
375
|
+
const readinessMatrix = buildConfigReadinessMatrix({
|
|
374
376
|
hubCompatibility,
|
|
375
377
|
workerProviderMode,
|
|
376
|
-
providerCatalogChecked,
|
|
377
|
-
providerCatalogBody,
|
|
378
|
-
|
|
378
|
+
providerCatalogChecked,
|
|
379
|
+
providerCatalogBody,
|
|
380
|
+
hubJobsChecked,
|
|
381
|
+
hubJobsBody,
|
|
382
|
+
});
|
|
379
383
|
const roleProfiles = loadRoleProfiles();
|
|
380
384
|
const workspaceReadiness = await assessWorkspaceReadiness({ cwd: process.cwd() });
|
|
381
385
|
const extensionContract = buildExtensionContract({
|
package/src/workflow-runtime.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// appear here.
|
|
16
16
|
|
|
17
17
|
import { resolveModelPolicy, shouldAutoReview, getRoleState } from './policy.mjs';
|
|
18
|
-
import { buildOutcome, decideNextStep, JOB_PHASES, canTransition } from './workflow.mjs';
|
|
18
|
+
import { applyWorkspaceEvidence, buildOutcome, decideNextStep, JOB_PHASES, canTransition } from './workflow.mjs';
|
|
19
19
|
import { parseDeliveryReport } from './delivery.mjs';
|
|
20
20
|
import { classifyFailure } from './failure-classification.mjs';
|
|
21
21
|
import { createCanonicalJobEvent } from './job-contracts.mjs';
|
|
@@ -70,17 +70,6 @@ function mutationDetected(before, after) {
|
|
|
70
70
|
return after.fingerprint != null && before.fingerprint !== after.fingerprint;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function deliveryClaimsChanges(outcome) {
|
|
74
|
-
return Array.isArray(outcome?.changes) && outcome.changes.length > 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function workspaceEvidenceOK(outcome, candidate) {
|
|
78
|
-
if (!candidate) return true;
|
|
79
|
-
const hasChanges = Array.isArray(candidate.changed_files) && candidate.changed_files.length > 0;
|
|
80
|
-
if (outcome?.execution_status !== 'completed') return true;
|
|
81
|
-
return !deliveryClaimsChanges(outcome) || hasChanges;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
73
|
function sumUsage(attempts) {
|
|
85
74
|
const tokens = { input: 0, output: 0, reasoning: 0 };
|
|
86
75
|
for (const a of attempts) {
|
|
@@ -181,9 +170,10 @@ export function createWorkflowRuntime(adapters, {
|
|
|
181
170
|
requested_isolation: spec.requested_isolation ?? null,
|
|
182
171
|
workspace_branch: spec.workspace_branch ?? null,
|
|
183
172
|
timeout_seconds: spec.timeout_seconds ?? null,
|
|
184
|
-
profile_id: spec.profile_id ?? null,
|
|
185
|
-
allow_fallback: spec.allow_fallback !== false,
|
|
186
|
-
|
|
173
|
+
profile_id: spec.profile_id ?? null,
|
|
174
|
+
allow_fallback: spec.allow_fallback !== false,
|
|
175
|
+
allow_no_changes: spec.allow_no_changes === true,
|
|
176
|
+
routing: spec.routing ?? 'auto',
|
|
187
177
|
review_strictness: spec.review_strictness ?? null,
|
|
188
178
|
workspace_context: spec.workspace_context ? { ...spec.workspace_context } : null,
|
|
189
179
|
effort: spec.effort ?? 'max',
|
|
@@ -385,14 +375,17 @@ export function createWorkflowRuntime(adapters, {
|
|
|
385
375
|
failJob(job, Object.assign(new Error(ar.error ?? 'infrastructure failure'), { code: WORKFLOW_ERROR_CODES.ATTEMPT_INFRA_FAILURE }));
|
|
386
376
|
return;
|
|
387
377
|
}
|
|
388
|
-
|
|
378
|
+
let outcome = ar.outcome ?? buildOutcome({
|
|
389
379
|
result: ar.result ?? '',
|
|
390
380
|
stopReason: ar.stopReason,
|
|
391
381
|
executionStatus: ar.status === 'done' ? 'completed' : 'failed',
|
|
392
382
|
});
|
|
393
383
|
transition(job, JOB_PHASES.VERIFYING, `attempt ${attempt} complete`);
|
|
394
384
|
|
|
395
|
-
|
|
385
|
+
let workspaceEvidenceAvailable = false;
|
|
386
|
+
let workspaceHasChanges = false;
|
|
387
|
+
|
|
388
|
+
// Capture the latest candidate after every attempt. Capture failure is
|
|
396
389
|
// not allowed to delete the only recoverable state: retain the worktree
|
|
397
390
|
// and preserve the worker business result, while surfacing the warning.
|
|
398
391
|
if (alloc?.ok && alloc.isolation === 'worktree') {
|
|
@@ -402,17 +395,23 @@ export function createWorkflowRuntime(adapters, {
|
|
|
402
395
|
job.retain_workspace = true;
|
|
403
396
|
job.candidate = null;
|
|
404
397
|
job.events.push({ at: clock(), phase: job.phase, type: 'candidate/failed', attempt, message: 'candidate capture failed; worktree retained for recovery' });
|
|
405
|
-
} else {
|
|
406
|
-
job.candidate = candidate;
|
|
407
|
-
attemptView.candidate_fingerprint = candidate.fingerprint ?? null;
|
|
408
|
-
|
|
409
|
-
|
|
398
|
+
} else {
|
|
399
|
+
job.candidate = candidate;
|
|
400
|
+
attemptView.candidate_fingerprint = candidate.fingerprint ?? null;
|
|
401
|
+
workspaceEvidenceAvailable = true;
|
|
402
|
+
workspaceHasChanges = Array.isArray(candidate.changed_files) && candidate.changed_files.length > 0;
|
|
403
|
+
if (candidate.complete === false || candidate.replayable === false) {
|
|
410
404
|
job.retain_workspace = true;
|
|
411
405
|
job.events.push({ at: clock(), phase: job.phase, type: 'candidate/incomplete', attempt, message: 'candidate is not fully replayable; worktree retained' });
|
|
412
406
|
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
outcome = applyWorkspaceEvidence(outcome, {
|
|
410
|
+
evidenceAvailable: workspaceEvidenceAvailable,
|
|
411
|
+
hasChanges: workspaceHasChanges,
|
|
412
|
+
allowNoChanges: job.allow_no_changes,
|
|
413
|
+
});
|
|
414
|
+
job.outcome = outcome;
|
|
416
415
|
|
|
417
416
|
const reviewRequested = !isReviewJob && shouldAutoReview(config);
|
|
418
417
|
const reviewerAuto = getRoleState(config, 'reviewer') !== 'disabled';
|
|
@@ -580,8 +579,9 @@ export function createWorkflowRuntime(adapters, {
|
|
|
580
579
|
current_model: job.attempts[job.attempts.length - 1]?.model ?? null,
|
|
581
580
|
model_class_hint: job.model_class_hint,
|
|
582
581
|
source: job.source,
|
|
583
|
-
profile_id: job.profile_id,
|
|
584
|
-
|
|
582
|
+
profile_id: job.profile_id,
|
|
583
|
+
allow_no_changes: job.allow_no_changes,
|
|
584
|
+
routing: job.routing,
|
|
585
585
|
workspace_context: job.workspace_context ? { ...job.workspace_context } : null,
|
|
586
586
|
workspace_branch: job.workspace_branch,
|
|
587
587
|
timeout_seconds: job.timeout_seconds,
|