@ran-sh/dsh-crew 0.3.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.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +8 -0
  3. package/.mcp.json +8 -0
  4. package/LICENSE +21 -0
  5. package/README.de.md +359 -0
  6. package/README.es.md +359 -0
  7. package/README.fr.md +359 -0
  8. package/README.hi.md +359 -0
  9. package/README.id.md +359 -0
  10. package/README.ja.md +359 -0
  11. package/README.ko.md +359 -0
  12. package/README.md +360 -0
  13. package/README.pt.md +359 -0
  14. package/README.ru.md +359 -0
  15. package/README.th.md +359 -0
  16. package/README.tr.md +359 -0
  17. package/README.vi.md +359 -0
  18. package/README.zh-TW.md +359 -0
  19. package/README.zh.md +305 -0
  20. package/agents/ds-flash.md +26 -0
  21. package/agents/ds-pro.md +32 -0
  22. package/agents/ds-reviewer.md +23 -0
  23. package/agents/ds-worker.md +22 -0
  24. package/codex/agents/ds-flash.toml +30 -0
  25. package/codex/agents/ds-pro.toml +31 -0
  26. package/codex/agents/ds-reviewer.toml +28 -0
  27. package/codex/agents/ds-worker.toml +28 -0
  28. package/codex/prompts/dsh-config.md +3 -0
  29. package/codex/prompts/dsh-status.md +1 -0
  30. package/commands/config.md +11 -0
  31. package/commands/off.md +5 -0
  32. package/commands/on.md +5 -0
  33. package/commands/status.md +5 -0
  34. package/cordis.patch.yml +4 -0
  35. package/docs/images/dsh-crew-host.png +0 -0
  36. package/docs/images/dsh-crew-jobs.png +0 -0
  37. package/docs/images/dsh-crew-logo.png +0 -0
  38. package/docs/images/dsh-crew-overview.png +0 -0
  39. package/lib/client.js +2765 -0
  40. package/package.json +125 -0
  41. package/scripts/build-client.mjs +28 -0
  42. package/scripts/live-crew-smoke.mjs +39 -0
  43. package/scripts/live-policy-matrix.mjs +177 -0
  44. package/scripts/policy-probe.mjs +101 -0
  45. package/scripts/setup.mjs +294 -0
  46. package/scripts/smoke-real.mjs +110 -0
  47. package/scripts/smoke.mjs +78 -0
  48. package/scripts/verify-installer-fix.mjs +26 -0
  49. package/src/adaptive-routing.mjs +260 -0
  50. package/src/client/activation-summary.tsx +64 -0
  51. package/src/client/entry.tsx +236 -0
  52. package/src/client/index.tsx +1120 -0
  53. package/src/config-readiness.mjs +59 -0
  54. package/src/delivery.mjs +205 -0
  55. package/src/dsh-cli-runtime.mjs +251 -0
  56. package/src/failure-classification.mjs +172 -0
  57. package/src/hub/entry.mjs +98 -0
  58. package/src/hub/index.mjs +757 -0
  59. package/src/hub-client.mjs +132 -0
  60. package/src/hub-compatibility.mjs +49 -0
  61. package/src/i18n.mjs +19 -0
  62. package/src/install/cli.mjs +28 -0
  63. package/src/install/install-legacy.mjs +460 -0
  64. package/src/install/install.mjs +451 -0
  65. package/src/jobs.mjs +275 -0
  66. package/src/mcp-runtime.mjs +257 -0
  67. package/src/model-catalog.mjs +173 -0
  68. package/src/model-routing.mjs +391 -0
  69. package/src/multimodal.mjs +0 -0
  70. package/src/policy-legacy.mjs +830 -0
  71. package/src/policy.mjs +197 -0
  72. package/src/readiness-matrix.mjs +169 -0
  73. package/src/runtime-controls.mjs +90 -0
  74. package/src/runtime-identity.mjs +108 -0
  75. package/src/server.mjs +477 -0
  76. package/src/status-shard.mjs +52 -0
  77. package/src/structured-error-code.mjs +39 -0
  78. package/src/vision-route.mjs +138 -0
  79. package/src/workflow-runtime.mjs +567 -0
  80. package/src/workflow.mjs +160 -0
  81. package/src/workspace-audit.mjs +231 -0
  82. package/src/workspace-isolation.mjs +306 -0
  83. package/statusline/statusline.sh +14 -0
  84. package/statusline/worker-segment.sh +35 -0
  85. package/worker.cordis.yml +77 -0
package/package.json ADDED
@@ -0,0 +1,125 @@
1
+ {
2
+ "name": "@ran-sh/dsh-crew",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "main": "./src/hub/entry.mjs",
6
+ "description": "DeepSeek Harness plugin: dispatch work to DSH agents from Claude Code / Codex, as native subagents with live progress",
7
+ "dsh": {
8
+ "bundle": {
9
+ "patch": "./cordis.patch.yml"
10
+ },
11
+ "client": {
12
+ "inject": [
13
+ "@deepseek-ai/dsh-client-runtime",
14
+ "@deepseek-ai/dsh-client-locale",
15
+ "@deepseek-ai/dsh-client-ui-slots",
16
+ "@deepseek-ai/dsh-client-ui-settings"
17
+ ],
18
+ "platform": "web"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "mcp": "node src/server.mjs",
23
+ "build:client": "tsdown src/client/entry.tsx --format cjs --platform browser --target es2022 --tsconfig tsconfig.client.json --deps.never-bundle react --deps.never-bundle react/jsx-runtime --deps.never-bundle react-dom --deps.never-bundle react-dom/client --out-dir .client-build --clean --logLevel warn && node scripts/build-client.mjs"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.17.5",
27
+ "zod": "^3.24.0"
28
+ },
29
+ "exports": {
30
+ ".": "./src/hub/entry.mjs",
31
+ "./client": "./lib/client.js",
32
+ "./package.json": "./package.json"
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^18.0.0",
36
+ "react-dom": "^18.0.0",
37
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
38
+ "@deepseek-ai/dsh-agent-presets": "0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-agent-spine-demo": "0.1.0-rc.6",
40
+ "@deepseek-ai/dsh-bash-local": "0.1.0-rc.6",
41
+ "@deepseek-ai/dsh-compaction-basic": "0.1.0-rc.6",
42
+ "@deepseek-ai/dsh-fs-local": "0.1.0-rc.6",
43
+ "@deepseek-ai/dsh-fs-observation-policy": "0.1.0-rc.6",
44
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
45
+ "@deepseek-ai/dsh-llm-deepseek": "0.1.0-rc.6",
46
+ "@deepseek-ai/dsh-sandbox-local": "0.1.0-rc.6",
47
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.0-rc.6",
48
+ "@deepseek-ai/dsh-sdk-client": "^0.1.0-rc.5",
49
+ "@deepseek-ai/dsh-sdk-jsonrpc-demo": "^0.1.0-rc.5",
50
+ "@deepseek-ai/dsh-sdk-jsonrpc-server": "0.1.0-rc.6",
51
+ "@deepseek-ai/dsh-session": "0.1.0-rc.6",
52
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.1.0-rc.6",
53
+ "@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.6",
54
+ "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6",
55
+ "@deepseek-ai/dsh-tool-fs": "0.1.0-rc.6",
56
+ "@deepseek-ai/dsh-tool-todo": "0.1.0-rc.6"
57
+ },
58
+ "devDependencies": {
59
+ "react": "^18.3.1",
60
+ "react-dom": "^18.3.1",
61
+ "tsdown": "^0.12.0",
62
+ "typescript": "^5.6.0",
63
+ "@types/react": "^18.3.0",
64
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
65
+ "@deepseek-ai/dsh-agent-presets": "0.1.0-rc.6",
66
+ "@deepseek-ai/dsh-agent-spine-demo": "0.1.0-rc.6",
67
+ "@deepseek-ai/dsh-bash-local": "0.1.0-rc.6",
68
+ "@deepseek-ai/dsh-compaction-basic": "0.1.0-rc.6",
69
+ "@deepseek-ai/dsh-fs-local": "0.1.0-rc.6",
70
+ "@deepseek-ai/dsh-fs-observation-policy": "0.1.0-rc.6",
71
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-llm-deepseek": "0.1.0-rc.6",
73
+ "@deepseek-ai/dsh-sandbox-local": "0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-sdk-client": "^0.1.0-rc.5",
76
+ "@deepseek-ai/dsh-sdk-jsonrpc-demo": "^0.1.0-rc.5",
77
+ "@deepseek-ai/dsh-sdk-jsonrpc-server": "0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-session": "0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.1.0-rc.6",
80
+ "@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.6",
81
+ "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6",
82
+ "@deepseek-ai/dsh-tool-fs": "0.1.0-rc.6",
83
+ "@deepseek-ai/dsh-tool-todo": "0.1.0-rc.6"
84
+ },
85
+ "license": "MIT",
86
+ "repository": {
87
+ "type": "git",
88
+ "url": "git+https://github.com/Ran-sh/dsh-crew.git"
89
+ },
90
+ "homepage": "https://github.com/Ran-sh/dsh-crew",
91
+ "bugs": {
92
+ "url": "https://github.com/Ran-sh/dsh-crew/issues"
93
+ },
94
+ "keywords": [
95
+ "dsh",
96
+ "deepseek",
97
+ "claude-code",
98
+ "codex",
99
+ "subagent",
100
+ "worker",
101
+ "multimodal",
102
+ "plugin",
103
+ "agent"
104
+ ],
105
+ "files": [
106
+ "lib",
107
+ "src",
108
+ "scripts",
109
+ "agents",
110
+ "commands",
111
+ "codex",
112
+ "statusline",
113
+ "cordis.patch.yml",
114
+ "worker.cordis.yml",
115
+ ".claude-plugin",
116
+ ".mcp.json",
117
+ "README.md",
118
+ "README.*.md",
119
+ "docs/images/dsh-crew-logo.png",
120
+ "docs/images/dsh-crew-overview.png",
121
+ "docs/images/dsh-crew-host.png",
122
+ "docs/images/dsh-crew-jobs.png",
123
+ "LICENSE"
124
+ ]
125
+ }
@@ -0,0 +1,28 @@
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
+ const compiledEntries = (await readdir(buildDir)).filter((name) => name.endsWith('.cjs'));
14
+ if (compiledEntries.length !== 1) {
15
+ throw new Error(`expected exactly one compiled client entry in .client-build, found ${compiledEntries.length}: ${compiledEntries.join(', ')}`);
16
+ }
17
+ const source = await readFile(join(buildDir, compiledEntries[0]), 'utf8');
18
+ const wrapped = [
19
+ `window.__ModuleLoader__.load({ id: ${JSON.stringify(pluginId)}, factory: (require) => {`,
20
+ 'var module = { exports: {} }; var exports = module.exports;',
21
+ source.replace(/\n?\/\/# sourceMappingURL=.*$/u, ''),
22
+ 'return module.exports; } });',
23
+ '',
24
+ ].join('\n');
25
+ await mkdir(join(root, 'lib'), { recursive: true });
26
+ await writeFile(join(root, 'lib', 'client.js'), wrapped);
27
+ await rm(buildDir, { recursive: true, force: true });
28
+ console.log(`built lib/client.js (${wrapped.length} bytes) as module "${pluginId}" from ${compiledEntries[0]}`);
@@ -0,0 +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
+ })();
@@ -0,0 +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
+ }
@@ -0,0 +1,101 @@
1
+ // Functional probe of the real MCP stdio server (src/server.mjs), no DSH
2
+ // instance and no DeepSeek credentials needed: policy rejections must happen
3
+ // before any worker runtime is touched.
4
+ // Run: node scripts/policy-probe.mjs
5
+
6
+ import { spawn } from 'node:child_process';
7
+
8
+ const child = spawn(process.execPath, ['src/server.mjs'], { stdio: ['pipe', 'pipe', 'pipe'] });
9
+ let buffer = '';
10
+ const pending = new Map();
11
+ let nextId = 1;
12
+
13
+ child.stdout.on('data', (d) => {
14
+ buffer += d.toString();
15
+ let idx;
16
+ while ((idx = buffer.indexOf('\n')) >= 0) {
17
+ const line = buffer.slice(0, idx);
18
+ buffer = buffer.slice(idx + 1);
19
+ if (!line.trim()) continue;
20
+ let msg;
21
+ try { msg = JSON.parse(line); } catch { continue; }
22
+ if (msg.id !== undefined && pending.has(msg.id)) {
23
+ pending.get(msg.id)(msg);
24
+ pending.delete(msg.id);
25
+ }
26
+ }
27
+ });
28
+
29
+ function call(method, params) {
30
+ return new Promise((resolve) => {
31
+ const id = nextId++;
32
+ pending.set(id, resolve);
33
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
34
+ });
35
+ }
36
+
37
+ const results = [];
38
+ function check(name, cond, detail) {
39
+ results.push({ name, ok: !!cond, detail });
40
+ console.log(`${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`);
41
+ }
42
+
43
+ await call('initialize', { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'probe', version: '1' } });
44
+ await new Promise((r) => setTimeout(r, 200));
45
+
46
+ // 1. Read config: must expose the new effective policy fields.
47
+ const cfg = await call('tools/call', { name: 'dsh_worker_config', arguments: {} });
48
+ const cfgText = cfg.result?.content?.[0]?.text;
49
+ let cfgJson = null;
50
+ try { cfgJson = JSON.parse(cfgText); } catch {}
51
+ check('config returns effective policy', !!cfgJson && typeof cfgJson.flash_state === 'string' && typeof cfgJson.pro_state === 'string' && typeof cfgJson.routing_guidance === 'string',
52
+ cfgJson ? `flash=${cfgJson.flash_state} pro=${cfgJson.pro_state} mode=${cfgJson.collaboration_mode}` : cfgText?.slice(0, 120));
53
+ check('config hides API keys', cfgText !== undefined && !/api_key|sk-/.test(cfgText));
54
+ check('config reports effective_policy + roles + legacy_source', !!cfgJson
55
+ && typeof cfgJson.effective_policy === 'string' && cfgJson.effective_policy.includes('mode=')
56
+ && Array.isArray(cfgJson.flash_roles) && cfgJson.flash_roles.length > 0
57
+ && Array.isArray(cfgJson.pro_roles) && cfgJson.pro_roles.length > 0
58
+ && typeof cfgJson.legacy_source === 'string',
59
+ cfgJson ? cfgJson.effective_policy : undefined);
60
+ check('config reflects main_agent_mode', !!cfgJson && ['direct-allowed', 'coordinator-first', 'dispatcher-only'].includes(cfgJson.main_agent_mode), cfgJson?.main_agent_mode);
61
+
62
+ // 2. Session clamp: flash-only + explicit pro must be refused with TIER_DISABLED.
63
+ const clamp = await call('tools/call', { name: 'dsh_worker_config', arguments: { tier_policy: 'flash-only' } });
64
+ check('session tier_policy clamp set', clamp.result !== undefined && !clamp.result.isError);
65
+
66
+ const refusePro = await call('tools/call', { name: 'dsh_run_worker', arguments: { task: 'probe', tier: 'pro' } });
67
+ const refuseText = refusePro.result?.content?.[0]?.text ?? '';
68
+ check('pro refused under flash-only', /TIER_DISABLED/.test(refuseText), refuseText.slice(0, 120));
69
+
70
+ // 3. subagents master switch via session enabled=false.
71
+ const off = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true, enabled: false } });
72
+ check('session enabled=false set', off.result !== undefined && !off.result.isError);
73
+ const refuseAll = await call('tools/call', { name: 'dsh_run_worker', arguments: { task: 'probe' } });
74
+ const refuseAllText = refuseAll.result?.content?.[0]?.text ?? '';
75
+ check('dispatch refused when disabled', /SUBAGENTS_DISABLED/.test(refuseAllText), refuseAllText.slice(0, 120));
76
+
77
+ // 4. dsh_spawn_worker shares the same policy.
78
+ const refuseSpawn = await call('tools/call', { name: 'dsh_spawn_worker', arguments: { task: 'probe' } });
79
+ const refuseSpawnText = refuseSpawn.result?.content?.[0]?.text ?? '';
80
+ check('spawn refused when disabled', /SUBAGENTS_DISABLED/.test(refuseSpawnText));
81
+
82
+ // 5. Manual semantics: flash manual + pro auto, no explicit tier → pro is chosen, never flash.
83
+ const manual = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true, tier_policy: 'auto' } });
84
+ // global config is balanced here; exercise manual via session state override:
85
+ const manualSet = await call('tools/call', { name: 'dsh_worker_config', arguments: { collaboration_mode: 'custom', flash_state: 'manual', pro_state: 'auto', default_tier: 'flash' } });
86
+ check('manual/custom session set', manualSet.result !== undefined && !manualSet.result.isError);
87
+ const manualCfg = await call('tools/call', { name: 'dsh_worker_config', arguments: {} });
88
+ const manualCfgJson = JSON.parse(manualCfg.result.content[0].text);
89
+ check('manual tier never auto-default (effective_default_tier=pro)',
90
+ manualCfgJson.effective_default_tier === 'pro', `got ${manualCfgJson.effective_default_tier}`);
91
+ check('effective states reported', manualCfgJson.flash_state === 'manual' && manualCfgJson.pro_state === 'auto');
92
+
93
+ // 6. Reset back to global defaults.
94
+ const reset = await call('tools/call', { name: 'dsh_worker_config', arguments: { reset: true } });
95
+ check('reset restores defaults', reset.result !== undefined && !reset.result.isError);
96
+
97
+ child.kill();
98
+ await new Promise((r) => setTimeout(r, 100));
99
+ const failed = results.filter((r) => !r.ok);
100
+ console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
101
+ process.exit(failed.length > 0 ? 1 : 0);