@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.
- package/README.md +7 -5
- package/cli.js +80 -28
- package/package.json +1 -1
- package/shared/build-kit/README.md +64 -9
- package/shared/build-kit/lib/local-ai-agent.js +281 -0
- package/shared/build-kit/lib/ralph.js +1 -1
- package/shared/build-kit/ralph-exec.js +92 -0
- package/shared/build-kit/ralph-local-ai.js +40 -0
- package/shared/build-kit/realtime-agent.js +1 -1
- package/shared/skills/learn-eventmodelers-api/SKILL.md +93 -43
- package/shared/skills/update-slice-status/SKILL.md +8 -0
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/bridge/templates/bridge/lib/AGENT.md +1 -1
- package/stacks/bridge/templates/bridge/ralph-local-ai.js +43 -0
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +11 -1
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +8 -3
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/react/templates/build-kit/README.md +64 -9
- package/stacks/react/templates/build-kit/lib/prompt.md +1 -1
- package/stacks/react/templates/build-kit/lib/ralph.js +1 -1
- package/stacks/react/templates/build-kit/ralph-local-ai.js +40 -0
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -1
- package/shared/build-kit/lib/ollama-agent.js +0 -147
- package/shared/build-kit/ralph-ollama.js +0 -40
- package/stacks/bridge/templates/bridge/ralph-ollama.js +0 -43
- 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-
|
|
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-
|
|
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 --
|
|
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 `--
|
|
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 — `--
|
|
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-
|
|
35
|
-
// code-export.mjs, lib/agent.sh, lib/
|
|
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-
|
|
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,
|
|
203
|
-
// convention that already matches our native SKILL.md format, so those
|
|
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-
|
|
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 (--
|
|
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('--
|
|
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/--
|
|
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 --
|
|
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.
|
|
3090
|
-
console.error(`❌ ${picked} is mutually exclusive with --bash/--
|
|
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/--
|
|
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
|
|
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 --
|
|
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.
|
|
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
|
|
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. --
|
|
3183
|
-
.option('--
|
|
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
|
-
|
|
3195
|
-
|
|
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
|
-
:
|
|
3216
|
-
? 'ralph-
|
|
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.
|
|
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
|
|
12
|
-
|
|
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-
|
|
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/
|
|
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
|
|
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;
|
|
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
|
-
##
|
|
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
|
-
|
|
70
|
-
|
|
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`):
|