@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.
package/package.json
CHANGED
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
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 = '
|
|
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([
|
package/src/workflow-runtime.mjs
CHANGED
|
@@ -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 =
|
|
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));
|