devlyn-cli 4.2.19 → 4.2.21
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/README.md +5 -5
- package/bin/cleanup-history.json +97 -0
- package/bin/devlyn.js +285 -267
- package/bin/instructions.js +44 -24
- package/bin/skill-history.json +1 -70
- package/package.json +1 -1
package/bin/instructions.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { Lexer } = require('marked');
|
|
5
5
|
const legacyTemplates = require('./instruction-templates.json');
|
|
6
|
+
const cleanupHistory = require('./cleanup-history.json');
|
|
6
7
|
|
|
7
8
|
const BEGIN = '<!-- devlyn:instructions:begin';
|
|
8
9
|
const END = '<!-- devlyn:instructions:end -->';
|
|
@@ -147,7 +148,7 @@ function importsAgentsMd(text) {
|
|
|
147
148
|
|
|
148
149
|
function customInstructions(text, name, template, managed = false) {
|
|
149
150
|
const known = new Set([
|
|
150
|
-
...legacyTemplates.paragraphs[name],
|
|
151
|
+
...(legacyTemplates.paragraphs[name] || []),
|
|
151
152
|
...instructionParagraphs(template).map((p) => p.key),
|
|
152
153
|
]);
|
|
153
154
|
const bom = text.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
@@ -169,6 +170,22 @@ function customInstructions(text, name, template, managed = false) {
|
|
|
169
170
|
return bom + paragraphs.filter((_, index) => keep.has(index)).map((p) => p.text).join('');
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
// Only the complete appended bytes are owned, including the separator added by old installers.
|
|
174
|
+
// A heading inside a fenced example is not an installed instruction block.
|
|
175
|
+
function removeEvaluatorBlocks(text) {
|
|
176
|
+
const headings = new Set(instructionParagraphs(text).filter((p) => p.level === 1).map((p) => p.start));
|
|
177
|
+
const ranges = [];
|
|
178
|
+
for (const match of text.matchAll(/\r?\n\r?\n---\r?\n\r?\n(?=# Devlyn Agent Instructions\r?\n)/g)) {
|
|
179
|
+
if (!headings.has(match.index + match[0].length)) continue;
|
|
180
|
+
for (const template of cleanupHistory.evaluators) {
|
|
181
|
+
const end = matchedEnd(text, match.index, template);
|
|
182
|
+
if (end !== null) { ranges.push({ start: match.index, end }); break; }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const { start, end } of ranges.reverse()) text = text.slice(0, start) + text.slice(end);
|
|
186
|
+
return text;
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
// Devlyn defaults in an instruction file: a managed block, or stock paragraphs of a template
|
|
173
190
|
// that a release before managed blocks copied in whole (the evidence migration replaces).
|
|
174
191
|
function holdsDevlynDefaults(name, text) {
|
|
@@ -196,22 +213,23 @@ function retainFile(file, bytes) {
|
|
|
196
213
|
|
|
197
214
|
// `withBlock` false removes the devlyn defaults the file holds, keeping everything else. `write` false writes nothing
|
|
198
215
|
// and returns whether the file needs an update, a merge included.
|
|
199
|
-
function updateInstructions(name, initial = '', withBlock = true, write = true) {
|
|
200
|
-
const projectPackage = path.join(
|
|
201
|
-
if (fs.existsSync(path.join(
|
|
216
|
+
function updateInstructions(name, initial = '', withBlock = true, write = true, directory = process.cwd(), evaluatorOnly = false) {
|
|
217
|
+
const projectPackage = path.join(directory, 'package.json');
|
|
218
|
+
if (fs.existsSync(path.join(directory, 'bin', 'devlyn.js')) && fs.existsSync(projectPackage)
|
|
202
219
|
&& JSON.parse(fs.readFileSync(projectPackage, 'utf8'))?.name === 'devlyn-cli') {
|
|
203
220
|
console.log(` → Skipping ${name}; shipped instruction template in devlyn-cli source checkout`);
|
|
204
221
|
return false;
|
|
205
222
|
}
|
|
206
|
-
const
|
|
207
|
-
const
|
|
223
|
+
const templateName = path.basename(name);
|
|
224
|
+
const templatePath = path.join(__dirname, '..', templateName);
|
|
225
|
+
const template = ['CLAUDE.md', 'AGENTS.md'].includes(templateName) ? fs.readFileSync(templatePath, 'utf8') : '';
|
|
208
226
|
if (template.includes(BEGIN) || template.includes(END)) {
|
|
209
227
|
throw new Error(`Packaged instruction template contains managed markers: ${templatePath}`);
|
|
210
228
|
}
|
|
211
|
-
const dest = path.join(
|
|
229
|
+
const dest = path.join(directory, name);
|
|
212
230
|
const stat = fs.lstatSync(dest, { throwIfNoEntry: false });
|
|
213
|
-
const sourceStat = fs.statSync(templatePath);
|
|
214
|
-
if (stat?.isFile() && stat.dev === sourceStat
|
|
231
|
+
const sourceStat = template ? fs.statSync(templatePath) : null;
|
|
232
|
+
if (stat?.isFile() && stat.dev === sourceStat?.dev && stat.ino === sourceStat?.ino) {
|
|
215
233
|
console.log(` → Using source ${name}; packaged template left unchanged`);
|
|
216
234
|
return false;
|
|
217
235
|
}
|
|
@@ -227,15 +245,16 @@ function updateInstructions(name, initial = '', withBlock = true, write = true)
|
|
|
227
245
|
} catch (cause) {
|
|
228
246
|
throw new InstructionError(`${name} must be UTF-8; original preserved. Convert its encoding and rerun installation.`, { cause });
|
|
229
247
|
}
|
|
248
|
+
current = removeEvaluatorBlocks(current);
|
|
230
249
|
const eol = current.match(/\r?\n/)?.[0] || '\n';
|
|
231
250
|
const block = managedBlock(template, eol);
|
|
232
251
|
const inserted = withBlock ? block : '';
|
|
233
|
-
const recovery = path.join(
|
|
252
|
+
const recovery = path.join(directory, '.devlyn', 'instructions');
|
|
234
253
|
const conflict = (reason) => {
|
|
235
254
|
if (!write) return true;
|
|
236
|
-
const incoming = path.join(recovery, `${name}.${digest(block)}.incoming`);
|
|
237
|
-
const backup = path.join(recovery, `${name}.${digest(before)}.backup`);
|
|
238
|
-
const guide = path.join(recovery, `${name}.${digest(before)}.${digest(block)}.merge.md`);
|
|
255
|
+
const incoming = path.join(recovery, `${encodeURIComponent(name)}.${digest(block)}.incoming`);
|
|
256
|
+
const backup = path.join(recovery, `${encodeURIComponent(name)}.${digest(before)}.backup`);
|
|
257
|
+
const guide = path.join(recovery, `${encodeURIComponent(name)}.${digest(before)}.${digest(block)}.merge.md`);
|
|
239
258
|
retainFile(backup, before);
|
|
240
259
|
retainFile(incoming, Buffer.from(block));
|
|
241
260
|
retainFile(guide, Buffer.from(`# Merge ${name}\n\n`
|
|
@@ -252,7 +271,9 @@ function updateInstructions(name, initial = '', withBlock = true, write = true)
|
|
|
252
271
|
+ `Installation is incomplete; earlier selected agents may already have been updated.`);
|
|
253
272
|
};
|
|
254
273
|
let content;
|
|
255
|
-
if (
|
|
274
|
+
if (evaluatorOnly) {
|
|
275
|
+
content = current;
|
|
276
|
+
} else if (current.includes(BEGIN) || current.includes(END)) {
|
|
256
277
|
const starts = [...current.matchAll(/^\uFEFF?<!-- devlyn:instructions:begin sha256=([a-f0-9]{64}) -->\r?\n/gm)];
|
|
257
278
|
const ends = [...current.matchAll(/^<!-- devlyn:instructions:end -->(?:\r?\n|$)/gm)];
|
|
258
279
|
if (starts.length !== 1 || ends.length !== 1
|
|
@@ -265,29 +286,27 @@ function updateInstructions(name, initial = '', withBlock = true, write = true)
|
|
|
265
286
|
if (end.index < bodyStart) return conflict('reversed managed markers');
|
|
266
287
|
const previous = current.slice(bodyStart, end.index);
|
|
267
288
|
const custom = digest(normalize(previous)) === start[1] ? ''
|
|
268
|
-
: customInstructions(previous,
|
|
289
|
+
: customInstructions(previous, templateName,
|
|
269
290
|
'Project-specific instructions outside this managed block take precedence over these defaults.\n\n' + template, true);
|
|
270
291
|
const after = current.slice(end.index + end[0].length);
|
|
271
292
|
content = current.slice(0, start.index + (start[0].startsWith('\uFEFF') ? 1 : 0))
|
|
272
293
|
+ custom + (custom ? eol : '') + inserted + after;
|
|
273
|
-
// A block removed from the end takes along the blank lines that set it apart.
|
|
274
|
-
if (!inserted && !after) content = content.replace(/(\r?\n)(?:\r?\n)+$/, '$1');
|
|
275
294
|
} else {
|
|
276
295
|
const normalized = normalize(template);
|
|
277
296
|
const bodyStart = normalized.indexOf('## North Star\n');
|
|
278
297
|
const preamble = normalized.slice(0, bodyStart);
|
|
279
298
|
const body = normalized.slice(bodyStart);
|
|
280
299
|
const ranges = legacyRanges(current, [
|
|
281
|
-
{ length: normalized.length, sha256: digest(normalized),
|
|
282
|
-
...(bodyStart > 0 ? { preamble, body: { length: body.length, sha256: digest(body) } } : {}) },
|
|
283
|
-
...(legacyTemplates[
|
|
300
|
+
...(normalized ? [{ length: normalized.length, sha256: digest(normalized),
|
|
301
|
+
...(bodyStart > 0 ? { preamble, body: { length: body.length, sha256: digest(body) } } : {}) }] : []),
|
|
302
|
+
...(legacyTemplates[templateName] || []),
|
|
284
303
|
]);
|
|
285
304
|
if (ranges.length === 1 && !LEGACY.test(current.slice(0, ranges[0].start)
|
|
286
305
|
+ ranges[0].custom + current.slice(ranges[0].end))) {
|
|
287
306
|
const { start, end, custom } = ranges[0];
|
|
288
307
|
content = current.slice(0, start) + custom + inserted + current.slice(end);
|
|
289
308
|
} else {
|
|
290
|
-
const custom = customInstructions(current,
|
|
309
|
+
const custom = customInstructions(current, templateName, template);
|
|
291
310
|
content = custom + (custom && inserted ? eol + eol : '') + inserted;
|
|
292
311
|
}
|
|
293
312
|
}
|
|
@@ -295,7 +314,7 @@ function updateInstructions(name, initial = '', withBlock = true, write = true)
|
|
|
295
314
|
if (bytes.equals(before)) return false;
|
|
296
315
|
if (!write) return true;
|
|
297
316
|
if (exists) {
|
|
298
|
-
const backup = path.join(recovery, `${name}.${digest(before)}.backup`);
|
|
317
|
+
const backup = path.join(recovery, `${encodeURIComponent(name)}.${digest(before)}.backup`);
|
|
299
318
|
retainFile(backup, before);
|
|
300
319
|
console.log(` → Saved previous ${name}: ${backup}`);
|
|
301
320
|
}
|
|
@@ -306,11 +325,12 @@ function updateInstructions(name, initial = '', withBlock = true, write = true)
|
|
|
306
325
|
if (fs.existsSync(dest) !== exists || (exists && !fs.readFileSync(dest).equals(before))) {
|
|
307
326
|
throw new InstructionError(`${name} changed during installation; preserved it. Rerun installation.`);
|
|
308
327
|
}
|
|
309
|
-
fs.
|
|
328
|
+
if (!withBlock && content === '') fs.unlinkSync(dest);
|
|
329
|
+
else fs.renameSync(temp, dest);
|
|
310
330
|
} finally {
|
|
311
331
|
if (fs.existsSync(temp)) fs.unlinkSync(temp);
|
|
312
332
|
}
|
|
313
|
-
console.log(!exists ? ` → Created ${name}`
|
|
333
|
+
console.log(!withBlock && content === '' ? ` ✕ ${name} (removed)` : !exists ? ` → Created ${name}`
|
|
314
334
|
: ` → ${withBlock ? 'Updated Devlyn defaults in' : 'Removed Devlyn defaults from'} ${name}; project-specific instructions preserved`);
|
|
315
335
|
return true;
|
|
316
336
|
}
|
package/bin/skill-history.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"source": "skills: config/skills/* and optional-skills/* trees from every first-parent HEAD commit touching those paths and every local tag, derived by scripts/update-skill-history.js using bin/skill-ownership.js normalization (LF, install-path stamps, OS metadata and Python bytecode)
|
|
2
|
+
"source": "skills: config/skills/* and optional-skills/* trees from every first-parent HEAD commit touching those paths and every local tag, derived by scripts/update-skill-history.js using bin/skill-ownership.js normalization (LF, install-path stamps, OS metadata and Python bytecode)",
|
|
3
3
|
"skills": {
|
|
4
4
|
"_shared": [
|
|
5
5
|
"015eea12cc07cfb9eb318c30855f6deacb5bf43bd508a511dffd2e48c9d82964",
|
|
@@ -619,74 +619,5 @@
|
|
|
619
619
|
"711cd2633df2062104f12d50037a07a1012a90ab18c422cb5fdf665fcb9dc987",
|
|
620
620
|
"d6a98c105a0d1952de90c39ca0734cb70cbdd116e6b4a4d45507d796bd2a9e88"
|
|
621
621
|
]
|
|
622
|
-
},
|
|
623
|
-
"commands": {
|
|
624
|
-
"commands/devlyn.handoff.md": [
|
|
625
|
-
"91d143c6305e95e4af6992eda0d127e94a394c2e91a2d409b4d41ca03c23f470"
|
|
626
|
-
],
|
|
627
|
-
"commands/devlyn.clean.md": [
|
|
628
|
-
"918c6ae867c57a4d9748237adae0df8c78a1447c7c028a0af988b59a5fbb0d9a"
|
|
629
|
-
],
|
|
630
|
-
"commands/devlyn.design-system.md": [
|
|
631
|
-
"f81166b5ba08d9ee1f5cff33c911ea387e5cb50141a9712a96fc4434c60a673f"
|
|
632
|
-
],
|
|
633
|
-
"commands/devlyn.design-ui.md": [
|
|
634
|
-
"0496a6b2dd8c6bb0474b71ef9a319a6b95bb76972d0f07a01c33f8860fe83dfb",
|
|
635
|
-
"4e2aafcc0de6ce7f3022a353aaa9fb4330e76a50a91ab970b9b9a0f192f9f1f2"
|
|
636
|
-
],
|
|
637
|
-
"commands/devlyn.discover-product.md": [
|
|
638
|
-
"a609401d29cd2bdad7a48c7c8f096b442fe03a17c6b2c3151936c47b4a16e6c6",
|
|
639
|
-
"1e66aa35bf300ec6167e1b888f43a92ebd9e60d501397ff0334c9a7809d433f5"
|
|
640
|
-
],
|
|
641
|
-
"commands/devlyn.evaluate.md": [
|
|
642
|
-
"8f17ccc639d54adab24c49dcf471376bab97511675d00554056a4c1f67a14115"
|
|
643
|
-
],
|
|
644
|
-
"commands/devlyn.feature-spec.md": [
|
|
645
|
-
"d442ed7ae8d14a255c37f1b120b758f69794be88e2970a8a0c0e51519344b6c7"
|
|
646
|
-
],
|
|
647
|
-
"commands/devlyn.implement-ui.md": [
|
|
648
|
-
"a8d0813781b2f7a3df5a4751c92755bf7c84c53428cf63cea34c965939b8c478"
|
|
649
|
-
],
|
|
650
|
-
"commands/devlyn.product-spec.md": [
|
|
651
|
-
"7a92d2df2586b44e2c4d9182eeb3d61f7adc2a13bed4d2cd26abcfe086585895"
|
|
652
|
-
],
|
|
653
|
-
"commands/devlyn.recommend-features.md": [
|
|
654
|
-
"2b92523017e1da10c299dafb19d84736de5084b5b46693de7fd2d6df51dc56b4"
|
|
655
|
-
],
|
|
656
|
-
"commands/devlyn.resolve.md": [
|
|
657
|
-
"48c0c3ee553f87f1f0733190effc7220c01c82837a5300ef68c9ffdf30c5d3a1",
|
|
658
|
-
"24595766e1a81a1fcacb78261b03c3665bc686bcac8b1805d1c4db379584879f",
|
|
659
|
-
"eaad5354cfc20ed378ffd2a2e1ca6bade1310bd2a47ab93e5942e9599c3ba9a8"
|
|
660
|
-
],
|
|
661
|
-
"commands/devlyn.review.md": [
|
|
662
|
-
"55888e08bf31ead1d359f218277cfa7efc3c6cb0f96fadc665d5a2406ff115d3",
|
|
663
|
-
"22f532f587f6d2fbf2662e38d5073b85a030141573f86d92ad40b9d292836fb7",
|
|
664
|
-
"7f9ce1db4ffd3cdaf9fd949e4b07de04e0931c91cd846d4ab220ff6114a45b17",
|
|
665
|
-
"3a511fc678e599ff4b4c37fd9191399a599e1ba9ea9c40fc4afcbe29cb188d2b"
|
|
666
|
-
],
|
|
667
|
-
"commands/devlyn.team-design-ui.md": [
|
|
668
|
-
"02485911588e9f52afb5a5082fd7804ee7a41bad44323960a2afe1a16a49d628"
|
|
669
|
-
],
|
|
670
|
-
"commands/devlyn.team-resolve.md": [
|
|
671
|
-
"8f11fa0c3203c0dd5bf09b5243dcff5968a36275682c54b3a012262fc545859a",
|
|
672
|
-
"09fc163d34dfe8906dd8d22501a028bfbbad639499acab01e5e60f06fc520382",
|
|
673
|
-
"ccfc5083813ee50d407109616ccbc2509aa7526316cebbf7a271537abdfff3b7",
|
|
674
|
-
"72f66be3c4c27a46e76d56d13a0d381880c054a03243188f98da2faf6dc77295",
|
|
675
|
-
"54c52c3f27998998ed55cc648d071bbb10444761b819c59ed118b795596a4aee"
|
|
676
|
-
],
|
|
677
|
-
"commands/devlyn.team-review.md": [
|
|
678
|
-
"58b36cfe2749968d697d486a104a1b139e4312730b0ac2933550634f6ece48db",
|
|
679
|
-
"7e5c2d93a23d51af7939dae29c70095dbd8a4a681cc06d4d578f2de29df346a3",
|
|
680
|
-
"c8e145955a303141af4082dae174e3b3f1681e4c9698e25ae337eadd31fb883b"
|
|
681
|
-
],
|
|
682
|
-
"commands/devlyn.update-docs.md": [
|
|
683
|
-
"86945afccf24762a88d26eef3703fbf6c5a90a3343fd3de72a9cb26b6f62d3b5"
|
|
684
|
-
],
|
|
685
|
-
"commands/devlyn.pencil-pull.md": [
|
|
686
|
-
"dfdd3d19ca676558bfa2bb3ce398181b0a081ab88d1feaf69c54c706a9671631"
|
|
687
|
-
],
|
|
688
|
-
"commands/devlyn.pencil-push.md": [
|
|
689
|
-
"5f2e8b29609cbb2af72941579823bec598265ebf29cd3a375d6aa47d95b74354"
|
|
690
|
-
]
|
|
691
622
|
}
|
|
692
623
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.21",
|
|
4
4
|
"description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — core engineering principles for CLAUDE.md/AGENTS.md and a loop designer that splits one intent into tasks agents drain hands-free",
|
|
5
5
|
"homepage": "https://github.com/fysoul17/devlyn-cli#readme",
|
|
6
6
|
"bin": {
|