@ran-sh/dsh-crew 0.5.2 → 0.5.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.
- package/agents/ds-worker.md +5 -4
- package/codex/agents/ds-worker.toml +1 -1
- package/docs/job-contracts.md +12 -4
- package/docs/readiness-matrix.md +6 -2
- package/package.json +1 -1
- package/scripts/setup.mjs +62 -23
- package/src/config-readiness.mjs +50 -10
- package/src/hub/index.mjs +34 -9
- package/src/install/windows-startup.mjs +23 -6
- package/src/orchestrator.mjs +53 -0
- package/src/readiness-matrix.mjs +4 -3
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +38 -34
- package/src/workflow-runtime.mjs +27 -27
- package/src/workflow.mjs +63 -5
- package/windows/start-dsh-crew.cmd +51 -49
- package/windows/start-dsh-crew.ps1 +326 -0
- package/windows/start-dsh-crew.vbs +1 -1
- package/zcode/AGENTS.md +5 -0
- package/zcode/agents/ds-reviewer.md +21 -4
- package/zcode/agents/ds-worker.md +23 -4
package/agents/ds-worker.md
CHANGED
|
@@ -7,10 +7,11 @@ model: haiku
|
|
|
7
7
|
You are a thin dispatcher for the DSH worker role. You NEVER do the task yourself.
|
|
8
8
|
|
|
9
9
|
1. Take the task you were given and pass it VERBATIM (plus any file paths / context you were given) to the `dsh_run_worker` tool with:
|
|
10
|
-
- `role`: `"worker"`
|
|
11
|
-
- `effort`: omit it entirely (the session/global default applies) unless the task explicitly names an effort level
|
|
12
|
-
- `cwd`: the current project directory
|
|
13
|
-
|
|
10
|
+
- `role`: `"worker"`
|
|
11
|
+
- `effort`: omit it entirely (the session/global default applies) unless the task explicitly names an effort level
|
|
12
|
+
- `cwd`: the current project directory
|
|
13
|
+
- For an explicitly read-only search or analysis task that must make zero file changes, pass `constraints: { allow_no_changes: true }`. Otherwise omit this constraint.
|
|
14
|
+
2. Wait for the tool to return.
|
|
14
15
|
3. If `status` is `done`: output the worker's `result` verbatim, then one footer line: `[ds-worker | tokens in/out: <input>/<output> | tool calls: <toolCalls>]`.
|
|
15
16
|
4. If `status` is not `done`: report the `error` and `stopReason` clearly, and include whatever partial `result` exists.
|
|
16
17
|
|
|
@@ -8,7 +8,7 @@ model = "gpt-5.4-mini"
|
|
|
8
8
|
developer_instructions = """
|
|
9
9
|
You are a thin dispatcher. You NEVER do the task yourself.
|
|
10
10
|
|
|
11
|
-
1. Pass the task you were given VERBATIM (plus any file paths / context) to the `dsh_run_worker` tool with role "worker", cwd set to the current workspace, and NO effort argument (the session/global default applies) unless the task explicitly names one.
|
|
11
|
+
1. Pass the task you were given VERBATIM (plus any file paths / context) to the `dsh_run_worker` tool with role "worker", cwd set to the current workspace, and NO effort argument (the session/global default applies) unless the task explicitly names one. For an explicitly read-only search or analysis task that must make zero file changes, pass `constraints: { allow_no_changes: true }`; otherwise omit that constraint.
|
|
12
12
|
2. Wait for the tool to return.
|
|
13
13
|
3. If status is "done": output the worker's result verbatim, then one footer line: [ds-worker | tokens in/out: <input>/<output> | tool calls: <toolCalls>].
|
|
14
14
|
4. Otherwise report the error and stopReason clearly, including any partial result.
|
package/docs/job-contracts.md
CHANGED
|
@@ -80,10 +80,18 @@ long jobs without replaying the entire event history.
|
|
|
80
80
|
The isolated Hub also exposes `/extension`, `/profiles`, `/workspaces`,
|
|
81
81
|
`/jobs/:id/contract`, and `/jobs/:id/events`. All remain loopback-only.
|
|
82
82
|
|
|
83
|
-
Per-job precedence is request `constraints` > Profile > session defaults.
|
|
84
|
-
`workspace.branch` pins the isolated base revision; `workspace.worktree` accepts
|
|
85
|
-
`auto`, `existing`, or `none`. Workspace preflight reports `READY`, `CONFLICT`,
|
|
86
|
-
`READ_ONLY`, or `UNAVAILABLE` before dispatch.
|
|
83
|
+
Per-job precedence is request `constraints` > Profile > session defaults.
|
|
84
|
+
`workspace.branch` pins the isolated base revision; `workspace.worktree` accepts
|
|
85
|
+
`auto`, `existing`, or `none`. Workspace preflight reports `READY`, `CONFLICT`,
|
|
86
|
+
`READ_ONLY`, or `UNAVAILABLE` before dispatch.
|
|
87
|
+
|
|
88
|
+
`constraints.allow_no_changes: true` is reserved for explicitly read-only
|
|
89
|
+
search, inspection, or analysis work. It does not blindly convert a partial
|
|
90
|
+
Delivery Report into success: Crew requires an isolated candidate with zero
|
|
91
|
+
changed files, a complete Delivery Report, at least one passing evidence check,
|
|
92
|
+
and no failed check. If the Worker reports no changes but the workspace contains
|
|
93
|
+
edits—or reports changes while the workspace is empty—the workflow fails closed
|
|
94
|
+
with workspace-evidence mismatch.
|
|
87
95
|
|
|
88
96
|
The CLI is a JSON projection of the same local HTTP surface:
|
|
89
97
|
|
package/docs/readiness-matrix.md
CHANGED
|
@@ -24,7 +24,10 @@ The matrix separates three kinds of evidence:
|
|
|
24
24
|
2. `ci` — platform validation such as Linux deterministic, Windows regressions, and future macOS smoke.
|
|
25
25
|
3. `real-execution` — provider/model and workflow behavior that requires a genuine DSH execution.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Live checks are populated automatically. The config report also consumes the
|
|
28
|
+
compatible Hub's bounded job registry, so a completed Worker or Reviewer can
|
|
29
|
+
promote the corresponding generic real-execution row. Other CI and execution
|
|
30
|
+
rows remain `NOT_RUN` until a trusted higher layer supplies explicit evidence.
|
|
28
31
|
|
|
29
32
|
## Target rows
|
|
30
33
|
|
|
@@ -32,7 +35,8 @@ Only live checks are populated automatically by the current runtime. CI and real
|
|
|
32
35
|
- `windows_regressions`
|
|
33
36
|
- `macos_smoke`
|
|
34
37
|
- `hub_compatibility`
|
|
35
|
-
- `provider_catalog`
|
|
38
|
+
- `provider_catalog`
|
|
39
|
+
- `model_execution`
|
|
36
40
|
- `deepseek_flash`
|
|
37
41
|
- `deepseek_pro`
|
|
38
42
|
- `opencode_go_mimo_qwen`
|
package/package.json
CHANGED
package/scripts/setup.mjs
CHANGED
|
@@ -23,20 +23,57 @@ import {
|
|
|
23
23
|
|
|
24
24
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
25
|
|
|
26
|
-
function commandExists(name) {
|
|
27
|
-
const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
|
|
26
|
+
function commandExists(name) {
|
|
27
|
+
const r = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', [name], {
|
|
28
28
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
29
29
|
});
|
|
30
|
-
return r.status === 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
return r.status === 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PACKAGE_MANAGER_COMMAND = /^(?:npm|npx|pnpm|yarn|yarnpkg|bun|bunx)$/i;
|
|
34
|
+
|
|
35
|
+
export function spawnCommand(command, platform = process.platform) {
|
|
36
|
+
if (platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command)) {
|
|
37
|
+
return `${command}.cmd`;
|
|
38
|
+
}
|
|
39
|
+
return command;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function spawnNeedsShell(command, platform = process.platform) {
|
|
43
|
+
return platform === 'win32' && PACKAGE_MANAGER_COMMAND.test(command);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function spawnInvocation(command, args = [], platform = process.platform, environment = process.env) {
|
|
47
|
+
const executable = spawnCommand(command, platform);
|
|
48
|
+
if (!spawnNeedsShell(command, platform)) return { command: executable, args };
|
|
49
|
+
const unsafe = args.find((arg) => !/^[A-Za-z0-9@._:+/=\-]+$/.test(String(arg)));
|
|
50
|
+
if (unsafe !== undefined) throw new Error(`unsafe package-manager argument: ${unsafe}`);
|
|
51
|
+
return {
|
|
52
|
+
command: environment.ComSpec || 'cmd.exe',
|
|
53
|
+
args: ['/d', '/s', '/c', [executable, ...args].join(' ')],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function run(cmd, args, opts = {}) {
|
|
58
|
+
let invocation;
|
|
59
|
+
try {
|
|
60
|
+
invocation = spawnInvocation(cmd, args);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { ok: false, status: -1, stdout: '', stderr: String(error?.message ?? error), error };
|
|
63
|
+
}
|
|
64
|
+
const r = spawnSync(invocation.command, invocation.args, {
|
|
65
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: opts.shell === true, cwd: opts.cwd ?? ROOT,
|
|
66
|
+
env: opts.env ?? process.env,
|
|
67
|
+
});
|
|
68
|
+
const processError = r.error ? `${r.error.name}: ${r.error.message}` : '';
|
|
69
|
+
return {
|
|
70
|
+
ok: r.status === 0,
|
|
71
|
+
status: r.status ?? -1,
|
|
72
|
+
stdout: r.stdout ?? '',
|
|
73
|
+
stderr: r.stderr || processError,
|
|
74
|
+
error: r.error ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
40
77
|
|
|
41
78
|
/**
|
|
42
79
|
* Detect the DSH CLI. `allowDownload` is intentionally opt-in: installation
|
|
@@ -106,18 +143,18 @@ export async function setupInstall({
|
|
|
106
143
|
installer = realInstaller,
|
|
107
144
|
depRunner = null,
|
|
108
145
|
} = {}) {
|
|
109
|
-
log('DSH Crew installer');
|
|
110
|
-
if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
log('DSH Crew installer');
|
|
147
|
+
if (!checkRoot(root)) return { ok: false, error: `not a dsh-crew checkout: ${root}` };
|
|
148
|
+
const pnpmOk = commandExists('pnpm');
|
|
149
|
+
|
|
150
|
+
if (depRunner) {
|
|
113
151
|
const r = depRunner(root);
|
|
114
152
|
if (!r.ok) { mark(log, false, r.text); return { ok: false, error: r.text }; }
|
|
115
153
|
mark(log, true, r.text);
|
|
116
|
-
} else if (!dryRun) {
|
|
117
|
-
const
|
|
118
|
-
const cmd = pnpmOk ? 'pnpm' : 'npx';
|
|
154
|
+
} else if (!dryRun) {
|
|
155
|
+
const cmd = pnpmOk ? 'pnpm' : 'npx';
|
|
119
156
|
const baseArgs = pnpmOk ? [] : ['-y', 'pnpm'];
|
|
120
|
-
const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile']);
|
|
157
|
+
const d = run(cmd, [...baseArgs, 'install', '--frozen-lockfile'], { cwd: root });
|
|
121
158
|
if (!d.ok) {
|
|
122
159
|
if (depsPresent(root)) mark(log, true, `dependencies (already present; install skipped — ${(d.stderr || d.stdout || '').trim().split('\n')[0]})`);
|
|
123
160
|
else {
|
|
@@ -126,9 +163,11 @@ export async function setupInstall({
|
|
|
126
163
|
}
|
|
127
164
|
} else mark(log, true, 'dependencies');
|
|
128
165
|
} else mark(log, true, 'dependencies (dry-run)');
|
|
129
|
-
|
|
130
|
-
if (!dryRun) {
|
|
131
|
-
const b =
|
|
166
|
+
|
|
167
|
+
if (!dryRun) {
|
|
168
|
+
const b = pnpmOk
|
|
169
|
+
? run('pnpm', ['run', 'build:client'], { cwd: root })
|
|
170
|
+
: run('npx', ['-y', 'pnpm', 'run', 'build:client'], { cwd: root });
|
|
132
171
|
if (!b.ok) {
|
|
133
172
|
log(`✗ client build failed:\n${(b.stderr || b.stdout || '').slice(0, 600)}`);
|
|
134
173
|
return { ok: false, error: 'client build failed' };
|
package/src/config-readiness.mjs
CHANGED
|
@@ -1,31 +1,71 @@
|
|
|
1
1
|
import { buildReadinessMatrix, READINESS_REASON_CODES } from './readiness-matrix.mjs';
|
|
2
2
|
|
|
3
|
-
function warningCodes(catalogBody) {
|
|
3
|
+
function warningCodes(catalogBody) {
|
|
4
4
|
const hints = Array.isArray(catalogBody?.health?.hints) ? catalogBody.health.hints : [];
|
|
5
5
|
return [...new Set(hints
|
|
6
6
|
.filter((hint) => hint?.level === 'warning' && typeof hint?.code === 'string' && hint.code.trim())
|
|
7
7
|
.map((hint) => hint.code.trim()))];
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function verifiedHubJob(job) {
|
|
11
|
+
return job?.status === 'done'
|
|
12
|
+
&& job?.task_status === 'success'
|
|
13
|
+
&& job?.delivery_complete === true
|
|
14
|
+
&& job?.workspace_evidence_ok === true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildHubExecutionRows(hubJobs = []) {
|
|
18
|
+
const jobs = Array.isArray(hubJobs) ? hubJobs : [];
|
|
19
|
+
const workerPassed = jobs.some((job) => job?.role === 'worker' && verifiedHubJob(job));
|
|
20
|
+
const reviewerPassed = jobs.some((job) => job?.role === 'reviewer'
|
|
21
|
+
&& verifiedHubJob(job)
|
|
22
|
+
&& job?.review_verdict === 'approve');
|
|
23
|
+
return [
|
|
24
|
+
workerPassed
|
|
25
|
+
? { id: 'model_execution', status: 'PASS', reason_code: 'REAL_EXECUTION_PASSED', evidence_source: 'hub-jobs' }
|
|
26
|
+
: { id: 'model_execution', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
|
|
27
|
+
reviewerPassed
|
|
28
|
+
? { id: 'reviewer_pipeline', status: 'PASS', reason_code: 'REAL_REVIEW_PASSED', evidence_source: 'hub-jobs' }
|
|
29
|
+
: { id: 'reviewer_pipeline', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
|
|
30
|
+
];
|
|
31
|
+
}
|
|
9
32
|
|
|
10
33
|
/**
|
|
11
34
|
* Enrich the conservative runtime matrix with evidence the config report has
|
|
12
35
|
* already collected. This function performs no I/O and never reads provider
|
|
13
36
|
* configuration, credentials, quotas, pricing, or hidden catalog expectations.
|
|
14
37
|
*/
|
|
15
|
-
export function buildConfigReadinessMatrix({
|
|
16
|
-
platform = process.platform,
|
|
17
|
-
hubCompatibility = null,
|
|
18
|
-
workerProviderMode = null,
|
|
19
|
-
providerCatalogChecked = false,
|
|
20
|
-
providerCatalogBody = null,
|
|
21
|
-
|
|
38
|
+
export function buildConfigReadinessMatrix({
|
|
39
|
+
platform = process.platform,
|
|
40
|
+
hubCompatibility = null,
|
|
41
|
+
workerProviderMode = null,
|
|
42
|
+
providerCatalogChecked = false,
|
|
43
|
+
providerCatalogBody = null,
|
|
44
|
+
hubJobsChecked = false,
|
|
45
|
+
hubJobsBody = null,
|
|
46
|
+
} = {}) {
|
|
22
47
|
const warnings = warningCodes(providerCatalogBody);
|
|
23
48
|
const catalogResponseOk = !!providerCatalogBody
|
|
24
49
|
&& typeof providerCatalogBody === 'object'
|
|
25
50
|
&& providerCatalogBody.ok !== false;
|
|
26
51
|
const catalogOk = providerCatalogChecked && catalogResponseOk && warnings.length === 0;
|
|
27
52
|
|
|
28
|
-
const evidence = {};
|
|
53
|
+
const evidence = {};
|
|
54
|
+
const hubJobs = hubCompatibility?.compatible === true
|
|
55
|
+
&& hubJobsChecked
|
|
56
|
+
&& hubJobsBody?.ok !== false
|
|
57
|
+
&& Array.isArray(hubJobsBody?.jobs)
|
|
58
|
+
? hubJobsBody.jobs
|
|
59
|
+
: [];
|
|
60
|
+
for (const row of buildHubExecutionRows(hubJobs)) {
|
|
61
|
+
if (row.status === 'PASS') {
|
|
62
|
+
evidence[row.id] = {
|
|
63
|
+
status: row.status,
|
|
64
|
+
reason_code: row.reason_code,
|
|
65
|
+
evidence_source: row.evidence_source,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
29
69
|
if (
|
|
30
70
|
workerProviderMode !== 'deepseek-official'
|
|
31
71
|
&& hubCompatibility?.compatible === true
|
package/src/hub/index.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { buildDirectSelectionTrace, resolveWorkerModel, resolveModel } from '../
|
|
|
20
20
|
import { readHarnessModelCatalog } from '../model-catalog.mjs';
|
|
21
21
|
import { appendDeliveryInstructions, parseDeliveryReport, formatDeliveryMetadata } from '../delivery.mjs';
|
|
22
22
|
import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from '../workspace-audit.mjs';
|
|
23
|
-
import { buildOutcome, JOB_PHASES } from '../workflow.mjs';
|
|
23
|
+
import { applyWorkspaceEvidence, buildOutcome, JOB_PHASES } from '../workflow.mjs';
|
|
24
24
|
import { boundedMachineCodeFromError } from '../structured-error-code.mjs';
|
|
25
25
|
import { createCanonicalJobEvent, projectWorkflowView } from '../job-contracts.mjs';
|
|
26
26
|
import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
|
|
@@ -29,6 +29,7 @@ import { addContextReferences, buildWorkspaceTask, isSafeBranchName, loadWorkspa
|
|
|
29
29
|
import { buildExtensionContract } from '../extension-contract.mjs';
|
|
30
30
|
import { cleanupIsolatedWorkspace, createIsolatedWorkspace } from '../workspace-isolation.mjs';
|
|
31
31
|
import { assessWorkspaceReadiness } from '../workspace-readiness.mjs';
|
|
32
|
+
import { buildHubExecutionRows } from '../config-readiness.mjs';
|
|
32
33
|
|
|
33
34
|
// policy.mjs is pure (no @deepseek-ai imports, no ctx access), so importing it
|
|
34
35
|
// here is safe for the profile-realm discipline: it never pulls in package
|
|
@@ -119,7 +120,20 @@ export function hubCanonicalEvents(job = {}) {
|
|
|
119
120
|
}));
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
// ---------- job registry ----------
|
|
123
|
+
// ---------- job registry ----------
|
|
124
|
+
|
|
125
|
+
export function applyHubWorkspaceEvidence({ outcome, workspaceDiff, allowNoChanges = false, isolation = 'shared', role = 'worker' } = {}) {
|
|
126
|
+
const changes = workspaceDiff?.changes ?? {};
|
|
127
|
+
const hasChanges = ['modified', 'deleted', 'renamed', 'untracked']
|
|
128
|
+
.some((key) => Array.isArray(changes[key]) && changes[key].length > 0);
|
|
129
|
+
const evidenceAvailable = workspaceDiff?.kind === 'git' && workspaceDiff.dirtyBaseline !== true;
|
|
130
|
+
return applyWorkspaceEvidence(outcome, {
|
|
131
|
+
evidenceAvailable,
|
|
132
|
+
hasChanges,
|
|
133
|
+
allowNoChanges: allowNoChanges === true && isolation === 'worktree',
|
|
134
|
+
requireNoChangeAuthorization: role === 'worker',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
123
137
|
|
|
124
138
|
// Exported for the unit tests (test/hub-windows.test.mjs); instantiation
|
|
125
139
|
// needs only a duck-typed ctx, so spawn()'s path guard is testable without a
|
|
@@ -145,6 +159,10 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
145
159
|
startedAt: job.startedAt, endedAt: job.endedAt,
|
|
146
160
|
isolation: job.isolation ?? 'shared', workspace_branch: job.workspace_branch ?? null,
|
|
147
161
|
delivery_complete: !!job.delivery_complete,
|
|
162
|
+
allow_no_changes: job.allow_no_changes === true,
|
|
163
|
+
task_status: job.outcome?.task_status ?? null,
|
|
164
|
+
workspace_evidence_ok: job.outcome?.workspace_evidence_ok ?? null,
|
|
165
|
+
review_verdict: job.review?.verdict ?? null,
|
|
148
166
|
workspace_diff_available: !!job.workspaceDiff && job.workspaceDiff.kind === 'git',
|
|
149
167
|
workspace_retained: job.workspace_retained === true,
|
|
150
168
|
cleanup_warning: job.cleanup_warning ?? null,
|
|
@@ -182,7 +200,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
182
200
|
* `role` (worker | reviewer) records who does the work; `tier` remains the
|
|
183
201
|
* legacy model-class slot. Reviewer-role jobs always use the pro slot.
|
|
184
202
|
*/
|
|
185
|
-
async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context }) {
|
|
203
|
+
async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context, allow_no_changes }) {
|
|
186
204
|
// role is only honored when the caller explicitly names it; a legacy
|
|
187
205
|
// tier-only spawn (role === undefined) keeps the exact v0.1 resolution.
|
|
188
206
|
const hasRole = role === 'worker' || role === 'reviewer';
|
|
@@ -291,6 +309,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
291
309
|
effort, reasoning_effort: selection.reasoningEffort,
|
|
292
310
|
task, source, cwd: executionCwd, requested_cwd: cwd,
|
|
293
311
|
isolation: isolatedWorkspace ? 'worktree' : 'shared',
|
|
312
|
+
allow_no_changes: allow_no_changes === true,
|
|
294
313
|
workspace_branch: workspace_branch ?? null, isolatedWorkspace,
|
|
295
314
|
profile_id: profile_id ?? null, workspace_context: workspace_context ?? null,
|
|
296
315
|
prompt: workerPrompt, delivery: delivery === 'review' ? 'review' : 'coding',
|
|
@@ -451,6 +470,13 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
451
470
|
job.workspaceDiff = job.baseline.kind === 'git'
|
|
452
471
|
? await captureWorkspaceDiff({ cwd: executionCwd, baseline: job.baseline }).catch(() => ({ kind: 'no-git', reason: NOT_A_GIT_REPOSITORY, error: 'workspace diff failed' }))
|
|
453
472
|
: job.baseline;
|
|
473
|
+
job.outcome = applyHubWorkspaceEvidence({
|
|
474
|
+
outcome: job.outcome,
|
|
475
|
+
workspaceDiff: job.workspaceDiff,
|
|
476
|
+
allowNoChanges: job.allow_no_changes,
|
|
477
|
+
isolation: job.isolation,
|
|
478
|
+
role: job.role,
|
|
479
|
+
});
|
|
454
480
|
if (job.role === 'reviewer' && job.review && job.workspaceDiff?.kind === 'git') {
|
|
455
481
|
const changes = job.workspaceDiff.changes ?? {};
|
|
456
482
|
const mutated = ['modified', 'deleted', 'renamed', 'untracked'].some((key) => Array.isArray(changes[key]) && changes[key].length > 0);
|
|
@@ -609,6 +635,9 @@ export function resolveHubSpawnPayload(payload, getConfig = () => ({}), dependen
|
|
|
609
635
|
if (raw.constraints?.allow_fallback !== undefined && typeof raw.constraints.allow_fallback !== 'boolean') {
|
|
610
636
|
return { ok: false, code: 'JOB_CONSTRAINTS_INVALID', error: 'allow_fallback must be boolean' };
|
|
611
637
|
}
|
|
638
|
+
if (raw.constraints?.allow_no_changes !== undefined && typeof raw.constraints.allow_no_changes !== 'boolean') {
|
|
639
|
+
return { ok: false, code: 'JOB_CONSTRAINTS_INVALID', error: 'allow_no_changes must be boolean' };
|
|
640
|
+
}
|
|
612
641
|
const profileRegistry = dependencies.profileRegistry ?? loadRoleProfiles();
|
|
613
642
|
const workspaceRegistry = dependencies.workspaceRegistry ?? loadWorkspaceContexts();
|
|
614
643
|
if (!profileRegistry.ok) return { ok: false, code: 'PROFILE_FILE_INVALID', error: 'role profile registry is invalid' };
|
|
@@ -647,6 +676,7 @@ export function resolveHubSpawnPayload(payload, getConfig = () => ({}), dependen
|
|
|
647
676
|
workspace_branch: raw.workspace?.branch ?? withRefs.context?.default_branch ?? null,
|
|
648
677
|
timeout_seconds: raw.constraints?.timeout_seconds ?? profile.timeout_seconds,
|
|
649
678
|
allow_fallback: raw.constraints?.allow_fallback ?? profile.fallback,
|
|
679
|
+
allow_no_changes: raw.constraints?.allow_no_changes === true,
|
|
650
680
|
routing: profile.routing,
|
|
651
681
|
review_strictness: profile.review_strictness,
|
|
652
682
|
profile_id: resolvedProfile.profile_id,
|
|
@@ -852,12 +882,7 @@ export async function apply(ctx) {
|
|
|
852
882
|
? { status: 'UNAVAILABLE', reason_code: 'WORKSPACE_CONTEXT_NOT_FOUND' }
|
|
853
883
|
: await assessWorkspaceReadiness({ cwd: requestedContext?.repo_root ?? null });
|
|
854
884
|
const liveJobs = typeof hub.list === 'function' ? hub.list() : [];
|
|
855
|
-
const modelExecution = liveJobs
|
|
856
|
-
? { id: 'model_execution', status: 'PASS', reason_code: 'REAL_EXECUTION_PASSED' }
|
|
857
|
-
: { id: 'model_execution', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE' };
|
|
858
|
-
const reviewerExecution = liveJobs.some((job) => job?.role === 'reviewer' && job?.status === 'done')
|
|
859
|
-
? { id: 'reviewer_pipeline', status: 'PASS', reason_code: 'REAL_REVIEW_PASSED' }
|
|
860
|
-
: { id: 'reviewer_pipeline', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE' };
|
|
885
|
+
const [modelExecution, reviewerExecution] = buildHubExecutionRows(liveJobs);
|
|
861
886
|
const contract = buildExtensionContract({
|
|
862
887
|
config,
|
|
863
888
|
readinessMatrix: { rows: [
|
|
@@ -11,6 +11,7 @@ import { homedir } from 'node:os';
|
|
|
11
11
|
|
|
12
12
|
export const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
|
|
13
13
|
export const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
|
|
14
|
+
export const WINDOWS_HELPER_FILENAME = 'start-dsh-crew.ps1';
|
|
14
15
|
|
|
15
16
|
function defaultStartupDir({ home, env }) {
|
|
16
17
|
if (home === homedir() && env.APPDATA) {
|
|
@@ -21,9 +22,11 @@ function defaultStartupDir({ home, env }) {
|
|
|
21
22
|
|
|
22
23
|
function paths({ home, startupDir, env }) {
|
|
23
24
|
const launcherFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_LAUNCHER_FILENAME);
|
|
25
|
+
const helperFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_HELPER_FILENAME);
|
|
24
26
|
const resolvedStartupDir = startupDir ?? defaultStartupDir({ home, env });
|
|
25
27
|
return {
|
|
26
28
|
launcherFile,
|
|
29
|
+
helperFile,
|
|
27
30
|
startupFile: join(resolvedStartupDir, WINDOWS_STARTUP_FILENAME),
|
|
28
31
|
};
|
|
29
32
|
}
|
|
@@ -41,12 +44,22 @@ export function windowsStartupStatus({
|
|
|
41
44
|
} = {}) {
|
|
42
45
|
if (platform !== 'win32') return { supported: false, installed: false, ready: false };
|
|
43
46
|
const resolved = paths({ home, startupDir, env });
|
|
44
|
-
const installed = existsSync(resolved.startupFile)
|
|
45
|
-
|
|
47
|
+
const installed = existsSync(resolved.startupFile)
|
|
48
|
+
|| existsSync(resolved.launcherFile)
|
|
49
|
+
|| existsSync(resolved.helperFile);
|
|
50
|
+
let ready = existsSync(resolved.startupFile)
|
|
51
|
+
&& existsSync(resolved.launcherFile)
|
|
52
|
+
&& existsSync(resolved.helperFile);
|
|
46
53
|
if (ready) {
|
|
47
54
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
55
|
+
const startup = readFileSync(resolved.startupFile, 'utf16le').replace(/^\uFEFF/, '');
|
|
56
|
+
const launcher = readFileSync(resolved.launcherFile, 'utf8');
|
|
57
|
+
const helper = readFileSync(resolved.helperFile, 'utf8');
|
|
58
|
+
ready = startup.includes(resolved.launcherFile)
|
|
59
|
+
&& startup.includes('--watch')
|
|
60
|
+
&& launcher.includes(WINDOWS_HELPER_FILENAME)
|
|
61
|
+
&& helper.includes('DSH Crew managed Windows launcher')
|
|
62
|
+
&& helper.includes('DSHCrewServiceSupervisor');
|
|
50
63
|
} catch { ready = false; }
|
|
51
64
|
}
|
|
52
65
|
return { supported: true, installed, ready, ...resolved };
|
|
@@ -62,19 +75,23 @@ export function installWindowsStartup({
|
|
|
62
75
|
if (platform !== 'win32') return { ok: true, supported: false, changed: false };
|
|
63
76
|
if (!root) return { ok: false, supported: true, code: 'STARTUP_SOURCE_REQUIRED' };
|
|
64
77
|
const sourceLauncher = join(root, 'windows', WINDOWS_LAUNCHER_FILENAME);
|
|
78
|
+
const sourceHelper = join(root, 'windows', WINDOWS_HELPER_FILENAME);
|
|
65
79
|
const sourceVbs = join(root, 'windows', 'start-dsh-crew.vbs');
|
|
66
|
-
if (!existsSync(sourceLauncher) || !existsSync(sourceVbs)) {
|
|
80
|
+
if (!existsSync(sourceLauncher) || !existsSync(sourceHelper) || !existsSync(sourceVbs)) {
|
|
67
81
|
return { ok: false, supported: true, code: 'STARTUP_ASSET_MISSING' };
|
|
68
82
|
}
|
|
69
83
|
const resolved = paths({ home, startupDir, env });
|
|
70
84
|
mkdirSync(dirname(resolved.launcherFile), { recursive: true });
|
|
71
85
|
mkdirSync(dirname(resolved.startupFile), { recursive: true });
|
|
72
86
|
const beforeLauncher = existsSync(resolved.launcherFile) ? readFileSync(resolved.launcherFile) : null;
|
|
87
|
+
const beforeHelper = existsSync(resolved.helperFile) ? readFileSync(resolved.helperFile) : null;
|
|
73
88
|
const beforeStartup = existsSync(resolved.startupFile) ? readFileSync(resolved.startupFile) : null;
|
|
74
89
|
copyFileSync(sourceLauncher, resolved.launcherFile);
|
|
90
|
+
copyFileSync(sourceHelper, resolved.helperFile);
|
|
75
91
|
const rendered = renderVbs(readFileSync(sourceVbs, 'utf8'), resolved.launcherFile);
|
|
76
92
|
writeFileSync(resolved.startupFile, `\uFEFF${rendered}`, 'utf16le');
|
|
77
93
|
const changed = !beforeLauncher?.equals(readFileSync(resolved.launcherFile))
|
|
94
|
+
|| !beforeHelper?.equals(readFileSync(resolved.helperFile))
|
|
78
95
|
|| !beforeStartup?.equals(readFileSync(resolved.startupFile));
|
|
79
96
|
return { ok: true, supported: true, changed, ...resolved };
|
|
80
97
|
}
|
|
@@ -88,7 +105,7 @@ export function uninstallWindowsStartup({
|
|
|
88
105
|
if (platform !== 'win32') return { ok: true, supported: false, removed: false };
|
|
89
106
|
const resolved = paths({ home, startupDir, env });
|
|
90
107
|
let removed = false;
|
|
91
|
-
for (const file of [resolved.startupFile, resolved.launcherFile]) {
|
|
108
|
+
for (const file of [resolved.startupFile, resolved.launcherFile, resolved.helperFile]) {
|
|
92
109
|
if (existsSync(file)) {
|
|
93
110
|
rmSync(file, { force: true });
|
|
94
111
|
removed = true;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
function explicitSource(value) {
|
|
4
|
+
if (typeof value !== 'string') return null;
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
return /^[a-z0-9][a-z0-9._-]{0,63}$/.test(normalized) ? normalized : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function classifyParent(value) {
|
|
10
|
+
const firstLine = String(value ?? '').split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
11
|
+
if (!firstLine) return 'unknown';
|
|
12
|
+
const command = firstLine.split(/[\\/]/).pop().toLowerCase().replace(/\.exe$/, '');
|
|
13
|
+
if (command.includes('claude')) return 'claude-code';
|
|
14
|
+
if (command.includes('codex')) return 'codex';
|
|
15
|
+
if (command.includes('zcode')) return 'zcode';
|
|
16
|
+
return /^[a-z0-9][a-z0-9._-]{0,63}$/.test(command) ? command : 'unknown';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readParentProcess({ platform, pid }) {
|
|
20
|
+
const options = {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
timeout: 2_000,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
};
|
|
26
|
+
if (platform === 'win32') {
|
|
27
|
+
return execFileSync('powershell.exe', [
|
|
28
|
+
'-NoLogo',
|
|
29
|
+
'-NoProfile',
|
|
30
|
+
'-NonInteractive',
|
|
31
|
+
'-Command',
|
|
32
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").Name`,
|
|
33
|
+
], options);
|
|
34
|
+
}
|
|
35
|
+
return execFileSync('ps', ['-o', 'comm=', '-p', String(pid)], options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function detectOrchestrator({
|
|
39
|
+
env = process.env,
|
|
40
|
+
platform = process.platform,
|
|
41
|
+
parentPid = process.ppid,
|
|
42
|
+
readParent = readParentProcess,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const explicit = explicitSource(env.DSH_ORCHESTRATOR);
|
|
45
|
+
if (explicit) return explicit;
|
|
46
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return 'claude-code';
|
|
47
|
+
if (!Number.isInteger(parentPid) || parentPid < 1) return 'unknown';
|
|
48
|
+
try {
|
|
49
|
+
return classifyParent(readParent({ platform, pid: parentPid }));
|
|
50
|
+
} catch {
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/readiness-matrix.mjs
CHANGED
|
@@ -26,9 +26,10 @@ const TARGET_ROWS = Object.freeze([
|
|
|
26
26
|
['linux_deterministic', 'ci'],
|
|
27
27
|
['windows_regressions', 'ci'],
|
|
28
28
|
['macos_smoke', 'ci'],
|
|
29
|
-
['hub_compatibility', 'live-runtime'],
|
|
30
|
-
['provider_catalog', 'live-runtime'],
|
|
31
|
-
['
|
|
29
|
+
['hub_compatibility', 'live-runtime'],
|
|
30
|
+
['provider_catalog', 'live-runtime'],
|
|
31
|
+
['model_execution', 'real-execution'],
|
|
32
|
+
['deepseek_flash', 'real-execution'],
|
|
32
33
|
['deepseek_pro', 'real-execution'],
|
|
33
34
|
['opencode_go_mimo_qwen', 'real-execution'],
|
|
34
35
|
['reviewer_pipeline', 'real-execution'],
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Keep this module pure and dependency-free so Hub, MCP and tests all use the
|
|
9
9
|
// exact same compatibility rules.
|
|
10
10
|
|
|
11
|
-
export const RUNTIME_VERSION = '0.5.
|
|
11
|
+
export const RUNTIME_VERSION = '0.5.4';
|
|
12
12
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
13
13
|
|
|
14
14
|
export const HUB_CAPABILITIES = Object.freeze([
|