devlyn-cli 4.3.5 → 4.3.6
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 +82 -11
- package/bin/skill-history.json +2 -1
- 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
|
@@ -1564,8 +1564,8 @@ function cleanupCurrentFingerprints() {
|
|
|
1564
1564
|
return Object.fromEntries([...CLEANUP_CORE, '_shared'].map((name) => [name, fingerprint(path.join(CONFIG_SOURCE, 'skills', name))]));
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
1567
|
-
function cleanupGit(root, args) {
|
|
1568
|
-
return spawnSync('git', ['-C', root, ...args], { env: { ...gitEnvironment(), GIT_OPTIONAL_LOCKS: '0' }, encoding: 'utf8' });
|
|
1567
|
+
function cleanupGit(root, args, options = {}) {
|
|
1568
|
+
return spawnSync('git', ['-C', root, ...args], { env: { ...gitEnvironment(), GIT_OPTIONAL_LOCKS: '0' }, encoding: 'utf8', ...options });
|
|
1569
1569
|
}
|
|
1570
1570
|
|
|
1571
1571
|
let cleanupPythonBinary;
|
|
@@ -1834,43 +1834,114 @@ function cleanupDeliver(scope, items, current) {
|
|
|
1834
1834
|
'--branch', branch, '--worktree', worktree, '--repository', scope.remote.repository,
|
|
1835
1835
|
'--remote', 'origin', '--base', scope.remote.base], scope.root).stdout);
|
|
1836
1836
|
if (allocated?.status !== 'ALLOCATED') return { error: cleanupReason('delivery-failed', { detail: allocated?.reason || 'allocation failed' }) };
|
|
1837
|
-
const release = (error) => {
|
|
1837
|
+
const release = (error = null) => {
|
|
1838
1838
|
for (const args of [['worktree', 'remove', '--force', worktree], ['branch', '-D', branch]]) {
|
|
1839
1839
|
const result = cleanupGit(scope.root, args);
|
|
1840
|
-
if (result.error || result.status !== 0) return { error: cleanupReason(error
|
|
1840
|
+
if (result.error || result.status !== 0) return { error: cleanupReason(error?.code || 'delivery-failed', { ...error?.params,
|
|
1841
1841
|
releaseError: result.error?.message || result.stderr.trim(), worktree, receipt: allocated.receipt }) };
|
|
1842
1842
|
}
|
|
1843
1843
|
fs.rmSync(path.dirname(allocated.receipt), { recursive: true, force: true });
|
|
1844
|
-
return { error };
|
|
1844
|
+
return error ? { error } : {};
|
|
1845
1845
|
};
|
|
1846
1846
|
let prepared;
|
|
1847
|
-
try { prepared = cleanupDeliveryCommit(worktree, items, current, release); }
|
|
1847
|
+
try { prepared = cleanupDeliveryCommit(worktree, items, current, release, cleanupRemovedUpstream(scope.root, worktree)); }
|
|
1848
1848
|
catch (error) { return release(cleanupReason('apply-failed', { detail: error.message })); }
|
|
1849
1849
|
if (prepared.error) return prepared;
|
|
1850
|
+
// Every reviewed copy is already gone from the remote base: the checkout only has to take it.
|
|
1851
|
+
if (!prepared.acceptance) return cleanupUpdateCheckout(scope);
|
|
1850
1852
|
const completed = lastJson(run(['complete', '--receipt', allocated.receipt, '--acceptance', prepared.acceptance, '--writers-stopped'], scope.root).stdout);
|
|
1851
1853
|
const status = completed?.delivery_status || completed?.status;
|
|
1852
|
-
if (status === 'COMPLETE' || status === 'MERGED') return { delivery: { state: 'merged', url: completed.pr || null } };
|
|
1854
|
+
if (status === 'COMPLETE' || status === 'MERGED') return { ...cleanupUpdateCheckout(scope), delivery: { state: 'merged', url: completed.pr || null } };
|
|
1853
1855
|
if (completed?.pr) return { delivery: { state: 'pr-open', url: completed.pr } };
|
|
1854
1856
|
const reason = completed?.reason || 'delivery did not complete';
|
|
1855
1857
|
const auth = /\b(authentication|unauthorized|gh auth login|could not read Username|HTTP 40[13])\b/i.test(reason);
|
|
1856
1858
|
return { error: cleanupReason(auth ? 'auth-failed' : 'delivery-failed', { detail: reason, ...(completed?.resume ? { resume: completed.resume } : {}) }) };
|
|
1857
1859
|
}
|
|
1858
1860
|
|
|
1861
|
+
// Whether the remote base (the allocated worktree's HEAD) removed a copy this checkout tracks: the
|
|
1862
|
+
// base holds no path of that name in any spelling, and the checkout holds the copy, under its
|
|
1863
|
+
// tracked spelling, as it was where the two histories meet. A copy committed only here, or changed
|
|
1864
|
+
// here since, is not one the base removed.
|
|
1865
|
+
function cleanupRemovedUpstream(root, worktree) {
|
|
1866
|
+
const read = (cwd, args, options) => {
|
|
1867
|
+
const result = cleanupGit(cwd, args, options);
|
|
1868
|
+
return !result.error && result.status === 0 ? result.stdout : null;
|
|
1869
|
+
};
|
|
1870
|
+
const object = (spec) => read(root, ['rev-parse', '--verify', '--quiet', spec])?.trim() || null;
|
|
1871
|
+
const base = read(worktree, ['rev-parse', 'HEAD'])?.trim();
|
|
1872
|
+
const fork = base ? read(root, ['merge-base', 'HEAD', base])?.trim() : null;
|
|
1873
|
+
let upstream; // the base's paths, folded to lower case, read once and only when needed
|
|
1874
|
+
return (relative) => {
|
|
1875
|
+
if (!fork) return false;
|
|
1876
|
+
if (upstream === undefined) {
|
|
1877
|
+
upstream = read(root, ['ls-tree', '-r', '-z', '--name-only', base], { maxBuffer: 1 << 30 })
|
|
1878
|
+
?.split('\0').filter(Boolean).map((name) => name.toLowerCase()) ?? null;
|
|
1879
|
+
}
|
|
1880
|
+
const lower = relative.toLowerCase();
|
|
1881
|
+
if (!upstream || upstream.some((name) => name === lower || name.startsWith(`${lower}/`))) return false;
|
|
1882
|
+
// The tracked spelling, which a case-insensitive file system lets differ from the one on disk.
|
|
1883
|
+
const tracked = read(root, ['ls-files', '-z', '--', `:(icase,literal)${relative}`])?.split('\0').find(Boolean);
|
|
1884
|
+
const spelled = tracked?.slice(0, relative.length);
|
|
1885
|
+
if (!spelled || spelled.toLowerCase() !== lower) return false;
|
|
1886
|
+
const before = object(`${fork}:${spelled}`);
|
|
1887
|
+
return before !== null && before === object(`HEAD:${spelled}`);
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// The copies the remote base dropped stay in this checkout until it takes that base: a checkout on
|
|
1892
|
+
// the base branch is fast-forwarded to it, as a task's owner reconciles after delivery. The base is
|
|
1893
|
+
// fetched into a ref this call owns (FETCH_HEAD can be rewritten by another fetch meanwhile) and
|
|
1894
|
+
// merged by its commit, only while the checkout is still the one checked before the fetch, and the
|
|
1895
|
+
// result must be that commit. Another branch, or a fast-forward Git refuses (local changes or ignored
|
|
1896
|
+
// files in the way, local commits), is left as it is.
|
|
1897
|
+
function cleanupUpdateCheckout(scope) {
|
|
1898
|
+
const failed = (detail) => ({ error: cleanupReason('update-checkout', { detail }) });
|
|
1899
|
+
const read = (args) => {
|
|
1900
|
+
const result = cleanupGit(scope.root, args);
|
|
1901
|
+
return !result.error && result.status === 0 ? result.stdout.trim() : null;
|
|
1902
|
+
};
|
|
1903
|
+
const checkout = () => [read(['symbolic-ref', '--quiet', '--short', 'HEAD']), read(['rev-parse', 'HEAD'])];
|
|
1904
|
+
const [branch, head] = checkout();
|
|
1905
|
+
if (branch !== scope.remote.base || !head) return failed(`${branch || 'a detached HEAD'} is checked out`);
|
|
1906
|
+
const ref = `refs/devlyn/cleanup/${crypto.randomBytes(8).toString('hex')}`;
|
|
1907
|
+
try {
|
|
1908
|
+
const fetched = cleanupGit(scope.root, ['fetch', '--quiet', '--no-write-fetch-head', 'origin', `+refs/heads/${scope.remote.base}:${ref}`]);
|
|
1909
|
+
if (fetched.error || fetched.status !== 0) return failed(fetched.error?.message || fetched.stderr.trim());
|
|
1910
|
+
const target = read(['rev-parse', '--verify', `${ref}^{commit}`]);
|
|
1911
|
+
const [now, at] = checkout();
|
|
1912
|
+
if (!target || now !== branch || at !== head) return failed('the checkout changed while the base was fetched');
|
|
1913
|
+
// The branch's own merge options (`--squash`, `-s ours`) would turn this into something else.
|
|
1914
|
+
const merged = cleanupGit(scope.root, ['-c', `branch.${branch}.mergeOptions=`,
|
|
1915
|
+
'merge', '--ff-only', '--no-squash', '--no-autostash', '--no-overwrite-ignore', '--quiet', target]);
|
|
1916
|
+
if (merged.error || merged.status !== 0) return failed(merged.error?.message || merged.stderr.trim());
|
|
1917
|
+
const [after, landed] = checkout();
|
|
1918
|
+
return after === branch && landed === target ? {} : failed('the fast-forward did not land on the base');
|
|
1919
|
+
} finally {
|
|
1920
|
+
cleanupGit(scope.root, ['update-ref', '-d', ref]);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1859
1924
|
// Inside the allocated worktree: prove every reviewed item, remove them, commit, and write the
|
|
1860
1925
|
// acceptance completion binds. A failure releases the worktree; so does a throw, in the caller.
|
|
1861
|
-
|
|
1926
|
+
// With every reviewed copy already removed by the remote base, the worktree is released with
|
|
1927
|
+
// nothing to deliver.
|
|
1928
|
+
function cleanupDeliveryCommit(worktree, items, current, release, removedUpstream) {
|
|
1862
1929
|
const fresh = cleanupProjectScope(worktree, current);
|
|
1863
1930
|
const available = new Map(fresh.items.map((item) => [`${item.kind}\u0000${item.relative}`, item]));
|
|
1864
1931
|
const pairs = items.map((reviewed) => {
|
|
1865
1932
|
const fresh = available.get(`${reviewed.kind}\u0000${reviewed.relative}`);
|
|
1866
1933
|
return { reviewed, fresh, before: fresh && cleanupPreimage(fresh.path) };
|
|
1867
1934
|
});
|
|
1868
|
-
const stale = pairs.filter(({ reviewed, fresh, before }) =>
|
|
1869
|
-
|
|
1935
|
+
const stale = pairs.filter(({ reviewed, fresh, before }) => (fresh
|
|
1936
|
+
? fresh.action !== reviewed.action || fresh.route !== 'delivery'
|
|
1937
|
+
|| before.type !== reviewed.before.type || before.sha256 !== reviewed.before.sha256
|
|
1938
|
+
: !removedUpstream(reviewed.relative)));
|
|
1870
1939
|
// Check the entire batch before mutating even one item; the remote base cannot replace the reviewed preimage.
|
|
1871
1940
|
if (stale.length) return release(cleanupReason('stale-plan', { detail: stale.map(({ reviewed }) => reviewed.relative).join(', ') }));
|
|
1941
|
+
const present = pairs.filter(({ fresh }) => fresh);
|
|
1942
|
+
if (!present.length) return release();
|
|
1872
1943
|
const applied = [];
|
|
1873
|
-
for (const { reviewed, fresh } of
|
|
1944
|
+
for (const { reviewed, fresh } of present) {
|
|
1874
1945
|
const error = cleanupApplyItem({ ...fresh, before: reviewed.before });
|
|
1875
1946
|
if (error) return release(cleanupReason(error.code, { ...error.params, detail: reviewed.relative }));
|
|
1876
1947
|
applied.push(fresh.relative);
|
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.6",
|
|
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.6",
|
|
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": "6eb4d078cc4fe235517b7a08e8d6965462cd6c91ab1853cc2f8a9f0b829f6191",
|
|
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": "c0aa4b8a25ec6873011859bc5c76cad58d1cd468e13018ea85761b8db51b81e9",
|
|
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": "4ca0474d48fdbee91769be02bd6a0b612313a390b0b0fe244679df0a43bc2e37",
|
|
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": "f8189a7e93f4e5ff3334d8ad884fffa679a2729f10fff815d24c4f0067534c73",
|
|
443
443
|
"executable": false
|
|
444
444
|
},
|
|
445
445
|
{
|
|
446
446
|
"path": "native/codex/plugins/devlyn/.codex-plugin/plugin.json",
|
|
447
|
-
"sha256": "
|
|
447
|
+
"sha256": "4ca0474d48fdbee91769be02bd6a0b612313a390b0b0fe244679df0a43bc2e37",
|
|
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": "4d754dc7026976c1e37d7d74b6733459fcd07b6a498bc3e8b4c4d575b02a7700",
|
|
1623
1623
|
"executable": false
|
|
1624
1624
|
}
|
|
1625
1625
|
],
|
|
1626
1626
|
"digest": {
|
|
1627
1627
|
"algorithm": "sha256",
|
|
1628
|
-
"value": "
|
|
1628
|
+
"value": "f4ee05e55111300285e2ec7b435934786fee40501365c8a2a2706b7ee168bc20"
|
|
1629
1629
|
}
|
|
1630
1630
|
}
|