@eventmodelers/cli 1.0.97 → 1.0.99
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 +45 -2
- package/cli.js +59 -5
- package/lib/modeling-exec.js +4 -86
- package/package.json +6 -1
- package/shared/build-kit/README.md +12 -4
- package/shared/build-kit/lib/mcp-register.js +96 -0
- package/shared/build-kit/lib/ralph.js +49 -0
- package/shared/build-kit/lib/tracing.js +6 -5
- package/shared/build-kit/ralph-claude.js +4 -1
- package/shared/build-kit/ralph-exec.js +8 -8
- package/shared/skills/learn-eventmodelers-api/SKILL.md +0 -1
- package/shared/skills/request-feedback/SKILL.md +23 -32
- package/stacks/axon/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/axon/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/blank/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/blank/templates/build-kit/lib/prompt.md +2 -9
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/kurrent/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +4 -4
- package/stacks/node/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/node/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/opencqrs/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/react/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/react/templates/build-kit/lib/prompt.md +2 -9
- package/stacks/supabase/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/supabase/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/supabase-react/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +2 -9
- package/stacks/umadb/templates/build-kit/CLAUDE.md +1 -6
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -8
- package/stacks/umadb/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/modeling-kit/templates/.claude/settings.local.json +0 -13
package/README.md
CHANGED
|
@@ -129,6 +129,7 @@ npx @eventmodelers/cli re-init # refresh an already-install
|
|
|
129
129
|
npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
|
|
130
130
|
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
|
|
131
131
|
npx @eventmodelers/cli run --agent <name> # pick the agent: claude (default), opencode, codex, gemini — bare --agent lists them with their exact commands
|
|
132
|
+
npx @eventmodelers/cli run --agent <name> --model <id> # …on a given model: -m for opencode/codex/gemini, --model for Claude, LOCAL_AI_MODEL for --local-ai
|
|
132
133
|
npx @eventmodelers/cli run --exec "<command>" # manual fallback for any other harness (ralph-exec.js) — agent tracing needs its JSON flag, e.g. "codex exec --json --full-auto"
|
|
133
134
|
npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
|
|
134
135
|
npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
|
|
@@ -199,20 +200,32 @@ instead of the warm Claude process — one process per turn, with the eventmodel
|
|
|
199
200
|
registered for it (`opencode.json`, `.gemini/settings.json`, or `-c` flags for Codex):
|
|
200
201
|
|
|
201
202
|
```bash
|
|
202
|
-
npx @eventmodelers/cli run --standalone --agent opencode # = --exec "opencode run"
|
|
203
|
+
npx @eventmodelers/cli run --standalone --agent opencode # = --exec "opencode run --standalone --auto"
|
|
203
204
|
npx @eventmodelers/cli run --modeling --agent codex # = --exec "codex exec --full-auto"
|
|
204
205
|
npx @eventmodelers/cli run --modeling --agent gemini # = --exec "gemini --yolo -p"
|
|
205
206
|
npx @eventmodelers/cli run --modeling --agent claude # the default, spelled out
|
|
207
|
+
npx @eventmodelers/cli run --worker --agent opencode --model opencode/big-pickle # pick the model (a free OpenCode Zen one here)
|
|
206
208
|
npx @eventmodelers/cli run --agent # list the agents and the command each runs
|
|
207
209
|
```
|
|
208
210
|
|
|
209
211
|
`--agent` is only a shortcut: to run a harness it does not know, or the same one with other
|
|
210
212
|
flags, pass the command yourself with `--exec "<command>"`. Keep the harness in its plain-text
|
|
211
|
-
output mode — its stdout is the turn's answer. For
|
|
213
|
+
output mode — its stdout is the turn's answer. For OpenCode the loop adds `--standalone` if you
|
|
214
|
+
leave it out — a plain `opencode run` goes to OpenCode's shared background service, which never
|
|
215
|
+
sees `$EVENTMODELERS_TOKEN`, so every eventmodelers tool fails to authenticate. For an unknown command the loop logs the MCP
|
|
212
216
|
server to register by hand (`<baseUrl>/mcp`, header `x-token` from `$EVENTMODELERS_TOKEN`).
|
|
213
217
|
Run `init-agents --hosts <harness>` to install the skills for a harness that does not read
|
|
214
218
|
`.claude/skills`. There are no subagents: self-directed work is done inline.
|
|
215
219
|
|
|
220
|
+
**In a container.** `docker/<agent>/Dockerfile` runs the modeling agent for one coding agent
|
|
221
|
+
(so far `docker/opencode`). Build from this directory and pass the board's credentials as env vars:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
docker build -f docker/opencode/Dockerfile -t eventmodelers-agent-opencode .
|
|
225
|
+
docker run --rm -e EVENTMODELERS_TOKEN=... -e EVENTMODELERS_ORGANIZATION_ID=... -e EVENTMODELERS_BOARD_ID=... \
|
|
226
|
+
-e OPENCODE_API_KEY=... eventmodelers-agent-opencode --model opencode/big-pickle
|
|
227
|
+
```
|
|
228
|
+
|
|
216
229
|
`--agent`, `--exec`, `--local-ai` and `--bash` each pick the runner, so pass at most one of them —
|
|
217
230
|
any two together are rejected. For a local or self-hosted model use `--local-ai`, not `--agent`.
|
|
218
231
|
|
|
@@ -699,6 +712,36 @@ Once your `init --build-kit` scaffold (see above) works against a real backend,
|
|
|
699
712
|
2. Add an entry for `<name>` to the `STACKS` object in `cli.js` (`label`, `kitSubdir: 'build-kit'`, `kitDirName: '.build-kit'`, `useShared: true`, `needsBoardId: true`).
|
|
700
713
|
3. Add it to this README's stack list, the "What gets installed" section, and the `stacks` command's output (generated from `STACKS`, so nothing to add there beyond the entry itself).
|
|
701
714
|
|
|
715
|
+
## Publishing a release
|
|
716
|
+
|
|
717
|
+
Pushing a version tag publishes the npm package **and** the agent docker images from the same commit, via `.github/workflows/release.yml`.
|
|
718
|
+
|
|
719
|
+
1. Write the release notes (`/release-notes` fills `RELEASE_NOTES.md` from the git history since the last release).
|
|
720
|
+
2. Bump `version` in `package.json` (`npm version <patch|minor|major> --no-git-tag-version`), and commit it together with the notes.
|
|
721
|
+
3. Tag that commit with the version, prefixed with `v`, and push the tag:
|
|
722
|
+
|
|
723
|
+
```bash
|
|
724
|
+
git tag v1.0.98
|
|
725
|
+
git push origin main v1.0.98
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
The workflow then:
|
|
729
|
+
|
|
730
|
+
| Job | What it does |
|
|
731
|
+
|---|---|
|
|
732
|
+
| `test` | Fails unless the tag equals `package.json`'s version (`v1.0.98` ↔ `1.0.98`), then runs `npm test` |
|
|
733
|
+
| `npm` | `npm publish --access public --provenance` for `@eventmodelers/cli` |
|
|
734
|
+
| `images` | Builds the six agent images for `linux/amd64` and `linux/arm64`, **scans each for credentials** (`.github/scripts/scan-image.sh`), and only then pushes it |
|
|
735
|
+
| `merge` | Combines the two architectures into one multi-arch image per name, tagged `:<tag>` and `:latest` |
|
|
736
|
+
|
|
737
|
+
Images land in `docker.io/nebulit/`: `eventmodelers-agent-claude`, `eventmodelers-agent-opencode`, and `em-studio-buildkit-{claude,opencode}-{node,java}`. They run the CLI from the tagged checkout, not from npm, so they don't wait on the npm publish.
|
|
738
|
+
|
|
739
|
+
**One-time setup:** an `NPM_TOKEN` repo secret (an npm automation token allowed to publish `@eventmodelers/cli`). The images push with the built-in `GITHUB_TOKEN`; new GHCR packages start private, so make them public in the org's package settings if they should be pullable without a login.
|
|
740
|
+
|
|
741
|
+
A failed run can't be re-published under the same version — npm refuses to overwrite one. If `npm` succeeded but an image job failed, re-run just the failed jobs from the Actions tab. If the tag itself was wrong, delete it (`git push --delete origin v1.0.98`), fix, bump to a new version and tag again.
|
|
742
|
+
|
|
743
|
+
To check an image locally before tagging: `docker build -f docker/<name>/Dockerfile -t scan/<name>:ci .` from this directory, then `../.github/scripts/scan-image.sh scan/<name>:ci`.
|
|
744
|
+
|
|
702
745
|
## Contributors
|
|
703
746
|
|
|
704
747
|
| Contributor | Contribution |
|
package/cli.js
CHANGED
|
@@ -76,7 +76,10 @@ function resolveLocalAiTarget(opts) {
|
|
|
76
76
|
// --exec stays the fallback for any harness or flag set not listed here.
|
|
77
77
|
const AGENT_PRESETS = {
|
|
78
78
|
claude: null,
|
|
79
|
-
|
|
79
|
+
// --standalone: a plain `opencode run` hands the turn to opencode's shared background service,
|
|
80
|
+
// started with some other env — so the MCP header's {env:EVENTMODELERS_TOKEN} resolves to nothing
|
|
81
|
+
// there and every eventmodelers tool fails to authenticate. A private server inherits ours.
|
|
82
|
+
opencode: { modeling: 'opencode run --standalone --auto', build: 'opencode run --standalone --auto --format json' },
|
|
80
83
|
codex: { modeling: 'codex exec --full-auto', build: 'codex exec --json --full-auto' },
|
|
81
84
|
gemini: { modeling: 'gemini --yolo -p', build: 'gemini --yolo --output-format stream-json -p' },
|
|
82
85
|
};
|
|
@@ -1176,6 +1179,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1176
1179
|
// placeholder, never the literal secret (see connect/SKILL.md's Security notes).
|
|
1177
1180
|
ensureMcpRegistered(targetDir, config.baseUrl || DEFAULT_BASE_URL);
|
|
1178
1181
|
ensureEnvToken(targetDir, config.token);
|
|
1182
|
+
ensureMcpEnabled(targetDir);
|
|
1179
1183
|
// Only build stacks trace (slice build cost); they are the ones running the shared runtime.
|
|
1180
1184
|
if (stackCfg.useShared) await configureAgentTracing(configPath, options.print);
|
|
1181
1185
|
}
|
|
@@ -1366,6 +1370,7 @@ async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
|
|
|
1366
1370
|
});
|
|
1367
1371
|
ensureMcpRegistered(targetDir, cfg.baseUrl || DEFAULT_BASE_URL);
|
|
1368
1372
|
ensureEnvToken(targetDir, cfg.token);
|
|
1373
|
+
ensureMcpEnabled(targetDir);
|
|
1369
1374
|
await configureAgentTracing(configPath, globalOpts.print, true);
|
|
1370
1375
|
}
|
|
1371
1376
|
|
|
@@ -1542,6 +1547,25 @@ function ensureMcpRegistered(projectDir, baseUrl) {
|
|
|
1542
1547
|
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
|
|
1543
1548
|
}
|
|
1544
1549
|
|
|
1550
|
+
// `.mcp.json` alone does not make Claude Code use a server: a project server also has to be
|
|
1551
|
+
// approved, and declining that prompt (or dismissing `/mcp`) in an interactive session writes
|
|
1552
|
+
// `disabledMcpjsonServers` into `.claude/settings.local.json`, after which every later `claude` in
|
|
1553
|
+
// the project — the runner's included — silently runs without the eventmodelers tools and the
|
|
1554
|
+
// agent falls back to curl. Whoever starts the runner wants the server on, so this clears that
|
|
1555
|
+
// entry and approves it explicitly. Only this project-local file is touched.
|
|
1556
|
+
function ensureMcpEnabled(projectDir) {
|
|
1557
|
+
const settingsPath = join(projectDir, '.claude', 'settings.local.json');
|
|
1558
|
+
const settings = readJsonSafe(settingsPath);
|
|
1559
|
+
const disabled = settings.disabledMcpjsonServers ?? [];
|
|
1560
|
+
const enabled = settings.enabledMcpjsonServers ?? [];
|
|
1561
|
+
if (!disabled.includes('eventmodelers') && enabled.includes('eventmodelers')) return;
|
|
1562
|
+
settings.disabledMcpjsonServers = disabled.filter((n) => n !== 'eventmodelers');
|
|
1563
|
+
if (!settings.disabledMcpjsonServers.length) delete settings.disabledMcpjsonServers;
|
|
1564
|
+
settings.enabledMcpjsonServers = [...enabled, 'eventmodelers'].filter((n, i, a) => a.indexOf(n) === i);
|
|
1565
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
1566
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1545
1569
|
// Companion to ensureMcpRegistered: that function deliberately never writes the
|
|
1546
1570
|
// token itself, on the assumption the caller sets EVENTMODELERS_TOKEN in whatever
|
|
1547
1571
|
// process spawns `claude` (true for `run --modeling`'s own spawn). It is NOT true
|
|
@@ -1916,7 +1940,7 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1916
1940
|
// untargeted is handed straight back to the queue for another agent to take. It says
|
|
1917
1941
|
// nothing about the standalone lane — a self-directed turn is nobody's task, so an
|
|
1918
1942
|
// exclusive standalone agent still works the board on its own initiative.
|
|
1919
|
-
async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null, exec = null } = {}) {
|
|
1943
|
+
async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null, exec = null, model = null } = {}) {
|
|
1920
1944
|
// --worker: only prompts — no chat, and no board-change turns (it has nowhere to propose).
|
|
1921
1945
|
const chat = !worker;
|
|
1922
1946
|
const configLibPath = join(kitDir, 'lib', 'config.js');
|
|
@@ -1935,7 +1959,7 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
1935
1959
|
// run (resolveModelingCredentials) and handed over whole. Walking the filesystem here
|
|
1936
1960
|
// would also print loadLocalConfig's "no config found — platform sync disabled" note,
|
|
1937
1961
|
// which is exactly backwards when a complete config was just passed in.
|
|
1938
|
-
const local = overrides ? { ...overrides } : loadLocalConfig(kitDir);
|
|
1962
|
+
const local = overrides ? { ...overrides } : applyEnvOverrides(loadLocalConfig(kitDir));
|
|
1939
1963
|
// The global install's overrides carry their own agent id, kept per board in
|
|
1940
1964
|
// ~/.eventmodelers/boards/<board>.json — one dir driving several boards must not have
|
|
1941
1965
|
// them all upsert one shared alive row. A project install keeps its id in the project
|
|
@@ -1951,7 +1975,7 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
1951
1975
|
// The identity flags go on last: the global install's `overrides` carry the agent id
|
|
1952
1976
|
// resolveModelingCredentials just minted for this run, which would otherwise win back over
|
|
1953
1977
|
// an explicit --id.
|
|
1954
|
-
const cfg = { ...(await fetchPlatformConfig(local)), ...(overrides ?? {}), ...(identity.agentId ? { agentId: identity.agentId } : {}), ...(identity.agentName ? { agentName: identity.agentName } : {}) }; // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
|
|
1978
|
+
const cfg = { ...applyEnvOverrides(await fetchPlatformConfig(local)), ...(overrides ?? {}), ...(identity.agentId ? { agentId: identity.agentId } : {}), ...(identity.agentName ? { agentName: identity.agentName } : {}), ...(model ? { model } : {}) }; // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
|
|
1955
1979
|
if (!cfg.boardId) {
|
|
1956
1980
|
console.error('❌ --modeling needs a boardId — a modeling agent always runs for exactly one board. Run `/connect board=<uuid>` once, or add boardId to .eventmodelers/config.json.');
|
|
1957
1981
|
process.exit(1);
|
|
@@ -3552,6 +3576,7 @@ credentialFlags(program
|
|
|
3552
3576
|
// (see the beta-api protected-resource-metadata incident this fixed).
|
|
3553
3577
|
ensureMcpRegistered(targetDir, cfg.baseUrl || DEFAULT_BASE_URL);
|
|
3554
3578
|
ensureEnvToken(targetDir, cfg.token);
|
|
3579
|
+
ensureMcpEnabled(targetDir);
|
|
3555
3580
|
if (existsSync(join(targetDir, STACKS.node.kitDirName))) {
|
|
3556
3581
|
await configureAgentTracing(configPath, globalOpts.print, !Object.values(overrides).some(Boolean));
|
|
3557
3582
|
}
|
|
@@ -3563,6 +3588,7 @@ credentialFlags(program
|
|
|
3563
3588
|
.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')
|
|
3564
3589
|
.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.`)
|
|
3565
3590
|
.option('--agent [name]', `Pick the agent that does the work by name: ${Object.keys(AGENT_PRESETS).join(', ')}. claude is the default runner; any other is a shortcut for --exec with that harness's headless command filled in — plain-text output for --modeling/--standalone/--worker, JSON output (for agent tracing) for a build kit. Bare --agent lists the agents and the exact command each runs, then stops. Mutually exclusive with --exec, --local-ai and --bash.`)
|
|
3591
|
+
.option('--model <id>', 'The model the agent runs on, for this run only: `-m <id>` on the --agent opencode/codex/gemini command (e.g. opencode/big-pickle, gpt-5-codex, gemini-2.5-pro), `--model <id>` for Claude (overriding `model` from config), LOCAL_AI_MODEL for --local-ai. Not with --exec — put the harness\'s own model flag in the command there.')
|
|
3566
3592
|
.option('--exec [command]', 'Fallback for any harness --agent does not cover, or other flags than its preset. Hand each prompt to an external agent command instead of the default Claude runner — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto", "gemini --yolo -p" or "opencode run". One process per prompt; 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. A build kit runs it via ralph-exec.js; there, 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. With --modeling/--standalone/--worker it is how any agent takes board prompts and chats: the loop claims each prompt or chat message and runs the command once for it. Keep the harness in its plain-text output mode there — stdout is the turn\'s answer — the eventmodelers MCP server is registered for it on every run (opencode.json, .gemini/settings.json, or -c flags for codex; any other command logs what to register by hand), authenticating with $EVENTMODELERS_TOKEN from the env the command inherits, and `init-agents --hosts <harness>` installs the skills for a harness that does not read .claude/skills. No subagents: self-directed work is done inline.')
|
|
3567
3593
|
.option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
|
|
3568
3594
|
.option('--modeling', 'Connect a modeling agent to the board: it takes prompts and chat messages and hands each to an AI — by default one Claude process kept warm across prompts, for low-latency voice/live use; --agent (or --local-ai, or --exec as the manual fallback) picks another. 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.')
|
|
@@ -3587,6 +3613,22 @@ credentialFlags(program
|
|
|
3587
3613
|
// --agent is --exec with the command filled in for the loop this run picks.
|
|
3588
3614
|
const agentExec = resolveAgentPreset(opts, (opts.modeling || opts.standalone || opts.worker || opts.global) ? 'modeling' : 'build');
|
|
3589
3615
|
if (agentExec) opts.exec = agentExec;
|
|
3616
|
+
// --model: one flag, spelled the way each runner takes it. A hand-written --exec command is
|
|
3617
|
+
// the one place it can't go — where the model flag goes in an unknown command is the author's.
|
|
3618
|
+
const model = opts.model === undefined ? null : String(opts.model).trim();
|
|
3619
|
+
if (model === '') {
|
|
3620
|
+
console.error('❌ --model needs a non-empty value.');
|
|
3621
|
+
process.exit(1);
|
|
3622
|
+
}
|
|
3623
|
+
if (model && opts.exec && !agentExec) {
|
|
3624
|
+
console.error('❌ --model does not apply to --exec — put the harness\'s own model flag in the command (e.g. --exec "opencode run --standalone -m <id>"), or use --agent.');
|
|
3625
|
+
process.exit(1);
|
|
3626
|
+
}
|
|
3627
|
+
if (model && agentExec) opts.exec = `${agentExec} -m '${model.replace(/'/g, `'\\''`)}'`;
|
|
3628
|
+
else if (model && resolveLocalAiTarget(opts) !== null) process.env.LOCAL_AI_MODEL = model;
|
|
3629
|
+
// The default Claude runner: the build kit's ralph-claude.js reads RALPH_MODEL from the env it
|
|
3630
|
+
// inherits; the modeling loop takes it as runModeling's `model`.
|
|
3631
|
+
else if (model) process.env.RALPH_MODEL = model;
|
|
3590
3632
|
if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
|
|
3591
3633
|
console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
|
|
3592
3634
|
}
|
|
@@ -3697,7 +3739,7 @@ credentialFlags(program
|
|
|
3697
3739
|
const runnerLabel = modelingLocalAi ? 'local model' : opts.exec ? 'one agent process per turn' : 'warm Claude process';
|
|
3698
3740
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (${runnerLabel}) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
3699
3741
|
try {
|
|
3700
|
-
await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, worker: !!opts.worker, overrides, maxAgents, identity, localAi: modelingLocalAi, exec: opts.exec ?? null });
|
|
3742
|
+
await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, worker: !!opts.worker, overrides, maxAgents, identity, localAi: modelingLocalAi, exec: opts.exec ?? null, model: !opts.exec && modelingLocalAi === null ? model : null });
|
|
3701
3743
|
} catch (err) {
|
|
3702
3744
|
console.error('[modeling] Fatal:', err);
|
|
3703
3745
|
process.exit(1);
|
|
@@ -3736,6 +3778,18 @@ credentialFlags(program
|
|
|
3736
3778
|
process.exit(1);
|
|
3737
3779
|
}
|
|
3738
3780
|
|
|
3781
|
+
// ralph-claude.js hands every turn to a `claude` that only discovers MCP servers from
|
|
3782
|
+
// `.mcp.json` at its own startup, so it is rewritten on every run (see ensureMcpRegistered) —
|
|
3783
|
+
// a file deleted, or written for another baseUrl, since `init` would otherwise leave the agent
|
|
3784
|
+
// without its MCP tools. The token needs no file here: the runner puts it in claude's env.
|
|
3785
|
+
if (runner === 'ralph-claude.js' && !opts.local) {
|
|
3786
|
+
const runCfg = loadEffectiveConfig(cwd, kitDir, globalOpts.config).config;
|
|
3787
|
+
if (runCfg.token) {
|
|
3788
|
+
ensureMcpRegistered(dirname(kitDir), runCfg.baseUrl || DEFAULT_BASE_URL);
|
|
3789
|
+
ensureMcpEnabled(dirname(kitDir));
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3739
3793
|
console.log(`▶ Starting ${relative(cwd, runnerPath)}...\n`);
|
|
3740
3794
|
const cmd = runner.endsWith('.sh') ? `"${runnerPath}"` : `node "${runnerPath}"`;
|
|
3741
3795
|
try {
|
package/lib/modeling-exec.js
CHANGED
|
@@ -18,95 +18,13 @@
|
|
|
18
18
|
// reply. So run the harness in its plain-text mode, not a JSON one.
|
|
19
19
|
|
|
20
20
|
import { spawn } from 'child_process';
|
|
21
|
-
import {
|
|
21
|
+
import { mkdtempSync, writeFileSync } from 'fs';
|
|
22
22
|
import { tmpdir } from 'os';
|
|
23
|
-
import {
|
|
23
|
+
import { join } from 'path';
|
|
24
24
|
import { turnTimeoutMs, superviseTurn } from '../shared/build-kit/lib/turn.js';
|
|
25
|
+
import { shellQuote, harnessOf, registerMcp } from '../shared/build-kit/lib/mcp-register.js';
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
// text with backticks and $ in it, so it cannot go in unquoted.
|
|
28
|
-
function shellQuote(s) {
|
|
29
|
-
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Adds or refreshes one key in a project JSON config, leaving everything else in it alone. A file
|
|
33
|
-
// that isn't plain JSON (a JSONC one with comments) is not rewritten — returns false instead, and
|
|
34
|
-
// the caller says what to add by hand.
|
|
35
|
-
function mergeJsonConfig(path, apply) {
|
|
36
|
-
let config = {};
|
|
37
|
-
if (existsSync(path)) {
|
|
38
|
-
try { config = JSON.parse(readFileSync(path, 'utf8')); } catch { return false; }
|
|
39
|
-
}
|
|
40
|
-
apply(config);
|
|
41
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
-
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// How each known harness learns about the eventmodelers MCP server — the --exec counterpart of
|
|
47
|
-
// ensureMcpRegistered's `.mcp.json` for Claude, and like it rewritten every run, always into the
|
|
48
|
-
// harness's *project* config in the agent's working dir (never a user-wide one), and always with
|
|
49
|
-
// the token as an env reference the harness resolves from the env this runner hands it, never its
|
|
50
|
-
// value. Each entry returns the command to run, which is where a harness without a usable project
|
|
51
|
-
// config (Codex only loads one for a trusted project) takes the server as flags instead. Adding a
|
|
52
|
-
// harness is one entry here.
|
|
53
|
-
const MCP_REGISTRATIONS = {
|
|
54
|
-
opencode: (command, projectDir, url) => {
|
|
55
|
-
const entry = {
|
|
56
|
-
type: 'remote',
|
|
57
|
-
url,
|
|
58
|
-
headers: { 'x-token': '{env:EVENTMODELERS_TOKEN}', 'x-agent-id': '{env:EVENTMODELERS_AGENT_ID}' },
|
|
59
|
-
};
|
|
60
|
-
const ok = mergeJsonConfig(join(projectDir, 'opencode.json'), (config) => {
|
|
61
|
-
// OpenCode 2 nests servers under mcp.servers and still reads the older flat mcp.<name>;
|
|
62
|
-
// follow whichever form the file already uses.
|
|
63
|
-
config.mcp = config.mcp || {};
|
|
64
|
-
if (config.mcp.servers) config.mcp.servers.eventmodelers = entry;
|
|
65
|
-
else config.mcp.eventmodelers = entry;
|
|
66
|
-
});
|
|
67
|
-
return { command, file: ok ? 'opencode.json' : null, manual: ok ? null : `add mcp.eventmodelers = ${JSON.stringify(entry)} to opencode.json` };
|
|
68
|
-
},
|
|
69
|
-
gemini: (command, projectDir, url) => {
|
|
70
|
-
const entry = { httpUrl: url, headers: { 'x-token': '$EVENTMODELERS_TOKEN', 'x-agent-id': '$EVENTMODELERS_AGENT_ID' } };
|
|
71
|
-
const ok = mergeJsonConfig(join(projectDir, '.gemini', 'settings.json'), (config) => {
|
|
72
|
-
config.mcpServers = config.mcpServers || {};
|
|
73
|
-
config.mcpServers.eventmodelers = entry;
|
|
74
|
-
});
|
|
75
|
-
return { command, file: ok ? '.gemini/settings.json' : null, manual: ok ? null : `add mcpServers.eventmodelers = ${JSON.stringify(entry)} to .gemini/settings.json` };
|
|
76
|
-
},
|
|
77
|
-
codex: (command, _projectDir, url) => ({
|
|
78
|
-
// -c values are TOML; env_http_headers names the env var each header is read from.
|
|
79
|
-
command: `${command} -c ${shellQuote(`mcp_servers.eventmodelers.url="${url}"`)} -c ${shellQuote('mcp_servers.eventmodelers.env_http_headers={x-token="EVENTMODELERS_TOKEN",x-agent-id="EVENTMODELERS_AGENT_ID"}')}`,
|
|
80
|
-
file: null,
|
|
81
|
-
manual: null,
|
|
82
|
-
}),
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Which known harness a command runs — the first word naming one, so `npx @openai/codex exec`
|
|
86
|
-
// and `/opt/bin/opencode run` are recognised as well as the bare names.
|
|
87
|
-
export function harnessOf(command) {
|
|
88
|
-
for (const word of String(command).trim().split(/\s+/).slice(0, 3)) {
|
|
89
|
-
const name = word.split('/').pop().replace(/@.*$/, '');
|
|
90
|
-
if (MCP_REGISTRATIONS[name]) return name;
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Registers the MCP server for the command's harness. Returns the command to run with it, and
|
|
96
|
-
// whether the server is now registered — false for an unknown command or a config it could not
|
|
97
|
-
// update, where the turn still has to connect the way the kit says to.
|
|
98
|
-
export function registerMcp(command, projectDir, baseUrl, log) {
|
|
99
|
-
const url = `${baseUrl}/mcp`;
|
|
100
|
-
const harness = harnessOf(command);
|
|
101
|
-
if (!harness) {
|
|
102
|
-
log(`MCP: unknown agent command — register the eventmodelers MCP server with it yourself: ${url}, header x-token from $EVENTMODELERS_TOKEN (and x-agent-id from $EVENTMODELERS_AGENT_ID)`);
|
|
103
|
-
return { command, registered: false };
|
|
104
|
-
}
|
|
105
|
-
const reg = MCP_REGISTRATIONS[harness](command, projectDir, url);
|
|
106
|
-
if (reg.manual) log(`MCP: could not update the ${harness} config (not plain JSON) — ${reg.manual}`);
|
|
107
|
-
else log(`MCP: eventmodelers registered for ${harness}${reg.file ? ` in ${reg.file}` : ' via -c flags'}`);
|
|
108
|
-
return { command: reg.command, registered: !reg.manual };
|
|
109
|
-
}
|
|
27
|
+
export { harnessOf, registerMcp };
|
|
110
28
|
|
|
111
29
|
export function createModelingExecRunner({ cfg, command: rawCommand, projectDir, log }) {
|
|
112
30
|
if (!rawCommand) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.99",
|
|
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": {
|
|
@@ -39,5 +39,10 @@
|
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/Nebulit-GmbH/Eventmodelers-Build-Kits.git",
|
|
46
|
+
"directory": "eventmodelers-cli"
|
|
42
47
|
}
|
|
43
48
|
}
|
|
@@ -94,7 +94,7 @@ rewriting a full coding agent inside the kit, so it is left out on purpose.
|
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
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
|
|
97
|
+
npx @eventmodelers/cli run --exec "opencode run --standalone --auto --format json" # OpenCode, with an Ollama/vLLM/LM Studio provider in its config
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
Or keep Claude Code as the builder and point `anthropicBaseUrl` at a local endpoint that
|
|
@@ -126,21 +126,29 @@ known ones have a shortcut that fills in the command, JSON flag for agent tracin
|
|
|
126
126
|
(bare `--agent` lists them):
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
npx @eventmodelers/cli run --agent opencode # = --exec "opencode run --auto --format json"
|
|
129
|
+
npx @eventmodelers/cli run --agent opencode # = --exec "opencode run --standalone --auto --format json"
|
|
130
130
|
npx @eventmodelers/cli run --agent codex # = --exec "codex exec --json --full-auto"
|
|
131
131
|
npx @eventmodelers/cli run --agent gemini # = --exec "gemini --yolo --output-format stream-json -p"
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
+
Add `--model <id>` to pick the model — it becomes the harness's `-m <id>`
|
|
135
|
+
(`run --agent opencode --model opencode/big-pickle`); on the default Claude runner it
|
|
136
|
+
overrides `model` from config for that run.
|
|
137
|
+
|
|
134
138
|
`--exec` is the manual fallback: any other harness, or one of these with other flags:
|
|
135
139
|
|
|
136
140
|
```bash
|
|
137
141
|
npx @eventmodelers/cli run --exec "codex exec --full-auto"
|
|
138
|
-
npx @eventmodelers/cli run --exec "opencode run"
|
|
142
|
+
npx @eventmodelers/cli run --exec "opencode run --standalone --auto" # --standalone: see below
|
|
139
143
|
|
|
140
144
|
# …or persist it and use the bare flag
|
|
141
145
|
RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
|
|
142
146
|
```
|
|
143
147
|
|
|
148
|
+
Run OpenCode with `--standalone`: a plain `opencode run` hands the turn to OpenCode's shared
|
|
149
|
+
background service, which was started with another environment and never sees the variables
|
|
150
|
+
this runner sets (`EVENTMODELERS_TOKEN`, `RALPH_PROMPT_FILE`, …).
|
|
151
|
+
|
|
144
152
|
The prompt is appended to the command as one shell-quoted argument, and is also written
|
|
145
153
|
to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
|
|
146
154
|
child runs with the project dir as its cwd and inherits stdio — a harness owns its own
|
|
@@ -154,7 +162,7 @@ so without the flag a turn is not traced (the runner logs a hint). Add the flag
|
|
|
154
162
|
|
|
155
163
|
```bash
|
|
156
164
|
npx @eventmodelers/cli run --exec "codex exec --json --full-auto"
|
|
157
|
-
npx @eventmodelers/cli run --exec "opencode run --auto --format json"
|
|
165
|
+
npx @eventmodelers/cli run --exec "opencode run --standalone --auto --format json"
|
|
158
166
|
npx @eventmodelers/cli run --exec "gemini --yolo --output-format stream-json -p"
|
|
159
167
|
```
|
|
160
168
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Registers the eventmodelers MCP server with an external agent harness (OpenCode, Gemini, Codex),
|
|
2
|
+
// for both loops that hand turns to one: the modeling loop (lib/modeling-exec.js) and the build
|
|
3
|
+
// loop (ralph-exec.js).
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
|
|
8
|
+
// POSIX single-quote escaping: close, insert an escaped quote, reopen. A turn is multi-line
|
|
9
|
+
// text with backticks and $ in it, so it cannot go in unquoted.
|
|
10
|
+
export function shellQuote(s) {
|
|
11
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Adds or refreshes one key in a project JSON config, leaving everything else in it alone. A file
|
|
15
|
+
// that isn't plain JSON (a JSONC one with comments) is not rewritten — returns false instead, and
|
|
16
|
+
// the caller says what to add by hand.
|
|
17
|
+
function mergeJsonConfig(path, apply) {
|
|
18
|
+
let config = {};
|
|
19
|
+
if (existsSync(path)) {
|
|
20
|
+
try { config = JSON.parse(readFileSync(path, 'utf8')); } catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
apply(config);
|
|
23
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
24
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// How each known harness learns about the eventmodelers MCP server — the --exec counterpart of
|
|
29
|
+
// ensureMcpRegistered's `.mcp.json` for Claude, and like it rewritten every run, always into the
|
|
30
|
+
// harness's *project* config in the agent's working dir (never a user-wide one), and always with
|
|
31
|
+
// the token as an env reference the harness resolves from the env this runner hands it, never its
|
|
32
|
+
// value. Each entry returns the command to run, which is where a harness without a usable project
|
|
33
|
+
// config (Codex only loads one for a trusted project) takes the server as flags instead. Adding a
|
|
34
|
+
// harness is one entry here.
|
|
35
|
+
const MCP_REGISTRATIONS = {
|
|
36
|
+
opencode: (command, projectDir, url) => {
|
|
37
|
+
const entry = {
|
|
38
|
+
type: 'remote',
|
|
39
|
+
url,
|
|
40
|
+
headers: { 'x-token': '{env:EVENTMODELERS_TOKEN}', 'x-agent-id': '{env:EVENTMODELERS_AGENT_ID}' },
|
|
41
|
+
};
|
|
42
|
+
const ok = mergeJsonConfig(join(projectDir, 'opencode.json'), (config) => {
|
|
43
|
+
// OpenCode 2 nests servers under mcp.servers and still reads the older flat mcp.<name>;
|
|
44
|
+
// follow whichever form the file already uses.
|
|
45
|
+
config.mcp = config.mcp || {};
|
|
46
|
+
if (config.mcp.servers) config.mcp.servers.eventmodelers = entry;
|
|
47
|
+
else config.mcp.eventmodelers = entry;
|
|
48
|
+
});
|
|
49
|
+
// A plain `opencode run` hands the turn to opencode's shared background service, which was
|
|
50
|
+
// started with some other env: {env:EVENTMODELERS_TOKEN} resolves to nothing there and the
|
|
51
|
+
// server refuses every call. --standalone runs a private server that inherits this one's env.
|
|
52
|
+
const standalone = /\s--(standalone|server)\b/.test(` ${command}`) ? command : `${command} --standalone`;
|
|
53
|
+
return { command: standalone, file: ok ? 'opencode.json' : null, manual: ok ? null : `add mcp.eventmodelers = ${JSON.stringify(entry)} to opencode.json` };
|
|
54
|
+
},
|
|
55
|
+
gemini: (command, projectDir, url) => {
|
|
56
|
+
const entry = { httpUrl: url, headers: { 'x-token': '$EVENTMODELERS_TOKEN', 'x-agent-id': '$EVENTMODELERS_AGENT_ID' } };
|
|
57
|
+
const ok = mergeJsonConfig(join(projectDir, '.gemini', 'settings.json'), (config) => {
|
|
58
|
+
config.mcpServers = config.mcpServers || {};
|
|
59
|
+
config.mcpServers.eventmodelers = entry;
|
|
60
|
+
});
|
|
61
|
+
return { command, file: ok ? '.gemini/settings.json' : null, manual: ok ? null : `add mcpServers.eventmodelers = ${JSON.stringify(entry)} to .gemini/settings.json` };
|
|
62
|
+
},
|
|
63
|
+
codex: (command, _projectDir, url) => ({
|
|
64
|
+
// -c values are TOML; env_http_headers names the env var each header is read from.
|
|
65
|
+
command: `${command} -c ${shellQuote(`mcp_servers.eventmodelers.url="${url}"`)} -c ${shellQuote('mcp_servers.eventmodelers.env_http_headers={x-token="EVENTMODELERS_TOKEN",x-agent-id="EVENTMODELERS_AGENT_ID"}')}`,
|
|
66
|
+
file: null,
|
|
67
|
+
manual: null,
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Which known harness a command runs — the first word naming one, so `npx @openai/codex exec`
|
|
72
|
+
// and `/opt/bin/opencode run` are recognised as well as the bare names.
|
|
73
|
+
export function harnessOf(command) {
|
|
74
|
+
for (const word of String(command).trim().split(/\s+/).slice(0, 3)) {
|
|
75
|
+
const name = word.split('/').pop().replace(/@.*$/, '');
|
|
76
|
+
if (MCP_REGISTRATIONS[name]) return name;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Registers the MCP server for the command's harness. Returns the command to run with it, and
|
|
82
|
+
// whether the server is now registered — false for an unknown command or a config it could not
|
|
83
|
+
// update, where the turn still has to connect the way the kit says to.
|
|
84
|
+
export function registerMcp(command, projectDir, baseUrl, log) {
|
|
85
|
+
const url = `${baseUrl}/mcp`;
|
|
86
|
+
const harness = harnessOf(command);
|
|
87
|
+
if (!harness) {
|
|
88
|
+
log(`MCP: unknown agent command — register the eventmodelers MCP server with it yourself: ${url}, header x-token from $EVENTMODELERS_TOKEN (and x-agent-id from $EVENTMODELERS_AGENT_ID)`);
|
|
89
|
+
return { command, registered: false };
|
|
90
|
+
}
|
|
91
|
+
const reg = MCP_REGISTRATIONS[harness](command, projectDir, url);
|
|
92
|
+
if (reg.manual) log(`MCP: could not update the ${harness} config (not plain JSON) — ${reg.manual}`);
|
|
93
|
+
else log(`MCP: eventmodelers registered for ${harness}${reg.file ? ` in ${reg.file}` : ' via -c flags'}`);
|
|
94
|
+
return { command: reg.command, registered: !reg.manual };
|
|
95
|
+
}
|
|
96
|
+
|
|
@@ -89,6 +89,17 @@ function* configCandidates(kitDir) {
|
|
|
89
89
|
yield join(homedir(), '.eventmodelers', 'config.json');
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
const ENV_CONFIG_MAP = {
|
|
93
|
+
EVENTMODELERS_ORGANIZATION_ID: 'organizationId',
|
|
94
|
+
EVENTMODELERS_BOARD_ID: 'boardId',
|
|
95
|
+
EVENTMODELERS_TOKEN: 'token',
|
|
96
|
+
EVENTMODELERS_BASE_URL: 'baseUrl',
|
|
97
|
+
EVENTMODELERS_ANTHROPIC_BASE_URL: 'anthropicBaseUrl',
|
|
98
|
+
EVENTMODELERS_MODEL: 'model',
|
|
99
|
+
EVENTMODELERS_SUBAGENT_MODEL: 'subagentModel',
|
|
100
|
+
EVENTMODELERS_AGENT_NAME: 'agentName',
|
|
101
|
+
};
|
|
102
|
+
|
|
92
103
|
function loadLocalConfig(kitDir) {
|
|
93
104
|
const merged = {};
|
|
94
105
|
const sources = [];
|
|
@@ -109,6 +120,11 @@ function loadLocalConfig(kitDir) {
|
|
|
109
120
|
if (hasCredentials(merged)) break;
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
// EVENTMODELERS_* env vars win over any config.json, as they do for the CLI's own commands —
|
|
124
|
+
// a container gets its credentials this way, and a stale file in a mounted project must not beat them.
|
|
125
|
+
for (const [envVar, field] of Object.entries(ENV_CONFIG_MAP)) {
|
|
126
|
+
if (process.env[envVar]) merged[field] = process.env[envVar];
|
|
127
|
+
}
|
|
112
128
|
if (process.env.BASE_URL) merged.baseUrl = process.env.BASE_URL;
|
|
113
129
|
else if (!merged.baseUrl) merged.baseUrl = 'https://api.eventmodelers.ai';
|
|
114
130
|
|
|
@@ -280,6 +296,37 @@ async function fetchAndPersistSlices(cfg, kitDir) {
|
|
|
280
296
|
console.log(`[agent] Persisted ${slices.length} slice(s)`);
|
|
281
297
|
}
|
|
282
298
|
|
|
299
|
+
// The summary refresh above leaves slice.json as an id/title/status stub, and a build agent that
|
|
300
|
+
// reads a stub reports the slice as empty. So before a slice is handed to an agent — a Planned
|
|
301
|
+
// build or a queued task — its full definition (elements, fields, specifications) is fetched
|
|
302
|
+
// here and merged in. Best effort: on failure the agent still has /load-slice to fall back on.
|
|
303
|
+
async function fetchFullSlice(cfg, kitDir, sliceId) {
|
|
304
|
+
if (!sliceId) return;
|
|
305
|
+
const slicesDir = join(kitDir, '.slices');
|
|
306
|
+
try {
|
|
307
|
+
let entry = null;
|
|
308
|
+
for (const ctx of readdirSync(slicesDir, { withFileTypes: true })) {
|
|
309
|
+
if (!ctx.isDirectory()) continue;
|
|
310
|
+
const indexPath = join(slicesDir, ctx.name, 'index.json');
|
|
311
|
+
if (!existsSync(indexPath)) continue;
|
|
312
|
+
const found = (JSON.parse(readFileSync(indexPath, 'utf-8')).slices ?? []).find((e) => e.id === sliceId);
|
|
313
|
+
if (found) { entry = { ...found, contextSlug: found.contextSlug ?? ctx.name }; break; }
|
|
314
|
+
}
|
|
315
|
+
if (!entry) return;
|
|
316
|
+
const query = new URLSearchParams({ contextName: entry.contextName, sliceId });
|
|
317
|
+
const url = `${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${query}`;
|
|
318
|
+
const { slices } = await fetchJSON(url, { headers: { 'x-token': cfg.token, ...agentHeaders(cfg) } });
|
|
319
|
+
const full = slices?.find((sl) => sl.id === sliceId);
|
|
320
|
+
if (!full) return;
|
|
321
|
+
const sliceJsonPath = join(slicesDir, entry.contextSlug, entry.folder, 'slice.json');
|
|
322
|
+
const existing = existsSync(sliceJsonPath) ? JSON.parse(readFileSync(sliceJsonPath, 'utf-8')) : {};
|
|
323
|
+
mkdirSync(dirname(sliceJsonPath), { recursive: true });
|
|
324
|
+
writeFileSync(sliceJsonPath, JSON.stringify({ ...existing, ...full }, null, 2), 'utf-8');
|
|
325
|
+
} catch (err) {
|
|
326
|
+
console.log(`[ralph] full slice fetch failed for ${sliceId} (${err.message}) — the agent will have to /load-slice it`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
283
330
|
// tasks.json has two writers: this process, queueing slice changes, and the agent, removing
|
|
284
331
|
// the task it just handled (as each stack's prompt.md tells it to). An unlocked read-modify-
|
|
285
332
|
// write from both sides at once lost whichever update landed first. So the two never overlap:
|
|
@@ -655,6 +702,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
|
|
|
655
702
|
|
|
656
703
|
if (credentialed && hasPendingTasks(kitDir)) {
|
|
657
704
|
const prompt = readFileSync(promptFile, 'utf-8');
|
|
705
|
+
for (const t of readTasks(join(kitDir, 'tasks.json'))) await fetchFullSlice(cfg, kitDir, t.payload?.sliceId);
|
|
658
706
|
await runWithRetry('onTask: loading slice from board...', () => inTurn(kitDir, () => onTask(prompt)));
|
|
659
707
|
await fetchAndPersistSlices(cfg, kitDir).catch(() => {});
|
|
660
708
|
didWork = true;
|
|
@@ -674,6 +722,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
|
|
|
674
722
|
}
|
|
675
723
|
|
|
676
724
|
const prompt = readFileSync(backendPromptFile, 'utf-8');
|
|
725
|
+
if (credentialed) await fetchFullSlice(cfg, kitDir, planned.id);
|
|
677
726
|
const built = await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, () => onPlannedSlice(prompt, {
|
|
678
727
|
sliceId: planned.id,
|
|
679
728
|
sliceTitle: planned.title,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// What a slice costs: one trace per build turn against one slice, posted to the platform's
|
|
2
|
-
// /api/org/:orgId/agent-traces. Only the runner can see what a turn cost, so it reports it.
|
|
2
|
+
// /api/org/:orgId/boards/:boardId/agent-traces. Only the runner can see what a turn cost, so it reports it.
|
|
3
3
|
//
|
|
4
4
|
// Best effort: each trace is appended to a local JSONL file, then POSTed once. No retries, no
|
|
5
5
|
// buffering. A failed upload is logged and forgotten; the file is the record.
|
|
@@ -110,13 +110,14 @@ export function sumUsage(parts) {
|
|
|
110
110
|
|
|
111
111
|
/** Records slice build turns. The agent id is the session: the platform requires one, and a
|
|
112
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 } = {}) {
|
|
113
|
+
export function createSliceTracer({ baseUrl, token, organizationId, boardId: defaultBoardId, agentId, traceFile, log = () => {}, enabled = true } = {}) {
|
|
114
114
|
const canUpload = !!(enabled && baseUrl && token && organizationId && agentId);
|
|
115
115
|
|
|
116
116
|
async function post(trace) {
|
|
117
|
-
|
|
117
|
+
// The route is per board — a trace without a board has nowhere to go, so it stays local.
|
|
118
|
+
if (!canUpload || !trace.boardId) return;
|
|
118
119
|
try {
|
|
119
|
-
const res = await fetch(`${baseUrl}/api/org/${organizationId}/agent-traces`, {
|
|
120
|
+
const res = await fetch(`${baseUrl}/api/org/${organizationId}/boards/${trace.boardId}/agent-traces`, {
|
|
120
121
|
method: 'POST',
|
|
121
122
|
headers: { 'Content-Type': 'application/json', 'x-token': token, 'x-agent-id': agentId },
|
|
122
123
|
body: JSON.stringify({ traces: [trace] }),
|
|
@@ -141,7 +142,7 @@ export function createSliceTracer({ baseUrl, token, organizationId, agentId, tra
|
|
|
141
142
|
return {
|
|
142
143
|
/** One build turn: `{sliceId, sliceTitle, context, ticketNumber, boardId, attempt, status}` plus its usage.
|
|
143
144
|
* ticketNumber is the slice's ticket at build time — it can change, so it travels with each turn. */
|
|
144
|
-
record({ sliceId, sliceTitle = null, context = null, ticketNumber = null, boardId = null, attempt = null, status = 'ok' }, usage) {
|
|
145
|
+
record({ sliceId, sliceTitle = null, context = null, ticketNumber = null, boardId = defaultBoardId ?? null, attempt = null, status = 'ok' }, usage) {
|
|
145
146
|
if (!sliceId || !usage) return;
|
|
146
147
|
const trace = {
|
|
147
148
|
id: newId(), occurredAt: new Date().toISOString(),
|
|
@@ -26,6 +26,7 @@ const tracer = createSliceTracer({
|
|
|
26
26
|
baseUrl: cfg.baseUrl,
|
|
27
27
|
token: cfg.token,
|
|
28
28
|
organizationId: cfg.organizationId,
|
|
29
|
+
boardId: cfg.boardId,
|
|
29
30
|
agentId: cfg.agentId,
|
|
30
31
|
traceFile: join(projectDir, '.eventmodelers', 'trace', 'slices.jsonl'),
|
|
31
32
|
log: (line) => console.log(`[ralph] ${line}`),
|
|
@@ -40,7 +41,9 @@ const timeoutMs = turnTimeoutMs(cfg);
|
|
|
40
41
|
const verbose = process.env.RALPH_VERBOSE === '1';
|
|
41
42
|
|
|
42
43
|
const claudeArgs = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
|
43
|
-
|
|
44
|
+
// RALPH_MODEL is `eventmodelers run --model` — one run's choice, over the configured `model`.
|
|
45
|
+
const model = process.env.RALPH_MODEL || cfg.model;
|
|
46
|
+
if (model) claudeArgs.push('--model', model);
|
|
44
47
|
const claudeEnv = {
|
|
45
48
|
...process.env,
|
|
46
49
|
...(cfg.anthropicBaseUrl ? { ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl } : {}),
|