@ran-sh/dsh-crew 1.9.0 → 1.9.1
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/.claude-plugin/marketplace.json +16 -16
- package/.claude-plugin/plugin.json +14 -14
- package/LICENSE +21 -21
- package/README.de.md +359 -359
- package/README.es.md +359 -359
- package/README.fr.md +359 -359
- package/README.hi.md +359 -359
- package/README.id.md +359 -359
- package/README.ja.md +359 -359
- package/README.ko.md +359 -359
- package/README.md +126 -126
- package/README.pt.md +359 -359
- package/README.ru.md +359 -359
- package/README.th.md +359 -359
- package/README.tr.md +359 -359
- package/README.vi.md +359 -359
- package/README.zh-TW.md +359 -359
- package/README.zh.md +116 -116
- package/agents/ds-flash.md +24 -24
- package/agents/ds-pro.md +24 -24
- package/agents/ds-reviewer.md +23 -23
- package/agents/ds-worker.md +23 -23
- package/bin/dsh-crew.mjs +16 -16
- package/codex/agents/ds-flash.toml +34 -34
- package/codex/agents/ds-pro.toml +34 -34
- package/codex/agents/ds-reviewer.toml +32 -32
- package/codex/agents/ds-worker.toml +32 -32
- package/codex/prompts/dsh-config.md +19 -19
- package/codex/prompts/dsh-status.md +5 -5
- package/commands/config.md +23 -23
- package/commands/off.md +5 -5
- package/commands/on.md +5 -5
- package/commands/status.md +9 -9
- package/cordis.patch.yml +4 -4
- package/docs/gpt-relay-extension.md +103 -103
- package/docs/installation.md +138 -138
- package/docs/job-contracts.md +103 -103
- package/docs/readiness-matrix.md +85 -85
- package/docs/ui-surfaces.md +107 -107
- package/official-web-bridge/cordis.patch.yml +4 -4
- package/official-web-bridge/entry.mjs +1 -1
- package/official-web-bridge/overlay-entry.mjs +59 -59
- package/official-web-bridge/package.json +25 -25
- package/package.json +1 -1
- package/scripts/build-client.mjs +49 -49
- package/scripts/live-crew-smoke.mjs +39 -39
- package/scripts/live-policy-matrix.mjs +177 -177
- package/scripts/policy-probe.mjs +101 -101
- package/scripts/remove-legacy-official-bridge.ps1 +89 -89
- package/scripts/setup.mjs +393 -393
- package/scripts/smoke-real.mjs +110 -110
- package/scripts/smoke.mjs +78 -78
- package/scripts/verify-crew-ui-polish.mjs +145 -145
- package/scripts/verify-history-ui.mjs +97 -97
- package/scripts/verify-installer-fix.mjs +26 -26
- package/scripts/verify-npm-install.mjs +311 -311
- package/scripts/verify-official-bridge-e2e.mjs +192 -192
- package/src/adaptive-routing.mjs +260 -260
- package/src/client/activation-summary.tsx +64 -64
- package/src/client/collapsible-sections.mjs +55 -55
- package/src/client/history-panel.tsx +108 -108
- package/src/client/host-readiness.mjs +71 -71
- package/src/client/index.tsx +1711 -1711
- package/src/client/model-callability-view.mjs +17 -17
- package/src/client/panel-chrome.tsx +40 -40
- package/src/client/quick-entry.tsx +10 -10
- package/src/client/quick-panel.tsx +275 -275
- package/src/client/readiness-envelope.mjs +34 -34
- package/src/client/surface-detection.mjs +43 -43
- package/src/config-readiness.mjs +226 -226
- package/src/credential-reference.mjs +38 -38
- package/src/delivery.mjs +205 -205
- package/src/dsh-cli-runtime.mjs +1021 -1021
- package/src/dsh-cohort.mjs +20 -20
- package/src/extension-contract.mjs +104 -104
- package/src/failure-classification.mjs +201 -201
- package/src/history/admission-gate.mjs +67 -67
- package/src/history/archive-store.mjs +272 -272
- package/src/history/cleanup-plan.mjs +89 -89
- package/src/history/http.mjs +32 -32
- package/src/history/operation.mjs +86 -86
- package/src/history/runner-detach.mjs +34 -34
- package/src/history/runner.mjs +52 -52
- package/src/history/runtime.mjs +36 -36
- package/src/history/service.mjs +177 -177
- package/src/history/state.mjs +27 -27
- package/src/hub/entry.mjs +104 -104
- package/src/hub/index.mjs +2694 -2694
- package/src/hub-client.mjs +154 -154
- package/src/hub-compatibility.mjs +42 -42
- package/src/i18n.mjs +19 -19
- package/src/information-flow.mjs +67 -67
- package/src/install/cli.mjs +28 -28
- package/src/install/install-legacy.mjs +711 -711
- package/src/install/install.mjs +483 -483
- package/src/install/npx-lifecycle.mjs +3456 -3456
- package/src/install/official-frontend-assets.mjs +78 -78
- package/src/install/official-web.mjs +95 -95
- package/src/install/payload-content.mjs +88 -88
- package/src/install/windows-startup.mjs +236 -236
- package/src/install/windows-supervisor-adapter.mjs +443 -443
- package/src/install/windows-supervisor-lifecycle.mjs +782 -782
- package/src/install/zcode.mjs +397 -397
- package/src/job-contracts.mjs +255 -255
- package/src/local-request-guard.mjs +60 -60
- package/src/mcp-runtime.mjs +340 -340
- package/src/model-callability-contract.mjs +79 -79
- package/src/model-catalog.mjs +180 -180
- package/src/model-routing.mjs +586 -586
- package/src/model-schedule.mjs +207 -207
- package/src/official-web-bridge.mjs +447 -447
- package/src/policy.mjs +235 -235
- package/src/provider-delete-adapters.mjs +1934 -1934
- package/src/provider-health.mjs +130 -130
- package/src/provider-inventory.mjs +182 -182
- package/src/provider-layer-migration-adapters.mjs +759 -759
- package/src/provider-layer-migration.mjs +198 -198
- package/src/provider-lifecycle-state.mjs +103 -103
- package/src/provider-lifecycle.mjs +252 -252
- package/src/provider-profile-store.mjs +390 -390
- package/src/provider-settings-store.mjs +633 -633
- package/src/provider-store-lock.mjs +67 -67
- package/src/readiness-matrix.mjs +181 -181
- package/src/removable-waiter.mjs +29 -29
- package/src/role-profiles.mjs +107 -107
- package/src/runtime-controls.mjs +84 -84
- package/src/runtime-identity-contract.mjs +34 -34
- package/src/runtime-identity.mjs +235 -235
- package/src/runtime-readiness-snapshot.mjs +285 -285
- package/src/server.mjs +581 -581
- package/src/session-origins.mjs +60 -60
- package/src/standalone-sdk.mjs +23 -23
- package/src/status-shard.mjs +63 -63
- package/src/structured-error-code.mjs +38 -38
- package/src/supervisor/restart-request.mjs +256 -256
- package/src/workflow-runtime.mjs +739 -739
- package/src/workflow.mjs +155 -155
- package/src/workspace-audit.mjs +231 -231
- package/src/workspace-context.mjs +146 -146
- package/src/workspace-isolation.mjs +455 -455
- package/src/workspace-readiness.mjs +32 -32
- package/statusline/statusline.sh +14 -14
- package/statusline/worker-segment.sh +35 -35
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +1302 -1302
- package/windows/supervisor-control.ps1 +467 -467
- package/worker.cordis.yml +67 -67
- package/zcode/agents/ds-reviewer.md +31 -31
- package/zcode/agents/ds-worker.md +31 -31
- package/zcode/commands/dsh-config.md +17 -17
- package/zcode/commands/dsh-status.md +5 -5
package/scripts/build-client.mjs
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
// Wrap the compiled client bundle into the dsh-web module-loader artifact:
|
|
2
|
-
// `window.__ModuleLoader__.load({ id, factory })`. The id must equal the
|
|
3
|
-
// package name (= the cordis entry name). Pattern from dsh-noema.
|
|
4
|
-
|
|
5
|
-
import { readFile, writeFile, rm, mkdir, readdir } from 'node:fs/promises';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
|
-
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
11
|
-
const pluginId = manifest.name;
|
|
12
|
-
const buildDir = join(root, '.client-build');
|
|
13
|
-
|
|
14
|
-
// Dual entry: the full 3210 control plane (entry.cjs) and the narrow 3080
|
|
15
|
-
// quick panel (quick-entry.cjs). They are wrapped under DIFFERENT module
|
|
16
|
-
// ids so the official surface never receives full control-plane code.
|
|
17
|
-
const fullEntry = 'entry.cjs';
|
|
18
|
-
const quickEntry = 'quick-entry.cjs';
|
|
19
|
-
const built = (await readdir(buildDir)).filter((name) => name.endsWith('.cjs'));
|
|
20
|
-
for (const expected of [fullEntry, quickEntry]) {
|
|
21
|
-
if (!built.includes(expected)) {
|
|
22
|
-
throw new Error(`missing compiled client entry ${expected} in .client-build (found: ${built.join(', ')})`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function wrap(source, id) {
|
|
27
|
-
// The host resolves package modules, not discarded tsdown shared chunks.
|
|
28
|
-
if (/require\(["']\.\.?\//.test(source)) throw new Error(`client ${id} depends on an unshipped relative chunk`);
|
|
29
|
-
return [
|
|
30
|
-
`window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`,
|
|
31
|
-
'var module = { exports: {} }; var exports = module.exports;',
|
|
32
|
-
source.replace(/\n?\/\/# sourceMappingURL=.*$/u, ''),
|
|
33
|
-
'return module.exports; } });',
|
|
34
|
-
'',
|
|
35
|
-
].join('\n');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const fullSource = await readFile(join(buildDir, fullEntry), 'utf8');
|
|
39
|
-
const fullWrapped = wrap(fullSource, pluginId);
|
|
40
|
-
await mkdir(join(root, 'lib'), { recursive: true });
|
|
41
|
-
await writeFile(join(root, 'lib', 'client.js'), fullWrapped);
|
|
42
|
-
|
|
43
|
-
const bridgeManifest = JSON.parse(await readFile(join(root, 'official-web-bridge', 'package.json'), 'utf8'));
|
|
44
|
-
const quickSource = await readFile(join(buildDir, quickEntry), 'utf8');
|
|
45
|
-
const quickWrapped = wrap(quickSource, bridgeManifest.name);
|
|
46
|
-
await mkdir(join(root, 'official-web-bridge', 'lib'), { recursive: true });
|
|
47
|
-
await writeFile(join(root, 'official-web-bridge', 'lib', 'client.js'), quickWrapped);
|
|
48
|
-
await rm(buildDir, { recursive: true, force: true });
|
|
49
|
-
console.log(`built lib/client.js (${fullWrapped.length} bytes) and official-web-bridge/lib/client.js (${quickWrapped.length} bytes) from ${fullEntry} + ${quickEntry}`);
|
|
1
|
+
// Wrap the compiled client bundle into the dsh-web module-loader artifact:
|
|
2
|
+
// `window.__ModuleLoader__.load({ id, factory })`. The id must equal the
|
|
3
|
+
// package name (= the cordis entry name). Pattern from dsh-noema.
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile, rm, mkdir, readdir } from 'node:fs/promises';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
11
|
+
const pluginId = manifest.name;
|
|
12
|
+
const buildDir = join(root, '.client-build');
|
|
13
|
+
|
|
14
|
+
// Dual entry: the full 3210 control plane (entry.cjs) and the narrow 3080
|
|
15
|
+
// quick panel (quick-entry.cjs). They are wrapped under DIFFERENT module
|
|
16
|
+
// ids so the official surface never receives full control-plane code.
|
|
17
|
+
const fullEntry = 'entry.cjs';
|
|
18
|
+
const quickEntry = 'quick-entry.cjs';
|
|
19
|
+
const built = (await readdir(buildDir)).filter((name) => name.endsWith('.cjs'));
|
|
20
|
+
for (const expected of [fullEntry, quickEntry]) {
|
|
21
|
+
if (!built.includes(expected)) {
|
|
22
|
+
throw new Error(`missing compiled client entry ${expected} in .client-build (found: ${built.join(', ')})`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wrap(source, id) {
|
|
27
|
+
// The host resolves package modules, not discarded tsdown shared chunks.
|
|
28
|
+
if (/require\(["']\.\.?\//.test(source)) throw new Error(`client ${id} depends on an unshipped relative chunk`);
|
|
29
|
+
return [
|
|
30
|
+
`window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`,
|
|
31
|
+
'var module = { exports: {} }; var exports = module.exports;',
|
|
32
|
+
source.replace(/\n?\/\/# sourceMappingURL=.*$/u, ''),
|
|
33
|
+
'return module.exports; } });',
|
|
34
|
+
'',
|
|
35
|
+
].join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fullSource = await readFile(join(buildDir, fullEntry), 'utf8');
|
|
39
|
+
const fullWrapped = wrap(fullSource, pluginId);
|
|
40
|
+
await mkdir(join(root, 'lib'), { recursive: true });
|
|
41
|
+
await writeFile(join(root, 'lib', 'client.js'), fullWrapped);
|
|
42
|
+
|
|
43
|
+
const bridgeManifest = JSON.parse(await readFile(join(root, 'official-web-bridge', 'package.json'), 'utf8'));
|
|
44
|
+
const quickSource = await readFile(join(buildDir, quickEntry), 'utf8');
|
|
45
|
+
const quickWrapped = wrap(quickSource, bridgeManifest.name);
|
|
46
|
+
await mkdir(join(root, 'official-web-bridge', 'lib'), { recursive: true });
|
|
47
|
+
await writeFile(join(root, 'official-web-bridge', 'lib', 'client.js'), quickWrapped);
|
|
48
|
+
await rm(buildDir, { recursive: true, force: true });
|
|
49
|
+
console.log(`built lib/client.js (${fullWrapped.length} bytes) and official-web-bridge/lib/client.js (${quickWrapped.length} bytes) from ${fullEntry} + ${quickEntry}`);
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
// Live Crew worker smoke against a Crew-owned dsh-crew Hub (Stage 4 provider
|
|
2
|
-
// routing). Targets the dedicated Crew Hub (DSH_CREW_HUB env or default
|
|
3
|
-
// http://127.0.0.1:3210); it never targets or mutates the official web profile.
|
|
4
|
-
// Creates a temp fixture as cwd, spawns flash, polls, prints job metadata.
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
const BASE = (process.env.DSH_CREW_HUB ?? 'http://127.0.0.1:3210').replace(/\/$/, '') + '/_dsh/dsh-crew';
|
|
9
|
-
|
|
10
|
-
(async () => {
|
|
11
|
-
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh4-fixture-'));
|
|
12
|
-
fs.writeFileSync(path.join(fixture, 'note.txt'), 'hello');
|
|
13
|
-
const cwd = fixture.replace(/\\/g, '/');
|
|
14
|
-
console.log('fixture:', cwd);
|
|
15
|
-
console.log('provider route:', JSON.stringify(await (await fetch(`${BASE}/provider`)).json()));
|
|
16
|
-
|
|
17
|
-
const tier = process.argv[2] ?? 'flash';
|
|
18
|
-
const res = await fetch(`${BASE}/jobs`, {
|
|
19
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
20
|
-
body: JSON.stringify({ task: 'Reply with exactly: CREW_OPENCODE_GO_OK', tier, effort: 'off', cwd }),
|
|
21
|
-
});
|
|
22
|
-
const spawned = await res.json();
|
|
23
|
-
console.log('spawn:', JSON.stringify(spawned).slice(0, 260));
|
|
24
|
-
if (!spawned.ok && !spawned.job) { process.exit(1); }
|
|
25
|
-
const id = (spawned.job ?? spawned).id;
|
|
26
|
-
|
|
27
|
-
// Poll for up to 120s.
|
|
28
|
-
for (let i = 0; i < 40; i++) {
|
|
29
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
30
|
-
const r = await (await fetch(`${BASE}/jobs/${id}?wait=0`)).json();
|
|
31
|
-
const j = r.job ?? r;
|
|
32
|
-
if (!['running', 'pending'].includes(j.status)) {
|
|
33
|
-
console.log('final:', JSON.stringify({ id: j.id, tier: j.tier, model: j.model, status: j.status, result: (j.result || '').slice(0, 120), error: (j.error || '').slice(0, 200), tokens: j.tokens }, null, 2));
|
|
34
|
-
process.exit(j.status === 'done' ? 0 : 2);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
console.log('TIMEOUT waiting for job', id);
|
|
38
|
-
process.exit(3);
|
|
39
|
-
})();
|
|
1
|
+
// Live Crew worker smoke against a Crew-owned dsh-crew Hub (Stage 4 provider
|
|
2
|
+
// routing). Targets the dedicated Crew Hub (DSH_CREW_HUB env or default
|
|
3
|
+
// http://127.0.0.1:3210); it never targets or mutates the official web profile.
|
|
4
|
+
// Creates a temp fixture as cwd, spawns flash, polls, prints job metadata.
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const BASE = (process.env.DSH_CREW_HUB ?? 'http://127.0.0.1:3210').replace(/\/$/, '') + '/_dsh/dsh-crew';
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh4-fixture-'));
|
|
12
|
+
fs.writeFileSync(path.join(fixture, 'note.txt'), 'hello');
|
|
13
|
+
const cwd = fixture.replace(/\\/g, '/');
|
|
14
|
+
console.log('fixture:', cwd);
|
|
15
|
+
console.log('provider route:', JSON.stringify(await (await fetch(`${BASE}/provider`)).json()));
|
|
16
|
+
|
|
17
|
+
const tier = process.argv[2] ?? 'flash';
|
|
18
|
+
const res = await fetch(`${BASE}/jobs`, {
|
|
19
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ task: 'Reply with exactly: CREW_OPENCODE_GO_OK', tier, effort: 'off', cwd }),
|
|
21
|
+
});
|
|
22
|
+
const spawned = await res.json();
|
|
23
|
+
console.log('spawn:', JSON.stringify(spawned).slice(0, 260));
|
|
24
|
+
if (!spawned.ok && !spawned.job) { process.exit(1); }
|
|
25
|
+
const id = (spawned.job ?? spawned).id;
|
|
26
|
+
|
|
27
|
+
// Poll for up to 120s.
|
|
28
|
+
for (let i = 0; i < 40; i++) {
|
|
29
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
30
|
+
const r = await (await fetch(`${BASE}/jobs/${id}?wait=0`)).json();
|
|
31
|
+
const j = r.job ?? r;
|
|
32
|
+
if (!['running', 'pending'].includes(j.status)) {
|
|
33
|
+
console.log('final:', JSON.stringify({ id: j.id, tier: j.tier, model: j.model, status: j.status, result: (j.result || '').slice(0, 120), error: (j.error || '').slice(0, 200), tokens: j.tokens }, null, 2));
|
|
34
|
+
process.exit(j.status === 'done' ? 0 : 2);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.log('TIMEOUT waiting for job', id);
|
|
38
|
+
process.exit(3);
|
|
39
|
+
})();
|
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
// Live policy acceptance matrix against a Crew-owned dsh-crew Hub (loopback
|
|
2
|
-
// API). Targets the dedicated Crew Hub (DSH_CREW_HUB env or default
|
|
3
|
-
// http://127.0.0.1:3210); it never targets or mutates the official web profile.
|
|
4
|
-
//
|
|
5
|
-
// Safety guarantees:
|
|
6
|
-
// - the worker cwd is always an auto-created temp fixture under os.tmpdir()
|
|
7
|
-
// (never a real user path — no hardcoded Desktop/Users paths);
|
|
8
|
-
// - the pre-test Crew config is snapshotted and restored in a try/finally, so
|
|
9
|
-
// the matrix can never leave the DSH policy in a test state, even on error;
|
|
10
|
-
// - if there was no ~/.config/dsh-crew/config.json before the run, that
|
|
11
|
-
// absence cannot be recreated through the public API (the config route only
|
|
12
|
-
// merges), so the script reports it instead of pretending.
|
|
13
|
-
//
|
|
14
|
-
// The helpers (createLiveFixture / withBaselineRestore / configFileExists) are
|
|
15
|
-
// exported and unit-tested in test/live-policy-matrix.test.mjs.
|
|
16
|
-
//
|
|
17
|
-
// Usage: node scripts/live-policy-matrix.mjs
|
|
18
|
-
|
|
19
|
-
import os from 'node:os';
|
|
20
|
-
import path from 'node:path';
|
|
21
|
-
import fs, { mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
22
|
-
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import { homedir } from 'node:os';
|
|
24
|
-
|
|
25
|
-
const BASE = (process.env.DSH_CREW_HUB ?? 'http://127.0.0.1:3210').replace(/\/$/, '') + '/_dsh/dsh-crew';
|
|
26
|
-
const CREW_CONFIG = () => path.join(homedir(), '.config', 'dsh-crew', 'config.json');
|
|
27
|
-
|
|
28
|
-
/** True when the global config file exists on disk before the run. */
|
|
29
|
-
export function configFileExists(configPath = CREW_CONFIG()) {
|
|
30
|
-
return existsSync(configPath);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Create a throwaway fixture dir under os.tmpdir() with minimal safe files. */
|
|
34
|
-
export async function createLiveFixture(opts = {}) {
|
|
35
|
-
const prefix = opts.prefix ?? 'dsh-crew-live-acceptance-';
|
|
36
|
-
const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
37
|
-
const src = path.join(dir, 'src');
|
|
38
|
-
fs.mkdirSync(src, { recursive: true });
|
|
39
|
-
writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'dsh-crew-live-fixture', private: true, type: 'module' }, null, 2) + '\n');
|
|
40
|
-
writeFileSync(path.join(src, 'math.js'), 'export function add(a, b) { return a + b; }\n');
|
|
41
|
-
writeFileSync(path.join(dir, 'README.md'), '# dsh-crew live acceptance fixture\n');
|
|
42
|
-
return dir;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Remove a fixture dir created by createLiveFixture. */
|
|
46
|
-
export function removeLiveFixture(dir) {
|
|
47
|
-
if (dir && dir.startsWith(os.tmpdir())) rmSync(dir, { recursive: true, force: true });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Snapshot the current config, run fn(), then restore the snapshot —
|
|
52
|
-
* always, even when fn() throws. Returns fn()'s value or rethrows.
|
|
53
|
-
*/
|
|
54
|
-
export async function withBaselineRestore(getConfig, setConfig, fn) {
|
|
55
|
-
const baseline = await getConfig();
|
|
56
|
-
try {
|
|
57
|
-
return await fn();
|
|
58
|
-
} finally {
|
|
59
|
-
try { await setConfig(baseline); } catch (err) { console.error('LIVE MATRIX: baseline restore failed —', err?.message ?? err); }
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function getConfig() {
|
|
64
|
-
const res = await fetch(`${BASE}/config`);
|
|
65
|
-
const body = await res.json();
|
|
66
|
-
if (!body.ok) throw new Error(`config GET failed: ${JSON.stringify(body)}`);
|
|
67
|
-
return body.config;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function setConfig(patch) {
|
|
71
|
-
const res = await fetch(`${BASE}/config`, {
|
|
72
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
73
|
-
body: JSON.stringify(patch),
|
|
74
|
-
});
|
|
75
|
-
const body = await res.json();
|
|
76
|
-
if (!body.ok) throw new Error(`config write failed: ${JSON.stringify(body)}`);
|
|
77
|
-
return body.config;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function spawn(body) {
|
|
81
|
-
const res = await fetch(`${BASE}/jobs`, {
|
|
82
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
83
|
-
body: JSON.stringify(body),
|
|
84
|
-
});
|
|
85
|
-
return res.json();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
89
|
-
if (isMain) {
|
|
90
|
-
let passed = 0, failed = 0;
|
|
91
|
-
const check = (name, ok, detail = '') => {
|
|
92
|
-
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`);
|
|
93
|
-
ok ? passed++ : failed++;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const fixture = await createLiveFixture();
|
|
97
|
-
const CWD = fixture;
|
|
98
|
-
const hadConfig = configFileExists();
|
|
99
|
-
console.log(`fixture: ${CWD} (exists=${existsSync(CWD)})`);
|
|
100
|
-
console.log(`config file existed before run: ${hadConfig}`);
|
|
101
|
-
|
|
102
|
-
await withBaselineRestore(getConfig, setConfig, async () => {
|
|
103
|
-
// ---------- Test A: Subagents OFF ----------
|
|
104
|
-
await setConfig({ subagents_enabled: false, collaboration_mode: 'balanced' });
|
|
105
|
-
{
|
|
106
|
-
const r = await spawn({ task: 't', tier: 'flash', cwd: CWD });
|
|
107
|
-
check('A1: Subagents OFF blocks flash dispatch', r.ok === false && r.code === 'SUBAGENTS_DISABLED', r.error ?? '');
|
|
108
|
-
const r2 = await spawn({ task: 't', cwd: CWD });
|
|
109
|
-
check('A2: Subagents OFF blocks default dispatch too', r2.ok === false && r2.code === 'SUBAGENTS_DISABLED', r2.error ?? '');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------- Test B/C: Flash Only ----------
|
|
113
|
-
await setConfig({ subagents_enabled: true, collaboration_mode: 'flash-only' });
|
|
114
|
-
{
|
|
115
|
-
const r = await spawn({ task: 't', cwd: CWD });
|
|
116
|
-
check('B1: Flash Only + missing tier spawns flash', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
117
|
-
const r2 = await spawn({ task: 't', tier: 'pro', cwd: CWD });
|
|
118
|
-
check('C1: Flash Only + explicit pro rejected TIER_DISABLED', r2.ok === false && r2.code === 'TIER_DISABLED', r2.error ?? '');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ---------- Test D/E: Pro Only ----------
|
|
122
|
-
await setConfig({ collaboration_mode: 'pro-only' });
|
|
123
|
-
{
|
|
124
|
-
const r = await spawn({ task: 't', cwd: CWD });
|
|
125
|
-
check('D1: Pro Only + missing tier spawns pro (resolved-tier regression)', r.ok === true && r.job.tier === 'pro', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
126
|
-
const r2 = await spawn({ task: 't', tier: 'flash', cwd: CWD });
|
|
127
|
-
check('E1: Pro Only + explicit flash rejected TIER_DISABLED', r2.ok === false && r2.code === 'TIER_DISABLED', r2.error ?? '');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ---------- Test F: Balanced ----------
|
|
131
|
-
await setConfig({ collaboration_mode: 'balanced', flash_state: 'auto', pro_state: 'auto' });
|
|
132
|
-
{
|
|
133
|
-
const r = await spawn({ task: 't', cwd: CWD });
|
|
134
|
-
check('F1: Balanced + missing tier uses default (flash)', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
135
|
-
const r2 = await spawn({ task: 't', tier: 'pro', cwd: CWD });
|
|
136
|
-
check('F2: Balanced + explicit pro allowed', r2.ok === true && r2.job.tier === 'pro', r2.ok ? `tier=${r2.job.tier}` : r2.error);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ---------- Test G: Manual semantics ----------
|
|
140
|
-
await setConfig({ collaboration_mode: 'custom', flash_state: 'auto', pro_state: 'manual' });
|
|
141
|
-
{
|
|
142
|
-
const r = await spawn({ task: 't', cwd: CWD });
|
|
143
|
-
check('G1: Custom flash=auto/pro=manual + no tier → flash (manual not auto-default)', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
144
|
-
const r2 = await spawn({ task: 't', tier: 'pro', cwd: CWD });
|
|
145
|
-
check('G2: Custom + explicit pro allowed (manual is callable when named)', r2.ok === true && r2.job.tier === 'pro', r2.ok ? `tier=${r2.job.tier}` : r2.error);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ---------- Test H: both disabled ----------
|
|
149
|
-
await setConfig({ collaboration_mode: 'custom', flash_state: 'disabled', pro_state: 'disabled' });
|
|
150
|
-
{
|
|
151
|
-
const r = await spawn({ task: 't', cwd: CWD });
|
|
152
|
-
check('H1: both disabled → NO_WORKER_TIER (no fallback)', r.ok === false && r.code === 'NO_WORKER_TIER', r.error ?? '');
|
|
153
|
-
const r2 = await spawn({ task: 't', tier: 'flash', cwd: CWD });
|
|
154
|
-
check('H2: both disabled + explicit flash still rejected', r2.ok === false && r2.code === 'TIER_DISABLED', r2.error ?? '');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ---------- Test I: Review Pipeline config semantics ----------
|
|
158
|
-
await setConfig({ collaboration_mode: 'review-pipeline' });
|
|
159
|
-
{
|
|
160
|
-
const c = await getConfig();
|
|
161
|
-
check('I1: review-pipeline GET exposes normalized pro_reviews_flash=true', c.pro_reviews_flash === true, `normalized=${c.pro_reviews_flash}`);
|
|
162
|
-
const { normalizeGlobalConfig, shouldRunProReview } = await import('../src/policy.mjs');
|
|
163
|
-
check('I1b: review-pipeline effective pro_reviews_flash=true (policy layer)', shouldRunProReview(normalizeGlobalConfig(c)) === true);
|
|
164
|
-
const r = await spawn({ task: 't', cwd: CWD });
|
|
165
|
-
check('I2: review-pipeline default tier flash', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
removeLiveFixture(fixture);
|
|
170
|
-
const restored = await getConfig();
|
|
171
|
-
console.log(`\nrestore check: mode=${restored.collaboration_mode} flash=${restored.flash_state} pro=${restored.pro_state} subagents=${restored.subagents_enabled}`);
|
|
172
|
-
if (!hadConfig && configFileExists()) {
|
|
173
|
-
console.log('NOTE: ~/.config/dsh-crew/config.json was absent before this run; the public API cannot delete it, so the file remains (values were reset to the pre-test snapshot).');
|
|
174
|
-
}
|
|
175
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
176
|
-
process.exit(failed > 0 ? 1 : 0);
|
|
177
|
-
}
|
|
1
|
+
// Live policy acceptance matrix against a Crew-owned dsh-crew Hub (loopback
|
|
2
|
+
// API). Targets the dedicated Crew Hub (DSH_CREW_HUB env or default
|
|
3
|
+
// http://127.0.0.1:3210); it never targets or mutates the official web profile.
|
|
4
|
+
//
|
|
5
|
+
// Safety guarantees:
|
|
6
|
+
// - the worker cwd is always an auto-created temp fixture under os.tmpdir()
|
|
7
|
+
// (never a real user path — no hardcoded Desktop/Users paths);
|
|
8
|
+
// - the pre-test Crew config is snapshotted and restored in a try/finally, so
|
|
9
|
+
// the matrix can never leave the DSH policy in a test state, even on error;
|
|
10
|
+
// - if there was no ~/.config/dsh-crew/config.json before the run, that
|
|
11
|
+
// absence cannot be recreated through the public API (the config route only
|
|
12
|
+
// merges), so the script reports it instead of pretending.
|
|
13
|
+
//
|
|
14
|
+
// The helpers (createLiveFixture / withBaselineRestore / configFileExists) are
|
|
15
|
+
// exported and unit-tested in test/live-policy-matrix.test.mjs.
|
|
16
|
+
//
|
|
17
|
+
// Usage: node scripts/live-policy-matrix.mjs
|
|
18
|
+
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import fs, { mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
|
|
25
|
+
const BASE = (process.env.DSH_CREW_HUB ?? 'http://127.0.0.1:3210').replace(/\/$/, '') + '/_dsh/dsh-crew';
|
|
26
|
+
const CREW_CONFIG = () => path.join(homedir(), '.config', 'dsh-crew', 'config.json');
|
|
27
|
+
|
|
28
|
+
/** True when the global config file exists on disk before the run. */
|
|
29
|
+
export function configFileExists(configPath = CREW_CONFIG()) {
|
|
30
|
+
return existsSync(configPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Create a throwaway fixture dir under os.tmpdir() with minimal safe files. */
|
|
34
|
+
export async function createLiveFixture(opts = {}) {
|
|
35
|
+
const prefix = opts.prefix ?? 'dsh-crew-live-acceptance-';
|
|
36
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
37
|
+
const src = path.join(dir, 'src');
|
|
38
|
+
fs.mkdirSync(src, { recursive: true });
|
|
39
|
+
writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'dsh-crew-live-fixture', private: true, type: 'module' }, null, 2) + '\n');
|
|
40
|
+
writeFileSync(path.join(src, 'math.js'), 'export function add(a, b) { return a + b; }\n');
|
|
41
|
+
writeFileSync(path.join(dir, 'README.md'), '# dsh-crew live acceptance fixture\n');
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Remove a fixture dir created by createLiveFixture. */
|
|
46
|
+
export function removeLiveFixture(dir) {
|
|
47
|
+
if (dir && dir.startsWith(os.tmpdir())) rmSync(dir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Snapshot the current config, run fn(), then restore the snapshot —
|
|
52
|
+
* always, even when fn() throws. Returns fn()'s value or rethrows.
|
|
53
|
+
*/
|
|
54
|
+
export async function withBaselineRestore(getConfig, setConfig, fn) {
|
|
55
|
+
const baseline = await getConfig();
|
|
56
|
+
try {
|
|
57
|
+
return await fn();
|
|
58
|
+
} finally {
|
|
59
|
+
try { await setConfig(baseline); } catch (err) { console.error('LIVE MATRIX: baseline restore failed —', err?.message ?? err); }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function getConfig() {
|
|
64
|
+
const res = await fetch(`${BASE}/config`);
|
|
65
|
+
const body = await res.json();
|
|
66
|
+
if (!body.ok) throw new Error(`config GET failed: ${JSON.stringify(body)}`);
|
|
67
|
+
return body.config;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function setConfig(patch) {
|
|
71
|
+
const res = await fetch(`${BASE}/config`, {
|
|
72
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
73
|
+
body: JSON.stringify(patch),
|
|
74
|
+
});
|
|
75
|
+
const body = await res.json();
|
|
76
|
+
if (!body.ok) throw new Error(`config write failed: ${JSON.stringify(body)}`);
|
|
77
|
+
return body.config;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function spawn(body) {
|
|
81
|
+
const res = await fetch(`${BASE}/jobs`, {
|
|
82
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
return res.json();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
89
|
+
if (isMain) {
|
|
90
|
+
let passed = 0, failed = 0;
|
|
91
|
+
const check = (name, ok, detail = '') => {
|
|
92
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`);
|
|
93
|
+
ok ? passed++ : failed++;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const fixture = await createLiveFixture();
|
|
97
|
+
const CWD = fixture;
|
|
98
|
+
const hadConfig = configFileExists();
|
|
99
|
+
console.log(`fixture: ${CWD} (exists=${existsSync(CWD)})`);
|
|
100
|
+
console.log(`config file existed before run: ${hadConfig}`);
|
|
101
|
+
|
|
102
|
+
await withBaselineRestore(getConfig, setConfig, async () => {
|
|
103
|
+
// ---------- Test A: Subagents OFF ----------
|
|
104
|
+
await setConfig({ subagents_enabled: false, collaboration_mode: 'balanced' });
|
|
105
|
+
{
|
|
106
|
+
const r = await spawn({ task: 't', tier: 'flash', cwd: CWD });
|
|
107
|
+
check('A1: Subagents OFF blocks flash dispatch', r.ok === false && r.code === 'SUBAGENTS_DISABLED', r.error ?? '');
|
|
108
|
+
const r2 = await spawn({ task: 't', cwd: CWD });
|
|
109
|
+
check('A2: Subagents OFF blocks default dispatch too', r2.ok === false && r2.code === 'SUBAGENTS_DISABLED', r2.error ?? '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------- Test B/C: Flash Only ----------
|
|
113
|
+
await setConfig({ subagents_enabled: true, collaboration_mode: 'flash-only' });
|
|
114
|
+
{
|
|
115
|
+
const r = await spawn({ task: 't', cwd: CWD });
|
|
116
|
+
check('B1: Flash Only + missing tier spawns flash', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
117
|
+
const r2 = await spawn({ task: 't', tier: 'pro', cwd: CWD });
|
|
118
|
+
check('C1: Flash Only + explicit pro rejected TIER_DISABLED', r2.ok === false && r2.code === 'TIER_DISABLED', r2.error ?? '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------- Test D/E: Pro Only ----------
|
|
122
|
+
await setConfig({ collaboration_mode: 'pro-only' });
|
|
123
|
+
{
|
|
124
|
+
const r = await spawn({ task: 't', cwd: CWD });
|
|
125
|
+
check('D1: Pro Only + missing tier spawns pro (resolved-tier regression)', r.ok === true && r.job.tier === 'pro', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
126
|
+
const r2 = await spawn({ task: 't', tier: 'flash', cwd: CWD });
|
|
127
|
+
check('E1: Pro Only + explicit flash rejected TIER_DISABLED', r2.ok === false && r2.code === 'TIER_DISABLED', r2.error ?? '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------- Test F: Balanced ----------
|
|
131
|
+
await setConfig({ collaboration_mode: 'balanced', flash_state: 'auto', pro_state: 'auto' });
|
|
132
|
+
{
|
|
133
|
+
const r = await spawn({ task: 't', cwd: CWD });
|
|
134
|
+
check('F1: Balanced + missing tier uses default (flash)', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
135
|
+
const r2 = await spawn({ task: 't', tier: 'pro', cwd: CWD });
|
|
136
|
+
check('F2: Balanced + explicit pro allowed', r2.ok === true && r2.job.tier === 'pro', r2.ok ? `tier=${r2.job.tier}` : r2.error);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------- Test G: Manual semantics ----------
|
|
140
|
+
await setConfig({ collaboration_mode: 'custom', flash_state: 'auto', pro_state: 'manual' });
|
|
141
|
+
{
|
|
142
|
+
const r = await spawn({ task: 't', cwd: CWD });
|
|
143
|
+
check('G1: Custom flash=auto/pro=manual + no tier → flash (manual not auto-default)', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
144
|
+
const r2 = await spawn({ task: 't', tier: 'pro', cwd: CWD });
|
|
145
|
+
check('G2: Custom + explicit pro allowed (manual is callable when named)', r2.ok === true && r2.job.tier === 'pro', r2.ok ? `tier=${r2.job.tier}` : r2.error);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------- Test H: both disabled ----------
|
|
149
|
+
await setConfig({ collaboration_mode: 'custom', flash_state: 'disabled', pro_state: 'disabled' });
|
|
150
|
+
{
|
|
151
|
+
const r = await spawn({ task: 't', cwd: CWD });
|
|
152
|
+
check('H1: both disabled → NO_WORKER_TIER (no fallback)', r.ok === false && r.code === 'NO_WORKER_TIER', r.error ?? '');
|
|
153
|
+
const r2 = await spawn({ task: 't', tier: 'flash', cwd: CWD });
|
|
154
|
+
check('H2: both disabled + explicit flash still rejected', r2.ok === false && r2.code === 'TIER_DISABLED', r2.error ?? '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------- Test I: Review Pipeline config semantics ----------
|
|
158
|
+
await setConfig({ collaboration_mode: 'review-pipeline' });
|
|
159
|
+
{
|
|
160
|
+
const c = await getConfig();
|
|
161
|
+
check('I1: review-pipeline GET exposes normalized pro_reviews_flash=true', c.pro_reviews_flash === true, `normalized=${c.pro_reviews_flash}`);
|
|
162
|
+
const { normalizeGlobalConfig, shouldRunProReview } = await import('../src/policy.mjs');
|
|
163
|
+
check('I1b: review-pipeline effective pro_reviews_flash=true (policy layer)', shouldRunProReview(normalizeGlobalConfig(c)) === true);
|
|
164
|
+
const r = await spawn({ task: 't', cwd: CWD });
|
|
165
|
+
check('I2: review-pipeline default tier flash', r.ok === true && r.job.tier === 'flash', r.ok ? `tier=${r.job.tier}` : r.error);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
removeLiveFixture(fixture);
|
|
170
|
+
const restored = await getConfig();
|
|
171
|
+
console.log(`\nrestore check: mode=${restored.collaboration_mode} flash=${restored.flash_state} pro=${restored.pro_state} subagents=${restored.subagents_enabled}`);
|
|
172
|
+
if (!hadConfig && configFileExists()) {
|
|
173
|
+
console.log('NOTE: ~/.config/dsh-crew/config.json was absent before this run; the public API cannot delete it, so the file remains (values were reset to the pre-test snapshot).');
|
|
174
|
+
}
|
|
175
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
176
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
177
|
+
}
|