@eventmodelers/cli 2.0.51 → 2.0.53

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
@@ -42,7 +42,7 @@ import { recordAgentStatus } from './shared/build-kit/lib/slice-run.js';
42
42
  import { findSliceDir, fingerprint, readBaseline, writeBaseline, compare } from './lib/ui-baseline.js';
43
43
  import { defaultAgentName, withInstanceSuffix } from './shared/build-kit/lib/agent-name.js';
44
44
  import { acquireAgentId } from './shared/build-kit/lib/agent-lock.js';
45
- import { readEvents } from './shared/build-kit/lib/slice-events.js';
45
+ import { readEvents, appendEvent, locationOf } from './shared/build-kit/lib/slice-events.js';
46
46
  import { findPromptEvent, replayRequest } from './shared/build-kit/lib/prompt-replay.js';
47
47
  import { prunableEvents, pruneLog, rememberPruneAnswer, shouldOfferPrune } from './shared/build-kit/lib/event-prune.js';
48
48
  import { fold } from './shared/build-kit/lib/slice-state.js';
@@ -1060,15 +1060,28 @@ async function resolveStack(cliStack) {
1060
1060
  );
1061
1061
  }
1062
1062
 
1063
+ // Names what is installed now (from the kit's install manifest) and what this run installs.
1064
+ function printInstallVersions(kitDir, stackKey) {
1065
+ const prev = readJsonSafe(join(kitDir, '.eventmodelers', 'install-manifest.json'));
1066
+ if (prev.version) {
1067
+ const same = prev.version === CLI_VERSION;
1068
+ console.log(`Installed: v${prev.version}${prev.stack ? ` (${prev.stack})` : ''}`);
1069
+ console.log(`Installing: v${CLI_VERSION}${stackKey ? ` (${stackKey})` : ''}${same ? ' — same version, files are refreshed' : ''}\n`);
1070
+ } else {
1071
+ console.log(`Installed: nothing yet\nInstalling: v${CLI_VERSION}${stackKey ? ` (${stackKey})` : ''}\n`);
1072
+ }
1073
+ }
1074
+
1063
1075
  async function installStack(stackKey, stackCfg, options = {}) {
1064
- console.log('🚀 Eventmodelers CLI\n');
1065
- console.log(`Using: ${stackKey} (${stackCfg.label})\n`);
1076
+ console.log(`🚀 Eventmodelers CLI v${CLI_VERSION}\n`);
1077
+ console.log(`Using: ${stackKey} (${stackCfg.label})`);
1066
1078
 
1067
1079
  // Almost always the cwd. The exception is the global modeling install, which is
1068
1080
  // scaffolded into ~/.eventmodelers/kit from wherever `run --standalone` was invoked
1069
1081
  // (see ensureGlobalKit) — the mirror image of options.templatesSource below: where we
1070
1082
  // install TO, versus where we install FROM.
1071
1083
  const targetDir = options.targetDir ? resolve(options.targetDir) : process.cwd();
1084
+ printInstallVersions(join(targetDir, stackCfg.kitDirName), stackKey);
1072
1085
  // `init --git <url>` passes a resolved clone dir's templates/ here instead — every
1073
1086
  // other input (STACKS, MODELING_KIT) keeps using the built-in path.
1074
1087
  const templatesSource = options.templatesSource || join(__dirname, 'stacks', stackKey, 'templates');
@@ -1487,6 +1500,8 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
1487
1500
  // `re-init` for a build kit: re-copy shared/build-kit (ralph.js, the runners, tracing, adapters)
1488
1501
  // over the installed kit, then re-run the config steps. Stack templates and skills are untouched.
1489
1502
  async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
1503
+ console.log(`🚀 Eventmodelers CLI v${CLI_VERSION}\n`);
1504
+ printInstallVersions(kitDir, readJsonSafe(join(kitDir, '.eventmodelers', 'install-manifest.json')).stack);
1490
1505
  console.log(`📦 Refreshing the shared runtime in ${relative(targetDir, kitDir)}/ (stack files untouched)...`);
1491
1506
  copyDirContents(join(__dirname, 'shared', 'build-kit'), kitDir);
1492
1507
  // The kit is now this CLI's runtime — record that, so the startup "kit is outdated" hint stops.
@@ -5350,6 +5365,10 @@ program
5350
5365
  if (!wanted) {
5351
5366
  const claimed = Object.entries(fold(readEvents(kitDir)).slices).filter(([, s]) => s.claim);
5352
5367
  if (claimed.length !== 1) {
5368
+ if (!claimed.length) {
5369
+ // Where it looked: an agent running from another kit's directory finds no claim in that kit's log.
5370
+ try { appendEvent(kitDir, { type: 'claim-not-found', command: 'build-baseline', claimed: 0, ...locationOf(kitDir, cwd) }, agentEventMeta(cwd, kitDir, command)); } catch { /* best effort */ }
5371
+ }
5353
5372
  console.error(`❌ ${claimed.length ? `${claimed.length} slices are claimed` : 'No slice is claimed by the run loop'} — name the slice with --slice.`);
5354
5373
  process.exit(1);
5355
5374
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "2.0.51",
3
+ "version": "2.0.53",
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": {
@@ -26,8 +26,10 @@ function describe(e) {
26
26
  return [e.slice, e.status ?? ''];
27
27
  case 'claimed': {
28
28
  const tree = e.cleanTree === true ? 'clean tree' : e.cleanTree === false ? 'dirty tree' : 'not a git tree';
29
- return [e.slice, `pid ${e.pid}, ${tree}${e.base ? `, ${e.base.branch}@${String(e.base.head).slice(0, 8)}` : ''}${e.ticketNumber ? `, ticket ${e.ticketNumber}` : ''}${e.assignee ? `, assignee ${e.assignee}` : ''}`];
29
+ return [e.slice, `pid ${e.pid}, ${tree}${e.kitDir ? `, kit ${e.kitDir}` : ''}${e.base ? `, ${e.base.branch}@${String(e.base.head).slice(0, 8)}` : ''}${e.ticketNumber ? `, ticket ${e.ticketNumber}` : ''}${e.assignee ? `, assignee ${e.assignee}` : ''}`];
30
30
  }
31
+ case 'claim-not-found':
32
+ return [e.slice ?? e.wanted ?? '', `${e.command ?? 'command'} looked in ${e.kitDir ?? '?'} (cwd ${e.cwd ?? '?'}), ${e.claimed ?? 0} claimed there`];
31
33
  case 'claim-confirmed':
32
34
  return [e.slice, ''];
33
35
  case 'released':
@@ -8,6 +8,7 @@
8
8
  import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, writeFileSync } from 'fs';
9
9
  import { randomUUID } from 'crypto';
10
10
  import { join } from 'path';
11
+ import { homedir } from 'os';
11
12
 
12
13
  export const EVENTS_DIR = 'events';
13
14
  export const INDEX_FILE = 'events.log';
@@ -85,6 +86,14 @@ function nextSeq(kitDir) {
85
86
  // concurrent processes never interleave however large the event. After a torn index line the new id
86
87
  // starts on a fresh line instead of being glued onto the fragment. Every event gets the next sequence number
87
88
  // (`seq`), which — not the timestamp — defines the order. Returns the event id.
89
+ // Where a process ran, for events: the kit it works against and its working directory. The log is shared with
90
+ // everyone who can open the board, so the home directory is shown as `~` rather than the user's name.
91
+ export function locationOf(kitDir, cwd = process.cwd()) {
92
+ const home = homedir();
93
+ const tidy = (p) => (home && p && (p === home || p.startsWith(home + '/')) ? `~${p.slice(home.length)}` : p);
94
+ return { kitDir: tidy(kitDir), cwd: tidy(cwd) };
95
+ }
96
+
88
97
  export function appendEvent(kitDir, event, meta) {
89
98
  const dir = join(kitDir, EVENTS_DIR);
90
99
  mkdirSync(dir, { recursive: true });
@@ -5,7 +5,7 @@
5
5
  // `board` is injected: { setStatus(sliceId, status), claim(sliceId, fromStatus), readStatus(sliceId) → status | null }. null means
6
6
  // no board (--local): nothing is pushed or read, the log alone decides.
7
7
 
8
- import { appendEvent, readEvents } from './slice-events.js';
8
+ import { appendEvent, readEvents, locationOf } from './slice-events.js';
9
9
  import { fold, decideSettle, decideRecovery, statusKey } from './slice-state.js';
10
10
  import { adoptAgentEdits, projectStatus } from './slice-projection.js';
11
11
  import { discardUnfinishedWork, gitBase } from './slice-git.js';
@@ -35,17 +35,25 @@ export function isProcessAlive(pid) {
35
35
  // take the stale file for an edit.
36
36
  export function recordAgentStatus({ kitDir, status, sliceId, meta }) {
37
37
  const claimed = Object.entries(fold(readEvents(kitDir)).slices).filter(([, s]) => s.claim);
38
+ // A refusal is recorded with where it looked: the usual cause is an agent running from another kit's
39
+ // directory (an app with a root kit and a web/ kit), whose log has no claim for the slice.
40
+ const refuse = (message) => {
41
+ try {
42
+ appendEvent(kitDir, { type: 'claim-not-found', ...(sliceId ? { wanted: sliceId } : {}), claimed: claimed.length, command: 'slice-status', ...locationOf(kitDir) }, meta);
43
+ } catch { /* best effort: the refusal itself is what matters */ }
44
+ throw new Error(message);
45
+ };
38
46
  let match;
39
47
  if (sliceId) {
40
48
  const wanted = sliceId.trim().toLowerCase();
41
49
  match = claimed.find(([id, s]) => id === sliceId || s.claim.title?.toLowerCase() === wanted);
42
- if (!match) throw new Error(`slice "${sliceId}" is not claimed by the run loop — only a slice the loop is building has its status recorded here`);
50
+ if (!match) refuse(`slice "${sliceId}" is not claimed by the run loop — only a slice the loop is building has its status recorded here`);
43
51
  } else if (claimed.length === 1) {
44
52
  match = claimed[0];
45
53
  } else if (claimed.length > 1) {
46
54
  throw new Error(`${claimed.length} slices are claimed (${claimed.map(([, s]) => s.claim.title).join(', ')}) — name one with --slice`);
47
55
  } else {
48
- throw new Error('no slice is claimed by the run loop. For a task from tasks.json, set the status with the update-slice-status skill instead');
56
+ refuse('no slice is claimed by the run loop. For a task from tasks.json, set the status with the update-slice-status skill instead');
49
57
  }
50
58
  const [id, { claim }] = match;
51
59
  recordEvent(kitDir, { type: 'agent-recorded', slice: id, status }, meta);
@@ -78,7 +86,7 @@ export async function claimSlice({ kitDir, planned, board, meta, cleanTree, pid
78
86
  }
79
87
  }
80
88
  const state = recordEvent(kitDir, {
81
- type: 'claimed', slice: planned.id, ctx: planned.ctx, title: planned.title, pid, base: gitBase(kitDir), cleanTree,
89
+ type: 'claimed', slice: planned.id, ctx: planned.ctx, title: planned.title, pid, base: gitBase(kitDir), cleanTree, ...locationOf(kitDir),
82
90
  // What the board says about the slice at build time, when it says anything: both can change between builds.
83
91
  ...(planned.ticketNumber ? { ticketNumber: planned.ticketNumber } : {}),
84
92
  ...(planned.assignee ? { assignee: planned.assignee } : {}),
@@ -1,7 +1,7 @@
1
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
2
  // runner that sees its agent's individual tool calls records them through here, so every runner logs the same shape.
3
3
 
4
- import { appendEvent } from './slice-events.js';
4
+ import { appendEvent, locationOf } from './slice-events.js';
5
5
 
6
6
  // Only the tool's name is recorded — never its input. A Bash command, a skill's arguments or a path can hold
7
7
  // credentials or local details, and this log is shared with everyone who can open the board. Best effort, like the
@@ -9,7 +9,7 @@ import { appendEvent } from './slice-events.js';
9
9
  export function recordToolCall({ kitDir, slice, tool, meta }) {
10
10
  if (!slice?.sliceId) return;
11
11
  try {
12
- appendEvent(kitDir, { type: 'tool-call', slice: slice.sliceId, tool }, meta);
12
+ appendEvent(kitDir, { type: 'tool-call', slice: slice.sliceId, tool, ...locationOf(kitDir) }, meta);
13
13
  } catch (err) {
14
14
  console.error(`[ralph] could not record a tool call (${err.message})`);
15
15
  }