@ran-sh/dsh-crew 0.3.0

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 (85) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +8 -0
  3. package/.mcp.json +8 -0
  4. package/LICENSE +21 -0
  5. package/README.de.md +359 -0
  6. package/README.es.md +359 -0
  7. package/README.fr.md +359 -0
  8. package/README.hi.md +359 -0
  9. package/README.id.md +359 -0
  10. package/README.ja.md +359 -0
  11. package/README.ko.md +359 -0
  12. package/README.md +360 -0
  13. package/README.pt.md +359 -0
  14. package/README.ru.md +359 -0
  15. package/README.th.md +359 -0
  16. package/README.tr.md +359 -0
  17. package/README.vi.md +359 -0
  18. package/README.zh-TW.md +359 -0
  19. package/README.zh.md +305 -0
  20. package/agents/ds-flash.md +26 -0
  21. package/agents/ds-pro.md +32 -0
  22. package/agents/ds-reviewer.md +23 -0
  23. package/agents/ds-worker.md +22 -0
  24. package/codex/agents/ds-flash.toml +30 -0
  25. package/codex/agents/ds-pro.toml +31 -0
  26. package/codex/agents/ds-reviewer.toml +28 -0
  27. package/codex/agents/ds-worker.toml +28 -0
  28. package/codex/prompts/dsh-config.md +3 -0
  29. package/codex/prompts/dsh-status.md +1 -0
  30. package/commands/config.md +11 -0
  31. package/commands/off.md +5 -0
  32. package/commands/on.md +5 -0
  33. package/commands/status.md +5 -0
  34. package/cordis.patch.yml +4 -0
  35. package/docs/images/dsh-crew-host.png +0 -0
  36. package/docs/images/dsh-crew-jobs.png +0 -0
  37. package/docs/images/dsh-crew-logo.png +0 -0
  38. package/docs/images/dsh-crew-overview.png +0 -0
  39. package/lib/client.js +2765 -0
  40. package/package.json +125 -0
  41. package/scripts/build-client.mjs +28 -0
  42. package/scripts/live-crew-smoke.mjs +39 -0
  43. package/scripts/live-policy-matrix.mjs +177 -0
  44. package/scripts/policy-probe.mjs +101 -0
  45. package/scripts/setup.mjs +294 -0
  46. package/scripts/smoke-real.mjs +110 -0
  47. package/scripts/smoke.mjs +78 -0
  48. package/scripts/verify-installer-fix.mjs +26 -0
  49. package/src/adaptive-routing.mjs +260 -0
  50. package/src/client/activation-summary.tsx +64 -0
  51. package/src/client/entry.tsx +236 -0
  52. package/src/client/index.tsx +1120 -0
  53. package/src/config-readiness.mjs +59 -0
  54. package/src/delivery.mjs +205 -0
  55. package/src/dsh-cli-runtime.mjs +251 -0
  56. package/src/failure-classification.mjs +172 -0
  57. package/src/hub/entry.mjs +98 -0
  58. package/src/hub/index.mjs +757 -0
  59. package/src/hub-client.mjs +132 -0
  60. package/src/hub-compatibility.mjs +49 -0
  61. package/src/i18n.mjs +19 -0
  62. package/src/install/cli.mjs +28 -0
  63. package/src/install/install-legacy.mjs +460 -0
  64. package/src/install/install.mjs +451 -0
  65. package/src/jobs.mjs +275 -0
  66. package/src/mcp-runtime.mjs +257 -0
  67. package/src/model-catalog.mjs +173 -0
  68. package/src/model-routing.mjs +391 -0
  69. package/src/multimodal.mjs +0 -0
  70. package/src/policy-legacy.mjs +830 -0
  71. package/src/policy.mjs +197 -0
  72. package/src/readiness-matrix.mjs +169 -0
  73. package/src/runtime-controls.mjs +90 -0
  74. package/src/runtime-identity.mjs +108 -0
  75. package/src/server.mjs +477 -0
  76. package/src/status-shard.mjs +52 -0
  77. package/src/structured-error-code.mjs +39 -0
  78. package/src/vision-route.mjs +138 -0
  79. package/src/workflow-runtime.mjs +567 -0
  80. package/src/workflow.mjs +160 -0
  81. package/src/workspace-audit.mjs +231 -0
  82. package/src/workspace-isolation.mjs +306 -0
  83. package/statusline/statusline.sh +14 -0
  84. package/statusline/worker-segment.sh +35 -0
  85. package/worker.cordis.yml +77 -0
package/src/server.mjs ADDED
@@ -0,0 +1,477 @@
1
+ // MCP stdio server exposing the DSH worker pool to Claude Code / Codex.
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+ import { startJob, waitJob, cancelJob, listJobs, getJob, jobView } from './jobs.mjs';
7
+ import { hubStatus, hub } from './hub-client.mjs';
8
+ import { hubCompatibilityMessage, resolveHubExecutionMode } from './hub-compatibility.mjs';
9
+ import { RUNTIME_VERSION } from './runtime-identity.mjs';
10
+ import { resolveWorkerModel } from './model-routing.mjs';
11
+ import { runtimeActivationMetadata } from './runtime-controls.mjs';
12
+ import { buildConfigReadinessMatrix } from './config-readiness.mjs';
13
+ import { classifyFailure, classifyFailureCode } from './failure-classification.mjs';
14
+ import {
15
+ normalizeGlobalConfig,
16
+ deriveLegacyConfig,
17
+ getEffectiveTierState,
18
+ getRoutingGuidance,
19
+ chooseDefaultTier,
20
+ canDispatchRole,
21
+ resolveRoleTierHint,
22
+ shouldAutoReview,
23
+ } from './policy.mjs';
24
+ import { buildMcpWorkflowRuntime } from './mcp-runtime.mjs';
25
+
26
+ const server = new McpServer({ name: 'dsh-crew', version: RUNTIME_VERSION });
27
+
28
+ const tierSchema = z.enum(['flash', 'pro']).optional().describe('Legacy worker tier (compatibility only): Flash/Pro now act as a model-class hint, not a role. Prefer role=worker / role=reviewer; the backend resolves the actual provider/model from the Model Policy.');
29
+ const roleSchema = z.enum(['worker', 'reviewer']).optional().describe('Dispatch role: worker executes implementation / fixes / tests / search; reviewer independently reviews a completed implementation. A coding request defaults to worker; reviewer runs on explicit request or via the automatic review workflow.');
30
+ const effortSchema = z.enum(['off', 'high', 'max']).optional().describe('Reasoning effort for the worker. Omit to use the session default.');
31
+
32
+ // Session-level configuration. This MCP server process lives exactly as long
33
+ // as one Claude Code / Codex session, so plain memory IS session scope.
34
+ // Initial values come from the global config (~/.config/dsh-crew/config.json,
35
+ // edited on the DSH settings page); dsh_worker_config overrides per session.
36
+ // All dispatch decisions (dsh_run_worker AND dsh_spawn_worker, hub AND
37
+ // standalone) go through the same policy resolver in src/policy.mjs.
38
+ import { readGlobalConfig } from './install/install.mjs';
39
+ const initialGlobalConfig = normalizeGlobalConfig(readGlobalConfig());
40
+ const legacyDefaults = deriveLegacyConfig(initialGlobalConfig);
41
+ const currentGlobalConfig = () => normalizeGlobalConfig(readGlobalConfig());
42
+ const sessionConfig = {
43
+ enabled: true,
44
+ default_tier: legacyDefaults.default_tier,
45
+ default_effort: legacyDefaults.default_effort,
46
+ mode: legacyDefaults.mode, // auto | hub | standalone
47
+ default_timeout_seconds: legacyDefaults.default_timeout_seconds,
48
+ tier_policy: undefined,
49
+ escalate_on_failure: legacyDefaults.escalate_on_failure,
50
+ preset_flash: legacyDefaults.preset_flash ?? 'default',
51
+ preset_pro: legacyDefaults.preset_pro ?? 'default',
52
+ collaboration_mode: undefined,
53
+ main_agent_mode: undefined,
54
+ flash_state: undefined,
55
+ pro_state: undefined,
56
+ pro_reviews_flash: undefined,
57
+ };
58
+
59
+ function resetSessionConfig() {
60
+ Object.assign(sessionConfig, {
61
+ enabled: true,
62
+ default_tier: legacyDefaults.default_tier,
63
+ default_effort: legacyDefaults.default_effort,
64
+ mode: legacyDefaults.mode,
65
+ default_timeout_seconds: legacyDefaults.default_timeout_seconds,
66
+ tier_policy: undefined,
67
+ escalate_on_failure: legacyDefaults.escalate_on_failure,
68
+ preset_flash: legacyDefaults.preset_flash ?? 'default',
69
+ preset_pro: legacyDefaults.preset_pro ?? 'default',
70
+ collaboration_mode: undefined,
71
+ main_agent_mode: undefined,
72
+ flash_state: undefined,
73
+ pro_state: undefined,
74
+ pro_reviews_flash: undefined,
75
+ });
76
+ }
77
+
78
+ function presetForTier(tier) {
79
+ const p = tier === 'flash' ? sessionConfig.preset_flash : sessionConfig.preset_pro;
80
+ return !p || p === 'default' ? undefined : p;
81
+ }
82
+
83
+ function text(obj) {
84
+ let payload = obj;
85
+ if (obj && typeof obj === 'object' && !Array.isArray(obj) && obj.failure === undefined) {
86
+ if (obj.code) {
87
+ payload = { ...obj, failure: classifyFailureCode(obj.code) };
88
+ } else if (obj.status !== undefined || obj.phase !== undefined || obj.outcome !== undefined || obj.error_code !== undefined) {
89
+ payload = {
90
+ ...obj,
91
+ failure: classifyFailure({
92
+ phase: obj.phase,
93
+ status: obj.status,
94
+ errorCode: obj.error_code,
95
+ outcome: obj.outcome,
96
+ decision: obj.decision,
97
+ review: obj.review,
98
+ childAttempts: obj.child_attempts,
99
+ }),
100
+ };
101
+ }
102
+ }
103
+ return { content: [{ type: 'text', text: typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2) }] };
104
+ }
105
+
106
+ function detectOrchestrator() {
107
+ if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return 'claude-code';
108
+ try {
109
+ const { execSync } = require('node:child_process');
110
+ const comm = execSync(`ps -o comm= -p ${process.ppid}`, { encoding: 'utf8' }).trim().toLowerCase();
111
+ if (comm.includes('claude')) return 'claude-code';
112
+ if (comm.includes('codex')) return 'codex';
113
+ return comm.split('/').pop() || 'unknown';
114
+ } catch { return 'unknown'; }
115
+ }
116
+ import { createRequire } from 'node:module';
117
+ const require = createRequire(import.meta.url);
118
+ const ORCHESTRATOR = detectOrchestrator();
119
+
120
+ function policyRejection(decision) {
121
+ return text({
122
+ error: decision.error.message,
123
+ code: decision.error.policyCode,
124
+ note: 'DSH Crew policy rejection — report this to the user instead of doing the task yourself.',
125
+ });
126
+ }
127
+
128
+ function dispatchDisabled() {
129
+ return text({
130
+ error: 'worker dispatch is disabled for this session (set via dsh_worker_config). Report this to the user instead of doing the task yourself.',
131
+ code: 'SUBAGENTS_DISABLED',
132
+ });
133
+ }
134
+
135
+ async function resolveMode() {
136
+ const status = await hubStatus();
137
+ const decision = resolveHubExecutionMode(sessionConfig.mode, status);
138
+ if (!decision.ok) {
139
+ throw Object.assign(new Error(decision.error), {
140
+ code: decision.code,
141
+ hubStatus: status,
142
+ });
143
+ }
144
+ return decision.mode;
145
+ }
146
+
147
+ /** Reviewer prompt built only from structured outcome + sanitized candidate. */
148
+ function buildReviewTask(task, view) {
149
+ const parts = [
150
+ 'You are the automatic reviewer of a completed worker implementation. REVIEW ONLY: inspect the candidate and report findings. Do NOT modify any files unless the user explicitly asks for fixes.',
151
+ '',
152
+ 'Original task:',
153
+ task,
154
+ ];
155
+ const o = view?.outcome;
156
+ if (o) {
157
+ parts.push('', 'Worker outcome:');
158
+ parts.push(`task_status=${o.task_status} tests_status=${o.tests_status ?? 'none'} delivery=${o.delivery?.complete ? 'complete' : 'incomplete'}`);
159
+ }
160
+ const c = view?.candidate;
161
+ if (c) {
162
+ parts.push('', 'Candidate changed files:', Array.isArray(c.changed_files) && c.changed_files.length ? c.changed_files.join('\n') : '(none)');
163
+ if (c.base_revision) parts.push(`Base revision: ${c.base_revision}`);
164
+ if (c.patch) parts.push('', 'Candidate patch (sanitized):', String(c.patch).slice(0, 8000));
165
+ }
166
+ parts.push('', 'Report: 1) does the implementation satisfy the task, 2) concrete issues (bugs, style, risks), 3) suggested fixes. End your message with ## Review Findings / ## Evidence / ## Risks / ## Verdict (approved | needs changes | rejected).');
167
+ return parts.join('\n');
168
+ }
169
+
170
+ const workflowRuntime = buildMcpWorkflowRuntime({
171
+ getSessionConfig: () => sessionConfig,
172
+ resolveMode,
173
+ presetForTier,
174
+ readGlobalConfig,
175
+ buildReviewTask,
176
+ attemptTimeoutMs: () => (sessionConfig.default_timeout_seconds ?? 1800) * 1000,
177
+ });
178
+
179
+ server.registerTool('dsh_run_worker', {
180
+ title: 'Run DSH worker (blocking)',
181
+ description: 'Delegate a task to a DSH (DeepSeek Harness) coding agent and wait for its final result. The worker is a full DSH agent with its own tools and sandbox. Pass role=worker for implementation and role=reviewer for an independent review pass. Disabled roles are refused by the DSH Crew policy; Manual roles only run when explicitly requested. Blocks until the worker finishes.',
182
+ inputSchema: {
183
+ task: z.string().describe('Full task description for the worker, self-contained'),
184
+ role: roleSchema,
185
+ tier: tierSchema,
186
+ legacy_tier: tierSchema.describe('Legacy model-class hint (flash | pro), forwarded verbatim by deprecated ds-flash/ds-pro aliases; only influences which model class backs a worker role, never the role gate'),
187
+ effort: effortSchema,
188
+ cwd: z.string().optional().describe('Workspace directory for the worker (defaults to current project)'),
189
+ timeout_seconds: z.number().int().positive().max(7200).optional(),
190
+ },
191
+ }, async ({ task, role, tier, legacy_tier, effort, cwd, timeout_seconds }) => {
192
+ if (!sessionConfig.enabled) return dispatchDisabled();
193
+ const globalConfig = currentGlobalConfig();
194
+ const hint = resolveRoleTierHint(role, legacy_tier ?? tier);
195
+ if (!hint.ok) return text({ error: hint.error, code: hint.code, note: 'Resolve the conflict on the caller side: pass only role, or only tier.' });
196
+ const effRole = hint.role;
197
+ let effTier;
198
+ let decision;
199
+ if (role === undefined) {
200
+ decision = chooseDefaultTier(globalConfig, legacy_tier ?? tier, sessionConfig);
201
+ if (!decision.ok) return policyRejection(decision);
202
+ effTier = decision.tier;
203
+ } else {
204
+ decision = canDispatchRole(globalConfig, effRole, true, sessionConfig);
205
+ if (!decision.ok) return policyRejection(decision);
206
+ if (effRole === 'reviewer') effTier = 'pro';
207
+ else if (legacy_tier !== undefined) effTier = legacy_tier;
208
+ else if (tier !== undefined) effTier = tier;
209
+ else { const slot = chooseDefaultTier(globalConfig, undefined, sessionConfig); effTier = slot.ok ? slot.tier : 'flash'; }
210
+ }
211
+ const workDir = cwd ?? process.cwd();
212
+ const e = effort ?? sessionConfig.default_effort;
213
+ const timeout = timeout_seconds ?? sessionConfig.default_timeout_seconds;
214
+
215
+ const spec = {
216
+ role: effRole,
217
+ delivery: effRole === 'reviewer' ? 'review' : 'coding',
218
+ model_class_hint: effTier,
219
+ task,
220
+ cwd: workDir,
221
+ effort: e,
222
+ source: ORCHESTRATOR,
223
+ };
224
+ const wf = workflowRuntime.start(spec);
225
+ await workflowRuntime.wait(wf.id, timeout * 1000);
226
+ const view = workflowRuntime.get(wf.id, { withResult: true });
227
+ if (view.status === 'running') {
228
+ return text({ ...view, note: `still running after ${timeout}s; poll with dsh_worker_result`, status: 'running' });
229
+ }
230
+ if (view.phase === 'failed') {
231
+ return text({ ...view, note: 'workflow failed — see error / error_code.', status: 'failed' });
232
+ }
233
+ return text({ ...view, note: effRole === 'reviewer' ? 'review complete' : 'workflow complete' });
234
+ });
235
+
236
+ server.registerTool('dsh_worker_config', {
237
+ title: 'Session worker configuration',
238
+ description: 'Read or update session-level worker settings: enable/disable dispatch, default tier/effort/timeout, execution mode, tier policy, escalation, collaboration mode, per-tier state and review behavior. Call with no arguments to read the current configuration, runtime control state, activation boundaries, global defaults, session overrides, effective policy and routing guidance. Settings last for this session only.',
239
+ inputSchema: {
240
+ enabled: z.boolean().optional().describe('false = refuse all worker dispatch this session'),
241
+ default_tier: z.enum(['flash', 'pro']).optional(),
242
+ default_effort: z.enum(['off', 'high', 'max']).optional(),
243
+ mode: z.enum(['auto', 'hub', 'standalone']).optional(),
244
+ default_timeout_seconds: z.number().int().positive().max(7200).optional(),
245
+ tier_policy: z.enum(['auto', 'flash-only', 'pro-only']).optional().describe('session hard clamp: flash-only / pro-only pin every dispatch to one tier'),
246
+ escalate_on_failure: z.boolean().optional().describe('allow an unverified worker attempt to retry through the stronger model policy (applies to run and spawn)'),
247
+ preset_flash: z.string().optional().describe('hub-mode agent preset for flash workers (preset id, or "default")'),
248
+ preset_pro: z.string().optional().describe('hub-mode agent preset for pro workers (preset id, or "default")'),
249
+ collaboration_mode: z.enum(['flash-only', 'pro-only', 'balanced', 'review-pipeline', 'custom']).optional().describe('session override of the global collaboration mode'),
250
+ main_agent_mode: z.enum(['direct-allowed', 'coordinator-first', 'dispatcher-only']).optional().describe('session override of the host routing guidance'),
251
+ flash_state: z.enum(['disabled', 'manual', 'auto']).optional().describe('session override of the flash tier state (custom mode)'),
252
+ pro_state: z.enum(['disabled', 'manual', 'auto']).optional().describe('session override of the pro tier state (custom mode)'),
253
+ pro_reviews_flash: z.boolean().optional().describe('enable the automatic reviewer after a verified worker workflow (applies to run and spawn)'),
254
+ reset: z.boolean().optional().describe('true = restore all defaults first'),
255
+ },
256
+ }, async ({ reset, ...patch }) => {
257
+ if (reset) resetSessionConfig();
258
+ if (patch.tier_policy === 'auto') {
259
+ sessionConfig.tier_policy = undefined;
260
+ sessionConfig.collaboration_mode = 'balanced';
261
+ delete patch.tier_policy;
262
+ }
263
+ for (const [k, v] of Object.entries(patch)) if (v !== undefined) sessionConfig[k] = v;
264
+ return text(await buildConfigReport());
265
+ });
266
+
267
+ const SAFE_GLOBAL_KEYS = [
268
+ 'default_tier', 'default_effort', 'mode', 'default_timeout_seconds', 'hub_url',
269
+ 'tier_policy', 'escalate_on_failure', 'subagents_enabled', 'collaboration_mode',
270
+ 'main_agent_mode', 'flash_state', 'pro_state', 'flash_roles', 'pro_roles',
271
+ 'pro_reviews_flash', 'worker_provider_mode', 'vision_enabled', 'imagegen_enabled',
272
+ 'flash_model_priority', 'flash_model_priority_configured', 'flash_model_fallback',
273
+ 'pro_model_priority', 'pro_model_priority_configured', 'pro_model_fallback',
274
+ 'vision_provider', 'vision_model', 'imagegen_provider',
275
+ 'preset_flash', 'preset_pro',
276
+ ];
277
+
278
+ async function buildConfigReport() {
279
+ const globalConfig = currentGlobalConfig();
280
+ const legacy = deriveLegacyConfig(globalConfig);
281
+ const flashState = getEffectiveTierState(globalConfig, 'flash', sessionConfig);
282
+ const proState = getEffectiveTierState(globalConfig, 'pro', sessionConfig);
283
+ const defaultDecision = chooseDefaultTier(globalConfig, undefined, sessionConfig);
284
+ const overrides = {};
285
+ for (const [k, v] of Object.entries(sessionConfig)) if (v !== undefined) overrides[k] = v;
286
+ const collaborationMode = sessionConfig.collaboration_mode ?? globalConfig.collaboration_mode;
287
+ const mainAgentMode = sessionConfig.main_agent_mode ?? globalConfig.main_agent_mode;
288
+ const runtimeControls = workflowRuntime.refreshRuntimeControls();
289
+ const activationBoundaries = globalConfig.config_activation ?? runtimeActivationMetadata();
290
+ const hubCompatibility = await hubStatus({ force: true });
291
+ let effectiveWorkerProvider = null;
292
+ let effectiveWorkerSelection = { flash: null, pro: null };
293
+ let providerResolutionError;
294
+ let providerCatalogChecked = false;
295
+ let providerCatalogBody = null;
296
+ const workerProviderMode = globalConfig.worker_provider_mode ?? 'deepseek-official';
297
+ if (workerProviderMode === 'deepseek-official') {
298
+ effectiveWorkerSelection = {
299
+ flash: { provider: 'deepseek-official', model: 'deepseek-v4-flash', source: 'legacy-strict' },
300
+ pro: { provider: 'deepseek-official', model: 'deepseek-v4-pro', source: 'legacy-strict' },
301
+ };
302
+ } else if (hubCompatibility.compatible) {
303
+ try {
304
+ providerCatalogChecked = true;
305
+ const res = await fetch(`${globalConfig.hub_url}/_dsh/dsh-crew/models`);
306
+ const body = await res.json();
307
+ providerCatalogBody = body;
308
+ if (!body?.ok) providerResolutionError = body?.error ?? 'Unable to read Harness model catalog.';
309
+ else for (const tier of ['flash', 'pro']) {
310
+ const selected = resolveWorkerModel({
311
+ tier,
312
+ priority: globalConfig[`${tier}_model_priority`],
313
+ priorityConfigured: globalConfig[`${tier}_model_priority_configured`],
314
+ fallback: globalConfig[`${tier}_model_fallback`],
315
+ catalog: body,
316
+ harnessDefault: body.harness_default,
317
+ });
318
+ effectiveWorkerSelection[tier] = selected.ok
319
+ ? { provider: selected.provider, model: selected.model, source: selected.source }
320
+ : { code: selected.code, error: selected.message };
321
+ }
322
+ } catch (err) {
323
+ providerCatalogChecked = true;
324
+ providerResolutionError = err?.message ?? String(err);
325
+ }
326
+ } else if (hubCompatibility.reachable) {
327
+ providerResolutionError = hubCompatibilityMessage(hubCompatibility);
328
+ }
329
+ effectiveWorkerProvider = effectiveWorkerSelection.flash?.provider ?? null;
330
+ const readinessMatrix = buildConfigReadinessMatrix({
331
+ hubCompatibility,
332
+ workerProviderMode,
333
+ providerCatalogChecked,
334
+ providerCatalogBody,
335
+ });
336
+ return {
337
+ enabled: sessionConfig.enabled,
338
+ default_tier: sessionConfig.default_tier ?? legacy.default_tier,
339
+ default_effort: sessionConfig.default_effort ?? legacy.default_effort,
340
+ mode: sessionConfig.mode ?? legacy.mode,
341
+ default_timeout_seconds: sessionConfig.default_timeout_seconds ?? legacy.default_timeout_seconds,
342
+ tier_policy: sessionConfig.tier_policy ?? legacy.tier_policy,
343
+ escalate_on_failure: sessionConfig.escalate_on_failure ?? legacy.escalate_on_failure,
344
+ preset_flash: sessionConfig.preset_flash ?? legacy.preset_flash,
345
+ preset_pro: sessionConfig.preset_pro ?? legacy.preset_pro,
346
+ worker_provider_mode: workerProviderMode,
347
+ effective_worker_provider: effectiveWorkerProvider,
348
+ effective_worker_selection: effectiveWorkerSelection,
349
+ provider_resolution_error: providerResolutionError,
350
+ flash_model_priority: globalConfig.flash_model_priority ?? [],
351
+ flash_model_fallback: globalConfig.flash_model_fallback ?? 'harness-default',
352
+ pro_model_priority: globalConfig.pro_model_priority ?? [],
353
+ pro_model_fallback: globalConfig.pro_model_fallback ?? 'harness-default',
354
+ subagents_enabled: sessionConfig.enabled !== false && globalConfig.subagents_enabled !== false,
355
+ collaboration_mode: collaborationMode,
356
+ main_agent_mode: mainAgentMode,
357
+ flash_state: flashState,
358
+ pro_state: proState,
359
+ flash_roles: globalConfig.flash_roles ?? [],
360
+ pro_roles: globalConfig.pro_roles ?? [],
361
+ pro_reviews_flash: sessionConfig.pro_reviews_flash ?? shouldAutoReview(globalConfig, sessionConfig),
362
+ effective_policy: `mode=${collaborationMode} flash=${flashState} pro=${proState} subagents=${sessionConfig.enabled !== false && globalConfig.subagents_enabled !== false}`,
363
+ legacy_source: sessionConfig.tier_policy !== undefined
364
+ ? `session tier_policy clamp (${sessionConfig.tier_policy})`
365
+ : `global collaboration mode (${collaborationMode})`,
366
+ effective_default_tier: defaultDecision.ok ? defaultDecision.tier : null,
367
+ effective_default_tier_reason: defaultDecision.ok ? defaultDecision.guidance : (defaultDecision.error.policyCode ?? 'none'),
368
+ routing_guidance: getRoutingGuidance(globalConfig, sessionConfig),
369
+ session_overrides: overrides,
370
+ global_defaults: Object.fromEntries(SAFE_GLOBAL_KEYS.map((k) => [k, globalConfig[k]])),
371
+ runtime_controls: runtimeControls,
372
+ activation_boundaries: activationBoundaries,
373
+ readiness_matrix: readinessMatrix,
374
+ hub_reachable: hubCompatibility.reachable,
375
+ hub_compatible: hubCompatibility.compatible,
376
+ hub_compatibility: hubCompatibility,
377
+ };
378
+ }
379
+
380
+ server.registerTool('dsh_spawn_worker', {
381
+ title: 'Spawn DSH worker (async)',
382
+ description: 'Start a DSH (DeepSeek Harness) coding workflow in the background and return immediately with a workflow id. Use dsh_worker_status / dsh_worker_result to follow up. Role policy is enforced exactly like dsh_run_worker, and the workflow (verification, escalation, automatic reviewer pass) runs exactly the same for async jobs — only the caller does not await.',
383
+ inputSchema: {
384
+ task: z.string(),
385
+ role: roleSchema,
386
+ tier: tierSchema,
387
+ legacy_tier: tierSchema.describe('Legacy model-class hint (flash | pro) — only influences the model class backing a worker role, never the role gate'),
388
+ effort: effortSchema,
389
+ cwd: z.string().optional(),
390
+ },
391
+ }, async ({ task, role, tier, legacy_tier, effort, cwd }) => {
392
+ if (!sessionConfig.enabled) return dispatchDisabled();
393
+ const globalConfig = currentGlobalConfig();
394
+ const hint = resolveRoleTierHint(role, legacy_tier ?? tier);
395
+ if (!hint.ok) return text({ error: hint.error, code: hint.code, note: 'Resolve the conflict on the caller side: pass only role, or only tier.' });
396
+ const effRole = hint.role;
397
+ let effTier;
398
+ let decision;
399
+ if (role === undefined) {
400
+ decision = chooseDefaultTier(globalConfig, legacy_tier ?? tier, sessionConfig);
401
+ if (!decision.ok) return policyRejection(decision);
402
+ effTier = decision.tier;
403
+ } else {
404
+ decision = canDispatchRole(globalConfig, effRole, true, sessionConfig);
405
+ if (!decision.ok) return policyRejection(decision);
406
+ if (effRole === 'reviewer') effTier = 'pro';
407
+ else if (legacy_tier !== undefined) effTier = legacy_tier;
408
+ else if (tier !== undefined) effTier = tier;
409
+ else { const slot = chooseDefaultTier(globalConfig, undefined, sessionConfig); effTier = slot.ok ? slot.tier : 'flash'; }
410
+ }
411
+ const workDir = cwd ?? process.cwd();
412
+ const e = effort ?? sessionConfig.default_effort;
413
+ const spec = {
414
+ role: effRole,
415
+ delivery: effRole === 'reviewer' ? 'review' : 'coding',
416
+ model_class_hint: effTier,
417
+ task,
418
+ cwd: workDir,
419
+ effort: e,
420
+ source: ORCHESTRATOR,
421
+ };
422
+ const wf = workflowRuntime.start(spec);
423
+ return text({ ...workflowRuntime.get(wf.id), workflow_id: wf.id, note: 'started in background; poll with dsh_worker_status / dsh_worker_result' });
424
+ });
425
+
426
+ server.registerTool('dsh_worker_status', {
427
+ title: 'DSH worker status',
428
+ description: 'List all DSH worker workflows in this session with phase, role, attempt, current model and token usage.',
429
+ inputSchema: {},
430
+ }, async () => {
431
+ return text(workflowRuntime.list());
432
+ });
433
+
434
+ server.registerTool('dsh_worker_result', {
435
+ title: 'DSH worker result',
436
+ description: 'Fetch the result of a worker workflow, optionally waiting for it to finish. Accepts workflow ids (wf-...), Hub attempt ids (hub-...) and legacy standalone ids (job-...).',
437
+ inputSchema: {
438
+ job_id: z.string(),
439
+ wait_seconds: z.number().int().min(0).max(7200).default(0).describe('0 = return current state immediately'),
440
+ },
441
+ }, async ({ job_id, wait_seconds }) => {
442
+ if (job_id.startsWith('wf-')) {
443
+ if (wait_seconds > 0) await workflowRuntime.wait(job_id, wait_seconds * 1000);
444
+ const view = workflowRuntime.get(job_id, { withResult: true });
445
+ if (!view) return text({ error: `no such workflow: ${job_id}` });
446
+ return text(view);
447
+ }
448
+ if (job_id.startsWith('hub-')) {
449
+ const status = await hubStatus();
450
+ if (!status.compatible) return text({ error: hubCompatibilityMessage(status), code: status.code, hub_compatibility: status });
451
+ return text(await hub.get(job_id, wait_seconds).catch((e) => ({ error: e.message })));
452
+ }
453
+ if (!getJob(job_id)) return text({ error: `no such job: ${job_id} (expected a wf- workflow id)` });
454
+ const job = await waitJob(job_id, wait_seconds > 0 ? wait_seconds * 1000 : 1);
455
+ return text(jobView(job, { withResult: true }));
456
+ });
457
+
458
+ server.registerTool('dsh_worker_cancel', {
459
+ title: 'Cancel DSH worker',
460
+ description: 'Cancel a worker workflow (stops the active attempt, never starts escalation/review, releases its worktree). Accepts workflow ids (wf-...), Hub attempt ids (hub-...) and legacy standalone ids (job-...).',
461
+ inputSchema: { job_id: z.string() },
462
+ }, async ({ job_id }) => {
463
+ if (job_id.startsWith('wf-')) {
464
+ const view = await workflowRuntime.cancel(job_id);
465
+ if (!view) return text({ error: `no such workflow: ${job_id}` });
466
+ return text({ ...view, note: 'cancelled' });
467
+ }
468
+ if (job_id.startsWith('hub-')) {
469
+ const status = await hubStatus();
470
+ if (!status.compatible) return text({ error: hubCompatibilityMessage(status), code: status.code, hub_compatibility: status });
471
+ return text(await hub.cancel(job_id).catch((e) => ({ error: e.message })));
472
+ }
473
+ if (!getJob(job_id)) return text({ error: `no such job: ${job_id} (expected a wf- workflow id)` });
474
+ return text(jobView(await cancelJob(job_id), { withResult: true }));
475
+ });
476
+
477
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,52 @@
1
+ // Sharded worker-status publishing: every writer (hub process, per-session
2
+ // standalone MCP server) owns one file under ~/.config/dsh-crew/status.d/
3
+ // and readers merge all fresh shards. Kills the last-writer-wins race that a
4
+ // single shared status.json had with multiple concurrent writers.
5
+
6
+ import { writeFileSync, mkdirSync, rmSync, readdirSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+
10
+ const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
11
+ const SHARD_DIR = join(CONFIG_DIR, 'status.d');
12
+ export const SHARD_FRESH_MS = 30 * 60 * 1000;
13
+
14
+ export function createShardWriter(kind) {
15
+ const writer = `${kind}-${process.pid}`;
16
+ const file = join(SHARD_DIR, `${writer}.json`);
17
+ const cleanup = () => { try { rmSync(file, { force: true }); } catch {} };
18
+ process.once('exit', cleanup);
19
+ process.once('SIGINT', () => { cleanup(); process.exit(130); });
20
+ process.once('SIGTERM', () => { cleanup(); process.exit(143); });
21
+ return {
22
+ writer,
23
+ publish(jobs) {
24
+ try {
25
+ mkdirSync(SHARD_DIR, { recursive: true });
26
+ writeFileSync(file, JSON.stringify({ updatedAt: new Date().toISOString(), writer, jobs }, null, 2));
27
+ } catch {}
28
+ },
29
+ dispose: cleanup,
30
+ };
31
+ }
32
+
33
+ /** Merge fresh shards (plus the legacy status.json during transition). */
34
+ export function readMergedStatus({ excludeWriter } = {}) {
35
+ const jobs = [];
36
+ const now = Date.now();
37
+ const consume = (raw) => {
38
+ try {
39
+ const shard = JSON.parse(raw);
40
+ if (shard.writer === excludeWriter) return;
41
+ if (now - +new Date(shard.updatedAt) > SHARD_FRESH_MS) return;
42
+ for (const job of shard.jobs ?? []) jobs.push({ ...job, origin: shard.writer ?? 'legacy' });
43
+ } catch {}
44
+ };
45
+ try {
46
+ for (const f of readdirSync(SHARD_DIR)) {
47
+ if (f.endsWith('.json')) { try { consume(readFileSync(join(SHARD_DIR, f), 'utf8')); } catch {} }
48
+ }
49
+ } catch {}
50
+ try { consume(readFileSync(join(CONFIG_DIR, 'status.json'), 'utf8')); } catch {}
51
+ return jobs;
52
+ }
@@ -0,0 +1,39 @@
1
+ // Shared bounded machine error-code contract for the Hub service boundary.
2
+ //
3
+ // A machine code is an uppercase snake-style identifier (A-Z, 0-9, single
4
+ // underscores) of at most 64 characters. Only such values may cross the Hub
5
+ // service/client boundary as a top-level `code`: values are taken from
6
+ // err.code / err.policyCode only and are never derived from error text or from
7
+ // arbitrary response payload fields. Invalid values are treated as absent and
8
+ // callers fall back to their own constant (e.g. HUB_REQUEST_FAILED).
9
+
10
+ export const MACHINE_CODE_MAX_LENGTH = 64;
11
+ const MACHINE_CODE_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
12
+
13
+ /**
14
+ * True when `value` is a string that satisfies the bounded machine-code
15
+ * contract (uppercase snake-style identifier, length 1..64). Null, non-strings,
16
+ * lowercase/mixed-case, blank, leading/trailing/double-underscore, and
17
+ * over-length values are rejected.
18
+ */
19
+ export function isBoundedMachineCode(value) {
20
+ return typeof value === 'string'
21
+ && value.length > 0
22
+ && value.length <= MACHINE_CODE_MAX_LENGTH
23
+ && MACHINE_CODE_RE.test(value);
24
+ }
25
+
26
+ /**
27
+ * First valid bounded machine code found on an error object: `code` wins over
28
+ * `policyCode`. Returns null when neither field is a valid bounded machine
29
+ * code, so callers can keep the raw error text unchanged and only add the
30
+ * optional top-level `code` when one genuinely exists.
31
+ */
32
+ export function boundedMachineCodeFromError(err) {
33
+ if (!err || typeof err !== 'object') return null;
34
+ for (const key of ['code', 'policyCode']) {
35
+ const value = err[key];
36
+ if (isBoundedMachineCode(value)) return value;
37
+ }
38
+ return null;
39
+ }