@eventmodelers/cli 1.0.74 → 1.0.76

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 (32) hide show
  1. package/README.md +7 -5
  2. package/cli.js +80 -28
  3. package/package.json +1 -1
  4. package/shared/build-kit/README.md +64 -9
  5. package/shared/build-kit/lib/local-ai-agent.js +281 -0
  6. package/shared/build-kit/lib/ralph.js +1 -1
  7. package/shared/build-kit/ralph-exec.js +92 -0
  8. package/shared/build-kit/ralph-local-ai.js +40 -0
  9. package/shared/build-kit/realtime-agent.js +1 -1
  10. package/shared/skills/learn-eventmodelers-api/SKILL.md +93 -43
  11. package/shared/skills/update-slice-status/SKILL.md +8 -0
  12. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -1
  13. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -1
  14. package/stacks/bridge/templates/bridge/lib/AGENT.md +1 -1
  15. package/stacks/bridge/templates/bridge/ralph-local-ai.js +43 -0
  16. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -1
  17. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +11 -1
  18. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +8 -3
  19. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
  20. package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -1
  21. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -1
  22. package/stacks/react/templates/build-kit/README.md +64 -9
  23. package/stacks/react/templates/build-kit/lib/prompt.md +1 -1
  24. package/stacks/react/templates/build-kit/lib/ralph.js +1 -1
  25. package/stacks/react/templates/build-kit/ralph-local-ai.js +40 -0
  26. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -1
  27. package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +1 -1
  28. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -1
  29. package/shared/build-kit/lib/ollama-agent.js +0 -147
  30. package/shared/build-kit/ralph-ollama.js +0 -40
  31. package/stacks/bridge/templates/bridge/ralph-ollama.js +0 -43
  32. package/stacks/react/templates/build-kit/ralph-ollama.js +0 -40
package/README.md CHANGED
@@ -78,7 +78,8 @@ your-project/
78
78
  │ └── config.json ← your token + org/board (gitignored) — shared by every kit in this project
79
79
  ├── .build-kit/ ← agent runner (name is .agent-modeling-kit/ for the modeling-kit stack)
80
80
  │ ├── ralph-claude.js ← realtime agent + task loop
81
- │ ├── ralph-ollama.js ← same, via local Ollama
81
+ │ ├── ralph-local-ai.js ← same, via a local/self-hosted model
82
+ │ ├── ralph-exec.js ← same, via an external agent command (Codex CLI, OpenCode, …)
82
83
  │ ├── ralph.sh ← bash-only loop (no realtime)
83
84
  │ ├── lib/ ← stack-specific agent prompts + helpers
84
85
  │ └── .slices/ ← board slices, written by `fetch`/`listen` (or pre-seeded by `init --demo`)
@@ -92,7 +93,7 @@ The six backend stacks (`node`, `supabase`, `axon`, `opencqrs`, `umadb`, `kurren
92
93
 
93
94
  `react` and `supabase-react` are two more registered stacks (installable the same way). `supabase-react` is real, filled-in content — a Vite + React 19 + TypeScript scaffold that authenticates and issues command POSTs via a Supabase session (`src/lib/api.ts`/`src/lib/supabase.ts`), plus `init-style-guide`/`learn-styleguide` skills so generated UI stays on-brand. It's UI-only: `.build-kit/CLAUDE.md` only routes `STATE_CHANGE`/`STATE_VIEW` slices to `build-state-change`/`build-state-view` — an `AUTOMATION` slice has no UI counterpart and gets flagged via `request-feedback` instead, since it belongs to whichever backend stack is installed alongside this one. It needs no overrides at all and uses `shared/build-kit`'s realtime agent as-is.
94
95
 
95
- `react` (the plain-REST/board-polling variant, no Supabase) is still in the same state as a fresh `init --build-kit` scaffold — CLAUDE.md, the `build-*` skills, and `templates/root/` are all TODO-marked placeholders, not real content, pending an equivalent reference implementation. It overrides `lib/ralph.js` (+ `ralph-claude.js`/`ralph-ollama.js`/`package.json`/`README.md`) for board-polling sync. Fill in the TODOs (and add a real `templates/root/` scaffold) against an actual project before relying on it.
96
+ `react` (the plain-REST/board-polling variant, no Supabase) is still in the same state as a fresh `init --build-kit` scaffold — CLAUDE.md, the `build-*` skills, and `templates/root/` are all TODO-marked placeholders, not real content, pending an equivalent reference implementation. It overrides `lib/ralph.js` (+ `ralph-claude.js`/`ralph-local-ai.js`/`package.json`/`README.md`) for board-polling sync. Fill in the TODOs (and add a real `templates/root/` scaffold) against an actual project before relying on it.
96
97
 
97
98
  ## Skills
98
99
 
@@ -124,7 +125,8 @@ npx @eventmodelers/cli init --stack <name> # scaffold a stack + install
124
125
  npx @eventmodelers/cli init --stack <name> --demo # same, plus a ready-made demo model in the kit's .slices/ to build against
125
126
  npx @eventmodelers/cli re-init # refresh an already-installed kit's scripts/skills only — never touches the root scaffold
126
127
  npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
127
- npx @eventmodelers/cli run --ollama # same, via local Ollama (ralph-ollama.js)
128
+ npx @eventmodelers/cli run --local-ai [target] # same, via a local/self-hosted model (ralph-local-ai.js)
129
+ npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js)
128
130
  npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
129
131
  npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
130
132
  npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
@@ -335,7 +337,7 @@ npx @eventmodelers/cli bridge
335
337
 
336
338
  `init --bridge` installs a `.bridge-kit/` (mirrors `.build-kit/`'s realtime + task-queue loop) plus only the skills for the chosen `--target` (`shared/bridge/<target>/`) — a `spec-kitty` bridge never installs Kiro's skills, and vice versa. `bridge` starts the loop: on every board slice change (not just "Planned", unlike build-kit), it regenerates that framework's spec artifacts from the current board state. It doesn't build code and doesn't claim slices.
337
339
 
338
- For `spec-kitty`, that sync is deterministic and stops well short of writing Spec Kitty's own artifacts — `lib/adapters/spec-kitty-adapter.js` fetches full slice detail and restates it as a plain markdown mission brief (one section per slice, its scenarios verbatim, nothing invented), then calls `spec-kitty intake --force` to install it at `.kittify/mission-brief.md`. It deliberately doesn't create the mission, write `spec.md`, or author work packages — Spec Kitty's own `/spec-kitty.specify` → `/spec-kitty.plan` → `/spec-kitty.tasks` pipeline does that, because those steps need real judgment (work package boundaries, which files a WP owns, which agent profile fits) that only makes sense with actual codebase context, which this adapter doesn't have. What it replaces is Spec Kitty's *interactive discovery interview*: `/spec-kitty.specify`'s own "Brief Context Detection" step reads `.kittify/mission-brief.md` when present and extracts requirements from it instead of asking the user, so the event model — not a live Q&A — becomes the input. No LLM call happens in this adapter's own path, and `bridge` picks it automatically whenever a target has one (`--claude` forces the Claude runner instead). Targets without a static adapter yet fall back to Claude re-running `bridge-<target>-specify`; pass `--ollama` for the local-Ollama runner instead (same caveat as build-kit's `--ollama`: `lib/ollama-agent.js` is shared as-is).
340
+ For `spec-kitty`, that sync is deterministic and stops well short of writing Spec Kitty's own artifacts — `lib/adapters/spec-kitty-adapter.js` fetches full slice detail and restates it as a plain markdown mission brief (one section per slice, its scenarios verbatim, nothing invented), then calls `spec-kitty intake --force` to install it at `.kittify/mission-brief.md`. It deliberately doesn't create the mission, write `spec.md`, or author work packages — Spec Kitty's own `/spec-kitty.specify` → `/spec-kitty.plan` → `/spec-kitty.tasks` pipeline does that, because those steps need real judgment (work package boundaries, which files a WP owns, which agent profile fits) that only makes sense with actual codebase context, which this adapter doesn't have. What it replaces is Spec Kitty's *interactive discovery interview*: `/spec-kitty.specify`'s own "Brief Context Detection" step reads `.kittify/mission-brief.md` when present and extracts requirements from it instead of asking the user, so the event model — not a live Q&A — becomes the input. No LLM call happens in this adapter's own path, and `bridge` picks it automatically whenever a target has one (`--claude` forces the Claude runner instead). Targets without a static adapter yet fall back to Claude re-running `bridge-<target>-specify`; pass `--local-ai` for the local-model runner instead (same caveat as build-kit's `--local-ai`: `lib/local-ai-agent.js` is shared as-is).
339
341
 
340
342
  Don't want the standing loop at all? `fetch` can call the same adapter for a single one-shot sync, no `.bridge-kit/` install required:
341
343
 
@@ -354,7 +356,7 @@ npx @eventmodelers/cli init --bridge --target spec-kitty --hook "git add .slices
354
356
  npx @eventmodelers/cli bridge
355
357
  ```
356
358
 
357
- `init --bridge --hook` persists the command to `.bridge-kit/bridge.json` — a plain, **committed** file (unlike `.eventmodelers/config.json`, which is gitignored for credentials) since the hook is project policy meant to be shared by every teammate and CI runner, not per-machine state. `bridge --hook "<command>"` overrides it for a single run without touching that file. Only one executor runs per invocation — `--ollama`, `--hook`, and `--claude` are mutually exclusive.
359
+ `init --bridge --hook` persists the command to `.bridge-kit/bridge.json` — a plain, **committed** file (unlike `.eventmodelers/config.json`, which is gitignored for credentials) since the hook is project policy meant to be shared by every teammate and CI runner, not per-machine state. `bridge --hook "<command>"` overrides it for a single run without touching that file. Only one executor runs per invocation — `--local-ai`, `--hook`, and `--claude` are mutually exclusive.
358
360
 
359
361
  The hook command runs with `BRIDGE_TASK_COUNT`, `BRIDGE_SLICE_ID`/`_TITLE`/`_STATUS` (the most recent change in the batch), and `BRIDGE_BATCH_FILE` (path to the full batch as JSON) in its environment. It's invoked once per batch, not once per slice — any change that arrives while the hook is still running is left queued for the next batch rather than dropped.
360
362
 
package/cli.js CHANGED
@@ -31,8 +31,8 @@ const __dirname = dirname(__filename);
31
31
 
32
32
  // Each stack is a template set under stacks/<key>/templates/{.claude,root,<kitSubdir>}.
33
33
  // Stacks with useShared:true also get shared/build-kit/* copied into their kit dir
34
- // first (ralph.js, ralph-claude.js, ralph-ollama.js, ralph.sh, realtime-agent.js,
35
- // code-export.mjs, lib/agent.sh, lib/ollama-agent.js, package.json, README.md) —
34
+ // first (ralph.js, ralph-claude.js, ralph-local-ai.js, ralph-exec.js, ralph.sh, realtime-agent.js,
35
+ // code-export.mjs, lib/agent.sh, lib/local-ai-agent.js, package.json, README.md) —
36
36
  // those files have no per-stack content, so they live once instead of being
37
37
  // copy-pasted into every stack (that copy-pasting is exactly how they drifted out
38
38
  // of sync before: a bugfix or default landing in one stack's copy but not another's).
@@ -41,6 +41,29 @@ const __dirname = dirname(__filename);
41
41
  // modeling-kit (below) is the one kit that opts out of all of this (useShared:false)
42
42
  // — it has no cold-spawn/tasks.json runtime at all, so none of shared/build-kit/*
43
43
  // applies to it; see its own templates/kit for its (much smaller) self-contained set.
44
+ // --- Local-AI runner selection ------------------------------------------------
45
+ // One runner drives every local/self-hosted backend. What differs between Ollama,
46
+ // vLLM, LM Studio and llama.cpp is the wire dialect (native /api/chat vs the
47
+ // OpenAI-compatible /v1/chat/completions) plus a default URL — both handled inside
48
+ // lib/local-ai-agent.js. Splitting this per vendor would multiply the one file that
49
+ // is currently shared across every stack, so these are presets, not runners.
50
+ // Claude (ralph-claude.js) stays the default executor; this is opt-in.
51
+ const LOCAL_AI_TARGETS = ['ollama', 'vllm', 'lmstudio', 'llamacpp'];
52
+
53
+ // null = not requested. true = requested with no preset (the agent then falls back to
54
+ // LOCAL_AI_*/config, defaulting to Ollama). A string is a validated preset name.
55
+ function resolveLocalAiTarget(opts) {
56
+ const raw = opts.localAi;
57
+ if (raw === undefined || raw === false) return null;
58
+ if (raw === true) return true;
59
+ if (!LOCAL_AI_TARGETS.includes(raw)) {
60
+ console.error(`❌ Unknown --local-ai target "${raw}" — one of: ${LOCAL_AI_TARGETS.join(', ')}.`);
61
+ console.error(' Any other OpenAI-compatible server works by URL instead: LOCAL_AI_URL=http://host:8000/v1');
62
+ process.exit(1);
63
+ }
64
+ return raw;
65
+ }
66
+
44
67
  const STACKS = {
45
68
  node: {
46
69
  label: 'Node.js / TypeScript',
@@ -86,7 +109,7 @@ const STACKS = {
86
109
  },
87
110
  // Frontend-only kits (UI-only: build STATE_CHANGE/STATE_VIEW slices, not
88
111
  // AUTOMATION — those belong to whichever backend stack is installed alongside).
89
- // react overrides lib/ralph.js (+ralph-claude.js/ralph-ollama.js/package.json/
112
+ // react overrides lib/ralph.js (+ralph-claude.js/ralph-local-ai.js/package.json/
90
113
  // README.md) for board-polling instead of the realtime channel every other
91
114
  // stack uses; supabase-react needs no overrides at all — it uses
92
115
  // shared/build-kit's realtime agent as-is. react's CLAUDE.md/build-*
@@ -199,9 +222,11 @@ const MCP_MANUAL_CLIENTS = [
199
222
  // directory tells it to go read the canonical .claude/skills/<name>/SKILL.md and
200
223
  // follow it — the same pattern spec-kitty uses (verified directly against its repo,
201
224
  // not just its docs: every "stub" host below reads plain Markdown, no per-host
202
- // format transform needed). Codex CLI, Mistral Vibe, Pi, and Letta Code share one
203
- // convention that already matches our native SKILL.md format, so those get the
204
- // real file copied as-is instead of a stub.
225
+ // format transform needed). Codex CLI, Mistral Vibe, Pi, Letta Code, and Hermes
226
+ // share one convention that already matches our native SKILL.md format, so those
227
+ // get the real file copied as-is instead of a stub. (Hermes also has a native
228
+ // .hermes/skills/ location, but its docs list .agents/skills/ as an equally
229
+ // first-class project skill root, so there's no reason to write the tree twice.)
205
230
  const AGENT_HOSTS = {
206
231
  cursor: { label: 'Cursor', dir: '.cursor/commands', kind: 'stub' },
207
232
  windsurf: { label: 'Windsurf', dir: '.windsurf/workflows', kind: 'stub' },
@@ -215,12 +240,33 @@ const AGENT_HOSTS = {
215
240
  augment: { label: 'Augment Code', dir: '.augment/commands', kind: 'stub' },
216
241
  antigravity: { label: 'Google Antigravity', dir: '.agent/workflows', kind: 'stub' },
217
242
  codex: {
218
- label: 'Codex CLI / Mistral Vibe / Pi / Letta Code (shared .agents/skills/ convention)',
243
+ label: 'Codex CLI / Mistral Vibe / Pi / Letta Code / Hermes (shared .agents/skills/ convention)',
219
244
  dir: '.agents/skills',
220
245
  kind: 'skill-package',
221
246
  },
222
247
  };
223
248
 
249
+ // Several hosts share one directory convention, so they resolve to a single
250
+ // canonical entry above instead of each getting their own: writing the same
251
+ // .agents/skills/ tree four times would be pure duplication, and `--hosts all`
252
+ // would print four "installed" lines for one install. These exist so people can
253
+ // name the agent they actually use — `--hosts pi` is a friendlier spelling of
254
+ // `--hosts codex`, not a different install.
255
+ const AGENT_HOST_ALIASES = {
256
+ pi: 'codex',
257
+ hermes: 'codex',
258
+ vibe: 'codex',
259
+ 'mistral-vibe': 'codex',
260
+ letta: 'codex',
261
+ 'letta-code': 'codex',
262
+ };
263
+
264
+ // Alias -> canonical, deduped and order-preserving. Unknown keys pass through
265
+ // untouched so the caller keeps reporting them as unknown.
266
+ function resolveAgentHostKeys(keys) {
267
+ return [...new Set(keys.map((k) => AGENT_HOST_ALIASES[k] || k))];
268
+ }
269
+
224
270
  function agentHostStub(skillName) {
225
271
  return `# ${skillName} (eventmodelers)\n\nThis host should read the canonical skill at:\n\n**\`.claude/skills/${skillName}/SKILL.md\`**\n\nFollow those instructions when this command is invoked.\n`;
226
272
  }
@@ -246,15 +292,18 @@ async function configureAgentHosts({ hosts, global: useGlobal } = {}) {
246
292
  if (!hostKeys || !hostKeys.length) {
247
293
  console.log('\nAvailable agent hosts:');
248
294
  Object.entries(AGENT_HOSTS).forEach(([key, h]) => console.log(` ${key.padEnd(12)} ${h.label}`));
295
+ const aliasKeys = Object.keys(AGENT_HOST_ALIASES);
296
+ if (aliasKeys.length) console.log(`\n (also accepted: ${aliasKeys.join(', ')})`);
249
297
  const answer = await prompt('\nWhich hosts? (comma-separated keys, or "all"): ');
250
298
  hostKeys = answer.trim() === 'all'
251
299
  ? Object.keys(AGENT_HOSTS)
252
300
  : answer.split(',').map((s) => s.trim()).filter(Boolean);
253
301
  }
302
+ hostKeys = resolveAgentHostKeys(hostKeys);
254
303
 
255
304
  const unknown = hostKeys.filter((k) => !AGENT_HOSTS[k]);
256
305
  if (unknown.length) {
257
- console.error(`❌ Unknown host(s): ${unknown.join(', ')}. Available: ${Object.keys(AGENT_HOSTS).join(', ')}`);
306
+ console.error(`❌ Unknown host(s): ${unknown.join(', ')}. Available: ${Object.keys(AGENT_HOSTS).join(', ')} (aliases: ${Object.keys(AGENT_HOST_ALIASES).join(', ')})`);
258
307
  process.exit(1);
259
308
  }
260
309
  if (!hostKeys.length) {
@@ -988,7 +1037,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
988
1037
  }
989
1038
 
990
1039
  // Make scripts executable
991
- for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-ollama.js']) {
1040
+ for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
992
1041
  const p = join(kitDir, script);
993
1042
  if (existsSync(p)) {
994
1043
  try { execSync(`chmod +x "${p}"`); } catch {}
@@ -1096,7 +1145,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
1096
1145
  } else if (isModelingKit) {
1097
1146
  console.log(' npx @eventmodelers/cli run --modeling\n');
1098
1147
  } else {
1099
- console.log(' npx @eventmodelers/cli run (--ollama or --bash for other runners)\n');
1148
+ console.log(' npx @eventmodelers/cli run (--local-ai or --bash for other runners)\n');
1100
1149
  }
1101
1150
  console.log('Connect this project to an MCP client (Claude Code, VS Code, ...):\n');
1102
1151
  console.log(` npx @eventmodelers/cli init-mcp\n`);
@@ -2798,7 +2847,7 @@ program
2798
2847
  program
2799
2848
  .command('init-agents')
2800
2849
  .description(`Expose installed skills to other AI agent hosts (${Object.keys(AGENT_HOSTS).join(', ')}) as thin stub commands pointing at the canonical .claude/skills/ files — no skill content duplicated per host`)
2801
- .option('--hosts <list>', `Comma-separated host keys (${Object.keys(AGENT_HOSTS).join(', ')})`)
2850
+ .option('--hosts <list>', `Comma-separated host keys (${Object.keys(AGENT_HOSTS).join(', ')}; aliases: ${Object.keys(AGENT_HOST_ALIASES).join(', ')})`)
2802
2851
  .option('--all', 'Expose to every known host')
2803
2852
  .option('--global', 'Read skills from ~/.claude/skills/ instead of the project')
2804
2853
  .action(async (opts) => {
@@ -3017,7 +3066,8 @@ credentialFlags(program
3017
3066
  credentialFlags(program
3018
3067
  .command('run')
3019
3068
  .description('Start the agent loop from the installed kit dir — build-kit stacks: ralph-claude.js (default); modeling-kit: --modeling, or --standalone, which needs no install at all')
3020
- .option('--ollama', 'Use ralph-ollama.js instead of the default Claude runner (build-kit stacks only)')
3069
+ .option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner, via ralph-local-ai.js (build-kit stacks only). Optional target preset picks the URL and wire dialect: ${LOCAL_AI_TARGETS.join(', ')} — bare --local-ai means ollama. Anything OpenAI-compatible (vLLM, LM Studio, llama.cpp, TGI) works by pointing LOCAL_AI_URL at it; see LOCAL_AI_* in the docs. Claude remains the default when this flag is absent.`)
3070
+ .option('--exec [command]', 'Hand each prompt to an external agent command instead of the default Claude runner, via ralph-exec.js (build-kit stacks only) — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto" or "opencode run". The prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent.')
3021
3071
  .option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
3022
3072
  .option('--modeling', 'Keep one Claude process warm across prompts instead of spawning a fresh one per task, for low-latency voice/live use. Runs from a modeling-kit install in this directory, or from the global install (~/.eventmodelers/kit) when there is none. Built into the CLI, not a per-project file.')
3023
3073
  .option('--standalone', 'Let the modeling agent work the board in the background, on its own initiative: on top of direct prompts it subscribes to the board\'s change channel (like the build agents do) and, whenever the board goes quiet after an edit — or has simply been idle for a while — it takes a turn nobody asked for. Changed nodes are a notification, not the task: it judges the model as a whole and fans the work out over parallel subagents, one per changed area (examples on a new node, specs for a new command or read model, a missing attribute along a chain, a screen, a question comment). Filling that detail in while the human keeps modeling is the point — it does not wait for the board to be finished. Implies --modeling.')
@@ -3071,13 +3121,13 @@ credentialFlags(program
3071
3121
  const bridgeKitDir = installedKitDirs.find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
3072
3122
  // A bridge kit is not a build-kit stand-in even though it also reuses
3073
3123
  // lib/ralph.js — it has its own `eventmodelers bridge` entrypoint (no
3074
- // onPlannedSlice/--ollama/--bash support), so it's excluded here rather
3124
+ // onPlannedSlice/--local-ai/--bash support), so it's excluded here rather
3075
3125
  // than falling through to the generic build-kit runner below.
3076
3126
  const buildKitDir = installedKitDirs.find((d) => d !== modelingKitDir && d !== bridgeKitDir) ?? null;
3077
3127
 
3078
3128
  // No overlap between the two stacks' runtimes: modeling-kit only ever runs the
3079
3129
  // warm, direct-dispatch loop (--modeling); build-kit only ever runs the
3080
- // cold-spawn/tasks.json loop (default, or --ollama/--bash). Neither falls back
3130
+ // cold-spawn/tasks.json loop (default, or --local-ai/--bash). Neither falls back
3081
3131
  // to the other's mechanism, so each side is gated explicitly below rather than
3082
3132
  // just being left to fail on a missing file.
3083
3133
  // --standalone implies --modeling: it already refused every other runner, so there
@@ -3086,8 +3136,8 @@ credentialFlags(program
3086
3136
  // no meaning for a build kit, which is scaffolded per project by definition.
3087
3137
  if (opts.modeling || opts.standalone || opts.global) {
3088
3138
  const picked = opts.modeling ? '--modeling' : opts.standalone ? '--standalone' : '--global';
3089
- if (opts.bash || opts.ollama) {
3090
- console.error(`❌ ${picked} is mutually exclusive with --bash/--ollama — those select a build-kit runner, which the modeling loop has no use for.`);
3139
+ if (opts.bash || opts.localAi || opts.exec) {
3140
+ console.error(`❌ ${picked} is mutually exclusive with --bash/--local-ai/--exec — those select a build-kit runner, which the modeling loop has no use for.`);
3091
3141
  process.exit(1);
3092
3142
  }
3093
3143
  if (opts.local) {
@@ -3136,7 +3186,7 @@ credentialFlags(program
3136
3186
  if (modelingKitDir) {
3137
3187
  console.error(`❌ A modeling-kit install (${MODELING_KIT.kitDirName}/) only runs via \`eventmodelers run --modeling\` — there is no cold-spawn/tasks.json loop for modeling-only projects.`);
3138
3188
  } else if (bridgeKitDir) {
3139
- console.error(`❌ A bridge-kit install (${BRIDGE_KIT.kitDirName}/) only runs via \`eventmodelers bridge\` — it has no --modeling/--ollama/--bash modes.`);
3189
+ console.error(`❌ A bridge-kit install (${BRIDGE_KIT.kitDirName}/) only runs via \`eventmodelers bridge\` — it has no --modeling/--local-ai/--bash modes.`);
3140
3190
  } else {
3141
3191
  console.error(`❌ No kit installed in ${cwd} — run \`eventmodelers install\` first.`);
3142
3192
  console.error(' (A modeling agent needs no install at all: eventmodelers run --standalone --board-id <uuid>)');
@@ -3145,9 +3195,10 @@ credentialFlags(program
3145
3195
  }
3146
3196
  const kitDir = buildKitDir;
3147
3197
 
3148
- const pickedCount = [opts.bash, opts.ollama].filter(Boolean).length;
3198
+ const localAiTarget = resolveLocalAiTarget(opts);
3199
+ const pickedCount = [opts.bash, localAiTarget !== null, !!opts.exec].filter(Boolean).length;
3149
3200
  if (pickedCount > 1) {
3150
- console.error('❌ --bash and --ollama are mutually exclusive — pick one.');
3201
+ console.error('❌ --bash, --local-ai and --exec are mutually exclusive — pick one runner.');
3151
3202
  process.exit(1);
3152
3203
  }
3153
3204
 
@@ -3155,7 +3206,7 @@ credentialFlags(program
3155
3206
  // is just a thin dispatcher so users don't have to remember the kit-dir name or which
3156
3207
  // runner file to invoke. Users (and the agent itself, via AGENT.md) may customize these
3157
3208
  // files freely; `run` always executes whatever is currently on disk.
3158
- const runner = opts.bash ? 'ralph.sh' : opts.ollama ? 'ralph-ollama.js' : 'ralph-claude.js';
3209
+ const runner = opts.bash ? 'ralph.sh' : opts.exec ? 'ralph-exec.js' : localAiTarget !== null ? 'ralph-local-ai.js' : 'ralph-claude.js';
3159
3210
  const runnerPath = join(kitDir, runner);
3160
3211
  if (!existsSync(runnerPath)) {
3161
3212
  console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
@@ -3165,13 +3216,13 @@ credentialFlags(program
3165
3216
  console.log(`▶ Starting ${relative(cwd, runnerPath)}...\n`);
3166
3217
  const cmd = runner.endsWith('.sh') ? `"${runnerPath}"` : `node "${runnerPath}"`;
3167
3218
  try {
3168
- // Only ralph-claude.js reads RALPH_VERBOSE — the bash loop and the ollama executor have
3219
+ // Only ralph-claude.js reads RALPH_VERBOSE — the bash loop and the local-AI executor have
3169
3220
  // their own separate output paths with no stream-json parsing to gate. RALPH_LOCAL is
3170
3221
  // read by all three runners (ralph.js's startRalph, and ralph.sh directly) to force the
3171
3222
  // local-only branch even when .eventmodelers/config.json has valid credentials.
3172
3223
  // RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
3173
3224
  // they reach both node runners but not ralph.sh, which has no heartbeat to identify.
3174
- execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '' } });
3225
+ execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}), ...(typeof opts.exec === 'string' ? { RALPH_EXEC_CMD: opts.exec } : {}) } });
3175
3226
  } catch (err) {
3176
3227
  process.exit(err.status || 1);
3177
3228
  }
@@ -3179,8 +3230,8 @@ credentialFlags(program
3179
3230
 
3180
3231
  program
3181
3232
  .command('bridge')
3182
- .description('Start the bridge agent loop from the installed .bridge-kit/ — translates board slice changes into another spec framework instead of building code. A deterministic adapter runs with no LLM call if one exists for the configured target (e.g. spec-kitty); otherwise Claude is the default executor. --ollama, --hook, or --claude override the pick.')
3183
- .option('--ollama', 'Use ralph-ollama.js instead of the default runner')
3233
+ .description('Start the bridge agent loop from the installed .bridge-kit/ — translates board slice changes into another spec framework instead of building code. A deterministic adapter runs with no LLM call if one exists for the configured target (e.g. spec-kitty); otherwise Claude is the default executor. --local-ai, --hook, or --claude override the pick.')
3234
+ .option('--local-ai [target]', `Use ralph-local-ai.js instead of the default runner — a local or self-hosted model. Optional target preset: ${LOCAL_AI_TARGETS.join(', ')} (bare flag means ollama).`)
3184
3235
  .option('--hook <command>', 'Run this shell command instead of an AI agent for each batch of slice changes (e.g. commit + push .slices/ for a CI pipeline to pick up) — overrides any hook persisted via `init --bridge --hook` for this run only')
3185
3236
  .option('--claude', 'Force the Claude runner even if a static adapter exists for this target')
3186
3237
  .action((opts) => {
@@ -3191,8 +3242,9 @@ program
3191
3242
  process.exit(1);
3192
3243
  }
3193
3244
 
3194
- if ([opts.ollama, opts.hook, opts.claude].filter(Boolean).length > 1) {
3195
- console.error('❌ --ollama, --hook, and --claude are mutually exclusive — pick one executor.');
3245
+ const localAiTarget = resolveLocalAiTarget(opts);
3246
+ if ([localAiTarget !== null, opts.hook, opts.claude].filter(Boolean).length > 1) {
3247
+ console.error('❌ --local-ai, --hook, and --claude are mutually exclusive — pick one executor.');
3196
3248
  process.exit(1);
3197
3249
  }
3198
3250
 
@@ -3212,8 +3264,8 @@ program
3212
3264
 
3213
3265
  const runner = hookCmd
3214
3266
  ? 'ralph-hook.js'
3215
- : opts.ollama
3216
- ? 'ralph-ollama.js'
3267
+ : localAiTarget !== null
3268
+ ? 'ralph-local-ai.js'
3217
3269
  : !opts.claude && hasStaticAdapter
3218
3270
  ? 'ralph-static.js'
3219
3271
  : 'ralph-claude.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.74",
3
+ "version": "1.0.76",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,8 +8,11 @@ Ralph's runtime directory. Contains the agent loop, realtime subscription, promp
8
8
  # Claude (default)
9
9
  node .build-kit/ralph-claude.js
10
10
 
11
- # Local Ollama model — run `ollama serve` first
12
- OLLAMA_MODEL=qwen3.5:9b node .build-kit/ralph-ollama.js
11
+ # Local or self-hosted model — Ollama (run `ollama serve` first)
12
+ LOCAL_AI_TARGET=ollama node .build-kit/ralph-local-ai.js
13
+
14
+ # …or any OpenAI-compatible server (vLLM, LM Studio, llama.cpp, TGI)
15
+ LOCAL_AI_TARGET=vllm LOCAL_AI_MODEL=Qwen/Qwen3-8B node .build-kit/ralph-local-ai.js
13
16
 
14
17
  # Custom project directory (defaults to the parent of .build-kit)
15
18
  node .build-kit/ralph-claude.js /path/to/project
@@ -22,7 +25,8 @@ node .build-kit/ralph-claude.js /path/to/project
22
25
  | File | Purpose |
23
26
  |------|---------|
24
27
  | `ralph-claude.js` | Runs the full loop using Claude Code as the executor |
25
- | `ralph-ollama.js` | Runs the full loop using a local Ollama model |
28
+ | `ralph-local-ai.js` | Runs the full loop using a local/self-hosted model (Ollama, vLLM, LM Studio, llama.cpp) |
29
+ | `ralph-exec.js` | Runs the full loop handing each prompt to an external agent command (Codex CLI, OpenCode, …) |
26
30
  | `ralph.sh` | Shell-based loop — alternative to the JS entry points |
27
31
  | `realtime-agent.js` | Standalone realtime agent — only needed to run it in a separate terminal |
28
32
 
@@ -31,7 +35,7 @@ node .build-kit/ralph-claude.js /path/to/project
31
35
  | File | Purpose |
32
36
  |------|---------|
33
37
  | `lib/ralph.js` | Shared library — realtime agent + loop logic; imported by the entry points |
34
- | `lib/ollama-agent.js` | Ollama executor — called by `ralph-ollama.js`, can also run manually |
38
+ | `lib/local-ai-agent.js` | Local-AI executor — called by `ralph-local-ai.js`, can also run manually |
35
39
  | `lib/agent.sh` | Thin shell wrapper around `claude` — called by `ralph.sh` |
36
40
  | `lib/prompt.md` | Phase 1 prompt: tells Claude how to load a slice from the board |
37
41
  | `lib/backend-prompt.md` | Phase 2 prompt: tells Claude how to build a planned slice |
@@ -41,13 +45,13 @@ node .build-kit/ralph-claude.js /path/to/project
41
45
 
42
46
  **Phase 1** — triggered when `tasks.json` has entries:
43
47
  - The realtime agent writes a task to `tasks.json` each time a `slice:changed` event arrives from the board
44
- - The loop picks it up and runs Claude (or Ollama) with `prompt.md`
48
+ - The loop picks it up and runs Claude (or a local model) with `prompt.md`
45
49
  - Claude loads the slice data and updates `.slices/`
46
50
 
47
51
  **Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
48
52
  - The loop runs Claude with `backend-prompt.md`
49
53
  - Claude implements the slice in the project
50
- - Phase 2 is Claude-only; Ollama mode skips it (ollama-agent handles its own queue)
54
+ - Phase 2 is Claude-only; local-AI mode skips it (local-ai-agent handles its own queue)
51
55
 
52
56
  Both phases run in a continuous loop with a 3-second idle sleep. The realtime agent runs concurrently in the same process.
53
57
 
@@ -63,13 +67,64 @@ node .build-kit/realtime-agent.js
63
67
  .build-kit/ralph.sh
64
68
  ```
65
69
 
66
- ## Ollama configuration
70
+ ## Local-AI configuration
71
+
72
+ `ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
73
+ Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
74
+
75
+ ```bash
76
+ LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
77
+ LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
78
+ LOCAL_AI_MODEL=qwen3.5:9b # model name as the server knows it
79
+ LOCAL_AI_API=openai # force the wire dialect: ollama | openai (normally inferred)
80
+ LOCAL_AI_API_KEY=local # sent as `Authorization: Bearer` on the openai dialect
81
+ LOCAL_AI_NUM_CTX=32768 # ollama only — context window (default 32768)
82
+ ```
83
+
84
+ **Do not lower `LOCAL_AI_NUM_CTX`.** The MCP tool schemas are ~16k tokens on their own.
85
+ Ollama's own default is 4096, which silently truncates them — the model then sees a
86
+ fragment of the tool list and invents tool names instead of failing, which is why the
87
+ default here is raised rather than left to the server. On the `openai` dialect the
88
+ equivalent is set when you launch the server (vLLM `--max-model-len 32768`,
89
+ llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
90
+
91
+
92
+ ## External agent commands (`--exec`)
93
+
94
+ Agentic harnesses that bring their own tool loop — Codex CLI, OpenCode, Gemini CLI —
95
+ are not `--local-ai` targets: `--local-ai` *supplies* the agent loop, while a harness
96
+ already is one and only wants a prompt. They go through `ralph-exec.js` instead:
67
97
 
68
98
  ```bash
69
- OLLAMA_MODEL=qwen3.5:9b # model to use (default: qwen3.5:9b)
70
- OLLAMA_URL=http://host:11434 # Ollama server URL (default: http://localhost:11434)
99
+ npx @eventmodelers/cli run --exec "codex exec --full-auto"
100
+ npx @eventmodelers/cli run --exec "opencode run"
101
+
102
+ # …or persist it and use the bare flag
103
+ RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
71
104
  ```
72
105
 
106
+ The prompt is appended to the command as one shell-quoted argument, and is also written
107
+ to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
108
+ child runs with the project dir as its cwd and inherits stdio — a harness owns its own
109
+ output format, so there is no condensed per-step logging here the way `ralph-claude.js`
110
+ has it.
111
+
112
+ Persist a default alongside the local-AI settings:
113
+
114
+ ```json
115
+ {
116
+ "localAi": {
117
+ "exec": "codex exec --full-auto"
118
+ }
119
+ }
120
+ ```
121
+
122
+ One caveat worth knowing before reaching for this: the kits' prompts assume Claude
123
+ Code's `Skill` tool and `CLAUDE.md`. Other harnesses read `AGENTS.md` and have no skill
124
+ primitive, so `init-agents` puts the skill files where they can find them, but
125
+ `lib/prompt.md` / `lib/backend-prompt.md` still need wording that says *read and follow*
126
+ a skill file rather than *invoke* it.
127
+
73
128
  ## Config
74
129
 
75
130
  Credentials are stored in `.build-kit/.eventmodelers/config.json` (written by `eventmodelers init`):