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.
@@ -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(process.cwd(), 'package.json');
201
- if (fs.existsSync(path.join(process.cwd(), 'bin', 'devlyn.js')) && fs.existsSync(projectPackage)
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 templatePath = path.join(__dirname, '..', name);
207
- const template = fs.readFileSync(templatePath, 'utf8');
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(process.cwd(), name);
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.dev && stat.ino === sourceStat.ino) {
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(process.cwd(), '.devlyn', 'instructions');
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 (current.includes(BEGIN) || current.includes(END)) {
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, name,
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[name] || []),
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, name, template);
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.renameSync(temp, dest);
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
  }
@@ -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); deprecated commands: commands/ and optional-commands/*/ files of devlyn-cli 0.0.1-0.5.8 (every published version that shipped them), LF-normalized SHA-256",
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.19",
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": {