devlyn-cli 4.3.6 → 4.3.8
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 +30 -15
- package/bin/harness-activation.js +33 -11
- package/bin/skill-history.json +3 -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 +9 -9
package/bin/devlyn.js
CHANGED
|
@@ -1843,11 +1843,25 @@ function cleanupDeliver(scope, items, current) {
|
|
|
1843
1843
|
fs.rmSync(path.dirname(allocated.receipt), { recursive: true, force: true });
|
|
1844
1844
|
return error ? { error } : {};
|
|
1845
1845
|
};
|
|
1846
|
+
const baseHead = cleanupGit(worktree, ['rev-parse', 'HEAD']);
|
|
1847
|
+
if (baseHead.error || baseHead.status !== 0) return release(cleanupReason('repository-unreadable', { detail: baseHead.error?.message || baseHead.stderr.trim() }));
|
|
1848
|
+
const base = baseHead.stdout.trim();
|
|
1846
1849
|
let prepared;
|
|
1847
|
-
try { prepared = cleanupDeliveryCommit(worktree, items, current, release,
|
|
1850
|
+
try { prepared = cleanupDeliveryCommit(worktree, items, current, release, cleanupSettledUpstream(scope.root, base)); }
|
|
1848
1851
|
catch (error) { return release(cleanupReason('apply-failed', { detail: error.message })); }
|
|
1852
|
+
if (prepared.error?.code === 'stale-plan') {
|
|
1853
|
+
const head = cleanupGit(scope.root, ['rev-parse', 'HEAD']);
|
|
1854
|
+
// A stale review must see the base's bytes next time. Local commits keep today's refusal.
|
|
1855
|
+
if (!head.error && head.status === 0 && head.stdout.trim() !== base) {
|
|
1856
|
+
const ancestor = cleanupGit(scope.root, ['merge-base', '--is-ancestor', head.stdout.trim(), base]);
|
|
1857
|
+
if (!ancestor.error && ancestor.status === 0) {
|
|
1858
|
+
const updated = cleanupUpdateCheckout(scope);
|
|
1859
|
+
if (updated.error) return updated;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1849
1863
|
if (prepared.error) return prepared;
|
|
1850
|
-
//
|
|
1864
|
+
// The remote base already settled every reviewed item: the checkout only has to take it.
|
|
1851
1865
|
if (!prepared.acceptance) return cleanupUpdateCheckout(scope);
|
|
1852
1866
|
const completed = lastJson(run(['complete', '--receipt', allocated.receipt, '--acceptance', prepared.acceptance, '--writers-stopped'], scope.root).stdout);
|
|
1853
1867
|
const status = completed?.delivery_status || completed?.status;
|
|
@@ -1858,18 +1872,16 @@ function cleanupDeliver(scope, items, current) {
|
|
|
1858
1872
|
return { error: cleanupReason(auth ? 'auth-failed' : 'delivery-failed', { detail: reason, ...(completed?.resume ? { resume: completed.resume } : {}) }) };
|
|
1859
1873
|
}
|
|
1860
1874
|
|
|
1861
|
-
//
|
|
1862
|
-
//
|
|
1863
|
-
//
|
|
1864
|
-
|
|
1865
|
-
function cleanupRemovedUpstream(root, worktree) {
|
|
1875
|
+
// With no fresh counterpart, the base settled an item only if it changed the path's content or
|
|
1876
|
+
// removed it in every spelling, while the checkout holds it under its tracked spelling unchanged
|
|
1877
|
+
// since the fork point. A copy committed or changed only here is not one the base settled.
|
|
1878
|
+
function cleanupSettledUpstream(root, base) {
|
|
1866
1879
|
const read = (cwd, args, options) => {
|
|
1867
1880
|
const result = cleanupGit(cwd, args, options);
|
|
1868
1881
|
return !result.error && result.status === 0 ? result.stdout : null;
|
|
1869
1882
|
};
|
|
1870
1883
|
const object = (spec) => read(root, ['rev-parse', '--verify', '--quiet', spec])?.trim() || null;
|
|
1871
|
-
const
|
|
1872
|
-
const fork = base ? read(root, ['merge-base', 'HEAD', base])?.trim() : null;
|
|
1884
|
+
const fork = read(root, ['merge-base', 'HEAD', base])?.trim();
|
|
1873
1885
|
let upstream; // the base's paths, folded to lower case, read once and only when needed
|
|
1874
1886
|
return (relative) => {
|
|
1875
1887
|
if (!fork) return false;
|
|
@@ -1878,17 +1890,20 @@ function cleanupRemovedUpstream(root, worktree) {
|
|
|
1878
1890
|
?.split('\0').filter(Boolean).map((name) => name.toLowerCase()) ?? null;
|
|
1879
1891
|
}
|
|
1880
1892
|
const lower = relative.toLowerCase();
|
|
1881
|
-
if (!upstream
|
|
1893
|
+
if (!upstream) return false;
|
|
1882
1894
|
// The tracked spelling, which a case-insensitive file system lets differ from the one on disk.
|
|
1883
1895
|
const tracked = read(root, ['ls-files', '-z', '--', `:(icase,literal)${relative}`])?.split('\0').find(Boolean);
|
|
1884
1896
|
const spelled = tracked?.slice(0, relative.length);
|
|
1885
1897
|
if (!spelled || spelled.toLowerCase() !== lower) return false;
|
|
1886
1898
|
const before = object(`${fork}:${spelled}`);
|
|
1887
|
-
|
|
1899
|
+
if (before === null || before !== object(`HEAD:${spelled}`)) return false;
|
|
1900
|
+
const after = object(`${base}:${spelled}`);
|
|
1901
|
+
return after !== null ? after !== before
|
|
1902
|
+
: !upstream.some((name) => name === lower || name.startsWith(`${lower}/`));
|
|
1888
1903
|
};
|
|
1889
1904
|
}
|
|
1890
1905
|
|
|
1891
|
-
// The
|
|
1906
|
+
// The remote base's cleanup or changed copies reach this checkout only when it takes that base: a checkout on
|
|
1892
1907
|
// the base branch is fast-forwarded to it, as a task's owner reconciles after delivery. The base is
|
|
1893
1908
|
// fetched into a ref this call owns (FETCH_HEAD can be rewritten by another fetch meanwhile) and
|
|
1894
1909
|
// merged by its commit, only while the checkout is still the one checked before the fetch, and the
|
|
@@ -1923,9 +1938,9 @@ function cleanupUpdateCheckout(scope) {
|
|
|
1923
1938
|
|
|
1924
1939
|
// Inside the allocated worktree: prove every reviewed item, remove them, commit, and write the
|
|
1925
1940
|
// acceptance completion binds. A failure releases the worktree; so does a throw, in the caller.
|
|
1926
|
-
// With every reviewed
|
|
1941
|
+
// With every reviewed item already settled by the remote base, the worktree is released with
|
|
1927
1942
|
// nothing to deliver.
|
|
1928
|
-
function cleanupDeliveryCommit(worktree, items, current, release,
|
|
1943
|
+
function cleanupDeliveryCommit(worktree, items, current, release, settledUpstream) {
|
|
1929
1944
|
const fresh = cleanupProjectScope(worktree, current);
|
|
1930
1945
|
const available = new Map(fresh.items.map((item) => [`${item.kind}\u0000${item.relative}`, item]));
|
|
1931
1946
|
const pairs = items.map((reviewed) => {
|
|
@@ -1935,7 +1950,7 @@ function cleanupDeliveryCommit(worktree, items, current, release, removedUpstrea
|
|
|
1935
1950
|
const stale = pairs.filter(({ reviewed, fresh, before }) => (fresh
|
|
1936
1951
|
? fresh.action !== reviewed.action || fresh.route !== 'delivery'
|
|
1937
1952
|
|| before.type !== reviewed.before.type || before.sha256 !== reviewed.before.sha256
|
|
1938
|
-
: !
|
|
1953
|
+
: !settledUpstream(reviewed.relative)));
|
|
1939
1954
|
// Check the entire batch before mutating even one item; the remote base cannot replace the reviewed preimage.
|
|
1940
1955
|
if (stale.length) return release(cleanupReason('stale-plan', { detail: stale.map(({ reviewed }) => reviewed.relative).join(', ') }));
|
|
1941
1956
|
const present = pairs.filter(({ fresh }) => fresh);
|
|
@@ -89,14 +89,15 @@ function effectiveConfig(context) {
|
|
|
89
89
|
const keys = {
|
|
90
90
|
claude: ['appendSystemPrompt', 'pluginDirs'], codex: ['developer_instructions', 'skills', 'marketplaces', 'plugins'],
|
|
91
91
|
grok: ['rules'], omp: ['appendSystemPrompt', 'skills'], antigravity: [], cursor: [],
|
|
92
|
-
opencode: ['inlineConfig'], kimi: ['ownerAgent'], copilot: ['skillsDirs'], qwen: ['appendSystemPrompt'],
|
|
92
|
+
opencode: ['inlineConfig', 'skillsPaths'], kimi: ['ownerAgent'], copilot: ['skillsDirs', 'instructionsDirs'], qwen: ['appendSystemPrompt'],
|
|
93
93
|
}[context.engine];
|
|
94
94
|
if (!object(config) || Object.keys(config).some((key) => !keys.includes(key))) return null;
|
|
95
|
-
for (const field of ['appendSystemPrompt', 'developer_instructions', 'rules', 'inlineConfig', 'skillsDirs']) {
|
|
95
|
+
for (const field of ['appendSystemPrompt', 'developer_instructions', 'rules', 'inlineConfig', 'skillsDirs', 'instructionsDirs']) {
|
|
96
96
|
if (config[field] !== undefined && config[field] !== null && !textValue(config[field])) return null;
|
|
97
97
|
}
|
|
98
98
|
if (context.engine === 'kimi' && typeof config.ownerAgent !== 'boolean') return null;
|
|
99
99
|
if (config.pluginDirs !== undefined && (!strings(config.pluginDirs) || !config.pluginDirs.every(path.isAbsolute))) return null;
|
|
100
|
+
if (config.skillsPaths != null && !strings(config.skillsPaths)) return null;
|
|
100
101
|
if (context.engine === 'codex') {
|
|
101
102
|
for (const key of ['marketplaces', 'plugins', 'skills']) if (config[key] != null && !object(config[key])) return null;
|
|
102
103
|
if (config.skills?.config !== undefined && (!Array.isArray(config.skills.config)
|
|
@@ -144,7 +145,10 @@ function discoveryRoots(context, env, config) {
|
|
|
144
145
|
for (const dir of config?.skills?.customDirectories || []) add(context.cwd, dir);
|
|
145
146
|
}
|
|
146
147
|
if (engine === 'codex' && process.platform !== 'win32') add('/etc/codex', 'skills');
|
|
147
|
-
if (engine === 'opencode')
|
|
148
|
+
if (engine === 'opencode') {
|
|
149
|
+
add(env.OPENCODE_CONFIG_DIR, 'skills');
|
|
150
|
+
for (const dir of config?.skillsPaths || []) add(context.cwd, dir);
|
|
151
|
+
}
|
|
148
152
|
if (engine === 'copilot') for (const dir of (config?.skillsDirs ?? env.COPILOT_SKILLS_DIRS ?? '').split(',').filter(Boolean)) add(context.cwd, dir);
|
|
149
153
|
for (let dir = path.resolve(context.cwd); ; dir = path.dirname(dir)) {
|
|
150
154
|
if (native[1]) add(dir, native[1], 'skills');
|
|
@@ -156,9 +160,11 @@ function discoveryRoots(context, env, config) {
|
|
|
156
160
|
return [...new Set(roots)];
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
function inspectSkills(context, release, env, config) {
|
|
163
|
+
function inspectSkills(context, release, env, config, active) {
|
|
160
164
|
const found = [], errors = [], hashes = new Map();
|
|
161
165
|
for (const root of discoveryRoots(context, env, config)) {
|
|
166
|
+
// Reusing the selected, verified release is not a competing OpenCode source.
|
|
167
|
+
if (active && context.engine === 'opencode' && config?.skillsPaths && root === path.join(release.root, 'config/skills')) continue;
|
|
162
168
|
for (const name of SKILLS) {
|
|
163
169
|
const dir = path.join(root, name);
|
|
164
170
|
try {
|
|
@@ -227,7 +233,7 @@ function planActivation(releaseRoot, input, env = process.env) {
|
|
|
227
233
|
fail('release-version-conflict', { version: context.recordedRelease.version }, 3);
|
|
228
234
|
}
|
|
229
235
|
const config = effectiveConfig(context);
|
|
230
|
-
const { found, errors } = inspectSkills(context, release, env, config);
|
|
236
|
+
const { found, errors } = inspectSkills(context, release, env, config, !off);
|
|
231
237
|
if (errors.length) status.hide = channel('partial', null, errors);
|
|
232
238
|
status.inheritedDevlyn = found.filter((entry) => entry.owned).map((entry) => entry.path);
|
|
233
239
|
const supported = supportedVersion(context.engine, context.version);
|
|
@@ -368,19 +374,35 @@ function planActivation(releaseRoot, input, env = process.env) {
|
|
|
368
374
|
let inline;
|
|
369
375
|
try { inline = config.inlineConfig == null ? {} : JSON.parse(config.inlineConfig); }
|
|
370
376
|
catch (error) { if (!(error instanceof SyntaxError)) throw error; inline = null; }
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
const invalidField = !object(inline) || (inline.instructions !== undefined && !strings(inline.instructions))
|
|
378
|
+
? 'inlineConfig.instructions'
|
|
379
|
+
: config.skillsPaths != null && inline.skills !== undefined && !object(inline.skills) ? 'inlineConfig.skills' : null;
|
|
380
|
+
if (invalidField) {
|
|
381
|
+
status.principles = channel('partial', null, [reason('config-unreadable', { field: invalidField })]);
|
|
382
|
+
status.skills = channel('partial', null, [reason('config-unreadable', { field: invalidField })]);
|
|
374
383
|
break;
|
|
375
384
|
}
|
|
385
|
+
if (Array.isArray(config.skillsPaths) && !collisions.length && !errors.length) {
|
|
386
|
+
inline.skills = { ...inline.skills, paths: [...new Set([...config.skillsPaths, path.join(release.root, 'config/skills')])] };
|
|
387
|
+
native('skills.paths', 'read-file', `Read ${path.join(release.root, 'config/skills/devlyn-ideate/SKILL.md')}`);
|
|
388
|
+
} else fallback(config.skillsPaths == null ? [] : [...collisions, ...errors]);
|
|
376
389
|
plan.envPatch.OPENCODE_CONFIG_CONTENT = JSON.stringify({ ...inline,
|
|
377
390
|
instructions: [...(inline.instructions || []), file('devlyn-instructions.md', instructions)] });
|
|
378
|
-
principles('OPENCODE_CONFIG_CONTENT.instructions');
|
|
391
|
+
principles('OPENCODE_CONFIG_CONTENT.instructions');
|
|
379
392
|
break;
|
|
380
393
|
}
|
|
381
394
|
case 'copilot': {
|
|
382
|
-
|
|
383
|
-
|
|
395
|
+
if (!context.committedBlockApplies) {
|
|
396
|
+
const directory = path.join(context.runDir, 'copilot');
|
|
397
|
+
if (directory.includes(',')) {
|
|
398
|
+
status.principles = channel('unavailable', null, [reason('principles-unavailable', { detail: 'comma-in-path' })]);
|
|
399
|
+
} else {
|
|
400
|
+
file('copilot/devlyn.instructions.md', '---\napplyTo: "**"\n---\n' + instructions);
|
|
401
|
+
const dirs = config.instructionsDirs ?? env.COPILOT_CUSTOM_INSTRUCTIONS_DIRS ?? '';
|
|
402
|
+
plan.envPatch.COPILOT_CUSTOM_INSTRUCTIONS_DIRS = dirs.split(',').includes(directory) ? dirs : (dirs ? `${dirs},${directory}` : directory);
|
|
403
|
+
principles('COPILOT_CUSTOM_INSTRUCTIONS_DIRS');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
384
406
|
const root = path.join(release.root, 'config/skills');
|
|
385
407
|
if (root.includes(',') || collisions.length || errors.length) {
|
|
386
408
|
status.skills = channel('partial', null, root.includes(',') ? [reason('skills-unavailable', { detail: 'comma-in-path' })] : [...collisions, ...errors]);
|
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.8",
|
|
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.8",
|
|
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": "5841ad32184de3095edffc5d05ae78dded34dc0ce829e8f805218de8b0085b1f",
|
|
133
133
|
"executable": true
|
|
134
134
|
},
|
|
135
135
|
{
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"path": "bin/harness-activation.js",
|
|
142
|
-
"sha256": "
|
|
142
|
+
"sha256": "980f251957d9093b47ba2c5229b77145233a7253938220b69069468f7b4f8979",
|
|
143
143
|
"executable": false
|
|
144
144
|
},
|
|
145
145
|
{
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"path": "bin/skill-history.json",
|
|
167
|
-
"sha256": "
|
|
167
|
+
"sha256": "9012124a107faba4ee1a0719462920590ff04bcdf82d5bc40d178ef8f182ea82",
|
|
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": "54f89d39a32c9d822321ae2a2805c8fe071c2b78c6b84f1c29ac85e4ae59667f",
|
|
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": "6c507f8668ee2df5322e6bc0a7f1f7303410d2566b24709eaf4a9a581a25755c",
|
|
443
443
|
"executable": false
|
|
444
444
|
},
|
|
445
445
|
{
|
|
446
446
|
"path": "native/codex/plugins/devlyn/.codex-plugin/plugin.json",
|
|
447
|
-
"sha256": "
|
|
447
|
+
"sha256": "54f89d39a32c9d822321ae2a2805c8fe071c2b78c6b84f1c29ac85e4ae59667f",
|
|
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": "42de8c1332266cae7209808646f51e3b97f3c8e2dc2e4775e0e0ab9daa52bfd1",
|
|
1623
1623
|
"executable": false
|
|
1624
1624
|
}
|
|
1625
1625
|
],
|
|
1626
1626
|
"digest": {
|
|
1627
1627
|
"algorithm": "sha256",
|
|
1628
|
-
"value": "
|
|
1628
|
+
"value": "cac9f8a9d305a77ecff9139fe06bad8a9932a47dc1f800c20520c5a79b969070"
|
|
1629
1629
|
}
|
|
1630
1630
|
}
|