devlyn-cli 4.2.8 → 4.2.10
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 +1 -1
- package/bin/devlyn.js +56 -25
- package/bin/skill-history.json +1 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +20 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ That's it. The installer asks:
|
|
|
35
35
|
|
|
36
36
|
In a project, AGENTS.md readers load the skills from `.agents/skills/`; Claude Code loads only `.claude/skills/` and reads AGENTS.md when the project has no CLAUDE.md; a CLAUDE.md the installer creates imports an existing AGENTS.md (`@AGENTS.md`). Global installs skills only: `~/.agents/skills/` (omp, Pi, Grok) and `~/.codex/skills/` (Codex), plus `~/.claude/skills/` for Claude Code. Every target gets the `devlyn-ideate` skill and the `devlyn-engines` utility. In Codex / omp / Pi, invoke them as skills (`$devlyn-ideate`); in Claude Code and Grok they're slash commands (`/devlyn-ideate`). Rerunning refreshes skills and the managed instruction block while preserving project rules outside it. See [Migration from earlier versions](#migration-from-earlier-versions) for legacy migration and merge recovery.
|
|
37
37
|
|
|
38
|
-
Without prompts, `npx devlyn-cli -y` installs AGENTS.md + `.agents/skills/` plus every target this project already has; add `--claude` for Claude Code. With `--global` it installs for every project on this machine, plus `~/.claude/skills/` with `--claude` or when it already has a devlyn install. After a project install or update, the installer names the changed files to commit so teammates and `/devlyn-ideate` drains can use them.
|
|
38
|
+
Without prompts, `npx devlyn-cli -y` installs AGENTS.md + `.agents/skills/` plus every target this project already has; add `--claude` for Claude Code. With `--global` it installs for every project on this machine, plus `~/.claude/skills/` with `--claude` or when it already has a devlyn install. After a project install or update, the installer names the changed files to commit, including installed files a `.gitignore` hides, so teammates and `/devlyn-ideate` drains can use them.
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
package/bin/devlyn.js
CHANGED
|
@@ -374,17 +374,18 @@ function cleanupDeprecated(skillsDir, written) {
|
|
|
374
374
|
// are in place. An optional skill found under an old spelling is installed under its new name
|
|
375
375
|
// before the old copy goes, so an interrupted migration never loses it. An unedited 0.6.0-0.7.1
|
|
376
376
|
// copy is refreshed in place. Any other folder under a 4.0 name is the user's own; `-y` leaves it.
|
|
377
|
-
function retireRenamedSkills(skillsDir, written) {
|
|
377
|
+
function retireRenamedSkills(skillsDir, written, installedSkills) {
|
|
378
378
|
const optional = new Set(OPTIONAL_ADDONS.filter((addon) => addon.type === 'local').map((addon) => addon.name));
|
|
379
379
|
const refreshed = [];
|
|
380
380
|
for (const [oldName, newName] of Object.entries(RENAMED_SKILLS)) {
|
|
381
381
|
const found = legacySkillPaths(skillsDir, oldName).filter((fullPath) => fs.existsSync(fullPath));
|
|
382
382
|
if (found.length > 0 && optional.has(newName)) {
|
|
383
|
-
installOptionalSkillInto(skillsDir, newName, written);
|
|
383
|
+
installOptionalSkillInto(skillsDir, newName, written, installedSkills);
|
|
384
384
|
refreshed.push(newName);
|
|
385
385
|
} else if (isShippedCopy(path.join(skillsDir, newName), PRE_STANDARD_SKILL_MD_SHA256)) {
|
|
386
386
|
refreshPreStandardCopy(skillsDir, newName);
|
|
387
387
|
written.push(path.join(skillsDir, newName));
|
|
388
|
+
installedSkills.set(path.join(skillsDir, newName), [path.join(skillsDir, newName, 'SKILL.md')]);
|
|
388
389
|
refreshed.push(newName);
|
|
389
390
|
}
|
|
390
391
|
for (const fullPath of found) {
|
|
@@ -396,7 +397,7 @@ function retireRenamedSkills(skillsDir, written) {
|
|
|
396
397
|
return refreshed;
|
|
397
398
|
}
|
|
398
399
|
|
|
399
|
-
function copyRecursive(src, dest, baseDir) {
|
|
400
|
+
function copyRecursive(src, dest, baseDir, files = []) {
|
|
400
401
|
const stats = fs.statSync(src);
|
|
401
402
|
|
|
402
403
|
if (stats.isDirectory()) {
|
|
@@ -405,7 +406,7 @@ function copyRecursive(src, dest, baseDir) {
|
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
for (const item of fs.readdirSync(src)) {
|
|
408
|
-
copyRecursive(path.join(src, item), path.join(dest, item), baseDir);
|
|
409
|
+
copyRecursive(path.join(src, item), path.join(dest, item), baseDir, files);
|
|
409
410
|
}
|
|
410
411
|
} else {
|
|
411
412
|
const destDir = path.dirname(dest);
|
|
@@ -413,8 +414,10 @@ function copyRecursive(src, dest, baseDir) {
|
|
|
413
414
|
fs.mkdirSync(destDir, { recursive: true });
|
|
414
415
|
}
|
|
415
416
|
fs.copyFileSync(src, dest);
|
|
417
|
+
files.push(dest);
|
|
416
418
|
log(` → ${path.relative(baseDir, dest)}`, 'dim');
|
|
417
419
|
}
|
|
420
|
+
return files;
|
|
418
421
|
}
|
|
419
422
|
|
|
420
423
|
function clearInstallMarker(skillsDir) {
|
|
@@ -615,14 +618,14 @@ function singleSelect(message, items, initial) {
|
|
|
615
618
|
}
|
|
616
619
|
|
|
617
620
|
// After preflight has cleared every selected addon before any change.
|
|
618
|
-
function installLocalSkill(skillName, roots, written = []) {
|
|
621
|
+
function installLocalSkill(skillName, roots, written = [], installedSkills = new Map()) {
|
|
619
622
|
if (!fs.existsSync(path.join(OPTIONAL_SKILLS_SOURCE, skillName))) {
|
|
620
623
|
log(` ⚠️ Skill "${skillName}" not found`, 'yellow');
|
|
621
624
|
return false;
|
|
622
625
|
}
|
|
623
626
|
log(`\n🛠️ Installing ${skillName}...`, 'cyan');
|
|
624
627
|
for (const root of roots) {
|
|
625
|
-
installOptionalSkillInto(root, skillName, written);
|
|
628
|
+
installOptionalSkillInto(root, skillName, written, installedSkills);
|
|
626
629
|
writeInstallMarker(root, [skillName], readInstallManifest(root));
|
|
627
630
|
written.push(path.join(root, DEVLYN_INSTALL_MARKER));
|
|
628
631
|
}
|
|
@@ -631,10 +634,10 @@ function installLocalSkill(skillName, roots, written = []) {
|
|
|
631
634
|
|
|
632
635
|
// One optional skill into one skill-loader directory, replacing any older copy of it. A
|
|
633
636
|
// copy under its name before the 4.0.0 rename goes only once the new one is complete.
|
|
634
|
-
function installOptionalSkillInto(target, skillName, written) {
|
|
637
|
+
function installOptionalSkillInto(target, skillName, written, installedSkills) {
|
|
635
638
|
const dest = path.join(target, skillName);
|
|
636
639
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
637
|
-
copyRecursive(path.join(OPTIONAL_SKILLS_SOURCE, skillName), dest, target);
|
|
640
|
+
installedSkills.set(dest, copyRecursive(path.join(OPTIONAL_SKILLS_SOURCE, skillName), dest, target));
|
|
638
641
|
assertCompleteSkillInstall(OPTIONAL_SKILLS_SOURCE, target, [skillName]);
|
|
639
642
|
written.push(dest);
|
|
640
643
|
const oldName = Object.keys(RENAMED_SKILLS).find((name) => RENAMED_SKILLS[name] === skillName);
|
|
@@ -696,7 +699,7 @@ function installAddon(addon) {
|
|
|
696
699
|
|
|
697
700
|
// The core skills into one skill root, replacing older copies and retired skills, then the
|
|
698
701
|
// marker that records a complete install.
|
|
699
|
-
function installCoreSkills(skillsDir, written = []) {
|
|
702
|
+
function installCoreSkills(skillsDir, written = [], installedSkills = new Map()) {
|
|
700
703
|
const sourceSkillsDir = path.join(CONFIG_SOURCE, 'skills');
|
|
701
704
|
log(`\n📁 Installing devlyn skills to ${skillsDir.replace(os.homedir(), '~')}`, 'green');
|
|
702
705
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
@@ -712,11 +715,11 @@ function installCoreSkills(skillsDir, written = []) {
|
|
|
712
715
|
if (!fs.existsSync(src)) continue;
|
|
713
716
|
// Full replace: copyRecursive is an overlay, so stale files would otherwise persist.
|
|
714
717
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
715
|
-
copyRecursive(src, dest, skillsDir);
|
|
718
|
+
installedSkills.set(dest, copyRecursive(src, dest, skillsDir));
|
|
716
719
|
written.push(dest);
|
|
717
720
|
}
|
|
718
721
|
assertCompleteSkillInstall(sourceSkillsDir, skillsDir, DEVLYN_CORE_SKILLS);
|
|
719
|
-
const refreshed = retireRenamedSkills(skillsDir, written);
|
|
722
|
+
const refreshed = retireRenamedSkills(skillsDir, written, installedSkills);
|
|
720
723
|
writeInstallMarker(skillsDir, [...DEVLYN_CORE_SKILLS, ...refreshed], previous);
|
|
721
724
|
written.push(path.join(skillsDir, DEVLYN_INSTALL_MARKER));
|
|
722
725
|
}
|
|
@@ -792,7 +795,7 @@ function agentsMdIsClaudeMd() {
|
|
|
792
795
|
return target !== null && fs.existsSync(target) && fs.realpathSync.native(target) === fs.realpathSync.native(claude);
|
|
793
796
|
}
|
|
794
797
|
|
|
795
|
-
function installAgentsProject(withClaude) {
|
|
798
|
+
function installAgentsProject(withClaude, installedSkills = new Map()) {
|
|
796
799
|
const written = [];
|
|
797
800
|
if (!agentsMdIsClaudeMd()) {
|
|
798
801
|
if (updateInstructions('AGENTS.md')) written.push('AGENTS.md');
|
|
@@ -802,7 +805,7 @@ function installAgentsProject(withClaude) {
|
|
|
802
805
|
throw new InstructionError('AGENTS.md is CLAUDE.md under another name here. Choose CLAUDE.md as well '
|
|
803
806
|
+ '(npx devlyn-cli -y --claude) so the devlyn block is written once, into CLAUDE.md.');
|
|
804
807
|
}
|
|
805
|
-
installCoreSkills(skillRoots('agents', false)[0], written);
|
|
808
|
+
installCoreSkills(skillRoots('agents', false)[0], written, installedSkills);
|
|
806
809
|
if (updateProjectGitignore('.agents/skills/.devlyn-install.json')) written.push('.gitignore');
|
|
807
810
|
return written;
|
|
808
811
|
}
|
|
@@ -834,7 +837,7 @@ function installClaudeInstructions() {
|
|
|
834
837
|
}
|
|
835
838
|
|
|
836
839
|
// Project CLAUDE.md and .claude/: skills and settings.
|
|
837
|
-
function installClaudeCore() {
|
|
840
|
+
function installClaudeCore(installedSkills = new Map()) {
|
|
838
841
|
const skillsDir = skillRoots('claude', false)[0];
|
|
839
842
|
const targetDir = path.dirname(skillsDir);
|
|
840
843
|
// Read before any write, so settings this install cannot merge leave the Claude target as it was.
|
|
@@ -866,7 +869,7 @@ function installClaudeCore() {
|
|
|
866
869
|
written.push(`.claude/${relPath}`);
|
|
867
870
|
}
|
|
868
871
|
}
|
|
869
|
-
installCoreSkills(skillsDir, written);
|
|
872
|
+
installCoreSkills(skillsDir, written, installedSkills);
|
|
870
873
|
if (updateProjectGitignore('.claude/skills/.devlyn-install.json')) written.push('.gitignore');
|
|
871
874
|
|
|
872
875
|
// One-hour foreground Bash calls and one-hour prompt caching serve a drain host that waits in the foreground.
|
|
@@ -924,30 +927,32 @@ function install(targets, global, optionalNames = []) {
|
|
|
924
927
|
}
|
|
925
928
|
const roots = targets.flatMap((target) => skillRoots(target, global));
|
|
926
929
|
const written = [];
|
|
930
|
+
const installedSkills = new Map();
|
|
927
931
|
if (!global) readProjectGitignore();
|
|
928
932
|
preflightSkills(roots, optionalNames,
|
|
929
933
|
!global && targets.includes('claude') ? path.dirname(skillRoots('claude', false)[0]) : null);
|
|
930
934
|
// AGENTS.md first, whatever order the menu gave: a new CLAUDE.md holds the block only when AGENTS.md does not.
|
|
931
935
|
for (const target of ['agents', 'claude'].filter((name) => targets.includes(name))) {
|
|
932
936
|
if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root));
|
|
933
|
-
else if (target === 'agents') written.push(...installAgentsProject(targets.includes('claude')));
|
|
934
|
-
else written.push(...installClaudeCore());
|
|
937
|
+
else if (target === 'agents') written.push(...installAgentsProject(targets.includes('claude'), installedSkills));
|
|
938
|
+
else written.push(...installClaudeCore(installedSkills));
|
|
935
939
|
}
|
|
936
|
-
for (const name of optionalNames) installLocalSkill(name, roots, written);
|
|
940
|
+
for (const name of optionalNames) installLocalSkill(name, roots, written, installedSkills);
|
|
937
941
|
log(`\n✅ devlyn ${PKG.version} installed`, 'green');
|
|
938
942
|
if (global) log('The principles block installs per project: run npx devlyn-cli in the project without --global.');
|
|
939
|
-
else noticeProjectChanges(written);
|
|
943
|
+
else noticeProjectChanges(written, installedSkills);
|
|
940
944
|
noticeGlobalDrift(roots);
|
|
941
945
|
return roots;
|
|
942
946
|
}
|
|
943
947
|
|
|
944
948
|
// Advisory only: a successful install stays successful when Git is unavailable or fails.
|
|
945
|
-
function noticeProjectChanges(paths) {
|
|
946
|
-
if (!paths.length) return;
|
|
949
|
+
function noticeProjectChanges(paths, installedSkills = new Map()) {
|
|
950
|
+
if (!paths.length && !installedSkills.size) return;
|
|
947
951
|
const cwd = projectDir();
|
|
948
|
-
|
|
952
|
+
const relative = (name) => path.relative(cwd, path.resolve(cwd, name)).split(path.sep).join('/');
|
|
953
|
+
paths = [...new Set(paths.map(relative))];
|
|
949
954
|
const env = { ...gitEnvironment(), GIT_OPTIONAL_LOCKS: '0' };
|
|
950
|
-
const git = (args) => spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env });
|
|
955
|
+
const git = (args, input) => spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env, input });
|
|
951
956
|
const repo = git(['rev-parse', '--show-toplevel']);
|
|
952
957
|
if (repo.error || repo.status !== 0) return;
|
|
953
958
|
const status = git(['status', '--porcelain=v1', '-z', '--untracked-files=all', '--no-renames',
|
|
@@ -973,10 +978,36 @@ function noticeProjectChanges(paths) {
|
|
|
973
978
|
return false;
|
|
974
979
|
}
|
|
975
980
|
})))];
|
|
976
|
-
|
|
981
|
+
// Check only files this run copied, grouped by skill: ignoring a whole skill is deliberate.
|
|
982
|
+
// check-ignore omits tracked files, so a complete committed install stays quiet on reruns.
|
|
983
|
+
const groups = [...installedSkills.values()].map((files) => files.map(relative));
|
|
984
|
+
const files = groups.flat();
|
|
985
|
+
let ignored = [];
|
|
986
|
+
let rule;
|
|
987
|
+
if (files.length) {
|
|
988
|
+
const result = git(['check-ignore', '-z', '--stdin'], files.join('\0') + '\0');
|
|
989
|
+
if (result.error || ![0, 1].includes(result.status)) return;
|
|
990
|
+
const matches = new Set(result.stdout.split('\0').filter(Boolean));
|
|
991
|
+
ignored = [...new Set(groups.flatMap((group) => {
|
|
992
|
+
const hidden = group.filter((file) => matches.has(file));
|
|
993
|
+
return hidden.length === group.length ? [] : hidden;
|
|
994
|
+
}))];
|
|
995
|
+
if (ignored.length) {
|
|
996
|
+
const verbose = git(['check-ignore', '-v', '-z', '--stdin'], ignored[0] + '\0');
|
|
997
|
+
if (verbose.error || verbose.status !== 0) return;
|
|
998
|
+
const [source, line, pattern] = verbose.stdout.split('\0');
|
|
999
|
+
rule = `${source}:${line}: ${pattern}`;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (!pending.length && !ignored.length) return;
|
|
977
1003
|
const quote = (name) => /^[a-zA-Z0-9_./-]+$/.test(name) ? name : `'${name.replace(/'/g, "'\\''")}'`;
|
|
978
1004
|
log('Commit these changes for teammates and /devlyn-ideate drains to use them:', 'yellow');
|
|
979
|
-
log(
|
|
1005
|
+
if (ignored.length) log(`.gitignore hides ${ignored.length} installed files (${rule}).`, 'yellow');
|
|
1006
|
+
const commands = [];
|
|
1007
|
+
if (pending.length) commands.push(`git add -- ${pending.map(quote).join(' ')}`);
|
|
1008
|
+
if (ignored.length) commands.push(`git add -f -- ${ignored.map(quote).join(' ')}`);
|
|
1009
|
+
commands.push(`git commit -m "chore(devlyn): update devlyn-cli to ${PKG.version}"`);
|
|
1010
|
+
log(` ${commands.join(' && ')}`, 'dim');
|
|
980
1011
|
}
|
|
981
1012
|
|
|
982
1013
|
// A devlyn install in a user root this run does not refresh drifts and can shadow the
|
package/bin/skill-history.json
CHANGED
|
@@ -312,6 +312,7 @@
|
|
|
312
312
|
"36b58dbdc26e51abc05a60872c4582faeebd67e9fe5626a3933467ba07c68dc4",
|
|
313
313
|
"40237cd5259dbcf7c490f130e844a9e32eea48a8edabd50800edee5029bf95a4",
|
|
314
314
|
"5536f8391ec4442f1b793dba8dd5d7820d45f4b6dadf1111cbac9d54469a01ca",
|
|
315
|
+
"573a6059a7383b37affd9c2e8bc6c88a4fb54117628a5a0746406cc32111e9aa",
|
|
315
316
|
"5824f07e455fe1c3942bf595a12e935258d4c0a851ca1bf741db915603d1c4e0",
|
|
316
317
|
"5954e371659ae1010757f0eec4b80dd65a899f9fcf76d4d84d9a75476a963218",
|
|
317
318
|
"65770e3f20c3a3afd2822e9acdcfc80b827f84a7737010b79332da70d431b05d",
|
|
@@ -486,19 +486,26 @@ def queue_view(anchor, recorded):
|
|
|
486
486
|
tracked.update(rel for rel in git(anchor, "ls-tree", "-r", "-z", "--name-only", head, "--", "docs/specs").split("\0")
|
|
487
487
|
if re.fullmatch(r"docs/specs/[^/]+/queue\.md", rel))
|
|
488
488
|
for rel in sorted(tracked, key=lambda rel: rel.split("/")[2]):
|
|
489
|
-
loop, data = rel.split("/")[2],
|
|
490
|
-
if loop
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
489
|
+
loop, data, read_error = rel.split("/")[2], None, None
|
|
490
|
+
if loop in recorded:
|
|
491
|
+
continue
|
|
492
|
+
try:
|
|
493
|
+
data = (anchor / rel).read_bytes() if (anchor / rel).is_file() else None
|
|
494
|
+
except OSError as exc:
|
|
495
|
+
read_error = exc
|
|
496
|
+
# A directory name alone proves nothing. A task row in the checkout or HEAD proves this is a loop queue; HEAD
|
|
497
|
+
# keeps that proof when the checkout loses its rows or cannot be read. Only a proven queue is parsed or blocks.
|
|
498
|
+
known = False
|
|
499
|
+
for candidate in (data, show(anchor, head, rel) if head else None):
|
|
500
|
+
for line in (candidate or b"").decode("utf-8", errors="replace").splitlines():
|
|
501
|
+
match = ROW_RE.fullmatch(line)
|
|
502
|
+
item = ITEM_RE.fullmatch(match["text"].rstrip()) if match else None
|
|
503
|
+
if item and item["loop"] == loop and bool(item["rest"]) == (match["mark"] == "F"):
|
|
504
|
+
known = True
|
|
505
|
+
if known:
|
|
506
|
+
if read_error is not None:
|
|
507
|
+
raise LoopError(f"{anchor / rel}: {read_error}") from read_error
|
|
508
|
+
loops[loop] = parse_loop(data, loop, anchor / rel)
|
|
502
509
|
claims = {}
|
|
503
510
|
for loop, (_, target) in loops.items():
|
|
504
511
|
if target:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.10",
|
|
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": {
|