@ran-sh/dsh-crew 2.2.6 → 2.2.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
@@ -12,9 +12,16 @@ The matrix is emitted by `hubStatus()` and is therefore visible inside the exist
12
12
  - `FAIL` — a check actually ran and produced an incompatible or failed result.
13
13
  - `BLOCKED` — the check could not run because required infrastructure or authorization was unavailable.
14
14
  - `SKIP` — the row is intentionally not applicable for the active policy/path.
15
+ - `NOT_APPLICABLE` — the row asks about a route this machine does not use, and the
16
+ matrix says so instead of omitting it. The two built-in DeepSeek rows are the live
17
+ case: they read `NOT_APPLICABLE` / `WORKER_PROVIDER_FOLLOWS_DSH` whenever a known,
18
+ other provider is selected, and keep `NOT_RUN` when the selection is unknown.
15
19
  - `NOT_RUN` — no trusted evidence has been supplied for the row.
16
20
 
17
- `BLOCKED` and `SKIP` are not failures. `NOT_RUN` is not success.
21
+ `BLOCKED` and `SKIP` are not failures. `NOT_RUN` is not success, and `NOT_APPLICABLE`
22
+ is an answer: it is reported as itself on any component that consumes the row, and it
23
+ cannot lower the readiness aggregate — only a `FAIL`, an unproven row or an
24
+ `UNAVAILABLE` component does.
18
25
 
19
26
  ## Evidence classes
20
27
 
@@ -61,7 +68,7 @@ actually run" — `provider_health`, `reviewer_health`, `model_execution`,
61
68
  `worker_primary_callable`, `reviewer_primary_callable`, `reviewer_pipeline` — is
62
69
  filtered to whatever route is currently selected, so an operator on other
63
70
  providers is covered by those and loses nothing by the two DeepSeek rows
64
- reading `NOT_RUN`. A static row per provider was tried and replaced by these
71
+ reading `NOT_APPLICABLE` under `follow-dsh`. A static row per provider was tried and replaced by these
65
72
  dynamic signals; `opencode_go_mimo_qwen` was that attempt's last trace in this
66
73
  document.
67
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
package/scripts/setup.mjs CHANGED
@@ -384,11 +384,19 @@ export async function setupStatus({ log = console.log, root = ROOT, home = homed
384
384
  } catch { dshPlugin = 'unknown'; }
385
385
  }
386
386
  log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
387
+ // The frontend is on-demand by design: 3210 is the service, and 3080 is started
388
+ // for a browser session. Rendering it as anything else reads as a missing piece
389
+ // of the installation, which it is not.
390
+ const frontendConfig = (installer.readGlobalConfig ?? realInstaller.readGlobalConfig)({ configFile: join(home, '.config', 'dsh-crew', 'config.json') });
391
+ const frontend = frontendConfig?.frontend_autostart === true
392
+ ? 'auto-started at login (dsh-crew open for a session URL)'
393
+ : 'available on demand (dsh-crew open)';
394
+ log(`Frontend (3080): ${frontend}`);
387
395
  log(`Codex Desktop integration: ${codex}`);
388
396
  log(`ZCode integration: ${zcode}`);
389
397
  log(`Claude Code integration: ${claude}`);
390
398
  log(`Windows login startup: ${windowsStartup}`);
391
- return { ok: true, dshPlugin, codex, zcode, claude, windowsStartup };
399
+ return { ok: true, dshPlugin, frontend, codex, zcode, claude, windowsStartup };
392
400
  }
393
401
 
394
402
  export async function runSetupCli({ argv = process.argv.slice(2), run: actions = {}, log = console.log } = {}) {
@@ -32,6 +32,10 @@ function workspaceComponent(workspace) {
32
32
  function readinessFromRow(entry, { pass = 'READY', notRun = 'DEGRADED' } = {}) {
33
33
  if (!entry) return component('UNAVAILABLE', 'NO_EVIDENCE');
34
34
  if (entry.status === 'PASS') return component(pass, entry.reason_code ?? 'CHECK_PASSED');
35
+ // A row that does not apply to this machine is reported as such, not as a check
36
+ // that has not run: NOT_APPLICABLE says the question was answered, and it cannot
37
+ // pull the aggregate down (see the readiness roll-up below).
38
+ if (entry.status === 'NOT_APPLICABLE') return component('NOT_APPLICABLE', entry.reason_code ?? 'CHECK_NOT_APPLICABLE');
35
39
  if (entry.status === 'NOT_RUN' || entry.status === 'SKIP') return component(notRun, entry.reason_code ?? 'CHECK_NOT_RUN');
36
40
  return component('UNAVAILABLE', entry.reason_code ?? 'CHECK_FAILED');
37
41
  }
@@ -50,7 +54,12 @@ function modelReadinessFromSnapshot(readinessSnapshot, matrix, runtime, expected
50
54
  });
51
55
  if (validation.ok && validation.state === 'CALLABLE') return component('READY', 'CURRENT_MODEL_CALLABLE', { captured_at: callability.captured_at, expires_at: callability.expires_at, runtime_id: callability.current_runtime_id });
52
56
  if (validation.ok && validation.state === 'NOT_CALLABLE') return component('UNAVAILABLE', callability.roles.worker?.state === 'NOT_CALLABLE' ? callability.roles.worker.reason_code : callability.roles.reviewer.reason_code ?? 'CURRENT_MODEL_NOT_CALLABLE');
57
+ // Aging evidence is reported as aging, not as a fault, and "nothing has proven
58
+ // this route yet" is reported as unknown. The validator's own success code is
59
+ // `MODEL_CALLABILITY_VALID`, and rendering that next to DEGRADED read as a
60
+ // contradiction: the projection was valid, the evidence was simply not current.
53
61
  if (validation.ok && validation.state === 'STALE') return component('DEGRADED', 'MODEL_EVIDENCE_STALE');
62
+ if (validation.ok) return component('DEGRADED', 'MODEL_CALLABILITY_UNKNOWN');
54
63
  return component('DEGRADED', validation.reason_code ?? 'MODEL_CALLABILITY_UNKNOWN');
55
64
  }
56
65
  if (providerHealthEvidence?.status === 'FAIL') return readinessFromRow(providerHealthEvidence);
@@ -17,6 +17,14 @@ import { integrationRoot } from './crew-paths.mjs';
17
17
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
18
18
  const MARKETPLACE_NAME = 'dsh-crew';
19
19
  const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
20
+ // Every name this installer has ever registered Crew under in a Claude host, and
21
+ // the only names its uninstall may remove. `dsh-workers` is the pre-rename
22
+ // identity: an operator who upgraded keeps that marketplace, plugin entry and its
23
+ // permission rules until the same uninstall cleans them, and nothing else in the
24
+ // file is Crew's to touch.
25
+ const CREW_CLAUDE_MARKETPLACES = [MARKETPLACE_NAME, 'dsh-workers'];
26
+ const CREW_CLAUDE_PLUGIN_KEYS = [PLUGIN_KEY, 'dsh-workers@dsh-workers'];
27
+ const CREW_CLAUDE_PERMISSION_PREFIXES = ['mcp__plugin_dsh-crew_', 'mcp__plugin_dsh-workers_'];
20
28
  // A Claude Code plugin refresh is a real copy of the plugin tree, and its cost
21
29
  // tracks machine load: 163s measured idle, ~6 minutes measured during an
22
30
  // activation. The install is the step that copies, so it gets the ceiling that
@@ -645,7 +653,7 @@ export function uninstallCodex({ home = homedir(), env = process.env } = {}) {
645
653
  export function claudeIntegrationLine(result) {
646
654
  if (result?.ok === false) return '✗ Claude Code integration failed';
647
655
  if (result?.detected === false) {
648
- return '- Claude Code not detected; settings registered, CLI step skipped';
656
+ return '- Claude Code not installed (claude CLI not found); nothing written';
649
657
  }
650
658
  if (result?.degraded === true) {
651
659
  return `- Claude Code integration registered, but not loaded: ${result.reason ?? 'plugin snapshot not refreshed'}`;
@@ -906,9 +914,22 @@ export async function runClaudeStep(args, { timeoutMs = CLAUDE_STEP_TIMEOUT_MS,
906
914
  };
907
915
  }
908
916
 
909
- export async function installClaudeCode({ home = homedir(), statusline = false, root = ROOT } = {}) {
917
+ export async function installClaudeCode({ home = homedir(), statusline = false, root = ROOT, resolveClaude = resolveClaudeCommand } = {}) {
910
918
  const actions = [];
911
919
 
920
+ // This registration exists only to make Crew callable FROM Claude Code, so a
921
+ // host without the `claude` CLI has nothing for it to serve. Writing it anyway
922
+ // left Crew's marketplace, plugin entry and permission rules in a file Crew
923
+ // could never exercise, next to a footprint that read back as an integration
924
+ // wanting a CLI the machine does not have. Detect once, up front, and write
925
+ // nothing when the host cannot use it: `host_detected` then reports the truth
926
+ // with no stale registration to explain away.
927
+ const claudeCommand = resolveClaude();
928
+ if (!claudeCommand) {
929
+ actions.push('claude CLI not found; no Claude configuration written');
930
+ return { ok: true, detected: false, actions };
931
+ }
932
+
912
933
  // The repository carries its own marketplace manifest (.claude-plugin/
913
934
  // marketplace.json with source "."), so the marketplace root IS the plugin
914
935
  // checkout itself — no parent-directory layout assumption. This keeps the
@@ -999,7 +1020,8 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
999
1020
  // Materialize the install through the claude CLI: registers the marketplace
1000
1021
  // in the plugin cache (settings alone leave a stale/absent cache entry) and
1001
1022
  // pulls the plugin so the next session loads it. Best-effort: settings are
1002
- // already correct, so a missing CLI just means one manual `plugin install`.
1023
+ // already correct, so a CLI that fails on its own terms leaves the operator one
1024
+ // manual `plugin install` short rather than an unregistered host.
1003
1025
  const registered = readJson(join(home, '.claude', 'plugins', 'known_marketplaces.json'), {})?.[MARKETPLACE_NAME];
1004
1026
  const installedRecord = readJson(join(home, '.claude', 'plugins', 'installed_plugins.json'), {})?.plugins?.[PLUGIN_KEY];
1005
1027
  const installedEntries = Array.isArray(installedRecord) ? installedRecord : [installedRecord];
@@ -1019,14 +1041,9 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
1019
1041
  actions.push('cli: skipped (non-default home; test mode)');
1020
1042
  return { ok: true, actions };
1021
1043
  }
1022
- // The CLI is best-effort, but a host that does not have it must not be told to
1023
- // run it. The checkout entry already gates on this; doing it here too keeps the
1024
- // two entries describing the same machine the same way.
1025
- const claudeCommand = resolveClaudeCommand();
1026
- if (!claudeCommand) {
1027
- actions.push('cli: skipped (claude not found)');
1028
- return { ok: true, detected: false, actions };
1029
- }
1044
+ // `claudeCommand` was resolved before anything was written: reaching this line
1045
+ // already proves the CLI is there, so the step is best-effort rather than
1046
+ // possibly-pointless.
1030
1047
  return refreshClaudePlugin({ home, root, actions, claudeCommand });
1031
1048
  }
1032
1049
 
@@ -1230,25 +1247,76 @@ export function installHudSegment({ home = homedir() } = {}) {
1230
1247
  return { ok: true, actions: [...(bak ? [`backup: ${bak}`] : []), 'statusLine: claude-hud now runs worker-segment.sh via --extra-cmd'] };
1231
1248
  }
1232
1249
 
1250
+ // Only the status line `--statusline` installs is Crew's to remove: it points at
1251
+ // Crew's own script. Any other status line in the file belongs to whoever
1252
+ // configured it, even when it happens to name this checkout.
1253
+ function crewOwnedStatusLine(value) {
1254
+ const command = typeof value?.command === 'string' ? value.command : '';
1255
+ return command.includes('statusline.sh') && command.includes('dsh-crew');
1256
+ }
1257
+
1258
+ // The claude CLI records, per marketplace and plugin, what it materialized, in
1259
+ // files of its own. Unregistering the settings and leaving those records behind
1260
+ // hands the next install a snapshot with no owner, so an explicit uninstall
1261
+ // clears them too. Crew-scoped and best-effort: a file that is absent, unreadable
1262
+ // or shaped differently than expected is left exactly as it is found.
1263
+ function removeCrewClaudeCacheRecords({ home, actions }) {
1264
+ const dir = join(home, '.claude', 'plugins');
1265
+ const marketsFile = join(dir, 'known_marketplaces.json');
1266
+ const markets = readJson(marketsFile, null);
1267
+ if (markets && typeof markets === 'object' && !Array.isArray(markets)) {
1268
+ const owned = CREW_CLAUDE_MARKETPLACES.filter((name) => Object.hasOwn(markets, name));
1269
+ if (owned.length) {
1270
+ backup(marketsFile);
1271
+ for (const name of owned) delete markets[name];
1272
+ writeFileSync(marketsFile, JSON.stringify(markets, null, 2) + '\n');
1273
+ actions.push(`marketplace cache: removed ${owned.join(', ')}`);
1274
+ }
1275
+ }
1276
+ const pluginsFile = join(dir, 'installed_plugins.json');
1277
+ const installed = readJson(pluginsFile, null);
1278
+ if (installed && typeof installed === 'object' && !Array.isArray(installed)
1279
+ && installed.plugins && typeof installed.plugins === 'object' && !Array.isArray(installed.plugins)) {
1280
+ const owned = CREW_CLAUDE_PLUGIN_KEYS.filter((key) => Object.hasOwn(installed.plugins, key));
1281
+ if (owned.length) {
1282
+ backup(pluginsFile);
1283
+ for (const key of owned) delete installed.plugins[key];
1284
+ writeFileSync(pluginsFile, JSON.stringify(installed, null, 2) + '\n');
1285
+ actions.push(`plugin cache: removed ${owned.join(', ')}`);
1286
+ }
1287
+ }
1288
+ }
1289
+
1233
1290
  export function uninstallClaudeCode({ home = homedir() } = {}) {
1291
+ const actions = [];
1234
1292
  const settingsFile = join(home, '.claude', 'settings.json');
1235
1293
  const settings = readJson(settingsFile, null);
1236
- if (!settings) return { ok: true, actions: ['settings.json not found'] };
1237
- backup(settingsFile);
1238
- const mpDir = join(home, '.config', 'dsh-crew', 'marketplace');
1239
- if (Array.isArray(settings.extraKnownMarketplaces)) {
1240
- settings.extraKnownMarketplaces = settings.extraKnownMarketplaces.filter((m) => m?.path !== mpDir);
1241
- } else if (settings.extraKnownMarketplaces) {
1242
- delete settings.extraKnownMarketplaces[MARKETPLACE_NAME];
1243
- }
1244
- if (Array.isArray(settings.enabledPlugins)) {
1245
- settings.enabledPlugins = settings.enabledPlugins.filter((p) => p !== PLUGIN_KEY);
1246
- } else if (settings.enabledPlugins) {
1247
- delete settings.enabledPlugins[PLUGIN_KEY];
1248
- }
1249
- if (settings.permissions?.allow) {
1250
- settings.permissions.allow = settings.permissions.allow.filter((r) => !r.startsWith('mcp__plugin_dsh-crew_'));
1294
+ if (settings) {
1295
+ backup(settingsFile);
1296
+ const mpDir = join(home, '.config', 'dsh-crew', 'marketplace');
1297
+ if (Array.isArray(settings.extraKnownMarketplaces)) {
1298
+ settings.extraKnownMarketplaces = settings.extraKnownMarketplaces.filter((m) => m?.path !== mpDir);
1299
+ } else if (settings.extraKnownMarketplaces) {
1300
+ for (const name of CREW_CLAUDE_MARKETPLACES) delete settings.extraKnownMarketplaces[name];
1301
+ }
1302
+ if (Array.isArray(settings.enabledPlugins)) {
1303
+ settings.enabledPlugins = settings.enabledPlugins.filter((p) => !CREW_CLAUDE_PLUGIN_KEYS.includes(p));
1304
+ } else if (settings.enabledPlugins) {
1305
+ for (const key of CREW_CLAUDE_PLUGIN_KEYS) delete settings.enabledPlugins[key];
1306
+ }
1307
+ if (settings.permissions?.allow) {
1308
+ settings.permissions.allow = settings.permissions.allow.filter((rule) => typeof rule !== 'string'
1309
+ || !CREW_CLAUDE_PERMISSION_PREFIXES.some((prefix) => rule.startsWith(prefix)));
1310
+ }
1311
+ if (crewOwnedStatusLine(settings.statusLine)) {
1312
+ delete settings.statusLine;
1313
+ actions.push('statusLine: removed (Crew-installed)');
1314
+ }
1315
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
1316
+ actions.push('unregistered from settings.json (backup kept)');
1317
+ } else {
1318
+ actions.push('settings.json not found');
1251
1319
  }
1252
- writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
1253
- return { ok: true, actions: ['unregistered from settings.json (backup kept)'] };
1320
+ removeCrewClaudeCacheRecords({ home, actions });
1321
+ return { ok: true, actions };
1254
1322
  }
@@ -3327,6 +3327,15 @@ export function npxStatus({
3327
3327
  }
3328
3328
  log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
3329
3329
  log(`Official 3080 UI bridge: ${officialWeb}`);
3330
+ // The frontend is not an install that can be missing: 3210 is the service, and
3331
+ // 3080 is started on demand. Saying so is the difference between "this machine is
3332
+ // incomplete" and "run `dsh-crew open` when you want the browser surface" — and
3333
+ // whether 3080 happens to be up right now does not change either answer.
3334
+ const frontendConfig = (installer.readGlobalConfig ?? realInstaller.readGlobalConfig)({ configFile: join(home, '.config', 'dsh-crew', 'config.json') });
3335
+ const frontend = frontendConfig?.frontend_autostart === true
3336
+ ? 'auto-started at login (dsh-crew open for a session URL)'
3337
+ : 'available on demand (dsh-crew open)';
3338
+ log(`Frontend (3080): ${frontend}`);
3330
3339
  // The desktop app carries its own bridge entry, which pins a revision: after a
3331
3340
  // payload update that entry is stale until it is re-pointed, so say which of the
3332
3341
  // three states the machine is in instead of leaving the panel silently old.
@@ -3349,6 +3358,7 @@ export function npxStatus({
3349
3358
  installedPath: pointer?.path ?? null,
3350
3359
  dshPlugin,
3351
3360
  officialWeb,
3361
+ frontend,
3352
3362
  codex,
3353
3363
  zcode,
3354
3364
  claude,
@@ -19,9 +19,10 @@ function plan(home, root) {
19
19
  // `core.autocrlf=true` checkout hashes to a different revision than the npm
20
20
  // payload it installs, which made readiness look for a snapshot that does not
21
21
  // exist and reported "needs repair" on a correct install (and made every payload
22
- // update look like a stale desktop revision).
22
+ // update look like a stale desktop revision). Lone CR collapses too: one line
23
+ // break is one line break in these sources.
23
24
  const canonicalBytes = (bytes) => (bytes.includes(0) ? bytes
24
- : Buffer.from(bytes.toString('utf8').replace(/\r\n/g, '\n'), 'utf8'));
25
+ : Buffer.from(bytes.toString('utf8').replace(/\r\n?/g, '\n'), 'utf8'));
25
26
  for (const [name, bytes] of files) files.set(name, canonicalBytes(bytes));
26
27
  const hash = createHash('sha256').update(JSON.stringify(metadata));
27
28
  for (const [name, bytes] of files) hash.update(name).update(bytes);
@@ -43,10 +44,12 @@ function plan(home, root) {
43
44
  // snapshot was written from the npm payload (LF). The bridge sources are text that
44
45
  // either line ending serves, so a CRLF checkout must not read as a drifted snapshot;
45
46
  // bytes are still compared first, and the tolerant path only applies to text.
47
+ // Lone CR is normalized as well, so a file saved with classic-Mac endings reads as
48
+ // the same source rather than as content drift.
46
49
  function sameTextContent(installed, expected) {
47
50
  if (installed.equals(expected)) return true;
48
51
  if (installed.includes(0) || expected.includes(0)) return false;
49
- const normalize = (buffer) => buffer.toString('utf8').replace(/\r\n/g, '\n');
52
+ const normalize = (buffer) => buffer.toString('utf8').replace(/\r\n?/g, '\n');
50
53
  return normalize(installed) === normalize(expected);
51
54
  }
52
55
 
@@ -61,7 +64,7 @@ export function officialFrontendAssetsReady({ home = homedir(), root } = {}) {
61
64
  try {
62
65
  const p = plan(home, root);
63
66
  const overlay = readFileSync(p.overlayFile, 'utf8');
64
- return matches(p) && (overlay === p.overlay || overlay.replace(/\r\n/g, '\n') === p.overlay.replace(/\r\n/g, '\n'));
67
+ return matches(p) && (overlay === p.overlay || overlay.replace(/\r\n?/g, '\n') === p.overlay.replace(/\r\n?/g, '\n'));
65
68
  } catch { return false; }
66
69
  }
67
70
 
@@ -129,9 +129,12 @@ function sha256File(file) {
129
129
  // Git checks these scripts out as CRLF on a `core.autocrlf=true` machine and
130
130
  // PowerShell accepts either, so a line-ending difference is not a difference this
131
131
  // check may report: comparing raw bytes made every Windows dev checkout read as
132
- // "needs repair" while the npm payload (LF) was installed correctly.
132
+ // "needs repair" while the npm payload (LF) was installed correctly. Lone CR is
133
+ // collapsed too — an editor that saved one of these files with classic-Mac
134
+ // endings is still the same script, and only a real character difference should
135
+ // send the operator to a repair step.
133
136
  function normalizeEol(text) {
134
- return String(text).replace(/\r\n/g, '\n');
137
+ return String(text).replace(/\r\n?/g, '\n');
135
138
  }
136
139
 
137
140
  function samePath(left, right) {
package/src/log-prune.mjs CHANGED
@@ -10,7 +10,7 @@ import { readdirSync, rmSync, statSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { tmpdir } from 'node:os';
12
12
 
13
- export const DEFAULT_KEEP_RUNS = 20;
13
+ export const DEFAULT_KEEP_RUNS = 10;
14
14
  export const DEFAULT_MAX_AGE_DAYS = 14;
15
15
  const RECENT_GUARD_MS = 5 * 60 * 1000;
16
16
 
@@ -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
- const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_RUN']);
8
+ const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_APPLICABLE', 'NOT_RUN']);
9
9
 
10
10
  export const READINESS_REASON_CODES = Object.freeze({
11
11
  LIVE_CHECK_PASSED: 'LIVE_CHECK_PASSED',
@@ -22,7 +22,7 @@ export const READINESS_REASON_CODES = Object.freeze({
22
22
  NO_CI_EVIDENCE: 'NO_CI_EVIDENCE',
23
23
  NO_EXECUTION_EVIDENCE: 'NO_EXECUTION_EVIDENCE',
24
24
  CREDENTIAL_STATUS_NOT_PROBED: 'CREDENTIAL_STATUS_NOT_PROBED',
25
- ROUTE_NOT_SELECTED: 'ROUTE_NOT_SELECTED',
25
+ WORKER_PROVIDER_FOLLOWS_DSH: 'WORKER_PROVIDER_FOLLOWS_DSH',
26
26
  EVIDENCE_REPORTED: 'EVIDENCE_REPORTED',
27
27
  });
28
28
 
@@ -166,15 +166,18 @@ export function buildReadinessMatrix({
166
166
  // The two built-in DeepSeek rows describe a route only when the operator runs
167
167
  // it. Under follow-dsh with a KNOWN, other selected provider they can never
168
168
  // gather evidence, and NOT_RUN reads like a check that should have run rather
169
- // than a route that was never chosen. An unknown selection stays NOT_RUN: this
170
- // matrix is conservative, and "we could not tell" must not read as "not
171
- // applicable". Reported evidence still wins, because it is applied below.
169
+ // than a route that was never chosen. They are reported NOT_APPLICABLE: the rows
170
+ // stay in the matrix (dropping them would hide that the question was asked and
171
+ // answered), and neither status can move the aggregate — only a FAIL, an
172
+ // UNPROVEN row or an UNAVAILABLE component does. An unknown selection stays
173
+ // NOT_RUN: this matrix is conservative, and "we could not tell" must not read as
174
+ // "not applicable". Reported evidence still wins, because it is applied below.
172
175
  const selectionKnown = typeof workerSelection?.provider === 'string' && workerSelection.provider.length > 0;
173
176
  const deepseekRouteSelected = workerProviderMode === 'deepseek-official'
174
177
  || workerSelection?.provider === 'deepseek-official';
175
178
  if (workerProviderMode !== null && selectionKnown && !deepseekRouteSelected) {
176
179
  for (const id of ['deepseek_flash', 'deepseek_pro']) {
177
- rows[id] = baseRow(id, 'real-execution', 'SKIP', READINESS_REASON_CODES.ROUTE_NOT_SELECTED, 'runtime-policy');
180
+ rows[id] = baseRow(id, 'real-execution', 'NOT_APPLICABLE', READINESS_REASON_CODES.WORKER_PROVIDER_FOLLOWS_DSH, 'runtime-policy');
178
181
  }
179
182
  }
180
183
 
@@ -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.6';
39
+ export const RUNTIME_VERSION = '2.2.7';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([
@@ -127,6 +127,24 @@ function Write-LaunchLog {
127
127
  }
128
128
  }
129
129
 
130
+ # Every launch appends to one file for the life of the machine, so it grows without
131
+ # bound while only its tail is ever read. Rotation runs once per launch before the
132
+ # first line is written and keeps two generations: a post-mortem needs the current
133
+ # run and the one before it. Deliberately best-effort — a locked, unreadable or
134
+ # huge log is a reason to leave it alone, never a reason to fail the launch.
135
+ $launcherLogMaxBytes = 5MB
136
+ function Rotate-LaunchLog {
137
+ try {
138
+ if (-not (Test-Path -LiteralPath $launcherLog -PathType Leaf)) { return }
139
+ if ((Get-Item -LiteralPath $launcherLog -ErrorAction Stop).Length -lt $launcherLogMaxBytes) { return }
140
+ $oldest = "$launcherLog.2"
141
+ $previous = "$launcherLog.1"
142
+ if (Test-Path -LiteralPath $oldest) { Remove-Item -LiteralPath $oldest -Force -ErrorAction Stop }
143
+ if (Test-Path -LiteralPath $previous) { Move-Item -LiteralPath $previous -Destination $oldest -Force -ErrorAction Stop }
144
+ Move-Item -LiteralPath $launcherLog -Destination $previous -Force -ErrorAction Stop
145
+ } catch { }
146
+ }
147
+
130
148
  function Resolve-OfficialHarnessCommand {
131
149
  $shim = Get-Command dsh.cmd -CommandType Application -ErrorAction Stop | Select-Object -First 1
132
150
  $root = Join-Path (Split-Path -Parent $shim.Source) 'node_modules\@deepseek-ai\dsh'
@@ -1528,6 +1546,7 @@ if ($env:DSH_CREW_LAUNCHER_TEST_IMPORT -eq '1') { return }
1528
1546
 
1529
1547
  try {
1530
1548
  New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
1549
+ Rotate-LaunchLog
1531
1550
  Write-LaunchLog ('Launcher started; mode={0}; user={1}' -f $Mode, $env:USERNAME)
1532
1551
  if (-not (Test-Path -LiteralPath $dshCli -PathType Leaf)) {
1533
1552
  throw "DSH CLI was not found at $dshCli. Configure DSH_CREW_DSH_CLI to a Crew-owned official CLI entry, or run: npm install -g @ran-sh/dsh-crew@latest; dsh-crew update"