@ran-sh/dsh-crew 2.0.3 → 2.0.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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.4",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -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.4';
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,