devlyn-cli 4.2.18 → 4.2.19

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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/bin/devlyn.js +93 -55
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -120,8 +120,9 @@ or delete them; delete `~/.grok/skills`, since Grok now reads `~/.agents/skills`
120
120
  Upgrading to 4.2.0: `/devlyn-resolve` and the pipeline helpers only it used are retired;
121
121
  no measured replacement ships in their place. Run the installer again where devlyn is
122
122
  installed. It removes the `devlyn-resolve` folders (and pre-4.0.0 `devlyn:resolve` ones)
123
- where it installs, removes the Stop hook it added to `.claude/settings.json` while keeping
124
- your own hooks, and names the other settings it once added for the pipeline
123
+ only when they are unchanged devlyn-cli copies; others are left in place and named. It
124
+ removes the Stop hook it added to `.claude/settings.json` while keeping your own hooks,
125
+ and names the other settings it once added for the pipeline
125
126
  (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` and its `.devlyn` and git permissions) without
126
127
  deleting them. `/devlyn-engines` keeps only the executor pin: `pair_judge_priority` and
127
128
  `roles` in `.devlyn/engines.json` stay as written, select nothing, and `clear` removes them.
@@ -147,7 +148,7 @@ devlyn skill by its old name or path (for example Codex `[[skills.config]]` or o
147
148
  `disabledExtensions`) needs the new name. A project that committed the old `devlyn:*`
148
149
  folders checks out on Windows once that re-sync is committed.
149
150
 
150
- Earlier versions of devlyn-cli shipped 16+ slash commands. The iter-0034 Phase 4 cutover (2026-05-04) and the 2026-05-14 follow-up consolidated them, 4.0.0 renamed every devlyn skill to the Agent Skills naming standard (`devlyn-<name>`: lowercase, digits and hyphens, the same as its folder), and after 4.1.0 resolve and design-ui were retired and the queue moved into ideate. Upgrades automatically purge the legacy skill directories.
151
+ Earlier versions of devlyn-cli shipped 16+ slash commands. The iter-0034 Phase 4 cutover (2026-05-04) and the 2026-05-14 follow-up consolidated them, 4.0.0 renamed every devlyn skill to the Agent Skills naming standard (`devlyn-<name>`: lowercase, digits and hyphens, the same as its folder), and after 4.1.0 resolve and design-ui were retired and the queue moved into ideate. Upgrades remove legacy skill directories only when they are unchanged devlyn-cli copies; others are left in place and named.
151
152
 
152
153
  | Old command | Now use |
153
154
  |---|---|
package/bin/devlyn.js CHANGED
@@ -110,12 +110,13 @@ function hasDevlynInstall(root) {
110
110
  const stat = (name) => fs.lstatSync(name, { throwIfNoEntry: false });
111
111
  if (!stat(root)?.isDirectory()) return false;
112
112
  const optional = new Set(OPTIONAL_ADDONS.map((addon) => addon.name));
113
- const names = [...DEVLYN_CORE_SKILLS, ...DEPRECATED_DIRS.map((name) => path.basename(name)),
113
+ const names = [...DEVLYN_CORE_SKILLS,
114
114
  ...Object.keys(RENAMED_SKILLS)].filter((name) => name.startsWith('devlyn')
115
115
  && !optional.has(name.replace(/[:\uF03A]/g, '-')));
116
116
  return !!stat(path.join(root, DEVLYN_INSTALL_LOCK))
117
117
  || names.some((name) => [...legacySkillPaths(root, name.replace(/^devlyn-/, 'devlyn:')),
118
118
  path.join(root, name.replace(/:/g, '-'))].some((full) => stat(full)?.isDirectory()))
119
+ || planDeprecatedSkills(root, {}).removals.size > 0
119
120
  || (!!stat(path.join(root, DEVLYN_INSTALL_MARKER)) && stat(path.join(root, '_shared'))?.isDirectory());
120
121
  } catch (error) {
121
122
  if (error.code === 'ENOTDIR') return false;
@@ -136,8 +137,15 @@ function hasDevlynClaude(global) {
136
137
  const optional = new Set(OPTIONAL_ADDONS.map((addon) => addon.name));
137
138
  const instructions = path.join(path.dirname(claudeDir), 'CLAUDE.md');
138
139
  const claude = fs.lstatSync(instructions, { throwIfNoEntry: false })?.isFile() ? fs.readFileSync(instructions, 'utf8') : '';
139
- return names(path.join(claudeDir, 'skills')).some((name) => name === DEVLYN_INSTALL_MARKER
140
- || (name.startsWith('devlyn') && !optional.has(name.replace(/[:\uF03A]/g, '-'))))
140
+ const root = path.join(claudeDir, 'skills');
141
+ const deprecated = new Map([...deprecatedSkillPaths(root)].map((full) => [path.basename(full).toLowerCase(), full]));
142
+ const { removals } = planDeprecatedSkills(root, {});
143
+ return names(root).some((name) => {
144
+ const canonical = deprecated.get(name.toLowerCase());
145
+ if (canonical) return removals.has(canonical);
146
+ return name === DEVLYN_INSTALL_MARKER
147
+ || (name.startsWith('devlyn') && !optional.has(name.replace(/[:\uF03A]/g, '-')));
148
+ })
141
149
  || names(path.join(claudeDir, 'commands')).some((name) => name.startsWith('devlyn.'))
142
150
  || holdsDevlynDefaults('CLAUDE.md', claude) || importsAgentsDefaults(claude);
143
151
  }
@@ -368,24 +376,13 @@ function listContents() {
368
376
  }
369
377
 
370
378
  // Old and retired skills in one skill root; nothing outside it.
371
- function cleanupDeprecated(skillsDir, written) {
379
+ function cleanupDeprecated(skillsDir, written, removals) {
372
380
  let removed = 0;
373
- const deprecated = DEPRECATED_DIRS.flatMap((relPath) =>
374
- legacySkillPaths(skillsDir, path.basename(relPath)).map((fullPath) => [relPath, fullPath]));
375
- for (const [relPath, fullPath] of deprecated) {
376
- if (fs.existsSync(fullPath)) {
377
- fs.rmSync(fullPath, { recursive: true });
378
- written.push(fullPath);
379
- log(` ✕ ${relPath}/ (removed)`, 'dim');
380
- removed++;
381
- }
382
- }
383
- for (const [name, hashes] of Object.entries(RETIRED_SKILL_MD_SHA256)) {
384
- const fullPath = path.join(skillsDir, name);
385
- if (isShippedCopy(fullPath, hashes)) {
381
+ for (const fullPath of removals) {
382
+ if (path.dirname(fullPath) === skillsDir) {
386
383
  fs.rmSync(fullPath, { recursive: true });
387
384
  written.push(fullPath);
388
- log(` ✕ skills/${name}/ (removed)`, 'dim');
385
+ log(` ✕ skills/${path.basename(fullPath)}/ (removed)`, 'dim');
389
386
  removed++;
390
387
  }
391
388
  }
@@ -462,24 +459,65 @@ function readInstallManifest(root) {
462
459
  // touching any target. Optional addons must be selected before this preflight too.
463
460
  function plannedSkills(root, optionalNames = []) {
464
461
  const names = new Set([...DEVLYN_CORE_SKILLS, ...optionalNames]);
465
- for (const relative of DEPRECATED_DIRS) {
466
- for (const full of legacySkillPaths(root, path.basename(relative))) names.add(path.basename(full));
467
- }
468
462
  for (const [oldName, newName] of Object.entries(RENAMED_SKILLS)) {
469
463
  const oldPaths = legacySkillPaths(root, oldName);
470
464
  oldPaths.forEach((full) => names.add(path.basename(full)));
471
465
  if (oldPaths.some((full) => fs.lstatSync(full, { throwIfNoEntry: false }))
472
466
  || isShippedCopy(path.join(root, newName), PRE_STANDARD_SKILL_MD_SHA256)) names.add(newName);
473
467
  }
468
+ return names;
469
+ }
470
+
471
+ function verifySkill(dest, name, manifest, removalOnly = false) {
472
+ const actual = fingerprint(dest);
473
+ const logicalName = name.replace(/[:\uF03A]/g, '-');
474
+ const source = path.join(DEVLYN_CORE_SKILLS.includes(logicalName)
475
+ ? path.join(CONFIG_SOURCE, 'skills') : OPTIONAL_SKILLS_SOURCE, logicalName);
476
+ const current = !removalOnly && fs.existsSync(source) && fingerprint(source) === actual;
477
+ const historical = SKILL_HISTORY.skills[logicalName]?.includes(actual);
478
+ const hashes = RETIRED_SKILL_MD_SHA256[name] || (removalOnly ? new Set() : PRE_STANDARD_SKILL_MD_SHA256);
479
+ const singleFile = isShippedCopy(dest, hashes);
480
+ if (!current && !historical && !singleFile && manifest[name] !== actual) {
481
+ throw new Error(manifest[name] ? 'modified contents (install manifest fingerprint mismatch)'
482
+ : fs.existsSync(source) || SKILL_HISTORY.skills[logicalName]?.length
483
+ ? 'fingerprint mismatch with known shipped copies'
484
+ : 'unknown installer ownership (no fingerprint history for this copy)');
485
+ }
486
+ }
487
+
488
+ // Removal-only paths never block an install: unverifiable copies stay with their owner.
489
+ function deprecatedSkillPaths(root) {
490
+ return new Set(DEPRECATED_DIRS.flatMap((relative) => legacySkillPaths(root, path.basename(relative))));
491
+ }
492
+
493
+ function planDeprecatedSkills(root, manifest) {
494
+ const removals = new Set();
495
+ const retained = new Set();
496
+ for (const dest of deprecatedSkillPaths(root)) {
497
+ try {
498
+ if (!fs.lstatSync(dest, { throwIfNoEntry: false })) continue;
499
+ verifySkill(dest, path.basename(dest), manifest, true);
500
+ removals.add(dest);
501
+ } catch (error) {
502
+ retained.add(dest);
503
+ }
504
+ }
505
+ return { removals, retained };
506
+ }
507
+
508
+ function planSkillRemovals(root, manifest) {
509
+ const plan = planDeprecatedSkills(root, manifest);
474
510
  for (const [name, hashes] of Object.entries(RETIRED_SKILL_MD_SHA256)) {
475
- if (isShippedCopy(path.join(root, name), hashes)) names.add(name);
511
+ const dest = path.join(root, name);
512
+ if (isShippedCopy(dest, hashes)) plan.removals.add(dest);
476
513
  }
477
- return names;
514
+ return plan;
478
515
  }
479
516
 
480
517
  // `commandDir`: the project .claude/ whose DEPRECATED_FILES this install deletes, or null.
481
518
  function preflightSkills(roots, optionalNames = [], commandDir = null) {
482
519
  const conflicts = [];
520
+ const plan = { removals: new Set(), retained: new Set() };
483
521
  try {
484
522
  // Do not follow a commands link into another installation either.
485
523
  const commands = commandDir && fs.lstatSync(path.join(commandDir, 'commands'), { throwIfNoEntry: false });
@@ -511,6 +549,9 @@ function preflightSkills(roots, optionalNames = [], commandDir = null) {
511
549
  conflicts.push(`${path.join(root, DEVLYN_INSTALL_MARKER)}: ${error.message}`);
512
550
  continue;
513
551
  }
552
+ const deprecated = planSkillRemovals(root, manifest);
553
+ for (const dest of deprecated.removals) plan.removals.add(dest);
554
+ for (const dest of deprecated.retained) plan.retained.add(dest);
514
555
  for (const name of plannedSkills(root, optionalNames)) {
515
556
  const dest = path.join(root, name);
516
557
  const isTracked = tracked.some((file) => file.toLowerCase() === dest.toLowerCase()
@@ -520,19 +561,7 @@ function preflightSkills(roots, optionalNames = [], commandDir = null) {
520
561
  if (isTracked) throw new Error('missing contents; cannot verify installer ownership');
521
562
  continue;
522
563
  }
523
- const actual = fingerprint(dest);
524
- const logicalName = name.replace(/[:\uF03A]/g, '-');
525
- const source = path.join(DEVLYN_CORE_SKILLS.includes(logicalName)
526
- ? path.join(CONFIG_SOURCE, 'skills') : OPTIONAL_SKILLS_SOURCE, logicalName);
527
- const current = fs.existsSync(source) && fingerprint(source) === actual;
528
- const historical = SKILL_HISTORY.skills[logicalName]?.includes(actual);
529
- const singleFile = isShippedCopy(dest, RETIRED_SKILL_MD_SHA256[name] || PRE_STANDARD_SKILL_MD_SHA256);
530
- if (!current && !historical && !singleFile && manifest[name] !== actual) {
531
- throw new Error(manifest[name] ? 'modified contents (install manifest fingerprint mismatch)'
532
- : fs.existsSync(source) || SKILL_HISTORY.skills[logicalName]?.length
533
- ? 'fingerprint mismatch with known shipped copies'
534
- : 'unknown installer ownership (no fingerprint history for this copy)');
535
- }
564
+ verifySkill(dest, name, manifest);
536
565
  } catch (error) {
537
566
  conflicts.push(`${dest}: ${isTracked ? 'git-tracked; ' : ''}${error.message}`);
538
567
  }
@@ -548,6 +577,7 @@ function preflightSkills(roots, optionalNames = [], commandDir = null) {
548
577
  + '\nFor tracked paths, also remove them from the Git index.'
549
578
  + '\nRerun the same command after resolving every conflict; -y does not bypass this check.');
550
579
  }
580
+ return plan;
551
581
  }
552
582
 
553
583
  function assertCompleteSkillInstall(sourceSkillsDir, skillsDir, skillNames) {
@@ -721,13 +751,14 @@ function installAddon(addon) {
721
751
 
722
752
  // The core skills into one skill root, replacing older copies and retired skills, then the
723
753
  // marker that records a complete install.
724
- function installCoreSkills(skillsDir, written = [], installedSkills = new Map()) {
754
+ function installCoreSkills(skillsDir, written = [], installedSkills = new Map(), plan) {
725
755
  const sourceSkillsDir = path.join(CONFIG_SOURCE, 'skills');
726
756
  log(`\n📁 Installing devlyn skills to ${skillsDir.replace(os.homedir(), '~')}`, 'green');
727
757
  fs.mkdirSync(skillsDir, { recursive: true });
728
758
  const previous = readInstallManifest(skillsDir);
759
+ plan ??= planSkillRemovals(skillsDir, previous);
729
760
  clearInstallMarker(skillsDir);
730
- const removed = cleanupDeprecated(skillsDir, written);
761
+ const removed = cleanupDeprecated(skillsDir, written, plan.removals);
731
762
  if (removed > 0) {
732
763
  log(`\n🧹 Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
733
764
  }
@@ -817,7 +848,7 @@ function agentsMdIsClaudeMd() {
817
848
  return target !== null && fs.existsSync(target) && fs.realpathSync.native(target) === fs.realpathSync.native(claude);
818
849
  }
819
850
 
820
- function installAgentsProject(withClaude, installedSkills = new Map()) {
851
+ function installAgentsProject(withClaude, installedSkills = new Map(), plan) {
821
852
  const written = [];
822
853
  if (!agentsMdIsClaudeMd()) {
823
854
  if (updateInstructions('AGENTS.md')) written.push('AGENTS.md');
@@ -827,7 +858,7 @@ function installAgentsProject(withClaude, installedSkills = new Map()) {
827
858
  throw new InstructionError('AGENTS.md is CLAUDE.md under another name here. Choose CLAUDE.md as well '
828
859
  + '(npx devlyn-cli -y --claude) so the devlyn block is written once, into CLAUDE.md.');
829
860
  }
830
- installCoreSkills(skillRoots('agents', false)[0], written, installedSkills);
861
+ installCoreSkills(skillRoots('agents', false)[0], written, installedSkills, plan);
831
862
  if (updateProjectGitignore('.agents/skills/.devlyn-install.json')) written.push('.gitignore');
832
863
  return written;
833
864
  }
@@ -859,7 +890,7 @@ function installClaudeInstructions() {
859
890
  }
860
891
 
861
892
  // Project CLAUDE.md and .claude/: skills and settings.
862
- function installClaudeCore(installedSkills = new Map()) {
893
+ function installClaudeCore(installedSkills = new Map(), plan) {
863
894
  const skillsDir = skillRoots('claude', false)[0];
864
895
  const targetDir = path.dirname(skillsDir);
865
896
  // Read before any write, so settings this install cannot merge leave the Claude target as it was.
@@ -891,7 +922,7 @@ function installClaudeCore(installedSkills = new Map()) {
891
922
  written.push(`.claude/${relPath}`);
892
923
  }
893
924
  }
894
- installCoreSkills(skillsDir, written, installedSkills);
925
+ installCoreSkills(skillsDir, written, installedSkills, plan);
895
926
  if (updateProjectGitignore('.claude/skills/.devlyn-install.json')) written.push('.gitignore');
896
927
 
897
928
  // One-hour foreground Bash calls and one-hour prompt caching serve a drain host that waits in the foreground.
@@ -1007,7 +1038,7 @@ function withUserSkillLocks(roots, action) {
1007
1038
  }
1008
1039
 
1009
1040
  // Installs the targets in one scope; returns the skill roots written.
1010
- function install(targets, global, optionalNames = []) {
1041
+ function install(targets, global, optionalNames = [], plan) {
1011
1042
  // In the home folder CLAUDE.md, AGENTS.md (agents read a parent folder's too) and
1012
1043
  // .claude/settings.json would apply to every project. Same folder by identity, so a link or
1013
1044
  // another spelling of the path is caught too.
@@ -1022,23 +1053,32 @@ function install(targets, global, optionalNames = []) {
1022
1053
  const written = [];
1023
1054
  const installedSkills = new Map();
1024
1055
  if (!global) readProjectGitignore();
1025
- preflightSkills(roots, optionalNames,
1056
+ plan ??= preflightSkills(roots, optionalNames,
1026
1057
  !global && targets.includes('claude') ? path.dirname(skillRoots('claude', false)[0]) : null);
1027
1058
  // AGENTS.md first, whatever order the menu gave: a new CLAUDE.md holds the block only when AGENTS.md does not.
1028
1059
  for (const target of ['agents', 'claude'].filter((name) => targets.includes(name))) {
1029
- if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root));
1030
- else if (target === 'agents') written.push(...installAgentsProject(targets.includes('claude'), installedSkills));
1031
- else written.push(...installClaudeCore(installedSkills));
1060
+ if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root, [], installedSkills, plan));
1061
+ else if (target === 'agents') written.push(...installAgentsProject(targets.includes('claude'), installedSkills, plan));
1062
+ else written.push(...installClaudeCore(installedSkills, plan));
1032
1063
  }
1033
1064
  for (const name of optionalNames) installLocalSkill(name, roots, written, installedSkills);
1034
1065
  log(`\n✅ devlyn ${PKG.version} installed`, 'green');
1035
1066
  if (global) log('The principles block installs per project: run npx devlyn-cli in the project without --global.');
1036
1067
  noticeGlobalDrift(roots);
1068
+ noticeRetainedSkills(plan.retained);
1037
1069
  if (!global) noticeProjectChanges(written, installedSkills);
1038
1070
  return roots;
1039
1071
  });
1040
1072
  }
1041
1073
 
1074
+ function noticeRetainedSkills(retained) {
1075
+ if (!retained.size) return;
1076
+ log('Left in place (not verified as unchanged devlyn-cli copies):\n'
1077
+ + [...retained].map((dest) => ` ${dest}`).join('\n')
1078
+ + '\nThese retired skills may still be visible to agents and may call removed helpers. '
1079
+ + 'Move them out of the skills folder, or delete them, if you no longer use them.', 'yellow');
1080
+ }
1081
+
1042
1082
  // Advisory only: a successful install stays successful when Git is unavailable or fails.
1043
1083
  function noticeProjectChanges(paths, installedSkills = new Map()) {
1044
1084
  if (!paths.length && !installedSkills.size) return;
@@ -1095,7 +1135,7 @@ function noticeProjectChanges(paths, installedSkills = new Map()) {
1095
1135
  }
1096
1136
  if (!pending.length && !ignored.length) return;
1097
1137
  const quote = (name) => /^[a-zA-Z0-9_./-]+$/.test(name) ? name : `'${name.replace(/'/g, "'\\''")}'`;
1098
- log('Commit these changes for teammates and /devlyn-ideate drains to use them:', 'yellow');
1138
+ log('Optional — commit (and push) these changes if teammates or /devlyn-ideate task worktrees should use them:', 'yellow');
1099
1139
  if (ignored.length) log(`.gitignore hides ${ignored.length} installed files (${rule}).`, 'yellow');
1100
1140
  const commands = [];
1101
1141
  if (pending.length) commands.push(`git add -- ${pending.map(quote).join(' ')}`);
@@ -1167,11 +1207,8 @@ function update() {
1167
1207
  }
1168
1208
 
1169
1209
  return withUserSkillLocks(userRoots, () => {
1170
- if (targets.length) {
1171
- readProjectGitignore();
1172
- preflightSkills(projectRoots, [], targets.includes('claude') ? path.dirname(skillRoots('claude', false)[0]) : null);
1173
- }
1174
- preflightSkills(userRoots);
1210
+ if (targets.length) readProjectGitignore();
1211
+ const plan = preflightSkills(roots, [], targets.includes('claude') ? path.dirname(skillRoots('claude', false)[0]) : null);
1175
1212
  // Ownership preflight has already refused unreadable or invalid markers.
1176
1213
  const current = PKG.version.split('.').map(BigInt);
1177
1214
  const newer = roots.flatMap((root) => {
@@ -1194,11 +1231,12 @@ function update() {
1194
1231
  + newer.join('\n') + '\nRun npx devlyn-cli@latest update.');
1195
1232
  }
1196
1233
 
1197
- userRoots.forEach((root) => installCoreSkills(root));
1198
- if (targets.length) install(targets, false);
1234
+ userRoots.forEach((root) => installCoreSkills(root, [], new Map(), plan));
1235
+ if (targets.length) install(targets, false, [], plan);
1199
1236
  else {
1200
1237
  log(`\n✅ devlyn ${PKG.version} installed`, 'green');
1201
1238
  noticeGlobalDrift(userRoots);
1239
+ noticeRetainedSkills(plan.retained);
1202
1240
  }
1203
1241
  });
1204
1242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.2.18",
3
+ "version": "4.2.19",
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": {