devlyn-cli 4.3.9 → 4.3.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/bin/devlyn.js +122 -46
- package/bin/skill-history.json +1 -0
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/codex/.agents/plugins/marketplace.json +1 -1
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/release.json +8 -8
package/bin/devlyn.js
CHANGED
|
@@ -1344,10 +1344,10 @@ function showHelp() {
|
|
|
1344
1344
|
// ---------------------------------------------------------------------------
|
|
1345
1345
|
// Legacy cleanup for the built-in harness: Devlyn delivers devlyn-cli per session, so every copy an
|
|
1346
1346
|
// earlier install left in a skill root, and the files those installs added, become legacy. A plan
|
|
1347
|
-
//
|
|
1348
|
-
// acts only on items both plans name the same way
|
|
1349
|
-
// widen it: untracked copies go in place, tracked ones through the project's delivery policy.
|
|
1350
|
-
//
|
|
1347
|
+
// takes the remote base by fast-forward when possible, then lists each item with its proof. Apply
|
|
1348
|
+
// plans again from disk and acts only on items both plans name the same way. A plan can narrow
|
|
1349
|
+
// cleanup but never widen it: untracked copies go in place, tracked ones through the project's delivery policy.
|
|
1350
|
+
// Removals need shipped-content proof or an explicit choice of a reviewed modified copy.
|
|
1351
1351
|
// ---------------------------------------------------------------------------
|
|
1352
1352
|
|
|
1353
1353
|
const CLEANUP_CORE = ['devlyn-ideate', 'devlyn-engines'];
|
|
@@ -1355,12 +1355,17 @@ const CLEANUP_CORE = ['devlyn-ideate', 'devlyn-engines'];
|
|
|
1355
1355
|
function cleanupReason(code, params = {}) { return { code, params }; }
|
|
1356
1356
|
|
|
1357
1357
|
// What a path is now, so apply can refuse an item that changed after it was reviewed.
|
|
1358
|
-
function cleanupPreimage(file) {
|
|
1358
|
+
function cleanupPreimage(file, exact = false) {
|
|
1359
1359
|
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
1360
1360
|
if (!stat) return { type: 'missing', sha256: null };
|
|
1361
1361
|
if (stat.isSymbolicLink()) return { type: 'link', sha256: null };
|
|
1362
1362
|
if (stat.isDirectory()) {
|
|
1363
|
-
try {
|
|
1363
|
+
try {
|
|
1364
|
+
if (!exact) return { type: 'directory', sha256: fingerprint(file) };
|
|
1365
|
+
const entries = fs.readdirSync(file).sort().map((name) => [name, cleanupPreimage(path.join(file, name), true)]);
|
|
1366
|
+
return { type: 'directory', sha256: entries.some(([, before]) => before.sha256 === null) ? null
|
|
1367
|
+
: crypto.createHash('sha256').update(JSON.stringify(entries)).digest('hex') };
|
|
1368
|
+
}
|
|
1364
1369
|
catch { return { type: 'directory', sha256: null }; }
|
|
1365
1370
|
}
|
|
1366
1371
|
if (stat.isFile()) return { type: 'file', sha256: crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') };
|
|
@@ -1414,6 +1419,46 @@ function cleanupMarkerOwned(file, current) {
|
|
|
1414
1419
|
return text === JSON.stringify({ schemaVersion: 1, package: PKG.name, version: marker.version, skills: marker.skills }, null, 2) + '\n';
|
|
1415
1420
|
}
|
|
1416
1421
|
|
|
1422
|
+
function cleanupSkillRoots(roots, current, includeModified = false) {
|
|
1423
|
+
const resolved = new Map();
|
|
1424
|
+
const linked = new Set();
|
|
1425
|
+
const active = new Set();
|
|
1426
|
+
for (const [base, root] of roots) {
|
|
1427
|
+
let target = root;
|
|
1428
|
+
const stat = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
1429
|
+
const parents = cleanupRealParents(base, root);
|
|
1430
|
+
if (parents && stat?.isSymbolicLink()) {
|
|
1431
|
+
const link = fs.readlinkSync(root);
|
|
1432
|
+
let dir = path.isAbsolute(link) ? path.parse(link).root : path.dirname(root);
|
|
1433
|
+
// Check each hop before normalizing `..`, so even a link followed by `..` is refused.
|
|
1434
|
+
for (const part of link.slice(path.parse(link).root.length).split(path.sep).filter(Boolean)) {
|
|
1435
|
+
dir = path.join(dir, part);
|
|
1436
|
+
if (!fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory()) { dir = null; break; }
|
|
1437
|
+
}
|
|
1438
|
+
if (dir && cleanupRealParents(base, dir)) {
|
|
1439
|
+
target = fs.realpathSync.native(dir);
|
|
1440
|
+
linked.add(target);
|
|
1441
|
+
}
|
|
1442
|
+
} else if (parents && stat?.isDirectory()) target = fs.realpathSync.native(root);
|
|
1443
|
+
if (!resolved.has(target)) resolved.set(target, base);
|
|
1444
|
+
if (cleanupInUse(root) || cleanupInUse(target)) active.add(target);
|
|
1445
|
+
}
|
|
1446
|
+
return [...resolved].flatMap(([root, base]) => {
|
|
1447
|
+
const items = cleanupSkillRoot(base, root, current);
|
|
1448
|
+
const occupied = includeModified && items.some((item) => item.kind === 'install-lock');
|
|
1449
|
+
const protectedCopies = new Set(items.filter((item) => [...linked].some((target) => target === item.path || target.startsWith(item.path + path.sep))));
|
|
1450
|
+
for (const item of items) {
|
|
1451
|
+
if ((protectedCopies.size && cleanupSupport(item)) || protectedCopies.has(item)) {
|
|
1452
|
+
Object.assign(item, { action: 'skip', reasons: [cleanupReason('dependent-copy')] });
|
|
1453
|
+
}
|
|
1454
|
+
if (cleanupAction(item, includeModified) === 'remove' && (active.has(root) || occupied)) {
|
|
1455
|
+
Object.assign(item, { action: 'skip', reasons: [cleanupReason(occupied ? 'occupied-lock' : 'in-use')] });
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return items.map((item) => linked.has(root) ? { ...item, keepRoot: true } : item);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1417
1462
|
// The legacy items of one skill root, each with its proof and the base its parents are checked from.
|
|
1418
1463
|
// `_shared` and the install marker go only when every copy beside them goes too: a kept, edited
|
|
1419
1464
|
// devlyn-ideate needs both.
|
|
@@ -1496,9 +1541,6 @@ function cleanupSkillRoot(base, root, current) {
|
|
|
1496
1541
|
reasons: proven ? [] : [cleanupReason('modified-copy')] });
|
|
1497
1542
|
}
|
|
1498
1543
|
}
|
|
1499
|
-
if (cleanupInUse(root)) {
|
|
1500
|
-
for (const item of items) if (item.action === 'remove') Object.assign(item, { action: 'skip', reasons: [cleanupReason('in-use')] });
|
|
1501
|
-
}
|
|
1502
1544
|
return items;
|
|
1503
1545
|
}
|
|
1504
1546
|
|
|
@@ -1565,7 +1607,8 @@ function cleanupCurrentFingerprints() {
|
|
|
1565
1607
|
}
|
|
1566
1608
|
|
|
1567
1609
|
function cleanupGit(root, args, options = {}) {
|
|
1568
|
-
return spawnSync('git', ['-C', root, ...args], {
|
|
1610
|
+
return spawnSync('git', ['-C', root, ...args], { encoding: 'utf8', ...options,
|
|
1611
|
+
env: { ...gitEnvironment(), GIT_OPTIONAL_LOCKS: '0', ...options.env } });
|
|
1569
1612
|
}
|
|
1570
1613
|
|
|
1571
1614
|
let cleanupPythonBinary;
|
|
@@ -1647,7 +1690,7 @@ function cleanupRetainSupport(items) {
|
|
|
1647
1690
|
return items;
|
|
1648
1691
|
}
|
|
1649
1692
|
|
|
1650
|
-
function cleanupProjectScope(project, current) {
|
|
1693
|
+
function cleanupProjectScope(project, current, includeModified = false) {
|
|
1651
1694
|
const { repositoryIdentity } = require('./harness-status');
|
|
1652
1695
|
const { physicalRoot } = require('./release');
|
|
1653
1696
|
const root = physicalRoot(project);
|
|
@@ -1659,12 +1702,12 @@ function cleanupProjectScope(project, current) {
|
|
|
1659
1702
|
// The home folder's roots are this machine's: the machine scope cleans them under the user-root locks.
|
|
1660
1703
|
if (cleanupSameFile(base, os.homedir())) return { ...scope, items: whole('home-folder') };
|
|
1661
1704
|
const items = [
|
|
1662
|
-
...['.agents', '.claude'].
|
|
1705
|
+
...cleanupSkillRoots(['.agents', '.claude'].map((dir) => [base, path.join(base, dir, 'skills')]), current, includeModified),
|
|
1663
1706
|
...cleanupCommands(base), ...cleanupProjectFiles(base), ...cleanupInstructions(base),
|
|
1664
1707
|
];
|
|
1665
|
-
const busy = identity && items.some((item) => item
|
|
1708
|
+
const busy = identity && items.some((item) => cleanupAction(item, includeModified) !== 'skip') ? cleanupQueueBusy(base) : null;
|
|
1666
1709
|
for (const item of items) {
|
|
1667
|
-
if (busy && item
|
|
1710
|
+
if (busy && cleanupAction(item, includeModified) !== 'skip') Object.assign(item, { action: 'skip', reasons: [busy] });
|
|
1668
1711
|
const relative = path.relative(base, item.path).split(path.sep).join('/');
|
|
1669
1712
|
let tracked = false;
|
|
1670
1713
|
try { tracked = cleanupTracked(item.path); }
|
|
@@ -1673,23 +1716,17 @@ function cleanupProjectScope(project, current) {
|
|
|
1673
1716
|
}
|
|
1674
1717
|
if (!scope.remote) {
|
|
1675
1718
|
for (const item of scope.items) {
|
|
1676
|
-
if (item.route === 'delivery' && item
|
|
1719
|
+
if (item.route === 'delivery' && cleanupAction(item, includeModified) !== 'skip') Object.assign(item, { action: 'skip', reasons: [cleanupReason('no-remote')] });
|
|
1677
1720
|
}
|
|
1678
1721
|
}
|
|
1679
1722
|
cleanupRetainSupport(scope.items);
|
|
1680
1723
|
return scope;
|
|
1681
1724
|
}
|
|
1682
1725
|
|
|
1683
|
-
function cleanupMachineScope(home, accountHomes, current) {
|
|
1726
|
+
function cleanupMachineScope(home, accountHomes, current, includeModified = false) {
|
|
1684
1727
|
const roots = [...['.agents', '.codex', '.claude', '.grok'].map((dir) => [home, path.join(home, dir, 'skills')]),
|
|
1685
1728
|
...accountHomes.map((account) => [path.dirname(account), path.join(account, 'skills')])];
|
|
1686
|
-
const
|
|
1687
|
-
const items = [];
|
|
1688
|
-
for (const [base, root] of roots) {
|
|
1689
|
-
if (seen.has(root)) continue;
|
|
1690
|
-
seen.add(root);
|
|
1691
|
-
items.push(...cleanupSkillRoot(base, root, current));
|
|
1692
|
-
}
|
|
1729
|
+
const items = cleanupSkillRoots(roots, current, includeModified);
|
|
1693
1730
|
items.push(...cleanupCommands(home));
|
|
1694
1731
|
for (const item of items) {
|
|
1695
1732
|
try {
|
|
@@ -1702,13 +1739,13 @@ function cleanupMachineScope(home, accountHomes, current) {
|
|
|
1702
1739
|
return { kind: 'machine', root: home, name: null, remote: null, items: cleanupRetainSupport(items) };
|
|
1703
1740
|
}
|
|
1704
1741
|
|
|
1705
|
-
function cleanupScopes(inputs, current) {
|
|
1742
|
+
function cleanupScopes(inputs, current, includeModified = false) {
|
|
1706
1743
|
const { sha256 } = require('./release');
|
|
1707
1744
|
const scopes = [];
|
|
1708
|
-
if (inputs.machine) scopes.push({ id: 'machine', ...cleanupMachineScope(os.homedir(), inputs.accountHomes, current) });
|
|
1745
|
+
if (inputs.machine) scopes.push({ id: 'machine', ...cleanupMachineScope(os.homedir(), inputs.accountHomes, current, includeModified) });
|
|
1709
1746
|
for (const project of inputs.projects) {
|
|
1710
1747
|
let scope;
|
|
1711
|
-
try { scope = cleanupProjectScope(project, current); }
|
|
1748
|
+
try { scope = cleanupProjectScope(project, current, includeModified); }
|
|
1712
1749
|
catch (error) {
|
|
1713
1750
|
scope = { kind: 'project', root: project, name: path.basename(project), remote: null, items: [{ kind: 'project', path: project, base: project,
|
|
1714
1751
|
relative: '', route: 'local', action: 'skip', reasons: [cleanupReason(error.reason?.code === 'repository-unreadable' ? 'repository-unreadable' : 'project-unreadable', { detail: error.message })] }] };
|
|
@@ -1716,13 +1753,24 @@ function cleanupScopes(inputs, current) {
|
|
|
1716
1753
|
scopes.push({ id: `project:${sha256(Buffer.from(project)).slice(0, 12)}`, ...scope });
|
|
1717
1754
|
}
|
|
1718
1755
|
for (const scope of scopes) {
|
|
1719
|
-
scope.items = scope.items.map((item, index) => ({ id: `${scope.id}:${index}`, ...item, before: cleanupPreimage(item.path) }));
|
|
1756
|
+
scope.items = scope.items.map((item, index) => ({ id: `${scope.id}:${index}`, ...item, before: cleanupPreimage(item.path, cleanupCanInclude(item)) }));
|
|
1720
1757
|
}
|
|
1721
1758
|
return scopes;
|
|
1722
1759
|
}
|
|
1723
1760
|
|
|
1724
1761
|
function cleanupPlan(inputs) {
|
|
1725
|
-
const
|
|
1762
|
+
const current = cleanupCurrentFingerprints();
|
|
1763
|
+
let scopes = cleanupScopes(inputs, current);
|
|
1764
|
+
let changed = false;
|
|
1765
|
+
for (const scope of scopes) {
|
|
1766
|
+
if (scope.kind === 'project' && scope.remote && scope.items.some((item) => item.route === 'delivery' && ['remove', 'edit'].includes(item.action))) {
|
|
1767
|
+
const updated = cleanupUpdateCheckout(scope);
|
|
1768
|
+
if (updated.changed) changed = true;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
// The review covers the bytes delivery acts on, including scopes sharing a checkout.
|
|
1772
|
+
if (changed) scopes = cleanupScopes(inputs, current);
|
|
1773
|
+
const plan = { schemaVersion: 1, version: PKG.version, inputs, scopes };
|
|
1726
1774
|
return { ...plan, digest: cleanupDigest(plan) };
|
|
1727
1775
|
}
|
|
1728
1776
|
|
|
@@ -1742,10 +1790,10 @@ function cleanupDigest(plan) {
|
|
|
1742
1790
|
|
|
1743
1791
|
// Apply one item a fresh plan proves right now; returns a reason when it is not what was proven.
|
|
1744
1792
|
function cleanupApplyItem(item) {
|
|
1745
|
-
const now = cleanupPreimage(item.path);
|
|
1793
|
+
const now = cleanupPreimage(item.path, Boolean(item.included));
|
|
1746
1794
|
if (now.type !== item.before.type || now.sha256 !== item.before.sha256) return cleanupReason('stale-plan');
|
|
1747
1795
|
if (!cleanupRealParents(item.base, item.path)) return cleanupReason('outside-link');
|
|
1748
|
-
if (item
|
|
1796
|
+
if (cleanupAction(item) === 'remove') {
|
|
1749
1797
|
fs.rmSync(item.path, { recursive: true }); // lstat/realParents never follow a link; rm removes the entry itself.
|
|
1750
1798
|
return null;
|
|
1751
1799
|
}
|
|
@@ -1765,16 +1813,17 @@ function cleanupApplyItem(item) {
|
|
|
1765
1813
|
function cleanupProven(reviewed, fresh) {
|
|
1766
1814
|
const key = (item) => `${item.kind}\u0000${item.path}`;
|
|
1767
1815
|
const now = new Map(fresh.items.map((item) => [key(item), item]));
|
|
1768
|
-
const asked = new Set(reviewed.items.filter((item) =>
|
|
1816
|
+
const asked = new Set(reviewed.items.filter((item) => ['remove', 'edit'].includes(cleanupAction(item))).map(key));
|
|
1769
1817
|
// A copy left out of the review stays, so the `_shared` and marker beside it stay too.
|
|
1770
1818
|
const narrowed = new Set(fresh.items.filter((item) => item.action === 'remove' && !cleanupSupport(item) && !asked.has(key(item)))
|
|
1771
1819
|
.map((item) => path.dirname(item.path)));
|
|
1772
|
-
return reviewed.items.filter((item) =>
|
|
1820
|
+
return reviewed.items.filter((item) => ['remove', 'edit'].includes(cleanupAction(item))).map((item) => {
|
|
1773
1821
|
const match = now.get(key(item));
|
|
1774
|
-
const same = match && match.action === item.action &&
|
|
1822
|
+
const same = match && match.action === item.action && (!item.included || (cleanupCanInclude(match) && item.before.sha256 !== null))
|
|
1823
|
+
&& match.relative === item.relative && match.route === item.route
|
|
1775
1824
|
&& match.before.type === item.before.type && match.before.sha256 === item.before.sha256
|
|
1776
1825
|
&& !(cleanupSupport(item) && narrowed.has(path.dirname(item.path)));
|
|
1777
|
-
return { reviewed: item, fresh: same ? match : null };
|
|
1826
|
+
return { reviewed: item, fresh: same ? { ...match, included: item.included } : null };
|
|
1778
1827
|
});
|
|
1779
1828
|
}
|
|
1780
1829
|
|
|
@@ -1783,10 +1832,19 @@ function cleanupSupport(item) {
|
|
|
1783
1832
|
return path.basename(item.path) === '_shared' || item.kind === 'install-marker';
|
|
1784
1833
|
}
|
|
1785
1834
|
|
|
1835
|
+
function cleanupCanInclude(item) {
|
|
1836
|
+
return !cleanupSupport(item) && item.action === 'skip'
|
|
1837
|
+
&& item.reasons.length === 1 && item.reasons[0].code === 'modified-copy';
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function cleanupAction(item, includeModified = false) {
|
|
1841
|
+
return item.included || (includeModified && cleanupCanInclude(item)) ? 'remove' : item.action;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1786
1844
|
function cleanupLocal(scope, items) {
|
|
1787
1845
|
const results = new Map();
|
|
1788
|
-
const roots = [...new Set(items.
|
|
1789
|
-
|
|
1846
|
+
const roots = [...new Set(items.filter((item) => item.keepRoot || path.basename(path.dirname(item.path)) === 'skills')
|
|
1847
|
+
.map((item) => path.dirname(item.path)).filter((dir) => fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory()))];
|
|
1790
1848
|
const act = (item) => {
|
|
1791
1849
|
try {
|
|
1792
1850
|
if (cleanupTracked(item.path)) { results.set(item.id, cleanupReason('tracked-copy')); return; }
|
|
@@ -1812,7 +1870,7 @@ function cleanupLocal(scope, items) {
|
|
|
1812
1870
|
// After the locks are released, so a root holds nothing but what stayed.
|
|
1813
1871
|
for (const root of new Set(items.filter((item) => results.get(item.id) === null && item.kind !== 'instructions')
|
|
1814
1872
|
.map((item) => path.dirname(item.path)))) {
|
|
1815
|
-
if (path.basename(root) !== 'skills') continue;
|
|
1873
|
+
if (path.basename(root) !== 'skills' || scope.items.some((item) => item.keepRoot && path.dirname(item.path) === root)) continue;
|
|
1816
1874
|
try { if (fs.lstatSync(root).isDirectory() && fs.readdirSync(root).length === 0) fs.rmdirSync(root); } catch {}
|
|
1817
1875
|
}
|
|
1818
1876
|
return results;
|
|
@@ -1920,7 +1978,8 @@ function cleanupUpdateCheckout(scope) {
|
|
|
1920
1978
|
if (branch !== scope.remote.base || !head) return failed(`${branch || 'a detached HEAD'} is checked out`);
|
|
1921
1979
|
const ref = `refs/devlyn/cleanup/${crypto.randomBytes(8).toString('hex')}`;
|
|
1922
1980
|
try {
|
|
1923
|
-
const fetched = cleanupGit(scope.root, ['fetch', '--quiet', '--no-write-fetch-head', 'origin', `+refs/heads/${scope.remote.base}:${ref}`]
|
|
1981
|
+
const fetched = cleanupGit(scope.root, ['fetch', '--quiet', '--no-write-fetch-head', 'origin', `+refs/heads/${scope.remote.base}:${ref}`],
|
|
1982
|
+
{ timeout: 30000, env: { GIT_TERMINAL_PROMPT: '0' } });
|
|
1924
1983
|
if (fetched.error || fetched.status !== 0) return failed(fetched.error?.message || fetched.stderr.trim());
|
|
1925
1984
|
const target = read(['rev-parse', '--verify', `${ref}^{commit}`]);
|
|
1926
1985
|
const [now, at] = checkout();
|
|
@@ -1930,7 +1989,8 @@ function cleanupUpdateCheckout(scope) {
|
|
|
1930
1989
|
'merge', '--ff-only', '--no-squash', '--no-autostash', '--no-overwrite-ignore', '--quiet', target]);
|
|
1931
1990
|
if (merged.error || merged.status !== 0) return failed(merged.error?.message || merged.stderr.trim());
|
|
1932
1991
|
const [after, landed] = checkout();
|
|
1933
|
-
|
|
1992
|
+
if (after !== branch || landed !== target) return failed('the fast-forward did not land on the base');
|
|
1993
|
+
return { changed: landed !== head };
|
|
1934
1994
|
} finally {
|
|
1935
1995
|
cleanupGit(scope.root, ['update-ref', '-d', ref]);
|
|
1936
1996
|
}
|
|
@@ -1941,14 +2001,15 @@ function cleanupUpdateCheckout(scope) {
|
|
|
1941
2001
|
// With every reviewed item already settled by the remote base, the worktree is released with
|
|
1942
2002
|
// nothing to deliver.
|
|
1943
2003
|
function cleanupDeliveryCommit(worktree, items, current, release, settledUpstream) {
|
|
1944
|
-
const fresh = cleanupProjectScope(worktree, current);
|
|
2004
|
+
const fresh = cleanupProjectScope(worktree, current, items.some((item) => item.included));
|
|
1945
2005
|
const available = new Map(fresh.items.map((item) => [`${item.kind}\u0000${item.relative}`, item]));
|
|
1946
2006
|
const pairs = items.map((reviewed) => {
|
|
1947
2007
|
const fresh = available.get(`${reviewed.kind}\u0000${reviewed.relative}`);
|
|
1948
|
-
return { reviewed, fresh, before: fresh && cleanupPreimage(fresh.path) };
|
|
2008
|
+
return { reviewed, fresh, before: fresh && cleanupPreimage(fresh.path, Boolean(reviewed.included)) };
|
|
1949
2009
|
});
|
|
1950
2010
|
const stale = pairs.filter(({ reviewed, fresh, before }) => (fresh
|
|
1951
2011
|
? fresh.action !== reviewed.action || fresh.route !== 'delivery'
|
|
2012
|
+
|| (reviewed.included && (!cleanupCanInclude(fresh) || before.sha256 === null))
|
|
1952
2013
|
|| before.type !== reviewed.before.type || before.sha256 !== reviewed.before.sha256
|
|
1953
2014
|
: !settledUpstream(reviewed.relative)));
|
|
1954
2015
|
// Check the entire batch before mutating even one item; the remote base cannot replace the reviewed preimage.
|
|
@@ -1957,12 +2018,14 @@ function cleanupDeliveryCommit(worktree, items, current, release, settledUpstrea
|
|
|
1957
2018
|
if (!present.length) return release();
|
|
1958
2019
|
const applied = [];
|
|
1959
2020
|
for (const { reviewed, fresh } of present) {
|
|
1960
|
-
const error = cleanupApplyItem({ ...fresh, before: reviewed.before });
|
|
2021
|
+
const error = cleanupApplyItem({ ...fresh, before: reviewed.before, included: reviewed.included });
|
|
1961
2022
|
if (error) return release(cleanupReason(error.code, { ...error.params, detail: reviewed.relative }));
|
|
1962
2023
|
applied.push(fresh.relative);
|
|
1963
2024
|
}
|
|
1964
2025
|
for (const dir of ['.agents/skills', '.claude/skills']) {
|
|
1965
|
-
|
|
2026
|
+
const full = path.join(worktree, dir);
|
|
2027
|
+
if (fresh.items.some((item) => item.keepRoot && cleanupSameFile(path.dirname(item.path), full))) continue;
|
|
2028
|
+
try { if (fs.lstatSync(full).isDirectory() && fs.readdirSync(full).length === 0) fs.rmdirSync(full); } catch {}
|
|
1966
2029
|
}
|
|
1967
2030
|
const add = cleanupGit(worktree, ['add', '-A', '--', ...applied]);
|
|
1968
2031
|
const commit = add.status === 0 && cleanupGit(worktree, ['-c', 'user.useConfigOnly=true', 'commit', '-q', '-m',
|
|
@@ -1973,7 +2036,8 @@ function cleanupDeliveryCommit(worktree, items, current, release, settledUpstrea
|
|
|
1973
2036
|
fs.mkdirSync(path.dirname(evidence), { recursive: true });
|
|
1974
2037
|
fs.writeFileSync(evidence, check.stdout);
|
|
1975
2038
|
let left = [];
|
|
1976
|
-
try { left = JSON.parse(check.stdout.trim().split('\n').at(-1)).data.scopes[0].items.filter((item) =>
|
|
2039
|
+
try { left = JSON.parse(check.stdout.trim().split('\n').at(-1)).data.scopes[0].items.filter((item) =>
|
|
2040
|
+
cleanupAction({ ...item, included: items.find((reviewed) => reviewed.relative === item.relative)?.included }) !== 'skip' && applied.includes(item.relative)); }
|
|
1977
2041
|
catch { return release(cleanupReason('delivery-failed', { detail: 'the check after cleanup could not run' })); }
|
|
1978
2042
|
if (left.length) return release(cleanupReason('delivery-failed', { detail: 'items remain after cleanup' }));
|
|
1979
2043
|
const head = cleanupGit(worktree, ['rev-parse', 'HEAD']);
|
|
@@ -1987,7 +2051,8 @@ function cleanupDeliveryCommit(worktree, items, current, release, settledUpstrea
|
|
|
1987
2051
|
|
|
1988
2052
|
function cleanupApply(plan, { events = false } = {}) {
|
|
1989
2053
|
const current = cleanupCurrentFingerprints();
|
|
1990
|
-
const
|
|
2054
|
+
const includeModified = plan.scopes.some((scope) => scope.items.some((item) => item.included));
|
|
2055
|
+
const fresh = new Map(cleanupScopes(plan.inputs, current, includeModified).map((scope) => [scope.id, scope]));
|
|
1991
2056
|
let seq = 0;
|
|
1992
2057
|
const emit = (scope) => { if (events) process.stdout.write(JSON.stringify({ schemaVersion: 1, event: 'scope', seq: ++seq, scope }) + '\n'); };
|
|
1993
2058
|
const scopes = [];
|
|
@@ -2035,9 +2100,10 @@ function cleanupCommand(args) {
|
|
|
2035
2100
|
const opts = options(args, {
|
|
2036
2101
|
plan: { type: 'boolean' }, apply: { type: 'boolean' }, machine: { type: 'boolean' }, events: { type: 'boolean' },
|
|
2037
2102
|
project: { type: 'string', multiple: true }, 'account-home': { type: 'string', multiple: true },
|
|
2038
|
-
'plan-file': { type: 'string' }, digest: { type: 'string' },
|
|
2103
|
+
'plan-file': { type: 'string' }, digest: { type: 'string' }, include: { type: 'string', multiple: true },
|
|
2039
2104
|
});
|
|
2040
2105
|
if (opts.plan === opts.apply) fail('invalid-arguments', { argument: 'exactly one of --plan or --apply' }, 2);
|
|
2106
|
+
if (opts.plan && opts.include) fail('invalid-arguments', { argument: '--include requires --apply' }, 2);
|
|
2041
2107
|
for (const dir of [...(opts.project || []), ...(opts['account-home'] || [])]) {
|
|
2042
2108
|
if (!path.isAbsolute(dir)) fail('invalid-arguments', { argument: 'absolute paths required' }, 2);
|
|
2043
2109
|
}
|
|
@@ -2055,6 +2121,16 @@ function cleanupCommand(args) {
|
|
|
2055
2121
|
fail('stale-plan', {}, 3);
|
|
2056
2122
|
}
|
|
2057
2123
|
if (plan.version !== PKG.version) fail('stale-plan', { version: plan.version }, 3);
|
|
2124
|
+
const reviewed = plan.scopes.flatMap((scope) => scope.items);
|
|
2125
|
+
for (const item of reviewed) delete item.included;
|
|
2126
|
+
const items = new Map(reviewed.map((item) => [item.id, item]));
|
|
2127
|
+
for (const id of opts.include || []) {
|
|
2128
|
+
const item = items.get(id);
|
|
2129
|
+
if (!item || !cleanupCanInclude(item) || reviewed.filter((item) => item.id === id).length !== 1) {
|
|
2130
|
+
fail('invalid-arguments', { argument: `--include ${id}` }, 2);
|
|
2131
|
+
}
|
|
2132
|
+
item.included = true;
|
|
2133
|
+
}
|
|
2058
2134
|
return cleanupApply(plan, { events: Boolean(opts.events) });
|
|
2059
2135
|
}
|
|
2060
2136
|
|
package/bin/skill-history.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.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": {
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "devlyn-cli",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.10",
|
|
5
5
|
"principles": {
|
|
6
6
|
"defaults": "config/skills/_shared/runtime-principles.md",
|
|
7
7
|
"memory": {
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
"path": "bin/devlyn.js",
|
|
132
|
-
"sha256": "
|
|
132
|
+
"sha256": "ae93be603c8a7511a40c82a7cfa337be05345f0876b83204027cec1ffeafd2ba",
|
|
133
133
|
"executable": true
|
|
134
134
|
},
|
|
135
135
|
{
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"path": "bin/skill-history.json",
|
|
167
|
-
"sha256": "
|
|
167
|
+
"sha256": "56d9ff1ce32f33e82806864a305bf2bbff35babe9952d46e3f434f88dabc3a2d",
|
|
168
168
|
"executable": false
|
|
169
169
|
},
|
|
170
170
|
{
|
|
@@ -309,7 +309,7 @@
|
|
|
309
309
|
},
|
|
310
310
|
{
|
|
311
311
|
"path": "native/claude/.claude-plugin/plugin.json",
|
|
312
|
-
"sha256": "
|
|
312
|
+
"sha256": "1f9fdd6fc824ceba8f7baac7ef900f17a7bc46f48089606b563297da52bd2070",
|
|
313
313
|
"executable": false
|
|
314
314
|
},
|
|
315
315
|
{
|
|
@@ -439,12 +439,12 @@
|
|
|
439
439
|
},
|
|
440
440
|
{
|
|
441
441
|
"path": "native/codex/.agents/plugins/marketplace.json",
|
|
442
|
-
"sha256": "
|
|
442
|
+
"sha256": "03d5b28a6f058d46f14fdae526a71021ff1bc5248ece0a473a94ca830b321bcf",
|
|
443
443
|
"executable": false
|
|
444
444
|
},
|
|
445
445
|
{
|
|
446
446
|
"path": "native/codex/plugins/devlyn/.codex-plugin/plugin.json",
|
|
447
|
-
"sha256": "
|
|
447
|
+
"sha256": "1f9fdd6fc824ceba8f7baac7ef900f17a7bc46f48089606b563297da52bd2070",
|
|
448
448
|
"executable": false
|
|
449
449
|
},
|
|
450
450
|
{
|
|
@@ -1619,12 +1619,12 @@
|
|
|
1619
1619
|
},
|
|
1620
1620
|
{
|
|
1621
1621
|
"path": "package.json",
|
|
1622
|
-
"sha256": "
|
|
1622
|
+
"sha256": "66f0ad6c0d8c86ce76b5c68a54412f0e398cba4ebc0f19d3257c3597e9ba490f",
|
|
1623
1623
|
"executable": false
|
|
1624
1624
|
}
|
|
1625
1625
|
],
|
|
1626
1626
|
"digest": {
|
|
1627
1627
|
"algorithm": "sha256",
|
|
1628
|
-
"value": "
|
|
1628
|
+
"value": "f4e978a00a488c4dd8274e1344285c6d8dfaa8201589bb46dd1f7d2588a5e2b1"
|
|
1629
1629
|
}
|
|
1630
1630
|
}
|