@ran-sh/dsh-crew 2.2.0 → 2.2.2
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/.claude-plugin/plugin.json +1 -1
- package/official-web-bridge/lib/client.js +1 -1
- package/official-web-bridge/overlay-entry.mjs +5 -1
- package/official-web-bridge/package.json +1 -1
- package/package.json +1 -1
- package/scripts/setup.mjs +39 -33
- package/scripts/verify-npm-install.mjs +2 -2
- package/src/adaptive-routing.mjs +2 -2
- package/src/ci-evidence.mjs +1 -1
- package/src/client/quick-panel.tsx +6 -2
- package/src/credential-lifecycle.mjs +0 -3
- package/src/credential-purge-state.mjs +1 -5
- package/src/credential-reference.mjs +0 -8
- package/src/crew-workspaces.mjs +1 -1
- package/src/delivery.mjs +2 -2
- package/src/dsh-cli-runtime.mjs +227 -30
- package/src/extension-contract.mjs +1 -1
- package/src/failure-classification.mjs +0 -10
- package/src/hub/entry.mjs +1 -1
- package/src/hub/index.mjs +57 -7
- package/src/i18n.mjs +0 -2
- package/src/install/crew-paths.mjs +16 -11
- package/src/install/crew-skill.mjs +1 -1
- package/src/install/desktop-profile.mjs +168 -0
- package/src/install/harness-config-import.mjs +204 -0
- package/src/install/install-legacy.mjs +3 -3
- package/src/install/npx-lifecycle.mjs +210 -125
- package/src/install/official-web.mjs +1 -1
- package/src/install/windows-startup.mjs +5 -5
- package/src/install/windows-supervisor-adapter.mjs +3 -2
- package/src/install/windows-supervisor-lifecycle.mjs +2 -2
- package/src/install/zcode.mjs +0 -2
- package/src/job-contracts.mjs +1 -1
- package/src/job-identity.mjs +2 -2
- package/src/jobs.mjs +3 -4
- package/src/model-routing.mjs +3 -4
- package/src/model-schedule.mjs +0 -1
- package/src/official-web-bridge.mjs +3 -1
- package/src/policy-legacy.mjs +10 -12
- package/src/provider-inventory.mjs +1 -1
- package/src/provider-layer-migration.mjs +0 -2
- package/src/provider-settings-store.mjs +19 -1
- package/src/readiness-matrix.mjs +1 -1
- package/src/release-in-use.mjs +1 -1
- package/src/role-profiles.mjs +2 -2
- package/src/runtime-controls.mjs +1 -1
- package/src/runtime-identity.mjs +1 -1
- package/src/status-shard.mjs +1 -1
- package/src/supervisor/restart-request.mjs +4 -4
- package/src/workflow-runtime.mjs +1 -2
- package/src/workspace-context.mjs +2 -2
- package/src/workspace-isolation.mjs +4 -4
- package/src/workspace-lock.mjs +2 -3
- package/scripts/policy-probe.mjs +0 -101
- package/scripts/verify-crew-ui-polish.mjs +0 -145
- package/scripts/verify-history-ui.mjs +0 -98
- package/scripts/verify-installer-fix.mjs +0 -26
|
@@ -5,6 +5,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { join, win32 } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
|
|
8
|
+
import { RELEASE_COHORT_FILENAME } from '../dsh-cohort.mjs';
|
|
8
9
|
import {
|
|
9
10
|
readMaintenanceSession,
|
|
10
11
|
readSupervisorHeartbeat,
|
|
@@ -47,7 +48,7 @@ export function hashSupervisorHelper(file) {
|
|
|
47
48
|
try { return createHash('sha256').update(readFileSync(file)).digest('hex'); } catch { return null; }
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
function canonicalWindowsPowerShellPath({
|
|
51
52
|
environment = process.env,
|
|
52
53
|
exists = existsSync,
|
|
53
54
|
} = {}) {
|
|
@@ -344,7 +345,7 @@ function prepareWindowsSupervisorConvergence({ home, root, platform }) {
|
|
|
344
345
|
let expectedDshVersion = text(manifest?.dependencies?.['@deepseek-ai/dsh'] ?? manifest?.peerDependencies?.['@deepseek-ai/dsh']);
|
|
345
346
|
if (!EXACT_VERSION_RE.test(expectedDshVersion ?? '')) {
|
|
346
347
|
try {
|
|
347
|
-
const cohort = JSON.parse(readFileSync(join(root,
|
|
348
|
+
const cohort = JSON.parse(readFileSync(join(root, RELEASE_COHORT_FILENAME), 'utf8'));
|
|
348
349
|
expectedDshVersion = text(cohort?.dsh_version);
|
|
349
350
|
} catch { expectedDshVersion = null; }
|
|
350
351
|
}
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
11
|
import { compareProcessToken, processStartToken } from '../process-identity.mjs';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const WINDOWS_SUPERVISOR_HANDOFF_SCHEMA = 1;
|
|
14
|
+
const WINDOWS_SUPERVISOR_HANDOFF_PHASES = Object.freeze([
|
|
15
15
|
'planned',
|
|
16
16
|
'backend-stopped',
|
|
17
17
|
'old-watcher-stopped',
|
package/src/install/zcode.mjs
CHANGED
package/src/job-contracts.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// internal details remain available only through an explicit detail=full
|
|
6
6
|
// request at the MCP boundary.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const JOB_CONTRACT_SCHEMA_VERSION = 1;
|
|
9
9
|
|
|
10
10
|
export const JOB_EVENT_TYPES = Object.freeze([
|
|
11
11
|
'job.created',
|
package/src/job-identity.mjs
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// give them all one title. There the workspace groups the sessions and this name
|
|
20
20
|
// tells them apart.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const JOB_NAME_PREFIX = 'Crew_';
|
|
23
|
+
const JOB_PURPOSE_MAX = 32;
|
|
24
24
|
|
|
25
25
|
export const JOB_NAME_RE = /^Crew_\d{8}_\d{6}_[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*(?:-\d+)?$/;
|
|
26
26
|
|
package/src/jobs.mjs
CHANGED
|
@@ -14,8 +14,8 @@ import { createShardWriter } from './status-shard.mjs';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { dirname, join, resolve, basename } from 'node:path';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
|
-
import { appendDeliveryInstructions, prependJobIdentity, parseDeliveryReport, formatDeliveryMetadata } from './delivery.mjs';
|
|
18
|
-
import { jobDisplayName } from './job-identity.mjs';
|
|
17
|
+
import { appendDeliveryInstructions, prependJobIdentity, parseDeliveryReport, formatDeliveryMetadata } from './delivery.mjs';
|
|
18
|
+
import { jobDisplayName } from './job-identity.mjs';
|
|
19
19
|
import { isCrewWorktreeName } from './workspace-isolation.mjs';
|
|
20
20
|
import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from './workspace-audit.mjs';
|
|
21
21
|
import { buildOutcome, JOB_PHASES } from './workflow.mjs';
|
|
@@ -28,7 +28,6 @@ import { readGlobalConfig } from './install/install.mjs';
|
|
|
28
28
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
29
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
30
30
|
const CREW_DSH_HOME = join(homedir(), '.config', 'dsh-crew', 'harness');
|
|
31
|
-
const STATUS_FILE = join(CONFIG_DIR, 'status.json');
|
|
32
31
|
const CORDIS = join(ROOT, 'worker.cordis.yml');
|
|
33
32
|
|
|
34
33
|
// Read the operator's schedule once per dispatch. An unreadable config must not
|
|
@@ -38,7 +37,7 @@ function readModelSchedule() {
|
|
|
38
37
|
try { return normalizeModelSchedule(readGlobalConfig().model_schedule); } catch { return normalizeModelSchedule(undefined); }
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
const TIERS = {
|
|
42
41
|
flash: { model: 'deepseek-v4-flash', label: 'V4 Flash' },
|
|
43
42
|
pro: { model: 'deepseek-v4-pro', label: 'V4 Pro' },
|
|
44
43
|
};
|
package/src/model-routing.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
|
|
8
8
|
import { scheduleAdmission } from './model-schedule.mjs';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
|
|
11
11
|
flash: 'deepseek-v4-flash',
|
|
12
12
|
pro: 'deepseek-v4-pro',
|
|
13
13
|
});
|
|
@@ -20,9 +20,8 @@ export const DEFAULT_ROLE_MODEL_PREFERENCES = Object.freeze({
|
|
|
20
20
|
reviewer: 'deepseek-v4-pro',
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
export const MODEL_FALLBACKS = ['harness-default'];
|
|
24
23
|
export const NO_WORKER_MODEL_AVAILABLE = 'NO_WORKER_MODEL_AVAILABLE';
|
|
25
|
-
|
|
24
|
+
const MODEL_SELECTION_TRACE_VERSION = 1;
|
|
26
25
|
export const MODEL_SELECTION_REASON_CODES = Object.freeze({
|
|
27
26
|
PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
|
|
28
27
|
PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
|
|
@@ -61,7 +60,7 @@ const BLOCKED_MODEL_CODES = Object.freeze({
|
|
|
61
60
|
[MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED]: 'MODEL_BLOCKED_PEAK',
|
|
62
61
|
});
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
function normalizeModelRef(raw) {
|
|
65
64
|
if (!raw || typeof raw !== 'object') return null;
|
|
66
65
|
const provider = typeof raw.provider === 'string' ? raw.provider.trim() : '';
|
|
67
66
|
const model = typeof raw.model === 'string' ? raw.model.trim() : '';
|
package/src/model-schedule.mjs
CHANGED
|
@@ -27,7 +27,6 @@ export const DEFAULT_PEAK_WINDOWS = Object.freeze([
|
|
|
27
27
|
|
|
28
28
|
export const DEFAULT_TIMEZONE_OFFSET_MINUTES = 8 * 60;
|
|
29
29
|
export const DEFAULT_PEAK_WEEKDAYS = Object.freeze([1, 2, 3, 4, 5]);
|
|
30
|
-
export const MINUTES_PER_DAY = 24 * 60;
|
|
31
30
|
// A fixed offset is bounded by the real-world range of UTC offsets; anything
|
|
32
31
|
// wider is a typo, not a timezone.
|
|
33
32
|
const MAX_OFFSET_MINUTES = 14 * 60;
|
|
@@ -406,7 +406,9 @@ export function registerOfficialWebBridge(ctx, options = {}) {
|
|
|
406
406
|
ui_role: 'quick-controls',
|
|
407
407
|
execution_plane: 'hub-3210',
|
|
408
408
|
listen_port: 3210,
|
|
409
|
-
|
|
409
|
+
// The usable entry: the hub mints its own session and redirects, because
|
|
410
|
+
// a bare port asks for a token only the hub's own process can sign.
|
|
411
|
+
full_control_plane_url: 'http://127.0.0.1:3210/_dsh/dsh-crew/control-plane',
|
|
410
412
|
});
|
|
411
413
|
},
|
|
412
414
|
});
|
package/src/policy-legacy.mjs
CHANGED
|
@@ -21,8 +21,8 @@ import { normalizeModelPriority } from './model-routing.mjs';
|
|
|
21
21
|
|
|
22
22
|
export const TIER_STATES = ['disabled', 'manual', 'auto'];
|
|
23
23
|
export const COLLABORATION_MODES = ['flash-only', 'pro-only', 'balanced', 'review-pipeline', 'custom'];
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const MAIN_AGENT_MODES = ['direct-allowed', 'coordinator-first', 'dispatcher-only'];
|
|
25
|
+
const ROLE_IDS = [
|
|
26
26
|
'implementation',
|
|
27
27
|
'simple_fix',
|
|
28
28
|
'tests',
|
|
@@ -52,7 +52,7 @@ export const POLICY_ERROR_CODES = {
|
|
|
52
52
|
ROLE_TIER_CONFLICT: 'ROLE_TIER_CONFLICT',
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const POLICY_ERROR_MESSAGES = {
|
|
56
56
|
[POLICY_ERROR_CODES.SUBAGENTS_DISABLED]:
|
|
57
57
|
'DSH Crew worker dispatch is disabled.',
|
|
58
58
|
[POLICY_ERROR_CODES.TIER_DISABLED]:
|
|
@@ -89,18 +89,16 @@ export const POLICY_ERROR_MESSAGES = {
|
|
|
89
89
|
// Everything new here reads a canonical config shape produced by
|
|
90
90
|
// migrateLegacyConfig; the legacy collab/tier functions stay for compatibility.
|
|
91
91
|
|
|
92
|
-
export const DISPATCH_ROLES = ['worker', 'reviewer'];
|
|
93
|
-
export const ROLE_STATES = TIER_STATES; // disabled | manual | auto
|
|
94
92
|
export const ROLE_MODEL_STRATEGIES = ['economy', 'balanced', 'quality', 'strong'];
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
const DEFAULT_WORKER_STRATEGY = 'balanced';
|
|
94
|
+
const DEFAULT_REVIEWER_STRATEGY = 'strong';
|
|
97
95
|
export const DEFAULT_MAX_PARALLEL = 3;
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
function normalizeRoleState(raw) {
|
|
100
98
|
return normalizeState(raw);
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
function normalizeIsolation(raw) {
|
|
104
102
|
return raw === 'worktree' || raw === 'shared' ? raw : 'worktree';
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -117,7 +115,7 @@ function normalizeMaxParallel(raw) {
|
|
|
117
115
|
* escalation_priority_configured, fallback, escalation: { enabled, max_attempts } }.
|
|
118
116
|
* Pure; unknown fields drop, empty priority falls back to the role default.
|
|
119
117
|
*/
|
|
120
|
-
|
|
118
|
+
function normalizeModelPolicy(raw = {}) {
|
|
121
119
|
return {
|
|
122
120
|
role: raw.role === 'reviewer' ? 'reviewer' : 'worker',
|
|
123
121
|
strategy: ROLE_MODEL_STRATEGIES.includes(raw.strategy)
|
|
@@ -444,7 +442,7 @@ export function resolveHubWorkerProvider({ worker_provider_mode, getCurrentSelec
|
|
|
444
442
|
}
|
|
445
443
|
|
|
446
444
|
/** Structured policy error; `code` is machine-readable, `message` user-facing. */
|
|
447
|
-
|
|
445
|
+
function policyError(code, extra = {}) {
|
|
448
446
|
const base = POLICY_ERROR_MESSAGES[code] ?? code;
|
|
449
447
|
return Object.assign(new Error(base.replace('{tier}', extra.tier ?? 'pro')), { policyCode: code });
|
|
450
448
|
}
|
|
@@ -677,7 +675,7 @@ export function shouldRunProReview(config, session = {}) {
|
|
|
677
675
|
}
|
|
678
676
|
|
|
679
677
|
/** Roles for a tier (host guidance for who does what, not a hard classifier). */
|
|
680
|
-
|
|
678
|
+
function getTierRoles(config, tier) {
|
|
681
679
|
const roles = tier === 'flash' ? config.flash_roles : config.pro_roles;
|
|
682
680
|
return normalizeRoles(roles, tier === 'flash' ? DEFAULT_FLASH_ROLES : DEFAULT_PRO_ROLES);
|
|
683
681
|
}
|
|
@@ -9,7 +9,7 @@ import { classifyCredentialReference } from './credential-reference.mjs';
|
|
|
9
9
|
const LIFECYCLE_ORIGINS = new Set(['builtin', 'profile-managed', 'dynamic', 'unknown']);
|
|
10
10
|
const OWNERSHIPS = new Set(['harness', 'crew-managed-profile', 'user-managed-profile', 'dynamic-user', 'unknown']);
|
|
11
11
|
const MUTABLE_AUTHORITY_KINDS = new Set(['crew-profile', 'harness-settings']);
|
|
12
|
-
|
|
12
|
+
const IMMUTABLE_HARNESS_PROVIDER_IDS = Object.freeze(['deepseek-official']);
|
|
13
13
|
|
|
14
14
|
function text(value) {
|
|
15
15
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -120,8 +120,6 @@ export function hasProviderLayerMigration(plan) {
|
|
|
120
120
|
return Array.isArray(plan?.providers) && plan.providers.length > 0;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const MIGRATION_STATES = Object.freeze(['PLANNED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING', 'VERIFIED', 'FAILED']);
|
|
124
|
-
|
|
125
123
|
function migrationCode(error, fallback = 'PROVIDER_MIGRATION_FAILED') {
|
|
126
124
|
const code = text(error?.code);
|
|
127
125
|
return code && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : fallback;
|
|
@@ -382,9 +382,27 @@ export function inspectProviderSettings(source) {
|
|
|
382
382
|
return { ok: true, schema: 'harness-settings-llm-pi-ai-v1', providerIds: parsed.entries.map((entry) => entry.id), revision };
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
/** Whether the settings document carries a Harness provider section at all. */
|
|
386
|
+
export function hasProviderSettingsSection(source) {
|
|
387
|
+
return typeof source === 'string' && source.split(/\r?\n/u).some((line) => /^llm-pi-ai:\s*$/u.test(line));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Whether the settings document declares a Harness default-model authority. */
|
|
391
|
+
export function hasHarnessDefaultBlock(source) {
|
|
392
|
+
return typeof source === 'string' && source.split(/\r?\n/u).some((line) => /^agent-default-model:\s*$/u.test(line));
|
|
393
|
+
}
|
|
394
|
+
|
|
385
395
|
export function readProviderSettingsDeclarations(source, { file = 'harness/settings.yaml' } = {}) {
|
|
386
396
|
const parsed = parseProviderMap(source);
|
|
387
|
-
|
|
397
|
+
// A settings file with no `llm-pi-ai:` section declares no providers. That is
|
|
398
|
+
// not an unsupported schema — it is the state every home starts in and the
|
|
399
|
+
// state a provider migration writes into — so only a section that EXISTS but
|
|
400
|
+
// cannot be parsed is refused.
|
|
401
|
+
if (!parsed.ok) {
|
|
402
|
+
return parsed.code === 'PROVIDER_SETTINGS_SCHEMA_UNSUPPORTED' && !hasProviderSettingsSection(source)
|
|
403
|
+
? { ok: true, declarations: [], absent: true, file }
|
|
404
|
+
: { ok: false, code: parsed.code };
|
|
405
|
+
}
|
|
388
406
|
return {
|
|
389
407
|
ok: true,
|
|
390
408
|
declarations: parsed.entries.map((entry) => {
|
package/src/readiness-matrix.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// (Hub handshake / catalog read) are intentionally separate from execution
|
|
6
6
|
// verification (real worker, reviewer, cancellation, timeout, etc.).
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_RUN']);
|
|
9
9
|
|
|
10
10
|
export const READINESS_REASON_CODES = Object.freeze({
|
|
11
11
|
LIVE_CHECK_PASSED: 'LIVE_CHECK_PASSED',
|
package/src/release-in-use.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { homedir } from 'node:os';
|
|
|
22
22
|
import { dirname, join, resolve } from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const IN_USE_DIRNAME = 'in-use';
|
|
26
26
|
|
|
27
27
|
// A claim file is `<pid>.json` or `<pid>-<nonce>.json`. The PID alone is not
|
|
28
28
|
// enough to identify a claim: two mounts inside one process would publish to the
|
package/src/role-profiles.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const ROLE_PROFILE_SCHEMA_VERSION = 1;
|
|
10
10
|
export const DEFAULT_ROLE_PROFILES = Object.freeze({
|
|
11
11
|
'worker-default': Object.freeze({
|
|
12
12
|
role: 'worker', routing: 'auto', isolation: 'worktree', fallback: true,
|
|
@@ -45,7 +45,7 @@ function normalizeProfile(id, raw) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function roleProfilesFile({ home = homedir() } = {}) {
|
|
49
49
|
return join(home, '.config', 'dsh-crew', 'profiles.json');
|
|
50
50
|
}
|
|
51
51
|
|
package/src/runtime-controls.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const R = ACTIVATION_BOUNDARY.RESTART_REQUIRED;
|
|
|
22
22
|
* running MCP process. `session: null` means the setting is not a session
|
|
23
23
|
* override surface.
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
const RUNTIME_SETTING_ACTIVATION = Object.freeze({
|
|
26
26
|
max_parallel: Object.freeze({ global: L, session: null, note: 'Admission limit refreshes before new workflow starts and when slots release; lowering never cancels active work.' }),
|
|
27
27
|
|
|
28
28
|
subagents_enabled: Object.freeze({ global: N, session: N }),
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export {
|
|
|
36
36
|
// included in the identity contract.
|
|
37
37
|
const RUNTIME_ID = randomUUID();
|
|
38
38
|
|
|
39
|
-
export const RUNTIME_VERSION = '2.2.
|
|
39
|
+
export const RUNTIME_VERSION = '2.2.2';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
package/src/status-shard.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { homedir } from 'node:os';
|
|
|
9
9
|
|
|
10
10
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
11
11
|
const SHARD_DIR = join(CONFIG_DIR, 'status.d');
|
|
12
|
-
|
|
12
|
+
const SHARD_FRESH_MS = 30 * 60 * 1000;
|
|
13
13
|
|
|
14
14
|
export function createShardWriter(kind) {
|
|
15
15
|
const writer = `${kind}-${process.pid}`;
|
|
@@ -18,18 +18,18 @@ import { randomUUID } from 'node:crypto';
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const RESTART_REQUEST_SCHEMA = 1;
|
|
22
|
+
const RESTART_REQUEST_TTL_MS = 60_000;
|
|
23
23
|
|
|
24
24
|
export function supervisorStateRoot(appRoot) {
|
|
25
25
|
return join(appRoot, 'supervisor');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function restartRequestsDir(appRoot) {
|
|
29
29
|
return join(supervisorStateRoot(appRoot), 'restart-requests');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
function restartResultsDir(appRoot) {
|
|
33
33
|
return join(supervisorStateRoot(appRoot), 'restart-results');
|
|
34
34
|
}
|
|
35
35
|
|
package/src/workflow-runtime.mjs
CHANGED
|
@@ -31,7 +31,6 @@ export const WORKFLOW_ERROR_CODES = {
|
|
|
31
31
|
REVIEWER_MUTATED_CANDIDATE: 'REVIEWER_MUTATED_CANDIDATE',
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
const TERMINAL = new Set([JOB_PHASES.COMPLETED, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED, JOB_PHASES.INTERRUPTED]);
|
|
35
34
|
|
|
36
35
|
function lines(value) {
|
|
37
36
|
if (typeof value !== 'string' || value.trim() === '') return [];
|
|
@@ -817,4 +816,4 @@ export function createWorkflowRuntime(adapters, {
|
|
|
817
816
|
return { start, wait, get, list, cancel, runtimeState, setMaxParallel, refreshRuntimeControls };
|
|
818
817
|
}
|
|
819
818
|
|
|
820
|
-
export { JOB_PHASES
|
|
819
|
+
export { JOB_PHASES };
|
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const WORKSPACE_CONTEXT_SCHEMA_VERSION = 1;
|
|
10
10
|
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
11
11
|
|
|
12
12
|
export function isSafeBranchName(value) {
|
|
@@ -51,7 +51,7 @@ function normalizeContext(id, raw) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
function workspaceContextsFile({ home = homedir() } = {}) {
|
|
55
55
|
return join(home, '.config', 'dsh-crew', 'workspaces.json');
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -28,9 +28,9 @@ export const GIT_ERROR = 'GIT_ERROR';
|
|
|
28
28
|
/** A valid repository whose HEAD does not resolve because nothing is committed. */
|
|
29
29
|
export const REPOSITORY_HAS_NO_COMMITS = 'REPOSITORY_HAS_NO_COMMITS';
|
|
30
30
|
export const WORKTREE_LOCKED = 'WORKTREE_LOCKED';
|
|
31
|
-
|
|
31
|
+
const WORKTREE_RESERVE_FAILED = 'WORKTREE_RESERVE_FAILED';
|
|
32
32
|
export const CANDIDATE_CAPTURE_FAILED = 'CANDIDATE_CAPTURE_FAILED';
|
|
33
|
-
|
|
33
|
+
const MAX_PARALLEL_CAP = 16;
|
|
34
34
|
export const DEFAULT_MAX_PARALLEL = 3;
|
|
35
35
|
// Worktree names read `Crew_YYYYMMDD_HHMMSS_<purpose>` so an operator can tell
|
|
36
36
|
// from the directory alone when a job ran and what it was for. Worktrees from
|
|
@@ -642,8 +642,8 @@ async function ownershipDrift(run, worktreePath, expect) {
|
|
|
642
642
|
* the registration is gone and no directory remains — success is never claimed
|
|
643
643
|
* while a worktree stays registered or on disk.
|
|
644
644
|
*/
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
const WORKTREE_CLEANUP_RETRIES = 3;
|
|
646
|
+
const WORKTREE_CLEANUP_BACKOFF_MS = 150;
|
|
647
647
|
|
|
648
648
|
/**
|
|
649
649
|
* Is `worktreePath` a worktree Crew created, for this repository?
|
package/src/workspace-lock.mjs
CHANGED
|
@@ -23,11 +23,10 @@ import { dirname } from 'node:path';
|
|
|
23
23
|
import { randomBytes } from 'node:crypto';
|
|
24
24
|
|
|
25
25
|
export const WORKSPACE_BUSY = 'WORKSPACE_BUSY';
|
|
26
|
-
|
|
27
|
-
export const WORKSPACE_LOCK_SCHEMA = 1;
|
|
26
|
+
const WORKSPACE_LOCK_SCHEMA = 1;
|
|
28
27
|
|
|
29
28
|
export const DEFAULT_LOCK_WAIT_MS = 120_000;
|
|
30
|
-
|
|
29
|
+
const DEFAULT_LOCK_POLL_MS = 500;
|
|
31
30
|
// The two reasons a lock outlives its job are not the same, and they are covered
|
|
32
31
|
// by different things. A holder whose process died is caught immediately by the
|
|
33
32
|
// pid check below. This bound covers the other case — the process is alive but
|
package/scripts/policy-probe.mjs
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Functional probe of the real MCP stdio server (src/server.mjs), no DSH
|
|
2
|
-
// instance and no DeepSeek credentials needed: policy rejections must happen
|
|
3
|
-
// before any worker runtime is touched.
|
|
4
|
-
// Run: node scripts/policy-probe.mjs
|
|
5
|
-
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
|
-
|
|
8
|
-
const child = spawn(process.execPath, ['src/server.mjs'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
9
|
-
let buffer = '';
|
|
10
|
-
const pending = new Map();
|
|
11
|
-
let nextId = 1;
|
|
12
|
-
|
|
13
|
-
child.stdout.on('data', (d) => {
|
|
14
|
-
buffer += d.toString();
|
|
15
|
-
let idx;
|
|
16
|
-
while ((idx = buffer.indexOf('\n')) >= 0) {
|
|
17
|
-
const line = buffer.slice(0, idx);
|
|
18
|
-
buffer = buffer.slice(idx + 1);
|
|
19
|
-
if (!line.trim()) continue;
|
|
20
|
-
let msg;
|
|
21
|
-
try { msg = JSON.parse(line); } catch { continue; }
|
|
22
|
-
if (msg.id !== undefined && pending.has(msg.id)) {
|
|
23
|
-
pending.get(msg.id)(msg);
|
|
24
|
-
pending.delete(msg.id);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
function call(method, params) {
|
|
30
|
-
return new Promise((resolve) => {
|
|
31
|
-
const id = nextId++;
|
|
32
|
-
pending.set(id, resolve);
|
|
33
|
-
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const results = [];
|
|
38
|
-
function check(name, cond, detail) {
|
|
39
|
-
results.push({ name, ok: !!cond, detail });
|
|
40
|
-
console.log(`${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
await call('initialize', { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'probe', version: '1' } });
|
|
44
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
45
|
-
|
|
46
|
-
// 1. Read config: must expose the new effective policy fields.
|
|
47
|
-
const cfg = await call('tools/call', { name: 'dsh_worker_config', arguments: {} });
|
|
48
|
-
const cfgText = cfg.result?.content?.[0]?.text;
|
|
49
|
-
let cfgJson = null;
|
|
50
|
-
try { cfgJson = JSON.parse(cfgText); } catch {}
|
|
51
|
-
check('config returns effective policy', !!cfgJson && typeof cfgJson.flash_state === 'string' && typeof cfgJson.pro_state === 'string' && typeof cfgJson.routing_guidance === 'string',
|
|
52
|
-
cfgJson ? `flash=${cfgJson.flash_state} pro=${cfgJson.pro_state} mode=${cfgJson.collaboration_mode}` : cfgText?.slice(0, 120));
|
|
53
|
-
check('config hides API keys', cfgText !== undefined && !/api_key|sk-/.test(cfgText));
|
|
54
|
-
check('config reports effective_policy + roles + legacy_source', !!cfgJson
|
|
55
|
-
&& typeof cfgJson.effective_policy === 'string' && cfgJson.effective_policy.includes('mode=')
|
|
56
|
-
&& Array.isArray(cfgJson.flash_roles) && cfgJson.flash_roles.length > 0
|
|
57
|
-
&& Array.isArray(cfgJson.pro_roles) && cfgJson.pro_roles.length > 0
|
|
58
|
-
&& typeof cfgJson.legacy_source === 'string',
|
|
59
|
-
cfgJson ? cfgJson.effective_policy : undefined);
|
|
60
|
-
check('config reflects main_agent_mode', !!cfgJson && ['direct-allowed', 'coordinator-first', 'dispatcher-only'].includes(cfgJson.main_agent_mode), cfgJson?.main_agent_mode);
|
|
61
|
-
|
|
62
|
-
// 2. Session clamp: flash-only + explicit pro must be refused with TIER_DISABLED.
|
|
63
|
-
const clamp = await call('tools/call', { name: 'dsh_worker_config', arguments: { tier_policy: 'flash-only' } });
|
|
64
|
-
check('session tier_policy clamp set', clamp.result !== undefined && !clamp.result.isError);
|
|
65
|
-
|
|
66
|
-
const refusePro = await call('tools/call', { name: 'dsh_run_worker', arguments: { task: 'probe', tier: 'pro' } });
|
|
67
|
-
const refuseText = refusePro.result?.content?.[0]?.text ?? '';
|
|
68
|
-
check('pro refused under flash-only', /TIER_DISABLED/.test(refuseText), refuseText.slice(0, 120));
|
|
69
|
-
|
|
70
|
-
// 3. subagents master switch via session enabled=false.
|
|
71
|
-
const off = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true, enabled: false } });
|
|
72
|
-
check('session enabled=false set', off.result !== undefined && !off.result.isError);
|
|
73
|
-
const refuseAll = await call('tools/call', { name: 'dsh_run_worker', arguments: { task: 'probe' } });
|
|
74
|
-
const refuseAllText = refuseAll.result?.content?.[0]?.text ?? '';
|
|
75
|
-
check('dispatch refused when disabled', /SUBAGENTS_DISABLED/.test(refuseAllText), refuseAllText.slice(0, 120));
|
|
76
|
-
|
|
77
|
-
// 4. dsh_spawn_worker shares the same policy.
|
|
78
|
-
const refuseSpawn = await call('tools/call', { name: 'dsh_spawn_worker', arguments: { task: 'probe' } });
|
|
79
|
-
const refuseSpawnText = refuseSpawn.result?.content?.[0]?.text ?? '';
|
|
80
|
-
check('spawn refused when disabled', /SUBAGENTS_DISABLED/.test(refuseSpawnText));
|
|
81
|
-
|
|
82
|
-
// 5. Manual semantics: flash manual + pro auto, no explicit tier → pro is chosen, never flash.
|
|
83
|
-
const manual = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true, tier_policy: 'auto' } });
|
|
84
|
-
// global config is balanced here; exercise manual via session state override:
|
|
85
|
-
const manualSet = await call('tools/call', { name: 'dsh_worker_config', arguments: { collaboration_mode: 'custom', flash_state: 'manual', pro_state: 'auto', default_tier: 'flash' } });
|
|
86
|
-
check('manual/custom session set', manualSet.result !== undefined && !manualSet.result.isError);
|
|
87
|
-
const manualCfg = await call('tools/call', { name: 'dsh_worker_config', arguments: {} });
|
|
88
|
-
const manualCfgJson = JSON.parse(manualCfg.result.content[0].text);
|
|
89
|
-
check('manual tier never auto-default (effective_default_tier=pro)',
|
|
90
|
-
manualCfgJson.effective_default_tier === 'pro', `got ${manualCfgJson.effective_default_tier}`);
|
|
91
|
-
check('effective states reported', manualCfgJson.flash_state === 'manual' && manualCfgJson.pro_state === 'auto');
|
|
92
|
-
|
|
93
|
-
// 6. Reset back to global defaults.
|
|
94
|
-
const reset = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true } });
|
|
95
|
-
check('reset restores defaults', reset.result !== undefined && !reset.result.isError);
|
|
96
|
-
|
|
97
|
-
child.kill();
|
|
98
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
99
|
-
const failed = results.filter((r) => !r.ok);
|
|
100
|
-
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
|
|
101
|
-
process.exit(failed.length > 0 ? 1 : 0);
|