@eventmodelers/cli 1.0.73 → 1.0.75

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 (67) hide show
  1. package/README.md +15 -8
  2. package/cli.js +47 -29
  3. package/package.json +2 -2
  4. package/shared/build-kit/README.md +26 -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-local-ai.js +40 -0
  8. package/shared/build-kit/realtime-agent.js +1 -1
  9. package/shared/skills/learn-eventmodelers-api/SKILL.md +4 -2
  10. package/stacks/blank/templates/.claude/skills/build-automation/SKILL.md +1 -1
  11. package/stacks/blank/templates/.claude/skills/build-state-change/SKILL.md +1 -1
  12. package/stacks/blank/templates/.claude/skills/build-state-view/SKILL.md +1 -1
  13. package/stacks/blank/templates/root/README.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/modeling-kit/templates/kit/README.md +1 -1
  17. package/stacks/react/templates/.claude/skills/build-automation/SKILL.md +1 -1
  18. package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +1 -1
  19. package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +1 -1
  20. package/stacks/react/templates/build-kit/README.md +26 -9
  21. package/stacks/react/templates/build-kit/lib/ralph.js +1 -1
  22. package/stacks/react/templates/build-kit/ralph-local-ai.js +40 -0
  23. package/shared/build-kit/lib/ollama-agent.js +0 -147
  24. package/shared/build-kit/ralph-ollama.js +0 -40
  25. package/stacks/bridge/templates/bridge/ralph-ollama.js +0 -43
  26. package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +0 -251
  27. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +0 -122
  28. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +0 -115
  29. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +0 -191
  30. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +0 -234
  31. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +0 -149
  32. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +0 -166
  33. package/stacks/cratis-csharp/templates/build-kit/CLAUDE.md +0 -78
  34. package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +0 -59
  35. package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +0 -140
  36. package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +0 -126
  37. package/stacks/cratis-csharp/templates/root/.frontend/index.css +0 -29
  38. package/stacks/cratis-csharp/templates/root/.frontend/index.html +0 -17
  39. package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +0 -18
  40. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +0 -42
  41. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +0 -11
  42. package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +0 -56
  43. package/stacks/cratis-csharp/templates/root/App.tsx +0 -23
  44. package/stacks/cratis-csharp/templates/root/CratisApp.csproj +0 -25
  45. package/stacks/cratis-csharp/templates/root/CratisApp.sln +0 -18
  46. package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +0 -3
  47. package/stacks/cratis-csharp/templates/root/Home.tsx +0 -102
  48. package/stacks/cratis-csharp/templates/root/Program.cs +0 -26
  49. package/stacks/cratis-csharp/templates/root/README.md +0 -192
  50. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +0 -47
  51. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +0 -11
  52. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +0 -12
  53. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +0 -17
  54. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +0 -1
  55. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +0 -51
  56. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +0 -18
  57. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +0 -27
  58. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +0 -1
  59. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +0 -22
  60. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +0 -3
  61. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +0 -1
  62. package/stacks/cratis-csharp/templates/root/appsettings.Development.json +0 -9
  63. package/stacks/cratis-csharp/templates/root/appsettings.json +0 -26
  64. package/stacks/cratis-csharp/templates/root/docker-compose.yml +0 -23
  65. package/stacks/cratis-csharp/templates/root/package.json +0 -33
  66. package/stacks/cratis-csharp/templates/root/tsconfig.json +0 -3
  67. package/stacks/react/templates/build-kit/ralph-ollama.js +0 -40
package/README.md CHANGED
@@ -18,7 +18,6 @@ Running without `--stack` shows an arrow-key picker. Or go straight to a stack:
18
18
  npx @eventmodelers/cli init --stack node # Node.js / TypeScript
19
19
  npx @eventmodelers/cli init --stack supabase # Supabase
20
20
  npx @eventmodelers/cli init --stack axon # Axon Framework (Java/Kotlin)
21
- npx @eventmodelers/cli init --stack cratis-csharp # Cratis (.NET/C#)
22
21
  npx @eventmodelers/cli init --stack opencqrs # OpenCQRS (Java, EventSourcingDB)
23
22
  npx @eventmodelers/cli init --stack umadb # UmaDB (Java)
24
23
  npx @eventmodelers/cli init --stack kurrent # Kurrent (Java, KurrentDB)
@@ -79,7 +78,7 @@ your-project/
79
78
  │ └── config.json ← your token + org/board (gitignored) — shared by every kit in this project
80
79
  ├── .build-kit/ ← agent runner (name is .agent-modeling-kit/ for the modeling-kit stack)
81
80
  │ ├── ralph-claude.js ← realtime agent + task loop
82
- │ ├── ralph-ollama.js ← same, via local Ollama
81
+ │ ├── ralph-local-ai.js ← same, via a local/self-hosted model
83
82
  │ ├── ralph.sh ← bash-only loop (no realtime)
84
83
  │ ├── lib/ ← stack-specific agent prompts + helpers
85
84
  │ └── .slices/ ← board slices, written by `fetch`/`listen` (or pre-seeded by `init --demo`)
@@ -89,11 +88,11 @@ your-project/
89
88
  └── CLAUDE.md ← agent instructions
90
89
  ```
91
90
 
92
- The seven backend stacks (`node`, `supabase`, `axon`, `cratis-csharp`, `opencqrs`, `umadb`, `kurrent`) also scaffold a real project skeleton into your project root (`templates/root/`) — source layout, build files, migrations, etc.
91
+ The six backend stacks (`node`, `supabase`, `axon`, `opencqrs`, `umadb`, `kurrent`) also scaffold a real project skeleton into your project root (`templates/root/`) — source layout, build files, migrations, etc.
93
92
 
94
93
  `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.
95
94
 
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-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.
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-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.
97
96
 
98
97
  ## Skills
99
98
 
@@ -125,7 +124,7 @@ npx @eventmodelers/cli init --stack <name> # scaffold a stack + install
125
124
  npx @eventmodelers/cli init --stack <name> --demo # same, plus a ready-made demo model in the kit's .slices/ to build against
126
125
  npx @eventmodelers/cli re-init # refresh an already-installed kit's scripts/skills only — never touches the root scaffold
127
126
  npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
128
- npx @eventmodelers/cli run --ollama # same, via local Ollama (ralph-ollama.js)
127
+ npx @eventmodelers/cli run --local-ai [target] # same, via a local/self-hosted model (ralph-local-ai.js)
129
128
  npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
130
129
  npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
131
130
  npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
@@ -162,11 +161,19 @@ npx @eventmodelers/cli init --build-kit # blank build-kit scaffold
162
161
  **Building a new kit for an unsupported stack:**
163
162
 
164
163
  ```bash
165
- npx @eventmodelers/cli init --build-kit
164
+ npx @eventmodelers/cli init --build-kit --demo
166
165
  ```
167
166
 
168
167
  This scaffolds `.build-kit/CLAUDE.md`, `lib/prompt.md`, `lib/backend-prompt.md`, and the `build-*` skills with TODO placeholders instead of real content. Fill in the TODOs against the actual stack you're integrating (build/test commands, file layout, framework idioms) while building something real with it, then follow "Adding a stack" below to promote it to a first-class stack once it works.
169
168
 
169
+ `--demo` is what gives you something to build *against* while you do that: it seeds the kit's `.slices/` with the ready-made 16-slice **Understanding Eventsourcing** model (see "Trying it out before you have a board of your own" above), so you don't need a board, credentials, or a connected project to exercise the kit you're writing. With it in place the whole loop is:
170
+
171
+ ```bash
172
+ npx @eventmodelers/cli run --local
173
+ ```
174
+
175
+ and the agent starts building — `--local` skips platform config and credential lookup entirely, so it works straight out of `init` with nothing connected. Every TODO you fill in gets exercised on the next iteration against real slice data covering all four slice types (state change, state view, automation, translation), which is exactly the coverage a new stack's `build-*` skills need before it's worth promoting.
176
+
170
177
  Installing both a build stack and `init-modeling` into the same project reuses this one `.eventmodelers/config.json` — run whichever `init` command second and it finds the existing config already satisfies the required fields and skips straight past the credential prompt.
171
178
 
172
179
  ### The modeling agent — `run --modeling` and `--standalone`
@@ -328,7 +335,7 @@ npx @eventmodelers/cli bridge
328
335
 
329
336
  `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.
330
337
 
331
- 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).
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 `--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).
332
339
 
333
340
  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:
334
341
 
@@ -347,7 +354,7 @@ npx @eventmodelers/cli init --bridge --target spec-kitty --hook "git add .slices
347
354
  npx @eventmodelers/cli bridge
348
355
  ```
349
356
 
350
- `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.
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 — `--local-ai`, `--hook`, and `--claude` are mutually exclusive.
351
358
 
352
359
  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.
353
360
 
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.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',
@@ -63,13 +86,6 @@ const STACKS = {
63
86
  useShared: true,
64
87
  needsBoardId: true,
65
88
  },
66
- 'cratis-csharp': {
67
- label: 'Cratis (.NET/C#)',
68
- kitSubdir: 'build-kit',
69
- kitDirName: '.build-kit',
70
- useShared: true,
71
- needsBoardId: true,
72
- },
73
89
  opencqrs: {
74
90
  label: 'OpenCQRS (Java, EventSourcingDB)',
75
91
  kitSubdir: 'build-kit',
@@ -93,7 +109,7 @@ const STACKS = {
93
109
  },
94
110
  // Frontend-only kits (UI-only: build STATE_CHANGE/STATE_VIEW slices, not
95
111
  // AUTOMATION — those belong to whichever backend stack is installed alongside).
96
- // 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/
97
113
  // README.md) for board-polling instead of the realtime channel every other
98
114
  // stack uses; supabase-react needs no overrides at all — it uses
99
115
  // shared/build-kit's realtime agent as-is. react's CLAUDE.md/build-*
@@ -995,7 +1011,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
995
1011
  }
996
1012
 
997
1013
  // Make scripts executable
998
- for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-ollama.js']) {
1014
+ for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-local-ai.js']) {
999
1015
  const p = join(kitDir, script);
1000
1016
  if (existsSync(p)) {
1001
1017
  try { execSync(`chmod +x "${p}"`); } catch {}
@@ -1103,7 +1119,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
1103
1119
  } else if (isModelingKit) {
1104
1120
  console.log(' npx @eventmodelers/cli run --modeling\n');
1105
1121
  } else {
1106
- console.log(' npx @eventmodelers/cli run (--ollama or --bash for other runners)\n');
1122
+ console.log(' npx @eventmodelers/cli run (--local-ai or --bash for other runners)\n');
1107
1123
  }
1108
1124
  console.log('Connect this project to an MCP client (Claude Code, VS Code, ...):\n');
1109
1125
  console.log(` npx @eventmodelers/cli init-mcp\n`);
@@ -3024,7 +3040,7 @@ credentialFlags(program
3024
3040
  credentialFlags(program
3025
3041
  .command('run')
3026
3042
  .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')
3027
- .option('--ollama', 'Use ralph-ollama.js instead of the default Claude runner (build-kit stacks only)')
3043
+ .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.`)
3028
3044
  .option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
3029
3045
  .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.')
3030
3046
  .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.')
@@ -3078,13 +3094,13 @@ credentialFlags(program
3078
3094
  const bridgeKitDir = installedKitDirs.find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
3079
3095
  // A bridge kit is not a build-kit stand-in even though it also reuses
3080
3096
  // lib/ralph.js — it has its own `eventmodelers bridge` entrypoint (no
3081
- // onPlannedSlice/--ollama/--bash support), so it's excluded here rather
3097
+ // onPlannedSlice/--local-ai/--bash support), so it's excluded here rather
3082
3098
  // than falling through to the generic build-kit runner below.
3083
3099
  const buildKitDir = installedKitDirs.find((d) => d !== modelingKitDir && d !== bridgeKitDir) ?? null;
3084
3100
 
3085
3101
  // No overlap between the two stacks' runtimes: modeling-kit only ever runs the
3086
3102
  // warm, direct-dispatch loop (--modeling); build-kit only ever runs the
3087
- // cold-spawn/tasks.json loop (default, or --ollama/--bash). Neither falls back
3103
+ // cold-spawn/tasks.json loop (default, or --local-ai/--bash). Neither falls back
3088
3104
  // to the other's mechanism, so each side is gated explicitly below rather than
3089
3105
  // just being left to fail on a missing file.
3090
3106
  // --standalone implies --modeling: it already refused every other runner, so there
@@ -3093,8 +3109,8 @@ credentialFlags(program
3093
3109
  // no meaning for a build kit, which is scaffolded per project by definition.
3094
3110
  if (opts.modeling || opts.standalone || opts.global) {
3095
3111
  const picked = opts.modeling ? '--modeling' : opts.standalone ? '--standalone' : '--global';
3096
- if (opts.bash || opts.ollama) {
3097
- console.error(`❌ ${picked} is mutually exclusive with --bash/--ollama — those select a build-kit runner, which the modeling loop has no use for.`);
3112
+ if (opts.bash || opts.localAi) {
3113
+ console.error(`❌ ${picked} is mutually exclusive with --bash/--local-ai — those select a build-kit runner, which the modeling loop has no use for.`);
3098
3114
  process.exit(1);
3099
3115
  }
3100
3116
  if (opts.local) {
@@ -3143,7 +3159,7 @@ credentialFlags(program
3143
3159
  if (modelingKitDir) {
3144
3160
  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.`);
3145
3161
  } else if (bridgeKitDir) {
3146
- console.error(`❌ A bridge-kit install (${BRIDGE_KIT.kitDirName}/) only runs via \`eventmodelers bridge\` — it has no --modeling/--ollama/--bash modes.`);
3162
+ console.error(`❌ A bridge-kit install (${BRIDGE_KIT.kitDirName}/) only runs via \`eventmodelers bridge\` — it has no --modeling/--local-ai/--bash modes.`);
3147
3163
  } else {
3148
3164
  console.error(`❌ No kit installed in ${cwd} — run \`eventmodelers install\` first.`);
3149
3165
  console.error(' (A modeling agent needs no install at all: eventmodelers run --standalone --board-id <uuid>)');
@@ -3152,9 +3168,10 @@ credentialFlags(program
3152
3168
  }
3153
3169
  const kitDir = buildKitDir;
3154
3170
 
3155
- const pickedCount = [opts.bash, opts.ollama].filter(Boolean).length;
3171
+ const localAiTarget = resolveLocalAiTarget(opts);
3172
+ const pickedCount = [opts.bash, localAiTarget !== null].filter(Boolean).length;
3156
3173
  if (pickedCount > 1) {
3157
- console.error('❌ --bash and --ollama are mutually exclusive — pick one.');
3174
+ console.error('❌ --bash and --local-ai are mutually exclusive — pick one.');
3158
3175
  process.exit(1);
3159
3176
  }
3160
3177
 
@@ -3162,7 +3179,7 @@ credentialFlags(program
3162
3179
  // is just a thin dispatcher so users don't have to remember the kit-dir name or which
3163
3180
  // runner file to invoke. Users (and the agent itself, via AGENT.md) may customize these
3164
3181
  // files freely; `run` always executes whatever is currently on disk.
3165
- const runner = opts.bash ? 'ralph.sh' : opts.ollama ? 'ralph-ollama.js' : 'ralph-claude.js';
3182
+ const runner = opts.bash ? 'ralph.sh' : localAiTarget !== null ? 'ralph-local-ai.js' : 'ralph-claude.js';
3166
3183
  const runnerPath = join(kitDir, runner);
3167
3184
  if (!existsSync(runnerPath)) {
3168
3185
  console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
@@ -3172,13 +3189,13 @@ credentialFlags(program
3172
3189
  console.log(`▶ Starting ${relative(cwd, runnerPath)}...\n`);
3173
3190
  const cmd = runner.endsWith('.sh') ? `"${runnerPath}"` : `node "${runnerPath}"`;
3174
3191
  try {
3175
- // Only ralph-claude.js reads RALPH_VERBOSE — the bash loop and the ollama executor have
3192
+ // Only ralph-claude.js reads RALPH_VERBOSE — the bash loop and the local-AI executor have
3176
3193
  // their own separate output paths with no stream-json parsing to gate. RALPH_LOCAL is
3177
3194
  // read by all three runners (ralph.js's startRalph, and ralph.sh directly) to force the
3178
3195
  // local-only branch even when .eventmodelers/config.json has valid credentials.
3179
3196
  // RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
3180
3197
  // they reach both node runners but not ralph.sh, which has no heartbeat to identify.
3181
- 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 ?? '' } });
3198
+ 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 } : {}) } });
3182
3199
  } catch (err) {
3183
3200
  process.exit(err.status || 1);
3184
3201
  }
@@ -3186,8 +3203,8 @@ credentialFlags(program
3186
3203
 
3187
3204
  program
3188
3205
  .command('bridge')
3189
- .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.')
3190
- .option('--ollama', 'Use ralph-ollama.js instead of the default runner')
3206
+ .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.')
3207
+ .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).`)
3191
3208
  .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')
3192
3209
  .option('--claude', 'Force the Claude runner even if a static adapter exists for this target')
3193
3210
  .action((opts) => {
@@ -3198,8 +3215,9 @@ program
3198
3215
  process.exit(1);
3199
3216
  }
3200
3217
 
3201
- if ([opts.ollama, opts.hook, opts.claude].filter(Boolean).length > 1) {
3202
- console.error('❌ --ollama, --hook, and --claude are mutually exclusive — pick one executor.');
3218
+ const localAiTarget = resolveLocalAiTarget(opts);
3219
+ if ([localAiTarget !== null, opts.hook, opts.claude].filter(Boolean).length > 1) {
3220
+ console.error('❌ --local-ai, --hook, and --claude are mutually exclusive — pick one executor.');
3203
3221
  process.exit(1);
3204
3222
  }
3205
3223
 
@@ -3219,8 +3237,8 @@ program
3219
3237
 
3220
3238
  const runner = hookCmd
3221
3239
  ? 'ralph-hook.js'
3222
- : opts.ollama
3223
- ? 'ralph-ollama.js'
3240
+ : localAiTarget !== null
3241
+ ? 'ralph-local-ai.js'
3224
3242
  : !opts.claude && hasStaticAdapter
3225
3243
  ? 'ralph-static.js'
3226
3244
  : 'ralph-claude.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.73",
4
- "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
3
+ "version": "1.0.75",
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": {
7
7
  "eventmodelers": "cli.js"
@@ -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,7 @@ 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) |
26
29
  | `ralph.sh` | Shell-based loop — alternative to the JS entry points |
27
30
  | `realtime-agent.js` | Standalone realtime agent — only needed to run it in a separate terminal |
28
31
 
@@ -31,7 +34,7 @@ node .build-kit/ralph-claude.js /path/to/project
31
34
  | File | Purpose |
32
35
  |------|---------|
33
36
  | `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 |
37
+ | `lib/local-ai-agent.js` | Local-AI executor — called by `ralph-local-ai.js`, can also run manually |
35
38
  | `lib/agent.sh` | Thin shell wrapper around `claude` — called by `ralph.sh` |
36
39
  | `lib/prompt.md` | Phase 1 prompt: tells Claude how to load a slice from the board |
37
40
  | `lib/backend-prompt.md` | Phase 2 prompt: tells Claude how to build a planned slice |
@@ -41,13 +44,13 @@ node .build-kit/ralph-claude.js /path/to/project
41
44
 
42
45
  **Phase 1** — triggered when `tasks.json` has entries:
43
46
  - 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`
47
+ - The loop picks it up and runs Claude (or a local model) with `prompt.md`
45
48
  - Claude loads the slice data and updates `.slices/`
46
49
 
47
50
  **Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
48
51
  - The loop runs Claude with `backend-prompt.md`
49
52
  - Claude implements the slice in the project
50
- - Phase 2 is Claude-only; Ollama mode skips it (ollama-agent handles its own queue)
53
+ - Phase 2 is Claude-only; local-AI mode skips it (local-ai-agent handles its own queue)
51
54
 
52
55
  Both phases run in a continuous loop with a 3-second idle sleep. The realtime agent runs concurrently in the same process.
53
56
 
@@ -63,13 +66,27 @@ node .build-kit/realtime-agent.js
63
66
  .build-kit/ralph.sh
64
67
  ```
65
68
 
66
- ## Ollama configuration
69
+ ## Local-AI configuration
70
+
71
+ `ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
72
+ Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
67
73
 
68
74
  ```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)
75
+ LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
76
+ LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
77
+ LOCAL_AI_MODEL=qwen3.5:9b # model name as the server knows it
78
+ LOCAL_AI_API=openai # force the wire dialect: ollama | openai (normally inferred)
79
+ LOCAL_AI_API_KEY=local # sent as `Authorization: Bearer` on the openai dialect
80
+ LOCAL_AI_NUM_CTX=32768 # ollama only — context window (default 32768)
71
81
  ```
72
82
 
83
+ **Do not lower `LOCAL_AI_NUM_CTX`.** The MCP tool schemas are ~16k tokens on their own.
84
+ Ollama's own default is 4096, which silently truncates them — the model then sees a
85
+ fragment of the tool list and invents tool names instead of failing, which is why the
86
+ default here is raised rather than left to the server. On the `openai` dialect the
87
+ equivalent is set when you launch the server (vLLM `--max-model-len 32768`,
88
+ llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
89
+
73
90
  ## Config
74
91
 
75
92
  Credentials are stored in `.build-kit/.eventmodelers/config.json` (written by `eventmodelers init`):