@eventmodelers/cli 2.0.3 → 2.0.5

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/cli.js CHANGED
@@ -3863,8 +3863,10 @@ credentialFlags(program
3863
3863
  }
3864
3864
  // Asked once, for the kit in this directory (the first one found when there is none), and
3865
3865
  // handed to the others — the modeling agent reads this directory's config.
3866
- const rootDir = dirs.find((dir) => dirname(dir) === cwd) ?? dirs[0];
3867
- console.log(`\nšŸ”‘ Credentials for ${relative(cwd, rootDir) || '.'}, shared by all`);
3866
+ // With no build kit (--modeling alone), the credentials belong to this directory itself:
3867
+ // a kit dir that need not exist, since only its parent is used.
3868
+ const rootDir = dirs.find((dir) => dirname(dir) === cwd) ?? dirs[0] ?? join(cwd, '.agent-modeling-kit');
3869
+ console.log(`\nšŸ”‘ Credentials for ${dirs.length ? relative(cwd, rootDir) || '.' : 'the modeling agent'}, shared by all`);
3868
3870
  const { organizationId, boardId, token, baseUrl } = await forceBuildKitCredentials(rootDir, cwd, opts, globalOpts);
3869
3871
  for (const dir of dirs.filter((d) => d !== rootDir)) {
3870
3872
  await forceBuildKitCredentials(dir, cwd, opts, globalOpts, { organizationId, boardId, token, baseUrl });
@@ -5154,5 +5156,12 @@ program
5154
5156
  }
5155
5157
  });
5156
5158
 
5157
- await program.parseAsync();
5159
+ try {
5160
+ await program.parseAsync();
5161
+ } catch (err) {
5162
+ // A bug or bad input must end as one readable line, not a Node stack trace.
5163
+ console.error(`āŒ ${err?.message ?? err}`);
5164
+ if (process.env.EVENTMODELERS_DEBUG && err?.stack) console.error(err.stack);
5165
+ process.exitCode = 1;
5166
+ }
5158
5167
  if (sharedRl) sharedRl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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": {
@@ -5,7 +5,7 @@
5
5
  // log, each carrying the slice, the iteration id, the board, the org and the date — and a
6
6
  // `slice-done` after an iteration that settled the slice as Done. Always on.
7
7
  // sync with --sync, the events that name a slice are uploaded to POST …/boards/:boardId/buildlog,
8
- // numbered (the event's own `seq`, which defines the order) and tagged with the kit type (backend, or ui for a kit that picks up from Waiting) —
8
+ // numbered (only while the slice has an iteration open — an agent records only when it is active; the event's own `seq`, which defines the order) and tagged with the kit type (backend, or ui for a kit that picks up from Waiting) —
9
9
  // constantly, on a short interval for as long as the loop runs, so a live dashboard follows the
10
10
  // build, and once more at the start and end of each iteration.
11
11
  //
@@ -21,6 +21,8 @@ import { statusKey } from './slice-state.js';
21
21
 
22
22
  export const SYNC_STATE_FILE = 'build-log-sync.json';
23
23
  export const SYNC_INTERVAL_MS = 2_000;
24
+ // Always shared, even with no iteration open: they are what opens and closes one.
25
+ const BOUNDARY = new Set(['iteration-started', 'iteration-closed', 'slice-done']);
24
26
  const NOOP = { iterationStarted: () => null, iterationClosed: () => {}, flush: async () => {}, start: () => {}, stop: () => {} };
25
27
 
26
28
  function readState(path) {
@@ -61,8 +63,13 @@ export function createBuildLogShare({ kitDir, cfg, meta, agentType = 'BUILD', ki
61
63
  const entries = [];
62
64
  for (const e of fresh) {
63
65
  if (!e.slice) continue;
66
+ // An agent records only while it is active: events of a slice with no iteration open are not shared. The kit mirrors
67
+ // every slice's board status as `board-observed`, whether or not it is working on it — that is not the agent's work.
68
+ // The boundaries themselves (the iteration's start and end, and slice-done after it) always are.
69
+ if (!iterations[e.slice] && !BOUNDARY.has(e.type)) continue;
64
70
  if (e.type === 'iteration-started') iterations[e.slice] = e.iteration;
65
71
  entries.push(toBuildLogEntry(e, { agentId: cfg.agentId, iteration: e.iteration ?? iterations[e.slice], boardId: cfg.boardId, organizationId: cfg.organizationId, kitType, seq: e.seq }));
72
+ if (e.type === 'iteration-closed') delete iterations[e.slice];
66
73
  }
67
74
  if (entries.length) {
68
75
  const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/buildlog`, {
@@ -364,7 +364,7 @@ async function fetchFullSlice(cfg, kitDir, sliceId) {
364
364
  }
365
365
 
366
366
  async function handleSliceChanged(payload, cfg, kitDir) {
367
- console.log(`[agent] slice:changed — slice="${payload.sliceTitle}" status="${payload.sliceStatus}"`);
367
+ console.log(`[agent] slice:changed — slice="${payload.sliceTitle ?? payload.sliceId}" status="${payload.sliceStatus}"`);
368
368
  // This agent's own status writes (claim → InProgress, → Done, → Blocked) come straight back
369
369
  // as slice:changed. Queuing them only buys a turn in which the agent reads its own echo and
370
370
  // skips it, and re-reading the slices for them is wasted too: ralphLoop re-reads them right
@@ -372,7 +372,7 @@ async function handleSliceChanged(payload, cfg, kitDir) {
372
372
  // is dropped — a person's edit or another agent's, or an event with no attribution at all,
373
373
  // is still handled, so a missing field degrades to queuing rather than losing work.
374
374
  if (payload?.agentId && cfg.agentId && payload.agentId === cfg.agentId) {
375
- console.log(`[agent] Own write — not queued (slice="${payload.sliceTitle}" status="${payload.sliceStatus}")`);
375
+ console.log(`[agent] Own write — not queued (slice="${payload.sliceTitle ?? payload.sliceId}" status="${payload.sliceStatus}")`);
376
376
  return;
377
377
  }
378
378
  await fetchAndPersistSlices(cfg, kitDir).catch((err) =>
@@ -383,7 +383,7 @@ async function handleSliceChanged(payload, cfg, kitDir) {
383
383
  if (statusKey(payload.sliceStatus) !== statusKey(readPickupStatus(kitDir))) {
384
384
  try {
385
385
  queueTask({ kitDir, payload, meta: eventMeta(cfg) });
386
- console.log(`[agent] Task queued — slice="${payload.sliceTitle}" status="${payload.sliceStatus}"`);
386
+ console.log(`[agent] Task queued — slice="${payload.sliceTitle ?? payload.sliceId}" status="${payload.sliceStatus}"`);
387
387
  } catch (err) {
388
388
  console.error('[agent] queueTask error:', err);
389
389
  }
@@ -824,9 +824,12 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false,
824
824
 
825
825
  const prompt = readFileSync(backendPromptFile, 'utf-8') + taskRequest(planned, cfg, readPickupStatus(kitDir));
826
826
  if (credentialed) await fetchFullSlice(cfg, kitDir, planned.id);
827
+ // The iteration opens first, so the claim (→ InProgress) is part of what the board's build log shows.
828
+ if (planned.id) share.iterationStarted(planned);
827
829
  if (planned.id) {
828
830
  const claimed = await claimSlice({ kitDir, planned, board, meta, cleanTree: isTreeClean(kitDir), pickupStatus: readPickupStatus(kitDir) });
829
831
  if (claimed !== 'won') {
832
+ await share.iterationClosed(planned, 'skipped');
830
833
  console.log(claimed === 'held'
831
834
  ? `[ralph] Another run loop holds "${planned.title}" in this kit — waiting.`
832
835
  : `[ralph] Could not claim "${planned.title}" — skipping it this iteration.`);
@@ -836,7 +839,6 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false,
836
839
  }
837
840
  }
838
841
  lastReply = null;
839
- if (planned.id) share.iterationStarted(planned);
840
842
  const built = await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, async () => { lastReply = await onPlannedSlice(prompt, {
841
843
  sliceId: planned.id,
842
844
  sliceTitle: planned.title,
@@ -869,7 +871,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false,
869
871
 
870
872
  // ── Public API ────────────────────────────────────────────────────────────────
871
873
 
872
- export { HttpError, loadLocalConfig, fetchPlatformConfig, retryTransient, startRealtimeAgent, ensureLearningsGitRules };
874
+ export { HttpError, eventMeta, loadLocalConfig, fetchPlatformConfig, retryTransient, startRealtimeAgent, ensureLearningsGitRules };
873
875
 
874
876
  // The connect line a runner puts in front of every prompt. --local must mean zero board
875
877
  // contact, so it is empty then — handed live credentials, the agent would treat itself as
@@ -0,0 +1,17 @@
1
+ // A slice build's tool calls as events in the slice-state log, so `run --sync` can show them on the board. Any
2
+ // runner that sees its agent's individual tool calls records them through here, so every runner logs the same shape.
3
+
4
+ import { appendEvent } from './slice-events.js';
5
+ import { redactString } from './redact.js';
6
+
7
+ // Best effort, like the rest of the sharing: a failed write must not cost the build. `detail` is the one-line
8
+ // account of the call's input, with the token and anything JWT-shaped masked — a Bash command can hold
9
+ // credentials, and this log is shared. Only a planned-slice build (`slice.sliceId`) records anything.
10
+ export function recordToolCall({ kitDir, slice, tool, detail, cfg, meta }) {
11
+ if (!slice?.sliceId) return;
12
+ try {
13
+ appendEvent(kitDir, { type: 'tool-call', slice: slice.sliceId, tool, detail: redactString(detail, [cfg?.token]) }, meta);
14
+ } catch (err) {
15
+ console.error(`[ralph] could not record a tool call (${err.message})`);
16
+ }
17
+ }
@@ -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, resolveAgentIdentity, connectHeader } from './lib/ralph.js';
5
+ import { startRalph, eventMeta, loadLocalConfig, resolveAgentIdentity, connectHeader } from './lib/ralph.js';
6
6
  import { createSliceTracer, usageFromClaudeResult } from './lib/tracing.js';
7
7
  import { turnTimeoutMs, superviseTurn } from './lib/turn.js';
8
+ import { recordToolCall } from './lib/tool-call-events.js';
8
9
  import { spawn } from 'child_process';
9
10
  import { dirname, join, resolve } from 'path';
10
11
  import { fileURLToPath } from 'url';
@@ -106,6 +107,7 @@ function runClaude(prompt, slice = null, extraEnv = {}) {
106
107
  for (const block of msg.message?.content ?? []) {
107
108
  if (block.type === 'text' && block.text && verbose) console.log(block.text);
108
109
  if (block.type === 'tool_use') {
110
+ recordToolCall({ kitDir, slice, tool: block.name, detail: describeToolUse(block), cfg, meta: eventMeta(cfg) });
109
111
  if (verbose) console.log(`→ ${describeToolUse(block)}`);
110
112
  else if (block.name === 'Skill') console.log(`→ Skill: ${block.input?.skill ?? ''}`);
111
113
  else console.log(`→ ${block.name}`);