@ran-sh/dsh-crew 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/ds-worker.md +5 -4
- package/codex/agents/ds-worker.toml +1 -1
- package/docs/job-contracts.md +12 -4
- package/docs/readiness-matrix.md +6 -2
- package/package.json +1 -1
- package/scripts/setup.mjs +62 -23
- package/src/config-readiness.mjs +50 -10
- package/src/hub/index.mjs +34 -9
- package/src/install/windows-startup.mjs +23 -6
- package/src/orchestrator.mjs +53 -0
- package/src/readiness-matrix.mjs +4 -3
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +38 -34
- package/src/workflow-runtime.mjs +27 -27
- package/src/workflow.mjs +63 -5
- package/windows/start-dsh-crew.cmd +51 -49
- package/windows/start-dsh-crew.ps1 +326 -0
- package/windows/start-dsh-crew.vbs +1 -1
- package/zcode/AGENTS.md +5 -0
- package/zcode/agents/ds-reviewer.md +21 -4
- package/zcode/agents/ds-worker.md +23 -4
package/src/server.mjs
CHANGED
|
@@ -26,8 +26,9 @@ import { buildReviewTask } from './information-flow.mjs';
|
|
|
26
26
|
import { projectWorkflowView } from './job-contracts.mjs';
|
|
27
27
|
import { loadRoleProfiles, resolveRoleProfile } from './role-profiles.mjs';
|
|
28
28
|
import { loadWorkspaceContexts, resolveWorkspaceContext, buildWorkspaceTask, addContextReferences, isSafeBranchName } from './workspace-context.mjs';
|
|
29
|
-
import { buildExtensionContract } from './extension-contract.mjs';
|
|
30
|
-
import { assessWorkspaceReadiness } from './workspace-readiness.mjs';
|
|
29
|
+
import { buildExtensionContract } from './extension-contract.mjs';
|
|
30
|
+
import { assessWorkspaceReadiness } from './workspace-readiness.mjs';
|
|
31
|
+
import { detectOrchestrator } from './orchestrator.mjs';
|
|
31
32
|
|
|
32
33
|
const server = new McpServer({ name: 'dsh-crew', version: RUNTIME_VERSION });
|
|
33
34
|
|
|
@@ -44,10 +45,11 @@ const workspaceSchema = z.object({
|
|
|
44
45
|
branch: z.string().refine(isSafeBranchName, 'invalid git branch name').optional(),
|
|
45
46
|
worktree: z.enum(['auto', 'existing', 'none']).optional(),
|
|
46
47
|
}).optional().describe('Per-job workspace overrides. auto isolates coding Workers; existing/none use the supplied workspace.');
|
|
47
|
-
const constraintsSchema = z.object({
|
|
48
|
-
timeout_seconds: z.number().int().positive().max(7200).optional(),
|
|
49
|
-
allow_fallback: z.boolean().optional(),
|
|
50
|
-
|
|
48
|
+
const constraintsSchema = z.object({
|
|
49
|
+
timeout_seconds: z.number().int().positive().max(7200).optional(),
|
|
50
|
+
allow_fallback: z.boolean().optional(),
|
|
51
|
+
allow_no_changes: z.boolean().optional().describe('Explicitly allow a verified read-only or analysis job to succeed with zero workspace changes.'),
|
|
52
|
+
}).optional().describe('Per-job constraints override profile and session defaults.');
|
|
51
53
|
|
|
52
54
|
// Session-level configuration. This MCP server process lives exactly as long
|
|
53
55
|
// as one Claude Code / Codex session, so plain memory IS session scope.
|
|
@@ -123,19 +125,7 @@ function text(obj) {
|
|
|
123
125
|
return { content: [{ type: 'text', text: typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2) }] };
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return 'claude-code';
|
|
128
|
-
try {
|
|
129
|
-
const { execSync } = require('node:child_process');
|
|
130
|
-
const comm = execSync(`ps -o comm= -p ${process.ppid}`, { encoding: 'utf8' }).trim().toLowerCase();
|
|
131
|
-
if (comm.includes('claude')) return 'claude-code';
|
|
132
|
-
if (comm.includes('codex')) return 'codex';
|
|
133
|
-
return comm.split('/').pop() || 'unknown';
|
|
134
|
-
} catch { return 'unknown'; }
|
|
135
|
-
}
|
|
136
|
-
import { createRequire } from 'node:module';
|
|
137
|
-
const require = createRequire(import.meta.url);
|
|
138
|
-
const ORCHESTRATOR = detectOrchestrator();
|
|
128
|
+
const ORCHESTRATOR = detectOrchestrator();
|
|
139
129
|
|
|
140
130
|
function policyRejection(decision) {
|
|
141
131
|
return text({
|
|
@@ -232,9 +222,10 @@ function prepareDispatch({ task, role, tier, legacy_tier, effort, cwd, timeout_s
|
|
|
232
222
|
profile_id: resolvedProfile.profile_id,
|
|
233
223
|
requested_isolation: requestedIsolation,
|
|
234
224
|
workspace_branch: workspaceOverride?.branch ?? withRefs.context?.default_branch ?? null,
|
|
235
|
-
timeout_seconds: effectiveTimeout,
|
|
236
|
-
allow_fallback: constraints?.allow_fallback ?? profileValue.fallback,
|
|
237
|
-
|
|
225
|
+
timeout_seconds: effectiveTimeout,
|
|
226
|
+
allow_fallback: constraints?.allow_fallback ?? profileValue.fallback,
|
|
227
|
+
allow_no_changes: constraints?.allow_no_changes === true,
|
|
228
|
+
routing: profileValue.routing,
|
|
238
229
|
review_strictness: profileValue.review_strictness,
|
|
239
230
|
workspace_context: withRefs.context,
|
|
240
231
|
},
|
|
@@ -333,10 +324,12 @@ async function buildConfigReport() {
|
|
|
333
324
|
const hubCompatibility = await hubStatus({ force: true });
|
|
334
325
|
let effectiveWorkerProvider = null;
|
|
335
326
|
let effectiveWorkerSelection = { flash: null, pro: null };
|
|
336
|
-
let providerResolutionError;
|
|
337
|
-
let providerCatalogChecked = false;
|
|
338
|
-
let providerCatalogBody = null;
|
|
339
|
-
|
|
327
|
+
let providerResolutionError;
|
|
328
|
+
let providerCatalogChecked = false;
|
|
329
|
+
let providerCatalogBody = null;
|
|
330
|
+
let hubJobsChecked = false;
|
|
331
|
+
let hubJobsBody = null;
|
|
332
|
+
const workerProviderMode = globalConfig.worker_provider_mode ?? 'deepseek-official';
|
|
340
333
|
if (workerProviderMode === 'deepseek-official') {
|
|
341
334
|
effectiveWorkerSelection = {
|
|
342
335
|
flash: { provider: 'deepseek-official', model: 'deepseek-v4-flash', source: 'legacy-strict' },
|
|
@@ -366,16 +359,27 @@ async function buildConfigReport() {
|
|
|
366
359
|
providerCatalogChecked = true;
|
|
367
360
|
providerResolutionError = err?.message ?? String(err);
|
|
368
361
|
}
|
|
369
|
-
} else if (hubCompatibility.reachable) {
|
|
370
|
-
providerResolutionError = hubCompatibilityMessage(hubCompatibility);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
362
|
+
} else if (hubCompatibility.reachable) {
|
|
363
|
+
providerResolutionError = hubCompatibilityMessage(hubCompatibility);
|
|
364
|
+
}
|
|
365
|
+
if (hubCompatibility.compatible) {
|
|
366
|
+
try {
|
|
367
|
+
hubJobsChecked = true;
|
|
368
|
+
const jobsRes = await fetch(`${globalConfig.hub_url}/_dsh/dsh-crew/jobs`, { signal: AbortSignal.timeout(800) });
|
|
369
|
+
hubJobsBody = await jobsRes.json();
|
|
370
|
+
} catch {
|
|
371
|
+
hubJobsChecked = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
effectiveWorkerProvider = effectiveWorkerSelection.flash?.provider ?? null;
|
|
375
|
+
const readinessMatrix = buildConfigReadinessMatrix({
|
|
374
376
|
hubCompatibility,
|
|
375
377
|
workerProviderMode,
|
|
376
|
-
providerCatalogChecked,
|
|
377
|
-
providerCatalogBody,
|
|
378
|
-
|
|
378
|
+
providerCatalogChecked,
|
|
379
|
+
providerCatalogBody,
|
|
380
|
+
hubJobsChecked,
|
|
381
|
+
hubJobsBody,
|
|
382
|
+
});
|
|
379
383
|
const roleProfiles = loadRoleProfiles();
|
|
380
384
|
const workspaceReadiness = await assessWorkspaceReadiness({ cwd: process.cwd() });
|
|
381
385
|
const extensionContract = buildExtensionContract({
|
package/src/workflow-runtime.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// appear here.
|
|
16
16
|
|
|
17
17
|
import { resolveModelPolicy, shouldAutoReview, getRoleState } from './policy.mjs';
|
|
18
|
-
import { buildOutcome, decideNextStep, JOB_PHASES, canTransition } from './workflow.mjs';
|
|
18
|
+
import { applyWorkspaceEvidence, buildOutcome, decideNextStep, JOB_PHASES, canTransition } from './workflow.mjs';
|
|
19
19
|
import { parseDeliveryReport } from './delivery.mjs';
|
|
20
20
|
import { classifyFailure } from './failure-classification.mjs';
|
|
21
21
|
import { createCanonicalJobEvent } from './job-contracts.mjs';
|
|
@@ -70,17 +70,6 @@ function mutationDetected(before, after) {
|
|
|
70
70
|
return after.fingerprint != null && before.fingerprint !== after.fingerprint;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function deliveryClaimsChanges(outcome) {
|
|
74
|
-
return Array.isArray(outcome?.changes) && outcome.changes.length > 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function workspaceEvidenceOK(outcome, candidate) {
|
|
78
|
-
if (!candidate) return true;
|
|
79
|
-
const hasChanges = Array.isArray(candidate.changed_files) && candidate.changed_files.length > 0;
|
|
80
|
-
if (outcome?.execution_status !== 'completed') return true;
|
|
81
|
-
return !deliveryClaimsChanges(outcome) || hasChanges;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
73
|
function sumUsage(attempts) {
|
|
85
74
|
const tokens = { input: 0, output: 0, reasoning: 0 };
|
|
86
75
|
for (const a of attempts) {
|
|
@@ -181,9 +170,10 @@ export function createWorkflowRuntime(adapters, {
|
|
|
181
170
|
requested_isolation: spec.requested_isolation ?? null,
|
|
182
171
|
workspace_branch: spec.workspace_branch ?? null,
|
|
183
172
|
timeout_seconds: spec.timeout_seconds ?? null,
|
|
184
|
-
profile_id: spec.profile_id ?? null,
|
|
185
|
-
allow_fallback: spec.allow_fallback !== false,
|
|
186
|
-
|
|
173
|
+
profile_id: spec.profile_id ?? null,
|
|
174
|
+
allow_fallback: spec.allow_fallback !== false,
|
|
175
|
+
allow_no_changes: spec.allow_no_changes === true,
|
|
176
|
+
routing: spec.routing ?? 'auto',
|
|
187
177
|
review_strictness: spec.review_strictness ?? null,
|
|
188
178
|
workspace_context: spec.workspace_context ? { ...spec.workspace_context } : null,
|
|
189
179
|
effort: spec.effort ?? 'max',
|
|
@@ -385,14 +375,17 @@ export function createWorkflowRuntime(adapters, {
|
|
|
385
375
|
failJob(job, Object.assign(new Error(ar.error ?? 'infrastructure failure'), { code: WORKFLOW_ERROR_CODES.ATTEMPT_INFRA_FAILURE }));
|
|
386
376
|
return;
|
|
387
377
|
}
|
|
388
|
-
|
|
378
|
+
let outcome = ar.outcome ?? buildOutcome({
|
|
389
379
|
result: ar.result ?? '',
|
|
390
380
|
stopReason: ar.stopReason,
|
|
391
381
|
executionStatus: ar.status === 'done' ? 'completed' : 'failed',
|
|
392
382
|
});
|
|
393
383
|
transition(job, JOB_PHASES.VERIFYING, `attempt ${attempt} complete`);
|
|
394
384
|
|
|
395
|
-
|
|
385
|
+
let workspaceEvidenceAvailable = false;
|
|
386
|
+
let workspaceHasChanges = false;
|
|
387
|
+
|
|
388
|
+
// Capture the latest candidate after every attempt. Capture failure is
|
|
396
389
|
// not allowed to delete the only recoverable state: retain the worktree
|
|
397
390
|
// and preserve the worker business result, while surfacing the warning.
|
|
398
391
|
if (alloc?.ok && alloc.isolation === 'worktree') {
|
|
@@ -402,17 +395,23 @@ export function createWorkflowRuntime(adapters, {
|
|
|
402
395
|
job.retain_workspace = true;
|
|
403
396
|
job.candidate = null;
|
|
404
397
|
job.events.push({ at: clock(), phase: job.phase, type: 'candidate/failed', attempt, message: 'candidate capture failed; worktree retained for recovery' });
|
|
405
|
-
} else {
|
|
406
|
-
job.candidate = candidate;
|
|
407
|
-
attemptView.candidate_fingerprint = candidate.fingerprint ?? null;
|
|
408
|
-
|
|
409
|
-
|
|
398
|
+
} else {
|
|
399
|
+
job.candidate = candidate;
|
|
400
|
+
attemptView.candidate_fingerprint = candidate.fingerprint ?? null;
|
|
401
|
+
workspaceEvidenceAvailable = true;
|
|
402
|
+
workspaceHasChanges = Array.isArray(candidate.changed_files) && candidate.changed_files.length > 0;
|
|
403
|
+
if (candidate.complete === false || candidate.replayable === false) {
|
|
410
404
|
job.retain_workspace = true;
|
|
411
405
|
job.events.push({ at: clock(), phase: job.phase, type: 'candidate/incomplete', attempt, message: 'candidate is not fully replayable; worktree retained' });
|
|
412
406
|
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
outcome = applyWorkspaceEvidence(outcome, {
|
|
410
|
+
evidenceAvailable: workspaceEvidenceAvailable,
|
|
411
|
+
hasChanges: workspaceHasChanges,
|
|
412
|
+
allowNoChanges: job.allow_no_changes,
|
|
413
|
+
});
|
|
414
|
+
job.outcome = outcome;
|
|
416
415
|
|
|
417
416
|
const reviewRequested = !isReviewJob && shouldAutoReview(config);
|
|
418
417
|
const reviewerAuto = getRoleState(config, 'reviewer') !== 'disabled';
|
|
@@ -580,8 +579,9 @@ export function createWorkflowRuntime(adapters, {
|
|
|
580
579
|
current_model: job.attempts[job.attempts.length - 1]?.model ?? null,
|
|
581
580
|
model_class_hint: job.model_class_hint,
|
|
582
581
|
source: job.source,
|
|
583
|
-
profile_id: job.profile_id,
|
|
584
|
-
|
|
582
|
+
profile_id: job.profile_id,
|
|
583
|
+
allow_no_changes: job.allow_no_changes,
|
|
584
|
+
routing: job.routing,
|
|
585
585
|
workspace_context: job.workspace_context ? { ...job.workspace_context } : null,
|
|
586
586
|
workspace_branch: job.workspace_branch,
|
|
587
587
|
timeout_seconds: job.timeout_seconds,
|
package/src/workflow.mjs
CHANGED
|
@@ -58,10 +58,68 @@ export function canTransition(from, to) {
|
|
|
58
58
|
return allowed.includes(to);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function splitSection(value) {
|
|
62
|
-
if (typeof value !== 'string' || value.trim() === '') return [];
|
|
63
|
-
return value.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
64
|
-
}
|
|
61
|
+
function splitSection(value) {
|
|
62
|
+
if (typeof value !== 'string' || value.trim() === '') return [];
|
|
63
|
+
return value.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const NO_CHANGE_SENTINELS = new Set([
|
|
67
|
+
'no files changed',
|
|
68
|
+
'no file changed',
|
|
69
|
+
'no changes',
|
|
70
|
+
'无文件变更',
|
|
71
|
+
'没有文件变更',
|
|
72
|
+
'未更改任何文件',
|
|
73
|
+
'无变更',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
function deliveryClaimsChanges(outcome) {
|
|
77
|
+
return Array.isArray(outcome?.changes) && outcome.changes.length > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function applyWorkspaceEvidence(outcome, {
|
|
81
|
+
evidenceAvailable = false,
|
|
82
|
+
hasChanges = false,
|
|
83
|
+
allowNoChanges = false,
|
|
84
|
+
requireNoChangeAuthorization = true,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const next = { ...outcome };
|
|
87
|
+
const claimsChanges = deliveryClaimsChanges(next);
|
|
88
|
+
if (evidenceAvailable && next.execution_status === 'completed') {
|
|
89
|
+
next.workspace_evidence_ok = claimsChanges === hasChanges;
|
|
90
|
+
}
|
|
91
|
+
const tests = Array.isArray(next.tests) ? next.tests : [];
|
|
92
|
+
const verifiedNoChange = requireNoChangeAuthorization === true
|
|
93
|
+
&& evidenceAvailable === true
|
|
94
|
+
&& allowNoChanges === true
|
|
95
|
+
&& hasChanges === false
|
|
96
|
+
&& claimsChanges === false
|
|
97
|
+
&& next.execution_status === 'completed'
|
|
98
|
+
&& next.workspace_evidence_ok === true
|
|
99
|
+
&& next.delivery?.complete === true
|
|
100
|
+
&& tests.some((test) => test.status === 'PASS')
|
|
101
|
+
&& !tests.some((test) => test.status === 'FAIL');
|
|
102
|
+
if (verifiedNoChange) {
|
|
103
|
+
next.task_status = 'success';
|
|
104
|
+
next.no_change_verified = true;
|
|
105
|
+
} else if (requireNoChangeAuthorization === true && claimsChanges === false && next.task_status === 'success') {
|
|
106
|
+
next.task_status = 'partial';
|
|
107
|
+
delete next.no_change_verified;
|
|
108
|
+
}
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseChanges(section) {
|
|
113
|
+
return splitSection(section).filter((line) => {
|
|
114
|
+
const normalized = line
|
|
115
|
+
.replace(/^(?:[-*+]\s+)+/, '')
|
|
116
|
+
.replace(/[`"'“”‘’]/g, '')
|
|
117
|
+
.replace(/[.!。!]+$/g, '')
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
return !NO_CHANGE_SENTINELS.has(normalized);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
65
123
|
|
|
66
124
|
function parseTests(section) {
|
|
67
125
|
return splitSection(section).map((line) => {
|
|
@@ -106,7 +164,7 @@ export function buildOutcome({ result = '', deliveryMeta, executionStatus, stopR
|
|
|
106
164
|
}),
|
|
107
165
|
confidence: null,
|
|
108
166
|
needs_escalation: false,
|
|
109
|
-
changes:
|
|
167
|
+
changes: parseChanges(parsed.sections.Diff),
|
|
110
168
|
tests,
|
|
111
169
|
tests_status: testsStatus ?? null,
|
|
112
170
|
risks: splitSection(parsed.sections.Risks),
|
|
@@ -2,54 +2,56 @@
|
|
|
2
2
|
setlocal EnableExtensions
|
|
3
3
|
title DSH Crew Launcher
|
|
4
4
|
|
|
5
|
-
set "
|
|
6
|
-
set "
|
|
7
|
-
set "
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
set "LAUNCH_REQUEST=%*"
|
|
6
|
+
set "LAUNCH_MODE=open"
|
|
7
|
+
set "LAUNCH_DIR=%~dp0"
|
|
8
|
+
|
|
9
|
+
if "%~1"=="" goto :run
|
|
10
|
+
if /i "%~1"=="--background" (
|
|
11
|
+
set "LAUNCH_MODE=background"
|
|
12
|
+
shift
|
|
13
|
+
goto :validate
|
|
14
|
+
)
|
|
15
|
+
if /i "%~1"=="--open" (
|
|
16
|
+
set "LAUNCH_MODE=open"
|
|
17
|
+
shift
|
|
18
|
+
goto :validate
|
|
19
|
+
)
|
|
20
|
+
if /i "%~1"=="--watch" (
|
|
21
|
+
set "LAUNCH_MODE=watch"
|
|
22
|
+
shift
|
|
23
|
+
goto :validate
|
|
24
|
+
)
|
|
25
|
+
if /i "%~1"=="--help" goto :help
|
|
26
|
+
goto :invalid_argument
|
|
27
|
+
|
|
28
|
+
:validate
|
|
29
|
+
if not "%~1"=="" goto :invalid_argument
|
|
30
|
+
|
|
31
|
+
:run
|
|
32
|
+
set "LAUNCH_HELPER=%LAUNCH_DIR%start-dsh-crew.ps1"
|
|
10
33
|
set "LAUNCH_LOG=%TEMP%\dsh-crew-launcher.log"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
if not exist "%LAUNCH_HELPER%" (
|
|
35
|
+
>>"%LAUNCH_LOG%" echo [%date% %time%] ERROR Managed launcher helper is missing: %LAUNCH_HELPER%
|
|
36
|
+
echo ERROR: DSH Crew launcher helper is missing.
|
|
37
|
+
echo Repair it with: dsh-crew update
|
|
38
|
+
if /i "%LAUNCH_MODE%"=="open" pause
|
|
39
|
+
exit /b 1
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%LAUNCH_HELPER%" -Mode "%LAUNCH_MODE%"
|
|
43
|
+
set "LAUNCH_EXIT=%ERRORLEVEL%"
|
|
44
|
+
if not "%LAUNCH_EXIT%"=="0" if /i "%LAUNCH_MODE%"=="open" pause
|
|
45
|
+
exit /b %LAUNCH_EXIT%
|
|
46
|
+
|
|
47
|
+
:invalid_argument
|
|
48
|
+
echo ERROR: Unsupported launcher arguments: %LAUNCH_REQUEST%
|
|
49
|
+
echo Use --open, --background, or --watch.
|
|
50
|
+
exit /b 64
|
|
51
|
+
|
|
52
|
+
:help
|
|
53
|
+
echo Usage: %~nx0 [--open ^| --background ^| --watch]
|
|
54
|
+
echo --open Start both services and open http://127.0.0.1:3080/.
|
|
55
|
+
echo --background Start both services silently without opening a browser.
|
|
56
|
+
echo --watch Keep both services healthy and restart them after an exit.
|
|
20
57
|
exit /b 0
|
|
21
|
-
|
|
22
|
-
:ensure_service
|
|
23
|
-
set "LAUNCH_PORT=%~1"
|
|
24
|
-
set "LAUNCH_PROFILE=%~2"
|
|
25
|
-
set "LAUNCH_HOME=%~3"
|
|
26
|
-
set "LAUNCH_URL=%~4"
|
|
27
|
-
|
|
28
|
-
call :health_check "%LAUNCH_URL%"
|
|
29
|
-
if not errorlevel 1 exit /b 0
|
|
30
|
-
|
|
31
|
-
powershell.exe -NoLogo -NoProfile -NonInteractive -Command "$listener=Get-NetTCPConnection -State Listen -LocalPort $env:LAUNCH_PORT -ErrorAction SilentlyContinue; if ($listener) { exit 0 } else { exit 1 }" >nul 2>&1
|
|
32
|
-
if not errorlevel 1 exit /b 1
|
|
33
|
-
|
|
34
|
-
powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -Command "try { $env:DSH_HOME=$env:LAUNCH_HOME; Start-Process -FilePath $env:DSH_CLI -ArgumentList @('--profile',$env:LAUNCH_PROFILE,'--host','127.0.0.1','--port',$env:LAUNCH_PORT,'--no-open') -WindowStyle Hidden -ErrorAction Stop } catch { ('['+(Get-Date -Format s)+'] Failed to start '+$env:LAUNCH_PROFILE+': '+$_.Exception.Message) | Add-Content -LiteralPath $env:LAUNCH_LOG; exit 1 }" >nul 2>&1
|
|
35
|
-
if errorlevel 1 exit /b 1
|
|
36
|
-
|
|
37
|
-
powershell.exe -NoLogo -NoProfile -NonInteractive -Command "$lastError=$null; $deadline=(Get-Date).AddSeconds(90); do { try { $r=Invoke-RestMethod -Uri ($env:LAUNCH_URL+'/_dsh/dsh-crew/extension') -TimeoutSec 2; if ($r.ok -eq $true -and $r.extension.runtime.runtime_version) { exit 0 } } catch { $lastError=$_.Exception.Message }; Start-Sleep -Milliseconds 500 } while ((Get-Date) -lt $deadline); if (-not $lastError) { $lastError='No healthy response before the startup deadline.' }; ('['+(Get-Date -Format s)+'] '+$env:LAUNCH_PROFILE+' health check failed: '+$lastError) | Add-Content -LiteralPath $env:LAUNCH_LOG; exit 1" >nul 2>&1
|
|
38
|
-
exit /b %ERRORLEVEL%
|
|
39
|
-
|
|
40
|
-
:health_check
|
|
41
|
-
set "HEALTH_URL=%~1"
|
|
42
|
-
powershell.exe -NoLogo -NoProfile -NonInteractive -Command "try { $r=Invoke-RestMethod -Uri ($env:HEALTH_URL+'/_dsh/dsh-crew/extension') -TimeoutSec 2; if ($r.ok -eq $true -and $r.extension.runtime.runtime_version) { exit 0 } } catch {}; exit 1" >nul 2>&1
|
|
43
|
-
exit /b %ERRORLEVEL%
|
|
44
|
-
|
|
45
|
-
:not_installed
|
|
46
|
-
echo [%date% %time%] DSH Crew is not installed completely.>>"%LAUNCH_LOG%"
|
|
47
|
-
exit /b 1
|
|
48
|
-
|
|
49
|
-
:official_missing
|
|
50
|
-
echo [%date% %time%] Official DeepSeek Harness web profile was not found.>>"%LAUNCH_LOG%"
|
|
51
|
-
exit /b 1
|
|
52
|
-
|
|
53
|
-
:failed
|
|
54
|
-
echo [%date% %time%] DSH Crew startup failed.>>"%LAUNCH_LOG%"
|
|
55
|
-
exit /b 1
|