devlyn-cli 2.5.7 → 2.6.1

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/README.md CHANGED
@@ -27,7 +27,7 @@ If devlyn-cli saved you time, [give it a star](https://github.com/fysoul17/devly
27
27
  npx devlyn-cli
28
28
  ```
29
29
 
30
- That's it. The interactive installer handles everything. Claude Code config is installed by default; optional AI CLI instructions can be selected during install. Choose **Codex CLI (OpenAI)** to install `AGENTS.md` and the `devlyn:resolve`, `devlyn:ideate`, and `devlyn:design-ui` skills into `~/.codex/skills/`. In Codex, invoke them as skills with `$devlyn:resolve`, `$devlyn:ideate`, or `$devlyn:design-ui` rather than Claude Code slash commands. Run it again anytime to update.
30
+ That's it. The installer opens with a single **agent selector** — pick any combination of **Claude Code**, **Codex CLI**, **oh-my-pi (omp)**, **Pi**, Gemini, Cursor, Copilot, or Windsurf, and devlyn installs into every one you choose in a single pass. Claude Code and any agent already present on your machine are pre-checked, so the common case is just Enter. Skill-capable agents receive the `devlyn:resolve`, `devlyn:ideate`, and `devlyn:design-ui` skills in the directory each one loads from: Codex → `~/.codex/skills/`, while **omp and Pi share `~/.agents/skills/`** — the cross-agent standard both read — so the bundle is written there once, not duplicated per agent. In Codex / omp / Pi, invoke them as skills (`$devlyn:resolve`, `$devlyn:ideate`, `$devlyn:design-ui`); in Claude Code they're slash commands (`/devlyn:resolve`). Run it again anytime to update. (`npx devlyn-cli -y` installs the Claude core non-interactively; `npx devlyn-cli agents <cli>` adds one agent later.)
31
31
 
32
32
  ---
33
33
 
@@ -39,7 +39,7 @@ devlyn-cli turns Claude Code into a hands-free development pipeline. The pipelin
39
39
  ideate (optional) → resolve → ship
40
40
  ```
41
41
 
42
- Codex note: when the optional Codex install is selected, these workflows are installed as Codex skills. Use `$devlyn:ideate`, `$devlyn:resolve`, or `$devlyn:design-ui` in Codex; the `/devlyn:*` slash-command form is for Claude Code.
42
+ Non-Claude agents (Codex / omp / Pi): when one of these is selected, the workflows install as that agent's skills. Use `$devlyn:ideate`, `$devlyn:resolve`, or `$devlyn:design-ui`; the `/devlyn:*` slash-command form is for Claude Code.
43
43
 
44
44
  ### Step 1 (optional) — Plan with `/devlyn:ideate`
45
45
 
package/bin/devlyn.js CHANGED
@@ -11,6 +11,19 @@ const AGENTS_SOURCE = path.join(__dirname, '..', 'agents-config');
11
11
  const OPTIONAL_SKILLS_SOURCE = path.join(__dirname, '..', 'optional-skills');
12
12
  const PKG = require('../package.json');
13
13
 
14
+ // The devlyn skill bundle installed into every skill-capable agent's loader
15
+ // directory. Single source of truth so codex/omp/pi stay in lockstep — adding a
16
+ // skill here installs it everywhere. Standards skills (code-*, root-cause-*,
17
+ // ui-*) stay Claude-core-only, matching the pre-existing Codex bundle.
18
+ const DEVLYN_CORE_SKILLS = ['devlyn:resolve', 'devlyn:ideate', 'devlyn:design-ui', '_shared'];
19
+
20
+ // Cross-agent shared skills directory read by BOTH oh-my-pi and Pi. Verified
21
+ // from the omp binary's skill-provider strings ("skills from .agents/skills —
22
+ // project walk-up + user home") and Pi's docs ("~/.agents/skills/"). Installing
23
+ // the bundle here once covers both agents — the de-dup driver writes it a single
24
+ // time even when both are selected.
25
+ const SHARED_AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
26
+
14
27
  // Cross-CLI agent installation targets
15
28
  // Each entry maps a CLI tool to where its agent instructions should be placed
16
29
  const CLI_TARGETS = {
@@ -22,9 +35,32 @@ const CLI_TARGETS = {
22
35
  // Codex auto-loads skills from ~/.codex/skills/ (user-global). Same
23
36
  // SKILL.md format as Claude Code; descriptions must stay ≤1024 chars.
24
37
  skillsDir: path.join(os.homedir(), '.codex', 'skills'),
25
- skillsToInstall: ['devlyn:resolve', 'devlyn:ideate', 'devlyn:design-ui', '_shared'],
38
+ skillsToInstall: DEVLYN_CORE_SKILLS,
26
39
  detect: () => fs.existsSync(path.join(process.cwd(), 'AGENTS.md')) || fs.existsSync(path.join(process.cwd(), '.codex')),
27
40
  },
41
+ omp: {
42
+ name: 'oh-my-pi (omp)',
43
+ instructionsFile: 'AGENTS.md',
44
+ baseInstructionsFile: 'AGENTS.md',
45
+ configDir: null, // omp discovers AGENTS.md at project root + user home
46
+ // omp loads skills from ~/.agents/skills (user home) — shared with Pi.
47
+ skillsDir: SHARED_AGENTS_SKILLS_DIR,
48
+ skillsToInstall: DEVLYN_CORE_SKILLS,
49
+ // Project-scoped only: machine-level ~/.omp must not auto-trigger a project
50
+ // AGENTS.md write via `npx devlyn-cli agents` in an unrelated repo.
51
+ detect: () => fs.existsSync(path.join(process.cwd(), '.omp')) || fs.existsSync(path.join(process.cwd(), '.agents')),
52
+ },
53
+ pi: {
54
+ name: 'Pi (earendil-works)',
55
+ instructionsFile: 'AGENTS.md',
56
+ baseInstructionsFile: 'AGENTS.md',
57
+ configDir: null, // Pi reads AGENTS.md / CLAUDE.md walking up from cwd
58
+ // Pi loads skills from ~/.agents/skills — shared with oh-my-pi.
59
+ skillsDir: SHARED_AGENTS_SKILLS_DIR,
60
+ skillsToInstall: DEVLYN_CORE_SKILLS,
61
+ // Project-scoped only (see omp): machine-level ~/.pi must not auto-trigger.
62
+ detect: () => fs.existsSync(path.join(process.cwd(), '.pi')) || fs.existsSync(path.join(process.cwd(), '.agents')),
63
+ },
28
64
  gemini: {
29
65
  name: 'Gemini CLI (Google)',
30
66
  instructionsFile: 'GEMINI.md',
@@ -363,9 +399,9 @@ function cleanManagedSkillDirs(sourceSkillsDir, targetSkillsDir) {
363
399
  return cleaned;
364
400
  }
365
401
 
366
- function multiSelect(items) {
402
+ function multiSelect(items, preselectedIndices = []) {
367
403
  return new Promise((resolve) => {
368
- const selected = new Set();
404
+ const selected = new Set(preselectedIndices.filter((i) => i >= 0 && i < items.length));
369
405
  let cursor = 0;
370
406
  let firstRender = true;
371
407
 
@@ -470,11 +506,15 @@ function installLocalSkill(skillName) {
470
506
  copyRecursive(src, dest, targetDir);
471
507
 
472
508
  // Mirror to every CLI skill-loader directory that already exists so optional
473
- // skills are picked up by Codex (and any future CLI with a skillsDir) the
474
- // same way required skills are. Existing dir, not new dir — we don't create
475
- // a Codex install just because someone opted into a Claude-side skill.
509
+ // skills are picked up by Codex/omp/Pi (and any future CLI with a skillsDir)
510
+ // the same way required skills are. Existing dir, not new dir — we don't
511
+ // create an agent install just because someone opted into a Claude-side skill.
512
+ // De-dup by directory: omp and Pi share ~/.agents/skills, so the mirror runs
513
+ // once per unique destination.
514
+ const mirrored = new Set();
476
515
  for (const cli of Object.values(CLI_TARGETS)) {
477
- if (!cli.skillsDir || !fs.existsSync(cli.skillsDir)) continue;
516
+ if (!cli.skillsDir || mirrored.has(cli.skillsDir) || !fs.existsSync(cli.skillsDir)) continue;
517
+ mirrored.add(cli.skillsDir);
478
518
  const cliDest = path.join(cli.skillsDir, skillName);
479
519
  if (fs.existsSync(cliDest)) fs.rmSync(cliDest, { recursive: true, force: true });
480
520
  copyRecursive(src, cliDest, cli.skillsDir);
@@ -556,94 +596,136 @@ function installSkillsForCLI(cliKey) {
556
596
  return copied;
557
597
  }
558
598
 
559
- function installAgentsForCLI(cliKey) {
599
+ // Strip devlyn's previously-appended "# Devlyn Agent Instructions" block from an
600
+ // instruction file (or the packaged base) before re-appending, so installs stay
601
+ // idempotent and never duplicate the block. Anchors on the LAST marker, not the
602
+ // first: the packaged AGENTS.md ships its own "# Devlyn Agent Instructions"
603
+ // section, so indexOf would truncate that real content — lastIndexOf targets the
604
+ // devlyn-appended block (always last) and preserves everything above it.
605
+ function stripManagedBlock(content) {
606
+ const marker = '# Devlyn Agent Instructions';
607
+ const markerIdx = content.lastIndexOf(marker);
608
+ if (markerIdx > 0) {
609
+ const sepIdx = content.lastIndexOf('\n---', markerIdx);
610
+ return content.slice(0, sepIdx > 0 ? sepIdx : markerIdx).trimEnd();
611
+ }
612
+ return content.trimEnd();
613
+ }
614
+
615
+ // One-line description of exactly what selecting a CLI target installs, so the
616
+ // unified selector stays honest — "Gemini" (instructions only) must not look
617
+ // identical to "Codex" (instructions + skills).
618
+ function targetDesc(cli) {
619
+ if (cli.skillsDir) {
620
+ return `${cli.instructionsFile} + devlyn skills → ${cli.skillsDir.replace(os.homedir(), '~')}`;
621
+ }
622
+ if (cli.configDir) return `Agent instructions → ${cli.configDir}/`;
623
+ return `${cli.instructionsFile} instructions only`;
624
+ }
625
+
626
+ // Install only the instruction file(s) for a CLI (no skills). Returns true when
627
+ // instruction content was written. Split from skills so the de-dup driver can
628
+ // write a shared destination (e.g. project AGENTS.md for codex/omp/pi) once.
629
+ function installInstructionsForCLI(cliKey) {
560
630
  const cli = CLI_TARGETS[cliKey];
561
631
  if (!cli) return false;
562
632
 
563
633
  const agents = fs.existsSync(AGENTS_SOURCE)
564
634
  ? fs.readdirSync(AGENTS_SOURCE).filter((f) => f.endsWith('.md'))
565
635
  : [];
636
+ if (agents.length === 0) return false;
566
637
 
567
- if (agents.length > 0) {
568
- log(`\n🤖 Installing agents for ${cli.name}...`, 'cyan');
638
+ log(`\n🤖 Installing agent instructions for ${cli.name}...`, 'cyan');
569
639
 
570
- if (cli.configDir) {
571
- // CLI supports an agents directory — copy agent files there
572
- const destDir = path.join(process.cwd(), cli.configDir);
573
- if (!fs.existsSync(destDir)) {
574
- fs.mkdirSync(destDir, { recursive: true });
575
- }
576
- for (const file of agents) {
577
- const src = path.join(AGENTS_SOURCE, file);
578
- const dest = path.join(destDir, file);
579
- fs.copyFileSync(src, dest);
580
- log(` → ${cli.configDir}/${file}`, 'dim');
581
- }
582
- } else {
583
- // CLI uses a single instructions file — append agent content
584
- const destFile = path.join(process.cwd(), cli.instructionsFile);
585
- const separator = '\n\n---\n\n# Devlyn Agent Instructions\n\n';
586
- const agentContent = agents.map((file) => {
587
- return fs.readFileSync(path.join(AGENTS_SOURCE, file), 'utf8');
588
- }).join('\n\n---\n\n');
589
-
590
- let existing = '';
591
- if (fs.existsSync(destFile)) {
592
- existing = fs.readFileSync(destFile, 'utf8');
593
- // Remove previous devlyn agent section if present
594
- const devlynMarker = '# Devlyn Agent Instructions';
595
- const markerIdx = existing.indexOf(devlynMarker);
596
- if (markerIdx > 0) {
597
- // Find the separator before the marker (---\n\n)
598
- const sepIdx = existing.lastIndexOf('---', markerIdx);
599
- existing = existing.slice(0, sepIdx > 0 ? sepIdx : markerIdx).trimEnd();
600
- }
601
- } else if (cli.baseInstructionsFile) {
602
- const baseInstructionsSrc = path.join(__dirname, '..', cli.baseInstructionsFile);
603
- if (fs.existsSync(baseInstructionsSrc)) {
604
- existing = fs.readFileSync(baseInstructionsSrc, 'utf8').trimEnd();
605
- }
640
+ if (cli.configDir) {
641
+ // CLI supports an agents directory — copy agent files there
642
+ const destDir = path.join(process.cwd(), cli.configDir);
643
+ if (!fs.existsSync(destDir)) {
644
+ fs.mkdirSync(destDir, { recursive: true });
645
+ }
646
+ for (const file of agents) {
647
+ const src = path.join(AGENTS_SOURCE, file);
648
+ const dest = path.join(destDir, file);
649
+ fs.copyFileSync(src, dest);
650
+ log(` → ${cli.configDir}/${file}`, 'dim');
651
+ }
652
+ } else {
653
+ // CLI uses a single instructions file — append agent content
654
+ const destFile = path.join(process.cwd(), cli.instructionsFile);
655
+ const separator = '\n\n---\n\n# Devlyn Agent Instructions\n\n';
656
+ const agentContent = agents.map((file) => {
657
+ return fs.readFileSync(path.join(AGENTS_SOURCE, file), 'utf8');
658
+ }).join('\n\n---\n\n');
659
+
660
+ let existing = '';
661
+ if (fs.existsSync(destFile)) {
662
+ existing = stripManagedBlock(fs.readFileSync(destFile, 'utf8'));
663
+ } else if (cli.baseInstructionsFile) {
664
+ const baseInstructionsSrc = path.join(__dirname, '..', cli.baseInstructionsFile);
665
+ if (fs.existsSync(baseInstructionsSrc)) {
666
+ existing = stripManagedBlock(fs.readFileSync(baseInstructionsSrc, 'utf8'));
606
667
  }
607
-
608
- fs.writeFileSync(destFile, existing + separator + agentContent + '\n');
609
- log(` → ${cli.instructionsFile} (agent instructions appended)`, 'dim');
610
668
  }
669
+
670
+ fs.writeFileSync(destFile, existing + separator + agentContent + '\n');
671
+ log(` → ${cli.instructionsFile} (agent instructions appended)`, 'dim');
611
672
  }
673
+ return true;
674
+ }
612
675
 
613
- // Always install global skills (Codex-style skill loader) regardless of
614
- // whether agent files were present. Agent install and skill install are
615
- // independent capabilities.
676
+ // Install both instructions and skills for a single CLI. Used by the
677
+ // `agents <cli>` command; multi-target paths use installSelectedCLITargets.
678
+ function installAgentsForCLI(cliKey) {
679
+ const instr = installInstructionsForCLI(cliKey);
616
680
  const skillsCopied = installSkillsForCLI(cliKey);
617
681
  if (skillsCopied > 0) {
618
682
  log(` → ${skillsCopied} skill${skillsCopied > 1 ? 's' : ''} installed (devlyn:resolve / devlyn:ideate / devlyn:design-ui / _shared)`, 'dim');
619
683
  }
684
+ return instr || skillsCopied > 0;
685
+ }
620
686
 
621
- return agents.length > 0 || skillsCopied > 0;
687
+ // Install instructions + skills for a set of selected CLI targets, writing each
688
+ // unique destination exactly once. omp and Pi share project AGENTS.md and the
689
+ // ~/.agents/skills dir, so selecting both does not duplicate writes — this is
690
+ // the researched "group the common" model: targets are the user's selection,
691
+ // destinations are the unit of work.
692
+ function installSelectedCLITargets(cliKeys) {
693
+ const instrDone = new Set();
694
+ const skillsDone = new Set();
695
+ let count = 0;
696
+ for (const cliKey of cliKeys) {
697
+ const cli = CLI_TARGETS[cliKey];
698
+ if (!cli) continue;
699
+ const instrKey = cli.configDir ? `dir:${cli.configDir}` : `file:${cli.instructionsFile}`;
700
+ if (!instrDone.has(instrKey)) {
701
+ installInstructionsForCLI(cliKey);
702
+ instrDone.add(instrKey);
703
+ }
704
+ if (cli.skillsDir && !skillsDone.has(cli.skillsDir)) {
705
+ const copied = installSkillsForCLI(cliKey);
706
+ if (copied > 0) {
707
+ log(` → ${copied} skill${copied > 1 ? 's' : ''} → ${cli.skillsDir.replace(os.homedir(), '~')}`, 'dim');
708
+ }
709
+ skillsDone.add(cli.skillsDir);
710
+ }
711
+ count++;
712
+ }
713
+ return count;
622
714
  }
623
715
 
624
716
  function installAgentsForAllDetected() {
625
717
  const detected = detectOtherCLIs();
626
718
  if (detected.length === 0) return 0;
627
-
628
- let installed = 0;
629
- for (const cliKey of detected) {
630
- if (installAgentsForCLI(cliKey)) installed++;
631
- }
632
- return installed;
719
+ return installSelectedCLITargets(detected);
633
720
  }
634
721
 
635
- async function init(skipPrompts = false) {
636
- showLogo();
637
- log('─'.repeat(44), 'dim');
638
-
639
- if (!fs.existsSync(CONFIG_SOURCE)) {
640
- log('❌ Config source not found', 'yellow');
641
- process.exit(1);
642
- }
643
-
644
- // Install core config
722
+ // Install the Claude Code core config: project .claude/ (skills, templates,
723
+ // settings, CLAUDE.md, .gitignore) plus global ~/.claude/settings.json tweaks.
724
+ // Extracted so the unified target selector installs it only when "Claude Code"
725
+ // is chosen. Behavior is unchanged from the previous unconditional core install.
726
+ function installClaudeCore() {
645
727
  const targetDir = getTargetDir();
646
- log('\n📁 Installing core config to .claude/', 'green');
728
+ log('\n📁 Installing Claude Code config to .claude/', 'green');
647
729
  const refreshed = cleanManagedSkillDirs(
648
730
  path.join(CONFIG_SOURCE, 'skills'),
649
731
  path.join(targetDir, 'skills'),
@@ -747,40 +829,55 @@ async function init(skipPrompts = false) {
747
829
  log(' → ~/.claude/settings.json (disabled adaptive thinking, enabled 1h prompt caching)', 'dim');
748
830
  }
749
831
 
750
- log('\n✅ Core config installed!', 'green');
832
+ log('\n✅ Claude Code config installed!', 'green');
833
+ }
834
+
835
+ async function init(skipPrompts = false) {
836
+ showLogo();
837
+ log('─'.repeat(44), 'dim');
751
838
 
752
- // Skip prompts if -y flag or non-interactive
839
+ if (!fs.existsSync(CONFIG_SOURCE)) {
840
+ log('❌ Config source not found', 'yellow');
841
+ process.exit(1);
842
+ }
843
+
844
+ // Non-interactive / -y: preserve historical behavior — install Claude core
845
+ // only. Multi-target selection needs a TTY; scripted runs add other agents
846
+ // via `npx devlyn-cli agents <cli>`.
753
847
  if (skipPrompts || !process.stdin.isTTY) {
848
+ installClaudeCore();
754
849
  log('\n💡 Add optional addons later: run `npx devlyn-cli` without -y', 'dim');
755
- log(' Add Codex instructions + skills later: run `npx devlyn-cli agents codex`', 'dim');
850
+ log(' Add another agent (Codex / omp / Pi / …) later: `npx devlyn-cli agents <cli>`', 'dim');
756
851
  log(`\n${COLORS.dim} Enjoying devlyn? Star it on GitHub — it helps others find it:${COLORS.reset}`);
757
852
  log(` ${COLORS.purple}→ https://github.com/fysoul17/devlyn-cli${COLORS.reset}\n`);
758
853
  return;
759
854
  }
760
855
 
761
- // Ask which non-Claude CLIs should receive instruction files.
762
- log('\n🤖 Optional AI CLI instructions:\n', 'blue');
763
- const cliOptions = Object.entries(CLI_TARGETS).map(([key, cli]) => {
764
- let desc;
765
- if (cli.configDir) {
766
- desc = `Install agents into ${cli.configDir}/`;
767
- } else if (cli.skillsDir) {
768
- desc = `Install ${cli.instructionsFile} + devlyn:resolve/devlyn:ideate/devlyn:design-ui skills (~/.codex/skills/; use $devlyn:* in Codex)`;
769
- } else {
770
- desc = `Install ${cli.instructionsFile}`;
771
- }
772
- return { key, name: cli.name, desc, type: 'cli' };
773
- });
774
- const selectedClis = await multiSelect(cliOptions);
775
- if (selectedClis.length > 0) {
776
- let agentsInstalled = 0;
777
- for (const selectedCli of selectedClis) {
778
- if (installAgentsForCLI(selectedCli.key)) agentsInstalled++;
779
- }
780
- log(` ✅ Agent instructions installed for ${agentsInstalled} CLI${agentsInstalled !== 1 ? 's' : ''}`, 'green');
781
- } else {
782
- log('💡 No additional CLI instructions selected', 'dim');
783
- log(' Run `npx devlyn-cli agents codex` later to install Codex AGENTS.md + devlyn skills', 'dim');
856
+ // Pick every agent to install devlyn into, up front. Claude Code is
857
+ // pre-checked (the happy path); every other agent is an explicit opt-in so a
858
+ // bare Enter never mutates a project's AGENTS.md or a shared skills dir for an
859
+ // agent the user didn't choose.
860
+ log('\n🎯 Select the agents to install devlyn into:\n', 'blue');
861
+ const targetOptions = [
862
+ { key: 'claude', name: 'Claude Code', desc: '.claude/ config — skills, templates, settings, CLAUDE.md', type: 'cli' },
863
+ ...Object.entries(CLI_TARGETS).map(([key, cli]) => ({ key, name: cli.name, desc: targetDesc(cli), type: 'cli' })),
864
+ ];
865
+ const preselected = [targetOptions.findIndex((o) => o.key === 'claude')];
866
+ const selectedKeys = (await multiSelect(targetOptions, preselected)).map((t) => t.key);
867
+
868
+ if (selectedKeys.length === 0) {
869
+ log('\n💡 No agents selected — nothing installed.', 'yellow');
870
+ log(' Run `npx devlyn-cli` again and pick at least one agent.\n', 'dim');
871
+ return;
872
+ }
873
+
874
+ if (selectedKeys.includes('claude')) {
875
+ installClaudeCore();
876
+ }
877
+ const cliKeys = selectedKeys.filter((k) => k !== 'claude');
878
+ if (cliKeys.length > 0) {
879
+ const installed = installSelectedCLITargets(cliKeys);
880
+ log(`\n ✅ Installed devlyn into ${installed} agent${installed !== 1 ? 's' : ''}`, 'green');
784
881
  }
785
882
 
786
883
  // Ask about optional addons (local skills + external packs)
@@ -806,9 +903,9 @@ async function init(skipPrompts = false) {
806
903
  function showHelp() {
807
904
  showLogo();
808
905
  log('Usage:', 'green');
809
- log(' npx devlyn-cli Install/update .claude config');
906
+ log(' npx devlyn-cli Install/update devlyn (pick agents: Claude / Codex / omp / Pi / …)');
810
907
  log(' npx devlyn-cli list List available skills & templates');
811
- log(' npx devlyn-cli -y Install without prompts');
908
+ log(' npx devlyn-cli -y Install Claude core without prompts');
812
909
  log(' npx devlyn-cli agents Install agents for detected CLIs');
813
910
  log(' npx devlyn-cli agents all Install agents for all supported CLIs');
814
911
  log(' npx devlyn-cli benchmark Run the resolve benchmark suite');
@@ -1065,12 +1162,10 @@ switch (command) {
1065
1162
  log('─'.repeat(44), 'dim');
1066
1163
  const subArg = args[1];
1067
1164
  if (subArg === 'all') {
1068
- // Install for all supported CLIs regardless of detection
1165
+ // Install for all supported CLIs regardless of detection. De-duped so the
1166
+ // shared ~/.agents/skills dir (omp + Pi) and shared AGENTS.md write once.
1069
1167
  log('\n🤖 Installing agents for all supported CLIs...', 'blue');
1070
- let count = 0;
1071
- for (const cliKey of Object.keys(CLI_TARGETS)) {
1072
- if (installAgentsForCLI(cliKey)) count++;
1073
- }
1168
+ const count = installSelectedCLITargets(Object.keys(CLI_TARGETS));
1074
1169
  log(`\n✅ Agents installed for ${count} CLI${count !== 1 ? 's' : ''}`, 'green');
1075
1170
  } else if (subArg && CLI_TARGETS[subArg]) {
1076
1171
  // Install for a specific CLI
@@ -39,7 +39,7 @@ Each phase routes to an engine and prepends the per-engine adapter header from `
39
39
  <modes>
40
40
  Three input shapes:
41
41
 
42
- 1. **Free-form**: `/devlyn:resolve "fix the login bug"`. PHASE 0 runs the complexity classifier and either proceeds with an internal mini-spec (trivial), drafts focused questions for in-prompt resolution (medium), or escalates to `/devlyn:ideate` (large/ambiguous). No mid-pipeline prompts in any branch.
42
+ 1. **Free-form**: `/devlyn:resolve "fix the login bug"` (inline goal) or `/devlyn:resolve --goal-file <path>` (goal text read from a file — the devlynd `ResolveAdapter` launcher path, injection-safe). PHASE 0 runs the complexity classifier and either proceeds with an internal mini-spec (trivial), drafts focused questions for in-prompt resolution (medium), or escalates to `/devlyn:ideate` (large/ambiguous). No mid-pipeline prompts in any branch.
43
43
  2. **Spec**: `/devlyn:resolve --spec docs/roadmap/phase-N/X.md`. Spec is read-only. Stage verification commands from sibling `spec.expected.json`; if absent, use the legacy `## Verification` JSON block.
44
44
  3. **Verify-only**: `/devlyn:resolve --verify-only <diff-or-PR-ref> --spec <path>`. Skips PHASE 1-4. Runs PHASE 5 (VERIFY) on the supplied diff against the spec.
45
45
  </modes>
@@ -58,6 +58,7 @@ Once `state.implement_passed_sha` is non-null (PHASE 2 returned and produced a d
58
58
  - `--engine MODE` (default `claude`) — picks the adapter for IMPLEMENT, CLEANUP, and the primary VERIFY judge. It does not disable VERIFY pair-mode; when a VERIFY pair trigger fires, the second judge uses the OTHER engine.
59
59
  - `--spec <path>` — switches to spec mode.
60
60
  - `--verify-only <ref>` — switches to verify-only mode. Requires `--spec`.
61
+ - `--goal-file <path>` — free-form mode: read the goal text from `<path>` (relative to cwd) instead of an inline positional goal. Used by the devlynd `ResolveAdapter` launcher (goal written to `.devlyn/goal.txt`, referenced by path — never inlined as argv). The file content is opaque goal text fed to the complexity classifier, **never re-parsed as flags**. Mutually exclusive with `--spec`, `--verify-only`, and an inline positional goal.
61
62
  - `--pair-verify` — force pair-mode JUDGE in PHASE 5 even when not auto-triggered.
62
63
  - `--no-pair` — disable conditional VERIFY pair-JUDGE for this run. Record `pair_trigger.skipped_reason: "user_no_pair"` whenever a trigger would otherwise fire.
63
64
  - `--risk-probes` — insert PHASE 1.5 cross-engine probe derivation. The OTHER engine converts visible `## Verification` bullets into bounded executable probes before IMPLEMENT; BUILD_GATE and VERIFY replay them mechanically.
@@ -67,12 +68,12 @@ Once `state.implement_passed_sha` is non-null (PHASE 2 returned and produced a d
67
68
 
68
69
  2. Engine pre-flight: follow `_shared/engine-preflight.md`. If a required engine is unavailable, halt with a BLOCKED verdict and setup instructions instead of downgrading.
69
70
 
70
- `--pair-verify` and `--no-pair` are mutually exclusive; if both are present, stop with `BLOCKED:invalid-flags`.
71
+ `--pair-verify` and `--no-pair` are mutually exclusive; if both are present, stop with `BLOCKED:invalid-flags`. Likewise `--goal-file` combined with `--spec`, `--verify-only`, or an inline positional goal — or a missing/repeated `--goal-file` value — stops with `BLOCKED:invalid-flags`. (These `BLOCKED:<reason>` strings are report-level only; the JSON `phases.*.verdict` carrier in `pipeline.state.json` always stays the bare enum `"BLOCKED"`.)
71
72
 
72
73
  3. Initialize `.devlyn/pipeline.state.json` per `references/state-schema.md`. Set `state.run_id`, `started_at`, `engine`, `pair_verify: true` only when `--pair-verify` was passed and `false` otherwise, `base_ref.{branch, sha}`, `rounds.{max_rounds, global: 0}`, `bypasses`, empty `phases`, empty `criteria`, and `risk_profile: { high_risk: false, reasons: [], risk_probes_enabled: false, pair_default_enabled: true }`. `risk_profile` is strict typed state: keep it an object, keep the three flags as JSON booleans, and keep `reasons` as a string array; never serialize booleans as strings.
73
74
 
74
75
  4. **Mode-specific init**:
75
- - **Free-form**: read `references/free-form-mode.md`. Run the complexity classifier deterministically (rules over keyword density / file count / spec-shape signals, plus pair-evidence intent). Set `state.complexity ∈ {trivial, medium, large}`. Trivial: write internal mini-spec to `.devlyn/criteria.generated.md` and proceed. Medium: synthesize a minimal spec from the goal + add 1-2 context anchors from the codebase, write to `.devlyn/criteria.generated.md`, proceed. Every free-form branch that writes criteria must set `state.source.type = "generated"`, `state.source.criteria_path = ".devlyn/criteria.generated.md"`, and `state.source.criteria_sha256` from the raw file bytes. Large: log `recommend: /devlyn:ideate first` in the final report and either halt (default) or proceed with assumed defaults if `--continue-on-large` flag set, except pair-evidence intent without an actionable solo-headroom hypothesis must halt with `BLOCKED:solo-headroom-hypothesis-required`, and unmeasured pair-candidate intent without solo ceiling avoidance must halt with `BLOCKED:solo-ceiling-avoidance-required`.
76
+ - **Free-form**: first resolve `goal_text` — the inline positional goal, OR (when `--goal-file <path>` was passed) the raw UTF-8 content of `<path>`, read as opaque goal text (never re-parsed as flags). Fail closed before classifying: empty/whitespace-only file → `BLOCKED:goal-file-empty`; missing/unreadable/not-a-regular-file/invalid-UTF-8 → `BLOCKED:goal-file-unreadable`; an absolute path, a `..` segment, or a path that canonicalizes outside cwd → `BLOCKED:goal-file-invalid-path`. (Like all PHASE-0 halts these are report-level `BLOCKED:<reason>` strings — no `phases.*.verdict` is written, so the bare-enum carrier rule is never violated.) Then read `references/free-form-mode.md` and run the complexity classifier deterministically on `goal_text` (rules over keyword density / file count / spec-shape signals, plus pair-evidence intent). Set `state.complexity ∈ {trivial, medium, large}`. Trivial: write internal mini-spec to `.devlyn/criteria.generated.md` and proceed. Medium: synthesize a minimal spec from the goal + add 1-2 context anchors from the codebase, write to `.devlyn/criteria.generated.md`, proceed. Every free-form branch that writes criteria must set `state.source.type = "generated"`, `state.source.criteria_path = ".devlyn/criteria.generated.md"`, and `state.source.criteria_sha256` from the raw file bytes. Large: log `recommend: /devlyn:ideate first` in the final report and either halt (default) or proceed with assumed defaults if `--continue-on-large` flag set, except pair-evidence intent without an actionable solo-headroom hypothesis must halt with `BLOCKED:solo-headroom-hypothesis-required`, and unmeasured pair-candidate intent without solo ceiling avoidance must halt with `BLOCKED:solo-ceiling-avoidance-required`.
76
77
  - **Spec**: validate spec exists. If sibling `spec.expected.json` exists, run `--check-expected <expected-path>` to validate both the expected contract, sibling spec `complexity` frontmatter, and any present actionable solo-headroom hypothesis; if the spec has a solo-headroom hypothesis, its observable command must match `spec.expected.json.verification_commands[].cmd`. Then stage `.devlyn/spec-verify.json` from `verification_commands`. Otherwise run `--check <spec-path>` to validate the legacy inline carrier plus supported `complexity` frontmatter and any present actionable solo-headroom hypothesis; if the spec uses an inline `## Verification` JSON carrier, any solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`. Then stage from the legacy inline carrier. Compute `state.source.spec_sha256`.
77
78
  - **Verify-only**: skip to PHASE 5 with `state.source.spec_path` set, the supplied diff captured at `.devlyn/external-diff.patch`.
78
79
 
@@ -1,6 +1,6 @@
1
1
  # Free-form mode — complexity classifier
2
2
 
3
- When `/devlyn:resolve` is invoked with a free-form goal (no `--spec`), PHASE 0 runs this classifier to set `state.complexity ∈ {trivial, medium, large}` and either proceeds with an internal mini-spec, drafts focused questions for in-prompt resolution, or recommends `/devlyn:ideate` first.
3
+ When `/devlyn:resolve` is invoked with a free-form goal (no `--spec`) — whether the goal is the inline positional text or the content of `--goal-file <path>` (PHASE 0 resolves `goal_text` from either source before classifying) — PHASE 0 runs this classifier to set `state.complexity ∈ {trivial, medium, large}` and either proceeds with an internal mini-spec, drafts focused questions for in-prompt resolution, or recommends `/devlyn:ideate` first.
4
4
 
5
5
  The classifier is rules-based / deterministic — not an LLM judgment call. Decision rules below.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "2.5.7",
3
+ "version": "2.6.1",
4
4
  "description": "AI development toolkit for Claude Code — ideate, resolve, and ship with context engineering and agent orchestration",
5
5
  "homepage": "https://github.com/fysoul17/devlyn-cli#readme",
6
6
  "bin": {
@@ -242,10 +242,11 @@ if [ ! -e "optional-skills/devlyn:design-ui" ]; then
242
242
  else
243
243
  bad "devlyn:design-ui must not be installed as an optional addon"
244
244
  fi
245
- if grep -Fq "skillsToInstall: ['devlyn:resolve', 'devlyn:ideate', 'devlyn:design-ui', '_shared']" bin/devlyn.js; then
246
- ok "Codex install includes devlyn:design-ui"
245
+ if grep -Fq "const DEVLYN_CORE_SKILLS = ['devlyn:resolve', 'devlyn:ideate', 'devlyn:design-ui', '_shared'];" bin/devlyn.js \
246
+ && grep -Fq "skillsToInstall: DEVLYN_CORE_SKILLS" bin/devlyn.js; then
247
+ ok "Codex install includes devlyn:design-ui (via shared DEVLYN_CORE_SKILLS bundle)"
247
248
  else
248
- bad "Codex skillsToInstall must include devlyn:design-ui"
249
+ bad "Codex skillsToInstall must include devlyn:design-ui (shared DEVLYN_CORE_SKILLS bundle)"
249
250
  fi
250
251
  if ! grep -F "name: 'devlyn:design-ui'" bin/devlyn.js >/dev/null 2>&1; then
251
252
  ok "devlyn:design-ui is absent from OPTIONAL_ADDONS"