@ran-sh/dsh-crew 1.6.2 → 1.7.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crew",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Dispatch subtasks to DeepSeek Harness (DSH) agents as native subagents with live progress",
5
5
  "author": {
6
6
  "name": "ZSeven-W"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -0,0 +1,91 @@
1
+ // On-demand Crew guidance, installed as a user skill.
2
+ //
3
+ // Crew used to inject a "capability-aware delegation policy" block into the
4
+ // host's global instruction file. That made every host session — Codex, ZCode —
5
+ // carry Crew's delegation rules whether or not the operator wanted Crew that
6
+ // turn, which shapes how the host does its own work.
7
+ //
8
+ // The guidance now lives in a skill instead, loaded only when the operator asks
9
+ // for Crew. Nothing about the host's default behaviour changes; the knowledge is
10
+ // there the moment it is wanted.
11
+ //
12
+ // Each host reads its own skill directory, so the skill is copied to all of them
13
+ // — the same layout the Oracle skill uses, plus the shared `~/.agents/skills`
14
+ // that ZCode reads. Installing a skill replaces writing a policy block, so the
15
+ // host installers also *remove* any block a previous release left behind.
16
+
17
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { dirname, join, resolve } from 'node:path';
20
+
21
+ export const CREW_SKILL_NAME = 'dsh-crew';
22
+
23
+ /**
24
+ * Skill roots, in the order they are installed. `.agents/skills` is the shared
25
+ * directory ZCode reads; the other two are what Codex and Claude Code read. The
26
+ * Codex one is resolved through `CODEX_HOME` because Codex may point elsewhere —
27
+ // writing only to `~/.codex` would put the skill where Codex never looks.
28
+ */
29
+ export function crewSkillRoots({ home = homedir(), env = process.env } = {}) {
30
+ const codexHome = typeof env?.CODEX_HOME === 'string' && env.CODEX_HOME.trim()
31
+ ? resolve(env.CODEX_HOME.trim())
32
+ : join(home, '.codex');
33
+ return [
34
+ join(home, '.agents', 'skills'),
35
+ join(codexHome, 'skills'),
36
+ join(home, '.claude', 'skills'),
37
+ ];
38
+ }
39
+
40
+ export function crewSkillFiles({ home = homedir(), env = process.env } = {}) {
41
+ return crewSkillRoots({ home, env }).map((root) => join(root, CREW_SKILL_NAME, 'SKILL.md'));
42
+ }
43
+
44
+ /** The skill template shipped in the payload. */
45
+ export function crewSkillSource({ root }) {
46
+ return join(root, 'skills', CREW_SKILL_NAME, 'SKILL.md');
47
+ }
48
+
49
+ export function readCrewSkill({ root }) {
50
+ const file = crewSkillSource({ root });
51
+ return existsSync(file) ? readFileSync(file, 'utf8') : null;
52
+ }
53
+
54
+ /**
55
+ * Copy the skill template into every host skill directory. Idempotent: a file
56
+ * that already matches is left alone, so a repeat install stays quiet.
57
+ */
58
+ export function installCrewSkill({ home = homedir(), root, env = process.env } = {}) {
59
+ if (!root) return { ok: false, code: 'CREW_SKILL_SOURCE_REQUIRED' };
60
+ const content = readCrewSkill({ root });
61
+ if (content === null) return { ok: false, code: 'CREW_SKILL_TEMPLATE_MISSING', source: crewSkillSource({ root }) };
62
+ const written = [];
63
+ const files = crewSkillFiles({ home, env });
64
+ for (const file of files) {
65
+ let current = null;
66
+ try { current = readFileSync(file, 'utf8'); } catch { /* absent or unreadable */ }
67
+ if (current === content) continue;
68
+ try {
69
+ mkdirSync(dirname(file), { recursive: true });
70
+ writeFileSync(file, content);
71
+ written.push(file);
72
+ } catch (error) {
73
+ return { ok: false, code: 'CREW_SKILL_WRITE_FAILED', file, error: String(error?.message ?? error) };
74
+ }
75
+ }
76
+ return { ok: true, changed: written.length > 0, written, files, file: files[0] };
77
+ }
78
+
79
+ /**
80
+ * Remove the installed skill from every host directory. Only Crew's own skill
81
+ * folder is touched, so a user's other skills are never affected.
82
+ */
83
+ export function removeCrewSkill({ home = homedir(), env = process.env } = {}) {
84
+ const removed = [];
85
+ for (const dir of crewSkillRoots({ home, env }).map((root) => join(root, CREW_SKILL_NAME))) {
86
+ if (!existsSync(dir)) continue;
87
+ try { rmSync(dir, { recursive: true, force: true }); removed.push(dir); }
88
+ catch { /* best effort: a locked file is not worth failing an uninstall */ }
89
+ }
90
+ return { ok: true, changed: removed.length > 0, removed, file: removed[0] ?? null };
91
+ }
@@ -7,7 +7,8 @@ import { createHash } from 'node:crypto';
7
7
  import { dirname, join, resolve, relative } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { homedir } from 'node:os';
10
- import { normalizeModelPriority } from '../model-routing.mjs';
10
+ import { normalizeModelPriority } from '../model-routing.mjs';
11
+ import { crewSkillFiles, installCrewSkill, readCrewSkill, removeCrewSkill } from './crew-skill.mjs';
11
12
  import { zcodeStatus } from './zcode.mjs';
12
13
 
13
14
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
@@ -94,32 +95,13 @@ export function stripKnownLegacyCodexPolicy(text, { knownHashes = CODEX_LEGACY_P
94
95
  return `${keptPrefix}${match[0]}${keptSuffix}`;
95
96
  }
96
97
 
97
- function installGlobalCodexPolicy({ home, root, env = process.env }) {
98
- const file = join(codexHomeDir(home, env), 'AGENTS.md');
99
- const block = managedPolicyBlock(root);
100
- if (!block) return { ok: false, action: 'global policy template missing' };
101
- mkdirSync(dirname(file), { recursive: true });
102
- const current = readText(file) ?? '';
103
- const managed = new RegExp(`${POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'm');
104
- const template = readText(join(root, 'codex', 'AGENTS.md'))?.trim() ?? '';
105
- let next;
106
- if (managed.test(current)) next = current.replace(managed, block);
107
- else if (current.trim() === template) next = `${block}\n`;
108
- else next = `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
109
- next = stripKnownLegacyCodexPolicy(next);
110
- if (next !== current) {
111
- backup(file);
112
- writeFileSync(file, next);
113
- }
114
- return { ok: true, action: `global policy: ${file}` };
98
+ /** The installed skill is present and still matches the payload template. */
99
+ function crewSkillInstalled({ home, root, env = process.env }) {
100
+ const expected = readCrewSkill({ root });
101
+ if (expected === null) return false;
102
+ // Every host directory must hold the current template, not just the first.
103
+ return crewSkillFiles({ home, env }).every((file) => readText(file) === expected);
115
104
  }
116
-
117
- function globalCodexPolicyReady({ home, root = ROOT, env = process.env }) {
118
- const block = managedPolicyBlock(root);
119
- const text = readText(join(codexHomeDir(home, env), 'AGENTS.md'));
120
- return !!block && typeof text === 'string' && text.includes(block);
121
- }
122
-
123
105
  function uninstallGlobalCodexPolicy({ home, env = process.env }) {
124
106
  const file = join(codexHomeDir(home, env), 'AGENTS.md');
125
107
  const current = readText(file);
@@ -432,7 +414,7 @@ export function installStatus({ home = homedir(), root = ROOT, env = process.env
432
414
  status_prompt: expectedStatusPrompt !== null && readText(join(codexRoot, 'prompts', 'dsh-status.md')) === expectedStatusPrompt,
433
415
  mcp: !!expectedTarget && mcpTarget === expectedTarget,
434
416
  target_alignment: !!expectedTarget && workerTarget === expectedTarget && reviewerTarget === expectedTarget && mcpTarget === expectedTarget,
435
- global_policy: globalCodexPolicyReady({ home, root, env }),
417
+ skill: crewSkillInstalled({ home, root, env }),
436
418
  };
437
419
  const codexInstalled = Object.values(components).some(Boolean)
438
420
  || existsSync(join(codexRoot, 'agents', 'ds-flash.toml'))
@@ -466,6 +448,8 @@ export function uninstallCodex({ home = homedir(), env = process.env } = {}) {
466
448
  }
467
449
  const policyAction = uninstallGlobalCodexPolicy({ home, env });
468
450
  if (policyAction) actions.push(policyAction);
451
+ const skillAction = removeCrewSkill({ home, env });
452
+ if (skillAction.changed) actions.push(...skillAction.removed.map((dir) => `removed: ${dir}`));
469
453
  // Remove only the dsh-crew entry from [mcp_servers], keeping any other
470
454
  // MCP servers the user configured.
471
455
  const configFile = join(codexHomeDir(home, env), 'config.toml');
@@ -622,9 +606,15 @@ export function installCodex({ home = homedir(), scope, root = ROOT, env = proce
622
606
  if (scope !== 'project') {
623
607
  const act = writeGlobalCodexMcpServer(home, renderedPath, env);
624
608
  actions.push(...act);
625
- const policy = installGlobalCodexPolicy({ home, root, env });
626
- if (!policy.ok) return { ok: false, actions: [...actions, policy.action] };
627
- actions.push(policy.action);
609
+ // Crew guidance is a skill now, loaded only when the operator asks for it.
610
+ // Removing the policy block here cleans up what an earlier release put in
611
+ // the host instruction file; ignoring the result keeps the first install on
612
+ // a machine that never had one quiet.
613
+ const droppedPolicy = uninstallGlobalCodexPolicy({ home, env });
614
+ if (droppedPolicy) actions.push(`removed delegating policy block: ${join(codexHomeDir(home, env), 'AGENTS.md')}`);
615
+ const skill = installCrewSkill({ home, root, env });
616
+ if (!skill.ok) return { ok: false, actions: [...actions, `crew skill: ${skill.code}`] };
617
+ actions.push(...skill.written.map((file) => `skill: ${file}`));
628
618
  }
629
619
  return { ok: true, actions };
630
620
  }
@@ -9,6 +9,8 @@ import { dirname, join, resolve } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { homedir } from 'node:os';
11
11
 
12
+ import { crewSkillFiles, installCrewSkill, readCrewSkill, removeCrewSkill } from './crew-skill.mjs';
13
+
12
14
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
13
15
  const HOST = 'zcode';
14
16
  const SERVER = 'dsh-crew';
@@ -103,27 +105,6 @@ function expectedTarget({ root = ROOT } = {}) {
103
105
  return resolve(join(root, 'src', 'server.mjs'));
104
106
  }
105
107
 
106
- function managedPolicyBlock(root) {
107
- const template = readText(join(root, 'zcode', 'AGENTS.md'))?.trim();
108
- return template ? `${POLICY_START}\n${template}\n${POLICY_END}` : null;
109
- }
110
-
111
- function installPolicy({ home, root }) {
112
- const file = join(home, '.zcode', 'AGENTS.md');
113
- const block = managedPolicyBlock(root);
114
- if (!block) return { ok: false, code: 'ZCODE_POLICY_TEMPLATE_MISSING' };
115
- mkdirSync(dirname(file), { recursive: true });
116
- const current = readText(file) ?? '';
117
- const escapedStart = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
118
- const escapedEnd = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
119
- const managed = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`, 'm');
120
- const next = managed.test(current)
121
- ? current.replace(managed, block)
122
- : `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
123
- if (next !== current) { backup(file); writeFileSync(file, next); }
124
- return { ok: true, file };
125
- }
126
-
127
108
  function removePolicy({ home }) {
128
109
  const file = join(home, '.zcode', 'AGENTS.md');
129
110
  const current = readText(file);
@@ -277,7 +258,8 @@ function updateMcp({ home, root }) {
277
258
  return { ok: true, changed, config_file: source.file, config_kind: source.kind, target };
278
259
  }
279
260
 
280
- export function installZCode({ home = homedir(), root = ROOT } = {}) {
261
+ export function installZCode({ home = homedir(), root = ROOT, env = process.env } = {}) {
262
+ const actions = [];
281
263
  const templates = templateFiles({ home, root });
282
264
  const missing = templates.find(({ source }) => !existsSync(source));
283
265
  if (missing) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source: missing.source };
@@ -287,8 +269,14 @@ export function installZCode({ home = homedir(), root = ROOT } = {}) {
287
269
  // failed install is transaction-like and leaves user files untouched.
288
270
  const mcp = updateMcp({ home, root });
289
271
  if (!mcp.ok) return mcp;
290
- const policy = installPolicy({ home, root });
291
- if (!policy.ok) return policy;
272
+ // Crew guidance is a skill now, loaded only when the operator asks for it.
273
+ // The removal cleans up the policy block an earlier release wrote into the
274
+ // host instruction file; ignoring its result keeps a first install quiet.
275
+ const droppedPolicy = removePolicy({ home });
276
+ if (droppedPolicy) actions.push('policy: removed managed ZCode block');
277
+ const skill = installCrewSkill({ home, root, env });
278
+ if (!skill.ok) return { ok: false, code: skill.code, source: skill.source };
279
+ actions.push(...skill.written.map((file) => `skill: ${file}`));
292
280
  const installed = installTemplates({ home, root, priorFiles });
293
281
  if (!installed.ok) return installed;
294
282
  cleanupPriorMcpSources({ owned: previousOwnership, exceptFile: mcp.config_file });
@@ -303,16 +291,20 @@ export function installZCode({ home = homedir(), root = ROOT } = {}) {
303
291
  files: installed.records,
304
292
  configFiles: [currentSourceRecord, ...priorSourceRecords],
305
293
  });
306
- return { ok: true, ...mcp, policy_file: policy.file, files: installed.actions };
294
+ return { ok: true, ...mcp, skill_files: skill.written, files: installed.actions };
307
295
  }
308
296
 
309
- function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
297
+ function zcodeComponents({ home = homedir(), root = ROOT, env = process.env } = {}) {
310
298
  const expected = normalizePath(expectedTarget({ root }));
311
299
  const source = configSource({ home });
312
300
  const configured = serverTarget(source.servers[SERVER]);
313
301
  const owned = ownership({ home });
314
- const expectedPolicy = managedPolicyBlock(root);
315
- const installedPolicy = readText(join(home, '.zcode', 'AGENTS.md'));
302
+ // The guidance is a skill now; a policy block in the host instruction file is
303
+ // no longer written and no longer part of readiness.
304
+ const expectedSkill = readCrewSkill({ root });
305
+ // A block written by an earlier release still counts as an installed
306
+ // footprint, so uninstall knows there is something of ours to clean up.
307
+ const legacyPolicyText = readText(join(home, '.zcode', 'AGENTS.md'));
316
308
  const templates = templateFiles({ home, root });
317
309
  const templateReady = (entry) => {
318
310
  if (!entry) return false;
@@ -335,7 +327,7 @@ function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
335
327
  });
336
328
  const components = {
337
329
  mcp: configured === expected,
338
- policy: !!expectedPolicy && typeof installedPolicy === 'string' && installedPolicy.includes(expectedPolicy),
330
+ skill: expectedSkill !== null && readText(crewSkillFiles({ home, env })[0]) === expectedSkill,
339
331
  worker_agent: templateReady(templateBySuffix(join('agents', 'ds-worker.md'))),
340
332
  reviewer_agent: templateReady(templateBySuffix(join('agents', 'ds-reviewer.md'))),
341
333
  config_prompt: templateReady(templateBySuffix(join('commands', 'dsh-config.md'))),
@@ -345,9 +337,9 @@ function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
345
337
  && normalizePath(owned.target) === expected
346
338
  && ownershipFilesReady,
347
339
  };
348
- const policyFootprint = typeof installedPolicy === 'string'
349
- && installedPolicy.includes(POLICY_START)
350
- && installedPolicy.includes(POLICY_END);
340
+ const policyFootprint = typeof legacyPolicyText === 'string'
341
+ && legacyPolicyText.includes(POLICY_START)
342
+ && legacyPolicyText.includes(POLICY_END);
351
343
  const installed = Object.hasOwn(source.servers, SERVER)
352
344
  || !!owned
353
345
  || policyFootprint
@@ -355,8 +347,8 @@ function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
355
347
  return { components, source, expected, configured, owned, installed };
356
348
  }
357
349
 
358
- export function zcodeStatus({ home = homedir(), root = ROOT } = {}) {
359
- const { components, source, expected, configured, installed } = zcodeComponents({ home, root });
350
+ export function zcodeStatus({ home = homedir(), root = ROOT, env = process.env } = {}) {
351
+ const { components, source, expected, configured, installed } = zcodeComponents({ home, root, env });
360
352
  const missing = Object.entries(components).filter(([, value]) => !value).map(([key]) => key);
361
353
  return {
362
354
  installed,
@@ -369,7 +361,7 @@ export function zcodeStatus({ home = homedir(), root = ROOT } = {}) {
369
361
  };
370
362
  }
371
363
 
372
- export function uninstallZCode({ home = homedir() } = {}) {
364
+ export function uninstallZCode({ home = homedir(), env = process.env } = {}) {
373
365
  const actions = [];
374
366
  const owned = ownership({ home });
375
367
  if (owned) {
@@ -381,6 +373,8 @@ export function uninstallZCode({ home = homedir() } = {}) {
381
373
  rmSync(OWNERSHIP_FILE({ home }), { force: true });
382
374
  }
383
375
  if (removePolicy({ home })) actions.push('policy: removed managed ZCode block');
376
+ const skillAction = removeCrewSkill({ home, env });
377
+ if (skillAction.changed) actions.push(...skillAction.removed.map((dir) => `removed: ${dir}`));
384
378
  const managedFiles = Array.isArray(owned?.files) && owned.files.length
385
379
  ? owned.files
386
380
  : ['agents/ds-worker.md', 'agents/ds-reviewer.md', 'commands/dsh-config.md', 'commands/dsh-status.md']
@@ -36,7 +36,7 @@ export {
36
36
  // included in the identity contract.
37
37
  const RUNTIME_ID = randomUUID();
38
38
 
39
- export const RUNTIME_VERSION = '1.6.2';
39
+ export const RUNTIME_VERSION = '1.7.0';
40
40
  export const HUB_PROTOCOL_VERSION = 1;
41
41
 
42
42
  export const HUB_CAPABILITIES = Object.freeze([