@ran-sh/dsh-crew 1.8.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.
Files changed (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +1 -1
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -332
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +455 -401
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
@@ -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
+ }