@eventmodelers/cli 1.0.85 → 1.0.87

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.
@@ -17,7 +17,8 @@
17
17
  // RALPH_EXEC_CMD="opencode run" node ralph-exec.js
18
18
  // Or persist it as localAi.exec in .eventmodelers/config.json.
19
19
 
20
- import { startRalph, loadLocalConfig } from './lib/ralph.js';
20
+ import { startRalph, loadLocalConfig, resolveAgentIdentity } from './lib/ralph.js';
21
+ import { createSliceTracer, usageFromHarnessEvent, sumUsage } from './lib/tracing.js';
21
22
  import { spawn } from 'child_process';
22
23
  import { writeFileSync, mkdtempSync } from 'fs';
23
24
  import { tmpdir } from 'os';
@@ -29,6 +30,8 @@ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir,
29
30
 
30
31
  const cfg = loadLocalConfig(kitDir);
31
32
  const localOnly = process.env.RALPH_LOCAL === '1';
33
+ // Same as ralph-claude.js: childEnv is built at load time, before startRalph resolves identity.
34
+ Object.assign(cfg, resolveAgentIdentity(kitDir, 'BUILD', cfg));
32
35
  const execCmd = process.env.RALPH_EXEC_CMD || cfg.localAi?.exec;
33
36
 
34
37
  if (!execCmd) {
@@ -50,6 +53,21 @@ const childEnv = {
50
53
  ...(cfg.agentId && !localOnly ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
51
54
  };
52
55
 
56
+ // What each slice build turn cost — see lib/tracing.js. Same opt-in as ralph-claude.js, but a
57
+ // harness only reports usage in its JSON output mode (`codex exec --json`, `opencode run --format
58
+ // json`, `gemini --output-format stream-json`), so without that flag a turn has nothing to record.
59
+ const tracingOn = !localOnly && cfg.agentTracing === true;
60
+ if (!tracingOn && !localOnly) console.log('[ralph-exec] agent tracing off — no slice cost data is sent (turn on: eventmodelers config --agent-tracing on)');
61
+ const tracer = createSliceTracer({
62
+ baseUrl: cfg.baseUrl,
63
+ token: cfg.token,
64
+ organizationId: cfg.organizationId,
65
+ agentId: cfg.agentId,
66
+ traceFile: join(projectDir, '.eventmodelers', 'trace', 'slices.jsonl'),
67
+ log: (line) => console.log(`[ralph-exec] ${line}`),
68
+ enabled: tracingOn,
69
+ });
70
+
53
71
  const promptDir = mkdtempSync(join(tmpdir(), 'ralph-exec-'));
54
72
 
55
73
  // POSIX single-quote escaping: close, insert an escaped quote, reopen. The prompt is
@@ -60,22 +78,59 @@ function shellQuote(s) {
60
78
 
61
79
  console.log(`[ralph-exec] command: ${execCmd}`);
62
80
 
63
- function runExec(prompt) {
81
+ // Parses a harness's stdout for usage while still passing it through. Most harnesses emit JSONL;
82
+ // Gemini's `--output-format json` is one pretty-printed object, hence the whole-output fallback.
83
+ function collectUsage(stream) {
84
+ const parts = [];
85
+ let buffer = '';
86
+ let all = '';
87
+ const take = (text) => {
88
+ try { const u = usageFromHarnessEvent(JSON.parse(text)); if (u) parts.push(u); } catch { /* not JSON */ }
89
+ };
90
+ stream.on('data', (chunk) => {
91
+ process.stdout.write(chunk);
92
+ const text = chunk.toString();
93
+ all += text;
94
+ buffer += text;
95
+ const lines = buffer.split('\n');
96
+ buffer = lines.pop();
97
+ for (const line of lines) if (line.trim()) take(line);
98
+ });
99
+ return () => {
100
+ if (buffer.trim()) take(buffer);
101
+ if (!parts.length && all.trim()) take(all);
102
+ return sumUsage(parts);
103
+ };
104
+ }
105
+
106
+ // `slice` is set for a planned-slice build (see startRalph) — the only turns that are traced.
107
+ function runExec(prompt, slice = null) {
64
108
  return new Promise((resolvePromise, reject) => {
65
109
  const full = inlineHeader + prompt;
66
110
  const promptFile = join(promptDir, 'prompt.md');
67
111
  writeFileSync(promptFile, full);
112
+ const traced = tracingOn && !!slice;
113
+ const startedAt = Date.now();
68
114
 
69
115
  // stdio inherit: the harness owns its own output format, and there is no
70
116
  // cross-harness stream schema to parse into the condensed per-step logging
71
- // that ralph-claude.js does — so it goes straight through.
117
+ // that ralph-claude.js does — so it goes straight through. A traced turn pipes
118
+ // stdout instead, only to read usage from it; the output is still echoed as is.
72
119
  const proc = spawn(`${execCmd} ${shellQuote(full)}`, {
73
120
  cwd: projectDir,
74
- stdio: 'inherit',
121
+ stdio: ['inherit', traced ? 'pipe' : 'inherit', 'inherit'],
75
122
  shell: true,
76
123
  env: { ...childEnv, RALPH_PROMPT_FILE: promptFile },
77
124
  });
78
- proc.on('close', (code) => (code === 0 ? resolvePromise() : reject(new Error(`exec command exited ${code}`))));
125
+ const usage = traced ? collectUsage(proc.stdout) : null;
126
+ proc.on('close', (code) => {
127
+ if (traced) {
128
+ const u = usage();
129
+ if (u) tracer.record({ ...slice, status: code === 0 ? 'ok' : 'error' }, { ...u, durationMs: u.durationMs ?? Date.now() - startedAt });
130
+ else console.log('[ralph-exec] trace: no usage in the command output — run the harness in its JSON output mode to record slice cost');
131
+ }
132
+ code === 0 ? resolvePromise() : reject(new Error(`exec command exited ${code}`));
133
+ });
79
134
  proc.on('error', reject);
80
135
  });
81
136
  }
@@ -63,7 +63,6 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
63
63
  | `render_screen` | `boardId`, `nodeId`, `elements[]?` (SCREEN) or `pages[]?`+`backgroundColor?` (HTML_SCREEN), `description?` | Update an existing screen's content — exactly one of `elements`/`pages` | §4 `POST .../images/:id/sketch` + `image-nodes` |
64
64
  | `add_field_examples` | `boardId`, `nodeId?`, `name?`, `cellName?`, `timelineId?` | Fill empty field examples using linked-node context | — (MCP-only convenience) |
65
65
  | `get_attribute_chain` | `boardId`, `timelineId`, `targetCellName`, `sourceCellName` | Resolve every node between two cells, ordered target→source. Both cell names must come from a node's reported `cellName` (`get_board_outline`, `get_nodes`, `get_node`) — a guessed address that happens to exist silently resolves the wrong chain | — (MCP-only convenience) |
66
- | `verify_screen` | `boardId`, `nodeId` | Check a screen node exists and has rendered content — works for both SCREEN and HTML_SCREEN, dispatching on the node's actual type | — (MCP-only convenience) |
67
66
  | `get_image_snapshot_description` | `boardId`, `nodeId` | Load the `{elements:[...]}` sketch description from storage | — (reads what §4 sketch endpoints write) |
68
67
  | `validate_slice_data` | `sliceData` | Offline validation of a `SliceDataOutput` payload — no board access | — (MCP-only, pure function) |
69
68
  | `commit_board_to_git` | `boardId` | Force a git-extension commit/push, bypassing the autoCommit gate | — (MCP-only; git extension) |
@@ -231,6 +231,7 @@ mcp__eventmodelers__create_screen {
231
231
  "nodeId": "<node-uuid>",
232
232
  "chapterId": "<CHAPTER_ID>",
233
233
  "cellId": "<actorRowId>-<columnId>",
234
+ "title": "<Screen Title>",
234
235
  "pages": ["<div>...</div>"],
235
236
  "description": "<concise description of what this screen shows>",
236
237
  "fields": [ /* per "Mandatory Field Definitions" below — set in this same call */ ]
@@ -240,7 +241,7 @@ mcp__eventmodelers__create_screen {
240
241
 
241
242
  **Fallback (no MCP):** see `references/api-fallback.md` — "Mandatory Screen Rendering — Step B: Create the HTML_SCREEN node".
242
243
 
243
- The MCP `create_screen` call above already sets `meta.fields` (per "Mandatory Field Definitions" below) in the same call — no separate `node:changed` follow-up needed when using MCP.
244
+ The MCP `create_screen` call above already sets the node's `title` and `meta.fields` (per "Mandatory Field Definitions" below) in the same call — no separate `node:changed` follow-up needed when using MCP.
244
245
 
245
246
  A storyboard screen is placed at a *provisional* position — Steps 4 and 5 wire it to its COMMAND / READMODEL once those exist, and may move it first. Pass `autoConnect: false` on `create_screen` here so the placement doesn't pre-wire the screen to whatever happens to sit in the adjacent column; the real `SCREEN → COMMAND` and `READMODEL → SCREEN` edges are created deliberately in Steps 4 and 5. When creating several screens whose HTML is already authored, put them all in one `create_screen` call's `screens` array with `autoConnect: false`.
246
247
 
@@ -140,6 +140,7 @@ mcp__eventmodelers__create_screen {
140
140
  "nodeId": "<generated-uuid>",
141
141
  "chapterId": "<CHAPTER_ID>",
142
142
  "cellName": "<CELL_NAME>",
143
+ "title": "<Screen Title>",
143
144
  "pages": ["<div>...</div>"],
144
145
  "fields": [ /* optional — set meta.fields here too (see Step 5) instead of a separate call */ ]
145
146
  }]
@@ -1,134 +0,0 @@
1
- # .build-kit
2
-
3
- Ralph's runtime directory. Contains the agent loop, board poller, prompts, and Claude skills.
4
-
5
- > This is the **react** (API-access) stack — it talks to the board purely through
6
- > the plain REST `slicedata` endpoint on `api.eventmodelers.ai`, polled on an interval.
7
- > There is no Supabase/PocketBase realtime subscription and no direct database table
8
- > access anywhere in this kit. For instant push notifications instead of polling, use
9
- > the `supabase-react` stack.
10
-
11
- ## Quick start
12
-
13
- ```bash
14
- # Claude (default)
15
- node .build-kit/ralph-claude.js
16
-
17
- # Local or self-hosted model — Ollama (run `ollama serve` first)
18
- LOCAL_AI_TARGET=ollama node .build-kit/ralph-local-ai.js
19
-
20
- # …or any OpenAI-compatible server (vLLM, LM Studio, llama.cpp, TGI)
21
- LOCAL_AI_TARGET=vllm LOCAL_AI_MODEL=Qwen/Qwen3-8B node .build-kit/ralph-local-ai.js
22
-
23
- # Custom project directory (defaults to the parent of .build-kit)
24
- node .build-kit/ralph-claude.js /path/to/project
25
- ```
26
-
27
- ## Files
28
-
29
- **Entry points** (top level):
30
-
31
- | File | Purpose |
32
- |------|---------|
33
- | `ralph-claude.js` | Runs the full loop using Claude Code as the executor |
34
- | `ralph-local-ai.js` | Runs the full loop using a local/self-hosted model (Ollama, vLLM, LM Studio, llama.cpp) |
35
- | `ralph-exec.js` | Runs the full loop handing each prompt to an external agent command (Codex CLI, OpenCode, …) |
36
- | `ralph.sh` | Shell-based loop — alternative to the JS entry points |
37
-
38
- **Internals** (`lib/`):
39
-
40
- | File | Purpose |
41
- |------|---------|
42
- | `lib/ralph.js` | Shared library — board poller + loop logic; imported by the entry points |
43
- | `lib/local-ai-agent.js` | Local-AI executor — called by `ralph-local-ai.js`, can also run manually |
44
- | `lib/agent.sh` | Thin shell wrapper around `claude` — called by `ralph.sh` |
45
- | `lib/prompt.md` | Phase 1 prompt: tells Claude how to load a slice from the board |
46
- | `lib/backend-prompt.md` | Phase 2 prompt: tells Claude how to build a planned slice |
47
- | `lib/AGENT.md` | Agent instructions included in Claude's context |
48
-
49
- ## How it works
50
-
51
- **Phase 1** — triggered when `tasks.json` has entries:
52
- - The poller writes a task to `tasks.json` each time it notices a slice's status changed since the last poll
53
- - The loop picks it up and runs Claude (or a local model) with `prompt.md`
54
- - Claude loads the slice data and updates `.slices/`
55
-
56
- **Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
57
- - The loop runs Claude with `backend-prompt.md`
58
- - Claude implements the slice in the project
59
- - Phase 2 is Claude-only; local-AI mode skips it (local-ai-agent handles its own queue)
60
-
61
- Both phases run in a continuous loop with a 3-second idle sleep. The board poller runs concurrently in the same process, re-fetching `slicedata/slices` every `RALPH_POLL_INTERVAL_MS` (default 10s — see `lib/ralph.js`).
62
-
63
- ## Local-AI configuration
64
-
65
- `ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
66
- Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
67
-
68
- ```bash
69
- LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
70
- LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
71
- LOCAL_AI_MODEL=qwen3.5:9b # model name as the server knows it
72
- LOCAL_AI_API=openai # force the wire dialect: ollama | openai (normally inferred)
73
- LOCAL_AI_API_KEY=local # sent as `Authorization: Bearer` on the openai dialect
74
- LOCAL_AI_NUM_CTX=32768 # ollama only — context window (default 32768)
75
- ```
76
-
77
- **Do not lower `LOCAL_AI_NUM_CTX`.** The MCP tool schemas are ~16k tokens on their own.
78
- Ollama's own default is 4096, which silently truncates them — the model then sees a
79
- fragment of the tool list and invents tool names instead of failing, which is why the
80
- default here is raised rather than left to the server. On the `openai` dialect the
81
- equivalent is set when you launch the server (vLLM `--max-model-len 32768`,
82
- llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
83
-
84
-
85
- ## External agent commands (`--exec`)
86
-
87
- Agentic harnesses that bring their own tool loop — Codex CLI, OpenCode, Gemini CLI —
88
- are not `--local-ai` targets: `--local-ai` *supplies* the agent loop, while a harness
89
- already is one and only wants a prompt. They go through `ralph-exec.js` instead:
90
-
91
- ```bash
92
- npx @eventmodelers/cli run --exec "codex exec --full-auto"
93
- npx @eventmodelers/cli run --exec "opencode run"
94
-
95
- # …or persist it and use the bare flag
96
- RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
97
- ```
98
-
99
- The prompt is appended to the command as one shell-quoted argument, and is also written
100
- to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
101
- child runs with the project dir as its cwd and inherits stdio — a harness owns its own
102
- output format, so there is no condensed per-step logging here the way `ralph-claude.js`
103
- has it.
104
-
105
- Persist a default alongside the local-AI settings:
106
-
107
- ```json
108
- {
109
- "localAi": {
110
- "exec": "codex exec --full-auto"
111
- }
112
- }
113
- ```
114
-
115
- One caveat worth knowing before reaching for this: the kits' prompts assume Claude
116
- Code's `Skill` tool and `CLAUDE.md`. Other harnesses read `AGENTS.md` and have no skill
117
- primitive, so `init-agents` puts the skill files where they can find them, but
118
- `lib/prompt.md` / `lib/backend-prompt.md` still need wording that says *read and follow*
119
- a skill file rather than *invoke* it.
120
-
121
- ## Config
122
-
123
- Credentials are stored in `.build-kit/.eventmodelers/config.json` (written by `eventmodelers init`):
124
-
125
- ```json
126
- {
127
- "organizationId": "...",
128
- "boardId": "...",
129
- "token": "...",
130
- "baseUrl": "https://api.eventmodelers.ai"
131
- }
132
- ```
133
-
134
- Claude skills live in `.build-kit/.claude/skills/` and are available inside any Claude Code session started from `.build-kit/`.