devlyn-cli 4.2.17 → 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.
- package/README.md +6 -3
- package/bin/devlyn.js +93 -55
- package/bin/skill-history.json +2 -0
- package/config/skills/_shared/platform-support.py +13 -0
- package/config/skills/_shared/task-completion.md +4 -2
- package/config/skills/devlyn-ideate/references/loop.md +1 -1
- package/config/skills/devlyn-ideate/scripts/queue.py +15 -1
- 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
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|---|---|
|
|
@@ -231,6 +232,8 @@ Only `__pycache__` directories containing exclusively regular `*.pyc` files coun
|
|
|
231
232
|
|
|
232
233
|
On native Windows, use native Node/npm and Python plus Git for Windows Bash for the shipped shell wrapper. Run `npx devlyn-cli -y` in the project (add `--claude` for Claude Code). Skill folders follow the Agent Skills naming standard (for example `devlyn-ideate`), so a project that commits them checks out on Windows. Harness text is UTF-8 without requiring `PYTHONUTF8`.
|
|
233
234
|
|
|
235
|
+
New default drain roots disable Windows content indexing for new children; names stay indexed and Microsoft Defender scanning is unaffected. For Defender performance, use a Dev Drive via `--worktree-root` or an administrator's exclusion for the root; see [loop options](config/skills/devlyn-ideate/references/loop.md).
|
|
236
|
+
|
|
234
237
|
Windows completion preserves the workspace, owned refs and recovery receipt when writer cessation cannot be proved, even after merge; resume guidance and delivery status remain separate from product verification. The portability workflow checks out the repository on native Windows and tests the POSIX-packed npm artifact there. A POSIX pass alone does not establish Windows support: acceptance requires the passing Windows job for the exact source SHA/artifact hashes.
|
|
235
238
|
|
|
236
239
|
## Contributing
|
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,
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
374
|
-
|
|
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/${
|
|
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
|
-
|
|
511
|
+
const dest = path.join(root, name);
|
|
512
|
+
if (isShippedCopy(dest, hashes)) plan.removals.add(dest);
|
|
476
513
|
}
|
|
477
|
-
return
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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/bin/skill-history.json
CHANGED
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"a9c5242608e8b6f660f3ae9daf3fdefb210b6e4603bddee3d26ba59a55be4085",
|
|
112
112
|
"aa0d6fad740ead835720a528f197b29e6cd0b209d3b9db1b312b9895f991b037",
|
|
113
113
|
"ac3d475156af89e2e887118e92b972dbfefde6f0a07bbbdb68ba88fce5f7904d",
|
|
114
|
+
"ac404e4e846557b4c7b32050dee5f7c83648311b7d1cecdbdcae9eeda1b8eaf7",
|
|
114
115
|
"ac5485ee9218c7038c6d329a3877925995c5afcb2dbd16b1da4495ed769d2083",
|
|
115
116
|
"add41e1257ed525d6f225d9eb976a3978f126e604ca7c0f5aa1b223af78f1902",
|
|
116
117
|
"afb478a981c4fd91f66fbe10d04481cd4562825fd6785ba0aadc6ce3ee3da173",
|
|
@@ -322,6 +323,7 @@
|
|
|
322
323
|
"5954e371659ae1010757f0eec4b80dd65a899f9fcf76d4d84d9a75476a963218",
|
|
323
324
|
"65770e3f20c3a3afd2822e9acdcfc80b827f84a7737010b79332da70d431b05d",
|
|
324
325
|
"65c781764633b912e5947362eba0837c8027461a904975401d79ad4fe8714084",
|
|
326
|
+
"694c8920e50d92cba46dbb27eb44dd88035a89ed625d3cbf9f2bce9366d44c96",
|
|
325
327
|
"6c3e2e4b84b0b4f81aed3759704da7c14f3cbabf3beeb0d1654feda3a15a8675",
|
|
326
328
|
"6d9df58c117165e9d297aa6be0df8d076bac1ed49dc29ae93906af1969798ab1",
|
|
327
329
|
"6e7228610628fc4bca31dc0b0aaf5eb4445e1f123f2c54bd13921c5cd01129fe",
|
|
@@ -115,6 +115,15 @@ def system_exit_code(code):
|
|
|
115
115
|
return code - 0x100000000 if os.name == "nt" and code >= 0x80000000 else code
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def mark_not_content_indexed(path):
|
|
119
|
+
"""Disable Windows content indexing for a directory and children created afterwards."""
|
|
120
|
+
if os.name != "nt":
|
|
121
|
+
return
|
|
122
|
+
attrs = _kernel.GetFileAttributesW(str(path))
|
|
123
|
+
_checked(attrs != 0xFFFFFFFF, _kernel.GetFileAttributesW, (str(path),)) # INVALID_FILE_ATTRIBUTES.
|
|
124
|
+
_kernel.SetFileAttributesW(str(path), attrs | stat.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
|
|
125
|
+
|
|
126
|
+
|
|
118
127
|
if os.name == "nt":
|
|
119
128
|
import _winapi
|
|
120
129
|
import ctypes
|
|
@@ -164,6 +173,7 @@ if os.name == "nt":
|
|
|
164
173
|
("DuplicateHandle", [wintypes.HANDLE, wintypes.HANDLE, wintypes.HANDLE, ctypes.POINTER(wintypes.HANDLE), wintypes.DWORD, wintypes.BOOL, wintypes.DWORD], wintypes.BOOL),
|
|
165
174
|
("CloseHandle", [wintypes.HANDLE], wintypes.BOOL),
|
|
166
175
|
("PeekNamedPipe", [wintypes.HANDLE, ctypes.c_void_p, wintypes.DWORD, ctypes.c_void_p, ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p], wintypes.BOOL),
|
|
176
|
+
("SetFileAttributesW", [wintypes.LPCWSTR, wintypes.DWORD], wintypes.BOOL),
|
|
167
177
|
):
|
|
168
178
|
_function = getattr(_kernel, _name)
|
|
169
179
|
_function.argtypes, _function.restype, _function.errcheck = _args, _result, _checked
|
|
@@ -171,6 +181,9 @@ if os.name == "nt":
|
|
|
171
181
|
_kernel.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
172
182
|
_kernel.WaitForSingleObject.restype = wintypes.DWORD # WAIT_OBJECT_0 is successful zero, not BOOL.
|
|
173
183
|
|
|
184
|
+
_kernel.GetFileAttributesW.argtypes = [wintypes.LPCWSTR]
|
|
185
|
+
_kernel.GetFileAttributesW.restype = wintypes.DWORD # Failure is INVALID_FILE_ATTRIBUTES, not zero.
|
|
186
|
+
|
|
174
187
|
def _read_control(fd, count):
|
|
175
188
|
deadline = time.monotonic() + 10
|
|
176
189
|
data = b""
|
|
@@ -52,10 +52,12 @@ not write to the shared checkout. Queue drains retain their allocation order.
|
|
|
52
52
|
```sh
|
|
53
53
|
python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
|
|
54
54
|
--task '<task identity>' --branch '<absent task branch>' \
|
|
55
|
-
--worktree '<
|
|
55
|
+
--worktree '<repo parent>/<repo name>.devlyn.noindex/<task directory>' \
|
|
56
|
+
--repository '<owner/repo>' --remote origin --base main
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
Every delivery allocation owns a linked worktree; `--worktree` is required
|
|
59
|
+
Every delivery allocation owns a linked worktree; `--worktree` is required and
|
|
60
|
+
must name an absent path.
|
|
59
61
|
A publication allocation normally starts at the exact fetched remote base,
|
|
60
62
|
independent of the anchor's branch or dirty state, and leaves the anchor's
|
|
61
63
|
HEAD, index and files untouched. For a commit request, local-only work or
|
|
@@ -14,7 +14,7 @@ Both print one JSON object; exit 1 is `BLOCKED` with a `reason`.
|
|
|
14
14
|
- `status` takes `queue.lock` in the repository's Git directory and finishes `add`'s cleanup (step 1); it reports the running `skill` root and version, any `warnings`, reconciled counts (`pending`, `active`, `accepted`, `failed`, `blocked`, `legacy_pending`), the `next` task, `blockers`, deliveries still pending with their resume (drain again until the terminal commit is attached, then the printed completion command; add `--writers-stopped` only after stopping the task's writers), and under `cleanup` the package copies `add` kept or could not remove.
|
|
15
15
|
- `drain` ends `DRAINED` (no runnable task remains; open deliveries show per task), `WAITING` (a task waits with its reason, or legacy rows need planning) or `BLOCKED` (unreadable or ambiguous authoritative queue, invalid invocation, preflight refusal or held drain lock). Progress lines `devlyn-loop: <task>: <event>` go to stderr.
|
|
16
16
|
- `--local-only` (alias `--no-push`) keeps the loop local. A manifest `delivery: local-only` and any earlier local receipt of the loop keep that restriction for later drains. A local loop needs no remote; nothing is fetched or pushed. An `auto`/`pr` loop none of whose tasks has a pushed PR runs as a local loop from its capture and the branch it was added on. Once one has, that PR is never rewritten to local and the loop is never run locally: its tasks wait, naming the PR, while other work continues.
|
|
17
|
-
- Worktrees default to `<repo parent>/<repo name>.devlyn/<loop-id>/<task-id
|
|
17
|
+
- Worktrees default to `<repo parent>/<repo name>.devlyn.noindex/<loop-id>/<task-id>`; tasks already allocated under an earlier default resume where their receipts record them. On Windows, a default root newly created by the drain gets the not-content-indexed attribute, inherited by new children (content only; names stay indexed). Microsoft Defender scanning is unaffected; use a Dev Drive via `--worktree-root` or an administrator's Defender exclusion for the root.
|
|
18
18
|
|
|
19
19
|
The host supplies the executor and maps its configured engine to the argv; the loop has no model preference. Each `{packet}` in the argv is replaced by the task packet path, and each `{worktree_git_dir}` by the task worktree's absolute Git directory. The executor runs with the task worktree as its working directory and the null device as stdin; its stdout and stderr are appended to `executor.stdout` and `executor.stderr` in the packet's `evidence_dir`, never a pipe.
|
|
20
20
|
|
|
@@ -1062,6 +1062,17 @@ def allocate(v, row, opts):
|
|
|
1062
1062
|
if values["start"] and (drift := instruction_drift(anchor, start)):
|
|
1063
1063
|
return (f"the installed instructions ({drift}) in this checkout differ from those in its start commit {start}, so it would run "
|
|
1064
1064
|
f"without them; commit them and push them to origin/{manifest['base_ref']}")
|
|
1065
|
+
if os.name == "nt" and opts.default_worktree_root:
|
|
1066
|
+
try:
|
|
1067
|
+
opts.worktree_root.mkdir(exist_ok=False)
|
|
1068
|
+
except FileExistsError:
|
|
1069
|
+
pass # An existing root belongs to its owner; never change its attributes.
|
|
1070
|
+
else:
|
|
1071
|
+
try:
|
|
1072
|
+
shared("platform-support")["mark_not_content_indexed"](opts.worktree_root)
|
|
1073
|
+
except OSError as exc:
|
|
1074
|
+
opts.root_diagnostics.append(
|
|
1075
|
+
f"content indexing unchanged: {opts.worktree_root}: could not set FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: {exc}; continuing")
|
|
1065
1076
|
result = task_complete("allocate", exclude_receipts=v["invalid_receipts"], **values)
|
|
1066
1077
|
progress(identity, f"allocated {result['worktree']}")
|
|
1067
1078
|
return None
|
|
@@ -1426,7 +1437,9 @@ def drain(args):
|
|
|
1426
1437
|
args.repo = anchor # Preserve the repository root for failures reported by main.
|
|
1427
1438
|
diagnostics = skill_diagnostics(anchor)
|
|
1428
1439
|
require(any("{packet}" in part for part in args.executor), "executor argv must contain {packet}, replaced by the task packet path")
|
|
1429
|
-
args.
|
|
1440
|
+
args.default_worktree_root = args.worktree_root is None
|
|
1441
|
+
args.root_diagnostics = []
|
|
1442
|
+
args.worktree_root = Path(args.worktree_root).resolve() if args.worktree_root else anchor.parent / f"{anchor.name}.devlyn.noindex"
|
|
1430
1443
|
with lock(common, "drain.lock", blocking=False):
|
|
1431
1444
|
attempted, last, status, reason, refused = set(), None, "DRAINED", None, {}
|
|
1432
1445
|
try:
|
|
@@ -1455,6 +1468,7 @@ def drain(args):
|
|
|
1455
1468
|
last = view(anchor, common, args.local_only)
|
|
1456
1469
|
if last is None:
|
|
1457
1470
|
return {"status": status, "reason": reason, **diagnostics}
|
|
1471
|
+
last["cleanup"].extend(args.root_diagnostics)
|
|
1458
1472
|
annotate(last, refused)
|
|
1459
1473
|
prune_parents(last, args.worktree_root)
|
|
1460
1474
|
return {"status": status, "reason": reason, **diagnostics,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.2.
|
|
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": {
|