@ran-sh/dsh-crew 1.10.7 → 1.10.8

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.7",
3
+ "version": "1.10.8",
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.7",
3
+ "version": "1.10.8",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -0,0 +1,106 @@
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));
@@ -222,6 +222,15 @@ export function buildMcpWorkflowRuntime(deps) {
222
222
  source,
223
223
  preset,
224
224
  delivery,
225
+ // Isolation belongs to this client: it already allocated the workspace
226
+ // and `spec.cwd` is the result. Saying so keeps the hub executing in that
227
+ // cwd instead of resolving an isolation of its own and allocating a
228
+ // second workspace — which put the work somewhere the client was not
229
+ // looking and captured no candidate for.
230
+ requested_isolation: 'shared',
231
+ // The same authorization the client was given, so the hub judges the
232
+ // workspace evidence against the same task contract.
233
+ allow_no_changes: spec.allow_no_changes === true,
225
234
  });
226
235
  spec.onAttemptStarted?.(spawned.id);
227
236
 
@@ -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.7';
39
+ export const RUNTIME_VERSION = '1.10.8';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([
@@ -382,6 +382,10 @@ export function createWorkflowRuntime(adapters, {
382
382
  cwd: job.execution_cwd,
383
383
  effort: job.effort,
384
384
  timeout_seconds: job.timeout_seconds,
385
+ // Passed through so the executor judges the workspace evidence against
386
+ // the same task contract this workflow was given, instead of against a
387
+ // default that would refuse an authorization the caller granted.
388
+ allow_no_changes: job.allow_no_changes === true,
385
389
  policy,
386
390
  source: job.source,
387
391
  model_class_hint: job.model_class_hint,
package/src/workflow.mjs CHANGED
@@ -81,12 +81,18 @@ const NO_CHANGE_ASSERTION_RE = new RegExp(
81
81
  function isNoChangeAssertion(line) {
82
82
  const normalized = line
83
83
  .replace(/^(?:[-*+]\s+)+/, '')
84
- .replace(/[`"'“”‘’]/g, '')
84
+ .replace(/[`"'“”‘’*_]/g, '')
85
85
  .replace(/[.!。!]+$/, '')
86
86
  .trim()
87
87
  .toLowerCase();
88
88
  if (normalized === '') return false;
89
- return NO_CHANGE_ASSERTION_RE.test(normalized);
89
+ // The assertion is usually a whole line, but a report may also end with it —
90
+ // "**Final state: no files changed.**" is the same declaration stated as a
91
+ // summary. Testing each sentence and clause is what finds it without matching
92
+ // a phrase that merely appears inside an unrelated one.
93
+ return normalized
94
+ .split(/\s*(?:[.:;,—–()\[\]]|->)\s*/)
95
+ .some((fragment) => fragment !== '' && NO_CHANGE_ASSERTION_RE.test(fragment.trim()));
90
96
  }
91
97
 
92
98
  // Whether the report claims the workspace holds changes. A report that asserts