@ran-sh/dsh-crew 1.10.8 → 2.0.0

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": "1.10.8",
3
+ "version": "2.0.0",
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": "1.10.8",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
package/src/hub/index.mjs CHANGED
@@ -22,6 +22,7 @@ import { readHarnessModelCatalog } from '../model-catalog.mjs';
22
22
  import { appendDeliveryInstructions, prependJobIdentity, parseDeliveryReport, formatDeliveryMetadata } from '../delivery.mjs';
23
23
  import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from '../workspace-audit.mjs';
24
24
  import { applyWorkspaceEvidence, buildOutcome, JOB_PHASES } from '../workflow.mjs';
25
+ import { normalizeReviewVerdict } from '../workflow-runtime.mjs';
25
26
  import { boundedMachineCodeFromError } from '../structured-error-code.mjs';
26
27
  import { createCanonicalJobEvent, projectWorkflowView } from '../job-contracts.mjs';
27
28
  import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
@@ -1096,12 +1097,12 @@ export class WorkerRegistry { constructor(ctx) {
1096
1097
  job.delivery_missing = parsed.missing;
1097
1098
  job.delivery_metadata = formatDeliveryMetadata(parsed);
1098
1099
  if (job.role === 'reviewer') {
1099
- const verdictText = String(parsed.sections?.Verdict ?? '').trim().toLowerCase();
1100
- const verdict = /^(approve|approved|pass)\b/.test(verdictText)
1101
- ? 'approve'
1102
- : /request.*chang|chang.*request|reject|needs changes/i.test(verdictText)
1103
- ? 'request_changes'
1104
- : 'inconclusive';
1100
+ // The rule for reading a verdict lives in one place. This block used to
1101
+ // carry its own copy, and the two copies drifted: the shared one learned
1102
+ // to see through `**Approved**`, this one did not, so the Hub recorded
1103
+ // `inconclusive` for a reviewer whose report the workflow accepted —
1104
+ // which also kept the readiness matrix from seeing a real review.
1105
+ const verdict = normalizeReviewVerdict(parsed.sections?.Verdict ?? job.result ?? '');
1105
1106
  const lines = (value) => String(value ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 40);
1106
1107
  job.review = {
1107
1108
  verdict, status: job.status, delivery_complete: parsed.complete === true,
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { pathToFileURL } from 'node:url';
6
+ import { renameTree } from './tree-move.mjs';
6
7
 
7
8
  function plan(home, root) {
8
9
  const bridge = join(root, 'official-web-bridge');
@@ -61,13 +62,17 @@ export function installOfficialFrontendAssets({ home = homedir(), root } = {}) {
61
62
  mkdirSync(dirname(file), { recursive: true });
62
63
  writeFileSync(file, bytes);
63
64
  }
64
- try { renameSync(stage, p.snapshotRoot); stage = null; }
65
+ // Windows can refuse this rename for a moment when something else holds a
66
+ // handle under the destination, and the refusal is not about the rename:
67
+ // the same call succeeds shortly after. Producing the snapshot first in a
68
+ // concurrent installer is the other outcome this tolerates.
69
+ try { renameTree(stage, p.snapshotRoot); stage = null; }
65
70
  catch (error) { if (!matches(p)) throw error; }
66
71
  }
67
72
  if (!matches(p)) throw new Error('frontend snapshot conflicts with its revision');
68
73
  temp = `${p.overlayFile}.${randomUUID()}.tmp`;
69
74
  writeFileSync(temp, p.overlay, { flag: 'wx', mode: 0o600 });
70
- renameSync(temp, p.overlayFile); temp = null;
75
+ renameTree(temp, p.overlayFile); temp = null;
71
76
  return { ok: true, changed: true, revision: p.revision, snapshotRoot: p.snapshotRoot, overlayFile: p.overlayFile };
72
77
  } catch (error) {
73
78
  return { ok: false, code: 'OFFICIAL_FRONTEND_ASSETS_FAILED', error: error.message };
@@ -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 = '1.10.8';
39
+ export const RUNTIME_VERSION = '2.0.0';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([
@@ -38,8 +38,23 @@ function lines(value) {
38
38
  return value.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
39
39
  }
40
40
 
41
+ // A reviewer writes its verdict as one line, and it decorates that line the way
42
+ // prose is decorated: bolded, bulleted, quoted, or introduced by a label. The
43
+ // contract asks for "approved / needs changes / rejected", and every one of
44
+ // those decorations used to fall through to `inconclusive` — which blocks
45
+ // acceptance, so turning on the automatic reviewer could reject correct work
46
+ // because the reviewer wrote **Approved** instead of Approved.
47
+ function stripVerdictDecoration(value) {
48
+ return String(value ?? '')
49
+ .replace(/^[\s>*_`#-]+/, '')
50
+ .replace(/^\**\s*(?:final\s+)?verdict\s*[:\-—]\s*/i, '')
51
+ .replace(/^[\s>*_`#-]+/, '')
52
+ .replace(/[\s*_`]+$/, '')
53
+ .trim();
54
+ }
55
+
41
56
  export function normalizeReviewVerdict(value) {
42
- const s = String(value ?? '').trim().toLowerCase();
57
+ const s = stripVerdictDecoration(value).toLowerCase();
43
58
  if (/^(approve|approved|pass)\b/.test(s) || s === 'pass' || s === 'approve') return 'approve';
44
59
  if (/request.*chang|chang.*request|reject|needs changes/i.test(s)) return 'request_changes';
45
60
  return 'inconclusive';
@@ -1,106 +0,0 @@
1
- // Drives the installed release's own MCP server over stdio, so the client-side
2
- // dispatch path is exercised by the shipped code rather than by a long-running
3
- // session whose MCP process predates the fix.
4
- import { spawn } from 'node:child_process';
5
-
6
- const serverPath = process.argv[2];
7
- const scenario = process.argv[3] ?? 'zero-change';
8
- const child = spawn(process.execPath, [serverPath], { stdio: ['pipe', 'pipe', 'pipe'], env: process.env });
9
-
10
- let buffer = '';
11
- let nextId = 1;
12
- const pending = new Map();
13
-
14
- child.stdout.on('data', (chunk) => {
15
- buffer += chunk.toString();
16
- let index;
17
- while ((index = buffer.indexOf('\n')) >= 0) {
18
- const line = buffer.slice(0, index).trim();
19
- buffer = buffer.slice(index + 1);
20
- if (line === '') continue;
21
- let message;
22
- try { message = JSON.parse(line); } catch { continue; }
23
- if (message.id !== undefined && pending.has(message.id)) {
24
- const { resolve } = pending.get(message.id);
25
- pending.delete(message.id);
26
- resolve(message);
27
- }
28
- }
29
- });
30
- const stderr = [];
31
- child.stderr.on('data', (chunk) => stderr.push(chunk.toString()));
32
-
33
- function send(method, params) {
34
- const id = nextId++;
35
- child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
36
- return new Promise((resolve) => pending.set(id, { resolve }));
37
- }
38
- function notify(method, params) {
39
- child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
40
- }
41
-
42
- const scenarios = {
43
- 'zero-change': {
44
- tool: 'dsh_run_worker',
45
- arguments: {
46
- task: 'Create a temporary PowerShell script at work/tmp-hello.ps1 that writes exactly the text 你好 to standard output with no trailing newline, run it, and verify its stdout is exactly 你好 byte-for-byte under both CP936 and UTF-8. Then delete the script and confirm the path no longer exists. Do not leave any file behind: the final workspace must be unchanged.',
47
- cwd: 'D:/Users/48376/Desktop/crew-verify-1',
48
- role: 'worker',
49
- workspace: { worktree: 'none' },
50
- constraints: { allow_no_changes: true, timeout_seconds: 600 },
51
- },
52
- },
53
- 'tool-list': { tool: null },
54
- };
55
-
56
- const scenarioSpec = scenarios[scenario];
57
- const init = await send('initialize', {
58
- protocolVersion: '2024-11-05',
59
- capabilities: {},
60
- clientInfo: { name: 'crew-verify', version: '1' },
61
- });
62
- if (init.error) {
63
- console.log('INIT_FAILED', JSON.stringify(init.error));
64
- process.exit(1);
65
- }
66
- notify('notifications/initialized', {});
67
- console.log('SERVER', JSON.stringify(init.result?.serverInfo ?? null));
68
-
69
- const listed = await send('tools/list', {});
70
- const tools = (listed.result?.tools ?? []).map((t) => t.name);
71
- console.log('TOOLS', JSON.stringify(tools));
72
-
73
- if (scenarioSpec.tool) {
74
- const started = Date.now();
75
- const call = await send('tools/call', { name: scenarioSpec.tool, arguments: scenarioSpec.arguments });
76
- console.log('ELAPSED_MS', Date.now() - started);
77
- if (call.error) {
78
- console.log('CALL_ERROR', JSON.stringify(call.error));
79
- } else {
80
- const text = (call.result?.content ?? []).map((c) => c.text ?? '').join('');
81
- let parsed = null;
82
- try { parsed = JSON.parse(text); } catch { /* keep raw */ }
83
- if (parsed) {
84
- const summary = parsed.evidence?.summary ?? {};
85
- console.log('RESULT', JSON.stringify({
86
- id: parsed.id,
87
- phase: parsed.phase,
88
- status: parsed.status,
89
- isolation: parsed.isolation,
90
- execution_cwd: parsed.execution_cwd,
91
- task_status: summary.task_status,
92
- tests_status: summary.tests_status,
93
- delivery_complete: summary.delivery_complete,
94
- workspace_evidence_ok: parsed.workspace_evidence_ok ?? parsed.evidence?.workspace_evidence_ok,
95
- no_change_verified: parsed.outcome?.no_change_verified,
96
- failure: parsed.failure?.reason_code ?? null,
97
- error: parsed.error ?? null,
98
- }, null, 1));
99
- } else {
100
- console.log('RAW', text.slice(0, 1500));
101
- }
102
- }
103
- }
104
-
105
- child.kill();
106
- if (stderr.length) console.log('STDERR_SAMPLE', stderr.join('').slice(0, 400));