@ran-sh/dsh-crew 2.2.0 → 2.2.1

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 (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/scripts/setup.mjs +6 -27
  4. package/scripts/verify-npm-install.mjs +2 -2
  5. package/src/adaptive-routing.mjs +2 -2
  6. package/src/ci-evidence.mjs +1 -1
  7. package/src/client/quick-panel.tsx +0 -1
  8. package/src/credential-lifecycle.mjs +0 -3
  9. package/src/credential-purge-state.mjs +1 -5
  10. package/src/credential-reference.mjs +0 -8
  11. package/src/crew-workspaces.mjs +1 -1
  12. package/src/delivery.mjs +2 -2
  13. package/src/dsh-cli-runtime.mjs +18 -22
  14. package/src/extension-contract.mjs +1 -1
  15. package/src/failure-classification.mjs +0 -10
  16. package/src/hub/entry.mjs +1 -1
  17. package/src/hub/index.mjs +0 -1
  18. package/src/i18n.mjs +0 -2
  19. package/src/install/crew-skill.mjs +1 -1
  20. package/src/install/install-legacy.mjs +3 -3
  21. package/src/install/npx-lifecycle.mjs +60 -112
  22. package/src/install/official-web.mjs +1 -1
  23. package/src/install/windows-startup.mjs +5 -5
  24. package/src/install/windows-supervisor-adapter.mjs +3 -2
  25. package/src/install/windows-supervisor-lifecycle.mjs +2 -2
  26. package/src/install/zcode.mjs +0 -2
  27. package/src/job-contracts.mjs +1 -1
  28. package/src/job-identity.mjs +2 -2
  29. package/src/jobs.mjs +3 -4
  30. package/src/model-routing.mjs +3 -4
  31. package/src/model-schedule.mjs +0 -1
  32. package/src/policy-legacy.mjs +10 -12
  33. package/src/provider-inventory.mjs +1 -1
  34. package/src/provider-layer-migration.mjs +0 -2
  35. package/src/readiness-matrix.mjs +1 -1
  36. package/src/release-in-use.mjs +1 -1
  37. package/src/role-profiles.mjs +2 -2
  38. package/src/runtime-controls.mjs +1 -1
  39. package/src/runtime-identity.mjs +1 -1
  40. package/src/status-shard.mjs +1 -1
  41. package/src/supervisor/restart-request.mjs +4 -4
  42. package/src/workflow-runtime.mjs +1 -2
  43. package/src/workspace-context.mjs +2 -2
  44. package/src/workspace-isolation.mjs +4 -4
  45. package/src/workspace-lock.mjs +2 -3
  46. package/scripts/policy-probe.mjs +0 -101
  47. package/scripts/verify-crew-ui-polish.mjs +0 -145
  48. package/scripts/verify-history-ui.mjs +0 -98
  49. package/scripts/verify-installer-fix.mjs +0 -26
@@ -44,9 +44,10 @@ import { samePayloadContent, capturePayloadContent } from './payload-content.mjs
44
44
  import { crewDshHome, crewProfileDir, claudeIntegrationLine } from './install.mjs';
45
45
  import { releaseClaimsState } from '../release-in-use.mjs';
46
46
  import { compareProcessToken, processStartToken } from '../process-identity.mjs';
47
+ import { RELEASE_COHORT_FILENAME } from '../dsh-cohort.mjs';
47
48
  import { renameTree } from './tree-move.mjs';
48
49
  import { checkRuntimeAdvance, normalizeRuntimeState, runtimeStateMayHaveStarted } from './runtime-lifecycle.mjs';
49
- import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
50
+ import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, gcRetainedRuntimes, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
50
51
  import {
51
52
  ensureOfficialWebIntegration,
52
53
  officialWebIntegrationStatus,
@@ -61,15 +62,15 @@ import {
61
62
  } from './windows-supervisor-adapter.mjs';
62
63
 
63
64
  export const CREW_APP_DIRNAME = 'app';
64
- export const RELEASES_DIRNAME = 'releases';
65
- export const CURRENT_POINTER_FILENAME = 'current.json';
66
- export const KEEP_RELEASES = 2;
65
+ const RELEASES_DIRNAME = 'releases';
66
+ const CURRENT_POINTER_FILENAME = 'current.json';
67
+ const KEEP_RELEASES = 2;
67
68
  export const INCOMPLETE_MARKER = '.dsh-crew-incomplete';
68
69
  const CREW_ROUTE_BASE = '/_dsh/dsh-crew';
69
70
  // Exact DSH cohort version: dotted numeric with optional -prerelease suffix.
70
71
  // Anything else (ranges, "../..", paths) is NOT an authorized cohort value
71
72
  // and must never reach rename/rmSync authority via retained-runtimes keys.
72
- export const EXACT_DSH_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
73
+ const EXACT_DSH_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
73
74
 
74
75
  export function npmCliInvocation(args, {
75
76
  platform = process.platform,
@@ -242,8 +243,8 @@ function writeCurrentPointer({ home, name, version, path }) {
242
243
  return pointer;
243
244
  }
244
245
 
245
- export const UPDATE_JOURNAL_FILENAME = 'update-journal.json';
246
- export const UPDATE_LOCK_FILENAME = 'update-in-progress.lock';
246
+ const UPDATE_JOURNAL_FILENAME = 'update-journal.json';
247
+ const UPDATE_LOCK_FILENAME = 'update-in-progress.lock';
247
248
 
248
249
  export function updateJournalFile({ home = homedir() } = {}) {
249
250
  return join(crewAppRoot({ home }), UPDATE_JOURNAL_FILENAME);
@@ -453,7 +454,7 @@ function writeUpdateJournal({ home, stage, prior = null, candidate = null, verif
453
454
  // identity matches the expected stage/prior/candidate. A crash between
454
455
  // verification success and this write leaves the journal unverified, so
455
456
  // reconcile refuses to finalize it.
456
- export function markJournalVerified({ home = homedir(), stage, prior = null, candidate = null, runtime = null } = {}) {
457
+ function markJournalVerified({ home = homedir(), stage, prior = null, candidate = null, runtime = null } = {}) {
457
458
  const current = readUpdateJournal({ home });
458
459
  if (!current || current.malformed) return { ok: false, code: 'JOURNAL_NOT_FOUND' };
459
460
  const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
@@ -481,82 +482,6 @@ function compensateActivationSync({ home, prior, manifest, log, installer }) {
481
482
  return { ok: true, version: manifest.version, path: prior.path };
482
483
  }
483
484
 
484
- // Undo a first-install candidate's activation surfaces: remove its profile
485
- // registration (dependency + bundle + junction) ONLY when each surface
486
- // still references THIS journal's candidate. The dependency must resolve
487
- // to exactly link:<candidateRealPath> (normalized slashes); a later
488
- // legitimate registration pointing elsewhere is never removed. Mixed or
489
- // unjudgeable state fails closed with the journal retained. When the
490
- // profile manifest is missing/unreadable the junction is still handled
491
- // independently so a "junction created, manifest never written" crash
492
- // cannot leave a dangling junction.
493
- function undoCandidateActivationSync({ home, candidateDir, candidateName }) {
494
- if (!candidateDir || !candidateName) return { ok: true, undone: false };
495
- const profileRoot = crewProfileDir({ home });
496
- const profileFile = join(profileRoot, 'package.json');
497
- let manifest = null;
498
- let manifestReadable = true;
499
- try { manifest = JSON.parse(readFileSync(profileFile, 'utf8')); } catch { manifest = null; manifestReadable = false; }
500
- const linkPath = join(profileRoot, 'node_modules', ...candidateName.split('/'));
501
- let linked = null;
502
- try {
503
- if (lstatSync(linkPath).isSymbolicLink()) linked = realpathSync(linkPath);
504
- } catch { linked = null; }
505
- let candidateReal = null;
506
- try { candidateReal = realpathSync(candidateDir); } catch { candidateReal = candidateDir; }
507
- const expectedDep = `link:${String(candidateReal).replace(/\\/g, '/')}`;
508
- const rawDep = manifest?.dependencies?.[candidateName];
509
- const depPointsAtCandidate = typeof rawDep === 'string'
510
- && rawDep.replace(/\\/g, '/') === expectedDep;
511
- const bundleNamesCandidate = Array.isArray(manifest?.dsh?.profile?.bundles) && manifest.dsh.profile.bundles.includes(candidateName);
512
- const linkPointsAtCandidate = linked !== null && linked === candidateReal;
513
- // Bundle carries no path: it NEVER grants deletion authority by itself.
514
- // It is removed only when dependency or junction proves THIS journal's
515
- // candidate still owns the registration. A re-pointed dep/junction with
516
- // a leftover same-name bundle fails closed (journal retained).
517
- const identityEvidence = depPointsAtCandidate || linkPointsAtCandidate;
518
- const bundlePointsAtCandidate = bundleNamesCandidate && identityEvidence;
519
- // Manifest unreadable but junction dangles at candidate: remove junction.
520
- if (!manifestReadable) {
521
- if (!linkPointsAtCandidate) return { ok: true, undone: false };
522
- try { rmSync(linkPath, { force: true }); } catch (error) {
523
- return { ok: false, code: 'CANDIDATE_UNDO_FAILED', error: String(error?.message ?? error) };
524
- }
525
- return { ok: true, undone: true };
526
- }
527
- if (!manifest) return { ok: true, undone: false };
528
- // Same-name package re-pointed elsewhere (dep or junction references a
529
- // different target) while the bundle still names it: mixed state that
530
- // cannot prove THIS candidate owns the registration. Fail closed,
531
- // retain the journal for operator inspection.
532
- const depPointsElsewhere = typeof rawDep === 'string' && !depPointsAtCandidate;
533
- const linkPointsElsewhere = linked !== null && !linkPointsAtCandidate;
534
- if ((depPointsElsewhere || linkPointsElsewhere) && bundleNamesCandidate) {
535
- return { ok: false, code: 'CANDIDATE_UNDO_AMBIGUOUS', error: 'same-name package re-pointed elsewhere; refusing bundle removal' };
536
- }
537
- if (!depPointsAtCandidate && !bundlePointsAtCandidate && !linkPointsAtCandidate) {
538
- return { ok: true, undone: false };
539
- }
540
- // Partial registration: only undo the surfaces that reference THIS
541
- // candidate; leave anything already re-pointed elsewhere untouched.
542
- try {
543
- const next = { ...manifest };
544
- if (depPointsAtCandidate) {
545
- next.dependencies = { ...manifest.dependencies };
546
- delete next.dependencies[candidateName];
547
- if (Object.keys(next.dependencies).length === 0) delete next.dependencies;
548
- }
549
- if (bundlePointsAtCandidate) {
550
- next.dsh = { ...manifest.dsh, profile: { ...manifest.dsh.profile, bundles: manifest.dsh.profile.bundles.filter((b) => b !== candidateName) } };
551
- }
552
- writeFileAtomic(profileFile, JSON.stringify(next, null, 2) + '\n');
553
- if (linkPointsAtCandidate) rmSync(linkPath, { force: true });
554
- } catch (error) {
555
- return { ok: false, code: 'CANDIDATE_UNDO_FAILED', error: String(error?.message ?? error) };
556
- }
557
- return { ok: true, undone: true };
558
- }
559
-
560
485
  // Canonical containment validation for a journal's runtime segment. Recovery
561
486
  // may rename/recursively-delete these roots, so liveRoot and retainedRoot
562
487
  // must be EXACTLY the canonical Crew-owned paths (resolved, never a suffix
@@ -760,6 +685,7 @@ export async function reconcileUpdateJournal({ home = homedir(), log = () => {},
760
685
  }
761
686
  clearUpdateJournal({ home });
762
687
  gcOldReleases({ home, protect: journal.prior?.path ?? null });
688
+ gcRetainedCohorts({ home });
763
689
  log(`- recovered update journal at stage ${journal.stage}: candidate ${candidateIdent.version} already committed, finalized`);
764
690
  return { ok: true, reconciled: true, stage: journal.stage, committed: true };
765
691
  }
@@ -803,6 +729,7 @@ export async function reconcileUpdateJournal({ home = homedir(), log = () => {},
803
729
  writeCurrentPointer({ home, name: candidateIdent.name, version: candidateIdent.version, path: candidateDir });
804
730
  clearUpdateJournal({ home });
805
731
  gcOldReleases({ home, protect: journal.prior?.path ?? null });
732
+ gcRetainedCohorts({ home });
806
733
  log(`- recovered update journal at stage ${journal.stage}: candidate ${candidateIdent.version} was verified and started, so the commit was completed`);
807
734
  return { ok: true, reconciled: true, stage: journal.stage, committed: true };
808
735
  }
@@ -1304,15 +1231,48 @@ export function commitActivatedRelease({ stageDir, manifest, home, prior = null
1304
1231
  writeCurrentPointer({ home, name: manifest.name, version: manifest.version, path: stageDir });
1305
1232
  clearUpdateJournal({ home });
1306
1233
  gcOldReleases({ home, protect: prior?.path ?? null });
1234
+ gcRetainedCohorts({ home });
1307
1235
  return stageDir;
1308
1236
  }
1309
1237
 
1310
- function commitStagedRelease({ stageDir, manifest, home, prior = null }) {
1311
- return commitActivatedRelease({ stageDir, manifest, home, prior });
1312
- }
1313
-
1314
1238
  const STALE_INCOMPLETE_MS = 24 * 60 * 60 * 1000;
1315
1239
 
1240
+ // A retained runtime cohort is needed only while some surviving release
1241
+ // resolves to it, so this runs next to release pruning at the commit points
1242
+ // that call gcOldReleases: dropping a release is what stops its cohort being
1243
+ // pinned. Resolution goes through resolveReleaseCohort, the same chain every
1244
+ // other caller uses (manifest pin, then the release-cohort.json sidecar), so a
1245
+ // release that names its cohort only in the sidecar keeps its runtime.
1246
+ //
1247
+ // Fail closed: the cohort set is trusted only when EVERY directory under
1248
+ // releases/ resolved, because "nothing pins it" and "could not tell" must not
1249
+ // prune the same way. Pruning is best-effort on top of that — it must never
1250
+ // turn an update that already committed its pointer into a failed one.
1251
+ //
1252
+ // performCoordinatedCohortUpdate commits without gcOldReleases and is
1253
+ // deliberately not covered here either; its prior tree is still the rollback
1254
+ // target at that point.
1255
+ function gcRetainedCohorts({ home }) {
1256
+ try {
1257
+ const releasesDir = crewReleasesDir({ home });
1258
+ if (!existsSync(releasesDir)) return [];
1259
+ const entries = readdirSync(releasesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
1260
+ const cohorts = [];
1261
+ for (const entry of entries) {
1262
+ const resolved = resolveReleaseCohort({ releaseDir: join(releasesDir, entry.name) });
1263
+ if (!resolved.ok) {
1264
+ process.emitWarning(`dsh-crew: release "${entry.name}" does not resolve to an exact cohort (${resolved.code ?? 'unknown'}); skipping retained-runtime pruning this pass`);
1265
+ return [];
1266
+ }
1267
+ cohorts.push(resolved.dshVersion);
1268
+ }
1269
+ return gcRetainedRuntimes({ home, cohorts });
1270
+ } catch (error) {
1271
+ process.emitWarning(`dsh-crew: retained-runtime pruning skipped: ${error?.message ?? error}`);
1272
+ return [];
1273
+ }
1274
+ }
1275
+
1316
1276
  function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
1317
1277
  const pointer = readCurrentPointer({ home });
1318
1278
  const releasesDir = crewReleasesDir({ home });
@@ -1362,7 +1322,7 @@ function gcOldReleases({ home, keep = KEEP_RELEASES, protect = null }) {
1362
1322
  return removed;
1363
1323
  }
1364
1324
 
1365
- export function listManagedReleases({ home = homedir() } = {}) {
1325
+ function listManagedReleases({ home = homedir() } = {}) {
1366
1326
  const pointer = readCurrentPointer({ home });
1367
1327
  const root = crewReleasesDir({ home });
1368
1328
  if (!existsSync(root)) return [];
@@ -1390,7 +1350,7 @@ export function listManagedReleases({ home = homedir() } = {}) {
1390
1350
  // Windows supervisor executes it and writes a VERIFIED result. This polls
1391
1351
  // until the result arrives (or the timeout elapses). NEVER talks to the
1392
1352
  // legacy 3080 supervisor endpoint.
1393
- export async function requestCrewRuntimeRestart({
1353
+ async function requestCrewRuntimeRestart({
1394
1354
  reason = null,
1395
1355
  fetchImpl = globalThis.fetch,
1396
1356
  pollIntervalMs = 1_000,
@@ -1442,18 +1402,6 @@ export async function requestCrewRuntimeRestart({
1442
1402
  }
1443
1403
  }
1444
1404
 
1445
- async function restartOwnedRuntime(fetchImpl = globalThis.fetch) {
1446
- return requestCrewRuntimeRestart({ fetchImpl, reason: 'lifecycle restart' });
1447
- }
1448
-
1449
- async function verifyRuntimeVersion(version, fetchImpl = globalThis.fetch) {
1450
- const response = await fetchImpl('http://127.0.0.1:3210/_dsh/dsh-crew/runtime', { headers: { accept: 'application/json' } });
1451
- const body = await response.json();
1452
- return response.ok && body?.ok === true && body.runtime_version === version
1453
- ? { ok: true, runtime_version: body.runtime_version }
1454
- : { ok: false, code: 'RUNTIME_VERSION_MISMATCH' };
1455
- }
1456
-
1457
1405
  // Cohort verifier: compares the TARGET DSH cohort against the Hub's
1458
1406
  // dsh_version domain (the installed @deepseek-ai/dsh package), NEVER
1459
1407
  // against Crew's own runtime_version (the dsh-crew release). A null
@@ -1509,7 +1457,7 @@ export async function verifyCrewRuntimeIdentity(version, fetchImpl = globalThis.
1509
1457
  // AND the expected DSH cohort (dsh_version domain), alongside full 3210
1510
1458
  // identity. A stale old-Crew process on the right cohort (or vice versa)
1511
1459
  // fails closed instead of reporting rollback success.
1512
- export async function verifyRollbackTarget({ crewVersion, dshVersion, fetchImpl = globalThis.fetch } = {}) {
1460
+ async function verifyRollbackTarget({ crewVersion, dshVersion, fetchImpl = globalThis.fetch } = {}) {
1513
1461
  let body = null;
1514
1462
  try {
1515
1463
  const response = await fetchImpl('http://127.0.0.1:3210/_dsh/dsh-crew/extension', { headers: { accept: 'application/json' } });
@@ -1910,10 +1858,10 @@ async function finishLifecycleAfterUpdateLock({
1910
1858
  // Lifecycle-owned cohort sidecar for a managed release directory. Historical
1911
1859
  // releases (pre-1.0.4) do not pin @deepseek-ai/dsh in their immutable
1912
1860
  // manifest; this records the cohort fact the lifecycle observed for them.
1913
- const cohortFile = (releaseDir) => join(releaseDir, 'release-cohort.json');
1861
+ const cohortFile = (releaseDir) => join(releaseDir, RELEASE_COHORT_FILENAME);
1914
1862
 
1915
1863
  // Exact DSH cohort version helper: delegates to the module-level regex.
1916
- export function isExactDshVersion(value) {
1864
+ function isExactDshVersion(value) {
1917
1865
  return typeof value === 'string' && EXACT_DSH_VERSION_RE.test(value);
1918
1866
  }
1919
1867
 
@@ -2242,7 +2190,7 @@ function registrationHealthy({ home, name, releaseDir }) {
2242
2190
  * config resolution. Package-manager children must see the user's real
2243
2191
  * environment, not our execution context.
2244
2192
  */
2245
- export function sanitizedPackageManagerEnv(baseEnv = process.env) {
2193
+ function sanitizedPackageManagerEnv(baseEnv = process.env) {
2246
2194
  const env = {};
2247
2195
  for (const [key, value] of Object.entries(baseEnv)) {
2248
2196
  if (/^npm_(config_|lifecycle|package_|execpath$|node_execpath$)/i.test(key)) continue;
@@ -2818,15 +2766,15 @@ async function npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime
2818
2766
  return { ok: true, version: manifest.version, path: releaseDir };
2819
2767
  }
2820
2768
 
2821
- export const UPDATE_PACKAGE_NAME = '@ran-sh/dsh-crew';
2822
- export const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
2769
+ const UPDATE_PACKAGE_NAME = '@ran-sh/dsh-crew';
2770
+ const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
2823
2771
 
2824
2772
  /**
2825
2773
  * Extract a packed npm tarball with the platform `tar` binary (bsdtar ships
2826
2774
  * with Windows 10+, macOS, and Linux) into destDir; npm tarballs always root
2827
2775
  * at `package/`.
2828
2776
  */
2829
- export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
2777
+ function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
2830
2778
  mkdirSync(destDir, { recursive: true });
2831
2779
  // GNU tar interprets a `C:\...` argument as a remote rsh host ("Cannot
2832
2780
  // connect to C:"), so anchor the invocation inside destDir and pass the
@@ -3449,9 +3397,9 @@ const CREDENTIAL_CAPABILITY_REQUIREMENTS = Object.freeze({
3449
3397
  'purge-plan': Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
3450
3398
  purge: Object.freeze(['credential-reference-inventory-v1', 'credential-purge-v1']),
3451
3399
  });
3452
- export const PRODUCTION_HUB_URL = 'http://127.0.0.1:3210';
3400
+ const PRODUCTION_HUB_URL = 'http://127.0.0.1:3210';
3453
3401
 
3454
- export async function assertProductionHub({ hubUrl, requiredCapabilities = [], purpose = 'command', fetchImpl = globalThis.fetch } = {}) {
3402
+ async function assertProductionHub({ hubUrl, requiredCapabilities = [], purpose = 'command', fetchImpl = globalThis.fetch } = {}) {
3455
3403
  if (hubUrl !== PRODUCTION_HUB_URL) throw new Error(`${purpose} requires the isolated 3210 Crew Hub`);
3456
3404
  const response = await fetchImpl(`${hubUrl}${CREW_ROUTE_BASE}/runtime`, { headers: { accept: 'application/json' } });
3457
3405
  let body;
@@ -3474,7 +3422,7 @@ export async function assertProductionHub({ hubUrl, requiredCapabilities = [], p
3474
3422
  * surface before sending a provider request. This prevents a same-port stale
3475
3423
  * Hub from turning a later 404 into an ambiguous destructive failure.
3476
3424
  */
3477
- export async function assertProviderHubCapabilities({ hubUrl, action, fetchImpl = globalThis.fetch } = {}) {
3425
+ async function assertProviderHubCapabilities({ hubUrl, action, fetchImpl = globalThis.fetch } = {}) {
3478
3426
  return assertProductionHub({
3479
3427
  hubUrl,
3480
3428
  requiredCapabilities: PROVIDER_CAPABILITY_REQUIREMENTS[action] ?? PROVIDER_CAPABILITY_REQUIREMENTS.list,
@@ -10,7 +10,7 @@ import { join } from 'node:path';
10
10
  export const OFFICIAL_BRIDGE_PACKAGE = '@ran-sh/dsh-crew-web-bridge';
11
11
  const STATE_FILENAME = 'official-web.json';
12
12
 
13
- export function officialWebProfileDir({ home = homedir() } = {}) {
13
+ function officialWebProfileDir({ home = homedir() } = {}) {
14
14
  return join(home, '.dsh', 'profiles', 'web');
15
15
  }
16
16
 
@@ -12,11 +12,11 @@ import { dirname, join, resolve } from 'node:path';
12
12
  import { homedir } from 'node:os';
13
13
  import { installOfficialFrontendAssets, officialFrontendAssetsReady } from './official-frontend-assets.mjs';
14
14
 
15
- export const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
16
- export const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
17
- export const WINDOWS_HELPER_FILENAME = 'start-dsh-crew.ps1';
18
- export const WINDOWS_CONTROL_FILENAME = 'supervisor-control.ps1';
19
- export const WINDOWS_SUPERVISOR_ASSET_MANIFEST = 'supervisor-assets.json';
15
+ const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
16
+ const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
17
+ const WINDOWS_HELPER_FILENAME = 'start-dsh-crew.ps1';
18
+ const WINDOWS_CONTROL_FILENAME = 'supervisor-control.ps1';
19
+ const WINDOWS_SUPERVISOR_ASSET_MANIFEST = 'supervisor-assets.json';
20
20
 
21
21
  function defaultStartupDir({ home, env }) {
22
22
  if (home === homedir() && env.APPDATA) {
@@ -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;
@@ -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;
@@ -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.1';
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}`;