@ran-sh/dsh-crew 1.9.0 → 1.10.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 (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +3 -2
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -340
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +463 -455
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
@@ -1,340 +1,340 @@
1
- // MCP-side workflow runtime wiring: builds the shared workflow runtime with
2
- // the real Hub / Standalone attempt adapters and the git-worktree workspace
3
- // adapter. This is the only place that knows which transport executes an
4
- // attempt; the MCP server (src/server.mjs) only does schema, validation,
5
- // policy gating and formatting.
6
- //
7
- // Behaviour decisions (escalation, review, candidate, queue, cancellation)
8
- // live entirely in workflow-runtime.mjs; the adapters here just execute.
9
-
10
- import { createWorkflowRuntime } from './workflow-runtime.mjs';
11
- import { CONFIG_SCHEMA_VERSION, normalizeGlobalConfig } from './policy.mjs';
12
- import { buildDirectSelectionTrace, enrichSelectionTrace } from './model-routing.mjs';
13
- import {
14
- createIsolatedWorkspace,
15
- cleanupIsolatedWorkspace,
16
- inspectRepository,
17
- captureCandidate as captureIsolationCandidate,
18
- } from './workspace-isolation.mjs';
19
- import { startJob, waitJob, jobView, cancelJob } from './jobs.mjs';
20
- import { hub } from './hub-client.mjs';
21
-
22
- const SESSION_CONFIG_KEYS = [
23
- 'default_tier', 'default_effort', 'mode', 'default_timeout_seconds',
24
- 'tier_policy', 'escalate_on_failure', 'collaboration_mode', 'main_agent_mode',
25
- 'flash_state', 'pro_state', 'pro_reviews_flash',
26
- ];
27
-
28
- const HUB_POLL_SLICE_SECONDS = 20;
29
-
30
- /** Keep each Hub request below intermediary/MCP transport idle deadlines. */
31
- export function hubPollWaitSeconds(remainingMs) {
32
- return Math.max(1, Math.min(HUB_POLL_SLICE_SECONDS, Math.ceil(remainingMs / 1000)));
33
- }
34
-
35
- /**
36
- * Merge only defined session overrides onto the live global config before the
37
- * workflow snapshots its policy. This keeps dsh_worker_config authoritative
38
- * for escalation/review decisions instead of applying it only to the MCP gate.
39
- */
40
- export function buildEffectiveRuntimeConfig(globalRaw = {}, session = {}) {
41
- const patch = {};
42
- for (const key of SESSION_CONFIG_KEYS) {
43
- if (session?.[key] !== undefined) patch[key] = session[key];
44
- }
45
- if (session?.enabled === false) patch.subagents_enabled = false;
46
- const base = normalizeGlobalConfig(globalRaw);
47
- if (Object.keys(patch).length === 0) return base;
48
-
49
- // Session controls are expressed through the compatibility vocabulary,
50
- // but a persisted schema-v4 config correctly ignores flat compatibility
51
- // mirrors. Project the session once through the legacy normalizer, then
52
- // apply only the policy dimensions the session is allowed to own onto the
53
- // canonical snapshot. Model priorities, provider routing, fallback,
54
- // adaptive ordering, health gates, and review gate remain untouched.
55
- const compatibility = { ...base, ...patch, config_schema_version: 2 };
56
- delete compatibility.worker;
57
- delete compatibility.review;
58
- delete compatibility.execution;
59
- delete compatibility.legacy;
60
- // Canonical flat mirrors describe the persisted base and must not outrank
61
- // this session's compatibility command. Remove the derived role/review
62
- // mirrors so the legacy projector can recompute them from the requested
63
- // session mode and review opt-in.
64
- delete compatibility.worker_state;
65
- delete compatibility.review_state;
66
- delete compatibility.auto_review;
67
- if (patch.collaboration_mode !== undefined) {
68
- delete compatibility.tier_policy;
69
- delete compatibility.flash_state;
70
- delete compatibility.pro_state;
71
- } else if (patch.tier_policy !== undefined) {
72
- delete compatibility.collaboration_mode;
73
- delete compatibility.flash_state;
74
- delete compatibility.pro_state;
75
- } else if (patch.flash_state !== undefined || patch.pro_state !== undefined) {
76
- compatibility.collaboration_mode = 'custom';
77
- delete compatibility.tier_policy;
78
- }
79
- Object.assign(compatibility, patch);
80
- const projected = normalizeGlobalConfig(compatibility);
81
- return normalizeGlobalConfig({
82
- ...base,
83
- config_schema_version: CONFIG_SCHEMA_VERSION,
84
- subagents_enabled: projected.subagents_enabled,
85
- main_agent_mode: projected.main_agent_mode,
86
- execution: {
87
- ...base.execution,
88
- enabled: projected.subagents_enabled,
89
- default_effort: projected.execution.default_effort,
90
- default_timeout_seconds: projected.execution.default_timeout_seconds,
91
- mode: projected.execution.mode,
92
- },
93
- worker: {
94
- ...base.worker,
95
- state: projected.worker.state,
96
- model_policy: {
97
- ...base.worker.model_policy,
98
- strategy: projected.worker.model_policy.strategy,
99
- escalation: {
100
- ...base.worker.model_policy.escalation,
101
- enabled: projected.worker.model_policy.escalation.enabled,
102
- },
103
- },
104
- },
105
- review: {
106
- ...base.review,
107
- state: projected.review.state,
108
- auto_review: projected.review.auto_review,
109
- },
110
- legacy: projected.legacy,
111
- });
112
- }
113
-
114
- /**
115
- * Translate a role/workflow attempt into the legacy model-class slot needed by
116
- * Standalone and DeepSeek Official Hub routing. A user-requested strong worker
117
- * starts on Pro; every escalated worker attempt also uses the strong slot.
118
- */
119
- export function resolveAttemptTier({ role = 'worker', attempt = 0, modelClassHint } = {}) {
120
- if (role === 'reviewer') return 'pro';
121
- if (Number.isInteger(attempt) && attempt > 0) return 'pro';
122
- if (modelClassHint === 'pro') return 'pro';
123
- return 'flash';
124
- }
125
-
126
- /** Map a Hub/Standalone job view into the AttemptResult the runtime expects. */
127
- export function attemptFromView(view, spec) {
128
- const role = view?.role ?? spec.role ?? 'worker';
129
- const logicalAttempt = spec.attempt ?? 0;
130
- const source = view?.selection_source ?? null;
131
- const transportTrace = view?.selection_trace ?? buildDirectSelectionTrace({
132
- role,
133
- logicalAttempt,
134
- modelClassHint: spec.model_class_hint ?? null,
135
- strategy: source ?? 'transport-selection',
136
- candidateSet: logicalAttempt > 0 ? 'escalation' : 'primary',
137
- provider: view?.provider ?? null,
138
- model: view?.model ?? null,
139
- execution_context: view?.execution_context ?? null,
140
- source: source ?? 'transport-selection',
141
- escalationReason: spec.escalation_reason ?? null,
142
- });
143
- const selectionTrace = enrichSelectionTrace(transportTrace, {
144
- role,
145
- logicalAttempt,
146
- modelClassHint: spec.model_class_hint ?? null,
147
- escalationReason: spec.escalation_reason ?? null,
148
- });
149
- return {
150
- id: view?.id ?? spec.id,
151
- role,
152
- // `view.attempt` may be an adapter routing-attempt (see below); the
153
- // workflow's logical attempt number is always the spec value.
154
- attempt: logicalAttempt,
155
- provider: view?.provider ?? null,
156
- model: view?.model ?? null,
157
- execution_context: view?.execution_context ?? null,
158
- selection_source: source,
159
- selection_trace: selectionTrace,
160
- status: view?.status ?? 'failed',
161
- result: view?.result ?? null,
162
- stopReason: view?.stopReason ?? null,
163
- outcome: view?.outcome ?? null,
164
- usage: view?.tokens ?? null,
165
- error: view?.error ?? null,
166
- error_code: view?.error_code ?? view?.code ?? null,
167
- };
168
- }
169
-
170
- function timedOutAttempt(view, spec, timeoutMs) {
171
- return {
172
- ...attemptFromView(view, spec),
173
- status: 'failed',
174
- stopReason: 'timeout',
175
- error: `attempt timed out after ${Math.ceil(timeoutMs / 1000)}s and was cancelled before any retry`,
176
- timed_out: true,
177
- };
178
- }
179
-
180
- /**
181
- * Build the workflow runtime used by the MCP server.
182
- *
183
- * deps:
184
- * getSessionConfig() -> session-level config (effort, timeout, mode...)
185
- * resolveMode() -> 'hub' | 'standalone'
186
- * presetForTier(tier) -> hub agent preset id for a tier slot (optional)
187
- * readGlobalConfig() -> raw global config reader
188
- * buildReviewTask(task, view) -> reviewer prompt builder
189
- * attemptTimeoutMs() -> default attempt execution timeout (optional)
190
- */
191
- export function buildMcpWorkflowRuntime(deps) {
192
- const { getSessionConfig, resolveMode, presetForTier, readGlobalConfig, buildReviewTask } = deps;
193
- const getConfig = () => buildEffectiveRuntimeConfig(readGlobalConfig(), getSessionConfig?.() ?? {});
194
- const getRuntimeControls = () => ({
195
- max_parallel: getConfig().execution?.max_parallel ?? 3,
196
- });
197
-
198
- const executeAttempt = async (spec) => {
199
- const session = getSessionConfig?.() ?? {};
200
- const effort = spec.effort ?? session.default_effort ?? 'max';
201
- const timeoutMs = (spec.timeout_seconds ?? session.default_timeout_seconds ?? 1800) * 1000;
202
- const tier = resolveAttemptTier({ role: spec.role, attempt: spec.attempt, modelClassHint: spec.model_class_hint });
203
- const delivery = spec.role === 'reviewer' || spec.delivery === 'review' ? 'review' : 'coding';
204
- const source = spec.source ?? 'api';
205
- const preset = presetForTier?.(tier);
206
-
207
- if ((await resolveMode()) === 'hub') {
208
- // Hub follow-dsh currently uses `attempt` to choose primary vs escalation
209
- // model policy. For an explicit worker+pro hint, route selection through
210
- // the strong pool while preserving the workflow's logical attempt number
211
- // in the normalized AttemptResult.
212
- const routingAttempt = spec.role === 'worker' && spec.attempt === 0 && spec.model_class_hint === 'pro'
213
- ? 1
214
- : spec.attempt;
215
- const spawned = await hub.spawn({
216
- task: spec.task,
217
- tier,
218
- role: spec.role,
219
- attempt: routingAttempt,
220
- effort,
221
- cwd: spec.cwd,
222
- source,
223
- preset,
224
- delivery,
225
- });
226
- spec.onAttemptStarted?.(spawned.id);
227
-
228
- const deadline = Date.now() + timeoutMs;
229
- let resolved = spawned;
230
- while (resolved?.status === 'running') {
231
- const remainingMs = deadline - Date.now();
232
- if (remainingMs <= 0) {
233
- let cancelled = resolved;
234
- try { cancelled = await hub.cancel(spawned.id); } catch {}
235
- return timedOutAttempt(cancelled, spec, timeoutMs);
236
- }
237
- const waitSeconds = hubPollWaitSeconds(remainingMs);
238
- resolved = await hub.get(spawned.id, waitSeconds);
239
- }
240
- return attemptFromView(resolved, spec);
241
- }
242
-
243
- const job = startJob({
244
- task: spec.task,
245
- tier,
246
- role: spec.role,
247
- attempt: spec.attempt,
248
- effort,
249
- cwd: spec.cwd,
250
- timeoutMs,
251
- source,
252
- delivery,
253
- modelClassHint: spec.model_class_hint ?? null,
254
- escalationReason: spec.escalation_reason ?? null,
255
- });
256
- spec.onAttemptStarted?.(job.id);
257
- await waitJob(job.id, timeoutMs);
258
- if (job.status === 'running') {
259
- await cancelJob(job.id).catch(() => {});
260
- return timedOutAttempt(jobView(job, { withResult: true }), spec, timeoutMs);
261
- }
262
- return attemptFromView(jobView(job, { withResult: true }), spec);
263
- };
264
-
265
- const cancelAttempt = async (id) => {
266
- if (String(id ?? '').startsWith('hub-')) {
267
- try { await hub.cancel(id); } catch {}
268
- return;
269
- }
270
- try { await cancelJob(id); } catch {}
271
- };
272
-
273
- const allocateWorkspace = async (job) => {
274
- const config = getConfig();
275
- const isolation = config.execution?.isolation ?? 'worktree';
276
- // Explicit shared mode uses the requested workspace. Readonly profiles,
277
- // including the default Reviewer, use a disposable worktree below so an
278
- // accidental edit can be detected and never pollutes the primary tree.
279
- if (job.requested_isolation === 'shared' || isolation === 'shared') {
280
- return { ok: true, execution_cwd: job.requested_cwd, isolation: 'shared', base_revision: null, primary_workspace_dirty: false, handle: null };
281
- }
282
- // Isolated roles fail closed when the workspace
283
- // is not a git repo — never silently fall back to sharing the working tree.
284
- const repo = await inspectRepository({ cwd: job.requested_cwd });
285
- if (!repo.ok) {
286
- return { ok: false, reason: repo.reason ?? 'ISOLATION_UNAVAILABLE', error: `${job.role ?? 'worker'} needs an isolated git worktree: ${repo.error ?? repo.reason}` };
287
- }
288
- // The worktree name carries what the job was for, so an operator reading the
289
- // directory list can tell a worker tree from a reviewer tree without opening
290
- // anything. Role is the honest answer; it is what the job actually is.
291
- const created = await createIsolatedWorkspace({
292
- cwd: job.requested_cwd,
293
- jobId: job.id,
294
- purpose: job.role ?? 'job',
295
- baseRevision: job.workspace_branch ?? repo.baseRevision,
296
- });
297
- if (!created.ok) {
298
- return { ok: false, reason: created.reason ?? 'WORKTREE_CREATE_FAILED', error: `worktree create failed: ${created.error ?? ''}` };
299
- }
300
- return {
301
- ok: true,
302
- execution_cwd: created.worktreePath,
303
- base_revision: created.baseRevision,
304
- isolation: 'worktree',
305
- primary_workspace_dirty: repo.dirty === true,
306
- handle: { worktreePath: created.worktreePath, repoRoot: created.repoRoot },
307
- };
308
- };
309
-
310
- const captureCandidate = async ({ cwd, baseRevision }) => captureIsolationCandidate({ worktreePath: cwd, baseRevision });
311
-
312
- const releaseWorkspace = async (handle) => {
313
- if (!handle) return { ok: true };
314
- try {
315
- const r = await cleanupIsolatedWorkspace({ worktreePath: handle.worktreePath, repoRoot: handle.repoRoot });
316
- return { ok: r.ok, error: r.ok ? undefined : r.error };
317
- } catch (err) {
318
- return { ok: false, error: err?.message ?? String(err) };
319
- }
320
- };
321
-
322
- const initialConfig = getConfig();
323
- const runtime = createWorkflowRuntime(
324
- {
325
- executeAttempt,
326
- cancelAttempt,
327
- allocateWorkspace,
328
- captureCandidate,
329
- releaseWorkspace,
330
- buildReviewTask,
331
- getConfig,
332
- getRuntimeControls,
333
- },
334
- {
335
- maxParallel: initialConfig.execution?.max_parallel ?? 3,
336
- },
337
- );
338
-
339
- return runtime;
340
- }
1
+ // MCP-side workflow runtime wiring: builds the shared workflow runtime with
2
+ // the real Hub / Standalone attempt adapters and the git-worktree workspace
3
+ // adapter. This is the only place that knows which transport executes an
4
+ // attempt; the MCP server (src/server.mjs) only does schema, validation,
5
+ // policy gating and formatting.
6
+ //
7
+ // Behaviour decisions (escalation, review, candidate, queue, cancellation)
8
+ // live entirely in workflow-runtime.mjs; the adapters here just execute.
9
+
10
+ import { createWorkflowRuntime } from './workflow-runtime.mjs';
11
+ import { CONFIG_SCHEMA_VERSION, normalizeGlobalConfig } from './policy.mjs';
12
+ import { buildDirectSelectionTrace, enrichSelectionTrace } from './model-routing.mjs';
13
+ import {
14
+ createIsolatedWorkspace,
15
+ cleanupIsolatedWorkspace,
16
+ inspectRepository,
17
+ captureCandidate as captureIsolationCandidate,
18
+ } from './workspace-isolation.mjs';
19
+ import { startJob, waitJob, jobView, cancelJob } from './jobs.mjs';
20
+ import { hub } from './hub-client.mjs';
21
+
22
+ const SESSION_CONFIG_KEYS = [
23
+ 'default_tier', 'default_effort', 'mode', 'default_timeout_seconds',
24
+ 'tier_policy', 'escalate_on_failure', 'collaboration_mode', 'main_agent_mode',
25
+ 'flash_state', 'pro_state', 'pro_reviews_flash',
26
+ ];
27
+
28
+ const HUB_POLL_SLICE_SECONDS = 20;
29
+
30
+ /** Keep each Hub request below intermediary/MCP transport idle deadlines. */
31
+ export function hubPollWaitSeconds(remainingMs) {
32
+ return Math.max(1, Math.min(HUB_POLL_SLICE_SECONDS, Math.ceil(remainingMs / 1000)));
33
+ }
34
+
35
+ /**
36
+ * Merge only defined session overrides onto the live global config before the
37
+ * workflow snapshots its policy. This keeps dsh_worker_config authoritative
38
+ * for escalation/review decisions instead of applying it only to the MCP gate.
39
+ */
40
+ export function buildEffectiveRuntimeConfig(globalRaw = {}, session = {}) {
41
+ const patch = {};
42
+ for (const key of SESSION_CONFIG_KEYS) {
43
+ if (session?.[key] !== undefined) patch[key] = session[key];
44
+ }
45
+ if (session?.enabled === false) patch.subagents_enabled = false;
46
+ const base = normalizeGlobalConfig(globalRaw);
47
+ if (Object.keys(patch).length === 0) return base;
48
+
49
+ // Session controls are expressed through the compatibility vocabulary,
50
+ // but a persisted schema-v4 config correctly ignores flat compatibility
51
+ // mirrors. Project the session once through the legacy normalizer, then
52
+ // apply only the policy dimensions the session is allowed to own onto the
53
+ // canonical snapshot. Model priorities, provider routing, fallback,
54
+ // adaptive ordering, health gates, and review gate remain untouched.
55
+ const compatibility = { ...base, ...patch, config_schema_version: 2 };
56
+ delete compatibility.worker;
57
+ delete compatibility.review;
58
+ delete compatibility.execution;
59
+ delete compatibility.legacy;
60
+ // Canonical flat mirrors describe the persisted base and must not outrank
61
+ // this session's compatibility command. Remove the derived role/review
62
+ // mirrors so the legacy projector can recompute them from the requested
63
+ // session mode and review opt-in.
64
+ delete compatibility.worker_state;
65
+ delete compatibility.review_state;
66
+ delete compatibility.auto_review;
67
+ if (patch.collaboration_mode !== undefined) {
68
+ delete compatibility.tier_policy;
69
+ delete compatibility.flash_state;
70
+ delete compatibility.pro_state;
71
+ } else if (patch.tier_policy !== undefined) {
72
+ delete compatibility.collaboration_mode;
73
+ delete compatibility.flash_state;
74
+ delete compatibility.pro_state;
75
+ } else if (patch.flash_state !== undefined || patch.pro_state !== undefined) {
76
+ compatibility.collaboration_mode = 'custom';
77
+ delete compatibility.tier_policy;
78
+ }
79
+ Object.assign(compatibility, patch);
80
+ const projected = normalizeGlobalConfig(compatibility);
81
+ return normalizeGlobalConfig({
82
+ ...base,
83
+ config_schema_version: CONFIG_SCHEMA_VERSION,
84
+ subagents_enabled: projected.subagents_enabled,
85
+ main_agent_mode: projected.main_agent_mode,
86
+ execution: {
87
+ ...base.execution,
88
+ enabled: projected.subagents_enabled,
89
+ default_effort: projected.execution.default_effort,
90
+ default_timeout_seconds: projected.execution.default_timeout_seconds,
91
+ mode: projected.execution.mode,
92
+ },
93
+ worker: {
94
+ ...base.worker,
95
+ state: projected.worker.state,
96
+ model_policy: {
97
+ ...base.worker.model_policy,
98
+ strategy: projected.worker.model_policy.strategy,
99
+ escalation: {
100
+ ...base.worker.model_policy.escalation,
101
+ enabled: projected.worker.model_policy.escalation.enabled,
102
+ },
103
+ },
104
+ },
105
+ review: {
106
+ ...base.review,
107
+ state: projected.review.state,
108
+ auto_review: projected.review.auto_review,
109
+ },
110
+ legacy: projected.legacy,
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Translate a role/workflow attempt into the legacy model-class slot needed by
116
+ * Standalone and DeepSeek Official Hub routing. A user-requested strong worker
117
+ * starts on Pro; every escalated worker attempt also uses the strong slot.
118
+ */
119
+ export function resolveAttemptTier({ role = 'worker', attempt = 0, modelClassHint } = {}) {
120
+ if (role === 'reviewer') return 'pro';
121
+ if (Number.isInteger(attempt) && attempt > 0) return 'pro';
122
+ if (modelClassHint === 'pro') return 'pro';
123
+ return 'flash';
124
+ }
125
+
126
+ /** Map a Hub/Standalone job view into the AttemptResult the runtime expects. */
127
+ export function attemptFromView(view, spec) {
128
+ const role = view?.role ?? spec.role ?? 'worker';
129
+ const logicalAttempt = spec.attempt ?? 0;
130
+ const source = view?.selection_source ?? null;
131
+ const transportTrace = view?.selection_trace ?? buildDirectSelectionTrace({
132
+ role,
133
+ logicalAttempt,
134
+ modelClassHint: spec.model_class_hint ?? null,
135
+ strategy: source ?? 'transport-selection',
136
+ candidateSet: logicalAttempt > 0 ? 'escalation' : 'primary',
137
+ provider: view?.provider ?? null,
138
+ model: view?.model ?? null,
139
+ execution_context: view?.execution_context ?? null,
140
+ source: source ?? 'transport-selection',
141
+ escalationReason: spec.escalation_reason ?? null,
142
+ });
143
+ const selectionTrace = enrichSelectionTrace(transportTrace, {
144
+ role,
145
+ logicalAttempt,
146
+ modelClassHint: spec.model_class_hint ?? null,
147
+ escalationReason: spec.escalation_reason ?? null,
148
+ });
149
+ return {
150
+ id: view?.id ?? spec.id,
151
+ role,
152
+ // `view.attempt` may be an adapter routing-attempt (see below); the
153
+ // workflow's logical attempt number is always the spec value.
154
+ attempt: logicalAttempt,
155
+ provider: view?.provider ?? null,
156
+ model: view?.model ?? null,
157
+ execution_context: view?.execution_context ?? null,
158
+ selection_source: source,
159
+ selection_trace: selectionTrace,
160
+ status: view?.status ?? 'failed',
161
+ result: view?.result ?? null,
162
+ stopReason: view?.stopReason ?? null,
163
+ outcome: view?.outcome ?? null,
164
+ usage: view?.tokens ?? null,
165
+ error: view?.error ?? null,
166
+ error_code: view?.error_code ?? view?.code ?? null,
167
+ };
168
+ }
169
+
170
+ function timedOutAttempt(view, spec, timeoutMs) {
171
+ return {
172
+ ...attemptFromView(view, spec),
173
+ status: 'failed',
174
+ stopReason: 'timeout',
175
+ error: `attempt timed out after ${Math.ceil(timeoutMs / 1000)}s and was cancelled before any retry`,
176
+ timed_out: true,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Build the workflow runtime used by the MCP server.
182
+ *
183
+ * deps:
184
+ * getSessionConfig() -> session-level config (effort, timeout, mode...)
185
+ * resolveMode() -> 'hub' | 'standalone'
186
+ * presetForTier(tier) -> hub agent preset id for a tier slot (optional)
187
+ * readGlobalConfig() -> raw global config reader
188
+ * buildReviewTask(task, view) -> reviewer prompt builder
189
+ * attemptTimeoutMs() -> default attempt execution timeout (optional)
190
+ */
191
+ export function buildMcpWorkflowRuntime(deps) {
192
+ const { getSessionConfig, resolveMode, presetForTier, readGlobalConfig, buildReviewTask } = deps;
193
+ const getConfig = () => buildEffectiveRuntimeConfig(readGlobalConfig(), getSessionConfig?.() ?? {});
194
+ const getRuntimeControls = () => ({
195
+ max_parallel: getConfig().execution?.max_parallel ?? 3,
196
+ });
197
+
198
+ const executeAttempt = async (spec) => {
199
+ const session = getSessionConfig?.() ?? {};
200
+ const effort = spec.effort ?? session.default_effort ?? 'max';
201
+ const timeoutMs = (spec.timeout_seconds ?? session.default_timeout_seconds ?? 1800) * 1000;
202
+ const tier = resolveAttemptTier({ role: spec.role, attempt: spec.attempt, modelClassHint: spec.model_class_hint });
203
+ const delivery = spec.role === 'reviewer' || spec.delivery === 'review' ? 'review' : 'coding';
204
+ const source = spec.source ?? 'api';
205
+ const preset = presetForTier?.(tier);
206
+
207
+ if ((await resolveMode()) === 'hub') {
208
+ // Hub follow-dsh currently uses `attempt` to choose primary vs escalation
209
+ // model policy. For an explicit worker+pro hint, route selection through
210
+ // the strong pool while preserving the workflow's logical attempt number
211
+ // in the normalized AttemptResult.
212
+ const routingAttempt = spec.role === 'worker' && spec.attempt === 0 && spec.model_class_hint === 'pro'
213
+ ? 1
214
+ : spec.attempt;
215
+ const spawned = await hub.spawn({
216
+ task: spec.task,
217
+ tier,
218
+ role: spec.role,
219
+ attempt: routingAttempt,
220
+ effort,
221
+ cwd: spec.cwd,
222
+ source,
223
+ preset,
224
+ delivery,
225
+ });
226
+ spec.onAttemptStarted?.(spawned.id);
227
+
228
+ const deadline = Date.now() + timeoutMs;
229
+ let resolved = spawned;
230
+ while (resolved?.status === 'running') {
231
+ const remainingMs = deadline - Date.now();
232
+ if (remainingMs <= 0) {
233
+ let cancelled = resolved;
234
+ try { cancelled = await hub.cancel(spawned.id); } catch {}
235
+ return timedOutAttempt(cancelled, spec, timeoutMs);
236
+ }
237
+ const waitSeconds = hubPollWaitSeconds(remainingMs);
238
+ resolved = await hub.get(spawned.id, waitSeconds);
239
+ }
240
+ return attemptFromView(resolved, spec);
241
+ }
242
+
243
+ const job = startJob({
244
+ task: spec.task,
245
+ tier,
246
+ role: spec.role,
247
+ attempt: spec.attempt,
248
+ effort,
249
+ cwd: spec.cwd,
250
+ timeoutMs,
251
+ source,
252
+ delivery,
253
+ modelClassHint: spec.model_class_hint ?? null,
254
+ escalationReason: spec.escalation_reason ?? null,
255
+ });
256
+ spec.onAttemptStarted?.(job.id);
257
+ await waitJob(job.id, timeoutMs);
258
+ if (job.status === 'running') {
259
+ await cancelJob(job.id).catch(() => {});
260
+ return timedOutAttempt(jobView(job, { withResult: true }), spec, timeoutMs);
261
+ }
262
+ return attemptFromView(jobView(job, { withResult: true }), spec);
263
+ };
264
+
265
+ const cancelAttempt = async (id) => {
266
+ if (String(id ?? '').startsWith('hub-')) {
267
+ try { await hub.cancel(id); } catch {}
268
+ return;
269
+ }
270
+ try { await cancelJob(id); } catch {}
271
+ };
272
+
273
+ const allocateWorkspace = async (job) => {
274
+ const config = getConfig();
275
+ const isolation = config.execution?.isolation ?? 'worktree';
276
+ // Explicit shared mode uses the requested workspace. Readonly profiles,
277
+ // including the default Reviewer, use a disposable worktree below so an
278
+ // accidental edit can be detected and never pollutes the primary tree.
279
+ if (job.requested_isolation === 'shared' || isolation === 'shared') {
280
+ return { ok: true, execution_cwd: job.requested_cwd, isolation: 'shared', base_revision: null, primary_workspace_dirty: false, handle: null };
281
+ }
282
+ // Isolated roles fail closed when the workspace
283
+ // is not a git repo — never silently fall back to sharing the working tree.
284
+ const repo = await inspectRepository({ cwd: job.requested_cwd });
285
+ if (!repo.ok) {
286
+ return { ok: false, reason: repo.reason ?? 'ISOLATION_UNAVAILABLE', error: `${job.role ?? 'worker'} needs an isolated git worktree: ${repo.error ?? repo.reason}` };
287
+ }
288
+ // The worktree name carries what the job was for, so an operator reading the
289
+ // directory list can tell a worker tree from a reviewer tree without opening
290
+ // anything. Role is the honest answer; it is what the job actually is.
291
+ const created = await createIsolatedWorkspace({
292
+ cwd: job.requested_cwd,
293
+ jobId: job.id,
294
+ purpose: job.role ?? 'job',
295
+ baseRevision: job.workspace_branch ?? repo.baseRevision,
296
+ });
297
+ if (!created.ok) {
298
+ return { ok: false, reason: created.reason ?? 'WORKTREE_CREATE_FAILED', error: `worktree create failed: ${created.error ?? ''}` };
299
+ }
300
+ return {
301
+ ok: true,
302
+ execution_cwd: created.worktreePath,
303
+ base_revision: created.baseRevision,
304
+ isolation: 'worktree',
305
+ primary_workspace_dirty: repo.dirty === true,
306
+ handle: { worktreePath: created.worktreePath, repoRoot: created.repoRoot },
307
+ };
308
+ };
309
+
310
+ const captureCandidate = async ({ cwd, baseRevision }) => captureIsolationCandidate({ worktreePath: cwd, baseRevision });
311
+
312
+ const releaseWorkspace = async (handle) => {
313
+ if (!handle) return { ok: true };
314
+ try {
315
+ const r = await cleanupIsolatedWorkspace({ worktreePath: handle.worktreePath, repoRoot: handle.repoRoot });
316
+ return { ok: r.ok, error: r.ok ? undefined : r.error };
317
+ } catch (err) {
318
+ return { ok: false, error: err?.message ?? String(err) };
319
+ }
320
+ };
321
+
322
+ const initialConfig = getConfig();
323
+ const runtime = createWorkflowRuntime(
324
+ {
325
+ executeAttempt,
326
+ cancelAttempt,
327
+ allocateWorkspace,
328
+ captureCandidate,
329
+ releaseWorkspace,
330
+ buildReviewTask,
331
+ getConfig,
332
+ getRuntimeControls,
333
+ },
334
+ {
335
+ maxParallel: initialConfig.execution?.max_parallel ?? 3,
336
+ },
337
+ );
338
+
339
+ return runtime;
340
+ }