@ran-sh/dsh-crew 2.2.8 → 2.2.9

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.8",
3
+ "version": "2.2.9",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "2.2.8",
3
+ "version": "2.2.9",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -1247,26 +1247,35 @@ export function installHudSegment({ home = homedir() } = {}) {
1247
1247
  return { ok: true, actions: [...(bak ? [`backup: ${bak}`] : []), 'statusLine: claude-hud now runs worker-segment.sh via --extra-cmd'] };
1248
1248
  }
1249
1249
 
1250
- // Crew-owned directories, identified by path SEGMENT rather than by substring: the
1251
- // payload tree lives under `.../dsh-crew/...` and the pre-rename identity used
1252
- // `dsh-workers`. A user path that merely contains those words (`my-dsh-crew-notes`)
1253
- // is not Crew's, and nothing here may treat it as if it were.
1254
- function crewOwnedDirectory(value) {
1255
- if (typeof value !== 'string' || !value.trim()) return false;
1256
- return value.split(/[\\/]+/).filter(Boolean).some((segment) => {
1257
- const lower = segment.toLowerCase();
1258
- return lower === 'dsh-crew' || lower === 'dsh-workers';
1259
- });
1250
+ // The paths Crew has recorded as its own in this Claude host: the marketplace records
1251
+ // it wrote (under the current and pre-rename names, in settings and in the CLI's own
1252
+ // cache), the constant an older installer used, and the checkout this module runs
1253
+ // from. Only these are grounds for removing anything: a directory that merely happens
1254
+ // to be named `dsh-crew` belongs to whoever made it, and a path that is not recorded
1255
+ // here cannot be identified as Crew's, so it is left alone.
1256
+ function crewOwnedRoots({ home, settings, markets, installed }) {
1257
+ const roots = [];
1258
+ const push = (value) => { if (typeof value === 'string' && value.trim() && !roots.includes(value.trim())) roots.push(value.trim()); };
1259
+ for (const name of CREW_CLAUDE_MARKETPLACES) {
1260
+ push(settings?.extraKnownMarketplaces?.[name]?.source?.path);
1261
+ push(markets?.[name]?.source?.path);
1262
+ push(markets?.[name]?.installLocation);
1263
+ }
1264
+ for (const key of CREW_CLAUDE_PLUGIN_KEYS) {
1265
+ const entries = Array.isArray(installed?.plugins?.[key]) ? installed.plugins[key] : [installed?.plugins?.[key]];
1266
+ for (const entry of entries) push(entry?.installPath);
1267
+ }
1268
+ push(join(home, '.config', 'dsh-crew', 'marketplace'));
1269
+ push(ROOT);
1270
+ return roots;
1260
1271
  }
1261
1272
 
1262
- // Only the status line `--statusline` installs is Crew's to remove: it points at
1263
- // Crew's own script, in Crew's own directory, in the exact shape the installer
1264
- // writes (`bash <root>/statusline/statusline.sh`). A command that merely mentions
1265
- // these words belongs to whoever wrote it.
1266
- function crewOwnedStatusLine(value) {
1273
+ function crewOwnedStatusLine(value, roots) {
1267
1274
  const command = typeof value?.command === 'string' ? value.command.trim() : '';
1268
- const match = /^bash\s+(.+[\\/])statusline[\\/]statusline\.sh$/i.exec(command);
1269
- return match ? crewOwnedDirectory(match[1]) : false;
1275
+ const match = /^bash\s+(.+)$/i.exec(command);
1276
+ if (!match) return false;
1277
+ const path = normalizedPath(match[1].trim());
1278
+ return path !== null && roots.some((root) => normalizedPath(join(root, 'statusline', 'statusline.sh')) === path);
1270
1279
  }
1271
1280
 
1272
1281
  // The claude CLI records, per marketplace and plugin, what it materialized, in
@@ -1304,16 +1313,19 @@ function removeCrewClaudeCacheRecords({ home, actions }) {
1304
1313
  export function uninstallClaudeCode({ home = homedir() } = {}) {
1305
1314
  const actions = [];
1306
1315
  const settingsFile = join(home, '.claude', 'settings.json');
1316
+ const cacheDir = join(home, '.claude', 'plugins');
1307
1317
  const settings = readJson(settingsFile, null);
1318
+ const markets = readJson(join(cacheDir, 'known_marketplaces.json'), null);
1319
+ const installed = readJson(join(cacheDir, 'installed_plugins.json'), null);
1320
+ const roots = crewOwnedRoots({ home, settings, markets, installed });
1308
1321
  if (settings) {
1309
1322
  backup(settingsFile);
1310
- const mpDir = join(home, '.config', 'dsh-crew', 'marketplace');
1311
1323
  if (Array.isArray(settings.extraKnownMarketplaces)) {
1312
- // Legacy array shape: entries are `{ path }` records this installer wrote under
1313
- // one of its own names, including the pre-rename one. Anything else in that
1314
- // array belongs to another plugin and stays.
1324
+ // Legacy array shape: an entry is removed only when it IS one of the paths Crew
1325
+ // recorded as its own. What the path is spelled with is not evidence — a
1326
+ // marketplace under someone else's `dsh-crew` directory stays.
1315
1327
  settings.extraKnownMarketplaces = settings.extraKnownMarketplaces
1316
- .filter((entry) => !crewOwnedDirectory(entry?.path) && entry?.path !== mpDir);
1328
+ .filter((entry) => !roots.some((root) => normalizedPath(entry?.path) === normalizedPath(root)));
1317
1329
  } else if (settings.extraKnownMarketplaces) {
1318
1330
  for (const name of CREW_CLAUDE_MARKETPLACES) delete settings.extraKnownMarketplaces[name];
1319
1331
  }
@@ -1326,7 +1338,7 @@ export function uninstallClaudeCode({ home = homedir() } = {}) {
1326
1338
  settings.permissions.allow = settings.permissions.allow.filter((rule) => typeof rule !== 'string'
1327
1339
  || !CREW_CLAUDE_PERMISSION_PREFIXES.some((prefix) => rule.startsWith(prefix)));
1328
1340
  }
1329
- if (crewOwnedStatusLine(settings.statusLine)) {
1341
+ if (crewOwnedStatusLine(settings.statusLine, roots)) {
1330
1342
  delete settings.statusLine;
1331
1343
  actions.push('statusLine: removed (Crew-installed)');
1332
1344
  }
package/src/log-prune.mjs CHANGED
@@ -2,9 +2,12 @@
2
2
  // new dsh-crew-dsh-crew-3210-<stamp>.out/.err.log pair, every desktop launch a
3
3
  // dsh-crew-web-*, every official launch a dsh-official-web-*, and the launcher
4
4
  // appends to dsh-crew-launcher.log forever. They are the only post-mortem evidence
5
- // for a crash-before-readiness, so the rule keeps the most recent runs instead of
6
- // deleting everything: per family, the newest `keep` runs survive, plus anything
7
- // written within the recency guard (a running process owns its own log files).
5
+ // for a crash-before-readiness, so the rule bounds them instead of deleting them all.
6
+ // A run survives when it is BOTH within its family's newest `keep` AND younger than
7
+ // the age bound; exceeding either one removes it, unless it was written within the
8
+ // recency guard (a running process owns its own log files). The newest `keep` runs
9
+ // therefore do not survive unconditionally — only a run that is also inside the age
10
+ // bound does, which is what keeps a burst of starts from filling the directory.
8
11
 
9
12
  import { readdirSync, rmSync, statSync } from 'node:fs';
10
13
  import { join } from 'node:path';
@@ -76,8 +76,17 @@ function normalizeEvidence(row, evidence) {
76
76
  // flip the row is how "this route is unused" would turn into "this route passed".
77
77
  // What was reported is kept as metadata rather than thrown away.
78
78
  if (row.status === 'NOT_APPLICABLE') {
79
- if (typeof evidence.status !== 'string') return row;
80
- return { ...row, reported_evidence: { status: evidence.status, ...(typeof evidence.reason_code === 'string' && evidence.reason_code.trim() ? { reason_code: evidence.reason_code.trim() } : {}), ...(typeof evidence.evidence_source === 'string' && evidence.evidence_source.trim() ? { evidence_source: evidence.evidence_source.trim() } : {}) } };
79
+ // The same sanitized fields an applied record keeps, so "the observation is not
80
+ // lost" is a statement the code supports rather than a claim about three of five.
81
+ const source = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
82
+ const reported = {
83
+ status: evidence.status,
84
+ ...(source(evidence.reason_code) ? { reason_code: source(evidence.reason_code) } : {}),
85
+ ...(source(evidence.evidence_source) ? { evidence_source: source(evidence.evidence_source) } : {}),
86
+ ...(source(evidence.evidence_ref) ? { evidence_ref: source(evidence.evidence_ref) } : {}),
87
+ ...(source(evidence.detail_code) && /^[A-Z][A-Z0-9_]{0,127}$/u.test(evidence.detail_code) ? { detail_code: evidence.detail_code } : {}),
88
+ };
89
+ return { ...row, reported_evidence: reported };
81
90
  }
82
91
  const reason = typeof evidence.reason_code === 'string' && evidence.reason_code.trim()
83
92
  ? evidence.reason_code.trim()
@@ -195,7 +204,7 @@ export function buildReadinessMatrix({
195
204
  const selectionKnown = typeof workerSelection?.provider === 'string' && workerSelection.provider.length > 0;
196
205
  const deepseekRouteSelected = workerProviderMode === 'deepseek-official'
197
206
  || workerSelection?.provider === 'deepseek-official';
198
- if (workerProviderMode !== null && selectionKnown && !deepseekRouteSelected) {
207
+ if (workerProviderMode === 'follow-dsh' && selectionKnown && !deepseekRouteSelected) {
199
208
  for (const id of POLICY_NOT_APPLICABLE_ROWS) {
200
209
  rows[id] = baseRow(id, 'real-execution', 'NOT_APPLICABLE', READINESS_REASON_CODES.WORKER_PROVIDER_FOLLOWS_DSH, 'runtime-policy');
201
210
  }
@@ -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.8';
39
+ export const RUNTIME_VERSION = '2.2.9';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([
@@ -108,12 +108,14 @@ function projectModelRole({ role, selected, health, jobs, runtime, now, executio
108
108
  if (healthCurrent && matchingHealth.state === 'callable') {
109
109
  return { state: 'CALLABLE', reason_code: matchingHealth.reason_code ?? 'PROVIDER_CALLABLE', selected, observed_at: matchingHealth.observed_at ?? null, expires_at: expiresAt, source: 'provider_health', last_success: null };
110
110
  }
111
- // What proves the route ran is a provider response, not a passing task contract:
112
- // a job that reached `done` with its execution completed has demonstrably called
113
- // the selected model even when the task itself came back `partial`. Failed and
114
- // cancelled work never reaches this predicate, and neither does a job that errored
115
- // before the model answered (its execution_status is `failed`, not `completed`).
116
- const responded = (job) => job?.task_status === 'success' || job?.execution_status === 'completed';
111
+ // Callability asks for positive proof that the provider answered, so the ledger
112
+ // fact that proves it is the one that says the execution ended normally: a job the
113
+ // worker recorded as `done` whose `execution_status` is `completed`. The task-level
114
+ // verdict is deliberately NOT part of this — a `partial` task still had a provider
115
+ // response — and a job whose execution failed or was never recorded is not evidence
116
+ // of a response, however its task was later classified. A recovery probe or a real
117
+ // run is what a route without such a job needs.
118
+ const responded = (job) => job?.execution_status === 'completed';
117
119
  const completed = (Array.isArray(jobs) ? jobs : [])
118
120
  .filter((job) => job?.role === role && job?.provider === selected.provider && job?.model === selected.model
119
121
  && job?.status === 'done' && responded(job)