@ran-sh/dsh-crew 0.5.6 → 1.0.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.
- package/README.md +20 -7
- package/README.zh.md +18 -7
- package/docs/installation.md +1 -1
- package/docs/ui-surfaces.md +65 -46
- package/lib/client.js +565 -28
- package/official-web-bridge/lib/client.js +565 -28
- package/package.json +1 -1
- package/src/client/collapsible-sections.mjs +3 -2
- package/src/client/host-readiness.mjs +16 -3
- package/src/client/index.tsx +331 -54
- package/src/config-readiness.mjs +66 -5
- package/src/credential-lifecycle.mjs +73 -0
- package/src/credential-purge-adapters.mjs +127 -0
- package/src/credential-purge-state.mjs +59 -0
- package/src/credential-reference-inventory.mjs +78 -0
- package/src/dsh-cli-runtime.mjs +36 -9
- package/src/extension-contract.mjs +16 -10
- package/src/hub/index.mjs +1004 -438
- package/src/hub-client.mjs +25 -5
- package/src/hub-compatibility.mjs +23 -7
- package/src/install/install.mjs +4 -4
- package/src/install/npx-lifecycle.mjs +495 -134
- package/src/job-contracts.mjs +27 -6
- package/src/jobs.mjs +2 -4
- package/src/local-request-guard.mjs +60 -0
- package/src/mcp-runtime.mjs +7 -5
- package/src/model-routing.mjs +141 -49
- package/src/multimodal.mjs +0 -0
- package/src/official-web-bridge.mjs +248 -95
- package/src/policy.mjs +62 -31
- package/src/provider-config-scrub.mjs +71 -0
- package/src/provider-delete-adapters.mjs +451 -0
- package/src/provider-desired-state.mjs +37 -0
- package/src/provider-health.mjs +124 -0
- package/src/provider-inventory.mjs +132 -0
- package/src/provider-lifecycle-state.mjs +95 -0
- package/src/provider-lifecycle.mjs +214 -0
- package/src/provider-profile-store.mjs +171 -0
- package/src/readiness-matrix.mjs +11 -5
- package/src/removable-waiter.mjs +29 -0
- package/src/runtime-identity.mjs +66 -21
- package/src/runtime-readiness-snapshot.mjs +87 -0
- package/src/server.mjs +64 -21
- package/src/status-shard.mjs +13 -2
- package/src/workflow-runtime.mjs +44 -10
- package/windows/start-dsh-crew.cmd +3 -3
- package/windows/start-dsh-crew.ps1 +49 -5
package/src/runtime-identity.mjs
CHANGED
|
@@ -5,10 +5,20 @@
|
|
|
5
5
|
// - HUB_PROTOCOL_VERSION changes only when Hub <-> MCP wire semantics become
|
|
6
6
|
// incompatible.
|
|
7
7
|
//
|
|
8
|
-
// Keep this module
|
|
9
|
-
//
|
|
8
|
+
// Keep this module dependency-light so Hub, MCP and tests all use the exact
|
|
9
|
+
// same compatibility rules.
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
// Production Crew execution is intentionally bound to the isolated 3210 Hub.
|
|
14
|
+
// These fields are public provenance only; no credential or session secret is
|
|
15
|
+
// included in the identity contract.
|
|
16
|
+
export const PRODUCTION_EXECUTION_PLANE = 'hub-3210';
|
|
17
|
+
export const PRODUCTION_PROFILE = 'dsh-crew';
|
|
18
|
+
export const PRODUCTION_LISTEN_PORT = 3210;
|
|
19
|
+
const RUNTIME_ID = randomUUID();
|
|
10
20
|
|
|
11
|
-
export const RUNTIME_VERSION = '0.
|
|
21
|
+
export const RUNTIME_VERSION = '1.0.0';
|
|
12
22
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
13
23
|
|
|
14
24
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -23,10 +33,17 @@ export const HUB_CAPABILITIES = Object.freeze([
|
|
|
23
33
|
'config',
|
|
24
34
|
'canonical-events',
|
|
25
35
|
'evidence',
|
|
26
|
-
'profiles',
|
|
27
|
-
'workspace-context',
|
|
28
|
-
'extension-contract',
|
|
29
|
-
|
|
36
|
+
'profiles',
|
|
37
|
+
'workspace-context',
|
|
38
|
+
'extension-contract',
|
|
39
|
+
'provider-inventory',
|
|
40
|
+
'provider-lifecycle-v1',
|
|
41
|
+
'provider-health-v1',
|
|
42
|
+
'provider-probe-stream-v1',
|
|
43
|
+
'credential-reference-inventory-v1',
|
|
44
|
+
'credential-purge-v1',
|
|
45
|
+
'runtime-provenance-v1',
|
|
46
|
+
]);
|
|
30
47
|
|
|
31
48
|
// Capabilities the current MCP workflow depends on for full Hub execution.
|
|
32
49
|
// Additional capabilities may be advertised without breaking compatibility.
|
|
@@ -43,9 +60,12 @@ export const HUB_COMPATIBILITY_CODES = Object.freeze({
|
|
|
43
60
|
UNREACHABLE: 'HUB_UNREACHABLE',
|
|
44
61
|
HTTP_ERROR: 'HUB_HTTP_ERROR',
|
|
45
62
|
SERVICE_MISMATCH: 'HUB_SERVICE_MISMATCH',
|
|
46
|
-
PROTOCOL_MISSING: 'HUB_PROTOCOL_MISSING',
|
|
47
|
-
PROTOCOL_MISMATCH: 'HUB_PROTOCOL_MISMATCH',
|
|
48
|
-
CAPABILITY_MISSING: 'HUB_CAPABILITY_MISSING',
|
|
63
|
+
PROTOCOL_MISSING: 'HUB_PROTOCOL_MISSING',
|
|
64
|
+
PROTOCOL_MISMATCH: 'HUB_PROTOCOL_MISMATCH',
|
|
65
|
+
CAPABILITY_MISSING: 'HUB_CAPABILITY_MISSING',
|
|
66
|
+
PROVENANCE_MISSING: 'HUB_PROVENANCE_MISSING',
|
|
67
|
+
EXECUTION_PLANE_MISMATCH: 'HUB_EXECUTION_PLANE_MISMATCH',
|
|
68
|
+
LISTEN_PORT_MISMATCH: 'HUB_LISTEN_PORT_MISMATCH',
|
|
49
69
|
});
|
|
50
70
|
|
|
51
71
|
function normalizedCapabilities(value) {
|
|
@@ -59,8 +79,12 @@ export function getHubRuntimeIdentity() {
|
|
|
59
79
|
surface: 'native-crew-harness',
|
|
60
80
|
ui_role: 'runtime',
|
|
61
81
|
runtime_version: RUNTIME_VERSION,
|
|
62
|
-
protocol_version: HUB_PROTOCOL_VERSION,
|
|
63
|
-
|
|
82
|
+
protocol_version: HUB_PROTOCOL_VERSION,
|
|
83
|
+
execution_plane: PRODUCTION_EXECUTION_PLANE,
|
|
84
|
+
profile: PRODUCTION_PROFILE,
|
|
85
|
+
listen_port: PRODUCTION_LISTEN_PORT,
|
|
86
|
+
runtime_id: RUNTIME_ID,
|
|
87
|
+
capabilities: [...HUB_CAPABILITIES],
|
|
64
88
|
};
|
|
65
89
|
}
|
|
66
90
|
|
|
@@ -71,30 +95,51 @@ export function getHubRuntimeIdentity() {
|
|
|
71
95
|
* `compatible` means the response is from dsh-crew and satisfies the current
|
|
72
96
|
* protocol + required capability contract.
|
|
73
97
|
*/
|
|
74
|
-
export function evaluateHubHandshake(body, { requiredCapabilities = REQUIRED_HUB_CAPABILITIES } = {}) {
|
|
98
|
+
export function evaluateHubHandshake(body, { requiredCapabilities = REQUIRED_HUB_CAPABILITIES, strictProduction = false } = {}) {
|
|
75
99
|
const capabilities = normalizedCapabilities(body?.capabilities);
|
|
76
100
|
const base = {
|
|
77
101
|
reachable: true,
|
|
78
102
|
compatible: false,
|
|
79
103
|
service: typeof body?.service === 'string' ? body.service : null,
|
|
80
|
-
runtime_version: typeof body?.runtime_version === 'string' ? body.runtime_version : null,
|
|
81
|
-
protocol_version: Number.isInteger(body?.protocol_version) ? body.protocol_version : null,
|
|
104
|
+
runtime_version: typeof body?.runtime_version === 'string' ? body.runtime_version : null,
|
|
105
|
+
protocol_version: Number.isInteger(body?.protocol_version) ? body.protocol_version : null,
|
|
106
|
+
execution_plane: typeof body?.execution_plane === 'string' ? body.execution_plane : null,
|
|
107
|
+
profile: typeof body?.profile === 'string' ? body.profile : null,
|
|
108
|
+
listen_port: Number.isInteger(body?.listen_port) ? body.listen_port : null,
|
|
109
|
+
runtime_id: typeof body?.runtime_id === 'string' ? body.runtime_id : null,
|
|
82
110
|
capabilities,
|
|
83
111
|
missing_capabilities: [],
|
|
84
112
|
code: null,
|
|
85
113
|
};
|
|
86
114
|
|
|
87
|
-
if (body?.service !== 'dsh-crew-hub') {
|
|
88
|
-
return { ...base, code: HUB_COMPATIBILITY_CODES.SERVICE_MISMATCH };
|
|
89
|
-
}
|
|
115
|
+
if (body?.service !== 'dsh-crew-hub') {
|
|
116
|
+
return { ...base, code: HUB_COMPATIBILITY_CODES.SERVICE_MISMATCH };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// An explicit execution identity that is not the isolated 3210 Crew plane
|
|
120
|
+
// must never be accepted as the production Hub. Missing legacy fields remain
|
|
121
|
+
// compatible with the older protocol contract and are handled below.
|
|
122
|
+
if (body?.execution_plane !== undefined && body.execution_plane !== PRODUCTION_EXECUTION_PLANE) {
|
|
123
|
+
return { ...base, code: HUB_COMPATIBILITY_CODES.EXECUTION_PLANE_MISMATCH };
|
|
124
|
+
}
|
|
125
|
+
if (body?.listen_port !== undefined && body.listen_port !== PRODUCTION_LISTEN_PORT) {
|
|
126
|
+
return { ...base, code: HUB_COMPATIBILITY_CODES.LISTEN_PORT_MISMATCH };
|
|
127
|
+
}
|
|
90
128
|
|
|
91
129
|
if (!Number.isInteger(body?.protocol_version)) {
|
|
92
130
|
return { ...base, code: HUB_COMPATIBILITY_CODES.PROTOCOL_MISSING };
|
|
93
131
|
}
|
|
94
132
|
|
|
95
|
-
if (body.protocol_version !== HUB_PROTOCOL_VERSION) {
|
|
96
|
-
return { ...base, code: HUB_COMPATIBILITY_CODES.PROTOCOL_MISMATCH };
|
|
97
|
-
}
|
|
133
|
+
if (body.protocol_version !== HUB_PROTOCOL_VERSION) {
|
|
134
|
+
return { ...base, code: HUB_COMPATIBILITY_CODES.PROTOCOL_MISMATCH };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (strictProduction && !(body?.execution_plane === PRODUCTION_EXECUTION_PLANE
|
|
138
|
+
&& body?.profile === PRODUCTION_PROFILE
|
|
139
|
+
&& Number.isInteger(body?.listen_port) && body.listen_port === PRODUCTION_LISTEN_PORT
|
|
140
|
+
&& typeof body?.runtime_id === 'string' && body.runtime_id.trim().length > 0)) {
|
|
141
|
+
return { ...base, code: HUB_COMPATIBILITY_CODES.PROVENANCE_MISSING };
|
|
142
|
+
}
|
|
98
143
|
|
|
99
144
|
const required = normalizedCapabilities(requiredCapabilities);
|
|
100
145
|
const advertised = new Set(capabilities);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Single, bounded readiness projection shared by the Hub extension surface,
|
|
2
|
+
// MCP diagnostics and the client. It consumes already-collected evidence and
|
|
3
|
+
// never performs I/O or carries raw task/result/error/credential content.
|
|
4
|
+
|
|
5
|
+
const MAX_HEALTH = 128;
|
|
6
|
+
|
|
7
|
+
function text(value) { return typeof value === 'string' && value.trim() ? value.trim() : null; }
|
|
8
|
+
|
|
9
|
+
function projectRuntime(runtime) {
|
|
10
|
+
if (!runtime || typeof runtime !== 'object') return null;
|
|
11
|
+
return {
|
|
12
|
+
execution_plane: text(runtime.execution_plane),
|
|
13
|
+
profile: text(runtime.profile),
|
|
14
|
+
listen_port: Number.isFinite(Number(runtime.listen_port)) ? Number(runtime.listen_port) : null,
|
|
15
|
+
runtime_id: text(runtime.runtime_id),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function projectSelection(value) {
|
|
20
|
+
if (!value || typeof value !== 'object') return null;
|
|
21
|
+
const provider = text(value.provider);
|
|
22
|
+
const model = text(value.model);
|
|
23
|
+
if (!provider || !model) return null;
|
|
24
|
+
return { provider, model, ...(text(value.source) ? { source: text(value.source) } : {}) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function projectHealth(entry) {
|
|
28
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
29
|
+
const provider = text(entry.provider);
|
|
30
|
+
const model = text(entry.model);
|
|
31
|
+
const state = text(entry.state);
|
|
32
|
+
if (!provider || !model || !state) return null;
|
|
33
|
+
return {
|
|
34
|
+
provider,
|
|
35
|
+
model,
|
|
36
|
+
state,
|
|
37
|
+
...(text(entry.reason_code) ? { reason_code: text(entry.reason_code) } : {}),
|
|
38
|
+
...(Number.isFinite(entry.observed_at) ? { observed_at: entry.observed_at } : {}),
|
|
39
|
+
...(Number.isFinite(entry.expires_at) ? { expires_at: entry.expires_at } : {}),
|
|
40
|
+
fresh: entry.fresh === true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matrixRow(matrix, id) {
|
|
45
|
+
return Array.isArray(matrix?.rows) ? matrix.rows.find((row) => row?.id === id) ?? null : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function projectWorkspace(workspace) {
|
|
49
|
+
if (!workspace || typeof workspace !== 'object') return null;
|
|
50
|
+
return {
|
|
51
|
+
...(text(workspace.status) ? { status: text(workspace.status) } : {}),
|
|
52
|
+
...(text(workspace.reason_code) ? { reason_code: text(workspace.reason_code) } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildRuntimeReadinessSnapshot({
|
|
57
|
+
runtime = null,
|
|
58
|
+
readinessMatrix = null,
|
|
59
|
+
selections = {},
|
|
60
|
+
health = [],
|
|
61
|
+
jobs = [],
|
|
62
|
+
workspace = null,
|
|
63
|
+
} = {}) {
|
|
64
|
+
const projectedHealth = (Array.isArray(health) ? health : []).map(projectHealth).filter(Boolean).slice(0, MAX_HEALTH);
|
|
65
|
+
const projectedJobs = Array.isArray(jobs) ? jobs : [];
|
|
66
|
+
const verified = projectedJobs.filter((job) => job?.status === 'done' && job?.task_status === 'success' && job?.delivery_complete === true).length;
|
|
67
|
+
const workerJobs = projectedJobs.filter((job) => job?.role === 'worker').length;
|
|
68
|
+
const reviewerJobs = projectedJobs.filter((job) => job?.role === 'reviewer').length;
|
|
69
|
+
const workerSelection = projectSelection(selections.worker ?? selections.flash);
|
|
70
|
+
const reviewerSelection = projectSelection(selections.reviewer ?? selections.pro);
|
|
71
|
+
return {
|
|
72
|
+
schema_version: 1,
|
|
73
|
+
runtime: projectRuntime(runtime),
|
|
74
|
+
readiness_matrix: readinessMatrix && typeof readinessMatrix === 'object' ? structuredClone(readinessMatrix) : null,
|
|
75
|
+
provider_lifecycle: matrixRow(readinessMatrix, 'provider_lifecycle_consistent'),
|
|
76
|
+
health: projectedHealth,
|
|
77
|
+
worker: { selected: workerSelection, health: projectedHealth.filter((entry) => entry.provider === workerSelection?.provider && entry.model === workerSelection?.model) },
|
|
78
|
+
reviewer: { selected: reviewerSelection, health: projectedHealth.filter((entry) => entry.provider === reviewerSelection?.provider && entry.model === reviewerSelection?.model) },
|
|
79
|
+
historical_evidence: {
|
|
80
|
+
job_count: projectedJobs.length,
|
|
81
|
+
verified_job_count: verified,
|
|
82
|
+
worker_job_count: workerJobs,
|
|
83
|
+
reviewer_job_count: reviewerJobs,
|
|
84
|
+
},
|
|
85
|
+
workspace: projectWorkspace(workspace),
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/server.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { hubCompatibilityMessage, resolveHubExecutionMode } from './hub-compatib
|
|
|
9
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
|
-
import { buildConfigReadinessMatrix } from './config-readiness.mjs';
|
|
12
|
+
import { buildConfigReadinessMatrix } from './config-readiness.mjs';
|
|
13
|
+
import { buildRuntimeReadinessSnapshot } from './runtime-readiness-snapshot.mjs';
|
|
13
14
|
import { classifyFailure, classifyFailureCode } from './failure-classification.mjs';
|
|
14
15
|
import {
|
|
15
16
|
normalizeGlobalConfig,
|
|
@@ -56,7 +57,9 @@ const constraintsSchema = z.object({
|
|
|
56
57
|
// Initial values come from the global config (~/.config/dsh-crew/config.json,
|
|
57
58
|
// edited on the DSH settings page); dsh_worker_config overrides per session.
|
|
58
59
|
// All dispatch decisions (dsh_run_worker AND dsh_spawn_worker, hub AND
|
|
59
|
-
//
|
|
60
|
+
// production dispatches go through the 3210-only policy resolver in
|
|
61
|
+
// src/policy.mjs; the historical standalone path is retained only for
|
|
62
|
+
// read/migration compatibility and is never selected for execution.
|
|
60
63
|
import { readGlobalConfig } from './install/install.mjs';
|
|
61
64
|
const initialGlobalConfig = normalizeGlobalConfig(readGlobalConfig());
|
|
62
65
|
const legacyDefaults = deriveLegacyConfig(initialGlobalConfig);
|
|
@@ -65,7 +68,7 @@ const sessionConfig = {
|
|
|
65
68
|
enabled: true,
|
|
66
69
|
default_tier: legacyDefaults.default_tier,
|
|
67
70
|
default_effort: legacyDefaults.default_effort,
|
|
68
|
-
mode: legacyDefaults.mode, // auto | hub
|
|
71
|
+
mode: legacyDefaults.mode === 'standalone' ? 'hub' : legacyDefaults.mode, // auto | hub; legacy standalone is migrated to hub
|
|
69
72
|
default_timeout_seconds: legacyDefaults.default_timeout_seconds,
|
|
70
73
|
tier_policy: undefined,
|
|
71
74
|
escalate_on_failure: legacyDefaults.escalate_on_failure,
|
|
@@ -83,7 +86,7 @@ function resetSessionConfig() {
|
|
|
83
86
|
enabled: true,
|
|
84
87
|
default_tier: legacyDefaults.default_tier,
|
|
85
88
|
default_effort: legacyDefaults.default_effort,
|
|
86
|
-
mode: legacyDefaults.mode,
|
|
89
|
+
mode: legacyDefaults.mode === 'standalone' ? 'hub' : legacyDefaults.mode,
|
|
87
90
|
default_timeout_seconds: legacyDefaults.default_timeout_seconds,
|
|
88
91
|
tier_policy: undefined,
|
|
89
92
|
escalate_on_failure: legacyDefaults.escalate_on_failure,
|
|
@@ -142,9 +145,9 @@ function dispatchDisabled() {
|
|
|
142
145
|
});
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
async function resolveMode() {
|
|
146
|
-
const status = await hubStatus();
|
|
147
|
-
const decision = resolveHubExecutionMode(sessionConfig.mode, status);
|
|
148
|
+
async function resolveMode() {
|
|
149
|
+
const status = await hubStatus();
|
|
150
|
+
const decision = resolveHubExecutionMode(sessionConfig.mode, status, { productionOnly: true });
|
|
148
151
|
if (!decision.ok) {
|
|
149
152
|
throw Object.assign(new Error(decision.error), {
|
|
150
153
|
code: decision.code,
|
|
@@ -274,7 +277,7 @@ server.registerTool('dsh_worker_config', {
|
|
|
274
277
|
enabled: z.boolean().optional().describe('false = refuse all worker dispatch this session'),
|
|
275
278
|
default_tier: z.enum(['flash', 'pro']).optional(),
|
|
276
279
|
default_effort: z.enum(['off', 'high', 'max']).optional(),
|
|
277
|
-
mode: z.enum(['auto', 'hub'
|
|
280
|
+
mode: z.enum(['auto', 'hub']).optional().describe('Execution mode. Production dispatch always uses the isolated 3210 Crew Harness; standalone is retained only as a legacy read/migration value.'),
|
|
278
281
|
default_timeout_seconds: z.number().int().positive().max(7200).optional(),
|
|
279
282
|
tier_policy: z.enum(['auto', 'flash-only', 'pro-only']).optional().describe('session hard clamp: flash-only / pro-only pin every dispatch to one tier'),
|
|
280
283
|
escalate_on_failure: z.boolean().optional().describe('allow an unverified worker attempt to retry through the stronger model policy (applies to run and spawn)'),
|
|
@@ -325,9 +328,13 @@ async function buildConfigReport() {
|
|
|
325
328
|
let effectiveWorkerProvider = null;
|
|
326
329
|
let effectiveWorkerSelection = { flash: null, pro: null };
|
|
327
330
|
let providerResolutionError;
|
|
328
|
-
let providerCatalogChecked = false;
|
|
329
|
-
let providerCatalogBody = null;
|
|
330
|
-
let
|
|
331
|
+
let providerCatalogChecked = false;
|
|
332
|
+
let providerCatalogBody = null;
|
|
333
|
+
let providerInventoryChecked = false;
|
|
334
|
+
let providerInventoryBody = null;
|
|
335
|
+
let providerHealthChecked = false;
|
|
336
|
+
let providerHealthBody = null;
|
|
337
|
+
let hubJobsChecked = false;
|
|
331
338
|
let hubJobsBody = null;
|
|
332
339
|
const workerProviderMode = globalConfig.worker_provider_mode ?? 'deepseek-official';
|
|
333
340
|
if (workerProviderMode === 'deepseek-official') {
|
|
@@ -362,9 +369,31 @@ async function buildConfigReport() {
|
|
|
362
369
|
} else if (hubCompatibility.reachable) {
|
|
363
370
|
providerResolutionError = hubCompatibilityMessage(hubCompatibility);
|
|
364
371
|
}
|
|
365
|
-
if (hubCompatibility.compatible) {
|
|
366
|
-
try {
|
|
367
|
-
|
|
372
|
+
if (hubCompatibility.compatible) {
|
|
373
|
+
try {
|
|
374
|
+
providerInventoryChecked = true;
|
|
375
|
+
const inventoryRes = await fetch(`${globalConfig.hub_url}/_dsh/dsh-crew/providers`, { signal: AbortSignal.timeout(800) });
|
|
376
|
+
providerInventoryBody = inventoryRes.ok
|
|
377
|
+
? await inventoryRes.json()
|
|
378
|
+
: { ok: false, code: 'PROVIDER_INVENTORY_UNAVAILABLE' };
|
|
379
|
+
} catch {
|
|
380
|
+
providerInventoryChecked = true;
|
|
381
|
+
providerInventoryBody = { ok: false, code: 'PROVIDER_INVENTORY_UNAVAILABLE' };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (hubCompatibility.compatible) {
|
|
385
|
+
try {
|
|
386
|
+
providerHealthChecked = true;
|
|
387
|
+
const healthRes = await fetch(`${globalConfig.hub_url}/_dsh/dsh-crew/provider-health`, { signal: AbortSignal.timeout(800) });
|
|
388
|
+
providerHealthBody = healthRes.ok ? await healthRes.json() : { ok: false, code: 'PROVIDER_HEALTH_UNAVAILABLE' };
|
|
389
|
+
} catch {
|
|
390
|
+
providerHealthChecked = true;
|
|
391
|
+
providerHealthBody = { ok: false, code: 'PROVIDER_HEALTH_UNAVAILABLE' };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (hubCompatibility.compatible) {
|
|
395
|
+
try {
|
|
396
|
+
hubJobsChecked = true;
|
|
368
397
|
const jobsRes = await fetch(`${globalConfig.hub_url}/_dsh/dsh-crew/jobs`, { signal: AbortSignal.timeout(800) });
|
|
369
398
|
hubJobsBody = await jobsRes.json();
|
|
370
399
|
} catch {
|
|
@@ -375,14 +404,26 @@ async function buildConfigReport() {
|
|
|
375
404
|
const readinessMatrix = buildConfigReadinessMatrix({
|
|
376
405
|
hubCompatibility,
|
|
377
406
|
workerProviderMode,
|
|
378
|
-
providerCatalogChecked,
|
|
379
|
-
providerCatalogBody,
|
|
380
|
-
|
|
407
|
+
providerCatalogChecked,
|
|
408
|
+
providerCatalogBody,
|
|
409
|
+
providerInventoryChecked,
|
|
410
|
+
providerInventoryBody,
|
|
411
|
+
providerHealthChecked,
|
|
412
|
+
providerHealthBody,
|
|
413
|
+
hubJobsChecked,
|
|
381
414
|
hubJobsBody,
|
|
382
415
|
});
|
|
383
416
|
const roleProfiles = loadRoleProfiles();
|
|
384
|
-
const workspaceReadiness = await assessWorkspaceReadiness({ cwd: process.cwd() });
|
|
385
|
-
const
|
|
417
|
+
const workspaceReadiness = await assessWorkspaceReadiness({ cwd: process.cwd() });
|
|
418
|
+
const readinessSnapshot = buildRuntimeReadinessSnapshot({
|
|
419
|
+
runtime: getHubRuntimeIdentity(),
|
|
420
|
+
readinessMatrix,
|
|
421
|
+
selections: { worker: effectiveWorkerSelection.worker ?? effectiveWorkerSelection.flash, reviewer: effectiveWorkerSelection.reviewer ?? effectiveWorkerSelection.pro },
|
|
422
|
+
health: providerHealthBody?.health,
|
|
423
|
+
jobs: hubJobsBody?.jobs,
|
|
424
|
+
workspace: workspaceReadiness,
|
|
425
|
+
});
|
|
426
|
+
const extensionContract = buildExtensionContract({
|
|
386
427
|
config: {
|
|
387
428
|
...globalConfig,
|
|
388
429
|
subagents_enabled: sessionConfig.enabled !== false && globalConfig.subagents_enabled !== false,
|
|
@@ -390,7 +431,8 @@ async function buildConfigReport() {
|
|
|
390
431
|
review_state: globalConfig.review_state,
|
|
391
432
|
escalate_on_failure: sessionConfig.escalate_on_failure ?? legacy.escalate_on_failure,
|
|
392
433
|
},
|
|
393
|
-
readinessMatrix,
|
|
434
|
+
readinessMatrix,
|
|
435
|
+
readinessSnapshot,
|
|
394
436
|
workspace: workspaceReadiness,
|
|
395
437
|
profiles: roleProfiles,
|
|
396
438
|
runtime: getHubRuntimeIdentity(),
|
|
@@ -432,7 +474,8 @@ async function buildConfigReport() {
|
|
|
432
474
|
global_defaults: Object.fromEntries(SAFE_GLOBAL_KEYS.map((k) => [k, globalConfig[k]])),
|
|
433
475
|
runtime_controls: runtimeControls,
|
|
434
476
|
activation_boundaries: activationBoundaries,
|
|
435
|
-
readiness_matrix: readinessMatrix,
|
|
477
|
+
readiness_matrix: readinessMatrix,
|
|
478
|
+
readiness_snapshot: readinessSnapshot,
|
|
436
479
|
role_profiles: roleProfiles,
|
|
437
480
|
extension_contract: extensionContract,
|
|
438
481
|
hub_reachable: hubCompatibility.reachable,
|
package/src/status-shard.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// and readers merge all fresh shards. Kills the last-writer-wins race that a
|
|
4
4
|
// single shared status.json had with multiple concurrent writers.
|
|
5
5
|
|
|
6
|
-
import { writeFileSync, mkdirSync, rmSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { writeFileSync, mkdirSync, rmSync, readdirSync, readFileSync, renameSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
|
|
@@ -21,9 +21,20 @@ export function createShardWriter(kind) {
|
|
|
21
21
|
return {
|
|
22
22
|
writer,
|
|
23
23
|
publish(jobs) {
|
|
24
|
+
// Same durability pattern as the profile/workspace registries: write a
|
|
25
|
+
// same-directory temp file as 0600 and rename it over the shard, so a
|
|
26
|
+
// reader never observes a partially written shard (parse failures are
|
|
27
|
+
// silently ignored, which would make the writer look vanished).
|
|
24
28
|
try {
|
|
25
29
|
mkdirSync(SHARD_DIR, { recursive: true });
|
|
26
|
-
|
|
30
|
+
const temp = `${file}.tmp-${Date.now()}`;
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(temp, JSON.stringify({ updatedAt: new Date().toISOString(), writer, jobs }, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
33
|
+
renameSync(temp, file);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
try { rmSync(temp, { force: true }); } catch {}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
27
38
|
} catch {}
|
|
28
39
|
},
|
|
29
40
|
dispose: cleanup,
|
package/src/workflow-runtime.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// appear here.
|
|
16
16
|
|
|
17
17
|
import { resolveModelPolicy, shouldAutoReview, getRoleState } from './policy.mjs';
|
|
18
|
+
import { raceWaiters } from './removable-waiter.mjs';
|
|
18
19
|
import { applyWorkspaceEvidence, buildOutcome, decideNextStep, JOB_PHASES, canTransition } from './workflow.mjs';
|
|
19
20
|
import { parseDeliveryReport } from './delivery.mjs';
|
|
20
21
|
import { classifyFailure } from './failure-classification.mjs';
|
|
@@ -298,7 +299,8 @@ export function createWorkflowRuntime(adapters, {
|
|
|
298
299
|
let config = {};
|
|
299
300
|
|
|
300
301
|
try {
|
|
301
|
-
config = adapters.getConfig?.() ?? {};
|
|
302
|
+
config = adapters.getConfig?.() ?? {};
|
|
303
|
+
job.review_gate = ['required', 'optional', 'off'].includes(config?.review?.gate) ? config.review.gate : 'required';
|
|
302
304
|
const alloc = await adapters.allocateWorkspace?.(job);
|
|
303
305
|
if (alloc && alloc.ok === false) {
|
|
304
306
|
failJob(job, Object.assign(new Error(alloc.error ?? alloc.reason), { code: alloc.reason }));
|
|
@@ -324,6 +326,19 @@ export function createWorkflowRuntime(adapters, {
|
|
|
324
326
|
const review = await runReviewerAttempt(job, reviewTask, config, before);
|
|
325
327
|
if (job.cancelling) { await cancelWorkflow(job); return; }
|
|
326
328
|
job.review = review;
|
|
329
|
+
const reviewApproved = review.status === 'done'
|
|
330
|
+
&& review.delivery_complete === true
|
|
331
|
+
&& review.verdict === 'approve';
|
|
332
|
+
if (!reviewApproved) {
|
|
333
|
+
const code = review.verdict === 'request_changes'
|
|
334
|
+
? 'REVIEW_CHANGES_REQUESTED'
|
|
335
|
+
: 'REVIEW_INCONCLUSIVE';
|
|
336
|
+
failJob(job, Object.assign(
|
|
337
|
+
new Error(`explicit review blocked acceptance: verdict=${review.verdict}, reviewer status=${review.status}, review delivery_complete=${review.delivery_complete === true}`),
|
|
338
|
+
{ code },
|
|
339
|
+
));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
327
342
|
if (job.review) transition(job, JOB_PHASES.READY, 'review complete');
|
|
328
343
|
finalize(job);
|
|
329
344
|
return;
|
|
@@ -453,6 +468,24 @@ export function createWorkflowRuntime(adapters, {
|
|
|
453
468
|
const review = await runReviewerAttempt(job, reviewTask, config, before, job.execution_cwd, job.base_revision);
|
|
454
469
|
if (job.cancelling) { await cancelWorkflow(job); return; }
|
|
455
470
|
job.review = review;
|
|
471
|
+
// The review verdict is an acceptance gate, not a footnote: only a
|
|
472
|
+
// reviewer that ran to completion, delivered its own auditable
|
|
473
|
+
// contract, and approved may finalize as COMPLETED. Anything else
|
|
474
|
+
// fails closed through the existing FAILED machinery so the MCP
|
|
475
|
+
// surface reports a failed workflow instead of a false "done".
|
|
476
|
+
const reviewApproved = review.status === 'done'
|
|
477
|
+
&& review.delivery_complete === true
|
|
478
|
+
&& review.verdict === 'approve';
|
|
479
|
+
if (!reviewApproved && job.review_gate === 'required') {
|
|
480
|
+
const code = review.verdict === 'request_changes'
|
|
481
|
+
? 'REVIEW_CHANGES_REQUESTED'
|
|
482
|
+
: 'REVIEW_INCONCLUSIVE';
|
|
483
|
+
failJob(job, Object.assign(
|
|
484
|
+
new Error(`automatic review blocked acceptance: verdict=${review.verdict}, reviewer status=${review.status}, review delivery_complete=${review.delivery_complete === true}`),
|
|
485
|
+
{ code },
|
|
486
|
+
));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
456
489
|
transition(job, JOB_PHASES.READY, decision.reason);
|
|
457
490
|
finalize(job);
|
|
458
491
|
return;
|
|
@@ -535,8 +568,9 @@ export function createWorkflowRuntime(adapters, {
|
|
|
535
568
|
id: ar.id,
|
|
536
569
|
role: ar.role ?? 'worker',
|
|
537
570
|
attempt,
|
|
538
|
-
provider: ar.provider ?? null,
|
|
539
|
-
model: ar.model ?? null,
|
|
571
|
+
provider: ar.provider ?? null,
|
|
572
|
+
model: ar.model ?? null,
|
|
573
|
+
execution_context: ar.execution_context ?? null,
|
|
540
574
|
selection_source: ar.selection_source ?? null,
|
|
541
575
|
selection_trace: ar.selection_trace ?? null,
|
|
542
576
|
status: ar.status ?? 'failed',
|
|
@@ -594,7 +628,8 @@ export function createWorkflowRuntime(adapters, {
|
|
|
594
628
|
phase: job.phase,
|
|
595
629
|
status: job.status,
|
|
596
630
|
attempt: job.attempts.length,
|
|
597
|
-
current_model: job.attempts[job.attempts.length - 1]?.model ?? null,
|
|
631
|
+
current_model: job.attempts[job.attempts.length - 1]?.model ?? null,
|
|
632
|
+
execution_context: job.attempts[job.attempts.length - 1]?.execution_context ?? null,
|
|
598
633
|
model_class_hint: job.model_class_hint,
|
|
599
634
|
source: job.source,
|
|
600
635
|
profile_id: job.profile_id,
|
|
@@ -619,12 +654,14 @@ export function createWorkflowRuntime(adapters, {
|
|
|
619
654
|
workspace_retained: job.workspace_retained === true,
|
|
620
655
|
decision: job.decision,
|
|
621
656
|
candidate_available: !!job.candidate,
|
|
622
|
-
review_status: job.review ? job.review.status ?? null : null,
|
|
657
|
+
review_status: job.review ? job.review.status ?? null : null,
|
|
658
|
+
review_gate: job.review_gate ?? 'required',
|
|
623
659
|
event_cursor: job.canonical_events.at(-1)?.sequence ?? 0,
|
|
624
660
|
};
|
|
625
661
|
if (withResult) {
|
|
626
662
|
v.child_attempts = job.attempts.map((a) => ({
|
|
627
|
-
id: a.id, role: a.role, attempt: a.attempt, provider: a.provider, model: a.model,
|
|
663
|
+
id: a.id, role: a.role, attempt: a.attempt, provider: a.provider, model: a.model,
|
|
664
|
+
execution_context: a.execution_context ?? null,
|
|
628
665
|
selection_source: a.selection_source, selection_trace: a.selection_trace ?? null,
|
|
629
666
|
status: a.status, stopReason: a.stopReason,
|
|
630
667
|
outcome_task: a.outcome?.task_status ?? null,
|
|
@@ -661,10 +698,7 @@ export function createWorkflowRuntime(adapters, {
|
|
|
661
698
|
const job = jobs.get(id);
|
|
662
699
|
if (!job) return undefined;
|
|
663
700
|
if (job.status !== 'running') return job;
|
|
664
|
-
await
|
|
665
|
-
new Promise((res) => job.waiters.push(res)),
|
|
666
|
-
timeoutMs > 0 ? new Promise((res) => setTimeout(res, timeoutMs)) : new Promise(() => {}),
|
|
667
|
-
]);
|
|
701
|
+
await raceWaiters(job.waiters, { timeoutMs });
|
|
668
702
|
return job;
|
|
669
703
|
}
|
|
670
704
|
|
|
@@ -51,7 +51,7 @@ exit /b 64
|
|
|
51
51
|
|
|
52
52
|
:help
|
|
53
53
|
echo Usage: %~nx0 [--open ^| --background ^| --watch]
|
|
54
|
-
echo --open Start
|
|
55
|
-
echo --background Start
|
|
56
|
-
echo --watch Keep
|
|
54
|
+
echo --open Start the 3080 console and its supervised 3210 backend, then open the console.
|
|
55
|
+
echo --background Start the 3080 console and its supervised 3210 backend silently.
|
|
56
|
+
echo --watch Keep the 3080 console and supervised 3210 backend healthy.
|
|
57
57
|
exit /b 0
|
|
@@ -15,8 +15,8 @@ $logRoot = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
|
|
|
15
15
|
$launcherLog = Join-Path $logRoot 'dsh-crew-launcher.log'
|
|
16
16
|
$startedAt = Get-Date
|
|
17
17
|
$services = @(
|
|
18
|
-
[pscustomobject]@{ Name = '
|
|
19
|
-
[pscustomobject]@{ Name = '
|
|
18
|
+
[pscustomobject]@{ Name = 'Official UI'; Profile = 'web'; Home = $officialHome; Port = 3080; Url = 'http://127.0.0.1:3080'; ManagedByBridge = $false; State = 'pending'; Process = $null; RootPid = $null; RootStartedAtUtcTicks = $null; ListenerPid = $null; ListenerStartedAtUtcTicks = $null; ConsecutiveFailures = 0; LastError = $null },
|
|
19
|
+
[pscustomobject]@{ Name = 'Crew backend'; Profile = 'dsh-crew'; Home = $crewHome; Port = 3210; Url = 'http://127.0.0.1:3210'; ManagedByBridge = $true; State = 'pending'; Process = $null; RootPid = $null; RootStartedAtUtcTicks = $null; ListenerPid = $null; ListenerStartedAtUtcTicks = $null; ConsecutiveFailures = 0; LastError = $null }
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
function Write-LaunchLog {
|
|
@@ -44,6 +44,18 @@ function Get-HealthState {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function Start-BridgedCrewService {
|
|
48
|
+
try {
|
|
49
|
+
$response = Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:3080/_dsh/dsh-crew/ping' -TimeoutSec 5
|
|
50
|
+
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 300) {
|
|
51
|
+
return [pscustomobject]@{ Ready = $true; Error = $null }
|
|
52
|
+
}
|
|
53
|
+
return [pscustomobject]@{ Ready = $false; Error = ('3080 bridge returned HTTP {0}.' -f $response.StatusCode) }
|
|
54
|
+
} catch {
|
|
55
|
+
return [pscustomobject]@{ Ready = $false; Error = ('3080 bridge could not start the supervised 3210 backend: {0}' -f $_.Exception.Message) }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
function Get-PortState {
|
|
48
60
|
param([int] $Port)
|
|
49
61
|
try {
|
|
@@ -175,7 +187,7 @@ function Wait-CrewServices {
|
|
|
175
187
|
if ($health.Ready) {
|
|
176
188
|
$service.State = 'ready'
|
|
177
189
|
$service.ConsecutiveFailures = 0
|
|
178
|
-
[void] (Set-TrackedListenerIdentity -Service $service)
|
|
190
|
+
if (-not $service.ManagedByBridge) { [void] (Set-TrackedListenerIdentity -Service $service) }
|
|
179
191
|
Write-LaunchLog ('{0} ready on {1}; runtime={2}' -f $service.Name, $service.Port, $health.Version)
|
|
180
192
|
} elseif ($service.Process -and $service.Process.HasExited) {
|
|
181
193
|
throw ('{0} exited before becoming ready; PID={1}; exit={2}; last health error: {3}' -f $service.Name, $service.Process.Id, $service.Process.ExitCode, $health.Error)
|
|
@@ -184,7 +196,9 @@ function Wait-CrewServices {
|
|
|
184
196
|
if (@($services | Where-Object State -eq 'starting').Count -gt 0) { Start-Sleep -Milliseconds 500 }
|
|
185
197
|
}
|
|
186
198
|
|
|
187
|
-
$notReady = @($services | Where-Object
|
|
199
|
+
$notReady = @($services | Where-Object {
|
|
200
|
+
$_.State -ne 'ready' -and -not ($_.ManagedByBridge -and $_.State -eq 'pending')
|
|
201
|
+
})
|
|
188
202
|
if ($notReady.Count -gt 0) {
|
|
189
203
|
$details = ($notReady | ForEach-Object { '{0}:{1} ({2})' -f $_.Profile, $_.Port, $_.LastError }) -join '; '
|
|
190
204
|
throw "Startup health deadline exceeded: $details"
|
|
@@ -193,21 +207,36 @@ function Wait-CrewServices {
|
|
|
193
207
|
|
|
194
208
|
function Ensure-CrewServices {
|
|
195
209
|
param([switch] $QuietHealthy)
|
|
210
|
+
$officialRestarted = $false
|
|
196
211
|
|
|
197
212
|
foreach ($service in $services) {
|
|
198
213
|
$health = Get-HealthState $service
|
|
214
|
+
if ($service.ManagedByBridge -and $officialRestarted) {
|
|
215
|
+
# A restarted 3080 process must re-establish ownership of 3210 through
|
|
216
|
+
# its own sidecar, even if an old detached listener still answers.
|
|
217
|
+
$service.State = 'pending'
|
|
218
|
+
$service.LastError = 'Waiting for the restarted 3080 bridge to claim 3210.'
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
199
221
|
if ($health.Ready) {
|
|
200
222
|
$wasReady = $service.State -eq 'ready'
|
|
201
223
|
$service.State = 'ready'
|
|
202
224
|
$service.ConsecutiveFailures = 0
|
|
203
225
|
$service.LastError = $null
|
|
204
|
-
if (-not $service.ListenerPid) { [void] (Set-TrackedListenerIdentity -Service $service) }
|
|
226
|
+
if (-not $service.ManagedByBridge -and -not $service.ListenerPid) { [void] (Set-TrackedListenerIdentity -Service $service) }
|
|
205
227
|
if (-not $QuietHealthy -or -not $wasReady) {
|
|
206
228
|
Write-LaunchLog ('{0} already ready on {1}; runtime={2}' -f $service.Name, $service.Port, $health.Version)
|
|
207
229
|
}
|
|
208
230
|
continue
|
|
209
231
|
}
|
|
210
232
|
|
|
233
|
+
if ($service.ManagedByBridge) {
|
|
234
|
+
$service.LastError = $health.Error
|
|
235
|
+
$service.ConsecutiveFailures += 1
|
|
236
|
+
$service.State = 'pending'
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
211
240
|
$service.LastError = $health.Error
|
|
212
241
|
$service.ConsecutiveFailures += 1
|
|
213
242
|
if ($service.Process -and $service.Process.HasExited) {
|
|
@@ -242,9 +271,24 @@ function Ensure-CrewServices {
|
|
|
242
271
|
Write-LaunchLog ('Supervisor detected {0} unavailable on {1}; restarting it.' -f $service.Name, $service.Port) 'WARN'
|
|
243
272
|
}
|
|
244
273
|
Start-CrewService $service
|
|
274
|
+
if (-not $service.ManagedByBridge) { $officialRestarted = $true }
|
|
245
275
|
}
|
|
246
276
|
|
|
247
277
|
Wait-CrewServices
|
|
278
|
+
|
|
279
|
+
# The 3080 official bridge is the sole owner of the 3210 child. Trigger it
|
|
280
|
+
# only after 3080 is healthy, then wait on the direct 3210 runtime contract.
|
|
281
|
+
foreach ($service in ($services | Where-Object ManagedByBridge -eq $true | Where-Object State -ne 'ready')) {
|
|
282
|
+
$bridge = Start-BridgedCrewService
|
|
283
|
+
if (-not $bridge.Ready) {
|
|
284
|
+
$service.LastError = $bridge.Error
|
|
285
|
+
throw $bridge.Error
|
|
286
|
+
}
|
|
287
|
+
$service.State = 'starting'
|
|
288
|
+
$service.ConsecutiveFailures = 0
|
|
289
|
+
$service.LastError = $null
|
|
290
|
+
}
|
|
291
|
+
if (@($services | Where-Object State -eq 'starting').Count -gt 0) { Wait-CrewServices }
|
|
248
292
|
}
|
|
249
293
|
|
|
250
294
|
function Start-ServiceSupervisor {
|