devlyn-cli 4.2.24 → 4.2.26
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 +12 -0
- package/bin/engines.js +35 -0
- package/bin/harness-activation.js +427 -0
- package/bin/harness-status.js +171 -0
- package/bin/instructions.js +13 -3
- package/bin/release.js +368 -0
- package/bin/skill-history.json +7 -0
- package/config/skills/_shared/adapters/omp.md +3 -0
- package/config/skills/_shared/runtime-principles.md +12 -2
- package/config/skills/_shared/task-complete.py +5 -0
- package/config/skills/_shared/task-completion.md +15 -4
- package/config/skills/_shared/warm-build-dirs.py +970 -0
- package/config/skills/devlyn-ideate/references/loop.md +2 -0
- package/package.json +7 -1
|
@@ -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 };
|
package/bin/instructions.js
CHANGED
|
@@ -13,12 +13,21 @@ const digest = (text) => crypto.createHash('sha256').update(text).digest('hex');
|
|
|
13
13
|
|
|
14
14
|
class InstructionError extends Error {}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
|
|
16
|
+
function managedBody(template) {
|
|
17
|
+
return 'Project-specific instructions outside this managed block take precedence over these defaults.\n\n'
|
|
18
18
|
+ normalize(template).replace(/\n?$/, '\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function managedBlock(template, eol = '\n') {
|
|
22
|
+
const body = managedBody(template);
|
|
19
23
|
return `${BEGIN} sha256=${digest(body)} -->\n${body}${END}\n`.replace(/\n/g, eol);
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
function sessionDefaults(template) {
|
|
27
|
+
return 'The following Devlyn instructions are defaults for this session. Applicable explicit user instructions and native owner/project instructions take precedence over these defaults. This delegation applies only to the Devlyn defaults below; it does not change other engine instructions or permissions.\n\n'
|
|
28
|
+
+ normalize(template);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
function matchedEnd(text, start, template) {
|
|
23
32
|
let end = start;
|
|
24
33
|
for (let n = 0; n < template.length && end < text.length; n++, end++) {
|
|
@@ -335,4 +344,5 @@ function updateInstructions(name, initial = '', withBlock = true, write = true,
|
|
|
335
344
|
return true;
|
|
336
345
|
}
|
|
337
346
|
|
|
338
|
-
module.exports = { updateInstructions, InstructionError, instructionParagraphs, holdsDevlynDefaults, importsAgentsMd
|
|
347
|
+
module.exports = { updateInstructions, InstructionError, instructionParagraphs, holdsDevlynDefaults, importsAgentsMd,
|
|
348
|
+
managedBody, managedBlock, sessionDefaults };
|
package/bin/release.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const { parseArgs } = require('util');
|
|
6
|
+
const { managedBody, managedBlock, sessionDefaults } = require('./instructions');
|
|
7
|
+
|
|
8
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
9
|
+
const CORE = ['devlyn-ideate', 'devlyn-engines', '_shared'];
|
|
10
|
+
const DEFAULTS = 'config/skills/_shared/runtime-principles.md';
|
|
11
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
12
|
+
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
13
|
+
|
|
14
|
+
class HarnessError extends Error {
|
|
15
|
+
constructor(code, params = {}, exitCode = 1) {
|
|
16
|
+
super(`${code}: ${JSON.stringify(params)}`);
|
|
17
|
+
this.reason = { code, params };
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function fail(code, params, exitCode) { throw new HarnessError(code, params, exitCode); }
|
|
22
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
23
|
+
function canonical(value) {
|
|
24
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
25
|
+
if (value && typeof value === 'object') {
|
|
26
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
|
|
27
|
+
}
|
|
28
|
+
return JSON.stringify(value);
|
|
29
|
+
}
|
|
30
|
+
function manifestDigest(manifest) {
|
|
31
|
+
const { digest, ...body } = manifest;
|
|
32
|
+
return sha256(canonical(body));
|
|
33
|
+
}
|
|
34
|
+
const jsonBytes = (value) => Buffer.from(JSON.stringify(value, null, 2) + '\n');
|
|
35
|
+
|
|
36
|
+
// POSIX manifest names must also be unambiguous on Windows and macOS volumes.
|
|
37
|
+
function validatePath(name) {
|
|
38
|
+
if (typeof name !== 'string' || !name || /[\\\x00-\x1f\x7f:<>"|?*]/.test(name)
|
|
39
|
+
|| path.posix.isAbsolute(name) || path.win32.isAbsolute(name)
|
|
40
|
+
|| name.split('/').some((part) => !part || part === '.' || part === '..' || /[. ]$/.test(part)
|
|
41
|
+
|| /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(part))) {
|
|
42
|
+
fail('release-unsafe-path', { path: String(name) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function validatePaths(names) {
|
|
46
|
+
const seen = new Map();
|
|
47
|
+
const files = new Set();
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
validatePath(name);
|
|
50
|
+
if (files.has(name)) fail('release-duplicate-path', { path: name });
|
|
51
|
+
files.add(name);
|
|
52
|
+
const parts = name.split('/');
|
|
53
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
54
|
+
const prefix = parts.slice(0, i).join('/');
|
|
55
|
+
const folded = prefix.normalize('NFC').toUpperCase().toLowerCase().toUpperCase();
|
|
56
|
+
if (seen.has(folded) && seen.get(folded) !== prefix) fail('release-case-collision', { path: name });
|
|
57
|
+
seen.set(folded, prefix);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
const parts = name.split('/');
|
|
62
|
+
for (let i = 1; i < parts.length; i++) {
|
|
63
|
+
if (files.has(parts.slice(0, i).join('/'))) fail('release-path-conflict', { path: name });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function regular(file, directory = false) {
|
|
68
|
+
const stat = fs.lstatSync(file);
|
|
69
|
+
if (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1) {
|
|
70
|
+
fail('release-unsafe-entry', { path: file });
|
|
71
|
+
}
|
|
72
|
+
return stat;
|
|
73
|
+
}
|
|
74
|
+
function physicalRoot(root) {
|
|
75
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) fail('invalid-arguments', { argument: 'absolute path required' }, 2);
|
|
76
|
+
regular(root, true);
|
|
77
|
+
return fs.realpathSync(root);
|
|
78
|
+
}
|
|
79
|
+
function safeFile(root, relative) {
|
|
80
|
+
validatePath(relative);
|
|
81
|
+
const parts = relative.split('/');
|
|
82
|
+
for (let i = 1; i < parts.length; i++) regular(path.join(root, ...parts.slice(0, i)), true);
|
|
83
|
+
const file = path.join(root, ...parts);
|
|
84
|
+
regular(file);
|
|
85
|
+
return file;
|
|
86
|
+
}
|
|
87
|
+
function readJson(file) {
|
|
88
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof SyntaxError) fail('invalid-json', { path: file });
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function treeFiles(root, relative = '', npmInstall = false) {
|
|
95
|
+
regular(path.join(root, relative), true);
|
|
96
|
+
const files = [];
|
|
97
|
+
for (const name of fs.readdirSync(path.join(root, relative)).sort()) {
|
|
98
|
+
const rel = relative ? `${relative}/${name}` : name;
|
|
99
|
+
validatePath(rel);
|
|
100
|
+
const stat = fs.lstatSync(path.join(root, rel));
|
|
101
|
+
if (stat.isDirectory()) {
|
|
102
|
+
// npm generates these launch shims after unpacking. Mutable install sources
|
|
103
|
+
// may contain them; immutable releases never do, and capture never copies them.
|
|
104
|
+
if (npmInstall && rel === 'node_modules/.bin') continue;
|
|
105
|
+
files.push(...treeFiles(root, rel, npmInstall));
|
|
106
|
+
}
|
|
107
|
+
else { regular(path.join(root, rel)); files.push(rel); }
|
|
108
|
+
}
|
|
109
|
+
validatePaths(files);
|
|
110
|
+
return files.sort();
|
|
111
|
+
}
|
|
112
|
+
function packageMetadata(root) {
|
|
113
|
+
const pkg = readJson(safeFile(root, 'package.json'));
|
|
114
|
+
if (pkg.name !== 'devlyn-cli' || !VERSION.test(pkg.version) || typeof pkg.engines?.node !== 'string') {
|
|
115
|
+
fail('release-schema', { field: 'package.json' }, 3);
|
|
116
|
+
}
|
|
117
|
+
return pkg;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function generatedArtifacts(root) {
|
|
121
|
+
const pkg = packageMetadata(root);
|
|
122
|
+
const template = fs.readFileSync(safeFile(root, 'CLAUDE.md'), 'utf8').replace(/\r\n/g, '\n');
|
|
123
|
+
const agents = fs.readFileSync(safeFile(root, 'AGENTS.md'), 'utf8').replace(/\r\n/g, '\n');
|
|
124
|
+
if (template !== agents || /<!-- devlyn:instructions:(?:begin|end)/.test(template)) {
|
|
125
|
+
fail('release-template-mismatch', {});
|
|
126
|
+
}
|
|
127
|
+
const outputs = new Map();
|
|
128
|
+
const put = (name, bytes, executable = false) => outputs.set(name, { bytes: Buffer.from(bytes), executable });
|
|
129
|
+
put(DEFAULTS, sessionDefaults(template));
|
|
130
|
+
put('harness/CLAUDE.md', managedBlock(template));
|
|
131
|
+
put('harness/AGENTS.md', managedBlock(template));
|
|
132
|
+
const plugin = { name: 'devlyn', version: pkg.version, description: 'Devlyn harness skills', skills: './skills/' };
|
|
133
|
+
put('native/claude/.claude-plugin/plugin.json', jsonBytes(plugin));
|
|
134
|
+
put('native/codex/plugins/devlyn/.codex-plugin/plugin.json', jsonBytes(plugin));
|
|
135
|
+
put('native/codex/.agents/plugins/marketplace.json', jsonBytes({
|
|
136
|
+
name: `devlyn-harness-v${pkg.version.replace(/\./g, '-')}`,
|
|
137
|
+
plugins: [{ name: 'devlyn', source: { source: 'local', path: './plugins/devlyn' } }],
|
|
138
|
+
}));
|
|
139
|
+
for (const skill of CORE) {
|
|
140
|
+
const source = `config/skills/${skill}`;
|
|
141
|
+
for (const relative of treeFiles(path.join(root, source))) {
|
|
142
|
+
// Python bytecode and OS metadata are not release inputs.
|
|
143
|
+
if (relative.split('/').includes('__pycache__') || /(?:^|\/)(?:\.DS_Store|Thumbs\.db)$|\.pyc$/.test(relative)) continue;
|
|
144
|
+
const name = `${source}/${relative}`;
|
|
145
|
+
const file = safeFile(root, name);
|
|
146
|
+
const bytes = outputs.get(name)?.bytes || fs.readFileSync(file);
|
|
147
|
+
const executable = name === DEFAULTS ? false : Boolean(fs.statSync(file).mode & 0o111);
|
|
148
|
+
for (const mirror of ['native/claude', 'native/codex/plugins/devlyn']) put(`${mirror}/skills/${skill}/${relative}`, bytes, executable);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return outputs;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function checkGenerated(root) {
|
|
155
|
+
const expected = generatedArtifacts(root);
|
|
156
|
+
for (const [name, { bytes, executable }] of expected) {
|
|
157
|
+
const file = safeFile(root, name);
|
|
158
|
+
if (!fs.readFileSync(file).equals(bytes) || (process.platform !== 'win32' && Boolean(fs.statSync(file).mode & 0o111) !== executable)) {
|
|
159
|
+
fail('release-generated-mismatch', { path: name });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const actual = ['harness', 'native'].flatMap((dir) => treeFiles(path.join(root, dir)).map((name) => `${dir}/${name}`));
|
|
163
|
+
if (actual.some((name) => !expected.has(name))) fail('release-generated-mismatch', { path: 'unlisted generated file' });
|
|
164
|
+
return expected.size;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function metadata(root) {
|
|
168
|
+
const pkg = packageMetadata(root);
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1, package: 'devlyn-cli', version: pkg.version,
|
|
171
|
+
principles: { defaults: DEFAULTS, memory: { claude: 'harness/CLAUDE.md', agents: 'harness/AGENTS.md' },
|
|
172
|
+
sourceBlockSha256: sha256(managedBody(fs.readFileSync(safeFile(root, 'CLAUDE.md'), 'utf8'))) },
|
|
173
|
+
skills: { root: 'config/skills', names: CORE.slice(0, 2), support: '_shared',
|
|
174
|
+
claudePlugin: 'native/claude', codexMarketplace: 'native/codex' },
|
|
175
|
+
stateCompat: {
|
|
176
|
+
loopManifest: { read: [1, 1], write: 1 }, addRecord: { read: [1, 1], write: 1 },
|
|
177
|
+
// CLI-2 introduces packet v2 and queueBinding together with their implementations.
|
|
178
|
+
taskPacket: { inspect: [1, 1], execute: [1, 1], write: 1 },
|
|
179
|
+
submission: { read: [1, 1], write: 1 }, review: { read: [1, 1], write: 1 },
|
|
180
|
+
acceptanceResult: { read: [1, 1], write: 1 }, invocationReceipt: { read: [1, 1], write: 1 },
|
|
181
|
+
legacyFormats: ['completion-receipt-unversioned', 'direct-acceptance-unversioned',
|
|
182
|
+
'expected-contract-unversioned', 'engines-config-unversioned', 'queue-markdown-inspect-only'],
|
|
183
|
+
},
|
|
184
|
+
requires: { node: pkg.engines.node, python: '>=3.11', git: true, windowsGitBash: true, githubDelivery: 'gh' },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function writeArtifact(root, name, bytes, executable) {
|
|
189
|
+
const parts = name.split('/');
|
|
190
|
+
for (let i = 1; i < parts.length; i++) {
|
|
191
|
+
const dir = path.join(root, ...parts.slice(0, i));
|
|
192
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
|
193
|
+
regular(dir, true);
|
|
194
|
+
}
|
|
195
|
+
const file = path.join(root, name);
|
|
196
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false })) regular(file);
|
|
197
|
+
fs.writeFileSync(file, bytes);
|
|
198
|
+
fs.chmodSync(file, executable ? 0o755 : 0o644);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildRelease(root = PACKAGE_ROOT) {
|
|
202
|
+
root = physicalRoot(root);
|
|
203
|
+
// npm omits links from its inventory; reject them before that omission can hide an input.
|
|
204
|
+
for (const dir of ['bin', 'config', 'optional-skills']) treeFiles(path.join(root, dir));
|
|
205
|
+
const outputs = generatedArtifacts(root);
|
|
206
|
+
// Generated roots are package-owned. Validate before removing stale mirror files.
|
|
207
|
+
for (const dir of ['harness', 'native']) {
|
|
208
|
+
if (fs.lstatSync(path.join(root, dir), { throwIfNoEntry: false })) {
|
|
209
|
+
treeFiles(path.join(root, dir));
|
|
210
|
+
fs.rmSync(path.join(root, dir), { recursive: true });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [name, { bytes, executable }] of outputs) writeArtifact(root, name, bytes, executable);
|
|
214
|
+
// npm's own inventory includes the exact bundled dependency closure and pack exclusions.
|
|
215
|
+
const npmCli = process.env.npm_execpath || (process.platform === 'win32'
|
|
216
|
+
? path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js') : null);
|
|
217
|
+
const result = spawnSync(npmCli ? process.execPath : 'npm', [
|
|
218
|
+
...(npmCli ? [npmCli] : []), 'pack', '--dry-run', '--ignore-scripts', '--json',
|
|
219
|
+
], { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
220
|
+
if (result.error || result.status !== 0) fail('release-pack-failed', { detail: result.error?.message || result.stderr.trim() });
|
|
221
|
+
const packed = JSON.parse(result.stdout)[0];
|
|
222
|
+
const names = packed.files.map((file) => file.path).filter((name) => name !== 'release.json').sort();
|
|
223
|
+
validatePaths(names);
|
|
224
|
+
const pkg = packageMetadata(root);
|
|
225
|
+
for (const dependency of pkg.bundleDependencies) {
|
|
226
|
+
if (!names.includes(`node_modules/${dependency}/package.json`)) fail('release-missing-dependency', { dependency });
|
|
227
|
+
}
|
|
228
|
+
const modes = new Map(packed.files.map((file) => [file.path, Boolean(file.mode & 0o111)]));
|
|
229
|
+
const manifest = { ...metadata(root), files: names.map((name) => ({
|
|
230
|
+
path: name, sha256: sha256(fs.readFileSync(safeFile(root, name))), executable: modes.get(name),
|
|
231
|
+
})) };
|
|
232
|
+
manifest.digest = { algorithm: 'sha256', value: manifestDigest(manifest) };
|
|
233
|
+
writeArtifact(root, 'release.json', jsonBytes(manifest), false);
|
|
234
|
+
return { manifestPath: path.join(root, 'release.json'), version: manifest.version,
|
|
235
|
+
digest: manifest.digest.value, fileCount: manifest.files.length };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function checkNpmShims(root, files) {
|
|
239
|
+
const dir = path.join(root, 'node_modules/.bin');
|
|
240
|
+
if (!fs.lstatSync(dir, { throwIfNoEntry: false })) return;
|
|
241
|
+
regular(dir, true);
|
|
242
|
+
const names = new Set(files.map((file) => file.path));
|
|
243
|
+
const targets = new Map();
|
|
244
|
+
for (const file of files) {
|
|
245
|
+
if (!/^node_modules\/(?:@[^/]+\/)?[^/]+\/package\.json$/.test(file.path)) continue;
|
|
246
|
+
const pkg = readJson(safeFile(root, file.path));
|
|
247
|
+
const bins = typeof pkg.bin === 'string' ? { [pkg.name.split('/').at(-1)]: pkg.bin } : pkg.bin || {};
|
|
248
|
+
for (const [name, relative] of Object.entries(bins)) {
|
|
249
|
+
validatePath(name);
|
|
250
|
+
if (name.includes('/') || typeof relative !== 'string') fail('release-schema', { field: 'dependency bin' }, 3);
|
|
251
|
+
const target = path.posix.normalize(`${path.posix.dirname(file.path)}/${relative}`);
|
|
252
|
+
validatePath(target);
|
|
253
|
+
if (!names.has(target)) fail('release-file-list-mismatch', { path: target });
|
|
254
|
+
for (const shim of [name, `${name}.cmd`, `${name}.ps1`]) {
|
|
255
|
+
if (!targets.has(shim)) targets.set(shim, new Set());
|
|
256
|
+
targets.get(shim).add(path.join(root, target));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const name of fs.readdirSync(dir)) {
|
|
261
|
+
const file = path.join(dir, name);
|
|
262
|
+
const stat = fs.lstatSync(file);
|
|
263
|
+
if (!targets.has(name)) fail('release-file-list-mismatch', { path: `node_modules/.bin/${name}` });
|
|
264
|
+
if (stat.isSymbolicLink()) {
|
|
265
|
+
if (!targets.get(name).has(path.resolve(dir, fs.readlinkSync(file)))) fail('release-unsafe-entry', { path: file });
|
|
266
|
+
} else regular(file); // Windows shims are regular files; never execute or copy them.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function verifyRelease(root, { npmInstall = false } = {}) {
|
|
271
|
+
root = physicalRoot(root);
|
|
272
|
+
const manifest = readJson(safeFile(root, 'release.json'));
|
|
273
|
+
if (!manifest || manifest.schemaVersion !== 1 || manifest.package !== 'devlyn-cli'
|
|
274
|
+
|| !VERSION.test(manifest.version) || manifest.digest?.algorithm !== 'sha256'
|
|
275
|
+
|| !SHA256.test(manifest.digest?.value) || !Array.isArray(manifest.files)
|
|
276
|
+
|| !manifest.files.every((f) => f && typeof f.path === 'string' && SHA256.test(f.sha256) && typeof f.executable === 'boolean')) {
|
|
277
|
+
fail('release-schema', { path: 'release.json' }, 3);
|
|
278
|
+
}
|
|
279
|
+
const names = manifest.files.map((file) => file.path);
|
|
280
|
+
validatePaths([...names, 'release.json']);
|
|
281
|
+
if (canonical(names) !== canonical([...names].sort())) fail('release-schema', { field: 'files order' }, 3);
|
|
282
|
+
if (manifestDigest(manifest) !== manifest.digest.value) fail('release-digest-mismatch', { path: 'release.json' });
|
|
283
|
+
const actual = treeFiles(root, '', npmInstall).filter((name) => name !== 'release.json');
|
|
284
|
+
if (canonical(actual) !== canonical(names)) fail('release-file-list-mismatch', {});
|
|
285
|
+
for (const file of manifest.files) {
|
|
286
|
+
const full = safeFile(root, file.path);
|
|
287
|
+
if (sha256(fs.readFileSync(full)) !== file.sha256) fail('release-file-digest-mismatch', { path: file.path });
|
|
288
|
+
// Windows has no POSIX execute bit; its declared mode is retained in the digest.
|
|
289
|
+
if (process.platform !== 'win32' && Boolean(fs.statSync(full).mode & 0o111) !== file.executable) {
|
|
290
|
+
fail('release-mode-mismatch', { path: file.path });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (npmInstall) checkNpmShims(root, manifest.files);
|
|
294
|
+
const { files, digest, ...declared } = manifest;
|
|
295
|
+
if (canonical(declared) !== canonical(metadata(root))) fail('release-schema', { field: 'metadata' }, 3);
|
|
296
|
+
checkGenerated(root);
|
|
297
|
+
return { release: { package: 'devlyn-cli', version: manifest.version, digest: digest.value, root }, valid: true };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function materializeRelease(source, store) {
|
|
301
|
+
const verified = verifyRelease(source, { npmInstall: true }).release;
|
|
302
|
+
if (!path.isAbsolute(store)) fail('invalid-arguments', { argument: 'absolute store required' }, 2);
|
|
303
|
+
// Check every existing store component before mkdir; never write through a store link.
|
|
304
|
+
const missing = [];
|
|
305
|
+
let parent = path.resolve(store);
|
|
306
|
+
while (!fs.lstatSync(parent, { throwIfNoEntry: false })) { missing.push(parent); parent = path.dirname(parent); }
|
|
307
|
+
regular(parent, true);
|
|
308
|
+
for (let dir = parent; dir !== path.dirname(dir); dir = path.dirname(dir)) regular(dir, true);
|
|
309
|
+
for (const dir of missing.reverse()) { fs.mkdirSync(dir, { recursive: true }); regular(dir, true); }
|
|
310
|
+
store = physicalRoot(store);
|
|
311
|
+
const packageDir = path.join(store, 'devlyn-cli');
|
|
312
|
+
fs.mkdirSync(packageDir, { recursive: true });
|
|
313
|
+
regular(packageDir, true);
|
|
314
|
+
const target = path.join(packageDir, verified.version);
|
|
315
|
+
const existing = () => {
|
|
316
|
+
const release = verifyRelease(target).release;
|
|
317
|
+
if (release.digest !== verified.digest) fail('release-version-conflict', { version: release.version }, 3);
|
|
318
|
+
return { release, created: false };
|
|
319
|
+
};
|
|
320
|
+
if (fs.lstatSync(target, { throwIfNoEntry: false })) return existing();
|
|
321
|
+
const temp = fs.mkdtempSync(path.join(packageDir, `.${verified.version}-`));
|
|
322
|
+
try {
|
|
323
|
+
const manifest = readJson(safeFile(verified.root, 'release.json'));
|
|
324
|
+
for (const file of [...manifest.files, { path: 'release.json', executable: false }]) {
|
|
325
|
+
writeArtifact(temp, file.path, fs.readFileSync(safeFile(verified.root, file.path)), file.executable);
|
|
326
|
+
}
|
|
327
|
+
const captured = verifyRelease(temp).release;
|
|
328
|
+
if (captured.digest !== verified.digest) fail('release-source-changed', {}, 3);
|
|
329
|
+
try { fs.renameSync(temp, target); }
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code) && fs.existsSync(target)) return existing();
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
return { release: { ...captured, root: target }, created: true };
|
|
335
|
+
} finally {
|
|
336
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function options(args, definitions) {
|
|
341
|
+
try { return parseArgs({ args, options: { json: { type: 'boolean' }, ...definitions }, strict: true }).values; }
|
|
342
|
+
catch (error) { fail('invalid-arguments', { detail: error.message }, 2); }
|
|
343
|
+
}
|
|
344
|
+
function releaseCommand(args) {
|
|
345
|
+
const opts = options(args, Object.fromEntries(['build', 'verify', 'materialize', 'root', 'source', 'store']
|
|
346
|
+
.map((key) => [key, { type: ['build', 'verify', 'materialize'].includes(key) ? 'boolean' : 'string' }])));
|
|
347
|
+
const actions = ['build', 'verify', 'materialize'].filter((key) => opts[key]);
|
|
348
|
+
const allowed = { build: ['build', 'json'], verify: ['verify', 'root', 'json'], materialize: ['materialize', 'source', 'store', 'json'] };
|
|
349
|
+
if (actions.length !== 1 || Object.keys(opts).some((key) => !allowed[actions[0]].includes(key))
|
|
350
|
+
|| (opts.verify && !opts.root) || (opts.materialize && (!opts.source || !opts.store))) fail('invalid-arguments', {}, 2);
|
|
351
|
+
if (opts.build) return buildRelease();
|
|
352
|
+
if (opts.verify) return verifyRelease(opts.root);
|
|
353
|
+
return materializeRelease(opts.source, opts.store);
|
|
354
|
+
}
|
|
355
|
+
function runJson(action) {
|
|
356
|
+
try { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: true, data: action() }) + '\n'); }
|
|
357
|
+
catch (error) {
|
|
358
|
+
const reason = error instanceof HarnessError ? error.reason
|
|
359
|
+
: { code: 'operation-failed', params: { detail: error.message, ...(error.code ? { systemCode: error.code } : {}) } };
|
|
360
|
+
process.stderr.write(`${reason.code}: ${JSON.stringify(reason.params)}\n`);
|
|
361
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: false, error: reason }) + '\n');
|
|
362
|
+
process.exitCode = error instanceof HarnessError ? error.exitCode : 1;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = { buildRelease, verifyRelease, materializeRelease, checkGenerated,
|
|
367
|
+
sha256, manifestDigest, physicalRoot, HarnessError, fail,
|
|
368
|
+
options, releaseCommand, runJson };
|
package/bin/skill-history.json
CHANGED
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
"161cb4f1b27a9f81ae1e96af6ff596399c8936578a11d601108a89cbdeea241d",
|
|
19
19
|
"196afa14871099a9afc4e484b00e8d8ccd928dba79db95a6a8776f58c8e78c39",
|
|
20
20
|
"1c1e5e44ff704b1b8791fabcad79a4158d8cd2f024569c1685ec3b185a98fbcc",
|
|
21
|
+
"20ac702b463f3eaa07fd6686d5209aed7993773504e35896fc732f7e653ec6cd",
|
|
21
22
|
"20bfec10dcec9a06701827a6a39faf3495f970f659f1e941cbfb62e880a0649c",
|
|
22
23
|
"20e11efa7b27dcd30b5550ba5e0e2e07df9ac522a76445a594803a0e29b2e13d",
|
|
23
24
|
"21723943c52573124ce3c74e9090a8ae7d7c9ecdeb78fdfa52b89c45ff1b3374",
|
|
24
25
|
"247904cad29c1c3ed810f848e66f982e8765e5c627ef3a18a876fd95eed9c29c",
|
|
25
26
|
"24f73bd06d359296088e43ce2e7bf5178e3f69752b559639585563ec35fc33f5",
|
|
27
|
+
"2692865ee864aebedb2b385384b2a8220bc780bd92310bc1cf2f56927de1e0b8",
|
|
26
28
|
"27d4da7863ed4652187f6a101da0e56b1d3d18e4565c720d5f8150ad0a193402",
|
|
27
29
|
"2a4361bca0a9fadc4a8e5e4578c383171cd64c65296fe882990502250df44d53",
|
|
28
30
|
"2bcac1ab5995e474c7dd46705413982876384d8b4da704126a66c10770a1a4d2",
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"353b483942a62b79d1c5bf31e30060ef7ed9a67ae2d8b27450a53acb98e4e23c",
|
|
34
36
|
"3554398c2be144938e4837d9b60c0976f685cfc2c0dd0451cda714fd053f3c7d",
|
|
35
37
|
"35c7e6c069c847ff147f2bf4f3b67a296652c122aa91a3966457a00dd0597b78",
|
|
38
|
+
"364aa97ec4065d8b78188f40281dfa575120a8fb91089120363e532566ef0dcb",
|
|
36
39
|
"37acccbdc76aee15311b5f47fd26c50b1652dbe7cbcbacfc37774841ab464028",
|
|
37
40
|
"393ad728eb0906ae61110fde3820c4fce015d8f94643a2877372b74a6b6eeb02",
|
|
38
41
|
"3b6193eabc83d815804533010ca7bff1f240286e7898380a5fc2adac71dcb513",
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
"7a0f6c692bd1f74759d44f1a145899c157e1787635db546ea8a192461421e353",
|
|
89
92
|
"7ca704d818bf9c044fed59fdd4107bd0463f9b0514d44729003e3e52aa796353",
|
|
90
93
|
"7e915cc7307af694e326c7f75cf831d187edd965f38d870323cd50f0f01a564b",
|
|
94
|
+
"7f5f9f318f465b01c573fa521922a218e0973056a53053d262ca6ffb84dda145",
|
|
91
95
|
"800543bb9d1f8b90a7c077f072aa45364a0136d3c4e9b7acddcdbc30d63eb0c8",
|
|
92
96
|
"823e497e01870123d4e73c8dac7472e161b5ffa2f6181ddcba71fcb829096948",
|
|
93
97
|
"83b6d37fcacc086a09b9c3eddfff2263bfef23f8fabf8d8833a7c9f9fb6186c5",
|
|
@@ -100,6 +104,7 @@
|
|
|
100
104
|
"9162fc2fed6f4a611d299485be3a397080beb3304865ce59b95576ba7d00650e",
|
|
101
105
|
"94a14a1b87df1d7297d12949a24e47a0d6c16a8fe30495ee759fcabb2e1aa398",
|
|
102
106
|
"978e4210e49b42827c98cf6e40c868c87dc6e3533a2b779ca2fa05abfc16ad0f",
|
|
107
|
+
"97e7ad89c74c609b15dfbf6f9f7ac61df8b50e97e4bc318f72922e058ecffba4",
|
|
103
108
|
"9815b6e6963e068872a6dbeae24068143bf92042bb035f67bafd1a224d65bcb0",
|
|
104
109
|
"998343c148618b937556b58b83749c05d5aa3283280d751c118da6a542fb87f2",
|
|
105
110
|
"9dd3ec225818efe0ad3726192db94ff129154e995617a9d3c150fc58ad0f9f2c",
|
|
@@ -144,6 +149,7 @@
|
|
|
144
149
|
"db867d1fd98058bb20e41e942abd95f08fbaa9465928025f69f0a0308fa71514",
|
|
145
150
|
"dd0bfdfd2db0b5f9ddef75d1b9ce3247698739fc551d292366dccd506fef6a36",
|
|
146
151
|
"ddb0c8dac58e95298330ab54e4d1fe4a108c968d9af713f267b4f8f4c6808507",
|
|
152
|
+
"de4f504265e960cb8d81752e9ae922f664790ee026b4454f1b07252d1a6282a4",
|
|
147
153
|
"e0a305cf3bf47c8bc357195d4d50b0f6177069e870c33040c258c93edae68646",
|
|
148
154
|
"e115205a744fef918a59d1b3a601df09a8b6df1d3e60613b9e8b87240929f8d8",
|
|
149
155
|
"e41013abf5e17445ef83a86974ac1de77beaa61e484e1916577ecc64bb34c220",
|
|
@@ -343,6 +349,7 @@
|
|
|
343
349
|
"a7d7d65fd5f672cb404e6069fdc42ad7077deaa80ed8500cfa8b382dab2e3e29",
|
|
344
350
|
"aba66b674683935e4c28c93333896dc116ffe67e7a35fe30b3599a8be507e94e",
|
|
345
351
|
"b2e4437fa981317362d03ee032e1cfd8b686316e966c6ba12ecd619a009c80a6",
|
|
352
|
+
"bdd0751eea04532c050334fda3bc5c61c6b429e485d97e0b8ebb4fae154da2c7",
|
|
346
353
|
"c1b8e3ebf38e2be8972aa003e9614a782cb4f195c69d20b95a475ebf9aaaab9e",
|
|
347
354
|
"c8e375aab574148542f392b093cbbf3f2e8dc623826b45e35df0a1a2c3862226",
|
|
348
355
|
"ce282a55328c2d071cacc377670fbebea4da4b9a7fee81b25293fb26763f17c9",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
The following Devlyn instructions are defaults for this session. Applicable explicit user instructions and native owner/project instructions take precedence over these defaults. This delegation applies only to the Devlyn defaults below; it does not change other engine instructions or permissions.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Project Instructions
|
|
4
|
+
|
|
5
|
+
devlyn-cli installs the `devlyn-ideate` and `devlyn-engines` skills alongside the principles below, which are non-negotiable on every change — yours and any sub-agent's.
|
|
4
6
|
|
|
5
7
|
## North Star
|
|
6
8
|
|
|
@@ -23,3 +25,11 @@ Three discipline rules govern HOW the principles are applied:
|
|
|
23
25
|
- **Root cause via flexible why-chain.** Keep asking "why?" until you find the violated invariant. **If the answer surfaces in 2 questions, stop.** If it takes 5 or 7, keep going. Strict counts are wrong; until-found is right.
|
|
24
26
|
- **First-principles thinking.** Challenge the requirement before optimizing the answer. Surface unstated assumptions, ambiguities, tradeoffs, and simpler alternatives BEFORE implementing — do not silently pick one interpretation when multiple exist, do not hide confusion, push back when a simpler path is genuinely better. Most "we have to do X" assumptions are habit, not necessity. Reduce the problem to its irreducible truths and rebuild from there.
|
|
25
27
|
- **Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.** — Saint-Exupéry. This is the operating definition of "done." A change is finished when no further line, branch, flag, or doc paragraph can be removed without breaking a learned failure mode. Not before.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
- `devlyn-ideate` — loop designer and intent queue: `plan <intent or document>` writes a validated loop package, `add` queues its tasks, `status` (or no arguments) reports the queue, `drain` executes it serially.
|
|
32
|
+
- Delivery — direct work edits the current checkout; deliver completed, verified requests with changes by project policy (default PR and merge), following `_shared/task-completion.md` in `.claude/skills/` or `.agents/skills/` (project, else `~`). With known concurrent writers, isolate before editing; on unexplained changes, pause writes until safe or isolated. An ideate drain delivers every task. Explicit limits win; commit alone stays local.
|
|
33
|
+
- Executor — `devlyn-engines` shows it and pins it in `.devlyn/engines.json`. A pinned executor does the implementation work, direct or drained: when it is not you, delegate to it, and when it is unavailable stop with `BLOCKED:<engine>-unavailable`. Without a pin, you are the executor.
|
|
34
|
+
|
|
35
|
+
Each skill's `SKILL.md` is the source of truth for its flags and workflow.
|