@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.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/official-web-bridge/lib/client.js +1 -1
  3. package/official-web-bridge/overlay-entry.mjs +5 -1
  4. package/official-web-bridge/package.json +1 -1
  5. package/package.json +1 -1
  6. package/scripts/setup.mjs +39 -33
  7. package/scripts/verify-npm-install.mjs +2 -2
  8. package/src/adaptive-routing.mjs +2 -2
  9. package/src/ci-evidence.mjs +1 -1
  10. package/src/client/quick-panel.tsx +6 -2
  11. package/src/credential-lifecycle.mjs +0 -3
  12. package/src/credential-purge-state.mjs +1 -5
  13. package/src/credential-reference.mjs +0 -8
  14. package/src/crew-workspaces.mjs +1 -1
  15. package/src/delivery.mjs +2 -2
  16. package/src/dsh-cli-runtime.mjs +227 -30
  17. package/src/extension-contract.mjs +1 -1
  18. package/src/failure-classification.mjs +0 -10
  19. package/src/hub/entry.mjs +1 -1
  20. package/src/hub/index.mjs +57 -7
  21. package/src/i18n.mjs +0 -2
  22. package/src/install/crew-paths.mjs +16 -11
  23. package/src/install/crew-skill.mjs +1 -1
  24. package/src/install/desktop-profile.mjs +168 -0
  25. package/src/install/harness-config-import.mjs +204 -0
  26. package/src/install/install-legacy.mjs +3 -3
  27. package/src/install/npx-lifecycle.mjs +210 -125
  28. package/src/install/official-web.mjs +1 -1
  29. package/src/install/windows-startup.mjs +5 -5
  30. package/src/install/windows-supervisor-adapter.mjs +3 -2
  31. package/src/install/windows-supervisor-lifecycle.mjs +2 -2
  32. package/src/install/zcode.mjs +0 -2
  33. package/src/job-contracts.mjs +1 -1
  34. package/src/job-identity.mjs +2 -2
  35. package/src/jobs.mjs +3 -4
  36. package/src/model-routing.mjs +3 -4
  37. package/src/model-schedule.mjs +0 -1
  38. package/src/official-web-bridge.mjs +3 -1
  39. package/src/policy-legacy.mjs +10 -12
  40. package/src/provider-inventory.mjs +1 -1
  41. package/src/provider-layer-migration.mjs +0 -2
  42. package/src/provider-settings-store.mjs +19 -1
  43. package/src/readiness-matrix.mjs +1 -1
  44. package/src/release-in-use.mjs +1 -1
  45. package/src/role-profiles.mjs +2 -2
  46. package/src/runtime-controls.mjs +1 -1
  47. package/src/runtime-identity.mjs +1 -1
  48. package/src/status-shard.mjs +1 -1
  49. package/src/supervisor/restart-request.mjs +4 -4
  50. package/src/workflow-runtime.mjs +1 -2
  51. package/src/workspace-context.mjs +2 -2
  52. package/src/workspace-isolation.mjs +4 -4
  53. package/src/workspace-lock.mjs +2 -3
  54. package/scripts/policy-probe.mjs +0 -101
  55. package/scripts/verify-crew-ui-polish.mjs +0 -145
  56. package/scripts/verify-history-ui.mjs +0 -98
  57. 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
- export function canonicalWindowsPowerShellPath({
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, 'release-cohort.json'), 'utf8'));
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
- export const WINDOWS_SUPERVISOR_HANDOFF_SCHEMA = 1;
14
- export const WINDOWS_SUPERVISOR_HANDOFF_PHASES = Object.freeze([
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',
@@ -395,5 +395,3 @@ export function uninstallZCode({ home = homedir(), env = process.env } = {}) {
395
395
  }
396
396
  return { ok: true, actions };
397
397
  }
398
-
399
- export function zcodeOwnershipFile({ home = homedir() } = {}) { return OWNERSHIP_FILE({ home }); }
@@ -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
- export const JOB_CONTRACT_SCHEMA_VERSION = 1;
8
+ const JOB_CONTRACT_SCHEMA_VERSION = 1;
9
9
 
10
10
  export const JOB_EVENT_TYPES = Object.freeze([
11
11
  'job.created',
@@ -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
- export const JOB_NAME_PREFIX = 'Crew_';
23
- export const JOB_PURPOSE_MAX = 32;
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
- export const TIERS = {
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
  };
@@ -7,7 +7,7 @@
7
7
  import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
8
8
  import { scheduleAdmission } from './model-schedule.mjs';
9
9
 
10
- export const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
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
- export const MODEL_SELECTION_TRACE_VERSION = 1;
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
- export function normalizeModelRef(raw) {
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() : '';
@@ -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
- full_control_plane_url: 'http://127.0.0.1:3210/',
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
  });
@@ -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
- export const MAIN_AGENT_MODES = ['direct-allowed', 'coordinator-first', 'dispatcher-only'];
25
- export const ROLE_IDS = [
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
- export const POLICY_ERROR_MESSAGES = {
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
- export const DEFAULT_WORKER_STRATEGY = 'balanced';
96
- export const DEFAULT_REVIEWER_STRATEGY = 'strong';
93
+ const DEFAULT_WORKER_STRATEGY = 'balanced';
94
+ const DEFAULT_REVIEWER_STRATEGY = 'strong';
97
95
  export const DEFAULT_MAX_PARALLEL = 3;
98
96
 
99
- export function normalizeRoleState(raw) {
97
+ function normalizeRoleState(raw) {
100
98
  return normalizeState(raw);
101
99
  }
102
100
 
103
- export function normalizeIsolation(raw) {
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
- export function normalizeModelPolicy(raw = {}) {
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
- export function policyError(code, extra = {}) {
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
- export function getTierRoles(config, tier) {
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
- export const IMMUTABLE_HARNESS_PROVIDER_IDS = Object.freeze(['deepseek-official']);
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
- if (!parsed.ok) return { ok: false, code: parsed.code };
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) => {
@@ -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
- export const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_RUN']);
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',
@@ -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
- export const IN_USE_DIRNAME = 'in-use';
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
@@ -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
- export const ROLE_PROFILE_SCHEMA_VERSION = 1;
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
- export function roleProfilesFile({ home = homedir() } = {}) {
48
+ function roleProfilesFile({ home = homedir() } = {}) {
49
49
  return join(home, '.config', 'dsh-crew', 'profiles.json');
50
50
  }
51
51
 
@@ -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
- export const RUNTIME_SETTING_ACTIVATION = Object.freeze({
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 }),
@@ -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.0';
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([
@@ -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
- export const SHARD_FRESH_MS = 30 * 60 * 1000;
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
- export const RESTART_REQUEST_SCHEMA = 1;
22
- export const RESTART_REQUEST_TTL_MS = 60_000;
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
- export function restartRequestsDir(appRoot) {
28
+ function restartRequestsDir(appRoot) {
29
29
  return join(supervisorStateRoot(appRoot), 'restart-requests');
30
30
  }
31
31
 
32
- export function restartResultsDir(appRoot) {
32
+ function restartResultsDir(appRoot) {
33
33
  return join(supervisorStateRoot(appRoot), 'restart-results');
34
34
  }
35
35
 
@@ -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, TERMINAL };
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
- export const WORKSPACE_CONTEXT_SCHEMA_VERSION = 1;
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
- export function workspaceContextsFile({ home = homedir() } = {}) {
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
- export const WORKTREE_RESERVE_FAILED = 'WORKTREE_RESERVE_FAILED';
31
+ const WORKTREE_RESERVE_FAILED = 'WORKTREE_RESERVE_FAILED';
32
32
  export const CANDIDATE_CAPTURE_FAILED = 'CANDIDATE_CAPTURE_FAILED';
33
- export const MAX_PARALLEL_CAP = 16;
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
- export const WORKTREE_CLEANUP_RETRIES = 3;
646
- export const WORKTREE_CLEANUP_BACKOFF_MS = 150;
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?
@@ -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
- export const WORKSPACE_LOCK_FILE = 'workspace.lock';
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
- export const DEFAULT_LOCK_POLL_MS = 500;
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
@@ -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);