@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.
- package/README.md +2 -2
- package/cli.js +109 -11
- package/package.json +1 -1
- package/shared/build-kit/README.md +43 -1
- package/shared/build-kit/lib/ralph.js +40 -11
- package/shared/build-kit/lib/tracing.js +155 -0
- package/shared/build-kit/ralph-claude.js +23 -3
- package/shared/build-kit/ralph-exec.js +60 -5
- package/shared/skills/learn-eventmodelers-api/SKILL.md +0 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +2 -1
- package/stacks/modeling-kit/templates/.claude/skills/html-screen/SKILL.md +1 -0
- package/stacks/react/templates/build-kit/README.md +0 -134
- package/stacks/react/templates/build-kit/lib/ralph.js +0 -517
- package/stacks/react/templates/build-kit/package.json +0 -9
- package/stacks/react/templates/build-kit/ralph-claude.js +0 -107
- package/stacks/react/templates/build-kit/ralph-local-ai.js +0 -40
package/README.md
CHANGED
|
@@ -125,8 +125,8 @@ npx @eventmodelers/cli init --stack <name> # scaffold a stack + install
|
|
|
125
125
|
npx @eventmodelers/cli init --stack <name> --demo # same, plus a ready-made demo model in the kit's .slices/ to build against
|
|
126
126
|
npx @eventmodelers/cli re-init # refresh an already-installed kit's scripts/skills only — never touches the root scaffold
|
|
127
127
|
npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
|
|
128
|
-
npx @eventmodelers/cli run --local-ai [target] #
|
|
129
|
-
npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js)
|
|
128
|
+
npx @eventmodelers/cli run --local-ai [target] # board work only via a local/self-hosted model (ralph-local-ai.js) — does NOT build slices; build locally with --exec
|
|
129
|
+
npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js) — agent tracing needs its JSON flag, e.g. "codex exec --json --full-auto"
|
|
130
130
|
npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
|
|
131
131
|
npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
|
|
132
132
|
npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
|
package/cli.js
CHANGED
|
@@ -32,13 +32,14 @@ const __dirname = dirname(__filename);
|
|
|
32
32
|
|
|
33
33
|
// Each stack is a template set under stacks/<key>/templates/{.claude,root,<kitSubdir>}.
|
|
34
34
|
// Stacks with useShared:true also get shared/build-kit/* copied into their kit dir
|
|
35
|
-
//
|
|
35
|
+
// (ralph.js, ralph-claude.js, ralph-local-ai.js, ralph-exec.js, ralph.sh, realtime-agent.js,
|
|
36
36
|
// code-export.mjs, lib/agent.sh, lib/local-ai-agent.js, package.json, README.md) —
|
|
37
37
|
// those files have no per-stack content, so they live once instead of being
|
|
38
38
|
// copy-pasted into every stack (that copy-pasting is exactly how they drifted out
|
|
39
39
|
// of sync before: a bugfix or default landing in one stack's copy but not another's).
|
|
40
|
-
// Each stack's own templates/<kitSubdir>/*
|
|
41
|
-
//
|
|
40
|
+
// Each stack's own templates/<kitSubdir>/* supplies the genuine per-stack files
|
|
41
|
+
// (lib/prompt.md, lib/backend-prompt.md, lib/AGENT.md, checks, etc.); shared/build-kit/*
|
|
42
|
+
// is copied after it, so the shared runtime always wins over any copy a stack still ships.
|
|
42
43
|
// modeling-kit (below) is the one kit that opts out of all of this (useShared:false)
|
|
43
44
|
// — it has no cold-spawn/tasks.json runtime at all, so none of shared/build-kit/*
|
|
44
45
|
// applies to it; see its own templates/kit for its (much smaller) self-contained set.
|
|
@@ -1011,10 +1012,13 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1011
1012
|
mkdirSync(kitDir, { recursive: true });
|
|
1012
1013
|
console.log(`📦 Installing agent kit into ${stackCfg.kitDirName}/...`);
|
|
1013
1014
|
|
|
1015
|
+
copyDirContents(join(templatesSource, stackCfg.kitSubdir), kitDir, { skip: ['.eventmodelers'] });
|
|
1016
|
+
// Shared runtime goes on LAST so it always wins: a stack (built-in or --git) that still
|
|
1017
|
+
// ships its own ralph.js / ralph-claude.js / ... is shipping a stale copy, and letting it
|
|
1018
|
+
// overlay the shared one silently drops runtime fixes (e.g. agent_id on the heartbeat).
|
|
1014
1019
|
if (stackCfg.useShared) {
|
|
1015
1020
|
copyDirContents(sharedBuildKit, kitDir);
|
|
1016
1021
|
}
|
|
1017
|
-
copyDirContents(join(templatesSource, stackCfg.kitSubdir), kitDir, { skip: ['.eventmodelers'] });
|
|
1018
1022
|
|
|
1019
1023
|
// An outdated community/--git stack that still ships root/CLAUDE.md (the pre-fix
|
|
1020
1024
|
// layout every built-in stack used to follow too) gets it relocated here instead
|
|
@@ -1122,6 +1126,8 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1122
1126
|
// placeholder, never the literal secret (see connect/SKILL.md's Security notes).
|
|
1123
1127
|
ensureMcpRegistered(targetDir, config.baseUrl || DEFAULT_BASE_URL);
|
|
1124
1128
|
ensureEnvToken(targetDir, config.token);
|
|
1129
|
+
// Only build stacks trace (slice build cost); they are the ones running the shared runtime.
|
|
1130
|
+
if (stackCfg.useShared) await configureAgentTracing(configPath, options.print);
|
|
1125
1131
|
}
|
|
1126
1132
|
|
|
1127
1133
|
// --- 6. Install manifest (drives precise `uninstall` later) ---
|
|
@@ -1154,6 +1160,23 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1154
1160
|
console.log(` npx @eventmodelers/cli init-agents\n`);
|
|
1155
1161
|
}
|
|
1156
1162
|
|
|
1163
|
+
// `agentTracing` in config.json: whether a build agent reports what each slice build cost to the
|
|
1164
|
+
// platform (see agent-tracking.md). Asked once per project; off unless the user opts in — unset
|
|
1165
|
+
// (e.g. --print installs) is off, and off means the runner sends nothing.
|
|
1166
|
+
// `reask` (a bare `init-config`) asks again even when it is already set, defaulting to the
|
|
1167
|
+
// current answer.
|
|
1168
|
+
async function configureAgentTracing(configPath, print, reask = false) {
|
|
1169
|
+
const current = readJsonSafe(configPath);
|
|
1170
|
+
if (print || (typeof current.agentTracing === 'boolean' && !reask)) return;
|
|
1171
|
+
const agentTracing = await selectPrompt('Should EM-Studio track the agent cost of each slice?', [
|
|
1172
|
+
{ label: 'Yes — track the cost per slice', value: true },
|
|
1173
|
+
{ label: 'No — send nothing', value: false },
|
|
1174
|
+
], current.agentTracing === true ? 0 : 1);
|
|
1175
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
1176
|
+
writeFileSync(configPath, JSON.stringify({ ...current, agentTracing }, null, 2));
|
|
1177
|
+
console.log(` ✓ Agent tracing ${agentTracing ? 'on' : 'off'} — change it with \`eventmodelers config --agent-tracing on|off\``);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1157
1180
|
// Extracted from installStack so `init-config` can reuse the exact same
|
|
1158
1181
|
// paste/manual/instructions/skip flow without also scaffolding a stack.
|
|
1159
1182
|
// `overrides` are values passed directly on the command line (--token, --board-id,
|
|
@@ -1254,6 +1277,43 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
|
|
|
1254
1277
|
return config;
|
|
1255
1278
|
}
|
|
1256
1279
|
|
|
1280
|
+
// `re-init` for a build kit: re-copy shared/build-kit (ralph.js, the runners, tracing, adapters)
|
|
1281
|
+
// over the installed kit, then re-run the config steps. Stack templates and skills are untouched.
|
|
1282
|
+
async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
|
|
1283
|
+
console.log(`📦 Refreshing the shared runtime in ${relative(targetDir, kitDir)}/ (stack files untouched)...`);
|
|
1284
|
+
copyDirContents(join(__dirname, 'shared', 'build-kit'), kitDir);
|
|
1285
|
+
for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
|
|
1286
|
+
const p = join(kitDir, script);
|
|
1287
|
+
if (existsSync(p)) {
|
|
1288
|
+
try { execSync(`chmod +x "${p}"`); } catch {}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
console.log('📦 Installing kit dependencies...');
|
|
1292
|
+
try {
|
|
1293
|
+
execSync('npm install', { cwd: kitDir, stdio: ['ignore', 'inherit', 'inherit'] });
|
|
1294
|
+
} catch {
|
|
1295
|
+
console.error(' ⚠️ npm install failed in kit — run it manually');
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
console.log('🔐 Configuring...');
|
|
1299
|
+
const configPath = globalOpts.config
|
|
1300
|
+
? resolve(targetDir, globalOpts.config)
|
|
1301
|
+
: join(targetDir, '.eventmodelers', 'config.json');
|
|
1302
|
+
const overrides = { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) };
|
|
1303
|
+
const cfg = await configureCredentials({
|
|
1304
|
+
config: loadEffectiveConfig(targetDir, kitDir, globalOpts.config).config,
|
|
1305
|
+
configPath,
|
|
1306
|
+
targetDir,
|
|
1307
|
+
requiredFields: ['organizationId', 'boardId', 'token'],
|
|
1308
|
+
overrides,
|
|
1309
|
+
print: globalOpts.print,
|
|
1310
|
+
force: opts.force,
|
|
1311
|
+
});
|
|
1312
|
+
ensureMcpRegistered(targetDir, cfg.baseUrl || DEFAULT_BASE_URL);
|
|
1313
|
+
ensureEnvToken(targetDir, cfg.token);
|
|
1314
|
+
await configureAgentTracing(configPath, globalOpts.print, true);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1257
1317
|
// Configure the MCP server registration for a project — split out of `init` into
|
|
1258
1318
|
// its own command (`init-mcp`) since not every harness/workflow wants an
|
|
1259
1319
|
// automatic .claude/settings.json edit or an interactive "connect elsewhere?"
|
|
@@ -1594,16 +1654,22 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
|
|
|
1594
1654
|
// common case to one keystroke. Skipped when there is no one to ask (--print, or a
|
|
1595
1655
|
// non-interactive stdin such as CI or a process supervisor), where the resolved value
|
|
1596
1656
|
// stands on its own exactly as before.
|
|
1657
|
+
//
|
|
1658
|
+
// The answer may also be the whole credentials blob from the account page — it names the
|
|
1659
|
+
// board too, so it answers this question and the "where do its credentials come from" one
|
|
1660
|
+
// below in a single paste.
|
|
1597
1661
|
let boardChosen = !!(explicit.boardId || process.env.EVENTMODELERS_BOARD_ID);
|
|
1662
|
+
let pasted = null;
|
|
1598
1663
|
if (!boardChosen && interactive) {
|
|
1599
1664
|
const answer = await prompt(boardId ? `\n Board ID [${boardId}]: ` : '\n Board ID: ');
|
|
1600
1665
|
if (answer) {
|
|
1601
|
-
|
|
1666
|
+
pasted = parseCredentialsPaste(answer, ['organizationId', 'token', 'boardId']);
|
|
1667
|
+
boardId = pasted ? pasted.boardId : answer;
|
|
1602
1668
|
boardChosen = true;
|
|
1603
1669
|
}
|
|
1604
1670
|
}
|
|
1605
1671
|
|
|
1606
|
-
let stored = boardId ? readJsonSafe(boardCredentialsPath(boardId)) : {};
|
|
1672
|
+
let stored = pasted ?? (boardId ? readJsonSafe(boardCredentialsPath(boardId)) : {});
|
|
1607
1673
|
|
|
1608
1674
|
// Follow a pointer left by an earlier answer: this directory (or the account config) keeps
|
|
1609
1675
|
// resolving to one board, but the credentials pasted for it named another. Without this the
|
|
@@ -1625,7 +1691,7 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
|
|
|
1625
1691
|
// implied — explicit credentials on the command line — or when there is no one to ask:
|
|
1626
1692
|
// --print, or a non-interactive stdin such as CI or a supervisor that would otherwise
|
|
1627
1693
|
// hang here forever (those keep using whatever is on file, silently).
|
|
1628
|
-
if (interactive && !credentialsNamed) {
|
|
1694
|
+
if (!pasted && interactive && !credentialsNamed) {
|
|
1629
1695
|
const hasAccountWide = !!(walked.token && walked.organizationId);
|
|
1630
1696
|
|
|
1631
1697
|
// What "keep" would keep. A pointer entry is deliberately not offered: it says the
|
|
@@ -2836,9 +2902,9 @@ const REINITIABLE_STACKS = { ...STACKS, [MODELING_KIT.key]: MODELING_KIT, [BLANK
|
|
|
2836
2902
|
|
|
2837
2903
|
credentialFlags(program
|
|
2838
2904
|
.command('re-init')
|
|
2839
|
-
.description(
|
|
2905
|
+
.description("Refresh an installed kit from the current CLI version. For a build kit: re-copies this CLI's shared runtime (ralph.js, ralph-claude.js, tracing, adapters) into .build-kit and re-runs the config (credentials unless complete, agent tracing) — the stack's own files and skills are left alone, so this works for --git stacks too. --stack <name> instead refreshes everything from that built-in stack; --modeling refreshes .agent-modeling-kit. Never touches the project root scaffold or the root CLAUDE.md router.")
|
|
2840
2906
|
.option('--modeling', 'Refresh the modeling kit (.agent-modeling-kit) instead of a build kit')
|
|
2841
|
-
.option('--stack <name>', `
|
|
2907
|
+
.option('--stack <name>', `Also refresh the stack's own files and skills from this built-in stack (${Object.keys(REINITIABLE_STACKS).join(', ')}) — or switch a .build-kit install to a different stack. Without it, only the shared runtime and the config are refreshed`)
|
|
2842
2908
|
.option('--hooks', 'Install the slice commit-scope guard (.githooks/pre-commit) and wire it up via `git config core.hooksPath .githooks` — same as `init --hooks`, for turning it on after the fact without a full re-scaffold. Off by default.')
|
|
2843
2909
|
.option('--global', 'Re-install skills into ~/.claude/skills/ instead of the project — defaults to however they were originally installed')
|
|
2844
2910
|
.option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json')
|
|
@@ -2866,6 +2932,14 @@ credentialFlags(program
|
|
|
2866
2932
|
}
|
|
2867
2933
|
|
|
2868
2934
|
const manifest = readJsonSafe(join(kitDir, '.eventmodelers', 'install-manifest.json'));
|
|
2935
|
+
|
|
2936
|
+
// A build kit without --stack: refresh only what this CLI owns — the shared runtime and the
|
|
2937
|
+
// config — and never the stack's own files. Works for any stack, --git ones included.
|
|
2938
|
+
if (!opts.modeling && !opts.stack) {
|
|
2939
|
+
await refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts });
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2869
2943
|
const stackKey = opts.modeling ? MODELING_KIT.key : (opts.stack || manifest.stack);
|
|
2870
2944
|
const stackCfg = stackKey ? REINITIABLE_STACKS[stackKey] : null;
|
|
2871
2945
|
|
|
@@ -3110,14 +3184,17 @@ credentialFlags(program
|
|
|
3110
3184
|
// (see the beta-api protected-resource-metadata incident this fixed).
|
|
3111
3185
|
ensureMcpRegistered(targetDir, cfg.baseUrl || DEFAULT_BASE_URL);
|
|
3112
3186
|
ensureEnvToken(targetDir, cfg.token);
|
|
3187
|
+
if (existsSync(join(targetDir, STACKS.node.kitDirName))) {
|
|
3188
|
+
await configureAgentTracing(configPath, globalOpts.print, !Object.values(overrides).some(Boolean));
|
|
3189
|
+
}
|
|
3113
3190
|
}
|
|
3114
3191
|
});
|
|
3115
3192
|
|
|
3116
3193
|
credentialFlags(program
|
|
3117
3194
|
.command('run')
|
|
3118
3195
|
.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')
|
|
3119
|
-
.option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner: a build kit runs it via ralph-local-ai.js, and --modeling/--standalone via lib/modeling-local-ai.js (board tools over MCP, but no skills and no subagent fan-out — those are Claude Code features). 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.`)
|
|
3120
|
-
.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.')
|
|
3196
|
+
.option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner: a build kit runs it via ralph-local-ai.js, and --modeling/--standalone via lib/modeling-local-ai.js (board tools over MCP, but no skills and no subagent fan-out — those are Claude Code features). 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. Board work only: --local-ai never builds a Planned slice (no file, shell or git tools). To build code with a local model, use --exec with a coding harness pointed at it, e.g. "codex exec --oss --full-auto". Claude remains the default when this flag is absent.`)
|
|
3197
|
+
.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. With agent tracing on, include the harness\'s JSON output flag (codex exec --json, opencode run --format json, gemini --output-format stream-json) — without it no slice cost is recorded.')
|
|
3121
3198
|
.option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
|
|
3122
3199
|
.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.')
|
|
3123
3200
|
.option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal — right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--global).')
|
|
@@ -3205,6 +3282,12 @@ credentialFlags(program
|
|
|
3205
3282
|
// credentials resolved from flags/env/~/.eventmodelers — so the current directory is
|
|
3206
3283
|
// neither read nor written, and the command works from anywhere.
|
|
3207
3284
|
let kitDir = opts.global ? null : modelingKitDir;
|
|
3285
|
+
// A pre-config.js install (the old ralph-based kit) shares the dir name but can't
|
|
3286
|
+
// drive this loop — fall through to the global install rather than refusing to run.
|
|
3287
|
+
if (kitDir && !existsSync(join(kitDir, 'lib', 'config.js'))) {
|
|
3288
|
+
console.log(`ℹ️ Ignoring ${relative(cwd, kitDir)} — an outdated kit without lib/config.js (run \`init --modeling\` here to replace it). Using the global install.\n`);
|
|
3289
|
+
kitDir = null;
|
|
3290
|
+
}
|
|
3208
3291
|
let projectDir = kitDir ? resolve(kitDir, '..') : null;
|
|
3209
3292
|
let overrides = null;
|
|
3210
3293
|
if (!kitDir) {
|
|
@@ -3824,10 +3907,25 @@ program
|
|
|
3824
3907
|
program
|
|
3825
3908
|
.command('config')
|
|
3826
3909
|
.description('Print the fully resolved config (merged across the directory hierarchy + EVENTMODELERS_* env vars), with the token masked')
|
|
3910
|
+
.option('--agent-tracing <on|off>', "Turn agent tracing on or off for this project (saved as `agentTracing` in the project's .eventmodelers/config.json). Off: the build agent sends no slice cost data to the platform at all. A non-Claude agent via run --exec is only traced when its command includes the harness's JSON output flag, e.g. \"codex exec --json --full-auto\".")
|
|
3827
3911
|
.action((opts, command) => {
|
|
3828
3912
|
const cwd = process.cwd();
|
|
3829
3913
|
const kitDir = findInstalledKitDir(cwd);
|
|
3830
3914
|
const explicitConfig = command.optsWithGlobals().config;
|
|
3915
|
+
|
|
3916
|
+
if (opts.agentTracing !== undefined) {
|
|
3917
|
+
const value = String(opts.agentTracing).toLowerCase();
|
|
3918
|
+
if (!['on', 'off'].includes(value)) {
|
|
3919
|
+
console.error('❌ --agent-tracing takes "on" or "off"');
|
|
3920
|
+
process.exit(1);
|
|
3921
|
+
}
|
|
3922
|
+
const configPath = explicitConfig
|
|
3923
|
+
? resolve(explicitConfig)
|
|
3924
|
+
: join(kitDir ? dirname(kitDir) : cwd, '.eventmodelers', 'config.json');
|
|
3925
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
3926
|
+
writeFileSync(configPath, JSON.stringify({ ...readJsonSafe(configPath), agentTracing: value === 'on' }, null, 2));
|
|
3927
|
+
console.log(`✓ Agent tracing ${value} — saved to ${relative(cwd, configPath) || configPath}\n`);
|
|
3928
|
+
}
|
|
3831
3929
|
const { sources, config } = loadEffectiveConfig(cwd, kitDir, explicitConfig);
|
|
3832
3930
|
|
|
3833
3931
|
const resolved = { ...config };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.87",
|
|
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": {
|
|
@@ -51,7 +51,7 @@ node .build-kit/ralph-claude.js /path/to/project
|
|
|
51
51
|
**Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
|
|
52
52
|
- The loop runs Claude with `backend-prompt.md`
|
|
53
53
|
- Claude implements the slice in the project
|
|
54
|
-
- Phase 2
|
|
54
|
+
- Phase 2 (building slices) never runs with `--local-ai` — see *`--local-ai` does not build code*. Build with `--exec` instead
|
|
55
55
|
|
|
56
56
|
Both phases run in a continuous loop with a 3-second idle sleep. The realtime agent runs concurrently in the same process.
|
|
57
57
|
|
|
@@ -72,6 +72,34 @@ node .build-kit/realtime-agent.js
|
|
|
72
72
|
`ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
|
|
73
73
|
Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
|
|
74
74
|
|
|
75
|
+
### `--local-ai` does not build code
|
|
76
|
+
|
|
77
|
+
**`--local-ai` works on the board only. It never builds a slice.** Planned slices stay
|
|
78
|
+
Planned. That is by design, not a bug:
|
|
79
|
+
|
|
80
|
+
- **Planned slices are never handed to it.** `ralph-local-ai.js` gives `startRalph` no
|
|
81
|
+
`onPlannedSlice`, so the loop only feeds it `tasks.json` prompts.
|
|
82
|
+
- **It has no coding tools.** `lib/local-ai-agent.js` exposes only the board MCP tools:
|
|
83
|
+
no file read/write, no shell, no git, no test run.
|
|
84
|
+
- **Its system prompt forbids file and shell access.** Such a request is answered with
|
|
85
|
+
`Blocked: …`.
|
|
86
|
+
- **A prompt stops after 12 tool-call rounds.** That is enough for board edits, but far
|
|
87
|
+
short of a slice build.
|
|
88
|
+
|
|
89
|
+
Here the kit is the agent loop and the model only picks tools. Building would mean
|
|
90
|
+
rewriting a full coding agent inside the kit, so it is left out on purpose.
|
|
91
|
+
|
|
92
|
+
**To build with a local model, use `--exec`** with a coding harness pointed at that model
|
|
93
|
+
(see *External agent commands* below):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx @eventmodelers/cli run --exec "codex exec --oss --json --full-auto" # Codex on local Ollama
|
|
97
|
+
npx @eventmodelers/cli run --exec "opencode run --auto --format json" # OpenCode, with an Ollama/vLLM/LM Studio provider in its config
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or keep Claude Code as the builder and point `anthropicBaseUrl` at a local endpoint that
|
|
101
|
+
speaks the Anthropic API.
|
|
102
|
+
|
|
75
103
|
```bash
|
|
76
104
|
LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
|
|
77
105
|
LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
|
|
@@ -109,6 +137,20 @@ child runs with the project dir as its cwd and inherits stdio — a harness owns
|
|
|
109
137
|
output format, so there is no condensed per-step logging here the way `ralph-claude.js`
|
|
110
138
|
has it.
|
|
111
139
|
|
|
140
|
+
**Agent tracing needs the harness's JSON output flag.** With agent tracing on
|
|
141
|
+
(`eventmodelers config --agent-tracing on`), slice builds are costed just like with Claude —
|
|
142
|
+
but only if the harness runs in its JSON output mode. Its normal output has no token counts,
|
|
143
|
+
so without the flag a turn is not traced (the runner logs a hint). Add the flag yourself:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npx @eventmodelers/cli run --exec "codex exec --json --full-auto"
|
|
147
|
+
npx @eventmodelers/cli run --exec "opencode run --auto --format json"
|
|
148
|
+
npx @eventmodelers/cli run --exec "gemini --yolo --output-format stream-json -p"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The terminal then shows the raw JSON events. Only OpenCode reports a dollar cost; Codex and
|
|
152
|
+
Gemini turns record tokens and duration with no cost. See `agent-tracking.md`.
|
|
153
|
+
|
|
112
154
|
Persist a default alongside the local-AI settings:
|
|
113
155
|
|
|
114
156
|
```json
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
// startRalph({ kitDir, projectDir, onTask, onPlannedSlice })
|
|
5
5
|
// onTask(prompt) — called when tasks.json has entries
|
|
6
|
-
// onPlannedSlice(prompt) — called when .slices/ has a "Planned" entry (omit to skip)
|
|
6
|
+
// onPlannedSlice(prompt, slice) — called when .slices/ has a "Planned" entry (omit to skip);
|
|
7
|
+
// `slice` ({sliceId, sliceTitle, context, ticketNumber, boardId, attempt}) lets a runner record what building it cost.
|
|
7
8
|
|
|
8
9
|
import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
9
10
|
import { join, dirname } from 'path';
|
|
@@ -250,7 +251,9 @@ async function fetchAndPersistSlices(cfg, kitDir) {
|
|
|
250
251
|
mkdirSync(sliceDir, { recursive: true });
|
|
251
252
|
const sliceJsonPath = join(sliceDir, 'slice.json');
|
|
252
253
|
const existingSlice = existsSync(sliceJsonPath) ? JSON.parse(readFileSync(sliceJsonPath, 'utf-8')) : {};
|
|
253
|
-
|
|
254
|
+
// The summary omits ticketNumber when the slice has none, so a cleared ticket must be cleared
|
|
255
|
+
// here too — otherwise the old one survives the merge and lands on the next build's trace.
|
|
256
|
+
writeFileSync(sliceJsonPath, JSON.stringify({ ...existingSlice, ...slice, ticketNumber: slice.ticketNumber ?? null }, null, 2), 'utf-8');
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
259
|
|
|
@@ -417,6 +420,17 @@ function readCurrentContext(kitDir) {
|
|
|
417
420
|
try { return JSON.parse(readFileSync(ctxPath, 'utf-8')).name || null; } catch { return null; }
|
|
418
421
|
}
|
|
419
422
|
|
|
423
|
+
// The slice's ticket as of the last fetch, from its slice.json — a trace carries the ticket the
|
|
424
|
+
// slice is built under, since it can be reassigned between builds.
|
|
425
|
+
function readTicketNumber(kitDir, ctx, folder) {
|
|
426
|
+
if (!folder) return null;
|
|
427
|
+
try {
|
|
428
|
+
return JSON.parse(readFileSync(join(kitDir, '.slices', ctx, folder, 'slice.json'), 'utf-8')).ticketNumber ?? null;
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
420
434
|
// Returns the first Planned slice IN THE CURRENT CONTEXT ONLY. If the current
|
|
421
435
|
// context has no planned work, returns null so the loop waits — it must NEVER
|
|
422
436
|
// cross into another context to find something to build.
|
|
@@ -428,7 +442,7 @@ function getFirstPlannedSlice(kitDir) {
|
|
|
428
442
|
try {
|
|
429
443
|
const { slices } = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
430
444
|
const planned = slices && slices.find((s) => (s.status || '').toLowerCase() === 'planned');
|
|
431
|
-
if (planned) return { id: planned.id ?? null, title: planned.slice || planned.id || null, ctx: currentCtx };
|
|
445
|
+
if (planned) return { id: planned.id ?? null, title: planned.slice || planned.id || null, ctx: currentCtx, ticketNumber: readTicketNumber(kitDir, currentCtx, planned.folder) };
|
|
432
446
|
} catch {}
|
|
433
447
|
return null;
|
|
434
448
|
}
|
|
@@ -566,7 +580,14 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
|
|
|
566
580
|
}
|
|
567
581
|
|
|
568
582
|
const prompt = readFileSync(backendPromptFile, 'utf-8');
|
|
569
|
-
await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, () => onPlannedSlice(prompt
|
|
583
|
+
await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, () => onPlannedSlice(prompt, {
|
|
584
|
+
sliceId: planned.id,
|
|
585
|
+
sliceTitle: planned.title,
|
|
586
|
+
context: planned.ctx,
|
|
587
|
+
ticketNumber: planned.ticketNumber,
|
|
588
|
+
boardId: cfg.boardId ?? null,
|
|
589
|
+
attempt: stuckSlice.count,
|
|
590
|
+
}));
|
|
570
591
|
console.log(`[ralph] Slice build complete — waiting for next slice`);
|
|
571
592
|
if (credentialed) await fetchAndPersistSlices(cfg, kitDir).catch(() => {});
|
|
572
593
|
didWork = true;
|
|
@@ -590,15 +611,23 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
|
|
|
590
611
|
|
|
591
612
|
export { loadLocalConfig, fetchPlatformConfig, retryOn401, startRealtimeAgent };
|
|
592
613
|
|
|
614
|
+
// Who this process is. RALPH_AGENT_ID/RALPH_AGENT_NAME are `eventmodelers run --id/--name`,
|
|
615
|
+
// passed down as env (see cli.js's run dispatcher): a per-run identity override so a second
|
|
616
|
+
// agent of the same type can run side by side without the two overwriting each other's
|
|
617
|
+
// heartbeat row, and so the board can show a name instead of a bare uuid. An override skips
|
|
618
|
+
// ensureAgentId rather than overwriting it — the project's stable id stays on disk for the next
|
|
619
|
+
// plain run. Exported so a runner can resolve it at load time, before startRalph runs — the env
|
|
620
|
+
// it hands its `claude` is built then. Idempotent: ensureAgentId returns the id it persisted.
|
|
621
|
+
export function resolveAgentIdentity(kitDir, agentType = 'BUILD', cfg = loadLocalConfig(kitDir)) {
|
|
622
|
+
const agentId = process.env.RALPH_AGENT_ID || ensureAgentId(kitDir, agentType);
|
|
623
|
+
const agentName = process.env.RALPH_AGENT_NAME || cfg.agentName || '';
|
|
624
|
+
process.env.EVENTMODELERS_AGENT_ID = agentId;
|
|
625
|
+
return { agentId, agentName, agentType };
|
|
626
|
+
}
|
|
627
|
+
|
|
593
628
|
export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice, agentType = 'BUILD', queueAllStatuses = false, localOnly = false }) {
|
|
594
629
|
const local = loadLocalConfig(kitDir);
|
|
595
|
-
|
|
596
|
-
// (see cli.js's run dispatcher): a per-run identity override so a second agent of the same
|
|
597
|
-
// type can run side by side without the two overwriting each other's heartbeat row, and so
|
|
598
|
-
// the board can show a name instead of a bare uuid. An override skips ensureAgentId rather
|
|
599
|
-
// than overwriting it — the project's stable id stays on disk for the next plain run.
|
|
600
|
-
local.agentId = process.env.RALPH_AGENT_ID || ensureAgentId(kitDir, agentType);
|
|
601
|
-
if (process.env.RALPH_AGENT_NAME) local.agentName = process.env.RALPH_AGENT_NAME;
|
|
630
|
+
Object.assign(local, resolveAgentIdentity(kitDir, agentType, local));
|
|
602
631
|
|
|
603
632
|
console.log(`Ralph — kit: ${kitDir}`);
|
|
604
633
|
console.log(` project: ${projectDir}`);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// What a slice costs: one trace per build turn against one slice, posted to the platform's
|
|
2
|
+
// /api/org/:orgId/boards/:boardId/agent-traces. Only the runner can see what a turn cost, so it reports it.
|
|
3
|
+
//
|
|
4
|
+
// Best effort: each trace is appended to a local JSONL file, then POSTed once. No retries, no
|
|
5
|
+
// buffering. A failed upload is logged and forgotten; the file is the record.
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { appendFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
|
|
11
|
+
export const newId = () => randomUUID();
|
|
12
|
+
|
|
13
|
+
const n = (v) => (Number.isFinite(v) && v > 0 ? Math.floor(v) : 0);
|
|
14
|
+
|
|
15
|
+
/** Claude Code's `result` message — the only one whose numbers are final. The `assistant` events
|
|
16
|
+
* repeat one call's usage per content block AND report it mid-stream, so they cannot be used for
|
|
17
|
+
* accounting. `modelUsage` names the dominant model, which matters when subagents ran on a
|
|
18
|
+
* cheaper one than the lead. Cost is list price, even on a subscription. */
|
|
19
|
+
export function usageFromClaudeResult(msg) {
|
|
20
|
+
const u = msg?.usage ?? {};
|
|
21
|
+
const [dominant] = Object.entries(msg?.modelUsage ?? {})
|
|
22
|
+
.sort((a, b) => (b[1]?.outputTokens ?? 0) - (a[1]?.outputTokens ?? 0))[0] ?? [];
|
|
23
|
+
return {
|
|
24
|
+
inputTokens: n(u.input_tokens),
|
|
25
|
+
outputTokens: n(u.output_tokens),
|
|
26
|
+
cacheReadTokens: n(u.cache_read_input_tokens),
|
|
27
|
+
cacheWriteTokens: n(u.cache_creation_input_tokens),
|
|
28
|
+
costUsd: Number.isFinite(msg?.total_cost_usd) ? msg.total_cost_usd : null,
|
|
29
|
+
durationMs: Number.isFinite(msg?.duration_ms) ? msg.duration_ms : null,
|
|
30
|
+
model: dominant ?? null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A local model's per-round usage. Ollama reports it at the top level of /api/chat,
|
|
35
|
+
* OpenAI-compatible servers under `usage`. Cost 0: a local model is genuinely free. */
|
|
36
|
+
export function usageFromLocalAi(raw, durationMs) {
|
|
37
|
+
const openai = raw?.usage;
|
|
38
|
+
if (!openai && !Number.isFinite(raw?.eval_count) && !Number.isFinite(raw?.prompt_eval_count)) return null;
|
|
39
|
+
return {
|
|
40
|
+
inputTokens: n(openai ? openai.prompt_tokens : raw.prompt_eval_count),
|
|
41
|
+
outputTokens: n(openai ? openai.completion_tokens : raw.eval_count),
|
|
42
|
+
costUsd: 0,
|
|
43
|
+
durationMs: Number.isFinite(durationMs) ? durationMs : null,
|
|
44
|
+
model: typeof raw?.model === 'string' ? raw.model : null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One event from an external harness's JSON output (ralph-exec.js), or null if it carries no usage.
|
|
49
|
+
* Matched on shape, not on the command name, so a wrapper script around a harness still counts:
|
|
50
|
+
* - Codex `codex exec --json`: `turn.completed` with `usage` (input_tokens includes the cached ones)
|
|
51
|
+
* - OpenCode `opencode run --format json`: `step_finish` per model step, with tokens and cost
|
|
52
|
+
* - Gemini CLI `--output-format stream-json`: the final `result` with `stats`;
|
|
53
|
+
* `--output-format json`: one object whose `stats.models` holds tokens per model
|
|
54
|
+
* Only OpenCode reports a cost — the others leave costUsd null rather than guessing a price. */
|
|
55
|
+
export function usageFromHarnessEvent(msg) {
|
|
56
|
+
if (msg?.type === 'turn.completed' && msg.usage) {
|
|
57
|
+
const u = msg.usage;
|
|
58
|
+
const cached = n(u.cached_input_tokens);
|
|
59
|
+
return { inputTokens: Math.max(0, n(u.input_tokens) - cached), outputTokens: n(u.output_tokens), cacheReadTokens: cached };
|
|
60
|
+
}
|
|
61
|
+
if (msg?.type === 'step_finish' && msg.part?.tokens) {
|
|
62
|
+
const t = msg.part.tokens;
|
|
63
|
+
return {
|
|
64
|
+
inputTokens: n(t.input),
|
|
65
|
+
outputTokens: n(t.output) + n(t.reasoning),
|
|
66
|
+
cacheReadTokens: n(t.cache?.read),
|
|
67
|
+
cacheWriteTokens: n(t.cache?.write),
|
|
68
|
+
costUsd: Number.isFinite(msg.part.cost) ? msg.part.cost : null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (msg?.type === 'result' && msg.stats && !('total_cost_usd' in msg)) {
|
|
72
|
+
const s = msg.stats;
|
|
73
|
+
return {
|
|
74
|
+
inputTokens: n(s.input_tokens),
|
|
75
|
+
outputTokens: n(s.output_tokens),
|
|
76
|
+
durationMs: Number.isFinite(s.duration_ms) ? s.duration_ms : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (msg?.stats?.models && typeof msg.stats.models === 'object') {
|
|
80
|
+
const models = Object.entries(msg.stats.models);
|
|
81
|
+
const tok = (key) => models.reduce((sum, [, m]) => sum + n(m?.tokens?.[key]), 0);
|
|
82
|
+
const cached = tok('cached');
|
|
83
|
+
const [dominant] = models.sort((a, b) => n(b[1]?.tokens?.candidates) - n(a[1]?.tokens?.candidates))[0] ?? [];
|
|
84
|
+
return {
|
|
85
|
+
inputTokens: Math.max(0, tok('prompt') - cached),
|
|
86
|
+
outputTokens: tok('candidates') + tok('thoughts'),
|
|
87
|
+
cacheReadTokens: cached,
|
|
88
|
+
model: dominant ?? null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Sums the usage of every event in one turn. A field no event reported stays null (cost, model)
|
|
95
|
+
* or 0 (tokens), so a harness without a cost does not show up as a free turn. */
|
|
96
|
+
export function sumUsage(parts) {
|
|
97
|
+
if (!parts.length) return null;
|
|
98
|
+
const add = (key) => parts.reduce((sum, p) => sum + n(p[key]), 0);
|
|
99
|
+
const costs = parts.map((p) => p.costUsd).filter(Number.isFinite);
|
|
100
|
+
return {
|
|
101
|
+
inputTokens: add('inputTokens'),
|
|
102
|
+
outputTokens: add('outputTokens'),
|
|
103
|
+
cacheReadTokens: add('cacheReadTokens'),
|
|
104
|
+
cacheWriteTokens: add('cacheWriteTokens'),
|
|
105
|
+
costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
|
|
106
|
+
durationMs: parts.findLast((p) => Number.isFinite(p.durationMs))?.durationMs ?? null,
|
|
107
|
+
model: parts.findLast((p) => p.model)?.model ?? null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Records slice build turns. The agent id is the session: the platform requires one, and a
|
|
112
|
+
* restart of the same build agent is still the same agent building the same slices. */
|
|
113
|
+
export function createSliceTracer({ baseUrl, token, organizationId, agentId, traceFile, log = () => {}, enabled = true } = {}) {
|
|
114
|
+
const canUpload = !!(enabled && baseUrl && token && organizationId && agentId);
|
|
115
|
+
|
|
116
|
+
async function post(trace) {
|
|
117
|
+
// The platform files a trace under the board in its path — a turn without a board has nowhere to go.
|
|
118
|
+
if (!canUpload || !trace.boardId) return;
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(`${baseUrl}/api/org/${organizationId}/boards/${trace.boardId}/agent-traces`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'Content-Type': 'application/json', 'x-token': token, 'x-agent-id': agentId },
|
|
123
|
+
body: JSON.stringify({ traces: [trace] }),
|
|
124
|
+
signal: AbortSignal.timeout(15_000),
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) log(`trace: upload failed (HTTP ${res.status}${await res.text().then((t) => `: ${t}`).catch(() => '')}) — kept locally only`);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log(`trace: upload error (${err.message}) — kept locally only`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function write(trace) {
|
|
133
|
+
if (!enabled || !traceFile) return;
|
|
134
|
+
try {
|
|
135
|
+
mkdirSync(dirname(traceFile), { recursive: true });
|
|
136
|
+
appendFileSync(traceFile, `${JSON.stringify(trace)}\n`);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (!write.warned) { write.warned = true; log(`trace: cannot write ${traceFile} (${err.message})`); }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
/** One build turn: `{sliceId, sliceTitle, context, ticketNumber, boardId, attempt, status}` plus its usage.
|
|
144
|
+
* ticketNumber is the slice's ticket at build time — it can change, so it travels with each turn. */
|
|
145
|
+
record({ sliceId, sliceTitle = null, context = null, ticketNumber = null, boardId = null, attempt = null, status = 'ok' }, usage) {
|
|
146
|
+
if (!sliceId || !usage) return;
|
|
147
|
+
const trace = {
|
|
148
|
+
id: newId(), occurredAt: new Date().toISOString(),
|
|
149
|
+
boardId, sliceId, sliceTitle, context, ticketNumber, agentId, sessionId: agentId, attempt, status, ...usage,
|
|
150
|
+
};
|
|
151
|
+
write(trace);
|
|
152
|
+
post(trace).catch(() => {});
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Ralph loop + realtime agent using Claude Code as the executor.
|
|
3
3
|
// Usage: node ralph-claude.js [project_dir]
|
|
4
4
|
|
|
5
|
-
import { startRalph, loadLocalConfig } from './lib/ralph.js';
|
|
5
|
+
import { startRalph, loadLocalConfig, resolveAgentIdentity } from './lib/ralph.js';
|
|
6
|
+
import { createSliceTracer, usageFromClaudeResult } from './lib/tracing.js';
|
|
6
7
|
import { spawn } from 'child_process';
|
|
7
|
-
import { dirname, resolve } from 'path';
|
|
8
|
+
import { dirname, join, resolve } from 'path';
|
|
8
9
|
import { fileURLToPath } from 'url';
|
|
9
10
|
|
|
10
11
|
const kitDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -12,6 +13,23 @@ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir,
|
|
|
12
13
|
|
|
13
14
|
const cfg = loadLocalConfig(kitDir);
|
|
14
15
|
const localOnly = process.env.RALPH_LOCAL === '1';
|
|
16
|
+
// Resolved here, not left to startRalph: claudeEnv below is built at load time, and without it
|
|
17
|
+
// the agent id never reached `claude`, so its MCP calls went unattributed.
|
|
18
|
+
Object.assign(cfg, resolveAgentIdentity(kitDir, 'BUILD', cfg));
|
|
19
|
+
|
|
20
|
+
// What each slice build turn cost — see lib/tracing.js. Off unless the project opted in
|
|
21
|
+
// (`agentTracing: true`, see `eventmodelers config`); nothing is ever sent with --local.
|
|
22
|
+
const tracingOn = !localOnly && cfg.agentTracing === true;
|
|
23
|
+
if (!tracingOn && !localOnly) console.log('[ralph] agent tracing off — no slice cost data is sent (turn on: eventmodelers config --agent-tracing on)');
|
|
24
|
+
const tracer = createSliceTracer({
|
|
25
|
+
baseUrl: cfg.baseUrl,
|
|
26
|
+
token: cfg.token,
|
|
27
|
+
organizationId: cfg.organizationId,
|
|
28
|
+
agentId: cfg.agentId,
|
|
29
|
+
traceFile: join(projectDir, '.eventmodelers', 'trace', 'slices.jsonl'),
|
|
30
|
+
log: (line) => console.log(`[ralph] ${line}`),
|
|
31
|
+
enabled: tracingOn,
|
|
32
|
+
});
|
|
15
33
|
// --local must mean zero board contact — never hand Claude live board
|
|
16
34
|
// credentials via the inline header, even if config.json has them, or it'll
|
|
17
35
|
// treat them as already-connected and skip straight to board sync.
|
|
@@ -60,7 +78,8 @@ function describeToolUse(block) {
|
|
|
60
78
|
}
|
|
61
79
|
}
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
// `slice` is set for a planned-slice build (see startRalph) — the only turns that are traced.
|
|
82
|
+
function runClaude(prompt, slice = null) {
|
|
64
83
|
return new Promise((resolve, reject) => {
|
|
65
84
|
const proc = spawn('claude', [...claudeArgs, '-p', inlineHeader + prompt], {
|
|
66
85
|
cwd: projectDir,
|
|
@@ -89,6 +108,7 @@ function runClaude(prompt) {
|
|
|
89
108
|
}
|
|
90
109
|
} else if (msg.type === 'result') {
|
|
91
110
|
console.log(`done (${msg.duration_ms}ms${msg.total_cost_usd ? `, $${msg.total_cost_usd.toFixed(4)}` : ''})`);
|
|
111
|
+
if (slice) tracer.record({...slice, status: msg.is_error ? 'error' : 'ok'}, usageFromClaudeResult(msg));
|
|
92
112
|
}
|
|
93
113
|
}
|
|
94
114
|
});
|