@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.
package/package.json
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
}
|
package/src/install/zcode.mjs
CHANGED
|
@@ -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
|
-
|
|
291
|
-
|
|
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,
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
|
349
|
-
&&
|
|
350
|
-
&&
|
|
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']
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -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.
|
|
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([
|