@ran-sh/dsh-crew 1.1.1 → 1.2.0-rc.4

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 (83) hide show
  1. package/.claude-plugin/marketplace.json +16 -17
  2. package/.claude-plugin/plugin.json +7 -1
  3. package/README.md +44 -15
  4. package/README.zh.md +37 -12
  5. package/agents/ds-flash.md +16 -18
  6. package/agents/ds-pro.md +16 -24
  7. package/agents/ds-reviewer.md +15 -15
  8. package/agents/ds-worker.md +16 -16
  9. package/bin/dsh-crew.mjs +16 -12
  10. package/codex/AGENTS.md +37 -85
  11. package/codex/agents/ds-flash.toml +18 -14
  12. package/codex/agents/ds-pro.toml +18 -15
  13. package/codex/agents/ds-reviewer.toml +17 -13
  14. package/codex/agents/ds-worker.toml +17 -13
  15. package/codex/prompts/dsh-config.md +17 -1
  16. package/codex/prompts/dsh-status.md +5 -1
  17. package/commands/config.md +16 -4
  18. package/commands/status.md +6 -2
  19. package/docs/installation.md +54 -9
  20. package/docs/readiness-matrix.md +14 -6
  21. package/docs/ui-surfaces.md +32 -17
  22. package/lib/client.js +5434 -4844
  23. package/official-web-bridge/lib/client.js +567 -479
  24. package/official-web-bridge/overlay-entry.mjs +59 -0
  25. package/package.json +6 -3
  26. package/scripts/build-client.mjs +2 -0
  27. package/scripts/setup.mjs +8 -7
  28. package/scripts/verify-crew-ui-polish.mjs +145 -0
  29. package/scripts/verify-history-ui.mjs +97 -0
  30. package/scripts/verify-npm-install.mjs +1 -1
  31. package/src/client/history-panel.tsx +108 -0
  32. package/src/client/host-readiness.mjs +15 -6
  33. package/src/client/index.tsx +86 -64
  34. package/src/client/model-callability-view.mjs +17 -0
  35. package/src/client/panel-chrome.tsx +40 -0
  36. package/src/client/quick-panel.tsx +88 -73
  37. package/src/client/readiness-envelope.mjs +34 -0
  38. package/src/config-readiness.mjs +181 -115
  39. package/src/extension-contract.mjs +44 -26
  40. package/src/history/admission-gate.mjs +51 -0
  41. package/src/history/archive-store.mjs +261 -0
  42. package/src/history/cleanup-plan.mjs +64 -0
  43. package/src/history/http.mjs +32 -0
  44. package/src/history/operation.mjs +86 -0
  45. package/src/history/runner.mjs +51 -0
  46. package/src/history/runtime.mjs +28 -0
  47. package/src/history/service.mjs +137 -0
  48. package/src/history/state.mjs +27 -0
  49. package/src/hub/index.mjs +141 -24
  50. package/src/hub-client.mjs +17 -15
  51. package/src/image-output.mjs +71 -0
  52. package/src/install/install-legacy.mjs +126 -23
  53. package/src/install/npx-lifecycle.mjs +373 -64
  54. package/src/install/official-frontend-assets.mjs +78 -0
  55. package/src/install/payload-content.mjs +88 -0
  56. package/src/install/windows-startup.mjs +107 -19
  57. package/src/install/windows-supervisor-adapter.mjs +443 -0
  58. package/src/install/windows-supervisor-lifecycle.mjs +782 -0
  59. package/src/install/zcode.mjs +53 -14
  60. package/src/job-contracts.mjs +37 -31
  61. package/src/jobs.mjs +2 -2
  62. package/src/mcp-runtime.mjs +74 -9
  63. package/src/model-callability-contract.mjs +79 -0
  64. package/src/multimodal.mjs +0 -0
  65. package/src/official-web-bridge.mjs +0 -2
  66. package/src/provider-health.mjs +130 -124
  67. package/src/readiness-matrix.mjs +20 -15
  68. package/src/runtime-identity-contract.mjs +34 -0
  69. package/src/runtime-identity.mjs +14 -4
  70. package/src/runtime-readiness-snapshot.mjs +285 -87
  71. package/src/server.mjs +86 -76
  72. package/src/standalone-sdk.mjs +23 -0
  73. package/src/supervisor/restart-request.mjs +64 -10
  74. package/src/workflow-runtime.mjs +28 -13
  75. package/windows/start-dsh-crew.cmd +57 -57
  76. package/windows/start-dsh-crew.ps1 +314 -51
  77. package/windows/supervisor-control.ps1 +467 -0
  78. package/zcode/AGENTS.md +4 -1
  79. package/zcode/agents/ds-reviewer.md +31 -36
  80. package/zcode/agents/ds-worker.md +31 -39
  81. package/zcode/commands/dsh-config.md +17 -6
  82. package/zcode/commands/dsh-status.md +5 -4
  83. package/.mcp.json +0 -8
@@ -40,6 +40,7 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
40
40
  import { fileURLToPath } from 'node:url';
41
41
  import { homedir } from 'node:os';
42
42
  import * as realInstaller from './install.mjs';
43
+ import { samePayloadContent, capturePayloadContent } from './payload-content.mjs';
43
44
  import { crewDshHome, crewProfileDir } from './install.mjs';
44
45
  import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration, migrateCrewDshRuntime, installDshInto, restoreRetainedRuntime, crewDshRuntimeRoot, payloadDshVersion, TARGET_DSH_VERSION } from '../dsh-cli-runtime.mjs';
45
46
  import {
@@ -47,6 +48,13 @@ import {
47
48
  officialWebIntegrationStatus,
48
49
  removeOfficialWebIntegration,
49
50
  } from './official-web.mjs';
51
+ import {
52
+ convergeWindowsSupervisor,
53
+ PENDING_RUNTIME_ACTIVATION,
54
+ releaseWindowsSupervisorConvergenceReservation,
55
+ reserveWindowsSupervisorConvergence,
56
+ windowsSupervisorConvergencePending,
57
+ } from './windows-supervisor-adapter.mjs';
50
58
 
51
59
  export const CREW_APP_DIRNAME = 'app';
52
60
  export const RELEASES_DIRNAME = 'releases';
@@ -848,6 +856,8 @@ export function stageCandidatePayload({
848
856
  if (!existsSync(join(sourceRoot, 'src', 'server.mjs')) || !existsSync(join(sourceRoot, 'cordis.patch.yml'))) {
849
857
  return { ok: false, code: 'CANDIDATE_NOT_RUNNABLE' };
850
858
  }
859
+ const content = capturePayloadContent(sourceRoot, manifest);
860
+ if (!content) return { ok: false, code: 'CANDIDATE_CONTENT_INVALID' };
851
861
 
852
862
  const releasesDir = crewReleasesDir({ home });
853
863
  mkdirSync(releasesDir, { recursive: true });
@@ -856,26 +866,12 @@ export function stageCandidatePayload({
856
866
  mkdirSync(stageDir, { recursive: true });
857
867
  writeFileSync(join(stageDir, INCOMPLETE_MARKER), String(Date.now()) + '\n');
858
868
 
859
- // Copy every top-level entry referenced by manifest.files (directories are
860
- // copied whole; single-segment wildcards match within their directory).
861
- const entries = new Set(['package.json']);
862
- for (const pattern of manifest.files ?? []) {
863
- if (typeof pattern !== 'string' || !pattern.trim()) continue;
864
- if (!pattern.includes('*')) { entries.add(pattern); continue; }
865
- const base = dirname(pattern);
866
- const regex = new RegExp(`^${pattern.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*')}$`);
867
- const scanDir = join(sourceRoot, base);
868
- if (!existsSync(scanDir)) continue;
869
- for (const item of readdirSync(scanDir)) {
870
- const candidatePath = base === '.' ? item : `${base.replace(/\\/g, '/')}/${item}`;
871
- if (regex.test(candidatePath)) entries.add(candidatePath);
872
- }
873
- }
874
-
875
- for (const entry of entries) {
876
- const source = join(sourceRoot, entry);
877
- if (!existsSync(source)) continue;
878
- cpSync(source, join(stageDir, entry), { recursive: true, force: true, dereference: true });
869
+ // Copy the validated byte snapshot, never re-resolve source paths here.
870
+ for (const directory of content.directories) mkdirSync(join(stageDir, directory), { recursive: true });
871
+ for (const [entry, bytes] of content.files) {
872
+ const destination = join(stageDir, entry);
873
+ mkdirSync(dirname(destination), { recursive: true });
874
+ writeFileSync(destination, bytes, { mode: content.modes.get(entry) });
879
875
  }
880
876
 
881
877
  // Persist an adjusted manifest: identity/runtime fields stay; production
@@ -1023,11 +1019,18 @@ export function validateInstalledPayload(dir, { expectedName, expectedVersion, a
1023
1019
  }
1024
1020
  }
1025
1021
  }
1026
- for (const rel of [
1022
+ const requiredArtifacts = [
1027
1023
  'cordis.patch.yml', 'src/server.mjs', 'src/hub/entry.mjs', 'lib/client.js', 'bin/dsh-crew.mjs',
1028
1024
  'official-web-bridge/package.json', 'official-web-bridge/cordis.patch.yml',
1029
1025
  'official-web-bridge/entry.mjs', 'official-web-bridge/lib/client.js',
1030
- ]) {
1026
+ ];
1027
+ if (manifest?.dshCrew?.windowsSupervisorHandoff === 1) {
1028
+ requiredArtifacts.push(
1029
+ 'windows/start-dsh-crew.ps1', 'windows/start-dsh-crew.cmd',
1030
+ 'windows/start-dsh-crew.vbs', 'windows/supervisor-control.ps1',
1031
+ );
1032
+ }
1033
+ for (const rel of requiredArtifacts) {
1031
1034
  if (!existsSync(join(dir, rel))) errors.push(`payload artifact missing: ${rel}`);
1032
1035
  }
1033
1036
  if (!allowIncomplete && existsSync(join(dir, INCOMPLETE_MARKER))) errors.push('release is still marked incomplete');
@@ -1040,6 +1043,9 @@ export function validateInstalledPayload(dir, { expectedName, expectedVersion, a
1040
1043
  // A crash at any point before the pointer write leaves the prior release
1041
1044
  // authoritative and the journal behind for reconcileUpdateJournal.
1042
1045
  export function beginReleaseActivation({ stageDir, manifest, home, prior = null }) {
1046
+ // Survives pointer commit and updater crashes; versions alone cannot prove
1047
+ // that a same-version code replacement has reached the running process.
1048
+ writeFileSync(join(stageDir, PENDING_RUNTIME_ACTIVATION), `${manifest.version}\n`);
1043
1049
  // Journal FIRST, marker removal second: a crash between the two leaves a
1044
1050
  // journaled (recoverable) candidate, never a marker-free orphan that
1045
1051
  // looks complete but was never in a transaction. Marker removal failure
@@ -1287,12 +1293,18 @@ export async function verifyRollbackTarget({ crewVersion, dshVersion, fetchImpl
1287
1293
  // requests. The npx process owns the runtime TREE swap; the supervisor owns
1288
1294
  // the PROCESS stop/start. The Node sidecar supervisor is deliberately not
1289
1295
  // used here: two authorities must never share the same kill rights.
1290
- function crewSupervisor({ home = homedir() } = {}) {
1296
+ export function createCrewSupervisor({
1297
+ home = homedir(),
1298
+ fetchImpl = globalThis.fetch,
1299
+ pollMs = 1_000,
1300
+ timeoutMs = 90_000,
1301
+ identityTimeoutMs = 3_000,
1302
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1303
+ now = Date.now,
1304
+ } = {}) {
1291
1305
  // The ONLY Crew app root both the hub and the Windows launcher agree on.
1292
1306
  // Requests written anywhere else are invisible to the supervisor.
1293
1307
  const appRoot = join(home, '.config', 'dsh-crew');
1294
- const pollMs = 1_000;
1295
- const timeoutMs = 90_000;
1296
1308
  // One maintenance transaction = one lease + the pre-stop identity proven
1297
1309
  // live. stop() fetches the CURRENT live runtime_id (the process exists);
1298
1310
  // start() reuses that proven identity + lease (the process is stopped by
@@ -1300,13 +1312,30 @@ function crewSupervisor({ home = homedir() } = {}) {
1300
1312
  // single one-shot transaction instead of two independent requests.
1301
1313
  let transaction = null;
1302
1314
  async function fetchCrewIdentity() {
1315
+ const controller = new AbortController();
1316
+ // Keep the discovery deadline alive even if an adapter has no active I/O.
1317
+ const timer = setTimeout(() => controller.abort(new Error('maintenance identity timeout')), identityTimeoutMs);
1303
1318
  try {
1304
- const response = await fetch('http://127.0.0.1:3210/_dsh/dsh-crew/extension', { headers: { accept: 'application/json' } });
1319
+ const response = await fetchImpl('http://127.0.0.1:3210/_dsh/dsh-crew/extension', {
1320
+ headers: { accept: 'application/json' },
1321
+ signal: controller.signal,
1322
+ });
1305
1323
  if (!response.ok) return null;
1306
1324
  const body = await response.json();
1307
1325
  const runtime = body?.extension?.runtime ?? null;
1308
- return runtime?.runtime_id ? { execution_plane: 'hub-3210', profile: 'dsh-crew', listen_port: 3210, runtime_id: runtime.runtime_id } : null;
1326
+ const canonical = body?.ok === true
1327
+ && runtime?.service === 'dsh-crew-hub'
1328
+ && runtime?.execution_plane === 'hub-3210'
1329
+ && runtime?.profile === 'dsh-crew'
1330
+ && Number(runtime?.listen_port) === 3210
1331
+ && Number(runtime?.protocol_version) === 1
1332
+ && typeof runtime?.runtime_id === 'string'
1333
+ && runtime.runtime_id.length > 0;
1334
+ return canonical
1335
+ ? { execution_plane: 'hub-3210', profile: 'dsh-crew', listen_port: 3210, runtime_id: runtime.runtime_id }
1336
+ : null;
1309
1337
  } catch { return null; }
1338
+ finally { clearTimeout(timer); }
1310
1339
  }
1311
1340
  async function writeMaintenance({ operation, lease, identity, extra = null }) {
1312
1341
  const { createSupervisorRequest, maintenanceResultsDir } = await import('../supervisor/restart-request.mjs');
@@ -1318,10 +1347,10 @@ function crewSupervisor({ home = homedir() } = {}) {
1318
1347
  extra,
1319
1348
  });
1320
1349
  if (!created.ok) return created;
1321
- const deadline = Date.now() + timeoutMs;
1350
+ const deadline = now() + timeoutMs;
1322
1351
  for (;;) {
1323
- await new Promise((resolve) => setTimeout(resolve, pollMs));
1324
- if (Date.now() > deadline) {
1352
+ await sleep(pollMs);
1353
+ if (now() > deadline) {
1325
1354
  return { ok: false, code: 'MAINTENANCE_TIMEOUT', error: `${operation} ${created.request.request_id} not completed within ${timeoutMs}ms` };
1326
1355
  }
1327
1356
  const resultFile = join(maintenanceResultsDir(appRoot), `${created.request.request_id}.json`);
@@ -1331,21 +1360,70 @@ function crewSupervisor({ home = homedir() } = {}) {
1331
1360
  result = JSON.parse(readFileSync(resultFile, 'utf8'));
1332
1361
  }
1333
1362
  } catch { /* not yet */ }
1334
- if (!result || result.request_id !== created.request.request_id) continue;
1335
- if (result.state === 'STOPPED' || result.state === 'VERIFIED') {
1336
- return { ok: true, state: result.state, request_id: result.request_id, detail: result.detail ?? null };
1363
+ if (!result) continue;
1364
+ const identityMatches = result.schema_version === 1
1365
+ && result.request_id === created.request.request_id
1366
+ && result.operation === created.request.operation
1367
+ && result.lease === created.request.lease
1368
+ && result.runtime_id === created.request.runtime_id;
1369
+ if (!identityMatches) {
1370
+ return {
1371
+ ok: false,
1372
+ code: 'MAINTENANCE_RESULT_IDENTITY_MISMATCH',
1373
+ error: 'maintenance result does not match the exact request identity',
1374
+ };
1375
+ }
1376
+ const expectedState = operation === 'maintenance-stop' ? 'STOPPED' : 'VERIFIED';
1377
+ if (result.state === expectedState) {
1378
+ return {
1379
+ ok: true,
1380
+ state: result.state,
1381
+ request_id: result.request_id,
1382
+ lease: result.lease,
1383
+ runtime_id: result.runtime_id,
1384
+ detail: result.detail ?? null,
1385
+ };
1337
1386
  }
1338
1387
  return { ok: false, code: `MAINTENANCE_${result.state ?? 'FAILED'}`, error: `maintenance ended in state ${result.state}`, detail: result.detail ?? null };
1339
1388
  }
1340
1389
  }
1341
1390
  return {
1342
- stopOwnedBackend: async () => {
1391
+ stopOwnedBackend: async ({ lease = null, runtimeId = null } = {}) => {
1392
+ const { readMaintenanceSession } = await import('../supervisor/restart-request.mjs');
1393
+ const durable = readMaintenanceSession(appRoot);
1394
+ if (!durable.ok) {
1395
+ transaction = null;
1396
+ return { ok: false, code: durable.code, error: 'durable maintenance session is malformed' };
1397
+ }
1398
+ if (durable.state === 'present') {
1399
+ const session = durable.session;
1400
+ if (lease && runtimeId && session.lease === lease && session.runtime_id === runtimeId) {
1401
+ transaction = { lease, runtime_id: runtimeId };
1402
+ return {
1403
+ ok: true,
1404
+ state: 'STOPPED',
1405
+ lease,
1406
+ runtime_id: runtimeId,
1407
+ resumed: true,
1408
+ request_id: session.request_id,
1409
+ };
1410
+ }
1411
+ transaction = null;
1412
+ return { ok: false, code: 'MAINTENANCE_SESSION_CONFLICT', error: 'a different durable maintenance session owns the stopped runtime window' };
1413
+ }
1343
1414
  const identity = await fetchCrewIdentity();
1344
1415
  if (!identity?.runtime_id) {
1345
1416
  transaction = null;
1346
1417
  return { ok: false, code: 'MAINTENANCE_IDENTITY_UNAVAILABLE', error: 'live 3210 runtime_id unavailable' };
1347
1418
  }
1348
- transaction = { lease: `txn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`, runtime_id: identity.runtime_id };
1419
+ if (runtimeId && runtimeId !== identity.runtime_id) {
1420
+ transaction = null;
1421
+ return { ok: false, code: 'MAINTENANCE_IDENTITY_CHANGED', error: 'live 3210 runtime_id changed before maintenance-stop' };
1422
+ }
1423
+ transaction = {
1424
+ lease: lease ?? `txn-${now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
1425
+ runtime_id: identity.runtime_id,
1426
+ };
1349
1427
  const result = await writeMaintenance({ operation: 'maintenance-stop', lease: transaction.lease, identity });
1350
1428
  if (!result.ok) {
1351
1429
  // A failed stop must not leave a usable lease behind: clear it so a
@@ -1355,20 +1433,223 @@ function crewSupervisor({ home = homedir() } = {}) {
1355
1433
  }
1356
1434
  return result;
1357
1435
  },
1358
- startOwnedBackend: async () => {
1436
+ startOwnedBackend: async ({ lease = null, runtimeId = null, expectedCrewVersion = null, expectedDshVersion = null } = {}) => {
1359
1437
  // Reuse the proven stop-phase identity + lease: the process is stopped
1360
1438
  // by design and fetching identity now would always fail.
1361
- if (!transaction?.lease || !transaction?.runtime_id) {
1439
+ const active = lease && runtimeId ? { lease, runtime_id: runtimeId } : transaction;
1440
+ if (!active?.lease || !active?.runtime_id) {
1362
1441
  return { ok: false, code: 'MAINTENANCE_NO_TRANSACTION', error: 'maintenance-start requires a completed maintenance-stop in the same supervisor' };
1363
1442
  }
1364
- const identity = { execution_plane: 'hub-3210', profile: 'dsh-crew', listen_port: 3210, runtime_id: transaction.runtime_id };
1365
- const result = await writeMaintenance({ operation: 'maintenance-start', lease: transaction.lease, identity });
1443
+ const identity = { execution_plane: 'hub-3210', profile: 'dsh-crew', listen_port: 3210, runtime_id: active.runtime_id };
1444
+ const extra = expectedCrewVersion || expectedDshVersion ? {
1445
+ ...(expectedCrewVersion ? { expected_crew_version: expectedCrewVersion } : {}),
1446
+ ...(expectedDshVersion ? { expected_dsh_version: expectedDshVersion } : {}),
1447
+ } : null;
1448
+ const result = await writeMaintenance({ operation: 'maintenance-start', lease: active.lease, identity, extra });
1366
1449
  transaction = null;
1367
1450
  return result;
1368
1451
  },
1369
1452
  };
1370
1453
  }
1371
1454
 
1455
+ const crewSupervisor = createCrewSupervisor;
1456
+
1457
+ function sameFilesystemPath(left, right) {
1458
+ if (typeof left !== 'string' || typeof right !== 'string') return false;
1459
+ const a = resolve(left);
1460
+ const b = resolve(right);
1461
+ return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
1462
+ }
1463
+
1464
+ function usesProductionWindowsSupervisor(home, supervisorConverger) {
1465
+ return typeof supervisorConverger !== 'function'
1466
+ && process.platform === 'win32'
1467
+ && sameFilesystemPath(home, homedir());
1468
+ }
1469
+
1470
+ async function acquireLifecycleUpdateLock({
1471
+ home,
1472
+ log,
1473
+ supervisorConverger,
1474
+ supervisorPendingChecker,
1475
+ }) {
1476
+ const production = usesProductionWindowsSupervisor(home, supervisorConverger);
1477
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1478
+ const updateLock = acquireUpdateLock({ home });
1479
+ if (!updateLock.ok) return updateLock;
1480
+ let pending;
1481
+ try {
1482
+ pending = typeof supervisorPendingChecker === 'function'
1483
+ ? await supervisorPendingChecker({ home })
1484
+ : production
1485
+ ? windowsSupervisorConvergencePending({ home })
1486
+ : false;
1487
+ } catch (error) {
1488
+ releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
1489
+ return { ok: false, code: 'SUPERVISOR_HANDOFF_PENDING_CHECK_FAILED', error: error?.message ?? String(error) };
1490
+ }
1491
+ // Holding updateLock makes this a stable observation: no other conforming
1492
+ // lifecycle operation can reserve a new handoff until we either return
1493
+ // this lock or release it for recovery.
1494
+ if (!pending) return updateLock;
1495
+
1496
+ const released = releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
1497
+ if (!released.ok || existsSync(updateLockFile({ home }))) {
1498
+ return { ok: false, code: 'UPDATE_LOCK_RELEASE_UNVERIFIED' };
1499
+ }
1500
+ const active = readCurrentPointer({ home });
1501
+ if (!active?.path || !existsSync(active.path)) {
1502
+ return { ok: false, code: 'SUPERVISOR_HANDOFF_RECOVERY_RELEASE_MISSING' };
1503
+ }
1504
+ const converge = typeof supervisorConverger === 'function' ? supervisorConverger : convergeWindowsSupervisor;
1505
+ let recovery;
1506
+ try {
1507
+ recovery = await converge({
1508
+ home,
1509
+ root: active.path,
1510
+ maintenanceClient: createCrewSupervisor({ home }),
1511
+ });
1512
+ } catch (error) {
1513
+ recovery = { ok: false, code: 'SUPERVISOR_HANDOFF_RECOVERY_THROWN', error: error?.message ?? String(error) };
1514
+ }
1515
+ if (recovery?.ok !== true) {
1516
+ log?.(`✗ pending Windows supervisor handoff recovery failed (${recovery?.code ?? 'unknown'}).`);
1517
+ return { ok: false, code: 'SUPERVISOR_HANDOFF_RECOVERY_FAILED', recovery };
1518
+ }
1519
+ }
1520
+ return { ok: false, code: 'SUPERVISOR_HANDOFF_RECOVERY_CHURN' };
1521
+ }
1522
+
1523
+ async function reservePostCommitSupervisor({
1524
+ home,
1525
+ result,
1526
+ supervisorConverger,
1527
+ supervisorReserver,
1528
+ }) {
1529
+ if (result?.ok !== true) return { result, reservation: null };
1530
+ const production = usesProductionWindowsSupervisor(home, supervisorConverger);
1531
+ const reserve = typeof supervisorReserver === 'function'
1532
+ ? supervisorReserver
1533
+ : production
1534
+ ? reserveWindowsSupervisorConvergence
1535
+ : null;
1536
+ if (!reserve) return { result, reservation: null };
1537
+ const active = readCurrentPointer({ home });
1538
+ if (!active?.path
1539
+ || !sameFilesystemPath(active.path, result.path)
1540
+ || (result.version && active.version !== result.version)) {
1541
+ return {
1542
+ result: {
1543
+ ...result,
1544
+ ok: false,
1545
+ committed: true,
1546
+ code: 'SUPERVISOR_CONVERGENCE_RELEASE_CHANGED',
1547
+ error: 'active Crew release changed before Windows supervisor reservation',
1548
+ },
1549
+ reservation: null,
1550
+ };
1551
+ }
1552
+ const reservation = await reserve({ home, root: active.path });
1553
+ if (reservation?.ok !== true) {
1554
+ return {
1555
+ result: {
1556
+ ...result,
1557
+ ok: false,
1558
+ committed: true,
1559
+ code: 'SUPERVISOR_CONVERGENCE_RESERVATION_FAILED',
1560
+ supervisor_convergence: reservation ?? null,
1561
+ },
1562
+ reservation: null,
1563
+ };
1564
+ }
1565
+ return { result, reservation };
1566
+ }
1567
+
1568
+ async function finalizeSupervisorConvergence({
1569
+ home,
1570
+ result,
1571
+ log,
1572
+ supervisorConverger,
1573
+ reservation = null,
1574
+ }) {
1575
+ if (result?.ok !== true) return result;
1576
+
1577
+ const defaultWindowsHome = process.platform === 'win32' && sameFilesystemPath(home, homedir());
1578
+ const converge = typeof supervisorConverger === 'function'
1579
+ ? supervisorConverger
1580
+ : defaultWindowsHome
1581
+ ? convergeWindowsSupervisor
1582
+ : null;
1583
+ if (!converge) return result;
1584
+
1585
+ const active = readCurrentPointer({ home });
1586
+ if (!active?.path
1587
+ || !sameFilesystemPath(active.path, result.path)
1588
+ || (result.version && active.version !== result.version)) {
1589
+ if (reservation) releaseWindowsSupervisorConvergenceReservation(reservation);
1590
+ return {
1591
+ ...result,
1592
+ ok: false,
1593
+ committed: true,
1594
+ code: 'SUPERVISOR_CONVERGENCE_RELEASE_CHANGED',
1595
+ error: 'active Crew release changed before Windows supervisor convergence',
1596
+ };
1597
+ }
1598
+
1599
+ let convergence;
1600
+ try {
1601
+ convergence = await converge({
1602
+ home,
1603
+ root: active.path,
1604
+ maintenanceClient: createCrewSupervisor({ home }),
1605
+ ...(reservation ? { reservation } : {}),
1606
+ });
1607
+ } catch (error) {
1608
+ convergence = {
1609
+ ok: false,
1610
+ code: 'SUPERVISOR_CONVERGENCE_THROWN',
1611
+ error: error?.message ?? String(error),
1612
+ };
1613
+ }
1614
+ if (convergence?.ok !== true) {
1615
+ log?.(`✗ Windows supervisor convergence failed (${convergence?.code ?? 'unknown'}); the payload remains committed and the handoff can be resumed.`);
1616
+ return {
1617
+ ...result,
1618
+ ok: false,
1619
+ committed: true,
1620
+ code: 'SUPERVISOR_CONVERGENCE_FAILED',
1621
+ supervisor_convergence: convergence ?? null,
1622
+ };
1623
+ }
1624
+ try { rmSync(join(active.path, PENDING_RUNTIME_ACTIVATION), { force: true }); }
1625
+ catch {
1626
+ return { ...result, ok: false, committed: true, code: 'RUNTIME_ACTIVATION_MARKER_CLEAR_FAILED', supervisor_convergence: convergence };
1627
+ }
1628
+ return { ...result, supervisor_convergence: convergence };
1629
+ }
1630
+
1631
+ async function finishLifecycleAfterUpdateLock({
1632
+ home,
1633
+ result,
1634
+ lockRelease,
1635
+ log,
1636
+ supervisorConverger,
1637
+ reservation = null,
1638
+ }) {
1639
+ if (result?.ok !== true) return result;
1640
+ if (lockRelease?.ok !== true || existsSync(updateLockFile({ home }))) {
1641
+ if (reservation) releaseWindowsSupervisorConvergenceReservation(reservation);
1642
+ return {
1643
+ ...result,
1644
+ ok: false,
1645
+ committed: true,
1646
+ code: 'UPDATE_LOCK_RELEASE_UNVERIFIED',
1647
+ error: 'update lock release could not be verified; supervisor convergence was not attempted',
1648
+ };
1649
+ }
1650
+ return finalizeSupervisorConvergence({ home, result, log, supervisorConverger, reservation });
1651
+ }
1652
+
1372
1653
  // ---- release cohort resolution -------------------------------------------------
1373
1654
 
1374
1655
  // Lifecycle-owned cohort sidecar for a managed release directory. Historical
@@ -1470,19 +1751,27 @@ export async function npxRollback({
1470
1751
  restart,
1471
1752
  verifyRuntime,
1472
1753
  supervisorFactory = crewSupervisor,
1754
+ supervisorConverger,
1755
+ supervisorPendingChecker,
1756
+ supervisorReserver,
1473
1757
  } = {}) {
1474
1758
  const targetVersion = typeof version === 'string' ? version.trim() : '';
1475
1759
  if (!targetVersion) return { ok: false, error: 'rollback requires a target version' };
1476
1760
  // Rollback shares the update mutual-exclusion lock: it mutates the same
1477
1761
  // pointer/registration/runtime surfaces as install/update, and the watch
1478
1762
  // supervisor must observe-only while it runs.
1479
- const updateLock = acquireUpdateLock({ home });
1763
+ const updateLock = await acquireLifecycleUpdateLock({ home, log, supervisorConverger, supervisorPendingChecker });
1480
1764
  if (!updateLock.ok) return { ok: false, error: `another update is in progress (${updateLock.code})` };
1765
+ let result;
1766
+ let lockRelease;
1767
+ let reservation = null;
1481
1768
  try {
1482
- return await npxRollbackInner({ home, version: targetVersion, log, installer, ensureRuntime, validatePayload, activate, restart, verifyRuntime, supervisorFactory });
1769
+ result = await npxRollbackInner({ home, version: targetVersion, log, installer, ensureRuntime, validatePayload, activate, restart, verifyRuntime, supervisorFactory });
1770
+ ({ result, reservation } = await reservePostCommitSupervisor({ home, result, supervisorConverger, supervisorReserver }));
1483
1771
  } finally {
1484
- releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
1772
+ lockRelease = releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
1485
1773
  }
1774
+ return finishLifecycleAfterUpdateLock({ home, result, lockRelease, log, supervisorConverger, reservation });
1486
1775
  }
1487
1776
 
1488
1777
  async function npxRollbackInner({ home, version: targetVersion, log, installer, ensureRuntime, validatePayload, activate, restart, verifyRuntime, supervisorFactory }) {
@@ -2044,7 +2333,7 @@ function currentInstallationHealth({ home }) {
2044
2333
  return { installed: true, healthy: validated.ok && registered, validated, registered, pointer };
2045
2334
  }
2046
2335
 
2047
- async function activateRelease({ home, releaseDir, manifest, log, installer }) {
2336
+ async function activateRelease({ home, releaseDir, manifest, log, installer, supervisorRoot = runningPackageRoot() }) {
2048
2337
  const registration = ensureCrewPluginRegistration({ home, root: releaseDir, name: manifest.name });
2049
2338
  if (!registration.ok) {
2050
2339
  log(`✗ Harness registration failed (${registration.code ?? 'unknown'})`);
@@ -2068,7 +2357,7 @@ async function activateRelease({ home, releaseDir, manifest, log, installer }) {
2068
2357
  log('✓ ZCode integration');
2069
2358
  }
2070
2359
 
2071
- const startup = installer.installWindowsStartup?.({ home, root: releaseDir });
2360
+ const startup = installer.installWindowsStartup?.({ home, root: supervisorRoot });
2072
2361
  if (startup?.ok === false) {
2073
2362
  log(`✗ Windows login startup failed (${startup.code ?? 'unknown'})`);
2074
2363
  return false;
@@ -2121,15 +2410,23 @@ export async function npxInstall({
2121
2410
  installer = realInstaller,
2122
2411
  ensureRuntime,
2123
2412
  npmInstaller,
2413
+ supervisorConverger,
2414
+ supervisorPendingChecker,
2415
+ supervisorReserver,
2124
2416
  } = {}) {
2125
2417
  log('DSH Crew installer (npx-managed)');
2126
- const updateLock = acquireUpdateLock({ home });
2418
+ const updateLock = await acquireLifecycleUpdateLock({ home, log, supervisorConverger, supervisorPendingChecker });
2127
2419
  if (!updateLock.ok) return { ok: false, error: `another update is in progress (${updateLock.code})` };
2420
+ let result;
2421
+ let lockRelease;
2422
+ let reservation = null;
2128
2423
  try {
2129
- return await npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime, npmInstaller });
2424
+ result = await npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime, npmInstaller });
2425
+ ({ result, reservation } = await reservePostCommitSupervisor({ home, result, supervisorConverger, supervisorReserver }));
2130
2426
  } finally {
2131
- releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
2427
+ lockRelease = releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
2132
2428
  }
2429
+ return finishLifecycleAfterUpdateLock({ home, result, lockRelease, log, supervisorConverger, reservation });
2133
2430
  }
2134
2431
 
2135
2432
  async function npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime, npmInstaller }) {
@@ -2140,7 +2437,8 @@ async function npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime
2140
2437
  if (!manifest?.name || !manifest?.version) return { ok: false, error: 'candidate package manifest invalid' };
2141
2438
 
2142
2439
  const health = currentInstallationHealth({ home });
2143
- if (health.installed && health.pointer.version === manifest.version && health.validated.ok) {
2440
+ if (health.installed && health.pointer.version === manifest.version && health.validated.ok
2441
+ && samePayloadContent(candidateRoot, health.pointer.path)) {
2144
2442
  // Same-version reinstall: repair activation surfaces without restaging.
2145
2443
  log(`- installed payload ${manifest.version} already present and valid; repairing activation`);
2146
2444
  const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest, log, installer });
@@ -2153,7 +2451,7 @@ async function npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime
2153
2451
 
2154
2452
  const staged = stageCandidatePayload({ sourceRoot: candidateRoot, home, log, npmInstaller });
2155
2453
  if (!staged.ok) {
2156
- log(`✗ staging failed (${staged.code})${staged.detail ? `: ${staged.detail.join('; ')}` : ''}`);
2454
+ log(`✗ staging failed (${staged.code})${staged.detail ? `: ${Array.isArray(staged.detail) ? staged.detail.join('; ') : String(staged.detail)}` : ''}`);
2157
2455
  return { ok: false, error: `staging failed (${staged.code})` };
2158
2456
  }
2159
2457
  log(`✓ candidate payload staged (${manifest.version})`);
@@ -2179,7 +2477,7 @@ async function npxInstallInner({ home, log, sourceRoot, installer, ensureRuntime
2179
2477
 
2180
2478
  log('');
2181
2479
  log('Done.');
2182
- log('Restart DeepSeek Harness and Codex Desktop.');
2480
+ log('Managed Windows supervisor convergence follows automatically when supported.');
2183
2481
  return { ok: true, version: manifest.version, path: releaseDir };
2184
2482
  }
2185
2483
 
@@ -2329,15 +2627,23 @@ export async function npxUpdate({
2329
2627
  ensureRuntime,
2330
2628
  npmInstaller,
2331
2629
  runner = spawnSync,
2630
+ supervisorConverger,
2631
+ supervisorPendingChecker,
2632
+ supervisorReserver,
2332
2633
  } = {}) {
2333
2634
  log('DSH Crew updater');
2334
- const updateLock = acquireUpdateLock({ home });
2635
+ const updateLock = await acquireLifecycleUpdateLock({ home, log, supervisorConverger, supervisorPendingChecker });
2335
2636
  if (!updateLock.ok) return { ok: false, error: `another update is in progress (${updateLock.code})` };
2637
+ let result;
2638
+ let lockRelease;
2639
+ let reservation = null;
2336
2640
  try {
2337
- return await npxUpdateInner({ home, log, sourceRoot, candidate, spec, installer, ensureRuntime, npmInstaller, runner });
2641
+ result = await npxUpdateInner({ home, log, sourceRoot, candidate, spec, installer, ensureRuntime, npmInstaller, runner });
2642
+ ({ result, reservation } = await reservePostCommitSupervisor({ home, result, supervisorConverger, supervisorReserver }));
2338
2643
  } finally {
2339
- releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
2644
+ lockRelease = releaseUpdateLock({ home, nonce: updateLock.nonce ?? null });
2340
2645
  }
2646
+ return finishLifecycleAfterUpdateLock({ home, result, lockRelease, log, supervisorConverger, reservation });
2341
2647
  }
2342
2648
 
2343
2649
  async function npxUpdateInner({ home, log, sourceRoot, candidate, spec, installer, ensureRuntime, npmInstaller, runner = spawnSync }) {
@@ -2373,7 +2679,8 @@ async function npxUpdateInner({ home, log, sourceRoot, candidate, spec, installe
2373
2679
 
2374
2680
  const health = currentInstallationHealth({ home });
2375
2681
 
2376
- if (health.installed && health.healthy && health.pointer.version === manifest.version) {
2682
+ if (health.installed && health.healthy && health.pointer.version === manifest.version
2683
+ && samePayloadContent(resolved.sourceRoot, health.pointer.path)) {
2377
2684
  log(`- already current (${manifest.version}); repairing registration/integrations idempotently`);
2378
2685
  const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest, log, installer });
2379
2686
  if (!activated) return { ok: false, error: 'activation failed' };
@@ -2402,7 +2709,7 @@ async function npxUpdateInner({ home, log, sourceRoot, candidate, spec, installe
2402
2709
 
2403
2710
  const staged = stageCandidatePayload({ sourceRoot: resolved.sourceRoot, home, log, npmInstaller });
2404
2711
  if (!staged.ok) {
2405
- log(`✗ staging failed (${staged.code})${staged.detail ? `: ${staged.detail.join('; ')}` : ''}`);
2712
+ log(`✗ staging failed (${staged.code})${staged.detail ? `: ${Array.isArray(staged.detail) ? staged.detail.join('; ') : String(staged.detail)}` : ''}`);
2406
2713
  return { ok: false, error: `staging failed (${staged.code})` };
2407
2714
  }
2408
2715
  log(`✓ candidate payload staged and validated (${manifest.version})`);
@@ -2478,8 +2785,8 @@ async function npxUpdateInner({ home, log, sourceRoot, candidate, spec, installe
2478
2785
  noteLauncherDivergence({ log, home });
2479
2786
  log('');
2480
2787
  log('Done.');
2481
- log('Restart DeepSeek Harness and Codex Desktop.');
2482
- return { ok: true, updated: true, version: manifest.version, path: staged.stageDir, coordinated: true, dsh_version: candidateDsh };
2788
+ log('Managed Windows supervisor convergence follows automatically when supported.');
2789
+ return { ok: true, updated: true, version: manifest.version, path: committedRelease.path, coordinated: true, dsh_version: candidateDsh };
2483
2790
  }
2484
2791
 
2485
2792
  if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
@@ -2501,7 +2808,7 @@ async function npxUpdateInner({ home, log, sourceRoot, candidate, spec, installe
2501
2808
  noteLauncherDivergence({ log, home });
2502
2809
  log('');
2503
2810
  log('Done.');
2504
- log('Restart DeepSeek Harness and Codex Desktop.');
2811
+ log('Managed Windows supervisor convergence follows automatically when supported.');
2505
2812
  return { ok: true, updated: true, version: manifest.version, path: releaseDir };
2506
2813
  } finally {
2507
2814
  resolved.cleanup?.();
@@ -2576,15 +2883,17 @@ export function npxStatus({
2576
2883
  } catch { dshPlugin = 'unknown'; }
2577
2884
  }
2578
2885
 
2886
+ const managedRoot = pointer?.path ?? runningPackageRoot();
2579
2887
  const st = installer.installStatus
2580
- ? installer.installStatus({ home, root: pointer?.path ?? runningPackageRoot() })
2581
- : realInstaller.installStatus({ home, root: pointer?.path ?? runningPackageRoot() });
2582
- const codex = st?.codex?.installed ? 'installed' : 'not installed';
2583
- const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
2584
- const claude = st?.claude?.installed ? 'installed' : 'not installed';
2888
+ ? installer.installStatus({ home, root: managedRoot })
2889
+ : realInstaller.installStatus({ home, root: managedRoot });
2890
+ const integrationLabel = (entry) => !entry?.installed ? 'not installed' : entry.ready === true ? 'installed' : 'needs repair';
2891
+ const codex = integrationLabel(st?.codex);
2892
+ const zcode = integrationLabel(st?.zcode);
2893
+ const claude = integrationLabel(st?.claude);
2585
2894
  const official = officialWebIntegrationStatus({ home, releaseDir: pointer?.path });
2586
2895
  const officialWeb = !official.legacy_present ? 'not present (native 3210 control plane)' : official.healthy ? 'legacy full bridge present (deprecated; manual cleanup available)' : 'legacy full bridge record present but unhealthy (deprecated)';
2587
- const startupState = installer.windowsStartupStatus?.({ home });
2896
+ const startupState = installer.windowsStartupStatus?.({ home, root: runningPackageRoot() });
2588
2897
  const windowsStartup = !startupState?.supported ? 'not supported'
2589
2898
  : startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
2590
2899