@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
@@ -35,7 +35,7 @@ import {
35
35
  // Re-exported so existing importers keep working while the cohort value now
36
36
  // lives in exactly one place (src/dsh-cohort.mjs).
37
37
  export { DSH_CLI_PACKAGE, TARGET_DSH_VERSION, TARGET_DSH_SPEC };
38
- export const CREW_DSH_RUNTIME_DIRNAME = 'runtime';
38
+ const CREW_DSH_RUNTIME_DIRNAME = 'runtime';
39
39
  const CREW_PROFILE_DEFAULT_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'];
40
40
  const PROFILE_PATCH_TEMPLATE = '[]\n';
41
41
  const PROFILE_PNPM_WORKSPACE = 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n';
@@ -73,11 +73,6 @@ export function crewDshRuntimeVersionDir({ home = homedir(), version = TARGET_DS
73
73
  return join(crewDshHome({ home }), `runtime-${version}`);
74
74
  }
75
75
 
76
- export function crewDshRuntimeEntry({ home = homedir(), platform = process.platform } = {}) {
77
- const suffix = platform === 'win32' ? '.cmd' : '';
78
- return join(crewDshRuntimeRoot({ home }), 'node_modules', '.bin', `dsh${suffix}`);
79
- }
80
-
81
76
  export function crewDshRuntimeModule({ home = homedir() } = {}) {
82
77
  return join(crewDshRuntimeRoot({ home }), 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js');
83
78
  }
@@ -225,14 +220,29 @@ export function ensureCrewDshRuntime({
225
220
  platform = process.platform,
226
221
  comspec = process.env.ComSpec ?? 'cmd.exe',
227
222
  env = process.env,
223
+ read = readFileSync,
224
+ write = writeFileSync,
225
+ log = () => {},
228
226
  } = {}) {
229
- const existing = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false });
227
+ const existing = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false, read });
230
228
  // Reuse only when the installed runtime already matches the requested
231
229
  // cohort. A stale cohort (e.g. an online 0.1.1-rc.2 runtime) must never be
232
230
  // mistaken for the target, and it must never be upgraded in place under a
233
231
  // live hub.
234
232
  if (existing?.kind === 'crew-runtime' && existing?.version === version) {
235
- return { ok: true, cli: existing, reused: true };
233
+ // The core matching is not enough: a tree whose family drifted boots that
234
+ // core against siblings from another cohort and dies on the first import
235
+ // the newer ones dropped. Report it as the cohort mismatch it is so the
236
+ // caller runs the staged (re)install instead of handing it to the launcher.
237
+ const cohort = verifyCrewRuntimeCohort({ root: crewDshRuntimeRoot({ home }), version, read });
238
+ if (cohort.ok) return { ok: true, cli: existing, reused: true };
239
+ return {
240
+ ok: false,
241
+ code: 'DSH_RUNTIME_COHORT_MISMATCH',
242
+ error: `Crew runtime family drifted from ${version}: ${describeCohortDrift(cohort.drift)}`,
243
+ installed: existing.version ?? null,
244
+ target: version,
245
+ };
236
246
  }
237
247
  if (existing?.kind === 'crew-runtime') {
238
248
  return {
@@ -256,13 +266,14 @@ export function ensureCrewDshRuntime({
256
266
  ? ['add', '--dir', runtimeRoot, '--ignore-scripts', packageSpec]
257
267
  : ['install', '--prefix', runtimeRoot, '--no-package-lock', '--ignore-scripts', '--omit=dev', packageSpec];
258
268
  const invocation = buildDshInvocation(packageManager, packageArgs, { platform, comspec });
259
- const result = runner(invocation.command, invocation.args, {
269
+ const runInstall = () => runner(invocation.command, invocation.args, {
260
270
  encoding: 'utf8',
261
271
  stdio: ['ignore', 'pipe', 'pipe'],
262
272
  shell: invocation.shell,
263
273
  env: { ...env },
264
274
  });
265
- const cli = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false });
275
+ const result = runInstall();
276
+ const cli = resolveDshCli({ home, env, platform, exists, findCommand, includeCompatibility: false, read });
266
277
  if (result.status !== 0 || !cli || cli.kind !== 'crew-runtime') {
267
278
  return {
268
279
  ok: false,
@@ -283,7 +294,30 @@ export function ensureCrewDshRuntime({
283
294
  target: version,
284
295
  };
285
296
  }
286
- return { ok: true, cli, reused: false, version: cli.version, runtimeRoot };
297
+ // The core is pinned by the install spec, but npm resolves its siblings from
298
+ // the core's caret ranges: a registry that published a newer prerelease of the
299
+ // family produces a mixed tree here, so lock and re-verify the family before
300
+ // this runtime can be reported reusable.
301
+ const locked = enforceCohortLock({ root: runtimeRoot, version, runInstall, read, write, log });
302
+ if (locked.failed) {
303
+ return {
304
+ ok: false,
305
+ code: 'DSH_RUNTIME_INSTALL_FAILED',
306
+ error: 'Crew-owned DSH runtime install failed',
307
+ status: locked.failed.status ?? -1,
308
+ stderrTail: String(locked.failed.stderr || locked.failed.stdout || '').trim().split(/\r?\n/).slice(-3).join(' | ').slice(0, 300),
309
+ };
310
+ }
311
+ if (!locked.ok) {
312
+ return {
313
+ ok: false,
314
+ code: 'DSH_RUNTIME_COHORT_INCONSISTENT',
315
+ error: `installed Crew runtime family does not match ${version}: ${describeCohortDrift(locked.drift)}`,
316
+ cohort: { drift: locked.drift },
317
+ target: version,
318
+ };
319
+ }
320
+ return { ok: true, cli, reused: false, version: cli.version, runtimeRoot, ...(locked.repairs ? { cohortRepaired: true } : {}) };
287
321
  }
288
322
 
289
323
  export function describeDshCli(cli) {
@@ -292,6 +326,104 @@ export function describeDshCli(cli) {
292
326
  return `${cli.kind}${version}`;
293
327
  }
294
328
 
329
+ // ---- installed cohort identity ---------------------------------------------
330
+ // The runtime tree is a COHORT, not a single package: @deepseek-ai/dsh plus the
331
+ // @deepseek-ai/dsh-* family it loads. The install pins the core exactly, but npm
332
+ // still resolves the family from the core's own caret ranges, so a registry that
333
+ // has published a newer prerelease of the family yields a MIXED tree — core
334
+ // alpha.1 with alpha.2 siblings — that installs cleanly and then dies at boot:
335
+ // the alpha.1 core imports exports the alpha.2 siblings no longer provide. Both
336
+ // the reuse gate and the install therefore judge every family member, and a tree
337
+ // that drifted is locked with overrides and reinstalled once before anything is
338
+ // reported reusable.
339
+
340
+ // A family package may carry its own nested copy of a sibling, and node
341
+ // resolves that copy FIRST for its parent — so a nested copy at the wrong
342
+ // version breaks the parent exactly like a top-level one. npm does produce
343
+ // these (a reconcile against a mixed tree nested whole subtrees), so the walk
344
+ // follows family packages down through their own node_modules and stops before
345
+ // it can descend into unrelated trees.
346
+ const COHORT_NESTED_DEPTH = 3;
347
+
348
+ function installedCohortEntries(root, read = readFileSync) {
349
+ const entries = [];
350
+ const visit = (scope, prefix, depth) => {
351
+ let names;
352
+ try { names = readdirSync(scope); } catch { return; }
353
+ for (const name of names) {
354
+ if (name !== 'dsh' && !name.startsWith('dsh-')) continue;
355
+ const dir = join(scope, name);
356
+ entries.push({ name: `@deepseek-ai/${name}`, version: readPackageVersionAt(dir, read), scope: prefix || 'node_modules' });
357
+ if (depth >= COHORT_NESTED_DEPTH) continue;
358
+ const nested = join(dir, 'node_modules', '@deepseek-ai');
359
+ if (existsSync(nested)) visit(nested, `${prefix ? `${prefix}/` : ''}${name}/node_modules`, depth + 1);
360
+ }
361
+ };
362
+ visit(join(root, 'node_modules', '@deepseek-ai'), '', 0);
363
+ return entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
364
+ }
365
+
366
+ /**
367
+ * Judge every installed copy of the family against the pinned cohort version.
368
+ * `drift` names each copy whose on-disk version differs (an unreadable manifest
369
+ * reads as null), so callers can report the exact packages instead of a bare
370
+ * mismatch.
371
+ */
372
+ export function verifyCrewRuntimeCohort({ root, version, read = readFileSync } = {}) {
373
+ const entries = installedCohortEntries(root, read);
374
+ const drift = entries.filter((entry) => entry.version !== version);
375
+ return { ok: entries.length > 0 && drift.length === 0, entries, drift };
376
+ }
377
+
378
+ function describeCohortDrift(drift) {
379
+ return drift
380
+ .map((entry) => (entry.scope && entry.scope !== 'node_modules'
381
+ ? `${entry.name}@${entry.version ?? 'missing'} (nested in ${entry.scope})`
382
+ : `${entry.name}@${entry.version ?? 'missing'}`))
383
+ .join(', ');
384
+ }
385
+
386
+ // Lock the whole family to the cohort version. npm v11 rejects an override that
387
+ // shares a name with a direct dependency unless the spec repeats it verbatim, so
388
+ // the core keeps its own exact dependency entry and is left out of `overrides`;
389
+ // pnpm reads overrides from its own field, so both are written.
390
+ function writeCohortOverrides({ root, version, names, read = readFileSync, write = writeFileSync }) {
391
+ const overrides = {};
392
+ for (const name of names) {
393
+ if (name === DSH_CLI_PACKAGE) continue;
394
+ overrides[name] = version;
395
+ }
396
+ if (Object.keys(overrides).length === 0) return false;
397
+ const manifestFile = join(root, 'package.json');
398
+ let manifest = null;
399
+ try { manifest = JSON.parse(read(manifestFile, 'utf8')); } catch { manifest = null; }
400
+ if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) manifest = {};
401
+ const next = {
402
+ ...manifest,
403
+ dependencies: { ...(manifest.dependencies ?? {}), [DSH_CLI_PACKAGE]: version },
404
+ overrides,
405
+ pnpm: { ...(manifest.pnpm ?? {}), overrides },
406
+ };
407
+ write(manifestFile, `${JSON.stringify(next, null, 2)}\n`);
408
+ return true;
409
+ }
410
+
411
+ // Prove the freshly installed tree is a coherent cohort. A drifting family is
412
+ // locked with overrides and reinstalled exactly ONCE; a tree that still drifts is
413
+ // reported to the caller, never handed to the launcher as reusable.
414
+ function enforceCohortLock({ root, version, runInstall, read = readFileSync, write = writeFileSync, log = () => {} }) {
415
+ const observed = verifyCrewRuntimeCohort({ root, version, read });
416
+ if (observed.ok) return { ok: true, repairs: 0 };
417
+ if (!writeCohortOverrides({ root, version, names: observed.entries.map((entry) => entry.name), read, write })) {
418
+ return { ok: false, drift: observed.drift };
419
+ }
420
+ log(` DSH cohort drifted (${describeCohortDrift(observed.drift)}); locking the family with overrides`);
421
+ const result = runInstall();
422
+ if (result?.status !== 0) return { ok: false, failed: result };
423
+ const after = verifyCrewRuntimeCohort({ root, version, read });
424
+ return after.ok ? { ok: true, repairs: 1 } : { ok: false, drift: after.drift };
425
+ }
426
+
295
427
  // Shared pnpm/npm install of the pinned DSH cohort into a specific root
296
428
  // directory. pnpm materializes ABSOLUTE-path .cmd shims and junctions that
297
429
  // point at the install root's own .pnpm store, so the tree must be installed
@@ -310,6 +442,9 @@ export function installDshInto({
310
442
  platform = process.platform,
311
443
  comspec = process.env.ComSpec ?? 'cmd.exe',
312
444
  env = process.env,
445
+ read = readFileSync,
446
+ write = writeFileSync,
447
+ log = () => {},
313
448
  }) {
314
449
  const pnpm = pnpmCommand ?? findCommand('pnpm');
315
450
  const npm = npmCommand ?? findCommand('npm');
@@ -322,12 +457,13 @@ export function installDshInto({
322
457
  ? ['add', '--dir', root, '--ignore-scripts', packageSpec]
323
458
  : ['install', '--prefix', root, '--no-package-lock', '--ignore-scripts', '--omit=dev', packageSpec];
324
459
  const invocation = buildDshInvocation(packageManager, packageArgs, { platform, comspec });
325
- const result = runner(invocation.command, invocation.args, {
460
+ const runInstall = () => runner(invocation.command, invocation.args, {
326
461
  encoding: 'utf8',
327
462
  stdio: ['ignore', 'pipe', 'pipe'],
328
463
  shell: invocation.shell,
329
464
  env: { ...env },
330
465
  });
466
+ const result = runInstall();
331
467
  if (result.status !== 0) {
332
468
  return {
333
469
  ok: false,
@@ -341,7 +477,7 @@ export function installDshInto({
341
477
  if (!exists(moduleEntry)) {
342
478
  return { ok: false, code: 'DSH_RUNTIME_INSTALL_INCOMPLETE', error: 'runtime entry missing after install' };
343
479
  }
344
- const installedVersion = packageVersion(moduleEntry);
480
+ const installedVersion = packageVersion(moduleEntry, read);
345
481
  if (installedVersion !== version) {
346
482
  return {
347
483
  ok: false,
@@ -351,7 +487,26 @@ export function installDshInto({
351
487
  target: version,
352
488
  };
353
489
  }
354
- return { ok: true, root, version: installedVersion };
490
+ const locked = enforceCohortLock({ root, version, runInstall, read, write, log });
491
+ if (locked.failed) {
492
+ return {
493
+ ok: false,
494
+ code: 'DSH_RUNTIME_INSTALL_FAILED',
495
+ error: 'Crew runtime install failed',
496
+ status: locked.failed.status ?? -1,
497
+ stderrTail: String(locked.failed.stderr || locked.failed.stdout || '').trim().split(/\r?\n/).slice(-3).join(' | ').slice(0, 300),
498
+ };
499
+ }
500
+ if (!locked.ok) {
501
+ return {
502
+ ok: false,
503
+ code: 'DSH_RUNTIME_COHORT_INCONSISTENT',
504
+ error: `installed Crew runtime family does not match ${version}: ${describeCohortDrift(locked.drift)}`,
505
+ cohort: { drift: locked.drift },
506
+ target: version,
507
+ };
508
+ }
509
+ return { ok: true, root, version: installedVersion, ...(locked.repairs ? { cohortRepaired: true } : {}) };
355
510
  }
356
511
 
357
512
  // Staged cohort migration: install the target cohort into a versioned
@@ -736,35 +891,36 @@ export async function restoreRetainedRuntime({
736
891
  }
737
892
 
738
893
  // GC retained runtimes that no release pins. A retained cohort is needed only
739
- // while some managed release (current or retained) declares it as its exact
740
- // @deepseek-ai/dsh dependency. Best-effort; never throws.
741
- export function gcRetainedRuntimes({ home = homedir(), releases = [], log = () => {} } = {}) {
894
+ // while some managed release resolves to it, so the caller passes the resolved
895
+ // cohort versions (see resolveReleaseCohort: manifest pin, then the
896
+ // release-cohort.json sidecar). Best-effort; never throws — a root that is
897
+ // missing, unreadable or not a directory prunes nothing rather than failing
898
+ // the update that just committed.
899
+ export function gcRetainedRuntimes({ home = homedir(), cohorts = [], log = () => {} } = {}) {
742
900
  const root = retainedRuntimesRoot({ home });
743
- if (!existsSync(root)) return [];
744
- const needed = new Set();
745
- for (const release of releases) {
746
- const spec = payloadDshSpec(release);
747
- if (spec && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(spec)) needed.add(spec);
748
- }
901
+ const needed = new Set(cohorts.filter((spec) => typeof spec === 'string' && EXACT_COHORT_RE.test(spec)));
902
+ let names;
903
+ try { names = readdirSync(root); } catch { return []; }
749
904
  const removed = [];
750
- for (const name of readdirSync(root)) {
905
+ for (const name of names) {
906
+ if (needed.has(name)) continue;
751
907
  const dir = join(root, name);
752
- if (!needed.has(name)) {
753
- try { rmSync(dir, { recursive: true, force: true }); removed.push(name); } catch { /* best effort */ }
754
- }
908
+ try { rmSync(dir, { recursive: true, force: true }); removed.push(name); } catch { /* best effort */ }
755
909
  }
756
910
  return removed;
757
911
  }
758
912
 
913
+ const EXACT_COHORT_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
914
+
759
915
  // Read the exact @deepseek-ai/dsh pin from a payload manifest (dependencies
760
916
  // first, then peerDependencies). Exact pins only: a range or absence yields
761
917
  // null so callers fail closed instead of guessing a cohort.
762
918
  function payloadDshSpec(manifest) {
763
919
  if (!manifest || typeof manifest !== 'object') return null;
764
920
  const direct = manifest.dependencies?.['@deepseek-ai/dsh'];
765
- if (typeof direct === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(direct)) return direct;
921
+ if (typeof direct === 'string' && EXACT_COHORT_RE.test(direct)) return direct;
766
922
  const peer = manifest.peerDependencies?.['@deepseek-ai/dsh'];
767
- if (typeof peer === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(peer)) return peer;
923
+ if (typeof peer === 'string' && EXACT_COHORT_RE.test(peer)) return peer;
768
924
  return null;
769
925
  }
770
926
 
@@ -964,7 +1120,7 @@ function ensureDirectoryLink(linkPath, targetRoot) {
964
1120
  * command. The profile manifest and one loader-visible directory link are the
965
1121
  * only state changed; no dependency resolution or policy bypass is attempted.
966
1122
  */
967
- export function ensurePluginRegistration({
1123
+ function ensurePluginRegistration({
968
1124
  profileRoot,
969
1125
  root,
970
1126
  name,
@@ -1088,3 +1244,44 @@ export function removeCrewPluginRegistration({ home = homedir(), name, profileRo
1088
1244
  if (linkStat) unlinkSync(linkPath);
1089
1245
  return { ok: true, removed: true };
1090
1246
  }
1247
+
1248
+ /**
1249
+ * Idempotently materialize the Crew-owned Harness `web` profile scaffold.
1250
+ *
1251
+ * `ensureCrewDshRuntime` supplies the CLI but nothing else a managed 3210/3080
1252
+ * boot needs: the Windows launcher preflight and the supervisor both require
1253
+ * `<crew DSH_HOME>/profiles/web/package.json`, and a fresh install that never
1254
+ * created it deadlocked (supervisor heartbeat timeout -> pending handoff ->
1255
+ * every later update refused). The scaffold is the same shape the plugin
1256
+ * registration writes for the dsh-crew profile: an existing valid manifest is
1257
+ * left untouched, a missing one is created from CREW_PROFILE_DEFAULT_BUNDLES,
1258
+ * and a corrupt one fails closed instead of being overwritten.
1259
+ */
1260
+ export function ensureCrewWebProfile({
1261
+ home = homedir(),
1262
+ profileName = 'web',
1263
+ defaultBundles = CREW_PROFILE_DEFAULT_BUNDLES,
1264
+ } = {}) {
1265
+ if (typeof profileName !== 'string' || !/^[A-Za-z0-9._-]+$/u.test(profileName)) {
1266
+ return { ok: false, code: 'INVALID_PROFILE_NAME' };
1267
+ }
1268
+ const profileRoot = join(crewDshHome({ home }), 'profiles', profileName);
1269
+ const profile = readProfileManifestForRegistration(profileRoot, { create: true, defaultBundles });
1270
+ if (profile.ok === false) return { ok: false, code: profile.code, profileRoot };
1271
+ let scaffoldChanged = false;
1272
+ try {
1273
+ scaffoldChanged = ensureProfileScaffold(profileRoot);
1274
+ if (profile.created) {
1275
+ writeFileSync(profile.profileManifest, JSON.stringify(profile.manifest, null, 2) + '\n');
1276
+ }
1277
+ } catch (error) {
1278
+ return { ok: false, code: 'CREW_WEB_PROFILE_SCAFFOLD_FAILED', error: String(error?.message ?? error), profileRoot };
1279
+ }
1280
+ return {
1281
+ ok: true,
1282
+ profileRoot,
1283
+ profileManifest: profile.profileManifest,
1284
+ created: profile.created === true,
1285
+ changed: profile.created === true || scaffoldChanged,
1286
+ };
1287
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { validateModelCallabilityV2 } from './model-callability-contract.mjs';
6
6
 
7
- export const EXTENSION_CONTRACT_SCHEMA_VERSION = 1;
7
+ const EXTENSION_CONTRACT_SCHEMA_VERSION = 1;
8
8
 
9
9
  function row(matrix, id) {
10
10
  return Array.isArray(matrix?.rows) ? matrix.rows.find((entry) => entry?.id === id) : undefined;
@@ -3,16 +3,6 @@
3
3
  // Classification consumes structured status/codes/outcomes only. It never
4
4
  // parses raw exception text, provider responses, credentials, quotas or logs.
5
5
 
6
- export const FAILURE_CATEGORIES = Object.freeze([
7
- 'none',
8
- 'policy',
9
- 'compatibility',
10
- 'provider',
11
- 'runtime',
12
- 'verification',
13
- 'cancelled',
14
- ]);
15
-
16
6
  export const FAILURE_REASON_CODES = Object.freeze({
17
7
  NONE: 'NONE',
18
8
  CANCELLED: 'CANCELLED',
package/src/hub/entry.mjs CHANGED
@@ -68,7 +68,7 @@ export function recordAdaptiveJobOutcome(job, store = getProcessAdaptiveHealthSt
68
68
  * a chained promise preserves all legacy completion/waiter behavior and runs
69
69
  * this observer only after the Hub's own finalizer populated endedAt/status.
70
70
  */
71
- export function installAdaptiveHealthObserver() {
71
+ function installAdaptiveHealthObserver() {
72
72
  if (WorkerRegistry.prototype[ADAPTIVE_OBSERVER_INSTALLED] === true) return false;
73
73
  const originalSpawn = WorkerRegistry.prototype.spawn;
74
74
  Object.defineProperty(WorkerRegistry.prototype, ADAPTIVE_OBSERVER_INSTALLED, {
package/src/hub/index.mjs CHANGED
@@ -40,7 +40,7 @@ import { raceWaiters } from '../removable-waiter.mjs';
40
40
  import { buildProviderInventory } from '../provider-inventory.mjs';
41
41
  import { buildProviderLayerMigrationPlan, executeProviderLayerMigration } from '../provider-layer-migration.mjs';
42
42
  import { inspectProviderProfile, readProviderDeclarations, readProviderMaterialization } from '../provider-profile-store.mjs';
43
- import { inspectProviderSettings, readHarnessDefault, readProviderSettingsDeclarations, readProviderSettingsMaterialization } from '../provider-settings-store.mjs';
43
+ import { hasHarnessDefaultBlock, inspectProviderSettings, readHarnessDefault, readProviderSettingsDeclarations, readProviderSettingsMaterialization } from '../provider-settings-store.mjs';
44
44
  import { normalizeProviderLifecycleState } from '../provider-lifecycle-state.mjs';
45
45
  import { createProviderHealthStore } from '../provider-health.mjs';
46
46
  import { planProviderDelete } from '../provider-lifecycle.mjs';
@@ -119,7 +119,6 @@ export const QUICK_CONFIG_KEYS = Object.freeze([
119
119
  const LEGACY_TIER_MODELS = { flash: 'deepseek-v4-flash', pro: 'deepseek-v4-pro' };
120
120
  // Local copy (the hub must not import jobs.mjs, which pulls the DSH SDK into
121
121
  // the profile realm): a valid dispatch role set.
122
- const ROLES = { worker: true, reviewer: true };
123
122
  const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
124
123
  const CREDENTIAL_PURGE_STATE_FILE = join(CONFIG_DIR, 'credential-purge-lifecycle.json');
125
124
  const RECOVERY_ACTION_PATTERN = /^[a-f0-9]{32}$/u;
@@ -252,6 +251,26 @@ export function hasCompleteProviderDeclarationEvidence(evidence) {
252
251
  && Object.values(evidence.sources ?? {}).every((source) => source?.present !== true || !source.code);
253
252
  }
254
253
 
254
+ /**
255
+ * Decide what the Harness default authority evidence means. A persisted
256
+ * `agent-default-model` that disagrees with the live default is a real
257
+ * inconsistency and fails closed; a live default with nothing persisted is the
258
+ * normal state of a home whose model comes from configuration (the profile and
259
+ * base layers) — and the state every settings mutation materializes FROM, so
260
+ * demanding a persisted copy first deadlocked the very migration that writes one.
261
+ */
262
+ export function projectHarnessDefaultEvidence({ liveDefault = null, persistedDefault = null, persistedDefaultDeclared = false } = {}) {
263
+ if (!liveDefault && !persistedDefault) return { ok: true, source: 'none' };
264
+ if (liveDefault && persistedDefault) {
265
+ return liveDefault.provider === persistedDefault.provider && liveDefault.model === persistedDefault.model
266
+ ? { ok: true, source: 'harness-settings' }
267
+ : { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_MISMATCH' };
268
+ }
269
+ return persistedDefaultDeclared
270
+ ? { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_MISMATCH' }
271
+ : { ok: true, source: 'harness-config' };
272
+ }
273
+
255
274
  export function hasAvailableProviderLifecycleEvidence(evidence) {
256
275
  return evidence?.ok === true;
257
276
  }
@@ -341,6 +360,7 @@ async function readProviderInventorySnapshot(hub, ctx, config) {
341
360
  } catch { declarationEvidence.ok = false; declarationEvidence.sources.profile = { present: true, code: 'PROVIDER_SOURCE_UNAVAILABLE' }; }
342
361
  const settingsFile = join(CONFIG_DIR, 'harness', 'settings.yaml');
343
362
  let settingsDefault = null;
363
+ let persistedDefaultDeclared = false;
344
364
  let defaultEvidence = { ok: true, source: 'none' };
345
365
  try {
346
366
  declarationEvidence.sources.settings = { present: existsSync(settingsFile) };
@@ -353,8 +373,15 @@ async function readProviderInventorySnapshot(hub, ctx, config) {
353
373
  }
354
374
  else { declarationEvidence.ok = false; declarationEvidence.sources.settings.code = parsed.code; }
355
375
  const parsedDefault = readHarnessDefault(source);
356
- if (parsedDefault.ok) settingsDefault = parsedDefault;
357
- else defaultEvidence = { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE' };
376
+ if (parsedDefault.ok) {
377
+ settingsDefault = parsedDefault;
378
+ persistedDefaultDeclared = true;
379
+ } else if (hasHarnessDefaultBlock(source)) {
380
+ // The block exists but cannot be read: fail closed on the mutation
381
+ // authority rather than treating the layer as absent.
382
+ defaultEvidence = { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_UNAVAILABLE' };
383
+ persistedDefaultDeclared = true;
384
+ }
358
385
  }
359
386
  } catch {
360
387
  declarationEvidence.ok = false;
@@ -364,9 +391,8 @@ async function readProviderInventorySnapshot(hub, ctx, config) {
364
391
  const liveDefault = catalog.harness_default && typeof catalog.harness_default.provider === 'string' && typeof catalog.harness_default.model === 'string'
365
392
  ? { provider: catalog.harness_default.provider, model: catalog.harness_default.model } : null;
366
393
  const persistedDefault = settingsDefault ? { provider: settingsDefault.provider, model: settingsDefault.model } : null;
367
- if (liveDefault || persistedDefault) {
368
- if (!liveDefault || !persistedDefault) defaultEvidence = { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_MISMATCH' };
369
- else if (liveDefault.provider !== persistedDefault.provider || liveDefault.model !== persistedDefault.model) defaultEvidence = { ok: false, code: 'PROVIDER_DEFAULT_AUTHORITY_MISMATCH' };
394
+ if (defaultEvidence.ok) {
395
+ defaultEvidence = projectHarnessDefaultEvidence({ liveDefault, persistedDefault, persistedDefaultDeclared });
370
396
  }
371
397
  // The persisted agent-default-model is the mutation authority. Keep it in
372
398
  // the inventory even when the live selection is stale; the evidence gate
@@ -1439,6 +1465,30 @@ export async function apply(ctx) {
1439
1465
  handler: (req, res) => sendJson(res, 200, { ok: true, service: 'dsh-crew-hub' }),
1440
1466
  }));
1441
1467
 
1468
+ // Operator entry into the full control plane. Its session token is minted per
1469
+ // process, and every surface that links here (the desktop app's panel, the
1470
+ // 3080 panel) runs in a different harness process whose connection service
1471
+ // cannot mint one for this port — linking the bare port landed the operator on
1472
+ // "dsh web authentication required". Only the hub itself can sign its own
1473
+ // session, so it redirects; no token is written to a page, a file or a log.
1474
+ // Loopback-only, because the Location carries a session credential.
1475
+ disposers.push(webServer.register({
1476
+ kind: 'exact', path: `${ROUTE_BASE}/control-plane`,
1477
+ handler: (req, res) => {
1478
+ if (!isLoopbackRequest(req)) return sendJson(res, 403, { ok: false, error: 'loopback only' });
1479
+ const runtime = getHubRuntimeIdentity();
1480
+ const base = `http://127.0.0.1:${Number(runtime?.listen_port) > 0 ? runtime.listen_port : 3210}/`;
1481
+ let target = base;
1482
+ try {
1483
+ const connection = webCtx.get?.('connection');
1484
+ const authenticated = connection?.authenticatedUrl?.(base);
1485
+ if (typeof authenticated === 'string' && authenticated.startsWith('http://')) target = authenticated;
1486
+ } catch { /* the bare control plane stays the honest fallback */ }
1487
+ res.writeHead(302, { location: target, 'cache-control': 'no-store' });
1488
+ res.end();
1489
+ },
1490
+ }));
1491
+
1442
1492
  disposers.push(webServer.register({
1443
1493
  kind: 'prefix', path: `${ROUTE_BASE}/jobs`,
1444
1494
  handler: async (req, res) => {
package/src/i18n.mjs CHANGED
@@ -13,7 +13,5 @@ export function setLang(lang) {
13
13
  return LANG;
14
14
  }
15
15
 
16
- export function getLang() { return LANG; }
17
-
18
16
  /** Pick the string for the active locale. Both branches are cheap literals. */
19
17
  export function tr(zh, en) { return LANG === 'zh' ? zh : en; }
@@ -9,7 +9,7 @@
9
9
  // the check disagreed about what "the installed server" is, and only the
10
10
  // installer was right.
11
11
 
12
- import { existsSync, realpathSync } from 'node:fs';
12
+ import { existsSync, readFileSync } from 'node:fs';
13
13
  import { dirname, join } from 'node:path';
14
14
  import { homedir } from 'node:os';
15
15
 
@@ -60,21 +60,26 @@ export function loaderLinkPath({ home = homedir(), name } = {}) {
60
60
  return join(crewProfileDir({ home }), 'node_modules', ...name.split('/'));
61
61
  }
62
62
 
63
- function sameDirectory(left, right) {
64
- try { return realpathSync(left) === realpathSync(right); } catch { return false; }
65
- }
66
-
67
63
  /**
68
64
  * The root the host integrations were installed from.
69
65
  *
70
- * Readiness must judge the integrations against the path they were actually
71
- * written with. That path is the loader link when it resolves to this release,
72
- * and the release directory itself otherwise — a machine whose link is missing
73
- * or points somewhere else is reported against the release, which is exactly
74
- * the mismatch the operator needs to see.
66
+ * The integrations embed the loader link path itself, and the link is what the
67
+ * host loads through: whichever payload it currently resolves to IS the live
68
+ * one, and an upgrade re-points the link so the same embedded path keeps
69
+ * resolving to the new release. Readiness therefore judges the integrations
70
+ * against the link whenever it resolves to a readable payload of this package,
71
+ * and falls back to the caller's root only when the link is broken or carries
72
+ * a different package. Gating on whether the link happens to realpath to the
73
+ * caller's root made the writer (link-rooted) and the validator
74
+ * (release-rooted) disagree about the same machine, and every clean install
75
+ * read back as "needs repair".
75
76
  */
76
77
  export function integrationRoot({ home = homedir(), root, name } = {}) {
77
78
  const link = loaderLinkPath({ home, name });
78
79
  if (!link || !root || !existsSync(link)) return root;
79
- return sameDirectory(link, root) ? link : root;
80
+ try {
81
+ const payload = JSON.parse(readFileSync(join(link, 'package.json'), 'utf8'));
82
+ if (payload?.name === name) return link;
83
+ } catch { /* broken or foreign link payload */ }
84
+ return root;
80
85
  }
@@ -42,7 +42,7 @@ export function crewSkillFiles({ home = homedir(), env = process.env } = {}) {
42
42
  }
43
43
 
44
44
  /** The skill template shipped in the payload. */
45
- export function crewSkillSource({ root }) {
45
+ function crewSkillSource({ root }) {
46
46
  return join(root, 'skills', CREW_SKILL_NAME, 'SKILL.md');
47
47
  }
48
48