@ran-sh/dsh-crew 1.10.6 → 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.
package/package.json
CHANGED
|
@@ -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));
|
package/src/mcp-runtime.mjs
CHANGED
|
@@ -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
|
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -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.
|
|
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([
|
package/src/workflow-runtime.mjs
CHANGED
|
@@ -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,
|
|
@@ -441,11 +445,23 @@ export function createWorkflowRuntime(adapters, {
|
|
|
441
445
|
}
|
|
442
446
|
}
|
|
443
447
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
448
|
+
// A hub-dispatched attempt was already judged by the Hub, which sees the
|
|
449
|
+
// primary workspace. This client only has evidence of its own when it
|
|
450
|
+
// captured a candidate, which it can only do in a worktree it owns; for a
|
|
451
|
+
// shared workspace it has none. Re-running the gate here with no evidence
|
|
452
|
+
// judged the same report again with nothing to judge it against, and
|
|
453
|
+
// downgraded an authorized zero-change task the Hub had already verified
|
|
454
|
+
// to `partial`. Where the client has no evidence, the Hub's verdict is
|
|
455
|
+
// the one that was reached on evidence, so it stands.
|
|
456
|
+
const clientJudged = workspaceEvidenceAvailable;
|
|
457
|
+
const hubJudged = typeof ar.outcome?.workspace_evidence_ok === 'boolean';
|
|
458
|
+
if (clientJudged || !hubJudged) {
|
|
459
|
+
outcome = applyWorkspaceEvidence(outcome, {
|
|
460
|
+
evidenceAvailable: clientJudged,
|
|
461
|
+
hasChanges: workspaceHasChanges,
|
|
462
|
+
allowNoChanges: job.allow_no_changes,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
449
465
|
job.outcome = outcome;
|
|
450
466
|
|
|
451
467
|
const reviewRequested = !isReviewJob && shouldAutoReview(config);
|
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(/[`"'
|
|
84
|
+
.replace(/[`"'“”‘’*_]/g, '')
|
|
85
85
|
.replace(/[.!。!]+$/, '')
|
|
86
86
|
.trim()
|
|
87
87
|
.toLowerCase();
|
|
88
88
|
if (normalized === '') return false;
|
|
89
|
-
|
|
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
|