@ran-sh/dsh-crew 2.0.3 → 2.0.5

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.0.3",
3
+ "version": "2.0.5",
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.0.3",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -600,7 +600,20 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
600
600
  } catch (err) {
601
601
  actions.push(`cli: plugin install failed — run manually: claude plugin install ${PLUGIN_KEY} (${String(err?.message ?? err).slice(0, 120)})`);
602
602
  }
603
- return { ok: true, actions };
603
+ // Report the state that resulted, not the step that was attempted. The CLI is
604
+ // best-effort — a machine without `claude` is a supported install, and its
605
+ // settings alone are correct — but a CLI that is present and slow, timed out,
606
+ // or failed leaves the snapshot exactly as stale as it was, and it is the
607
+ // snapshot that `installStatus` reads. Saying so here is what lets the caller
608
+ // stop printing a checkmark for a state it never verified.
609
+ if (claudeSnapshotReady(home, root)) return { ok: true, actions };
610
+ return {
611
+ ok: true,
612
+ degraded: true,
613
+ code: 'CLAUDE_PLUGIN_SNAPSHOT_STALE',
614
+ reason: `Claude Code has not loaded the plugin; run: claude plugin install ${PLUGIN_KEY}`,
615
+ actions,
616
+ };
604
617
  }
605
618
 
606
619
  export function installCodex({ home = homedir(), scope, root = ROOT, env = process.env } = {}) {
@@ -2626,6 +2626,25 @@ function currentInstallationHealth({ home }) {
2626
2626
  return { installed: true, healthy: validated.ok && registered, validated, registered, pointer };
2627
2627
  }
2628
2628
 
2629
+ /**
2630
+ * What the operator is told about the Claude Code integration.
2631
+ *
2632
+ * A missing `claude` CLI is a supported install, so `installClaudeCode` stays
2633
+ * best-effort and keeps `ok: true`. A CLI that is present and fails or times out
2634
+ * is a different thing: it leaves Claude Code without the plugin snapshot while
2635
+ * the settings still name it, which is the state that later reads as "needs
2636
+ * repair". This line used to be `${ok === false ? '✗' : '✓'}`, and since `ok` was
2637
+ * never false, the failure branch could not fire and a broken integration was
2638
+ * reported as a checkmark.
2639
+ */
2640
+ export function claudeIntegrationLine(result) {
2641
+ if (result?.ok === false) return '✗ Claude Code integration failed';
2642
+ if (result?.degraded === true) {
2643
+ return `- Claude Code integration registered, but not loaded: ${result.reason ?? 'plugin snapshot not refreshed'}`;
2644
+ }
2645
+ return '✓ Claude Code integration';
2646
+ }
2647
+
2629
2648
  async function activateRelease({ home, releaseDir, manifest, log, installer, supervisorRoot = runningPackageRoot() }) {
2630
2649
  const registration = ensureCrewPluginRegistration({ home, root: releaseDir, name: manifest.name });
2631
2650
  if (!registration.ok) {
@@ -2668,10 +2687,10 @@ async function activateRelease({ home, releaseDir, manifest, log, installer, sup
2668
2687
 
2669
2688
  const claude = await installer.installClaudeCode({ home, root: integrationRoot });
2670
2689
  if (claude.ok === false) {
2671
- log(`✗ Claude Code integration failed`);
2690
+ log(claudeIntegrationLine(claude));
2672
2691
  return false;
2673
2692
  }
2674
- log('✓ Claude Code integration');
2693
+ log(claudeIntegrationLine(claude));
2675
2694
 
2676
2695
  // The official ~/.dsh/profiles/web tree is read-only: activation never
2677
2696
  // repairs or mutates the official bridge. The isolated 3210 Crew backend
@@ -283,8 +283,10 @@ export function buildMcpWorkflowRuntime(deps) {
283
283
  const config = getConfig();
284
284
  const isolation = config.execution?.isolation ?? 'worktree';
285
285
  // Explicit shared mode uses the requested workspace. Readonly profiles,
286
- // including the default Reviewer, use a disposable worktree below so an
287
- // accidental edit can be detected and never pollutes the primary tree.
286
+ // including the default Reviewer, take the disposable worktree below, so an
287
+ // accidental edit cannot reach the primary tree at all. A reviewer that was
288
+ // explicitly given a shared workspace still gets fingerprinted around its
289
+ // attempt, so an edit there is detected even though it is not prevented.
288
290
  if (job.requested_isolation === 'shared' || isolation === 'shared') {
289
291
  return { ok: true, execution_cwd: job.requested_cwd, isolation: 'shared', base_revision: null, primary_workspace_dirty: false, handle: null };
290
292
  }
@@ -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.0.3';
39
+ export const RUNTIME_VERSION = '2.0.5';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([
@@ -289,7 +289,10 @@ export function createWorkflowRuntime(adapters, {
289
289
 
290
290
  async function releaseWorkspace(job) {
291
291
  if (job.retain_workspace) {
292
- job.workspace_retained = true;
292
+ // Retention means an isolated workspace was deliberately kept for
293
+ // recovery. A shared workspace is the caller's own and was never ours to
294
+ // keep, so an evidence failure there retains nothing.
295
+ if (job.workspaceHandle) job.workspace_retained = true;
293
296
  return;
294
297
  }
295
298
  if (!job.workspaceHandle) return;
@@ -342,7 +345,7 @@ export function createWorkflowRuntime(adapters, {
342
345
 
343
346
  if (isReviewJob) {
344
347
  transition(job, JOB_PHASES.REVIEWING, 'explicit reviewer');
345
- const before = alloc?.ok && alloc.isolation === 'worktree' ? await safeCapture(adapters, job.execution_cwd, job.base_revision) : null;
348
+ const before = alloc?.ok ? await safeCapture(adapters, job.execution_cwd, job.base_revision) : null;
346
349
  // Explicit reviewers inspect the caller's stated task/workspace
347
350
  // directly. The bounded worker-candidate capsule belongs only to the
348
351
  // automatic post-worker review path; constructing it with no worker
@@ -553,8 +556,13 @@ export function createWorkflowRuntime(adapters, {
553
556
  }
554
557
 
555
558
  async function runReviewerAttempt(job, task, config, beforeCandidate, cwd = job.execution_cwd, baseRevision = job.base_revision) {
559
+ // Reviewing is the one role whose entire contract is "changed nothing", so
560
+ // its evidence is not exempted by isolation. In a worktree the pre-review
561
+ // candidate comes from the allocator or the worker and its absence is a real
562
+ // failure; a shared workspace has no such candidate, so fingerprint the tree
563
+ // the reviewer is about to touch — otherwise the reviewer is the one actor
564
+ // whose edits nothing observes.
556
565
  const requireFingerprint = (candidate) => {
557
- if (job.isolation !== 'worktree') return;
558
566
  if (candidate?.ok === true && typeof candidate.fingerprint === 'string' && candidate.fingerprint.trim()) return;
559
567
  job.candidate_capture_failed = true;
560
568
  job.retain_workspace = true;
@@ -562,7 +570,9 @@ export function createWorkflowRuntime(adapters, {
562
570
  code: 'REVIEW_EVIDENCE_UNAVAILABLE',
563
571
  });
564
572
  };
565
- requireFingerprint(beforeCandidate);
573
+ const before = beforeCandidate
574
+ ?? (job.isolation === 'worktree' ? null : await safeCapture(adapters, cwd, baseRevision));
575
+ requireFingerprint(before);
566
576
  const policy = resolveModelPolicy(config, 'reviewer', { attempt: 0 });
567
577
  const attemptId = attemptIdFor(adapters, job.id, 'review');
568
578
  job.current_attempt_id = attemptId;
@@ -594,9 +604,9 @@ export function createWorkflowRuntime(adapters, {
594
604
  fallback_reason: attemptView.selection_trace?.fallback_reason ?? null,
595
605
  }, 0);
596
606
  if (ar.status === 'failed' && attemptView.error_code) job.error_code = attemptView.error_code;
597
- const afterCandidate = job.isolation === 'worktree' ? await safeCapture(adapters, cwd, baseRevision) : null;
607
+ const afterCandidate = await safeCapture(adapters, cwd, baseRevision);
598
608
  requireFingerprint(afterCandidate);
599
- const review = normalizeReview({ attemptResult: ar, beforeCandidate, afterCandidate });
609
+ const review = normalizeReview({ attemptResult: ar, beforeCandidate: before, afterCandidate });
600
610
  recordCanonical(job, 'review.completed', {
601
611
  attempt_id: attemptView.id,
602
612
  status: review.status,