@ran-sh/dsh-crew 1.1.0 → 1.2.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +16 -17
  2. package/.claude-plugin/plugin.json +7 -1
  3. package/README.md +44 -15
  4. package/README.zh.md +37 -12
  5. package/agents/ds-flash.md +16 -18
  6. package/agents/ds-pro.md +16 -24
  7. package/agents/ds-reviewer.md +15 -15
  8. package/agents/ds-worker.md +16 -16
  9. package/bin/dsh-crew.mjs +16 -12
  10. package/codex/AGENTS.md +37 -85
  11. package/codex/agents/ds-flash.toml +18 -14
  12. package/codex/agents/ds-pro.toml +18 -15
  13. package/codex/agents/ds-reviewer.toml +17 -13
  14. package/codex/agents/ds-worker.toml +17 -13
  15. package/codex/prompts/dsh-config.md +17 -1
  16. package/codex/prompts/dsh-status.md +5 -1
  17. package/commands/config.md +16 -4
  18. package/commands/status.md +6 -2
  19. package/docs/installation.md +54 -9
  20. package/docs/readiness-matrix.md +14 -6
  21. package/docs/ui-surfaces.md +32 -17
  22. package/lib/client.js +5434 -4844
  23. package/official-web-bridge/lib/client.js +567 -479
  24. package/official-web-bridge/overlay-entry.mjs +59 -0
  25. package/package.json +6 -3
  26. package/scripts/build-client.mjs +2 -0
  27. package/scripts/setup.mjs +8 -7
  28. package/scripts/verify-crew-ui-polish.mjs +145 -0
  29. package/scripts/verify-history-ui.mjs +97 -0
  30. package/scripts/verify-npm-install.mjs +1 -1
  31. package/src/client/history-panel.tsx +108 -0
  32. package/src/client/host-readiness.mjs +15 -6
  33. package/src/client/index.tsx +86 -64
  34. package/src/client/model-callability-view.mjs +17 -0
  35. package/src/client/panel-chrome.tsx +40 -0
  36. package/src/client/quick-panel.tsx +88 -73
  37. package/src/client/readiness-envelope.mjs +34 -0
  38. package/src/config-readiness.mjs +181 -115
  39. package/src/extension-contract.mjs +44 -26
  40. package/src/history/admission-gate.mjs +51 -0
  41. package/src/history/archive-store.mjs +261 -0
  42. package/src/history/cleanup-plan.mjs +64 -0
  43. package/src/history/http.mjs +32 -0
  44. package/src/history/operation.mjs +86 -0
  45. package/src/history/runner.mjs +51 -0
  46. package/src/history/runtime.mjs +28 -0
  47. package/src/history/service.mjs +137 -0
  48. package/src/history/state.mjs +27 -0
  49. package/src/hub/index.mjs +141 -24
  50. package/src/hub-client.mjs +17 -15
  51. package/src/image-output.mjs +71 -0
  52. package/src/install/install-legacy.mjs +126 -23
  53. package/src/install/npx-lifecycle.mjs +373 -64
  54. package/src/install/official-frontend-assets.mjs +78 -0
  55. package/src/install/payload-content.mjs +88 -0
  56. package/src/install/windows-startup.mjs +107 -19
  57. package/src/install/windows-supervisor-adapter.mjs +443 -0
  58. package/src/install/windows-supervisor-lifecycle.mjs +782 -0
  59. package/src/install/zcode.mjs +53 -14
  60. package/src/job-contracts.mjs +37 -31
  61. package/src/jobs.mjs +2 -2
  62. package/src/mcp-runtime.mjs +74 -9
  63. package/src/model-callability-contract.mjs +79 -0
  64. package/src/multimodal.mjs +0 -0
  65. package/src/official-web-bridge.mjs +0 -2
  66. package/src/provider-health.mjs +130 -124
  67. package/src/provider-profile-store.mjs +12 -0
  68. package/src/provider-settings-store.mjs +347 -5
  69. package/src/readiness-matrix.mjs +20 -15
  70. package/src/runtime-identity-contract.mjs +34 -0
  71. package/src/runtime-identity.mjs +14 -4
  72. package/src/runtime-readiness-snapshot.mjs +285 -87
  73. package/src/server.mjs +86 -76
  74. package/src/standalone-sdk.mjs +23 -0
  75. package/src/supervisor/restart-request.mjs +64 -10
  76. package/src/workflow-runtime.mjs +28 -13
  77. package/windows/start-dsh-crew.cmd +57 -57
  78. package/windows/start-dsh-crew.ps1 +327 -55
  79. package/windows/supervisor-control.ps1 +467 -0
  80. package/zcode/AGENTS.md +4 -1
  81. package/zcode/agents/ds-reviewer.md +31 -36
  82. package/zcode/agents/ds-worker.md +31 -39
  83. package/zcode/commands/dsh-config.md +17 -6
  84. package/zcode/commands/dsh-status.md +5 -4
  85. package/.mcp.json +0 -8
@@ -2,7 +2,8 @@
2
2
  // render Codex agent roles with real paths. Called from the CLI entry or the
3
3
  // DSH settings page. All edits are backed up and idempotent.
4
4
 
5
- import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readdirSync, rmSync, statSync } from 'node:fs';
5
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readdirSync, rmSync, statSync, lstatSync } from 'node:fs';
6
+ import { createHash } from 'node:crypto';
6
7
  import { dirname, join, resolve, relative } from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
8
9
  import { homedir } from 'node:os';
@@ -14,6 +15,9 @@ const MARKETPLACE_NAME = 'dsh-crew';
14
15
  const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
15
16
  const POLICY_START = '<!-- DSH CREW MANAGED POLICY:START -->';
16
17
  const POLICY_END = '<!-- DSH CREW MANAGED POLICY:END -->';
18
+ export const CODEX_LEGACY_POLICY_HASHES = Object.freeze([
19
+ '2d6f3839bb3df4bda90f481726281292b1a4b4585298b1cf9ec56215295b5c78',
20
+ ]);
17
21
  // dsh_worker_config is included so the session commands (/dsh-crew:config,
18
22
  // /dsh-config) and any orchestrator policy lookup run without an extra
19
23
  // authorization prompt.
@@ -36,12 +40,46 @@ function readText(file) {
36
40
  try { return readFileSync(file, 'utf8'); } catch { return null; }
37
41
  }
38
42
 
43
+ function normalizedPath(value) {
44
+ if (typeof value !== 'string' || !value.trim()) return null;
45
+ try {
46
+ const path = resolve(value);
47
+ return process.platform === 'win32' ? path.toLowerCase() : path;
48
+ } catch { return null; }
49
+ }
50
+
51
+ function renderedCodexRole(root, file) {
52
+ const source = readText(join(root, 'codex', 'agents', file));
53
+ if (source === null) return null;
54
+ const renderedPath = join(root, 'src', 'server.mjs').replace(/\\/g, '/');
55
+ return source.replace(/args = \[.*server\.mjs"\]/, `args = ["${renderedPath}"]`);
56
+ }
57
+
39
58
  function managedPolicyBlock(root) {
40
59
  const policy = readText(join(root, 'codex', 'AGENTS.md'))?.trim();
41
60
  if (!policy) return null;
42
61
  return `${POLICY_START}\n${policy}\n${POLICY_END}`;
43
62
  }
44
63
 
64
+ export function codexLegacyPolicyDigest(text) {
65
+ const canonical = typeof text === 'string' ? text.replace(/\r\n/g, '\n').trim() : '';
66
+ return canonical ? createHash('sha256').update(canonical, 'utf8').digest('hex') : null;
67
+ }
68
+
69
+ export function stripKnownLegacyCodexPolicy(text, { knownHashes = CODEX_LEGACY_POLICY_HASHES } = {}) {
70
+ if (typeof text !== 'string') return text;
71
+ const start = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
72
+ const end = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
73
+ const match = new RegExp(`${start}[\\s\\S]*?${end}`, 'm').exec(text);
74
+ if (!match) return text;
75
+ const allowed = new Set(Array.isArray(knownHashes) ? knownHashes : []);
76
+ const prefix = text.slice(0, match.index);
77
+ const suffix = text.slice(match.index + match[0].length);
78
+ const keptPrefix = allowed.has(codexLegacyPolicyDigest(prefix)) ? '' : prefix;
79
+ const keptSuffix = allowed.has(codexLegacyPolicyDigest(suffix)) ? '' : suffix;
80
+ return `${keptPrefix}${match[0]}${keptSuffix}`;
81
+ }
82
+
45
83
  function installGlobalCodexPolicy({ home, root }) {
46
84
  const file = join(home, '.codex', 'AGENTS.md');
47
85
  const block = managedPolicyBlock(root);
@@ -54,6 +92,7 @@ function installGlobalCodexPolicy({ home, root }) {
54
92
  if (managed.test(current)) next = current.replace(managed, block);
55
93
  else if (current.trim() === template) next = `${block}\n`;
56
94
  else next = `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
95
+ next = stripKnownLegacyCodexPolicy(next);
57
96
  if (next !== current) {
58
97
  backup(file);
59
98
  writeFileSync(file, next);
@@ -125,17 +164,61 @@ function isFile(file) {
125
164
  }
126
165
 
127
166
  function claudePluginRootReady(root) {
128
- return typeof root === 'string' && root.trim() !== ''
129
- && isFile(join(root, '.claude-plugin', 'plugin.json'))
130
- && isFile(join(root, '.mcp.json'))
131
- && isFile(join(root, 'src', 'server.mjs'));
167
+ if (typeof root !== 'string' || !root.trim() || !isFile(join(root, 'src', 'server.mjs'))) return false;
168
+ const plugin = readJson(join(root, '.claude-plugin', 'plugin.json'), null);
169
+ const mcp = plugin?.mcpServers?.['dsh-crew'];
170
+ return !!plugin
171
+ && typeof plugin.version === 'string'
172
+ && mcp?.command === 'node'
173
+ && Array.isArray(mcp.args)
174
+ && mcp.args.length === 1
175
+ && mcp.args[0] === '${CLAUDE_PLUGIN_ROOT}/src/server.mjs';
132
176
  }
133
177
 
134
- function claudeSnapshotReady(home) {
178
+ function managedClaudeFileManifest(root, { maxFiles = 512, maxBytes = 8 * 1024 * 1024 } = {}) {
179
+ const files = [];
180
+ let bytes = 0;
181
+ const add = (file, relativePath) => {
182
+ const info = lstatSync(file);
183
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error('unsupported snapshot entry');
184
+ const content = readFileSync(file);
185
+ bytes += content.length;
186
+ if (bytes > maxBytes || files.length >= maxFiles) throw new Error('snapshot manifest bound exceeded');
187
+ files.push([relativePath.replace(/\\/g, '/'), createHash('sha256').update(content).digest('hex')]);
188
+ };
189
+ const walk = (directory, relativeDirectory) => {
190
+ if (!existsSync(directory)) return;
191
+ const info = lstatSync(directory);
192
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new Error('unsupported snapshot directory');
193
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
194
+ const file = join(directory, entry.name);
195
+ const relativePath = join(relativeDirectory, entry.name);
196
+ if (entry.isSymbolicLink()) throw new Error('snapshot symlink not allowed');
197
+ if (entry.isDirectory()) walk(file, relativePath);
198
+ else if (entry.isFile()) add(file, relativePath);
199
+ else throw new Error('unsupported snapshot entry');
200
+ }
201
+ };
202
+ try {
203
+ add(join(root, '.claude-plugin', 'plugin.json'), join('.claude-plugin', 'plugin.json'));
204
+ if (isFile(join(root, 'package.json'))) add(join(root, 'package.json'), 'package.json');
205
+ for (const directory of ['agents', 'commands', 'src']) walk(join(root, directory), directory);
206
+ return files;
207
+ } catch { return null; }
208
+ }
209
+
210
+ function sameManagedClaudeFiles(expectedRoot, snapshotRoot) {
211
+ if (!claudePluginRootReady(expectedRoot) || !claudePluginRootReady(snapshotRoot)) return false;
212
+ const expected = managedClaudeFileManifest(expectedRoot);
213
+ const snapshot = managedClaudeFileManifest(snapshotRoot);
214
+ return expected !== null && snapshot !== null && JSON.stringify(snapshot) === JSON.stringify(expected);
215
+ }
216
+
217
+ function claudeSnapshotReady(home, root) {
135
218
  const installed = readJson(join(home, '.claude', 'plugins', 'installed_plugins.json'), {});
136
219
  const record = installed?.plugins?.[PLUGIN_KEY];
137
220
  const entries = Array.isArray(record) ? record : [record];
138
- return entries.some((entry) => claudePluginRootReady(entry?.installPath));
221
+ return entries.some((entry) => sameManagedClaudeFiles(root, entry?.installPath));
139
222
  }
140
223
 
141
224
  function claudePermissionsReady(settings) {
@@ -302,33 +385,43 @@ export function writeGlobalConfig(patch) {
302
385
  }
303
386
 
304
387
  /** What is currently installed where — drives the settings-page buttons. */
305
- export function installStatus({ home = homedir(), root } = {}) {
388
+ export function installStatus({ home = homedir(), root = ROOT } = {}) {
306
389
  const settings = readJson(join(home, '.claude', 'settings.json'), {});
307
390
  const enabled = settings.enabledPlugins;
308
391
  const claudeInstalled = !!(enabled && !Array.isArray(enabled) && enabled[PLUGIN_KEY]);
309
392
  const hudWired = typeof settings.statusLine?.command === 'string'
310
393
  && settings.statusLine.command.includes('worker-segment.sh');
394
+ const marketplaceRoot = settings?.extraKnownMarketplaces?.[MARKETPLACE_NAME]?.source?.path;
395
+ const installedPluginRecord = readJson(join(home, '.claude', 'plugins', 'installed_plugins.json'), {})?.plugins?.[PLUGIN_KEY];
396
+ const claudeFootprint = claudeInstalled || typeof marketplaceRoot === 'string' || installedPluginRecord !== undefined;
311
397
  const claudeComponents = {
312
398
  enabled: claudeInstalled,
313
- marketplace: claudePluginRootReady(settings?.extraKnownMarketplaces?.[MARKETPLACE_NAME]?.source?.path),
314
- snapshot: claudeSnapshotReady(home),
399
+ marketplace: normalizedPath(marketplaceRoot) === normalizedPath(root) && claudePluginRootReady(root),
400
+ snapshot: claudeSnapshotReady(home, root),
315
401
  permissions: claudePermissionsReady(settings),
316
402
  };
317
403
  const claudeMissing = Object.entries(claudeComponents).filter(([, present]) => !present).map(([key]) => key);
318
404
  const codexRoot = join(home, '.codex');
319
405
  const configFile = join(codexRoot, 'config.toml');
320
406
  const configText = readText(configFile) ?? '';
321
- const workerTarget = codexRoleTarget(join(codexRoot, 'agents', 'ds-worker.toml'), 'ds-worker');
322
- const reviewerTarget = codexRoleTarget(join(codexRoot, 'agents', 'ds-reviewer.toml'), 'ds-reviewer');
407
+ const workerFile = join(codexRoot, 'agents', 'ds-worker.toml');
408
+ const reviewerFile = join(codexRoot, 'agents', 'ds-reviewer.toml');
409
+ const workerTarget = codexRoleTarget(workerFile, 'ds-worker');
410
+ const reviewerTarget = codexRoleTarget(reviewerFile, 'ds-reviewer');
323
411
  const mcpTarget = codexMcpTarget(configText);
412
+ const expectedTarget = normalizedPath(join(root, 'src', 'server.mjs'));
413
+ const expectedWorkerRole = renderedCodexRole(root, 'ds-worker.toml');
414
+ const expectedReviewerRole = renderedCodexRole(root, 'ds-reviewer.toml');
415
+ const expectedConfigPrompt = readText(join(root, 'codex', 'prompts', 'dsh-config.md'));
416
+ const expectedStatusPrompt = readText(join(root, 'codex', 'prompts', 'dsh-status.md'));
324
417
  const components = {
325
- worker_role: !!workerTarget,
326
- reviewer_role: !!reviewerTarget,
327
- config_prompt: !!readText(join(codexRoot, 'prompts', 'dsh-config.md'))?.trim(),
328
- status_prompt: !!readText(join(codexRoot, 'prompts', 'dsh-status.md'))?.trim(),
329
- mcp: !!mcpTarget,
330
- target_alignment: !!workerTarget && workerTarget === reviewerTarget && workerTarget === mcpTarget,
331
- global_policy: globalCodexPolicyReady({ home }),
418
+ worker_role: expectedWorkerRole !== null && readText(workerFile) === expectedWorkerRole,
419
+ reviewer_role: expectedReviewerRole !== null && readText(reviewerFile) === expectedReviewerRole,
420
+ config_prompt: expectedConfigPrompt !== null && readText(join(codexRoot, 'prompts', 'dsh-config.md')) === expectedConfigPrompt,
421
+ status_prompt: expectedStatusPrompt !== null && readText(join(codexRoot, 'prompts', 'dsh-status.md')) === expectedStatusPrompt,
422
+ mcp: !!expectedTarget && mcpTarget === expectedTarget,
423
+ target_alignment: !!expectedTarget && workerTarget === expectedTarget && reviewerTarget === expectedTarget && mcpTarget === expectedTarget,
424
+ global_policy: globalCodexPolicyReady({ home, root }),
332
425
  };
333
426
  const codexInstalled = Object.values(components).some(Boolean)
334
427
  || existsSync(join(codexRoot, 'agents', 'ds-flash.toml'))
@@ -336,14 +429,14 @@ export function installStatus({ home = homedir(), root } = {}) {
336
429
  const missing = Object.entries(components).filter(([, present]) => !present).map(([key]) => key);
337
430
  return {
338
431
  claude: {
339
- installed: claudeInstalled,
432
+ installed: claudeFootprint,
340
433
  ready: claudeMissing.length === 0,
341
434
  hud: hudWired,
342
435
  components: claudeComponents,
343
436
  missing: claudeMissing,
344
437
  },
345
438
  codex: { installed: codexInstalled, ready: missing.length === 0, components, missing },
346
- zcode: zcodeStatus({ home, ...(root ? { root } : {}) }),
439
+ zcode: zcodeStatus({ home, root }),
347
440
  };
348
441
  }
349
442
 
@@ -450,6 +543,17 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
450
543
  // in the plugin cache (settings alone leave a stale/absent cache entry) and
451
544
  // pulls the plugin so the next session loads it. Best-effort: settings are
452
545
  // already correct, so a missing CLI just means one manual `plugin install`.
546
+ const registered = readJson(join(home, '.claude', 'plugins', 'known_marketplaces.json'), {})?.[MARKETPLACE_NAME];
547
+ const installedRecord = readJson(join(home, '.claude', 'plugins', 'installed_plugins.json'), {})?.plugins?.[PLUGIN_KEY];
548
+ const installedEntries = Array.isArray(installedRecord) ? installedRecord : [installedRecord];
549
+ const marketplaceCurrent = registered?.source?.source === 'directory'
550
+ && normalizedPath(registered.source.path) === normalizedPath(root)
551
+ && normalizedPath(registered.installLocation) === normalizedPath(root);
552
+ if (marketplaceCurrent && installedEntries.some((entry) => entry?.scope === 'user'
553
+ && sameManagedClaudeFiles(root, entry.installPath))) {
554
+ actions.push('cli: skipped (registered marketplace and snapshot already current)');
555
+ return { ok: true, actions };
556
+ }
453
557
  if (home !== homedir()) {
454
558
  actions.push('cli: skipped (non-default home; test mode)');
455
559
  return { ok: true, actions };
@@ -493,8 +597,7 @@ export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
493
597
  const dest = join(agentsDir, f);
494
598
  const bak = backup(dest);
495
599
  if (bak) actions.push(`backup: ${bak}`);
496
- const rendered = readFileSync(join(srcDir, f), 'utf8')
497
- .replace(/args = \[.*server\.mjs"\]/, `args = ["${renderedPath}"]`);
600
+ const rendered = renderedCodexRole(root, f);
498
601
  writeFileSync(dest, rendered);
499
602
  actions.push(`role: ${dest}`);
500
603
  }