@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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/setup.mjs +6 -27
- 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 +0 -1
- 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 +18 -22
- 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 +0 -1
- package/src/i18n.mjs +0 -2
- package/src/install/crew-skill.mjs +1 -1
- package/src/install/install-legacy.mjs +3 -3
- package/src/install/npx-lifecycle.mjs +60 -112
- 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/policy-legacy.mjs +10 -12
- package/src/provider-inventory.mjs +1 -1
- package/src/provider-layer-migration.mjs +0 -2
- 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
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2822
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3400
|
+
const PRODUCTION_HUB_URL = 'http://127.0.0.1:3210';
|
|
3453
3401
|
|
|
3454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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;
|
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;
|
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.1';
|
|
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}`;
|