devlyn-cli 4.2.25 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codex-projection.js +129 -0
- package/bin/devlyn.js +522 -0
- package/bin/engines.js +40 -0
- package/bin/harness-activation.js +426 -0
- package/bin/harness-status.js +171 -0
- package/bin/instructions.js +13 -3
- package/bin/release.js +381 -0
- package/bin/skill-history.json +5 -0
- package/config/skills/_shared/adapters/omp.md +3 -0
- package/config/skills/_shared/harness-store.py +201 -0
- package/config/skills/_shared/runtime-principles.md +12 -2
- package/config/skills/_shared/warm-build-dirs.py +154 -83
- package/config/skills/devlyn-ideate/SKILL.md +5 -1
- package/config/skills/devlyn-ideate/references/loop.md +12 -2
- package/config/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +5 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +156 -15
- package/package.json +8 -1
package/bin/engines.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Engine IDs from devlyn-os-v1/packages/contracts/src/engines.ts, in inventory order.
|
|
2
|
+
// CLI binary names (for example agy) are not engine IDs.
|
|
3
|
+
const ENGINE_IDS = Object.freeze([
|
|
4
|
+
'claude', 'codex', 'grok', 'omp', 'antigravity', 'cursor', 'opencode', 'kimi', 'copilot', 'qwen',
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
// The versions each channel was examined on. Newer releases of the same major are taken as
|
|
8
|
+
// compatible: these CLIs ship minor releases weekly, and gating on the examined minor would turn
|
|
9
|
+
// the harness off at every one of them. A new major (or a Cursor build older than the examined one)
|
|
10
|
+
// is refused, since any flag may change there.
|
|
11
|
+
const ENGINE_CAPABILITIES = Object.freeze({
|
|
12
|
+
claude: { minimum: '2.1.307', executor: true },
|
|
13
|
+
codex: { minimum: '0.163.0', executor: true },
|
|
14
|
+
grok: { minimum: '1.0.62', executor: false },
|
|
15
|
+
omp: { minimum: '16.5.0', executor: false },
|
|
16
|
+
antigravity: { minimum: '1.4.4', executor: false },
|
|
17
|
+
cursor: { minimum: '2025.09.12-4852336', executor: false },
|
|
18
|
+
opencode: { minimum: '1.19.2', executor: false },
|
|
19
|
+
kimi: { minimum: '2.3.0', executor: false },
|
|
20
|
+
copilot: { minimum: '1.0.98', executor: false },
|
|
21
|
+
qwen: { minimum: '0.26.0', executor: false },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function supportedVersion(engine, version) {
|
|
25
|
+
const capability = ENGINE_CAPABILITIES[engine];
|
|
26
|
+
if (engine === 'cursor') {
|
|
27
|
+
const build = typeof version === 'string' && version.match(/^(\d{4})\.(\d{2})\.(\d{2})-[0-9a-f]+$/);
|
|
28
|
+
return Boolean(build) && build.slice(1).join('') >= capability.minimum.slice(0, 10).replace(/\./g, '');
|
|
29
|
+
}
|
|
30
|
+
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) return false;
|
|
31
|
+
const actual = version.split('.').map(Number);
|
|
32
|
+
const floor = capability.minimum.split('.').map(Number);
|
|
33
|
+
if (actual[0] !== floor[0]) return false;
|
|
34
|
+
for (let i = 1; i < 3; i++) {
|
|
35
|
+
if (actual[i] !== floor[i]) return actual[i] > floor[i];
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { ENGINE_IDS, ENGINE_CAPABILITIES, supportedVersion };
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { ENGINE_IDS, ENGINE_CAPABILITIES, supportedVersion } = require('./engines');
|
|
5
|
+
const { verifyRelease, sha256, fail, options, HarnessError } = require('./release');
|
|
6
|
+
const { fingerprint } = require('./skill-ownership');
|
|
7
|
+
const history = require('./skill-history.json');
|
|
8
|
+
const { MARKER, marketplaceName, projectionPath, inspectProjection } = require('./codex-projection');
|
|
9
|
+
|
|
10
|
+
const SKILLS = ['devlyn-ideate', 'devlyn-engines'];
|
|
11
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
+
const textValue = (value) => typeof value === 'string' && !value.includes('\0');
|
|
13
|
+
const strings = (value) => Array.isArray(value) && value.every(textValue);
|
|
14
|
+
const reason = (code, params = {}) => ({ code, params });
|
|
15
|
+
const channel = (state = 'disabled', mechanism = null, reasons = []) => ({ state, mechanism, reasons });
|
|
16
|
+
const joinText = (...parts) => parts.filter((part) => part !== null && part !== undefined && part !== '').join('\n\n');
|
|
17
|
+
|
|
18
|
+
// TOML basic strings share JSON escaping except JSON's optional slash escape.
|
|
19
|
+
// Also escape DEL, forbidden in TOML strings. No shell quoting is involved.
|
|
20
|
+
function toml(value) {
|
|
21
|
+
if (typeof value === 'string') return JSON.stringify(value).replace(/\x7f/g, '\\u007f');
|
|
22
|
+
if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) return String(value);
|
|
23
|
+
if (Array.isArray(value)) return `[${value.map(toml).join(',')}]`;
|
|
24
|
+
if (object(value)) return `{${Object.entries(value).map(([key, item]) => `${toml(key)}=${toml(item)}`).join(',')}}`;
|
|
25
|
+
fail('config-unreadable', { field: 'TOML override', detail: 'unsupported value' }, 2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateContext(context) {
|
|
29
|
+
const allowed = ['schemaVersion', 'engine', 'version', 'launchMode', 'cwd', 'runDir', 'projectChoice',
|
|
30
|
+
'committedBlockApplies', 'initialPrompt', 'accountRef', 'accountHome', 'effectiveConfig', 'skillRoots', 'recordedRelease'];
|
|
31
|
+
if (!object(context) || context.schemaVersion !== 1 || Object.keys(context).some((key) => !allowed.includes(key))
|
|
32
|
+
|| !ENGINE_IDS.includes(context.engine) || !(context.version === null || textValue(context.version))
|
|
33
|
+
|| !['fresh', 'resume', 'fork'].includes(context.launchMode)
|
|
34
|
+
|| !['default', 'devlyn', 'none'].includes(context.projectChoice)
|
|
35
|
+
|| typeof context.committedBlockApplies !== 'boolean'
|
|
36
|
+
|| ![context.cwd, context.runDir].every((p) => textValue(p) && path.isAbsolute(p))
|
|
37
|
+
|| !(context.initialPrompt === null || (object(context.initialPrompt) && textValue(context.initialPrompt.text)
|
|
38
|
+
&& ['argv', 'stdin'].includes(context.initialPrompt.transport)
|
|
39
|
+
&& Object.keys(context.initialPrompt).every((k) => ['text', 'transport'].includes(k))))
|
|
40
|
+
|| (context.accountRef !== undefined && context.accountRef !== null && !textValue(context.accountRef))
|
|
41
|
+
|| (context.accountHome !== undefined && (!textValue(context.accountHome) || !path.isAbsolute(context.accountHome)))
|
|
42
|
+
|| (context.skillRoots !== undefined && (!strings(context.skillRoots) || !context.skillRoots.every(path.isAbsolute)))) {
|
|
43
|
+
fail('invalid-arguments', { argument: 'activation context' }, 2);
|
|
44
|
+
}
|
|
45
|
+
if (context.recordedRelease !== undefined && context.recordedRelease !== null) {
|
|
46
|
+
const ref = context.recordedRelease;
|
|
47
|
+
if (!object(ref) || ref.package !== 'devlyn-cli' || !textValue(ref.version)
|
|
48
|
+
|| !/^[a-f0-9]{64}$/.test(ref.digest) || !textValue(ref.root) || !path.isAbsolute(ref.root)
|
|
49
|
+
|| Object.keys(ref).some((key) => !['package', 'version', 'digest', 'root'].includes(key))) {
|
|
50
|
+
fail('invalid-arguments', { argument: 'recordedRelease' }, 2);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return context;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readContext(file) {
|
|
57
|
+
if (file !== '-' && (!textValue(file) || !path.isAbsolute(file))) fail('invalid-arguments', { argument: 'context-file' }, 2);
|
|
58
|
+
try {
|
|
59
|
+
return validateContext(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(file === '-' ? 0 : file))));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof HarnessError) throw error;
|
|
62
|
+
fail('config-unreadable', { field: 'context-file', detail: error.message }, 2);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function statusFor(context) {
|
|
67
|
+
const supported = ENGINE_CAPABILITIES[context.engine].executor;
|
|
68
|
+
return {
|
|
69
|
+
engine: context.engine, version: context.version, accountRef: context.accountRef ?? null,
|
|
70
|
+
overall: 'disabled', principles: channel(), skills: channel(), hide: channel(), invocation: null,
|
|
71
|
+
executor: { supported, reasons: supported ? [] : [reason('executor-unqualified', { engine: context.engine })] },
|
|
72
|
+
inheritedDevlyn: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function finish(plan) {
|
|
77
|
+
const states = [plan.channels.principles.state, plan.channels.skills.state];
|
|
78
|
+
plan.channels.overall = states.every((s) => s === 'disabled') ? 'disabled'
|
|
79
|
+
: states.every((s) => s === 'configured') ? 'configured'
|
|
80
|
+
: states.every((s) => s === 'unavailable') ? 'unavailable' : 'partial';
|
|
81
|
+
if (plan.channels.overall === 'configured' && plan.channels.hide.state === 'partial') plan.channels.overall = 'partial';
|
|
82
|
+
return plan;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The app supplies only effective values used by this recipe. Null means the
|
|
86
|
+
// model-free config read failed; {} means it succeeded with no values set.
|
|
87
|
+
function effectiveConfig(context) {
|
|
88
|
+
const config = context.effectiveConfig;
|
|
89
|
+
const keys = {
|
|
90
|
+
claude: ['appendSystemPrompt', 'pluginDirs'], codex: ['developer_instructions', 'skills', 'marketplaces', 'plugins'],
|
|
91
|
+
grok: ['rules'], omp: ['appendSystemPrompt', 'skills'], antigravity: [], cursor: [],
|
|
92
|
+
opencode: ['inlineConfig'], kimi: ['ownerAgent'], copilot: ['skillsDirs'], qwen: ['appendSystemPrompt'],
|
|
93
|
+
}[context.engine];
|
|
94
|
+
if (!object(config) || Object.keys(config).some((key) => !keys.includes(key))) return null;
|
|
95
|
+
for (const field of ['appendSystemPrompt', 'developer_instructions', 'rules', 'inlineConfig', 'skillsDirs']) {
|
|
96
|
+
if (config[field] !== undefined && config[field] !== null && !textValue(config[field])) return null;
|
|
97
|
+
}
|
|
98
|
+
if (context.engine === 'kimi' && typeof config.ownerAgent !== 'boolean') return null;
|
|
99
|
+
if (config.pluginDirs !== undefined && (!strings(config.pluginDirs) || !config.pluginDirs.every(path.isAbsolute))) return null;
|
|
100
|
+
if (context.engine === 'codex') {
|
|
101
|
+
for (const key of ['marketplaces', 'plugins', 'skills']) if (config[key] != null && !object(config[key])) return null;
|
|
102
|
+
if (config.skills?.config !== undefined && (!Array.isArray(config.skills.config)
|
|
103
|
+
|| !config.skills.config.every((entry) => object(entry) && textValue(entry.path) && path.isAbsolute(entry.path)
|
|
104
|
+
&& typeof entry.enabled === 'boolean' && Object.keys(entry).every((key) => ['path', 'enabled'].includes(key))))) return null;
|
|
105
|
+
for (const entry of Object.values(config.plugins || {})) {
|
|
106
|
+
if (!object(entry) || typeof entry.enabled !== 'boolean') return null;
|
|
107
|
+
}
|
|
108
|
+
for (const entry of Object.values(config.marketplaces || {})) {
|
|
109
|
+
if (!object(entry) || !textValue(entry.source_type) || !textValue(entry.source)) return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (context.engine === 'omp') {
|
|
113
|
+
if (config.skills != null && !object(config.skills)) return null;
|
|
114
|
+
for (const key of ['customDirectories', 'ignoredSkills']) {
|
|
115
|
+
if (config.skills?.[key] !== undefined && !strings(config.skills[key])) return null;
|
|
116
|
+
}
|
|
117
|
+
if (config.skills?.enableSkillCommands !== undefined && typeof config.skills.enableSkillCommands !== 'boolean') return null;
|
|
118
|
+
}
|
|
119
|
+
return config;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function discoveryRoots(context, env, config) {
|
|
123
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
124
|
+
const roots = [...(context.skillRoots || [])];
|
|
125
|
+
const add = (base, ...parts) => { if (base) roots.push(path.resolve(base, ...parts)); };
|
|
126
|
+
const engine = context.engine;
|
|
127
|
+
const native = {
|
|
128
|
+
claude: [env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), '.claude'],
|
|
129
|
+
codex: [context.accountHome || env.CODEX_HOME || path.join(home, '.codex'), '.agents'],
|
|
130
|
+
grok: [env.GROK_HOME || path.join(home, '.grok'), '.grok'],
|
|
131
|
+
omp: [env.PI_CODING_AGENT_DIR || path.join(home, '.omp/agent'), '.omp'],
|
|
132
|
+
antigravity: [path.join(home, '.gemini/antigravity-cli'), '.agents'],
|
|
133
|
+
cursor: [null, null], opencode: [path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode'), '.opencode'],
|
|
134
|
+
kimi: [env.KIMI_CODE_HOME || path.join(home, '.kimi-code'), '.kimi-code'],
|
|
135
|
+
copilot: [env.COPILOT_HOME || path.join(home, '.copilot'), '.github'],
|
|
136
|
+
qwen: [env.QWEN_HOME || path.join(home, '.qwen'), '.qwen'],
|
|
137
|
+
}[engine];
|
|
138
|
+
if (engine !== 'codex') add(native[0], 'skills');
|
|
139
|
+
const copilotDefaultHome = !env.COPILOT_HOME || path.resolve(env.COPILOT_HOME) === path.join(home, '.copilot');
|
|
140
|
+
if (!['claude', 'cursor'].includes(engine) && (engine !== 'copilot' || copilotDefaultHome)) add(home, '.agents/skills');
|
|
141
|
+
if (['omp', 'opencode', 'grok'].includes(engine)) add(home, '.claude/skills');
|
|
142
|
+
if (engine === 'omp') {
|
|
143
|
+
add(home, '.codex/skills'); add(home, '.agent/skills');
|
|
144
|
+
for (const dir of config?.skills?.customDirectories || []) add(context.cwd, dir);
|
|
145
|
+
}
|
|
146
|
+
if (engine === 'codex' && process.platform !== 'win32') add('/etc/codex', 'skills');
|
|
147
|
+
if (engine === 'opencode') add(env.OPENCODE_CONFIG_DIR, 'skills');
|
|
148
|
+
if (engine === 'copilot') for (const dir of (config?.skillsDirs ?? env.COPILOT_SKILLS_DIRS ?? '').split(',').filter(Boolean)) add(context.cwd, dir);
|
|
149
|
+
for (let dir = path.resolve(context.cwd); ; dir = path.dirname(dir)) {
|
|
150
|
+
if (native[1]) add(dir, native[1], 'skills');
|
|
151
|
+
if (!['claude', 'cursor'].includes(engine)) add(dir, '.agents/skills');
|
|
152
|
+
if (['grok', 'omp', 'opencode', 'copilot'].includes(engine)) add(dir, '.claude/skills');
|
|
153
|
+
if (['omp', 'antigravity'].includes(engine)) add(dir, '.agent/skills');
|
|
154
|
+
if (fs.existsSync(path.join(dir, '.git')) || dir === path.dirname(dir)) break;
|
|
155
|
+
}
|
|
156
|
+
return [...new Set(roots)];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function inspectSkills(context, release, env, config) {
|
|
160
|
+
const found = [], errors = [], hashes = new Map();
|
|
161
|
+
for (const root of discoveryRoots(context, env, config)) {
|
|
162
|
+
for (const name of SKILLS) {
|
|
163
|
+
const dir = path.join(root, name);
|
|
164
|
+
try {
|
|
165
|
+
if (!fs.lstatSync(dir, { throwIfNoEntry: false })) continue;
|
|
166
|
+
const hash = fingerprint(dir);
|
|
167
|
+
if (!hashes.has(name)) hashes.set(name, fingerprint(path.join(release.root, 'config/skills', name)));
|
|
168
|
+
found.push({ name, path: path.join(dir, 'SKILL.md'), owned: hash === hashes.get(name) || (history.skills[name] || []).includes(hash) });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// An unreadable/linked candidate can collide, but never authorizes hide.
|
|
171
|
+
found.push({ name, path: path.join(dir, 'SKILL.md'), owned: false });
|
|
172
|
+
errors.push(reason('config-unreadable', { path: dir, detail: error.message }));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { found, errors };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function skillCatalog(release, paths = path) {
|
|
180
|
+
const root = paths.join(release.root, 'config', 'skills');
|
|
181
|
+
return ['Devlyn session skill catalog (read the absolute file to invoke):',
|
|
182
|
+
...SKILLS.map((name) => `${name}: ${paths.join(root, name, 'SKILL.md')}`),
|
|
183
|
+
`This session's _shared folder is ${paths.join(root, '_shared')}.`,
|
|
184
|
+
'Use that absolute folder wherever the Devlyn defaults name _shared/, including the Quick Start references to .claude/skills/ or .agents/skills/.',
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function codexRegistrations(config, accountHome) {
|
|
189
|
+
const owned = [], errors = [];
|
|
190
|
+
for (const [key, plugin] of Object.entries(config.plugins || {})) {
|
|
191
|
+
if (!plugin.enabled || !key.startsWith('devlyn@')) continue;
|
|
192
|
+
const market = key.slice('devlyn@'.length), source = config.marketplaces?.[market];
|
|
193
|
+
if (source?.source_type !== 'local' || !path.isAbsolute(source.source)) continue;
|
|
194
|
+
const root = path.resolve(source.source, '../..');
|
|
195
|
+
if (path.join(root, 'native/codex') !== source.source) continue;
|
|
196
|
+
try {
|
|
197
|
+
// Name and marketplace source alone never prove ownership. Require the
|
|
198
|
+
// resolver's marker, release verification and exact projection payload.
|
|
199
|
+
const manifest = path.join(root, 'release.json');
|
|
200
|
+
if (!fs.lstatSync(manifest, { throwIfNoEntry: false })) continue;
|
|
201
|
+
const release = verifyRelease(root).release;
|
|
202
|
+
if (marketplaceName(release) !== market) continue;
|
|
203
|
+
if (!fs.lstatSync(path.join(projectionPath(release, accountHome), MARKER), { throwIfNoEntry: false })) continue;
|
|
204
|
+
inspectProjection(release, accountHome);
|
|
205
|
+
owned.push(key);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
errors.push(reason('config-unreadable', { field: 'plugins', name: key, detail: error.message }));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { owned, errors };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function planActivation(releaseRoot, input, env = process.env) {
|
|
214
|
+
const context = validateContext(input);
|
|
215
|
+
const status = statusFor(context);
|
|
216
|
+
const plan = { schemaVersion: 1, release: null, engineArgs: [], envPatch: {}, prompt: null,
|
|
217
|
+
files: [], machineSync: null, channels: status };
|
|
218
|
+
const continuation = context.launchMode !== 'fresh';
|
|
219
|
+
if (continuation && context.recordedRelease === undefined) {
|
|
220
|
+
status.principles = channel('partial', null, [reason('principles-unavailable', { launchMode: context.launchMode, detail: 'recorded-binding-missing' })]);
|
|
221
|
+
status.skills = channel('partial', null, [reason('skills-unavailable', { launchMode: context.launchMode, detail: 'recorded-binding-missing' })]);
|
|
222
|
+
return finish(plan);
|
|
223
|
+
}
|
|
224
|
+
const off = continuation ? context.recordedRelease === null : context.projectChoice === 'none';
|
|
225
|
+
const release = verifyRelease(continuation && !off ? context.recordedRelease.root : releaseRoot).release;
|
|
226
|
+
if (continuation && !off && (release.digest !== context.recordedRelease.digest || release.version !== context.recordedRelease.version)) {
|
|
227
|
+
fail('release-version-conflict', { version: context.recordedRelease.version }, 3);
|
|
228
|
+
}
|
|
229
|
+
const config = effectiveConfig(context);
|
|
230
|
+
const { found, errors } = inspectSkills(context, release, env, config);
|
|
231
|
+
if (errors.length) status.hide = channel('partial', null, errors);
|
|
232
|
+
status.inheritedDevlyn = found.filter((entry) => entry.owned).map((entry) => entry.path);
|
|
233
|
+
const supported = supportedVersion(context.engine, context.version);
|
|
234
|
+
const c = (key, value) => {
|
|
235
|
+
const encoded = `${key}=${toml(value)}`;
|
|
236
|
+
const previous = plan.engineArgs.findIndex((arg) => arg.startsWith(`${key}=`));
|
|
237
|
+
if (previous < 0) plan.engineArgs.push('-c', encoded);
|
|
238
|
+
else plan.engineArgs[previous] = encoded;
|
|
239
|
+
};
|
|
240
|
+
const accountHome = context.accountHome || env.CODEX_HOME || path.join(env.HOME || env.USERPROFILE || os.homedir(), '.codex');
|
|
241
|
+
let pluginOverrides = config?.plugins || {};
|
|
242
|
+
if (context.engine === 'codex' && config && supported) {
|
|
243
|
+
if (!path.isAbsolute(accountHome)) fail('config-unreadable', { field: 'CODEX_HOME' }, 2);
|
|
244
|
+
if (context.accountHome && env.CODEX_HOME && fs.realpathSync(context.accountHome) !== fs.realpathSync(env.CODEX_HOME)) {
|
|
245
|
+
fail('config-unreadable', { field: 'accountHome', detail: 'does not match finalized CODEX_HOME' }, 2);
|
|
246
|
+
}
|
|
247
|
+
const registrations = codexRegistrations(config, accountHome);
|
|
248
|
+
status.inheritedDevlyn.push(...registrations.owned);
|
|
249
|
+
pluginOverrides = { ...config.plugins };
|
|
250
|
+
for (const key of registrations.owned) pluginOverrides[key] = { ...pluginOverrides[key], enabled: false };
|
|
251
|
+
if (registrations.owned.length) {
|
|
252
|
+
c('plugins', pluginOverrides);
|
|
253
|
+
status.hide = channel('configured', 'plugins');
|
|
254
|
+
}
|
|
255
|
+
const hideErrors = [...errors, ...registrations.errors];
|
|
256
|
+
if (hideErrors.length) status.hide = channel('partial', null, hideErrors);
|
|
257
|
+
const owned = new Set(found.filter((entry) => entry.owned).map((entry) => entry.path));
|
|
258
|
+
if (owned.size) {
|
|
259
|
+
const entries = (config.skills?.config || []).map((entry) => ({ ...entry, ...(owned.has(entry.path) ? { enabled: false } : {}) }));
|
|
260
|
+
for (const file of owned) if (!entries.some((entry) => entry.path === file)) entries.push({ path: file, enabled: false });
|
|
261
|
+
c('skills.config', entries);
|
|
262
|
+
status.hide = channel(hideErrors.length ? 'partial' : 'configured', 'skills.config', hideErrors);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (status.inheritedDevlyn.length && status.hide.state === 'disabled') {
|
|
266
|
+
status.hide = channel('partial', null, [reason('skills-unavailable', { channel: 'selective-hide' })]);
|
|
267
|
+
}
|
|
268
|
+
if (off) return finish(plan);
|
|
269
|
+
plan.release = release;
|
|
270
|
+
if (!supported) {
|
|
271
|
+
const reasons = [reason('engine-version-unsupported', { minimum: ENGINE_CAPABILITIES[context.engine].minimum })];
|
|
272
|
+
status.principles = channel('unavailable', null, reasons);
|
|
273
|
+
status.skills = channel('unavailable', null, reasons);
|
|
274
|
+
return finish(plan);
|
|
275
|
+
}
|
|
276
|
+
if (!config) {
|
|
277
|
+
const reasons = [reason('config-unreadable', { field: 'effectiveConfig' })];
|
|
278
|
+
status.principles = channel('partial', null, reasons);
|
|
279
|
+
status.skills = channel('partial', null, reasons);
|
|
280
|
+
return finish(plan);
|
|
281
|
+
}
|
|
282
|
+
const defaults = context.committedBlockApplies ? '' : fs.readFileSync(path.join(release.root, 'config/skills/_shared/runtime-principles.md'), 'utf8');
|
|
283
|
+
const catalog = skillCatalog(release);
|
|
284
|
+
const instructions = joinText(defaults, catalog);
|
|
285
|
+
const file = (relativePath, utf8) => {
|
|
286
|
+
let parent = path.resolve(context.runDir);
|
|
287
|
+
const missing = [];
|
|
288
|
+
while (!fs.existsSync(parent)) { missing.unshift(path.basename(parent)); parent = path.dirname(parent); }
|
|
289
|
+
const physical = path.join(fs.realpathSync(parent), ...missing);
|
|
290
|
+
const relative = path.relative(release.root, physical);
|
|
291
|
+
if (!relative || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) {
|
|
292
|
+
fail('invalid-arguments', { argument: 'runDir must be outside the release' }, 2);
|
|
293
|
+
}
|
|
294
|
+
plan.files.push({ relativePath, utf8, sha256: sha256(utf8) });
|
|
295
|
+
return path.join(context.runDir, relativePath);
|
|
296
|
+
};
|
|
297
|
+
const principles = (mechanism) => { status.principles = channel('configured', context.committedBlockApplies ? 'native-project' : mechanism); };
|
|
298
|
+
const fallback = (reasons = []) => {
|
|
299
|
+
status.skills = channel('partial', 'absolute-catalog', [...reasons, reason('skills-catalog')]);
|
|
300
|
+
status.invocation = { kind: 'read-file', text: `Read ${path.join(release.root, 'config/skills/devlyn-ideate/SKILL.md')}` };
|
|
301
|
+
};
|
|
302
|
+
const native = (mechanism, kind, text) => { status.skills = channel('configured', mechanism); status.invocation = { kind, text }; };
|
|
303
|
+
const collisions = found.map((entry) => reason('name-collision', { name: entry.name, path: entry.path }));
|
|
304
|
+
switch (context.engine) {
|
|
305
|
+
case 'claude': {
|
|
306
|
+
const pluginDir = path.join(release.root, 'native/claude');
|
|
307
|
+
const conflicts = [];
|
|
308
|
+
let alreadySelected = false;
|
|
309
|
+
for (const dir of config.pluginDirs || []) {
|
|
310
|
+
try {
|
|
311
|
+
if (fs.realpathSync(dir) === pluginDir) { alreadySelected = true; continue; }
|
|
312
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, '.claude-plugin/plugin.json'), 'utf8'));
|
|
313
|
+
if (!object(manifest) || !textValue(manifest.name)) conflicts.push(reason('config-unreadable', { path: dir }));
|
|
314
|
+
else if (manifest.name === 'devlyn') conflicts.push(reason('name-collision', { name: 'devlyn', path: dir }));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
conflicts.push(reason('config-unreadable', { path: dir, detail: error.message }));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!conflicts.length && !alreadySelected) plan.engineArgs.push('--plugin-dir', pluginDir);
|
|
320
|
+
plan.engineArgs.push('--append-system-prompt-file',
|
|
321
|
+
file('devlyn-instructions.md', joinText(config.appendSystemPrompt, instructions)));
|
|
322
|
+
principles('append-system-prompt-file');
|
|
323
|
+
if (conflicts.length) fallback(conflicts);
|
|
324
|
+
else native('plugin-dir', 'slash', '/devlyn:devlyn-ideate');
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 'codex': {
|
|
328
|
+
const market = marketplaceName(release), key = `devlyn@${market}`;
|
|
329
|
+
const source = path.join(release.root, 'native/codex');
|
|
330
|
+
const existing = config.marketplaces?.[market];
|
|
331
|
+
let collision = (existing && (existing.source_type !== 'local' || existing.source !== source))
|
|
332
|
+
|| (!existing && config.plugins?.[key] !== undefined);
|
|
333
|
+
try { inspectProjection(release, accountHome); }
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (!(error instanceof HarnessError) || error.reason.code !== 'name-collision') throw error;
|
|
336
|
+
collision = true;
|
|
337
|
+
}
|
|
338
|
+
if (collision) fallback([reason('name-collision', { name: key, path: projectionPath(release, accountHome) })]);
|
|
339
|
+
else {
|
|
340
|
+
c('marketplaces', { ...config.marketplaces, [market]: { ...existing, source_type: 'local', source } });
|
|
341
|
+
c('plugins', { ...pluginOverrides, [key]: { ...config.plugins?.[key], enabled: true } });
|
|
342
|
+
plan.machineSync = { engine: 'codex', accountHome };
|
|
343
|
+
native('session-plugin', 'dollar', '$devlyn:devlyn-ideate');
|
|
344
|
+
}
|
|
345
|
+
c('developer_instructions', joinText(config.developer_instructions, instructions));
|
|
346
|
+
principles('developer_instructions');
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'omp': {
|
|
350
|
+
const skills = { ...config.skills,
|
|
351
|
+
customDirectories: [...new Set([...(config.skills?.customDirectories || []), path.join(release.root, 'config/skills')])] };
|
|
352
|
+
plan.engineArgs.push('--config', file('devlyn-omp.json', JSON.stringify({ skills }, null, 2) + '\n'),
|
|
353
|
+
'--append-system-prompt', joinText(config.appendSystemPrompt, instructions));
|
|
354
|
+
principles('append-system-prompt');
|
|
355
|
+
if (collisions.length || errors.length || skills.enableSkillCommands === false || skills.ignoredSkills?.length) fallback([...collisions, ...errors]);
|
|
356
|
+
else native('skills.customDirectories', 'slash', '/skill:devlyn-ideate');
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case 'grok':
|
|
360
|
+
plan.engineArgs.push('--rules', joinText(config.rules, instructions));
|
|
361
|
+
principles('rules'); fallback();
|
|
362
|
+
break;
|
|
363
|
+
case 'qwen':
|
|
364
|
+
plan.engineArgs.push('--append-system-prompt', joinText(config.appendSystemPrompt, instructions));
|
|
365
|
+
principles('append-system-prompt'); fallback();
|
|
366
|
+
break;
|
|
367
|
+
case 'opencode': {
|
|
368
|
+
let inline;
|
|
369
|
+
try { inline = config.inlineConfig == null ? {} : JSON.parse(config.inlineConfig); }
|
|
370
|
+
catch (error) { if (!(error instanceof SyntaxError)) throw error; inline = null; }
|
|
371
|
+
if (!object(inline) || (inline.instructions !== undefined && !strings(inline.instructions))) {
|
|
372
|
+
status.principles = channel('partial', null, [reason('config-unreadable', { field: 'inlineConfig.instructions' })]);
|
|
373
|
+
status.skills = channel('partial', null, [reason('config-unreadable', { field: 'inlineConfig.instructions' })]);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
plan.envPatch.OPENCODE_CONFIG_CONTENT = JSON.stringify({ ...inline,
|
|
377
|
+
instructions: [...(inline.instructions || []), file('devlyn-instructions.md', instructions)] });
|
|
378
|
+
principles('OPENCODE_CONFIG_CONTENT.instructions'); fallback();
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case 'copilot': {
|
|
382
|
+
status.principles = context.committedBlockApplies ? channel('configured', 'native-project')
|
|
383
|
+
: channel('unavailable', null, [reason('principles-unavailable')]);
|
|
384
|
+
const root = path.join(release.root, 'config/skills');
|
|
385
|
+
if (root.includes(',') || collisions.length || errors.length) {
|
|
386
|
+
status.skills = channel('partial', null, root.includes(',') ? [reason('skills-unavailable', { detail: 'comma-in-path' })] : [...collisions, ...errors]);
|
|
387
|
+
status.invocation = { kind: 'read-file', text: `Read ${path.join(root, 'devlyn-ideate/SKILL.md')}` };
|
|
388
|
+
} else {
|
|
389
|
+
const dirs = config.skillsDirs ?? env.COPILOT_SKILLS_DIRS ?? '';
|
|
390
|
+
plan.envPatch.COPILOT_SKILLS_DIRS = dirs.split(',').includes(root) ? dirs : (dirs ? `${dirs},${root}` : root);
|
|
391
|
+
native('COPILOT_SKILLS_DIRS', 'slash', '/devlyn-ideate');
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case 'kimi':
|
|
396
|
+
if (continuation || config.ownerAgent) {
|
|
397
|
+
status.principles = channel('partial', null, [reason('principles-unavailable', { launchMode: context.launchMode, ownerAgent: config.ownerAgent })]);
|
|
398
|
+
status.skills = channel('partial', null, [reason('skills-unavailable', { launchMode: context.launchMode, ownerAgent: config.ownerAgent })]);
|
|
399
|
+
} else {
|
|
400
|
+
plan.engineArgs.push('--agent-file', file('devlyn-kimi.md', joinText('${base_prompt}', instructions)));
|
|
401
|
+
principles('agent-file'); fallback();
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
case 'antigravity':
|
|
405
|
+
case 'cursor':
|
|
406
|
+
status.principles = channel('partial', context.initialPrompt ? 'initial-task' : null, [reason('principles-task-only')]);
|
|
407
|
+
if (context.initialPrompt) {
|
|
408
|
+
plan.prompt = { ...context.initialPrompt, text: joinText(instructions, context.initialPrompt.text) };
|
|
409
|
+
fallback();
|
|
410
|
+
} else status.skills = channel('partial', null, [reason('skills-unavailable', { detail: 'no-initial-task' })]);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
if (context.committedBlockApplies) status.principles = channel('configured', 'native-project');
|
|
414
|
+
if (plan.engineArgs.length || Object.keys(plan.envPatch).length || plan.prompt) plan.envPatch.DEVLYN_HARNESS_ROOT = release.root;
|
|
415
|
+
return finish(plan);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function activationCommand(args) {
|
|
419
|
+
const opts = options(args, { plan: { type: 'boolean' }, 'release-root': { type: 'string' }, 'context-file': { type: 'string' } });
|
|
420
|
+
if (!opts.plan || !opts['release-root'] || !path.isAbsolute(opts['release-root']) || !opts['context-file']) {
|
|
421
|
+
fail('invalid-arguments', { argument: 'plan, absolute release-root and context-file required' }, 2);
|
|
422
|
+
}
|
|
423
|
+
return planActivation(opts['release-root'], readContext(opts['context-file']));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = { planActivation, activationCommand, readContext, toml, skillCatalog };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const { Lexer } = require('marked');
|
|
5
|
+
const { managedBody } = require('./instructions');
|
|
6
|
+
const { ENGINE_IDS } = require('./engines');
|
|
7
|
+
const templates = require('./instruction-templates.json');
|
|
8
|
+
const history = require('./skill-history.json');
|
|
9
|
+
const { fingerprint, gitEnvironment } = require('./skill-ownership');
|
|
10
|
+
const { verifyRelease, sha256, physicalRoot, fail, options } = require('./release');
|
|
11
|
+
|
|
12
|
+
function git(root, args, encoding = 'utf8') {
|
|
13
|
+
return spawnSync('git', ['-C', root, ...args], {
|
|
14
|
+
env: { ...gitEnvironment(), GIT_OPTIONAL_LOCKS: '0' }, encoding, maxBuffer: 8 * 1024 * 1024,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function gitOutput(root, args) {
|
|
18
|
+
const result = git(root, args);
|
|
19
|
+
if (result.error || result.status !== 0) fail('repository-unreadable', { detail: result.error?.message || result.stderr.trim() });
|
|
20
|
+
return result.stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
function repositoryIdentity(project) {
|
|
23
|
+
const result = git(project, ['rev-parse', '--show-toplevel']);
|
|
24
|
+
if (result.error?.code === 'ENOENT' || (result.status === 128 && result.stderr.includes('not a git repository'))) return null;
|
|
25
|
+
if (result.error || result.status !== 0) fail('repository-unreadable', { detail: result.error?.message || result.stderr.trim() });
|
|
26
|
+
const root = fs.realpathSync(result.stdout.trim());
|
|
27
|
+
const commonGitDir = fs.realpathSync(gitOutput(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
|
|
28
|
+
// Committed identity survives missing/edited files and linked worktrees.
|
|
29
|
+
const pkg = git(root, ['show', 'HEAD:package.json']);
|
|
30
|
+
const cli = git(root, ['cat-file', '-e', 'HEAD:bin/devlyn.js']);
|
|
31
|
+
let sourceRepository = false;
|
|
32
|
+
if (pkg.status === 0 && cli.status === 0) {
|
|
33
|
+
try { sourceRepository = JSON.parse(pkg.stdout).name === 'devlyn-cli'; }
|
|
34
|
+
catch (error) { fail('repository-unreadable', { detail: error.message }); }
|
|
35
|
+
}
|
|
36
|
+
return { root, commonGitDir, sourceRepository };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function inspectInstruction(text, template, name) {
|
|
40
|
+
const normalized = text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
41
|
+
const body = managedBody(template);
|
|
42
|
+
const current = sha256(body);
|
|
43
|
+
const tokens = new Lexer({ gfm: false }).lex(normalized);
|
|
44
|
+
const hasMarkers = tokens.some((token) => token.type === 'html' && /^<!-- devlyn:instructions:(?:begin|end)/.test(token.raw));
|
|
45
|
+
const starts = [...normalized.matchAll(/^<!-- devlyn:instructions:begin sha256=([a-f0-9]{64}) -->\n/gm)];
|
|
46
|
+
const ends = [...normalized.matchAll(/^<!-- devlyn:instructions:end -->(?:\n|$)/gm)];
|
|
47
|
+
if (hasMarkers) {
|
|
48
|
+
if (starts.length !== 1 || ends.length !== 1 || ends[0].index < starts[0].index
|
|
49
|
+
|| normalized.split('<!-- devlyn:instructions:begin').length !== 2
|
|
50
|
+
|| normalized.split('<!-- devlyn:instructions:end').length !== 2) return { ownership: 'modified', blockSha256: null };
|
|
51
|
+
const previous = normalized.slice(starts[0].index + starts[0][0].length, ends[0].index);
|
|
52
|
+
const hash = sha256(previous);
|
|
53
|
+
if (hash !== starts[0][1]) return { ownership: 'modified', blockSha256: hash };
|
|
54
|
+
if (hash === current) return { ownership: 'current', blockSha256: hash };
|
|
55
|
+
const raw = previous.replace(/^Project-specific instructions outside this managed block take precedence over these defaults\.\n\n/, '');
|
|
56
|
+
const known = (templates[name] || []).some((entry) => entry.sha256 === sha256(raw));
|
|
57
|
+
return { ownership: known ? 'outdated' : 'modified', blockSha256: hash };
|
|
58
|
+
}
|
|
59
|
+
if (normalized === template.replace(/\r\n/g, '\n')) return { ownership: 'current', blockSha256: current };
|
|
60
|
+
const known = (templates[name] || []).some((entry) => entry.sha256 === sha256(normalized));
|
|
61
|
+
return { ownership: known ? 'outdated' : 'unowned', blockSha256: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requirements() {
|
|
65
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
66
|
+
const nodeSupported = (major === 20 && minor >= 17) || (major === 22 && minor >= 13)
|
|
67
|
+
|| (major === 23 && minor >= 5) || major > 23;
|
|
68
|
+
const node = { name: 'node', required: true, available: nodeSupported, version: process.versions.node,
|
|
69
|
+
reasons: nodeSupported ? [] : [{ code: 'requirement-version', params: { name: 'node' } }] };
|
|
70
|
+
const probes = [
|
|
71
|
+
['python', true, 'python3', ['-B', '--version']],
|
|
72
|
+
['git', true, 'git', ['--version']],
|
|
73
|
+
['git-bash', process.platform === 'win32', 'bash', ['--version']],
|
|
74
|
+
['gh', false, 'gh', ['--version']],
|
|
75
|
+
];
|
|
76
|
+
return [node, ...probes.map(([name, required, binary, argv]) => {
|
|
77
|
+
const result = spawnSync(binary, argv, { encoding: 'utf8', timeout: 5000, env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
78
|
+
const output = (result.stdout || result.stderr || '').trim();
|
|
79
|
+
const match = output.match(/\b(\d+)\.(\d+)(?:\.\d+)?/);
|
|
80
|
+
let available = !result.error && result.status === 0 && Boolean(match);
|
|
81
|
+
if (name === 'python' && available) available = Number(match[1]) > 3 || (Number(match[1]) === 3 && Number(match[2]) >= 11);
|
|
82
|
+
if (name === 'git-bash' && required && available) available = /mingw|msys/i.test(output);
|
|
83
|
+
return { name, required, available, version: match?.[0] || null,
|
|
84
|
+
reasons: available ? [] : [{ code: 'requirement-unavailable', params: { name } }] };
|
|
85
|
+
})];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function harnessStatus(project, { releaseRoot = path.resolve(__dirname, '..') } = {}) {
|
|
89
|
+
const release = verifyRelease(releaseRoot, { npmInstall: true }).release;
|
|
90
|
+
project = physicalRoot(project);
|
|
91
|
+
const repository = repositoryIdentity(project);
|
|
92
|
+
const root = repository?.root || project;
|
|
93
|
+
const instructions = [];
|
|
94
|
+
const ownership = [];
|
|
95
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
96
|
+
const file = path.join(root, name);
|
|
97
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
98
|
+
if (!stat) continue;
|
|
99
|
+
let inspected = { ownership: 'unowned', blockSha256: null };
|
|
100
|
+
let committed = false;
|
|
101
|
+
const reasons = [];
|
|
102
|
+
if (stat.isFile() && stat.nlink === 1) {
|
|
103
|
+
const bytes = fs.readFileSync(file);
|
|
104
|
+
let text;
|
|
105
|
+
try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (!(error instanceof TypeError)) throw error;
|
|
108
|
+
reasons.push({ code: 'instruction-encoding', params: { path: file } });
|
|
109
|
+
}
|
|
110
|
+
if (text !== undefined) inspected = inspectInstruction(text, fs.readFileSync(path.join(release.root, name), 'utf8'), name);
|
|
111
|
+
if (repository) {
|
|
112
|
+
const head = git(root, ['show', `HEAD:${name}`], 'buffer');
|
|
113
|
+
committed = head.status === 0 && head.stdout.equals(bytes);
|
|
114
|
+
}
|
|
115
|
+
} else reasons.push({ code: 'outside-root-link-or-special-file', params: { path: file } });
|
|
116
|
+
// Native trust/config/loading is established by CLI-3. A familiar filename is insufficient.
|
|
117
|
+
instructions.push({ path: file, committed, ...inspected, applicableEngines: [], applicability: 'unknown' });
|
|
118
|
+
ownership.push({ path: file, kind: 'instructions', proof: inspected.ownership === 'current' ? 'current'
|
|
119
|
+
: inspected.ownership === 'outdated' ? 'historical' : 'none', reasons });
|
|
120
|
+
}
|
|
121
|
+
const current = new Map();
|
|
122
|
+
for (const agent of ['.agents', '.claude']) {
|
|
123
|
+
for (const name of ['devlyn-ideate', 'devlyn-engines', '_shared']) {
|
|
124
|
+
const file = path.join(root, agent, 'skills', name);
|
|
125
|
+
// Do not traverse an ancestor link just to discover an installation.
|
|
126
|
+
const ancestors = [path.join(root, agent), path.join(root, agent, 'skills')];
|
|
127
|
+
if (ancestors.some((dir) => {
|
|
128
|
+
const stat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
129
|
+
return stat && !stat.isDirectory();
|
|
130
|
+
})) {
|
|
131
|
+
if (!ownership.some((entry) => entry.path === ancestors[0])) ownership.push({ path: ancestors[0], kind: 'core-skill',
|
|
132
|
+
proof: 'none', reasons: [{ code: 'outside-root-link-or-special-file', params: { path: ancestors[0] } }] });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!fs.lstatSync(file, { throwIfNoEntry: false })) continue;
|
|
136
|
+
let proof = 'none';
|
|
137
|
+
const reasons = [];
|
|
138
|
+
try {
|
|
139
|
+
const hash = fingerprint(file);
|
|
140
|
+
if (!current.has(name)) current.set(name, fingerprint(path.join(release.root, 'config/skills', name)));
|
|
141
|
+
if (hash === current.get(name)) proof = 'current';
|
|
142
|
+
else if ((history.skills[name] || []).includes(hash)) proof = 'historical';
|
|
143
|
+
} catch (error) {
|
|
144
|
+
reasons.push({ code: 'ownership-unverifiable', params: { path: file, detail: error.message } });
|
|
145
|
+
}
|
|
146
|
+
ownership.push({ path: file, kind: 'core-skill', proof, reasons });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Pure queue inspection and effective engine configuration belong to CLI-2/CLI-3.
|
|
150
|
+
return { release, repository, instructions, requirements: requirements(), ownership };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function statusCommand(args) {
|
|
154
|
+
const opts = options(args, { project: { type: 'string' }, engine: { type: 'string' }, 'context-file': { type: 'string' } });
|
|
155
|
+
if (!opts.project) fail('invalid-arguments', { argument: '--project' }, 2);
|
|
156
|
+
if (opts.engine && !ENGINE_IDS.includes(opts.engine)) {
|
|
157
|
+
fail('invalid-arguments', { argument: '--engine' }, 2);
|
|
158
|
+
}
|
|
159
|
+
const status = harnessStatus(opts.project);
|
|
160
|
+
if (opts['context-file']) {
|
|
161
|
+
const { readContext, planActivation } = require('./harness-activation');
|
|
162
|
+
const context = readContext(opts['context-file']);
|
|
163
|
+
if ((opts.engine && opts.engine !== context.engine) || physicalRoot(context.cwd) !== physicalRoot(opts.project)) {
|
|
164
|
+
fail('invalid-arguments', { argument: 'status context must match project and engine' }, 2);
|
|
165
|
+
}
|
|
166
|
+
status.engines = [planActivation(status.release.root, context).channels];
|
|
167
|
+
}
|
|
168
|
+
return status;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { harnessStatus, statusCommand, repositoryIdentity, inspectInstruction };
|