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 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, cleanupRemovedUpstream(scope.root, worktree)); }
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
- // Every reviewed copy is already gone from the remote base: the checkout only has to take it.
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
- // 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) {
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 base = read(worktree, ['rev-parse', 'HEAD'])?.trim();
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 || upstream.some((name) => name === lower || name.startsWith(`${lower}/`))) return false;
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
- return before !== null && before === object(`HEAD:${spelled}`);
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 copies the remote base dropped stay in this checkout until it takes that base: a checkout on
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 copy already removed by the remote base, the worktree is released with
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, removedUpstream) {
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
- : !removedUpstream(reviewed.relative)));
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') add(env.OPENCODE_CONFIG_DIR, 'skills');
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
- if (!object(inline) || (inline.instructions !== undefined && !strings(inline.instructions))) {
372
- status.principles = channel('partial', null, [reason('config-unreadable', { field: 'inlineConfig.instructions' })]);
373
- status.skills = channel('partial', null, [reason('config-unreadable', { field: 'inlineConfig.instructions' })]);
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'); fallback();
391
+ principles('OPENCODE_CONFIG_CONTENT.instructions');
379
392
  break;
380
393
  }
381
394
  case 'copilot': {
382
- status.principles = context.committedBlockApplies ? channel('configured', 'native-project')
383
- : channel('unavailable', null, [reason('principles-unavailable')]);
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]);
@@ -767,6 +767,8 @@
767
767
  "4.3.3",
768
768
  "4.3.4",
769
769
  "4.3.5",
770
- "4.3.6"
770
+ "4.3.6",
771
+ "4.3.7",
772
+ "4.3.8"
771
773
  ]
772
774
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.6",
3
+ "version": "4.3.8",
4
4
  "description": "Devlyn harness skills",
5
5
  "skills": "./skills/"
6
6
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "devlyn-harness-v4-3-6",
2
+ "name": "devlyn-harness-v4-3-8",
3
3
  "plugins": [
4
4
  {
5
5
  "name": "devlyn",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn",
3
- "version": "4.3.6",
3
+ "version": "4.3.8",
4
4
  "description": "Devlyn harness skills",
5
5
  "skills": "./skills/"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.3.6",
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.6",
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": "6eb4d078cc4fe235517b7a08e8d6965462cd6c91ab1853cc2f8a9f0b829f6191",
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": "94adca93876fd0d8a39b5dddd1e017296b9ff7d1cf6037871df3d32250ea83ba",
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": "c0aa4b8a25ec6873011859bc5c76cad58d1cd468e13018ea85761b8db51b81e9",
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": "4ca0474d48fdbee91769be02bd6a0b612313a390b0b0fe244679df0a43bc2e37",
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": "f8189a7e93f4e5ff3334d8ad884fffa679a2729f10fff815d24c4f0067534c73",
442
+ "sha256": "6c507f8668ee2df5322e6bc0a7f1f7303410d2566b24709eaf4a9a581a25755c",
443
443
  "executable": false
444
444
  },
445
445
  {
446
446
  "path": "native/codex/plugins/devlyn/.codex-plugin/plugin.json",
447
- "sha256": "4ca0474d48fdbee91769be02bd6a0b612313a390b0b0fe244679df0a43bc2e37",
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": "4d754dc7026976c1e37d7d74b6733459fcd07b6a498bc3e8b4c4d575b02a7700",
1622
+ "sha256": "42de8c1332266cae7209808646f51e3b97f3c8e2dc2e4775e0e0ab9daa52bfd1",
1623
1623
  "executable": false
1624
1624
  }
1625
1625
  ],
1626
1626
  "digest": {
1627
1627
  "algorithm": "sha256",
1628
- "value": "f4ee05e55111300285e2ec7b435934786fee40501365c8a2a2706b7ee168bc20"
1628
+ "value": "cac9f8a9d305a77ecff9139fe06bad8a9932a47dc1f800c20520c5a79b969070"
1629
1629
  }
1630
1630
  }