@ran-sh/dsh-crew 0.3.8 → 0.4.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/README.md +17 -1
- package/README.zh.md +16 -1
- package/docs/gpt-relay-extension.md +103 -0
- package/docs/job-contracts.md +107 -0
- package/docs/readiness-matrix.md +73 -0
- package/official-web-bridge/lib/client.js +3446 -3446
- package/package.json +4 -1
- package/scripts/verify-official-bridge-e2e.mjs +34 -13
- package/src/extension-contract.mjs +78 -0
- package/src/failure-classification.mjs +29 -0
- package/src/hub/index.mjs +376 -73
- package/src/information-flow.mjs +67 -0
- package/src/install/npx-lifecycle.mjs +83 -3
- package/src/job-contracts.mjs +228 -0
- package/src/mcp-runtime.mjs +16 -8
- package/src/official-web-bridge.mjs +20 -5
- package/src/role-profiles.mjs +107 -0
- package/src/runtime-identity.mjs +6 -1
- package/src/server.mjs +141 -98
- package/src/workflow-runtime.mjs +109 -10
- package/src/workspace-context.mjs +146 -0
- package/src/workspace-readiness.mjs +32 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Versioned, narrow Worker/Reviewer profiles. Profiles configure one DSH
|
|
2
|
+
// delegation; they are not general Agent personas and never contain prompts or
|
|
3
|
+
// credentials.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
export const ROLE_PROFILE_SCHEMA_VERSION = 1;
|
|
10
|
+
export const DEFAULT_ROLE_PROFILES = Object.freeze({
|
|
11
|
+
'worker-default': Object.freeze({
|
|
12
|
+
role: 'worker', routing: 'auto', isolation: 'worktree', fallback: true,
|
|
13
|
+
timeout_seconds: 1800, review_strictness: 'standard',
|
|
14
|
+
}),
|
|
15
|
+
'reviewer-default': Object.freeze({
|
|
16
|
+
role: 'reviewer', routing: 'stable', isolation: 'readonly', fallback: false,
|
|
17
|
+
timeout_seconds: 1800, review_strictness: 'strict',
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
22
|
+
const ROLES = new Set(['worker', 'reviewer']);
|
|
23
|
+
const ROUTING = new Set(['auto', 'priority', 'stable']);
|
|
24
|
+
const ISOLATION = new Set(['worktree', 'readonly', 'shared']);
|
|
25
|
+
const STRICTNESS = new Set(['standard', 'strict']);
|
|
26
|
+
|
|
27
|
+
function normalizeProfile(id, raw) {
|
|
28
|
+
if (!ID.test(id) || !raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
29
|
+
if (!ROLES.has(raw.role)) return null;
|
|
30
|
+
const base = DEFAULT_ROLE_PROFILES[`${raw.role}-default`];
|
|
31
|
+
const routing = raw.routing ?? base.routing;
|
|
32
|
+
const isolation = raw.isolation ?? base.isolation;
|
|
33
|
+
const reviewStrictness = raw.review_strictness ?? base.review_strictness;
|
|
34
|
+
const timeout = raw.timeout_seconds ?? base.timeout_seconds;
|
|
35
|
+
if (!ROUTING.has(routing) || !ISOLATION.has(isolation) || !STRICTNESS.has(reviewStrictness)) return null;
|
|
36
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 7200) return null;
|
|
37
|
+
if (raw.fallback !== undefined && typeof raw.fallback !== 'boolean') return null;
|
|
38
|
+
return {
|
|
39
|
+
role: raw.role,
|
|
40
|
+
routing,
|
|
41
|
+
isolation,
|
|
42
|
+
fallback: raw.fallback ?? base.fallback,
|
|
43
|
+
timeout_seconds: timeout,
|
|
44
|
+
review_strictness: reviewStrictness,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function roleProfilesFile({ home = homedir() } = {}) {
|
|
49
|
+
return join(home, '.config', 'dsh-crew', 'profiles.json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function loadRoleProfiles({ home = homedir(), file = roleProfilesFile({ home }) } = {}) {
|
|
53
|
+
if (!existsSync(file)) {
|
|
54
|
+
return { schema_version: ROLE_PROFILE_SCHEMA_VERSION, ok: true, source: 'defaults', profiles: { ...DEFAULT_ROLE_PROFILES }, errors: [] };
|
|
55
|
+
}
|
|
56
|
+
let raw;
|
|
57
|
+
try { raw = JSON.parse(readFileSync(file, 'utf8')); } catch {
|
|
58
|
+
return { schema_version: ROLE_PROFILE_SCHEMA_VERSION, ok: false, source: 'file', profiles: { ...DEFAULT_ROLE_PROFILES }, errors: [{ code: 'PROFILE_FILE_INVALID' }] };
|
|
59
|
+
}
|
|
60
|
+
return parseRoleProfiles(raw);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseRoleProfiles(raw) {
|
|
64
|
+
const errors = [];
|
|
65
|
+
const profiles = { ...DEFAULT_ROLE_PROFILES };
|
|
66
|
+
if (raw?.schema_version !== ROLE_PROFILE_SCHEMA_VERSION || !raw.profiles || typeof raw.profiles !== 'object' || Array.isArray(raw.profiles)) {
|
|
67
|
+
return { schema_version: ROLE_PROFILE_SCHEMA_VERSION, ok: false, source: 'file', profiles, errors: [{ code: 'PROFILE_FILE_INVALID' }] };
|
|
68
|
+
}
|
|
69
|
+
for (const [id, value] of Object.entries(raw.profiles)) {
|
|
70
|
+
if (id in DEFAULT_ROLE_PROFILES) {
|
|
71
|
+
const normalized = normalizeProfile(id, value);
|
|
72
|
+
if (!normalized || JSON.stringify(normalized) !== JSON.stringify(DEFAULT_ROLE_PROFILES[id])) {
|
|
73
|
+
errors.push({ code: 'PROFILE_DEFAULT_RESERVED', profile_id: id });
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const profile = normalizeProfile(id, value);
|
|
78
|
+
if (!profile) errors.push({ code: 'PROFILE_INVALID', profile_id: ID.test(id) ? id : '<invalid>' });
|
|
79
|
+
else profiles[id] = profile;
|
|
80
|
+
}
|
|
81
|
+
return { schema_version: ROLE_PROFILE_SCHEMA_VERSION, ok: errors.length === 0, source: 'file', profiles, errors: errors.slice(0, 32) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function saveRoleProfiles(document, { home = homedir(), file = roleProfilesFile({ home }) } = {}) {
|
|
85
|
+
const parsed = parseRoleProfiles(document);
|
|
86
|
+
if (!parsed.ok) return parsed;
|
|
87
|
+
const custom = Object.fromEntries(Object.entries(parsed.profiles).filter(([id]) => !(id in DEFAULT_ROLE_PROFILES)));
|
|
88
|
+
const payload = { schema_version: ROLE_PROFILE_SCHEMA_VERSION, profiles: custom };
|
|
89
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
90
|
+
const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(temp, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
93
|
+
renameSync(temp, file);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
rmSync(temp, { force: true });
|
|
96
|
+
return { ...parsed, ok: false, errors: [{ code: 'PROFILE_FILE_WRITE_FAILED' }], error_code: 'PROFILE_FILE_WRITE_FAILED' };
|
|
97
|
+
}
|
|
98
|
+
return { ...parsed, source: 'file' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveRoleProfile(registry, profileId, role = 'worker') {
|
|
102
|
+
const id = profileId ?? `${role}-default`;
|
|
103
|
+
const profile = registry?.profiles?.[id];
|
|
104
|
+
if (!profile) return { ok: false, code: 'PROFILE_NOT_FOUND', profile_id: id };
|
|
105
|
+
if (profile.role !== role) return { ok: false, code: 'PROFILE_ROLE_MISMATCH', profile_id: id, expected_role: role };
|
|
106
|
+
return { ok: true, profile_id: id, profile: { ...profile } };
|
|
107
|
+
}
|
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.
|
|
11
|
+
export const RUNTIME_VERSION = '0.4.1';
|
|
12
12
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
13
13
|
|
|
14
14
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -21,6 +21,11 @@ export const HUB_CAPABILITIES = Object.freeze([
|
|
|
21
21
|
'model-catalog',
|
|
22
22
|
'presets',
|
|
23
23
|
'config',
|
|
24
|
+
'canonical-events',
|
|
25
|
+
'evidence',
|
|
26
|
+
'profiles',
|
|
27
|
+
'workspace-context',
|
|
28
|
+
'extension-contract',
|
|
24
29
|
]);
|
|
25
30
|
|
|
26
31
|
// Capabilities the current MCP workflow depends on for full Hub execution.
|
package/src/server.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { z } from 'zod';
|
|
|
6
6
|
import { startJob, waitJob, cancelJob, listJobs, getJob, jobView } from './jobs.mjs';
|
|
7
7
|
import { hubStatus, hub } from './hub-client.mjs';
|
|
8
8
|
import { hubCompatibilityMessage, resolveHubExecutionMode } from './hub-compatibility.mjs';
|
|
9
|
-
import { RUNTIME_VERSION } from './runtime-identity.mjs';
|
|
9
|
+
import { RUNTIME_VERSION, getHubRuntimeIdentity } from './runtime-identity.mjs';
|
|
10
10
|
import { resolveWorkerModel } from './model-routing.mjs';
|
|
11
11
|
import { runtimeActivationMetadata } from './runtime-controls.mjs';
|
|
12
12
|
import { buildConfigReadinessMatrix } from './config-readiness.mjs';
|
|
@@ -22,12 +22,32 @@ import {
|
|
|
22
22
|
shouldAutoReview,
|
|
23
23
|
} from './policy.mjs';
|
|
24
24
|
import { buildMcpWorkflowRuntime } from './mcp-runtime.mjs';
|
|
25
|
+
import { buildReviewTask } from './information-flow.mjs';
|
|
26
|
+
import { projectWorkflowView } from './job-contracts.mjs';
|
|
27
|
+
import { loadRoleProfiles, resolveRoleProfile } from './role-profiles.mjs';
|
|
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';
|
|
25
31
|
|
|
26
32
|
const server = new McpServer({ name: 'dsh-crew', version: RUNTIME_VERSION });
|
|
27
33
|
|
|
28
34
|
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
35
|
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
36
|
const effortSchema = z.enum(['off', 'high', 'max']).optional().describe('Reasoning effort for the worker. Omit to use the session default.');
|
|
37
|
+
const detailSchema = z.enum(['compact', 'full']).default('compact').describe('compact returns the bounded Result Contract; full explicitly includes raw workflow/candidate details for debugging.');
|
|
38
|
+
const profileSchema = z.string().max(64).optional().describe('Versioned Worker/Reviewer profile id; defaults by role.');
|
|
39
|
+
const workspaceIdSchema = z.string().max(64).optional().describe('Workspace Context id from Crew-owned workspaces.json.');
|
|
40
|
+
const contextRefsSchema = z.array(z.string().max(256)).max(32).optional().describe('Additional workspace-relative instruction references; contents are not copied.');
|
|
41
|
+
const clientJobIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/).optional().describe('Optional caller id echoed in events and evidence; Crew still assigns its own workflow id.');
|
|
42
|
+
const workspaceSchema = z.object({
|
|
43
|
+
repo_root: z.string().optional(),
|
|
44
|
+
branch: z.string().refine(isSafeBranchName, 'invalid git branch name').optional(),
|
|
45
|
+
worktree: z.enum(['auto', 'existing', 'none']).optional(),
|
|
46
|
+
}).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
|
+
}).optional().describe('Per-job constraints override profile and session defaults.');
|
|
31
51
|
|
|
32
52
|
// Session-level configuration. This MCP server process lives exactly as long
|
|
33
53
|
// as one Claude Code / Codex session, so plain memory IS session scope.
|
|
@@ -144,29 +164,6 @@ async function resolveMode() {
|
|
|
144
164
|
return decision.mode;
|
|
145
165
|
}
|
|
146
166
|
|
|
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
167
|
const workflowRuntime = buildMcpWorkflowRuntime({
|
|
171
168
|
getSessionConfig: () => sessionConfig,
|
|
172
169
|
resolveMode,
|
|
@@ -176,61 +173,107 @@ const workflowRuntime = buildMcpWorkflowRuntime({
|
|
|
176
173
|
attemptTimeoutMs: () => (sessionConfig.default_timeout_seconds ?? 1800) * 1000,
|
|
177
174
|
});
|
|
178
175
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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();
|
|
176
|
+
function dispatchError(code, details = {}) {
|
|
177
|
+
return { ok: false, response: text({ error: details.error ?? code, code, ...details }) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function prepareDispatch({ task, role, tier, legacy_tier, effort, cwd, timeout_seconds, profile, workspace_id, context_refs, job_id, workspace: workspaceOverride, constraints }) {
|
|
193
181
|
const globalConfig = currentGlobalConfig();
|
|
194
|
-
const
|
|
195
|
-
if (!
|
|
182
|
+
const profileRegistry = loadRoleProfiles();
|
|
183
|
+
if (!profileRegistry.ok) return dispatchError('PROFILE_FILE_INVALID', { errors: profileRegistry.errors });
|
|
184
|
+
const profileRole = profile ? profileRegistry.profiles?.[profile]?.role : undefined;
|
|
185
|
+
const requestedRole = role ?? profileRole;
|
|
186
|
+
const hint = resolveRoleTierHint(requestedRole, legacy_tier ?? tier);
|
|
187
|
+
if (!hint.ok) return dispatchError(hint.code, { error: hint.error });
|
|
196
188
|
const effRole = hint.role;
|
|
189
|
+
const resolvedProfile = resolveRoleProfile(profileRegistry, profile, effRole);
|
|
190
|
+
if (!resolvedProfile.ok) return dispatchError(resolvedProfile.code, resolvedProfile);
|
|
191
|
+
|
|
197
192
|
let effTier;
|
|
198
193
|
let decision;
|
|
199
|
-
if (
|
|
194
|
+
if (requestedRole === undefined) {
|
|
200
195
|
decision = chooseDefaultTier(globalConfig, legacy_tier ?? tier, sessionConfig);
|
|
201
|
-
if (!decision.ok) return policyRejection(decision);
|
|
196
|
+
if (!decision.ok) return { ok: false, response: policyRejection(decision) };
|
|
202
197
|
effTier = decision.tier;
|
|
203
198
|
} else {
|
|
204
199
|
decision = canDispatchRole(globalConfig, effRole, true, sessionConfig);
|
|
205
|
-
if (!decision.ok) return policyRejection(decision);
|
|
200
|
+
if (!decision.ok) return { ok: false, response: policyRejection(decision) };
|
|
206
201
|
if (effRole === 'reviewer') effTier = 'pro';
|
|
207
202
|
else if (legacy_tier !== undefined) effTier = legacy_tier;
|
|
208
203
|
else if (tier !== undefined) effTier = tier;
|
|
209
204
|
else { const slot = chooseDefaultTier(globalConfig, undefined, sessionConfig); effTier = slot.ok ? slot.tier : 'flash'; }
|
|
210
205
|
}
|
|
211
|
-
const workDir = cwd ?? process.cwd();
|
|
212
|
-
const e = effort ?? sessionConfig.default_effort;
|
|
213
|
-
const timeout = timeout_seconds ?? sessionConfig.default_timeout_seconds;
|
|
214
206
|
|
|
215
|
-
const
|
|
207
|
+
const workDir = workspaceOverride?.repo_root ?? cwd ?? process.cwd();
|
|
208
|
+
const workspace = resolveWorkspaceContext(loadWorkspaceContexts(), { workspace_id, cwd: workDir });
|
|
209
|
+
if (!workspace.ok) return dispatchError(workspace.code, workspace);
|
|
210
|
+
const withRefs = addContextReferences(workspace.context, context_refs, { cwd: workDir });
|
|
211
|
+
if (!withRefs.ok) return dispatchError(withRefs.code);
|
|
212
|
+
const profileValue = resolvedProfile.profile;
|
|
213
|
+
const effectiveTimeout = constraints?.timeout_seconds ?? timeout_seconds ?? profileValue.timeout_seconds ?? sessionConfig.default_timeout_seconds;
|
|
214
|
+
const requestedIsolation = workspaceOverride?.worktree === 'auto'
|
|
215
|
+
? 'worktree'
|
|
216
|
+
: workspaceOverride?.worktree === 'existing' || workspaceOverride?.worktree === 'none'
|
|
217
|
+
? 'shared'
|
|
218
|
+
: profileValue.isolation;
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
216
221
|
role: effRole,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
timeout: effectiveTimeout,
|
|
223
|
+
spec: {
|
|
224
|
+
client_job_id: job_id ?? null,
|
|
225
|
+
role: effRole,
|
|
226
|
+
delivery: effRole === 'reviewer' ? 'review' : 'coding',
|
|
227
|
+
model_class_hint: effTier,
|
|
228
|
+
task: buildWorkspaceTask(task, withRefs.context),
|
|
229
|
+
cwd: workDir,
|
|
230
|
+
effort: effort ?? sessionConfig.default_effort,
|
|
231
|
+
source: ORCHESTRATOR,
|
|
232
|
+
profile_id: resolvedProfile.profile_id,
|
|
233
|
+
requested_isolation: requestedIsolation,
|
|
234
|
+
workspace_branch: workspaceOverride?.branch ?? withRefs.context?.default_branch ?? null,
|
|
235
|
+
timeout_seconds: effectiveTimeout,
|
|
236
|
+
allow_fallback: constraints?.allow_fallback ?? profileValue.fallback,
|
|
237
|
+
routing: profileValue.routing,
|
|
238
|
+
review_strictness: profileValue.review_strictness,
|
|
239
|
+
workspace_context: withRefs.context,
|
|
240
|
+
},
|
|
223
241
|
};
|
|
224
|
-
|
|
225
|
-
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
server.registerTool('dsh_run_worker', {
|
|
245
|
+
title: 'Run DSH worker (blocking)',
|
|
246
|
+
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.',
|
|
247
|
+
inputSchema: {
|
|
248
|
+
task: z.string().describe('Full task description for the worker, self-contained'),
|
|
249
|
+
role: roleSchema,
|
|
250
|
+
tier: tierSchema,
|
|
251
|
+
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'),
|
|
252
|
+
effort: effortSchema,
|
|
253
|
+
cwd: z.string().optional().describe('Workspace directory for the worker (defaults to current project)'),
|
|
254
|
+
timeout_seconds: z.number().int().positive().max(7200).optional(),
|
|
255
|
+
job_id: clientJobIdSchema,
|
|
256
|
+
workspace: workspaceSchema,
|
|
257
|
+
constraints: constraintsSchema,
|
|
258
|
+
profile: profileSchema,
|
|
259
|
+
workspace_id: workspaceIdSchema,
|
|
260
|
+
context_refs: contextRefsSchema,
|
|
261
|
+
detail: detailSchema,
|
|
262
|
+
},
|
|
263
|
+
}, async ({ task, role, tier, legacy_tier, effort, cwd, timeout_seconds, profile, workspace_id, context_refs, job_id, workspace, constraints, detail }) => {
|
|
264
|
+
if (!sessionConfig.enabled) return dispatchDisabled();
|
|
265
|
+
const prepared = prepareDispatch({ task, role, tier, legacy_tier, effort, cwd, timeout_seconds, profile, workspace_id, context_refs, job_id, workspace, constraints });
|
|
266
|
+
if (!prepared.ok) return prepared.response;
|
|
267
|
+
const wf = workflowRuntime.start(prepared.spec);
|
|
268
|
+
await workflowRuntime.wait(wf.id, prepared.timeout * 1000);
|
|
226
269
|
const view = workflowRuntime.get(wf.id, { withResult: true });
|
|
227
270
|
if (view.status === 'running') {
|
|
228
|
-
return text({ ...view, note: `still running after ${timeout}s; poll with dsh_worker_result`, status: 'running' });
|
|
271
|
+
return text({ ...projectWorkflowView(view, { detail }), note: `still running after ${prepared.timeout}s; poll with dsh_worker_result`, status: 'running' });
|
|
229
272
|
}
|
|
230
273
|
if (view.phase === 'failed') {
|
|
231
|
-
return text({ ...view, note: 'workflow failed — see error / error_code.', status: 'failed' });
|
|
274
|
+
return text({ ...projectWorkflowView(view, { detail }), note: 'workflow failed — see error / error_code.', status: 'failed' });
|
|
232
275
|
}
|
|
233
|
-
return text({ ...view, note:
|
|
276
|
+
return text({ ...projectWorkflowView(view, { detail }), note: prepared.role === 'reviewer' ? 'review complete' : 'workflow complete' });
|
|
234
277
|
});
|
|
235
278
|
|
|
236
279
|
server.registerTool('dsh_worker_config', {
|
|
@@ -333,6 +376,21 @@ async function buildConfigReport() {
|
|
|
333
376
|
providerCatalogChecked,
|
|
334
377
|
providerCatalogBody,
|
|
335
378
|
});
|
|
379
|
+
const roleProfiles = loadRoleProfiles();
|
|
380
|
+
const workspaceReadiness = await assessWorkspaceReadiness({ cwd: process.cwd() });
|
|
381
|
+
const extensionContract = buildExtensionContract({
|
|
382
|
+
config: {
|
|
383
|
+
...globalConfig,
|
|
384
|
+
subagents_enabled: sessionConfig.enabled !== false && globalConfig.subagents_enabled !== false,
|
|
385
|
+
worker_state: globalConfig.worker_state,
|
|
386
|
+
review_state: globalConfig.review_state,
|
|
387
|
+
escalate_on_failure: sessionConfig.escalate_on_failure ?? legacy.escalate_on_failure,
|
|
388
|
+
},
|
|
389
|
+
readinessMatrix,
|
|
390
|
+
workspace: workspaceReadiness,
|
|
391
|
+
profiles: roleProfiles,
|
|
392
|
+
runtime: getHubRuntimeIdentity(),
|
|
393
|
+
});
|
|
336
394
|
return {
|
|
337
395
|
enabled: sessionConfig.enabled,
|
|
338
396
|
default_tier: sessionConfig.default_tier ?? legacy.default_tier,
|
|
@@ -371,6 +429,8 @@ async function buildConfigReport() {
|
|
|
371
429
|
runtime_controls: runtimeControls,
|
|
372
430
|
activation_boundaries: activationBoundaries,
|
|
373
431
|
readiness_matrix: readinessMatrix,
|
|
432
|
+
role_profiles: roleProfiles,
|
|
433
|
+
extension_contract: extensionContract,
|
|
374
434
|
hub_reachable: hubCompatibility.reachable,
|
|
375
435
|
hub_compatible: hubCompatibility.compatible,
|
|
376
436
|
hub_compatibility: hubCompatibility,
|
|
@@ -387,39 +447,18 @@ server.registerTool('dsh_spawn_worker', {
|
|
|
387
447
|
legacy_tier: tierSchema.describe('Legacy model-class hint (flash | pro) — only influences the model class backing a worker role, never the role gate'),
|
|
388
448
|
effort: effortSchema,
|
|
389
449
|
cwd: z.string().optional(),
|
|
450
|
+
job_id: clientJobIdSchema,
|
|
451
|
+
workspace: workspaceSchema,
|
|
452
|
+
constraints: constraintsSchema,
|
|
453
|
+
profile: profileSchema,
|
|
454
|
+
workspace_id: workspaceIdSchema,
|
|
455
|
+
context_refs: contextRefsSchema,
|
|
390
456
|
},
|
|
391
|
-
}, async ({ task, role, tier, legacy_tier, effort, cwd }) => {
|
|
457
|
+
}, async ({ task, role, tier, legacy_tier, effort, cwd, profile, workspace_id, context_refs, job_id, workspace, constraints }) => {
|
|
392
458
|
if (!sessionConfig.enabled) return dispatchDisabled();
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
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);
|
|
459
|
+
const prepared = prepareDispatch({ task, role, tier, legacy_tier, effort, cwd, profile, workspace_id, context_refs, job_id, workspace, constraints });
|
|
460
|
+
if (!prepared.ok) return prepared.response;
|
|
461
|
+
const wf = workflowRuntime.start(prepared.spec);
|
|
423
462
|
return text({ ...workflowRuntime.get(wf.id), workflow_id: wf.id, note: 'started in background; poll with dsh_worker_status / dsh_worker_result' });
|
|
424
463
|
});
|
|
425
464
|
|
|
@@ -437,41 +476,45 @@ server.registerTool('dsh_worker_result', {
|
|
|
437
476
|
inputSchema: {
|
|
438
477
|
job_id: z.string(),
|
|
439
478
|
wait_seconds: z.number().int().min(0).max(7200).default(0).describe('0 = return current state immediately'),
|
|
479
|
+
after_sequence: z.number().int().min(0).default(0).describe('Return canonical events after this cursor for incremental watch.'),
|
|
480
|
+
detail: detailSchema,
|
|
440
481
|
},
|
|
441
|
-
}, async ({ job_id, wait_seconds }) => {
|
|
482
|
+
}, async ({ job_id, wait_seconds, after_sequence, detail }) => {
|
|
442
483
|
if (job_id.startsWith('wf-')) {
|
|
443
484
|
if (wait_seconds > 0) await workflowRuntime.wait(job_id, wait_seconds * 1000);
|
|
444
485
|
const view = workflowRuntime.get(job_id, { withResult: true });
|
|
445
486
|
if (!view) return text({ error: `no such workflow: ${job_id}` });
|
|
446
|
-
return text(view);
|
|
487
|
+
return text(projectWorkflowView(view, { detail, afterSequence: after_sequence }));
|
|
447
488
|
}
|
|
448
489
|
if (job_id.startsWith('hub-')) {
|
|
449
490
|
const status = await hubStatus();
|
|
450
491
|
if (!status.compatible) return text({ error: hubCompatibilityMessage(status), code: status.code, hub_compatibility: status });
|
|
451
|
-
|
|
492
|
+
const view = await hub.get(job_id, wait_seconds).catch((e) => ({ error: e.message }));
|
|
493
|
+
return text(projectWorkflowView(view, { detail, afterSequence: after_sequence }));
|
|
452
494
|
}
|
|
453
495
|
if (!getJob(job_id)) return text({ error: `no such job: ${job_id} (expected a wf- workflow id)` });
|
|
454
496
|
const job = await waitJob(job_id, wait_seconds > 0 ? wait_seconds * 1000 : 1);
|
|
455
|
-
return text(jobView(job, { withResult: true }));
|
|
497
|
+
return text(projectWorkflowView(jobView(job, { withResult: true }), { detail, afterSequence: after_sequence }));
|
|
456
498
|
});
|
|
457
499
|
|
|
458
500
|
server.registerTool('dsh_worker_cancel', {
|
|
459
501
|
title: 'Cancel DSH worker',
|
|
460
502
|
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 }) => {
|
|
503
|
+
inputSchema: { job_id: z.string(), detail: detailSchema },
|
|
504
|
+
}, async ({ job_id, detail }) => {
|
|
463
505
|
if (job_id.startsWith('wf-')) {
|
|
464
506
|
const view = await workflowRuntime.cancel(job_id);
|
|
465
507
|
if (!view) return text({ error: `no such workflow: ${job_id}` });
|
|
466
|
-
return text({ ...view, note: 'cancelled' });
|
|
508
|
+
return text({ ...projectWorkflowView(view, { detail }), note: 'cancelled' });
|
|
467
509
|
}
|
|
468
510
|
if (job_id.startsWith('hub-')) {
|
|
469
511
|
const status = await hubStatus();
|
|
470
512
|
if (!status.compatible) return text({ error: hubCompatibilityMessage(status), code: status.code, hub_compatibility: status });
|
|
471
|
-
|
|
513
|
+
const view = await hub.cancel(job_id).catch((e) => ({ error: e.message }));
|
|
514
|
+
return text(projectWorkflowView(view, { detail }));
|
|
472
515
|
}
|
|
473
516
|
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 }));
|
|
517
|
+
return text(projectWorkflowView(jobView(await cancelJob(job_id), { withResult: true }), { detail }));
|
|
475
518
|
});
|
|
476
519
|
|
|
477
520
|
await server.connect(new StdioServerTransport());
|