devlyn-cli 4.3.27 → 4.3.29

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 (72) 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 +5 -3
  6. package/bin/harness-activation.js +212 -19
  7. package/bin/instruction-templates.json +2 -0
  8. package/bin/skill-history.json +11 -0
  9. package/config/skills/_shared/engine-doctor.sh +2 -2
  10. package/config/skills/_shared/engine-preflight.md +2 -2
  11. package/config/skills/_shared/managed-storage.md +80 -0
  12. package/config/skills/_shared/platform-support.py +67 -28
  13. package/config/skills/_shared/role-config.py +16 -7
  14. package/config/skills/_shared/runtime-principles.md +1 -1
  15. package/config/skills/_shared/storage-client.py +31 -7
  16. package/config/skills/_shared/task-complete.py +1 -1
  17. package/config/skills/_shared/task-completion.md +2 -76
  18. package/config/skills/devlyn-engines/SKILL.md +1 -1
  19. package/config/skills/devlyn-ideate/SKILL.md +2 -2
  20. package/config/skills/devlyn-ideate/references/executor.md +62 -0
  21. package/config/skills/devlyn-ideate/references/loop.md +2 -59
  22. package/config/skills/devlyn-ideate/references/package-format.md +1 -1
  23. package/config/skills/devlyn-ideate/scripts/acceptance.py +442 -21
  24. package/config/skills/devlyn-ideate/scripts/queue.py +58 -10
  25. package/harness/AGENTS.md +2 -2
  26. package/harness/CLAUDE.md +2 -2
  27. package/native/claude/.claude-plugin/plugin.json +1 -1
  28. package/native/claude/skills/_shared/engine-doctor.sh +2 -2
  29. package/native/claude/skills/_shared/engine-preflight.md +2 -2
  30. package/native/claude/skills/_shared/managed-storage.md +80 -0
  31. package/native/claude/skills/_shared/platform-support.py +67 -28
  32. package/native/claude/skills/_shared/role-config.py +16 -7
  33. package/native/claude/skills/_shared/runtime-principles.md +1 -1
  34. package/native/claude/skills/_shared/storage-client.py +31 -7
  35. package/native/claude/skills/_shared/task-complete.py +1 -1
  36. package/native/claude/skills/_shared/task-completion.md +2 -76
  37. package/native/claude/skills/devlyn-engines/SKILL.md +1 -1
  38. package/native/claude/skills/devlyn-ideate/SKILL.md +2 -2
  39. package/native/claude/skills/devlyn-ideate/references/executor.md +62 -0
  40. package/native/claude/skills/devlyn-ideate/references/loop.md +2 -59
  41. package/native/claude/skills/devlyn-ideate/references/package-format.md +1 -1
  42. package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +442 -21
  43. package/native/claude/skills/devlyn-ideate/scripts/queue.py +58 -10
  44. package/native/codex/.agents/plugins/marketplace.json +1 -1
  45. package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
  46. package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +2 -2
  47. package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +2 -2
  48. package/native/codex/plugins/devlyn/skills/_shared/managed-storage.md +80 -0
  49. package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +67 -28
  50. package/native/codex/plugins/devlyn/skills/_shared/role-config.py +16 -7
  51. package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +1 -1
  52. package/native/codex/plugins/devlyn/skills/_shared/storage-client.py +31 -7
  53. package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +1 -1
  54. package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +2 -76
  55. package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +1 -1
  56. package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +2 -2
  57. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/executor.md +62 -0
  58. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +2 -59
  59. package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +1 -1
  60. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +442 -21
  61. package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +58 -10
  62. package/package.json +1 -11
  63. package/release.json +98 -68
  64. /package/config/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
  65. /package/config/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
  66. /package/config/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
  67. /package/native/claude/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
  68. /package/native/claude/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
  69. /package/native/claude/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
  70. /package/native/codex/plugins/devlyn/skills/_shared/adapters/{claude.md → claude.adapter.md} +0 -0
  71. /package/native/codex/plugins/devlyn/skills/_shared/adapters/{codex.md → codex.adapter.md} +0 -0
  72. /package/native/codex/plugins/devlyn/skills/_shared/adapters/{omp.md → omp.adapter.md} +0 -0
@@ -11,6 +11,7 @@ const SKILLS = ['devlyn-ideate', 'devlyn-engines'];
11
11
  const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
12
12
  const textValue = (value) => typeof value === 'string' && !value.includes('\0');
13
13
  const strings = (value) => Array.isArray(value) && value.every(textValue);
14
+ const isFile = (file) => fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
14
15
  const reason = (code, params = {}) => ({ code, params });
15
16
  const channel = (state = 'disabled', mechanism = null, reasons = []) => ({ state, mechanism, reasons });
16
17
  const joinText = (...parts) => parts.filter((part) => part !== null && part !== undefined && part !== '').join('\n\n');
@@ -90,6 +91,7 @@ function effectiveConfig(context) {
90
91
  claude: ['appendSystemPrompt', 'pluginDirs'], codex: ['developer_instructions', 'skills', 'marketplaces', 'plugins'],
91
92
  grok: ['rules'], omp: ['appendSystemPrompt', 'skills'], antigravity: [], cursor: [],
92
93
  opencode: ['inlineConfig', 'skillsPaths'], kimi: ['ownerAgent'], copilot: ['skillsDirs', 'instructionsDirs'], qwen: ['appendSystemPrompt'],
94
+ pi: ['appendSystemPrompt'],
93
95
  }[context.engine];
94
96
  if (!object(config) || Object.keys(config).some((key) => !keys.includes(key))) return null;
95
97
  for (const field of ['appendSystemPrompt', 'developer_instructions', 'rules', 'inlineConfig', 'skillsDirs', 'instructionsDirs']) {
@@ -120,8 +122,20 @@ function effectiveConfig(context) {
120
122
  return config;
121
123
  }
122
124
 
125
+ // pi's agent dir is PI_CODING_AGENT_DIR, else ~/.pi/agent. That ~, and a leading ~ pi expands in the
126
+ // variable, is os.homedir(): USERPROFILE on Windows, HOME elsewhere (config.js getAgentDir). The home of
127
+ // its user .agents/skills takes HOME first on every platform (package-manager.js getHomeDir).
128
+ function piFolders(cwd, env, platform = process.platform) {
129
+ const paths = platform === 'win32' ? path.win32 : path.posix;
130
+ const home = (platform === 'win32' ? env.USERPROFILE : env.HOME) || os.homedir();
131
+ const dir = env.PI_CODING_AGENT_DIR;
132
+ const agentDir = dir ? dir.replace(platform === 'win32' ? /^~(?=$|[\\/])/ : /^~(?=$|\/)/, () => home) : paths.join(home, '.pi', 'agent');
133
+ return { agentDir: paths.resolve(cwd, agentDir), agentsHome: env.HOME || home };
134
+ }
135
+
123
136
  function discoveryRoots(context, env, config) {
124
137
  const home = env.HOME || env.USERPROFILE || os.homedir();
138
+ const pi = piFolders(context.cwd, env);
125
139
  const roots = [...(context.skillRoots || [])];
126
140
  const add = (base, ...parts) => { if (base) roots.push(path.resolve(base, ...parts)); };
127
141
  const engine = context.engine;
@@ -135,10 +149,11 @@ function discoveryRoots(context, env, config) {
135
149
  kimi: [env.KIMI_CODE_HOME || path.join(home, '.kimi-code'), '.kimi-code'],
136
150
  copilot: [env.COPILOT_HOME || path.join(home, '.copilot'), '.github'],
137
151
  qwen: [env.QWEN_HOME || path.join(home, '.qwen'), '.qwen'],
152
+ pi: [pi.agentDir, null],
138
153
  }[engine];
139
154
  if (engine !== 'codex') add(native[0], 'skills');
140
155
  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');
156
+ if (!['claude', 'cursor'].includes(engine) && (engine !== 'copilot' || copilotDefaultHome)) add(engine === 'pi' ? pi.agentsHome : home, '.agents/skills');
142
157
  if (['omp', 'opencode', 'grok'].includes(engine)) add(home, '.claude/skills');
143
158
  if (engine === 'omp') {
144
159
  add(home, '.codex/skills'); add(home, '.agent/skills');
@@ -150,6 +165,7 @@ function discoveryRoots(context, env, config) {
150
165
  for (const dir of config?.skillsPaths || []) add(context.cwd, dir);
151
166
  }
152
167
  if (engine === 'copilot') for (const dir of (config?.skillsDirs ?? env.COPILOT_SKILLS_DIRS ?? '').split(',').filter(Boolean)) add(context.cwd, dir);
168
+ if (engine === 'pi') add(context.cwd, '.pi/skills');
153
169
  for (let dir = path.resolve(context.cwd); ; dir = path.dirname(dir)) {
154
170
  if (native[1]) add(dir, native[1], 'skills');
155
171
  if (!['claude', 'cursor'].includes(engine)) add(dir, '.agents/skills');
@@ -187,6 +203,153 @@ function kimiSkillRoots(context, env) {
187
203
  return { roots: [...roots], errors };
188
204
  }
189
205
 
206
+ // omp and pi read their own APPEND_SYSTEM.md only when no append text is passed, and the plan passes
207
+ // some, so the owner's text leads it. Text the caller supplies wins, an explicit empty string included.
208
+ // Only when it supplies none (the key absent, or null for unset) is the first existing file `lookup()`
209
+ // names read, as `devlyn run` reads omp's (harness-context.js ompOwnerAppend). Returns {text}, or
210
+ // {reason} when the owner's text cannot be known.
211
+ function ownerAppend(config, lookup) {
212
+ if (config.appendSystemPrompt != null) return { text: config.appendSystemPrompt };
213
+ try {
214
+ const found = lookup();
215
+ if (found.reason) return found;
216
+ const file = found.files.find(isFile);
217
+ return { text: file === undefined ? null : fs.readFileSync(file, 'utf8') };
218
+ } catch (error) {
219
+ return { reason: reason('config-unreadable', { path: error.path, detail: error.code || error.message }) };
220
+ }
221
+ }
222
+
223
+ // pi's user and project settings.json (settings-manager.js). Entries they name resolve through pi's own
224
+ // package manager, which the planner does not repeat. A file that cannot be read is null and reported.
225
+ function piSettings(cwd, agentDir) {
226
+ const settings = { errors: [] };
227
+ for (const [scope, file] of [['user', path.join(agentDir, 'settings.json')], ['project', path.join(cwd, '.pi/settings.json')]]) {
228
+ try {
229
+ settings[scope] = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '') || '{}') : {};
230
+ } catch (error) {
231
+ settings[scope] = null;
232
+ settings.errors.push(reason('config-unreadable', { path: file, detail: error.code || error.message }));
233
+ }
234
+ }
235
+ return settings;
236
+ }
237
+ const listed = (settings, ...keys) => keys.some((key) => settings?.[key] != null && settings[key].length !== 0);
238
+
239
+ // Whether the agent dir's extensions folder holds anything pi could load; pi skips dot entries and
240
+ // node_modules (package-manager.js collectAutoExtensionEntries).
241
+ function piExtensionEntries(agentDir) {
242
+ try {
243
+ return fs.readdirSync(path.join(agentDir, 'extensions')).some((name) => !name.startsWith('.') && name !== 'node_modules');
244
+ } catch (error) {
245
+ if (error.code === 'ENOENT') return false;
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ // pi reads a project's .pi/APPEND_SYSTEM.md only when it trusts the project, else its agent dir's
251
+ // (resource-loader discoverAppendSystemPromptFile). pi first asks the extensions it loads before trust,
252
+ // and the first yes or no wins (project-trust.js resolveProjectTrusted). With project settings held
253
+ // untrusted, those come only from the agent dir's extensions folder and the extensions or packages its
254
+ // user settings name (resource-loader loadProjectTrustExtensions, package-manager resolve): the app
255
+ // passes no -e, and pi's built-in extensions answer no trust question. With none, trust is pi's saved
256
+ // decision for the folder or its nearest decided parent (trust.json). Otherwise pi settles trust at
257
+ // launch, so which file it reads cannot be known here. The planner never writes trust.
258
+ function piAppendFiles(cwd, agentDir, userSettings) {
259
+ const project = path.join(cwd, '.pi/APPEND_SYSTEM.md');
260
+ const user = path.join(agentDir, 'APPEND_SYSTEM.md');
261
+ if (!isFile(project)) return { files: [user] };
262
+ const undecided = { reason: reason('principles-unavailable', { detail: 'project-trust-undecided', path: project }) };
263
+ if (userSettings === null || listed(userSettings, 'extensions', 'packages') || piExtensionEntries(agentDir)) return undecided;
264
+ const store = path.join(agentDir, 'trust.json');
265
+ let trusted = null;
266
+ try {
267
+ const saved = fs.existsSync(store) ? JSON.parse(fs.readFileSync(store, 'utf8').replace(/^\uFEFF/, '')) : {};
268
+ if (!object(saved) || Object.values(saved).some((value) => ![true, false, null].includes(value))) throw new Error('not a trust record');
269
+ for (let dir = fs.realpathSync(cwd); trusted === null; dir = path.dirname(dir)) {
270
+ if (typeof saved[dir] === 'boolean') trusted = saved[dir];
271
+ else if (dir === path.dirname(dir)) break;
272
+ }
273
+ } catch (error) {
274
+ return { reason: reason('config-unreadable', { path: store, detail: error.code || error.message }) };
275
+ }
276
+ if (trusted === null) return undecided;
277
+ return { files: [trusted ? project : user] };
278
+ }
279
+
280
+ // The files pi may load as skills below a root (package-manager.js collectSkillEntries). pi stops at a
281
+ // folder's SKILL.md unless an ignore file hides it, and takes other .md files at the root of its own
282
+ // folders and below it in .agents/skills. Ignore files are not read here, so every .md file is taken and
283
+ // every folder searched: at least what pi finds is found. Dot entries and node_modules are skipped as pi
284
+ // skips them.
285
+ function piSkillFiles(root) {
286
+ const files = [], seen = new Set();
287
+ const visit = (dir) => {
288
+ let entries;
289
+ try {
290
+ entries = fs.readdirSync(dir, { withFileTypes: true });
291
+ } catch (error) {
292
+ if (dir === root && error.code === 'ENOENT') return;
293
+ throw error;
294
+ }
295
+ seen.add(fs.realpathSync(dir));
296
+ for (const entry of entries) {
297
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
298
+ const full = path.join(dir, entry.name);
299
+ let kind = entry;
300
+ // pi follows links and skips one it cannot follow.
301
+ if (entry.isSymbolicLink()) { try { kind = fs.statSync(full); } catch { continue; } }
302
+ if (kind.isDirectory()) { if (!seen.has(fs.realpathSync(full))) visit(full); }
303
+ else if (kind.isFile() && entry.name.endsWith('.md')) files.push(full);
304
+ }
305
+ };
306
+ visit(root);
307
+ return files;
308
+ }
309
+
310
+ // The names pi may give a skill file (skills.js loadSkillFromFile): its frontmatter's string `name`, else,
311
+ // also for an empty one, its folder's name. A plain scalar may be no string (null, a number), so the
312
+ // folder counts then too.
313
+ // null when the name is not one simple line, which counts as a collision.
314
+ function piSkillNames(file) {
315
+ const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
316
+ const folder = path.basename(path.dirname(file));
317
+ const end = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
318
+ const lines = end < 0 ? [] : text.slice(4, end).split('\n');
319
+ if (/^\s/.test(lines.find((line) => line.trim() && !line.trim().startsWith('#')) ?? '')) return null;
320
+ // Lines that are or may be a top-level name key: quoted or complex keys, merges, anchors, flow mappings.
321
+ const keys = lines.flatMap((line, index) => (/^(?:name[ \t]*:|["'?{[&*!]|<<)/.test(line) ? [index] : []));
322
+ if (!keys.length) return [folder];
323
+ const match = keys.length === 1 && /^name:(.*)$/.exec(lines[keys[0]]);
324
+ if (!match) return null;
325
+ const value = match[1].replace(/\s#.*$/, '').trim();
326
+ const quoted = /^"([^"\\]*)"$|^'([^']*)'$/.exec(value);
327
+ if (quoted) return [(quoted[1] ?? quoted[2]) || folder];
328
+ const next = lines.slice(keys[0] + 1).find((line) => line.trim()) ?? '';
329
+ if (!value || /^[-?:,[\]{}#&*!|>'"%@`]/.test(value) || /^\s/.test(next)) return null;
330
+ return [value, folder];
331
+ }
332
+
333
+ // pi names a skill by its frontmatter wherever it sits below a root, and a found name wins over --skill.
334
+ // Files `found` already reports are skipped.
335
+ function piNameCollisions(roots, found) {
336
+ const collisions = [], errors = [], seen = new Set(found.map((entry) => entry.path));
337
+ for (const root of roots) {
338
+ try {
339
+ for (const file of piSkillFiles(root)) {
340
+ if (seen.has(file)) continue;
341
+ seen.add(file);
342
+ const names = piSkillNames(file);
343
+ if (names === null) collisions.push(reason('name-collision', { path: file }));
344
+ else for (const name of new Set(names)) if (SKILLS.includes(name)) collisions.push(reason('name-collision', { name, path: file }));
345
+ }
346
+ } catch (error) {
347
+ errors.push(reason('config-unreadable', { path: error.path || root, detail: error.code || error.message }));
348
+ }
349
+ }
350
+ return { collisions, errors };
351
+ }
352
+
190
353
  function inspectSkills(context, release, env, config, active) {
191
354
  const found = [], errors = [], hashes = new Map();
192
355
  for (const root of discoveryRoots(context, env, config)) {
@@ -249,14 +412,10 @@ function planActivation(releaseRoot, input, env = process.env) {
249
412
  const plan = { schemaVersion: 1, release: null, engineArgs: [], envPatch: {}, prompt: null,
250
413
  files: [], machineSync: null, channels: status };
251
414
  const continuation = context.launchMode !== 'fresh';
252
- if (continuation && context.recordedRelease === undefined) {
253
- status.principles = channel('partial', null, [reason('principles-unavailable', { launchMode: context.launchMode, detail: 'recorded-binding-missing' })]);
254
- status.skills = channel('partial', null, [reason('skills-unavailable', { launchMode: context.launchMode, detail: 'recorded-binding-missing' })]);
255
- return finish(plan);
256
- }
257
- const off = continuation ? context.recordedRelease === null : context.projectChoice === 'none';
258
- const release = verifyRelease(continuation && !off ? context.recordedRelease.root : releaseRoot).release;
259
- if (continuation && !off && (release.digest !== context.recordedRelease.digest || release.version !== context.recordedRelease.version)) {
415
+ const recorded = continuation && context.recordedRelease !== undefined;
416
+ const off = recorded ? context.recordedRelease === null : context.projectChoice === 'none';
417
+ const release = verifyRelease(recorded && !off ? context.recordedRelease.root : releaseRoot).release;
418
+ if (recorded && !off && (release.digest !== context.recordedRelease.digest || release.version !== context.recordedRelease.version)) {
260
419
  fail('release-version-conflict', { version: context.recordedRelease.version }, 3);
261
420
  }
262
421
  const config = effectiveConfig(context);
@@ -331,8 +490,10 @@ function planActivation(releaseRoot, input, env = process.env) {
331
490
  return path.join(context.runDir, relativePath);
332
491
  };
333
492
  const principles = (mechanism) => { status.principles = channel('configured', mechanism); };
334
- const fallback = (reasons = []) => {
335
- status.skills = channel('partial', 'absolute-catalog', [...reasons, reason('skills-catalog')]);
493
+ // The catalog travels in the principles text: without that text only the absolute invocation is left.
494
+ const fallback = (reasons = [], withCatalog = true) => {
495
+ status.skills = withCatalog ? channel('partial', 'absolute-catalog', [...reasons, reason('skills-catalog')])
496
+ : channel('partial', null, reasons);
336
497
  status.invocation = { kind: 'read-file', text: `Read ${path.join(release.root, 'config/skills/devlyn-ideate/SKILL.md')}` };
337
498
  };
338
499
  const native = (mechanism, kind, text) => { status.skills = channel('configured', mechanism); status.invocation = { kind, text }; };
@@ -385,11 +546,39 @@ function planActivation(releaseRoot, input, env = process.env) {
385
546
  case 'omp': {
386
547
  const skills = { ...config.skills,
387
548
  customDirectories: [...new Set([...(config.skills?.customDirectories || []), path.join(release.root, 'config/skills')])] };
388
- plan.engineArgs.push('--config', file('devlyn-omp.json', JSON.stringify({ skills }, null, 2) + '\n'),
389
- '--append-system-prompt', joinText(config.appendSystemPrompt, instructions));
390
- principles('append-system-prompt');
391
- if (collisions.length || errors.length || skills.enableSkillCommands === false || skills.ignoredSkills?.length) fallback([...collisions, ...errors]);
392
- else native('skills.customDirectories', 'slash', '/skill:devlyn-ideate');
549
+ // The files in the order `devlyn run` reads them (harness-context.js ompOwnerAppend).
550
+ const owner = ownerAppend(config, () => ({ files: [path.join(context.cwd, '.omp/APPEND_SYSTEM.md'),
551
+ path.join(env.HOME || env.USERPROFILE || os.homedir(), '.omp/agent/APPEND_SYSTEM.md')] }));
552
+ plan.engineArgs.push('--config', file('devlyn-omp.json', JSON.stringify({ skills }, null, 2) + '\n'));
553
+ // Owner text that cannot be known leaves the flag off, so the engine's own lookup stays intact.
554
+ if (owner.reason) status.principles = channel('partial', null, [owner.reason]);
555
+ else {
556
+ plan.engineArgs.push('--append-system-prompt', joinText(owner.text, instructions));
557
+ principles('append-system-prompt');
558
+ }
559
+ if (collisions.length || errors.length || skills.enableSkillCommands === false || skills.ignoredSkills?.length) {
560
+ fallback([...collisions, ...errors], !owner.reason);
561
+ } else native('skills.customDirectories', 'slash', '/skill:devlyn-ideate');
562
+ break;
563
+ }
564
+ case 'pi': {
565
+ const { agentDir } = piFolders(context.cwd, env);
566
+ const settings = piSettings(context.cwd, agentDir);
567
+ const owner = ownerAppend(config, () => piAppendFiles(context.cwd, agentDir, settings.user));
568
+ if (owner.reason) status.principles = channel('partial', null, [owner.reason]);
569
+ else {
570
+ plan.engineArgs.push('--append-system-prompt', file('devlyn-instructions.md', joinText(owner.text, instructions)));
571
+ principles('append-system-prompt');
572
+ }
573
+ // A discovered or settings-named skill wins over a --skill of the same name.
574
+ const named = piNameCollisions(discoveryRoots(context, env, config), found);
575
+ const reasons = [...collisions, ...named.collisions, ...errors, ...named.errors, ...settings.errors];
576
+ if (reasons.length || [settings.user, settings.project].some((scope) => listed(scope, 'skills', 'packages'))) {
577
+ fallback(reasons, !owner.reason);
578
+ } else {
579
+ for (const name of SKILLS) plan.engineArgs.push('--skill', path.join(release.root, 'config/skills', name));
580
+ native('--skill', 'slash', '/skill:devlyn-ideate');
581
+ }
393
582
  break;
394
583
  }
395
584
  case 'grok':
@@ -458,11 +647,15 @@ function planActivation(releaseRoot, input, env = process.env) {
458
647
  break;
459
648
  case 'antigravity':
460
649
  case 'cursor':
461
- status.principles = channel('partial', context.initialPrompt ? 'initial-task' : null, [reason('principles-task-only')]);
650
+ // Only a first task carries the principles and the catalog: without one nothing reaches the session.
462
651
  if (context.initialPrompt) {
652
+ status.principles = channel('partial', 'initial-task', [reason('principles-task-only')]);
463
653
  plan.prompt = { ...context.initialPrompt, text: joinText(instructions, context.initialPrompt.text) };
464
654
  fallback();
465
- } else status.skills = channel('partial', null, [reason('skills-unavailable', { detail: 'no-initial-task' })]);
655
+ } else {
656
+ status.principles = channel('unavailable', null, [reason('principles-unavailable', { detail: 'no-initial-task' })]);
657
+ status.skills = channel('unavailable', null, [reason('skills-unavailable', { detail: 'no-initial-task' })]);
658
+ }
466
659
  break;
467
660
  }
468
661
  if (plan.engineArgs.length || Object.keys(plan.envPatch).length || plan.prompt) plan.envPatch.DEVLYN_HARNESS_ROOT = release.root;
@@ -477,4 +670,4 @@ function activationCommand(args) {
477
670
  return planActivation(opts['release-root'], readContext(opts['context-file']));
478
671
  }
479
672
 
480
- module.exports = { planActivation, activationCommand, readContext, toml, skillCatalog };
673
+ module.exports = { planActivation, activationCommand, readContext, toml, skillCatalog, piFolders };
@@ -509,6 +509,7 @@
509
509
  "d5d8d02a1fb53cd28ed1c33e7184067b76defbfcc6cab9498b365dee35579013",
510
510
  "d75bdfc9cf08cfcb46214f69dac4f20b1a0a30b86d91127007fe587de9eaaeb0",
511
511
  "d768a149b5842e0d50eda941872188c8b29bcfb4b1aa0f4190fd4299f237fce0",
512
+ "d83ac69973a5d278e734d7cb55965c1fd9402700a5f4fd41bd9d4a4b4819bac2",
512
513
  "d86e60d02c872b5552afb7a99499d3f3433c4ee3117a95bdbfb48d78b7cb72df",
513
514
  "d8c50cb04441e6b568236bc6cd494b4de296dfc7e8b33cf96e7aeb94612df3b8",
514
515
  "d940c2df4cfffa42265e4f85e429762b99dd6d055c1658d782c7b472c6ebacd5",
@@ -1046,6 +1047,7 @@
1046
1047
  "d606733e95736b3e28e4dce4847b9775effc0944c4cd54c733400074991934ef",
1047
1048
  "d660cb0c81a7032782da6910fa05eafa83d36520ca5fdb95e7077da96216a3ca",
1048
1049
  "d6df42e35e82f71f257f5e79d038bf60a96e4965dbdb6bf08bc95c250754e5fe",
1050
+ "d83ac69973a5d278e734d7cb55965c1fd9402700a5f4fd41bd9d4a4b4819bac2",
1049
1051
  "d905ca576280d8a3d774bcadebfa57a9e22bdf7c1678ca9a548a1c25d643bf17",
1050
1052
  "d94088aedb09a1feb3ad388a69e242043b4a921ca8d76b4660d94e18e2836546",
1051
1053
  "d9547a0d7d9a97bffa84178d8ab73f3556f22c34120759d50eaa165c3ef0c318",
@@ -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",
@@ -167,6 +170,7 @@
167
170
  "d7388e36d76647a89ce52de615271928cf9fc78f980df399d2bc2e6d6675ec82",
168
171
  "d85fa24b9b2c439742c9a62d5d4e07a031a1cfe6024bbc06f98eff7063424179",
169
172
  "dac3da11ea5686382f5c9892a0b8e4ec25e625e5946283d8e2ad944aa84e1df2",
173
+ "db2a57a81fc9ffddb7f7f2b7a59ac4821563c345e2a47f825eade308f2707040",
170
174
  "db867d1fd98058bb20e41e942abd95f08fbaa9465928025f69f0a0308fa71514",
171
175
  "dd0bfdfd2db0b5f9ddef75d1b9ce3247698739fc551d292366dccd506fef6a36",
172
176
  "ddb0c8dac58e95298330ab54e4d1fe4a108c968d9af713f267b4f8f4c6808507",
@@ -174,6 +178,7 @@
174
178
  "e0a305cf3bf47c8bc357195d4d50b0f6177069e870c33040c258c93edae68646",
175
179
  "e115205a744fef918a59d1b3a601df09a8b6df1d3e60613b9e8b87240929f8d8",
176
180
  "e19204055bb16fff4263f95fac092ee8f971ee9055b7a969497e5714782a454c",
181
+ "e2a8157e0467cbe0c7cff583e34629bbd69c906830a1530a0e69a4cf4c448f2e",
177
182
  "e41013abf5e17445ef83a86974ac1de77beaa61e484e1916577ecc64bb34c220",
178
183
  "e725ec4eca42f737ee4bdaceb10c11db289414a1d4bb8be8fe103f89c89f07d5",
179
184
  "e812d02ebdbdb8d82eb85842d676e21bedddb0d00154eed99b9d29b43cb8fc02",
@@ -192,6 +197,7 @@
192
197
  "f9617e1034543f31ae2a3820301fbac6e4674e5d6fbc0d4b76751fe87b8e013e",
193
198
  "fd68c7a74272929a14e685e447a51058ffcdd4c1d6bef83cbba18045c07aee55",
194
199
  "fdb608b34a8600b4ce209d7f645ec5ad4475948be6ad95c941cc7ef02b5959df",
200
+ "fe357defe8ac6efd6bbd15b3bf35f79d79761c0f56d507488d9428536d4fb5f7",
195
201
  "fe386122e502410394ab60643e79aa736ddb5030257c863f4a198e23b56b4cef",
196
202
  "feaa255b2b6a7b0dc79877c4eb620c0f3cac88b50d2d1407a0a8a02ebc08ed49"
197
203
  ],
@@ -320,6 +326,7 @@
320
326
  "89f0efaa2efdfddc887b494138247980d97856f6cabe6a8dcc585b3a28e5fa88",
321
327
  "9bc8113592de34a7d559d8a5db6253e472357e947010ea2304701f1f7de3643d",
322
328
  "ee723ff072721cae3f5e130d72515d44414337cffadb9615902b0d578912bda2",
329
+ "fc2710a0ed3eb0aeb7d095378bcbd1c77a5c46d0973bda7a49f6ec0a173adb2c",
323
330
  "fed622aa1bcf9a3b437d42c39f6b35403c8cde657be0685b93bbe864e82ca8c4"
324
331
  ],
325
332
  "devlyn-evaluate": [
@@ -347,6 +354,7 @@
347
354
  "15f3eca3fad92b199814a00d42e4005a2838a40d800b9f755f7165d31773e7c9",
348
355
  "19207315f23c9a12bf2ced0e4e79bb7dfdc7cc5d0c9dd94095d9c6dfe1e8fea8",
349
356
  "1d8c3bd31c8b755acd0c3c9f416acb5024dfa99fbf5e2c14d8d6df00a24fe9b4",
357
+ "2e5b05df1af5891761a6460328d3a1b95aa6ead1b373b5147de6388f7cb98df2",
350
358
  "300d1af60cc53285935dc4b5b370e157707218d844a4303b96d7826eef36ed43",
351
359
  "302b1a6c109087dce02ed6af184d6aeeca6cecfd40ebe638beecf8bdf21cf59c",
352
360
  "338a2b5ddde0f09444040c45922716663b1df76ec1a6c97ebdeb770b9e3ede49",
@@ -363,6 +371,7 @@
363
371
  "6374290c74a8c17979e8c128905e106d38bd810c0451f0e9240c30e0f7ecabb0",
364
372
  "65770e3f20c3a3afd2822e9acdcfc80b827f84a7737010b79332da70d431b05d",
365
373
  "65c781764633b912e5947362eba0837c8027461a904975401d79ad4fe8714084",
374
+ "65ec310be43384375852e31ba3458313b01052668543849186a70715bfa81dcc",
366
375
  "694c8920e50d92cba46dbb27eb44dd88035a89ed625d3cbf9f2bce9366d44c96",
367
376
  "6b3143bcfe9c60a76f142d533177d85d36f856a163a226351317ed688738c05a",
368
377
  "6b403afbf5a117cc08603d3648b9dded4f9ac4ca02fc1d8e1f0035615148bb5c",
@@ -810,6 +819,8 @@
810
819
  "4.3.25",
811
820
  "4.3.26",
812
821
  "4.3.27",
822
+ "4.3.28",
823
+ "4.3.29",
813
824
  "4.3.3",
814
825
  "4.3.4",
815
826
  "4.3.5",
@@ -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
@@ -4,7 +4,7 @@ Used by `/devlyn-ideate` and `/devlyn-engines`: one rule for which engine execut
4
4
 
5
5
  ## Selection
6
6
 
7
- The executor is the `executor` pin in `.devlyn/engines.json` at the checkout root (set with `/devlyn-engines executor <name>`), else the CLI the user opened. A linked worktree or subdirectory has no pin unless one is set there. Pi and Grok have no executor adapter, so a drain from them needs a claude, codex or omp pin. Keys earlier releases wrote for retired pipeline roles (`pair_judge_priority`, `roles`) stay as written and select nothing: a worker profile never becomes the executor.
7
+ The executor is the `executor` pin in `.devlyn/engines.json` at the checkout root (set with `/devlyn-engines executor <name>`), else the CLI the user opened. A linked worktree or subdirectory has no pin unless one is set there. Only Claude and Codex can execute, so a drain from any other CLI needs a claude or codex pin. Keys earlier releases wrote for retired pipeline roles (`pair_judge_priority`, `roles`) stay as written and select nothing: a worker profile never becomes the executor.
8
8
 
9
9
  Before dispatching to the executor, run:
10
10
 
@@ -16,7 +16,7 @@ A pin is a promise. When the pinned engine's CLI is unavailable, the command exi
16
16
 
17
17
  ## Engines
18
18
 
19
- An engine is pinnable when `_shared/adapters/<name>.md` exists and does not declare `executor: no` under `## Role eligibility`. Adapters ship with devlyn-cli releases, and a reinstall replaces `_shared`. An adapter's `## Invocation` says how to start it. On native Windows, use native Node/npm and Python (`python3` on PATH), with Git for Windows Bash for the shipped shell wrapper.
19
+ An engine is pinnable when `_shared/adapters/<name>.adapter.md` exists and does not declare `executor: no`. Adapters ship with devlyn-cli releases, and a reinstall replaces `_shared`. An adapter's `## Invocation` says how to start it. On native Windows, use native Node/npm and Python (`python3` on PATH), with Git for Windows Bash for the shipped shell wrapper.
20
20
 
21
21
  ## Reporting a blocked engine
22
22
 
@@ -0,0 +1,80 @@
1
+ # Managed storage
2
+
3
+ New allocation and warming modes come from enrollment state alone:
4
+
5
+ | `~/.devlyn/storage-client.json` | recovery marker | unenroll journal | Mode |
6
+ |---|---|---|---|
7
+ | absent | absent | not read | standalone (never enrolled) |
8
+ | valid | any | not read | managed; a fenced authority refuses |
9
+ | invalid or unreadable | any | not read | refuse; repair registration |
10
+ | absent | present | verified `retired: true` | standalone (unenrollment complete) |
11
+ | absent | present | absent, unreadable or incomplete | refuse; Open Devlyn to repair its storage registration |
12
+
13
+ An unfinished retirement takes precedence over enrollment; follow its
14
+ operation-specific retained-helper resume to reconcile or cancel as named.
15
+ The journal `storage-client-unenroll.json` is read no-follow for retirement
16
+ recovery and the last two rows. Schema 1, canonical UUIDv7 operation, permanent `storage-client-fence.json`,
17
+ matching operation/root/lock identities and Rust's archive ordering/path
18
+ validation are required. If native root/lock identities or the home path changed,
19
+ completed retirement permits standalone only when `storage`, `control`,
20
+ `harness/state.json` and `storage-client.json` are all absent under the current
21
+ lifecycle root (checked without following links). Live inventory requires repair.
22
+ No repository-wide receipt scan selects mode. Other keys can allocate after
23
+ completed unenrollment; a leftover managed key retains its own recovery command.
24
+
25
+ Existing operations keep their managed identity. Receipts pin the control
26
+ identity; current enrollment verifies the helper before allocation. Recovery uses
27
+ the current verified `storage-client-recovery/pins.json`, including after uninstall
28
+ or unenrollment, and requires its authority binding to match the receipt, except
29
+ for a proven moved task: use its unflagged, operation-specific `recover-storage`
30
+ resume to release it, retain its files, and archive its receipt under `moved/`. Missing
31
+ or invalid pins retain resources and require repair; saved artifacts are never a
32
+ fallback. Enrolled checkout preserves
33
+ `managed_pending`/`managed_owned` and Rust's `storage-allocation.json` subreceipt.
34
+ After an interrupted reply, use the returned `recover-storage` resume. Never
35
+ delete managed receipts or replace an unfinished operation ID.
36
+
37
+ Owner `recover-storage --receipt <receipt.json> --operation-id <saved ID>
38
+ --for-removal` fences allocation and reconciles closure and the exact cache set.
39
+ Never-published allocations are abandoned. Accepted LOCAL_ONLY, FAILED, and PRs
40
+ observed CLOSED without merge share custody/recovery-ref verification and fenced
41
+ removal, retaining task branches and a durable terminal delivery. Release writers
42
+ with `complete --writers-stopped` first. A queue-produced FAILED checkout is
43
+ advanced by `read-tree -m -u <source_sha> <publish_sha>` only when HEAD is at the
44
+ attached publish commit and the index and worktree are exactly at source; other
45
+ differences refuse as dirty.
46
+
47
+ Removal-finished receipts with final COMPLETE, LOCAL_ONLY, FAILED or CLOSED
48
+ delivery move under `devlyn-completion/retired/`, regardless of scratch status.
49
+ Retirement moves scratch and its saved cleanup record with the audit and deletes
50
+ nothing. `complete --writers-stopped` saves the writer release there and retries
51
+ pending scratch, including an interrupted cleanup with no saved result; the next
52
+ allocation retries completed deliveries with released writers. `clean-scratch`
53
+ remains available there and never downgrades CLEAN.
54
+ Saved managed `complete`, `recover-storage` and `clean-scratch` resumes carry an
55
+ operation ID validated under the receipt lock. Without an ID, a locator selects
56
+ its active receipt. An ID selects only that operation, including its verified
57
+ retired receipt; a mismatched ID refuses. Retired paths also remain usable.
58
+ A new allocation may reuse a retired key without disturbing its audit.
59
+
60
+ Retained submodule repositories in the admin directory block removal even when
61
+ the checkout is absent; preserve/publish their source and explicitly resolve
62
+ them. Manual checkout removal is advised only after portable source checks and
63
+ record preservation succeed; changed or new source requires retention. If native
64
+ Windows or unobservable writers then require retention, stop all
65
+ writers, remove the task checkout by hand, keep its branch and receipt, then run
66
+ the returned owner-removal resume. The absent-tree path completes the fenced
67
+ registration removal. Scratch cleanup requires positive allocation closure,
68
+ without a cache-removal fence; actual checkout removal owns that fence, including
69
+ empty cache sets. Missing helpers or uncertain closure retain resources with a
70
+ repair/resume action. Compatible recovery artifacts and Rust's LaunchGate remain
71
+ required while managed obligations exist.
72
+
73
+ Managed allocations currently start cold; `build_dirs` retains each returned cache
74
+ and publication lease ID. Managed warming is deferred. Standalone allocations keep
75
+ the warming behavior in [task-completion.md](task-completion.md).
76
+
77
+ For managed receipts, use the saved `clean-scratch` resume with `--operation-id <saved ID>`.
78
+
79
+ New managed allocations use the recovery ref `refs/devlyn/completed/<id>.<operation>` so branch reuse
80
+ preserves earlier recovery refs.