devlyn-cli 4.3.28 → 4.3.30

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.
Files changed (73) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +10 -10
  4. package/bin/devlyn.js +92 -58
  5. package/bin/engines.js +10 -10
  6. package/bin/harness-activation.js +369 -23
  7. package/bin/harness-context.js +7 -7
  8. package/bin/instruction-templates.json +2 -0
  9. package/bin/skill-history.json +12 -0
  10. package/config/skills/_shared/engine-doctor.sh +2 -2
  11. package/config/skills/_shared/engine-preflight.md +2 -4
  12. package/config/skills/_shared/managed-storage.md +80 -0
  13. package/config/skills/_shared/platform-support.py +67 -28
  14. package/config/skills/_shared/role-config.py +16 -7
  15. package/config/skills/_shared/runtime-principles.md +1 -1
  16. package/config/skills/_shared/storage-client.py +31 -7
  17. package/config/skills/_shared/task-complete.py +1 -1
  18. package/config/skills/_shared/task-completion.md +5 -91
  19. package/config/skills/devlyn-engines/SKILL.md +1 -1
  20. package/config/skills/devlyn-ideate/SKILL.md +2 -2
  21. package/config/skills/devlyn-ideate/references/executor.md +62 -0
  22. package/config/skills/devlyn-ideate/references/loop.md +4 -63
  23. package/config/skills/devlyn-ideate/references/package-format.md +1 -2
  24. package/config/skills/devlyn-ideate/scripts/acceptance.py +442 -21
  25. package/config/skills/devlyn-ideate/scripts/queue.py +58 -10
  26. package/harness/AGENTS.md +2 -2
  27. package/harness/CLAUDE.md +2 -2
  28. package/native/claude/.claude-plugin/plugin.json +1 -1
  29. package/native/claude/skills/_shared/engine-doctor.sh +2 -2
  30. package/native/claude/skills/_shared/engine-preflight.md +2 -4
  31. package/native/claude/skills/_shared/managed-storage.md +80 -0
  32. package/native/claude/skills/_shared/platform-support.py +67 -28
  33. package/native/claude/skills/_shared/role-config.py +16 -7
  34. package/native/claude/skills/_shared/runtime-principles.md +1 -1
  35. package/native/claude/skills/_shared/storage-client.py +31 -7
  36. package/native/claude/skills/_shared/task-complete.py +1 -1
  37. package/native/claude/skills/_shared/task-completion.md +5 -91
  38. package/native/claude/skills/devlyn-engines/SKILL.md +1 -1
  39. package/native/claude/skills/devlyn-ideate/SKILL.md +2 -2
  40. package/native/claude/skills/devlyn-ideate/references/executor.md +62 -0
  41. package/native/claude/skills/devlyn-ideate/references/loop.md +4 -63
  42. package/native/claude/skills/devlyn-ideate/references/package-format.md +1 -2
  43. package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +442 -21
  44. package/native/claude/skills/devlyn-ideate/scripts/queue.py +58 -10
  45. package/native/codex/.agents/plugins/marketplace.json +1 -1
  46. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
  47. package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +2 -2
  48. package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +2 -4
  49. package/native/codex/plugins/devlyn/skills/_shared/managed-storage.md +80 -0
  50. package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +67 -28
  51. package/native/codex/plugins/devlyn/skills/_shared/role-config.py +16 -7
  52. package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +1 -1
  53. package/native/codex/plugins/devlyn/skills/_shared/storage-client.py +31 -7
  54. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +1 -1
  55. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +5 -91
  56. package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +1 -1
  57. package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +2 -2
  58. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/executor.md +62 -0
  59. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +4 -63
  60. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +1 -2
  61. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +442 -21
  62. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +58 -10
  63. package/package.json +1 -11
  64. package/release.json +99 -69
  65. /package/config/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
  66. /package/config/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
  67. /package/config/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
  68. /package/native/claude/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
  69. /package/native/claude/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
  70. /package/native/claude/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
  71. /package/native/codex/plugins/devlyn/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
  72. /package/native/codex/plugins/devlyn/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
  73. /package/native/codex/plugins/devlyn/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
@@ -8,9 +8,14 @@ const history = require('./skill-history.json');
8
8
  const { MARKER, marketplaceName, projectionPath, inspectProjection } = require('./codex-projection');
9
9
 
10
10
  const SKILLS = ['devlyn-ideate', 'devlyn-engines'];
11
+ // The version an engine's run-folder channel was examined on; bin/engines.js keeps the engine's floor. An
12
+ // older version, and Windows (channelGate), keep the plan they had before the channel: only a first task
13
+ // carries the principles.
14
+ const CHANNEL_MINIMUM = { antigravity: '1.5.4', cursor: '2026.11.05-86047df' };
11
15
  const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
12
16
  const textValue = (value) => typeof value === 'string' && !value.includes('\0');
13
17
  const strings = (value) => Array.isArray(value) && value.every(textValue);
18
+ const isFile = (file) => fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
14
19
  const reason = (code, params = {}) => ({ code, params });
15
20
  const channel = (state = 'disabled', mechanism = null, reasons = []) => ({ state, mechanism, reasons });
16
21
  const joinText = (...parts) => parts.filter((part) => part !== null && part !== undefined && part !== '').join('\n\n');
@@ -90,6 +95,7 @@ function effectiveConfig(context) {
90
95
  claude: ['appendSystemPrompt', 'pluginDirs'], codex: ['developer_instructions', 'skills', 'marketplaces', 'plugins'],
91
96
  grok: ['rules'], omp: ['appendSystemPrompt', 'skills'], antigravity: [], cursor: [],
92
97
  opencode: ['inlineConfig', 'skillsPaths'], kimi: ['ownerAgent'], copilot: ['skillsDirs', 'instructionsDirs'], qwen: ['appendSystemPrompt'],
98
+ pi: ['appendSystemPrompt'],
93
99
  }[context.engine];
94
100
  if (!object(config) || Object.keys(config).some((key) => !keys.includes(key))) return null;
95
101
  for (const field of ['appendSystemPrompt', 'developer_instructions', 'rules', 'inlineConfig', 'skillsDirs', 'instructionsDirs']) {
@@ -120,11 +126,27 @@ function effectiveConfig(context) {
120
126
  return config;
121
127
  }
122
128
 
129
+ // pi's agent dir is PI_CODING_AGENT_DIR, else ~/.pi/agent. That ~, and a leading ~ pi expands in the
130
+ // variable, is os.homedir(): USERPROFILE on Windows, HOME elsewhere (config.js getAgentDir). The home of
131
+ // its user .agents/skills takes HOME first on every platform (package-manager.js getHomeDir).
132
+ function piFolders(cwd, env, platform = process.platform) {
133
+ const paths = platform === 'win32' ? path.win32 : path.posix;
134
+ const home = (platform === 'win32' ? env.USERPROFILE : env.HOME) || os.homedir();
135
+ const dir = env.PI_CODING_AGENT_DIR;
136
+ const agentDir = dir ? dir.replace(platform === 'win32' ? /^~(?=$|[\\/])/ : /^~(?=$|\/)/, () => home) : paths.join(home, '.pi', 'agent');
137
+ return { agentDir: paths.resolve(cwd, agentDir), agentsHome: env.HOME || home };
138
+ }
139
+
123
140
  function discoveryRoots(context, env, config) {
124
141
  const home = env.HOME || env.USERPROFILE || os.homedir();
142
+ const pi = piFolders(context.cwd, env);
125
143
  const roots = [...(context.skillRoots || [])];
126
144
  const add = (base, ...parts) => { if (base) roots.push(path.resolve(base, ...parts)); };
127
145
  const engine = context.engine;
146
+ // Antigravity and Cursor search the folders their examined version reads only where they get their run-folder
147
+ // channel; elsewhere they keep the folders searched before it.
148
+ const channelled = ['antigravity', 'cursor'].includes(engine) && !channelGate(context);
149
+ const antigravity = channelled && engine === 'antigravity';
128
150
  const native = {
129
151
  claude: [env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), '.claude'],
130
152
  codex: [context.accountHome || env.CODEX_HOME || path.join(home, '.codex'), '.agents'],
@@ -135,10 +157,13 @@ function discoveryRoots(context, env, config) {
135
157
  kimi: [env.KIMI_CODE_HOME || path.join(home, '.kimi-code'), '.kimi-code'],
136
158
  copilot: [env.COPILOT_HOME || path.join(home, '.copilot'), '.github'],
137
159
  qwen: [env.QWEN_HOME || path.join(home, '.qwen'), '.qwen'],
160
+ pi: [pi.agentDir, null],
138
161
  }[engine];
139
162
  if (engine !== 'codex') add(native[0], 'skills');
140
163
  const copilotDefaultHome = !env.COPILOT_HOME || path.resolve(env.COPILOT_HOME) === path.join(home, '.copilot');
141
- if (!['claude', 'cursor'].includes(engine) && (engine !== 'copilot' || copilotDefaultHome)) add(home, '.agents/skills');
164
+ if (!['claude', 'cursor'].includes(engine) && !antigravity && (engine !== 'copilot' || copilotDefaultHome)) add(engine === 'pi' ? pi.agentsHome : home, '.agents/skills');
165
+ // The examined Antigravity reads ~/.gemini/skills and its personal folder ~/.gemini/config instead.
166
+ if (antigravity) { add(home, '.gemini/skills'); add(home, '.gemini/config/skills'); }
142
167
  if (['omp', 'opencode', 'grok'].includes(engine)) add(home, '.claude/skills');
143
168
  if (engine === 'omp') {
144
169
  add(home, '.codex/skills'); add(home, '.agent/skills');
@@ -150,12 +175,25 @@ function discoveryRoots(context, env, config) {
150
175
  for (const dir of config?.skillsPaths || []) add(context.cwd, dir);
151
176
  }
152
177
  if (engine === 'copilot') for (const dir of (config?.skillsDirs ?? env.COPILOT_SKILLS_DIRS ?? '').split(',').filter(Boolean)) add(context.cwd, dir);
178
+ if (engine === 'pi') add(context.cwd, '.pi/skills');
179
+ const walk = [];
153
180
  for (let dir = path.resolve(context.cwd); ; dir = path.dirname(dir)) {
181
+ walk.push(dir);
182
+ if (fs.existsSync(path.join(dir, '.git'))) break;
183
+ // Outside any repository the examined Antigravity reads the working directory alone.
184
+ if (dir === path.dirname(dir)) { if (antigravity) walk.splice(1); break; }
185
+ }
186
+ // The examined Cursor also searches nested folders below the working directory, which the planner does not.
187
+ const cursorFolders = channelled && engine === 'cursor' ? ['.cursor', '.agents', '.claude', '.codex', '.grok'] : [];
188
+ for (const folder of cursorFolders) add(home, folder, 'skills');
189
+ if (cursorFolders.length) add(home, '.cursor/skills-cursor');
190
+ for (const dir of walk) {
191
+ for (const folder of cursorFolders) add(dir, folder, 'skills');
154
192
  if (native[1]) add(dir, native[1], 'skills');
155
193
  if (!['claude', 'cursor'].includes(engine)) add(dir, '.agents/skills');
156
194
  if (['grok', 'omp', 'opencode', 'copilot'].includes(engine)) add(dir, '.claude/skills');
157
195
  if (['omp', 'antigravity'].includes(engine)) add(dir, '.agent/skills');
158
- if (fs.existsSync(path.join(dir, '.git')) || dir === path.dirname(dir)) break;
196
+ if (antigravity) { add(dir, '_agents/skills'); add(dir, '_agent/skills'); }
159
197
  }
160
198
  return [...new Set(roots)];
161
199
  }
@@ -187,6 +225,153 @@ function kimiSkillRoots(context, env) {
187
225
  return { roots: [...roots], errors };
188
226
  }
189
227
 
228
+ // omp and pi read their own APPEND_SYSTEM.md only when no append text is passed, and the plan passes
229
+ // some, so the owner's text leads it. Text the caller supplies wins, an explicit empty string included.
230
+ // Only when it supplies none (the key absent, or null for unset) is the first existing file `lookup()`
231
+ // names read, as `devlyn run` reads omp's (harness-context.js ompOwnerAppend). Returns {text}, or
232
+ // {reason} when the owner's text cannot be known.
233
+ function ownerAppend(config, lookup) {
234
+ if (config.appendSystemPrompt != null) return { text: config.appendSystemPrompt };
235
+ try {
236
+ const found = lookup();
237
+ if (found.reason) return found;
238
+ const file = found.files.find(isFile);
239
+ return { text: file === undefined ? null : fs.readFileSync(file, 'utf8') };
240
+ } catch (error) {
241
+ return { reason: reason('config-unreadable', { path: error.path, detail: error.code || error.message }) };
242
+ }
243
+ }
244
+
245
+ // pi's user and project settings.json (settings-manager.js). Entries they name resolve through pi's own
246
+ // package manager, which the planner does not repeat. A file that cannot be read is null and reported.
247
+ function piSettings(cwd, agentDir) {
248
+ const settings = { errors: [] };
249
+ for (const [scope, file] of [['user', path.join(agentDir, 'settings.json')], ['project', path.join(cwd, '.pi/settings.json')]]) {
250
+ try {
251
+ settings[scope] = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '') || '{}') : {};
252
+ } catch (error) {
253
+ settings[scope] = null;
254
+ settings.errors.push(reason('config-unreadable', { path: file, detail: error.code || error.message }));
255
+ }
256
+ }
257
+ return settings;
258
+ }
259
+ const listed = (settings, ...keys) => keys.some((key) => settings?.[key] != null && settings[key].length !== 0);
260
+
261
+ // Whether the agent dir's extensions folder holds anything pi could load; pi skips dot entries and
262
+ // node_modules (package-manager.js collectAutoExtensionEntries).
263
+ function piExtensionEntries(agentDir) {
264
+ try {
265
+ return fs.readdirSync(path.join(agentDir, 'extensions')).some((name) => !name.startsWith('.') && name !== 'node_modules');
266
+ } catch (error) {
267
+ if (error.code === 'ENOENT') return false;
268
+ throw error;
269
+ }
270
+ }
271
+
272
+ // pi reads a project's .pi/APPEND_SYSTEM.md only when it trusts the project, else its agent dir's
273
+ // (resource-loader discoverAppendSystemPromptFile). pi first asks the extensions it loads before trust,
274
+ // and the first yes or no wins (project-trust.js resolveProjectTrusted). With project settings held
275
+ // untrusted, those come only from the agent dir's extensions folder and the extensions or packages its
276
+ // user settings name (resource-loader loadProjectTrustExtensions, package-manager resolve): the app
277
+ // passes no -e, and pi's built-in extensions answer no trust question. With none, trust is pi's saved
278
+ // decision for the folder or its nearest decided parent (trust.json). Otherwise pi settles trust at
279
+ // launch, so which file it reads cannot be known here. The planner never writes trust.
280
+ function piAppendFiles(cwd, agentDir, userSettings) {
281
+ const project = path.join(cwd, '.pi/APPEND_SYSTEM.md');
282
+ const user = path.join(agentDir, 'APPEND_SYSTEM.md');
283
+ if (!isFile(project)) return { files: [user] };
284
+ const undecided = { reason: reason('principles-unavailable', { detail: 'project-trust-undecided', path: project }) };
285
+ if (userSettings === null || listed(userSettings, 'extensions', 'packages') || piExtensionEntries(agentDir)) return undecided;
286
+ const store = path.join(agentDir, 'trust.json');
287
+ let trusted = null;
288
+ try {
289
+ const saved = fs.existsSync(store) ? JSON.parse(fs.readFileSync(store, 'utf8').replace(/^\uFEFF/, '')) : {};
290
+ if (!object(saved) || Object.values(saved).some((value) => ![true, false, null].includes(value))) throw new Error('not a trust record');
291
+ for (let dir = fs.realpathSync(cwd); trusted === null; dir = path.dirname(dir)) {
292
+ if (typeof saved[dir] === 'boolean') trusted = saved[dir];
293
+ else if (dir === path.dirname(dir)) break;
294
+ }
295
+ } catch (error) {
296
+ return { reason: reason('config-unreadable', { path: store, detail: error.code || error.message }) };
297
+ }
298
+ if (trusted === null) return undecided;
299
+ return { files: [trusted ? project : user] };
300
+ }
301
+
302
+ // The files pi may load as skills below a root (package-manager.js collectSkillEntries). pi stops at a
303
+ // folder's SKILL.md unless an ignore file hides it, and takes other .md files at the root of its own
304
+ // folders and below it in .agents/skills. Ignore files are not read here, so every .md file is taken and
305
+ // every folder searched: at least what pi finds is found. Dot entries and node_modules are skipped as pi
306
+ // skips them.
307
+ function piSkillFiles(root) {
308
+ const files = [], seen = new Set();
309
+ const visit = (dir) => {
310
+ let entries;
311
+ try {
312
+ entries = fs.readdirSync(dir, { withFileTypes: true });
313
+ } catch (error) {
314
+ if (dir === root && error.code === 'ENOENT') return;
315
+ throw error;
316
+ }
317
+ seen.add(fs.realpathSync(dir));
318
+ for (const entry of entries) {
319
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
320
+ const full = path.join(dir, entry.name);
321
+ let kind = entry;
322
+ // pi follows links and skips one it cannot follow.
323
+ if (entry.isSymbolicLink()) { try { kind = fs.statSync(full); } catch { continue; } }
324
+ if (kind.isDirectory()) { if (!seen.has(fs.realpathSync(full))) visit(full); }
325
+ else if (kind.isFile() && entry.name.endsWith('.md')) files.push(full);
326
+ }
327
+ };
328
+ visit(root);
329
+ return files;
330
+ }
331
+
332
+ // The names pi may give a skill file (skills.js loadSkillFromFile): its frontmatter's string `name`, else,
333
+ // also for an empty one, its folder's name. A plain scalar may be no string (null, a number), so the
334
+ // folder counts then too.
335
+ // null when the name is not one simple line, which counts as a collision.
336
+ function piSkillNames(file) {
337
+ const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
338
+ const folder = path.basename(path.dirname(file));
339
+ const end = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
340
+ const lines = end < 0 ? [] : text.slice(4, end).split('\n');
341
+ if (/^\s/.test(lines.find((line) => line.trim() && !line.trim().startsWith('#')) ?? '')) return null;
342
+ // Lines that are or may be a top-level name key: quoted or complex keys, merges, anchors, flow mappings.
343
+ const keys = lines.flatMap((line, index) => (/^(?:name[ \t]*:|["'?{[&*!]|<<)/.test(line) ? [index] : []));
344
+ if (!keys.length) return [folder];
345
+ const match = keys.length === 1 && /^name:(.*)$/.exec(lines[keys[0]]);
346
+ if (!match) return null;
347
+ const value = match[1].replace(/\s#.*$/, '').trim();
348
+ const quoted = /^"([^"\\]*)"$|^'([^']*)'$/.exec(value);
349
+ if (quoted) return [(quoted[1] ?? quoted[2]) || folder];
350
+ const next = lines.slice(keys[0] + 1).find((line) => line.trim()) ?? '';
351
+ if (!value || /^[-?:,[\]{}#&*!|>'"%@`]/.test(value) || /^\s/.test(next)) return null;
352
+ return [value, folder];
353
+ }
354
+
355
+ // pi names a skill by its frontmatter wherever it sits below a root, and a found name wins over --skill.
356
+ // Files `found` already reports are skipped.
357
+ function piNameCollisions(roots, found) {
358
+ const collisions = [], errors = [], seen = new Set(found.map((entry) => entry.path));
359
+ for (const root of roots) {
360
+ try {
361
+ for (const file of piSkillFiles(root)) {
362
+ if (seen.has(file)) continue;
363
+ seen.add(file);
364
+ const names = piSkillNames(file);
365
+ if (names === null) collisions.push(reason('name-collision', { path: file }));
366
+ else for (const name of new Set(names)) if (SKILLS.includes(name)) collisions.push(reason('name-collision', { name, path: file }));
367
+ }
368
+ } catch (error) {
369
+ errors.push(reason('config-unreadable', { path: error.path || root, detail: error.code || error.message }));
370
+ }
371
+ }
372
+ return { collisions, errors };
373
+ }
374
+
190
375
  function inspectSkills(context, release, env, config, active) {
191
376
  const found = [], errors = [], hashes = new Map();
192
377
  for (const root of discoveryRoots(context, env, config)) {
@@ -214,10 +399,73 @@ function skillCatalog(release, paths = path) {
214
399
  return ['Devlyn session skill catalog (read the absolute file to invoke):',
215
400
  ...SKILLS.map((name) => `${name}: ${paths.join(root, name, 'SKILL.md')}`),
216
401
  `This session's _shared folder is ${paths.join(root, '_shared')}.`,
217
- 'Use that absolute folder wherever the Devlyn defaults name _shared/, including the Quick Start references to .claude/skills/ or .agents/skills/.',
402
+ 'Use that absolute folder wherever the Devlyn defaults name _shared/.',
218
403
  ].join('\n');
219
404
  }
220
405
 
406
+ // A skill entry of Antigravity's run folder: the release skill's own name and description lines as
407
+ // written, then the release SKILL.md to follow. A copy cannot be planned (plan files are text and the
408
+ // skills carry scripts), and since 1.5.2 Antigravity skips a link or a manifest entry that leads outside
409
+ // the added folder. Each line must be one complete top-level entry, or the plan fails.
410
+ function antigravitySkill(release, name) {
411
+ const dir = path.join(release.root, 'config/skills', name);
412
+ const file = path.join(dir, 'SKILL.md');
413
+ const text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
414
+ const end = text.startsWith('---\n') ? text.indexOf('\n---\n', 3) : -1;
415
+ const lines = end < 0 ? [] : text.slice(4, end).split('\n');
416
+ const header = ['name', 'description'].map((field) => {
417
+ const at = lines.flatMap((line, index) => (line.startsWith(`${field}:`) ? [index] : []));
418
+ if (at.length !== 1 || !lines[at[0]].slice(field.length + 1).trim() || /^\s/.test(lines[at[0] + 1] ?? '')) {
419
+ fail('config-unreadable', { path: file, field }, 1);
420
+ }
421
+ return lines[at[0]];
422
+ });
423
+ return `---\n${header.join('\n')}\n---\nRead ${file} and follow it as this skill. `
424
+ + `Its folder, ${dir}, is the skill's folder (DEVLYN_SKILL_DIR), not the folder of this file.\n`;
425
+ }
426
+
427
+ // The path with the links of its existing part resolved; parts that do not exist yet are kept as given.
428
+ function physicalPath(target) {
429
+ let parent = path.resolve(target);
430
+ const missing = [];
431
+ while (!fs.existsSync(parent)) { missing.unshift(path.basename(parent)); parent = path.dirname(parent); }
432
+ return path.join(fs.realpathSync(parent), ...missing);
433
+ }
434
+
435
+ // The reasons for the principles and the skills when neither reaches the session.
436
+ const withheld = (params) => [reason('principles-unavailable', params), reason('skills-unavailable', params)];
437
+
438
+ // Why an engine keeps the plan it had before its run-folder channel, as the reasons for the principles and the
439
+ // skills when no first task carries them, or null when it gets the channel. The channels were examined on macOS
440
+ // and are not planned on Windows, whatever the version: it cannot show who owns a folder, and an argument that
441
+ // misbehaves there would break a pane. Below the version a channel was examined on, updating the engine gets it.
442
+ function channelGate(context) {
443
+ if (process.platform === 'win32') return withheld({ detail: 'platform-unexamined' });
444
+ const minimum = CHANNEL_MINIMUM[context.engine];
445
+ if (supportedVersion(context.engine, context.version, minimum)) return null;
446
+ const outdated = reason('engine-version-unsupported', { minimum });
447
+ return [outdated, outdated];
448
+ }
449
+
450
+ // The first directory that makes a run-folder channel's `folder`, inside a resolved run directory (physicalPath),
451
+ // unsafe to hand an engine that loads what it finds there, or null. Every existing level from the folder up, the
452
+ // folder included, must be a real directory (a link is not one) of this user or root that no one else can write:
453
+ // another user could put a .git or a folder of their own there later. With `git`, no directory from the folder up
454
+ // may hold a .git entry of any kind: Antigravity 1.5.4 loads the agents and hooks of a Git root above an added
455
+ // folder with no trust check. A level that does not exist yet is skipped; a level lstat cannot read cannot be
456
+ // shown safe. Access-control lists are not read.
457
+ function unsafeRunDir(folder, git) {
458
+ const entry = (file) => {
459
+ try { return fs.lstatSync(file, { throwIfNoEntry: false }) ?? null; } catch { return false; }
460
+ };
461
+ for (let dir = folder; ; dir = path.dirname(dir)) {
462
+ const stats = entry(dir);
463
+ if (stats === false || (stats && (!stats.isDirectory() || ![process.getuid(), 0].includes(stats.uid) || stats.mode & 0o022))) return dir;
464
+ if (git && entry(path.join(dir, '.git')) !== null) return dir;
465
+ if (dir === path.dirname(dir)) return null;
466
+ }
467
+ }
468
+
221
469
  function codexRegistrations(config, accountHome) {
222
470
  const owned = [], errors = [];
223
471
  for (const [key, plugin] of Object.entries(config.plugins || {})) {
@@ -315,11 +563,7 @@ function planActivation(releaseRoot, input, env = process.env) {
315
563
  const catalog = skillCatalog(release);
316
564
  const instructions = joinText(defaults, catalog);
317
565
  const file = (relativePath, utf8) => {
318
- let parent = path.resolve(context.runDir);
319
- const missing = [];
320
- while (!fs.existsSync(parent)) { missing.unshift(path.basename(parent)); parent = path.dirname(parent); }
321
- const physical = path.join(fs.realpathSync(parent), ...missing);
322
- const relative = path.relative(release.root, physical);
566
+ const relative = path.relative(release.root, physicalPath(context.runDir));
323
567
  if (!relative || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) {
324
568
  fail('invalid-arguments', { argument: 'runDir must be outside the release' }, 2);
325
569
  }
@@ -327,11 +571,26 @@ function planActivation(releaseRoot, input, env = process.env) {
327
571
  return path.join(context.runDir, relativePath);
328
572
  };
329
573
  const principles = (mechanism) => { status.principles = channel('configured', mechanism); };
330
- const fallback = (reasons = []) => {
331
- status.skills = channel('partial', 'absolute-catalog', [...reasons, reason('skills-catalog')]);
574
+ // The catalog travels in the principles text: without that text only the absolute invocation is left.
575
+ const fallback = (reasons = [], withCatalog = true) => {
576
+ status.skills = withCatalog ? channel('partial', 'absolute-catalog', [...reasons, reason('skills-catalog')])
577
+ : channel('partial', null, reasons);
332
578
  status.invocation = { kind: 'read-file', text: `Read ${path.join(release.root, 'config/skills/devlyn-ideate/SKILL.md')}` };
333
579
  };
334
580
  const native = (mechanism, kind, text) => { status.skills = channel('configured', mechanism); status.invocation = { kind, text }; };
581
+ // Where no run-folder channel is planned, only a first task carries the principles and the catalog, ahead
582
+ // of its text; without one nothing reaches the session and each channel gives its reason.
583
+ const firstTask = ([principlesReason, skillsReason]) => {
584
+ if (context.initialPrompt) {
585
+ status.principles = channel('partial', 'initial-task', [reason('principles-task-only')]);
586
+ plan.prompt = { ...context.initialPrompt, text: joinText(instructions, context.initialPrompt.text) };
587
+ fallback();
588
+ } else {
589
+ status.principles = channel('unavailable', null, [principlesReason]);
590
+ status.skills = channel('unavailable', null, [skillsReason]);
591
+ }
592
+ };
593
+ // A skill folder that cannot be inspected is also found, so every inspection error comes with a collision.
335
594
  const collisions = found.map((entry) => reason('name-collision', { name: entry.name, path: entry.path }));
336
595
  switch (context.engine) {
337
596
  case 'claude': {
@@ -381,11 +640,39 @@ function planActivation(releaseRoot, input, env = process.env) {
381
640
  case 'omp': {
382
641
  const skills = { ...config.skills,
383
642
  customDirectories: [...new Set([...(config.skills?.customDirectories || []), path.join(release.root, 'config/skills')])] };
384
- plan.engineArgs.push('--config', file('devlyn-omp.json', JSON.stringify({ skills }, null, 2) + '\n'),
385
- '--append-system-prompt', joinText(config.appendSystemPrompt, instructions));
386
- principles('append-system-prompt');
387
- if (collisions.length || errors.length || skills.enableSkillCommands === false || skills.ignoredSkills?.length) fallback([...collisions, ...errors]);
388
- else native('skills.customDirectories', 'slash', '/skill:devlyn-ideate');
643
+ // The files in the order `devlyn run` reads them (harness-context.js ompOwnerAppend).
644
+ const owner = ownerAppend(config, () => ({ files: [path.join(context.cwd, '.omp/APPEND_SYSTEM.md'),
645
+ path.join(env.HOME || env.USERPROFILE || os.homedir(), '.omp/agent/APPEND_SYSTEM.md')] }));
646
+ plan.engineArgs.push('--config', file('devlyn-omp.json', JSON.stringify({ skills }, null, 2) + '\n'));
647
+ // Owner text that cannot be known leaves the flag off, so the engine's own lookup stays intact.
648
+ if (owner.reason) status.principles = channel('partial', null, [owner.reason]);
649
+ else {
650
+ plan.engineArgs.push('--append-system-prompt', joinText(owner.text, instructions));
651
+ principles('append-system-prompt');
652
+ }
653
+ if (collisions.length || errors.length || skills.enableSkillCommands === false || skills.ignoredSkills?.length) {
654
+ fallback([...collisions, ...errors], !owner.reason);
655
+ } else native('skills.customDirectories', 'slash', '/skill:devlyn-ideate');
656
+ break;
657
+ }
658
+ case 'pi': {
659
+ const { agentDir } = piFolders(context.cwd, env);
660
+ const settings = piSettings(context.cwd, agentDir);
661
+ const owner = ownerAppend(config, () => piAppendFiles(context.cwd, agentDir, settings.user));
662
+ if (owner.reason) status.principles = channel('partial', null, [owner.reason]);
663
+ else {
664
+ plan.engineArgs.push('--append-system-prompt', file('devlyn-instructions.md', joinText(owner.text, instructions)));
665
+ principles('append-system-prompt');
666
+ }
667
+ // A discovered or settings-named skill wins over a --skill of the same name.
668
+ const named = piNameCollisions(discoveryRoots(context, env, config), found);
669
+ const reasons = [...collisions, ...named.collisions, ...errors, ...named.errors, ...settings.errors];
670
+ if (reasons.length || [settings.user, settings.project].some((scope) => listed(scope, 'skills', 'packages'))) {
671
+ fallback(reasons, !owner.reason);
672
+ } else {
673
+ for (const name of SKILLS) plan.engineArgs.push('--skill', path.join(release.root, 'config/skills', name));
674
+ native('--skill', 'slash', '/skill:devlyn-ideate');
675
+ }
389
676
  break;
390
677
  }
391
678
  case 'grok':
@@ -452,14 +739,73 @@ function planActivation(releaseRoot, input, env = process.env) {
452
739
  } else fallback([...collisions, ...errors, ...kimi.errors]);
453
740
  }
454
741
  break;
455
- case 'antigravity':
456
- case 'cursor':
457
- status.principles = channel('partial', context.initialPrompt ? 'initial-task' : null, [reason('principles-task-only')]);
458
- if (context.initialPrompt) {
459
- plan.prompt = { ...context.initialPrompt, text: joinText(instructions, context.initialPrompt.text) };
460
- fallback();
461
- } else status.skills = channel('partial', null, [reason('skills-unavailable', { detail: 'no-initial-task' })]);
742
+ case 'antigravity': {
743
+ // Antigravity reads AGENTS.md (always on) and .agents/skills in every workspace folder, and a folder given
744
+ // by --add-dir is one. A conversation keeps its added folders and reads them from disk again on every
745
+ // launch, so the plan is the same in every launch mode and a caller hands a resumed conversation the same
746
+ // runDir: with another, both folders apply while the old one exists. A folder removed later only stops
747
+ // applying. It truncates a rule file over 24,000 bytes.
748
+ const gate = channelGate(context);
749
+ if (gate) {
750
+ firstTask(gate);
751
+ break;
752
+ }
753
+ // The engine is given the resolved folder, the one the checks ran on.
754
+ const folder = path.join(physicalPath(context.runDir), 'antigravity');
755
+ const unsafe = unsafeRunDir(folder, true);
756
+ if (unsafe) {
757
+ firstTask(withheld({ detail: 'run-dir-unsafe', path: unsafe }));
758
+ break;
759
+ }
760
+ const bytes = Buffer.byteLength(instructions), maximum = 24000;
761
+ if (bytes > maximum) {
762
+ status.principles = channel('unavailable', null, [reason('principles-unavailable', { detail: 'too-large', bytes, maximum })]);
763
+ } else {
764
+ file('antigravity/AGENTS.md', instructions);
765
+ principles('add-dir');
766
+ }
767
+ if (collisions.length) fallback([...collisions, ...errors], bytes <= maximum);
768
+ else {
769
+ for (const name of SKILLS) file(`antigravity/.agents/skills/${name}/SKILL.md`, antigravitySkill(release, name));
770
+ native('add-dir', 'slash', '/devlyn-ideate');
771
+ }
772
+ if (plan.files.length) plan.engineArgs.push('--add-dir', folder);
773
+ break;
774
+ }
775
+ case 'cursor': {
776
+ const gate = channelGate(context);
777
+ if (gate) {
778
+ firstTask(gate);
779
+ break;
780
+ }
781
+ // A --plugin-dir folder's sessionStart command hook runs when a session is created, and Cursor puts the
782
+ // additional_context it prints before the model as instructions, kept with the conversation (10,200
783
+ // characters were delivered whole). A resumed chat runs no hook and keeps what its first launch had
784
+ // (measured); a fork is treated the same way, because what it gets is not established. The hook is a
785
+ // command Cursor runs, so it is planned only in a safe run directory, by its resolved absolute path, quoted
786
+ // for the shell: no lookup through PATH or the working folder. The skills come from the release, whatever
787
+ // the run directory.
788
+ const hook = path.join(physicalPath(context.runDir), 'cursor');
789
+ const unsafe = unsafeRunDir(hook);
790
+ if (unsafe || !isFile('/bin/cat')) {
791
+ firstTask(withheld(unsafe ? { detail: 'run-dir-unsafe', path: unsafe } : { detail: 'platform-unexamined' }));
792
+ } else {
793
+ const command = `/bin/cat '${path.join(hook, 'context.json').replace(/'/g, "'\\''")}'`;
794
+ file('cursor/hooks/hooks.json', JSON.stringify({ version: 1, hooks: { sessionStart: [{ command }] } }) + '\n');
795
+ // Every non-ASCII character escaped, so no shell or code page can change the text.
796
+ file('cursor/context.json', JSON.stringify({ additional_context: instructions })
797
+ .replace(/[\u0080-\uffff]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`) + '\n');
798
+ plan.engineArgs.push('--plugin-dir', hook);
799
+ status.principles = continuation ? channel('partial', 'session-start-hook', [reason('principles-session-start-only')])
800
+ : channel('configured', 'session-start-hook');
801
+ }
802
+ if (collisions.length) fallback([...collisions, ...errors], plan.files.length > 0 || plan.prompt !== null);
803
+ else {
804
+ plan.engineArgs.push('--plugin-dir', path.join(release.root, 'config'));
805
+ native('plugin-dir', 'slash', '/devlyn-ideate');
806
+ }
462
807
  break;
808
+ }
463
809
  }
464
810
  if (plan.engineArgs.length || Object.keys(plan.envPatch).length || plan.prompt) plan.envPatch.DEVLYN_HARNESS_ROOT = release.root;
465
811
  return finish(plan);
@@ -473,4 +819,4 @@ function activationCommand(args) {
473
819
  return planActivation(opts['release-root'], readContext(opts['context-file']));
474
820
  }
475
821
 
476
- module.exports = { planActivation, activationCommand, readContext, toml, skillCatalog };
822
+ module.exports = { planActivation, activationCommand, readContext, toml, skillCatalog, piFolders };
@@ -8,7 +8,7 @@ const path = require('path');
8
8
  const crypto = require('crypto');
9
9
  const readline = require('readline');
10
10
  const { spawn, spawnSync } = require('child_process');
11
- const { verifyRelease, materializeRelease, fail } = require('./release');
11
+ const { physicalRoot, materializeRelease, fail } = require('./release');
12
12
 
13
13
  const ENGINES = Object.freeze(['claude', 'codex', 'omp']);
14
14
  const READ_MS = 6000;
@@ -209,13 +209,13 @@ function appState(home, common) {
209
209
  return { defaultRelease: state.defaultRelease || null, choice };
210
210
  }
211
211
 
212
- // A release chosen by a record must verify as recorded; it never yields to another release.
212
+ // A release chosen by a record must be the one recorded; its own planner verifies its files.
213
213
  function recorded(root, version, digest, source) {
214
- let release;
215
- try { release = verifyRelease(root).release; }
216
- catch (error) { fail('release-unavailable', { source, version, detail: error.reason?.code || error.message }, 3); }
217
- if (release.version !== version || release.digest !== digest) fail('release-version-conflict', { source, version }, 3);
218
- return { release, source };
214
+ let manifest;
215
+ try { root = physicalRoot(root); manifest = JSON.parse(fs.readFileSync(path.join(root, 'release.json'), 'utf8')); }
216
+ catch (error) { fail('release-unavailable', { source, version, detail: error.reason?.code || error.code || 'unreadable' }, 3); }
217
+ if (manifest?.version !== version || manifest?.digest?.value !== digest) fail('release-version-conflict', { source, version }, 3);
218
+ return { release: { package: 'devlyn-cli', version, digest, root }, source };
219
219
  }
220
220
 
221
221
  // The release a terminal launch plans with: an unfinished queue's bound release in this repository,
@@ -494,6 +494,7 @@
494
494
  "cc8bf51efb9ef8e5fa4134743f2d25434f4fd51595128edcfd2331dbf29d7dd5",
495
495
  "ccacf091bab4102d7156631c6227120a661bb4ec0df514e14a59db44b7bd5dfd",
496
496
  "ce1b0d9d0621fd057e684dc18b43156d30f589c72ed20189ea1232ea4f7ce6c6",
497
+ "ce8c1ea5c851fed75b898759fcf8359bd95ec04b30e547ed06bdf4101a2dddd8",
497
498
  "cebacda3b11db7aa161d96b12edd6f3f790ee2850404cd42ecdf2912ed53de08",
498
499
  "cefad7acb7dcd92777f888081aa6585b1418aa581366736fd44db68b1bd66dd0",
499
500
  "cf43eb0460beebdfe1efbfb1a35a39e5fe450137252ea2c83e51d978faa1f17e",
@@ -1024,6 +1025,7 @@
1024
1025
  "ce351bfa301b000f0786cac24959108a3db6cdf1cad3733cb07b394c62d35227",
1025
1026
  "ce3cceeb74119953afde95da84dbe1bd6e3da3dbde5f85dd123a0fe8afec5d36",
1026
1027
  "ce4b307083addc337a833547cb6998cb34a38d82e572587e9a57dc2cf46d275a",
1028
+ "ce8c1ea5c851fed75b898759fcf8359bd95ec04b30e547ed06bdf4101a2dddd8",
1027
1029
  "ce9c73a3305e4ec9cd559cec6fdd10b8f4a9f3124b42ea4598340dc16a575505",
1028
1030
  "ceddbf09792093c7417e72c7e2cf08e4780e9798dcd107292cdd095b023c2a8c",
1029
1031
  "cf04e7c73e16d8e0e561e89d4a6519a1c47b9ffa08f88d1e5bda75ad2983f4df",
@@ -4,6 +4,7 @@
4
4
  "_shared": [
5
5
  "015eea12cc07cfb9eb318c30855f6deacb5bf43bd508a511dffd2e48c9d82964",
6
6
  "0544f4e90951233c6cbe99755fb5c95728d7429b8b4ce5dd6e2829dd0816524c",
7
+ "0575d3635d8ec6f72120309a93af17e1801c636d3cf2b426d2feec064230c500",
7
8
  "05935881380d65ba34a964592e4587534fcea034b43b13673eff4108ed9389fd",
8
9
  "06ab3a4b9e1c97d9b3dcd238148d775f1165f3fc08898af2f3b76149162b78d3",
9
10
  "082124278a538e4ea8b6622d767f996e3d818ec85d6e0d8ddddafe9bb03118b2",
@@ -17,6 +18,7 @@
17
18
  "135f09d5382b4d375204bba3ff6fb3f6fdc34a1ec89fe88542da02d12dda0f0d",
18
19
  "1504c7a41e5f4a3206288d05e66fb48847222864f7825f05633883e2c562ec77",
19
20
  "161cb4f1b27a9f81ae1e96af6ff596399c8936578a11d601108a89cbdeea241d",
21
+ "16811f6cf716d3931562be0da4dae13b6aa5a9a126029a8ccddb7c797d0cb652",
20
22
  "196afa14871099a9afc4e484b00e8d8ccd928dba79db95a6a8776f58c8e78c39",
21
23
  "1c1e5e44ff704b1b8791fabcad79a4158d8cd2f024569c1685ec3b185a98fbcc",
22
24
  "20ac702b463f3eaa07fd6686d5209aed7993773504e35896fc732f7e653ec6cd",
@@ -72,6 +74,7 @@
72
74
  "59f0ecc2c1fd9620e4cac06be370e0bf9965a35c20616047445dbc42da6a1677",
73
75
  "5b8541a98c06de50443c2c3905c1b92ba143a972dfe618c4b296b799e0e1673c",
74
76
  "5be2e643f96b6ce503ae8d631c67729f0c24b6c5d2b431c087161b520ccbe417",
77
+ "5d3d614f8a75ae7cf1b5e29036e963ce76cf817fe9bd86457a4f8ff026b0036f",
75
78
  "60cdd1a8b220e629bcba3a24652323d16e3e6a15ba5b07c69ef2785a9c2eca70",
76
79
  "61ec4c0435a2f0b307df8e88d81d9862c3abc1bf758648f2c18039581596659b",
77
80
  "628a40e873d156e9179fd5a6bb3da9f910b6dde0886055804a506cdf8a631aaa",
@@ -121,6 +124,7 @@
121
124
  "97e7ad89c74c609b15dfbf6f9f7ac61df8b50e97e4bc318f72922e058ecffba4",
122
125
  "9815b6e6963e068872a6dbeae24068143bf92042bb035f67bafd1a224d65bcb0",
123
126
  "998343c148618b937556b58b83749c05d5aa3283280d751c118da6a542fb87f2",
127
+ "9ca5956a73070555a93f19ecb1f92a8f57cec6fd1170a2809eeacc49cc0841be",
124
128
  "9dd3ec225818efe0ad3726192db94ff129154e995617a9d3c150fc58ad0f9f2c",
125
129
  "9e4d06fd466fa896040109bac0018fcfb2f1604d55abb4180ff1f1a0b665bbc8",
126
130
  "9f14ca629e41730b1a9fb5d0ad1a39cff558fe34758d6ceccbb9de498f9f384e",
@@ -175,6 +179,7 @@
175
179
  "e0a305cf3bf47c8bc357195d4d50b0f6177069e870c33040c258c93edae68646",
176
180
  "e115205a744fef918a59d1b3a601df09a8b6df1d3e60613b9e8b87240929f8d8",
177
181
  "e19204055bb16fff4263f95fac092ee8f971ee9055b7a969497e5714782a454c",
182
+ "e2a8157e0467cbe0c7cff583e34629bbd69c906830a1530a0e69a4cf4c448f2e",
178
183
  "e41013abf5e17445ef83a86974ac1de77beaa61e484e1916577ecc64bb34c220",
179
184
  "e725ec4eca42f737ee4bdaceb10c11db289414a1d4bb8be8fe103f89c89f07d5",
180
185
  "e812d02ebdbdb8d82eb85842d676e21bedddb0d00154eed99b9d29b43cb8fc02",
@@ -193,6 +198,7 @@
193
198
  "f9617e1034543f31ae2a3820301fbac6e4674e5d6fbc0d4b76751fe87b8e013e",
194
199
  "fd68c7a74272929a14e685e447a51058ffcdd4c1d6bef83cbba18045c07aee55",
195
200
  "fdb608b34a8600b4ce209d7f645ec5ad4475948be6ad95c941cc7ef02b5959df",
201
+ "fe357defe8ac6efd6bbd15b3bf35f79d79761c0f56d507488d9428536d4fb5f7",
196
202
  "fe386122e502410394ab60643e79aa736ddb5030257c863f4a198e23b56b4cef",
197
203
  "feaa255b2b6a7b0dc79877c4eb620c0f3cac88b50d2d1407a0a8a02ebc08ed49"
198
204
  ],
@@ -321,6 +327,7 @@
321
327
  "89f0efaa2efdfddc887b494138247980d97856f6cabe6a8dcc585b3a28e5fa88",
322
328
  "9bc8113592de34a7d559d8a5db6253e472357e947010ea2304701f1f7de3643d",
323
329
  "ee723ff072721cae3f5e130d72515d44414337cffadb9615902b0d578912bda2",
330
+ "fc2710a0ed3eb0aeb7d095378bcbd1c77a5c46d0973bda7a49f6ec0a173adb2c",
324
331
  "fed622aa1bcf9a3b437d42c39f6b35403c8cde657be0685b93bbe864e82ca8c4"
325
332
  ],
326
333
  "devlyn-evaluate": [
@@ -348,6 +355,7 @@
348
355
  "15f3eca3fad92b199814a00d42e4005a2838a40d800b9f755f7165d31773e7c9",
349
356
  "19207315f23c9a12bf2ced0e4e79bb7dfdc7cc5d0c9dd94095d9c6dfe1e8fea8",
350
357
  "1d8c3bd31c8b755acd0c3c9f416acb5024dfa99fbf5e2c14d8d6df00a24fe9b4",
358
+ "2e5b05df1af5891761a6460328d3a1b95aa6ead1b373b5147de6388f7cb98df2",
351
359
  "300d1af60cc53285935dc4b5b370e157707218d844a4303b96d7826eef36ed43",
352
360
  "302b1a6c109087dce02ed6af184d6aeeca6cecfd40ebe638beecf8bdf21cf59c",
353
361
  "338a2b5ddde0f09444040c45922716663b1df76ec1a6c97ebdeb770b9e3ede49",
@@ -364,6 +372,7 @@
364
372
  "6374290c74a8c17979e8c128905e106d38bd810c0451f0e9240c30e0f7ecabb0",
365
373
  "65770e3f20c3a3afd2822e9acdcfc80b827f84a7737010b79332da70d431b05d",
366
374
  "65c781764633b912e5947362eba0837c8027461a904975401d79ad4fe8714084",
375
+ "65ec310be43384375852e31ba3458313b01052668543849186a70715bfa81dcc",
367
376
  "694c8920e50d92cba46dbb27eb44dd88035a89ed625d3cbf9f2bce9366d44c96",
368
377
  "6b3143bcfe9c60a76f142d533177d85d36f856a163a226351317ed688738c05a",
369
378
  "6b403afbf5a117cc08603d3648b9dded4f9ac4ca02fc1d8e1f0035615148bb5c",
@@ -390,6 +399,7 @@
390
399
  "c8e375aab574148542f392b093cbbf3f2e8dc623826b45e35df0a1a2c3862226",
391
400
  "ce282a55328c2d071cacc377670fbebea4da4b9a7fee81b25293fb26763f17c9",
392
401
  "cf61a917a804503ef1e26fbb1cf498181e843a0758ec001f84edb7bcac837448",
402
+ "d622e92c7bf32d315a5aabe2554efbc9f62790ace19a088ead9a7cef195fc715",
393
403
  "d6433bf0f96dd9dd485c3623885255738654674b3e23cfcc1a2cfd24f3cda96e",
394
404
  "db8d921aabf0d04811b55465a3757c62c4b39631aebaf6eecc994ca241cfbe56",
395
405
  "e25da746dc37b2e82c658a28d9481fd69741151045683eee2119582ddaf731cd",
@@ -812,7 +822,9 @@
812
822
  "4.3.26",
813
823
  "4.3.27",
814
824
  "4.3.28",
825
+ "4.3.29",
815
826
  "4.3.3",
827
+ "4.3.30",
816
828
  "4.3.4",
817
829
  "4.3.5",
818
830
  "4.3.6",
@@ -29,7 +29,7 @@ check_binary() {
29
29
 
30
30
  check_adapter() {
31
31
  # $1 = target name
32
- if [ -f "$ADAPTERS_DIR/$1.md" ]; then printf 'yes'; else printf 'no'; fi
32
+ if [ -f "$ADAPTERS_DIR/$1.adapter.md" ]; then printf 'yes'; else printf 'no'; fi
33
33
  }
34
34
 
35
35
  check_executor() {
@@ -47,7 +47,7 @@ for i in "${!TARGETS[@]}"; do
47
47
  kind="${KINDS[$i]}"
48
48
  binary="$(check_binary "${BINARIES[$i]}")"
49
49
  adapter="$(check_adapter "$target")"
50
- executor="$(check_executor "$ADAPTERS_DIR/$target.md")"
50
+ executor="$(check_executor "$ADAPTERS_DIR/$target.adapter.md")"
51
51
 
52
52
  note='-'
53
53
  case "$kind" in