@eventmodelers/cli 2.0.50 → 2.0.51

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
@@ -893,7 +893,16 @@ function loadEffectiveConfig(cwd, kitDir, explicitPath) {
893
893
  const kitConfigExists = kitConfigPath && existsSync(kitConfigPath);
894
894
  const parentConfigPath = findConfigInParents(cwd);
895
895
 
896
- const merged = { ...readJsonSafe(parentConfigPath), ...(kitConfigExists ? readJsonSafe(kitConfigPath) : {}) };
896
+ // Credentials are inherited from a parent config, an agent identity never is: every project keeps its
897
+ // own `agentIds` (see ensureAgentId). Callers write this merged object back into the project's own
898
+ // config.json, so inheriting it would copy the parent's id into a nested project (an app and its `web/`
899
+ // UI kit) and both loops would then run as one agent and fight over each other's claims.
900
+ const parentConfig = readJsonSafe(parentConfigPath);
901
+ if (parentConfigPath && resolve(parentConfigPath) !== resolve(cwd, '.eventmodelers', 'config.json')) {
902
+ delete parentConfig.agentIds;
903
+ delete parentConfig.agentId;
904
+ }
905
+ const merged = { ...parentConfig, ...(kitConfigExists ? readJsonSafe(kitConfigPath) : {}) };
897
906
  const sources = [parentConfigPath, kitConfigExists ? kitConfigPath : null].filter(Boolean);
898
907
 
899
908
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "2.0.50",
3
+ "version": "2.0.51",
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": {
@@ -491,9 +491,21 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', createAdap
491
491
  }, 10 * 60 * 1000);
492
492
 
493
493
  let lastPingFailed = false;
494
+ // Backoff: a failing ping (e.g. 402 AGENT_LIMIT_REACHED) doubles the wait, 15s → 5min, so a refused agent
495
+ // doesn't hammer the platform and flood the log. Any success resets it. `shutting_down` always goes out.
496
+ const PING_BASE_MS = 15_000, PING_MAX_MS = 5 * 60_000;
497
+ let pingDelayMs = PING_BASE_MS;
498
+ let nextPingAt = 0;
499
+ const failPing = () => {
500
+ lastPingFailed = true;
501
+ nextPingAt = Date.now() + pingDelayMs;
502
+ console.error(`[agent] ${ts()} Next ping in ${Math.round(pingDelayMs / 1000)}s`);
503
+ pingDelayMs = Math.min(pingDelayMs * 2, PING_MAX_MS);
504
+ };
494
505
  // `status`: 'shutting_down' is the last ping before an exit the board asked for; every other ping says what the agent is
495
506
  // doing right now — building (modeling, for a MODELING agent) while a turn runs, idle otherwise.
496
507
  const ping = async (status) => {
508
+ if (status !== 'shutting_down' && Date.now() < nextPingAt) return;
497
509
  const agentStatus = status === 'shutting_down' ? status : turnsInFlight > 0 ? (agentType === 'MODELING' ? 'modeling' : 'building') : 'idle';
498
510
  try {
499
511
  const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
@@ -504,7 +516,8 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', createAdap
504
516
  });
505
517
  if (!res.ok) {
506
518
  console.error(`[agent] ${ts()} Ping failed: ${res.status} ${await res.text().catch(() => '')}`);
507
- lastPingFailed = true;
519
+ failPing();
520
+ agentRefused = res.status === 402 || res.status === 403 ? `HTTP ${res.status}` : null;
508
521
  if (res.status === 401) {
509
522
  await refreshToken('ping-401').catch((err) => console.error(`[agent] ${ts()} Token refresh after 401 ping failed:`, err));
510
523
  }
@@ -512,9 +525,12 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', createAdap
512
525
  }
513
526
  if (lastPingFailed) console.log(`[agent] ${ts()} Ping recovered`);
514
527
  lastPingFailed = false;
528
+ agentRefused = null;
529
+ pingDelayMs = PING_BASE_MS;
530
+ nextPingAt = 0;
515
531
  } catch (err) {
516
532
  console.error(`[agent] ${ts()} Ping error:`, err);
517
- lastPingFailed = true;
533
+ failPing();
518
534
  }
519
535
  };
520
536
  await ping();
@@ -713,6 +729,9 @@ export async function blockStuckSlice(kitDir, cfg, credentialed, planned, attemp
713
729
  // otherwise. A change is reported at once (`reportStatus`, set once the heartbeat exists) instead of at the next 15s ping.
714
730
  let turnsInFlight = 0;
715
731
  let reportStatus = () => {};
732
+ // Set while the platform refuses this agent's heartbeat with 402/403 (e.g. AGENT_LIMIT_REACHED): the agent isn't
733
+ // a connected build agent, so the loop must not claim slices or change their status until a ping is accepted.
734
+ let agentRefused = null;
716
735
 
717
736
  async function runWithRetry(label, fn) {
718
737
  for (let attempt = 1; ; attempt++) {
@@ -865,6 +884,13 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false,
865
884
  while (true) {
866
885
  let didWork = false;
867
886
 
887
+ // Not accepted as a connected agent (license/agent limit): touch nothing on the board, just wait for the heartbeat to recover.
888
+ if (credentialed && agentRefused) {
889
+ console.log(`[ralph] Heartbeat refused by the platform (${agentRefused}) — not claiming slices until it is accepted.`);
890
+ await sleep(30_000);
891
+ continue;
892
+ }
893
+
868
894
  // A claim still open here is a leftover: its process died mid-build, or settling failed last time.
869
895
  await settleLeftoverClaims({ kitDir, board, meta, pickupStatus: readPickupStatus(kitDir) });
870
896
 
@@ -43,8 +43,8 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
43
43
  | `get_board_outline` | `boardId`, `chapterId` | One chapter's structure, compact: per-column node lists (`{id, type, title, lane, cellName}`) + a flat edge list, no HTML pages / field bodies / meta. `cellName` is each node's real cell address (column letter + row number) — the only read that returns cell names at all; use it when a placement needs a cell address. For an empty target cell, take the column's `letter` plus the lane's 1-based position in the outline's `lanes` list (e.g. `letter: "C"`, second lane → `C2`) — never convert array indexes yourself. The cheap "what is where and how is it wired" read — prefer over `get_nodes` (no projection) for orientation checks | — (MCP-only convenience) |
44
44
  | `get_connected_nodes` | `boardId`, `nodeId`, `chapterId?`, `direction?` (`inbound`/`outbound`/`both`), `depth?`, `types?`, `includeFields?` | Neighbours of **one** node — what feeds it and what it feeds. Answers from a single anchor, unlike `get_attribute_chain` (which needs both ends of the chain up front). `depth` follows a whole chain; `types` filters the result only, never the traversal. Each neighbour carries `via`: `"edge"` for a real connection, `"layout"` when the node has none in that direction and the neighbour was inferred from the grid using auto-connect's own window (own column + adjacent one, forward-only pairs). Real edges always win. The `layout` fallback is what makes hand-built/imported chapters — which routinely carry **zero** edges — readable instead of falsely empty | — (MCP-only convenience) |
45
45
  | `validate_model` | `boardId`, `chapterId`, `checks?[]` | Server-side Event Modeling structural checklist over one chapter — compact `findings` only. Checks: unplaced nodes, backward arrows (with the todo-list `EVENT→READMODEL` exception), zero/multi-issuer commands, sourceless read models, two-screens-in-a-column, missing scenarios. Replaces the manual per-type `get_nodes` + `get_node projection=edges` validation pass | — (MCP-only convenience) |
46
- | `add_scenario` | `boardId`, `timelineId`, `columnId`, `scenarios[]`, `compact?` | Append GWT scenario(s) to a column's spec node — created automatically, and a scenario `id` is generated when omitted. A given/when/then step may be addressed by `{title, type}` instead of a node id, resolved against that timeline, so no `get_spec_info` call is needed first (an ambiguous title is reported with its candidates). `compact: true` returns `{specNodeId, added, scenarioCount, isNewNode}` instead of echoing every scenario back | §6 `POST .../scenarios` |
47
- | `add_storyline` | `boardId`, `timelineId`, `columnId`, `storylines[]`, `compact?` | Append storyline(s) (ordered, branchable beats over existing elements) to a column's spec node. Storyline and beat ids are generated when omitted, and a beat may name its element by `{title, type}` instead of `refId` (same addressing as `add_scenario`; an ambiguous title is reported with its candidates). Use whenever `eventmodeling-elaborating-scenarios`'s GWT-vs-storyline decision rule calls for one (e.g. a todo list's open→close lifecycle) — not only when a user explicitly names "storyline"; that skill's own per-read-model judgment is the trigger, this catalog entry isn't a stricter gate on top of it. `compact: true` suppresses the full storyline echo | §6 `POST .../storylines` |
46
+ | `add_scenario` | `boardId`, `timelineId`, `columnId`, `scenarios[]`, `compact?` | Append GWT scenario(s) to a column's spec node — **the column of the original element, never of a `meta.linkedTo` copy** (follow `linkedTo` to the origin first); created automatically, and a scenario `id` is generated when omitted. A given/when/then step may be addressed by `{title, type}` instead of a node id, resolved against that timeline, so no `get_spec_info` call is needed first (an ambiguous title is reported with its candidates). `compact: true` returns `{specNodeId, added, scenarioCount, isNewNode}` instead of echoing every scenario back | §6 `POST .../scenarios` |
47
+ | `add_storyline` | `boardId`, `timelineId`, `columnId`, `storylines[]`, `compact?` | Append storyline(s) (ordered, branchable beats over existing elements) to a column's spec node — **the original's column, never a `meta.linkedTo` copy's**. Storyline and beat ids are generated when omitted, and a beat may name its element by `{title, type}` instead of `refId` (same addressing as `add_scenario`; an ambiguous title is reported with its candidates). Use whenever `eventmodeling-elaborating-scenarios`'s GWT-vs-storyline decision rule calls for one (e.g. a todo list's open→close lifecycle) — not only when a user explicitly names "storyline"; that skill's own per-read-model judgment is the trigger, this catalog entry isn't a stricter gate on top of it. `compact: true` suppresses the full storyline echo | §6 `POST .../storylines` |
48
48
  | `set_connection` | `boardId`, `connections[]` (each `source`, `target`, `action` `'connect'\|'remove'`), `compact?` | Add or remove type-checked directed edges, applied in order (a later entry can rely on an earlier edge); answers `{results}`, or with `compact: true` a `{connected, existed, removed, notFound, failed, errors}` tally | — (via `edges` on §3 events) |
49
49
  | `auto_connect_node` | `boardId`, `nodeIds[]` | Re-run auto-connect for nodes — incompatible neighbours come back in `skipped`, not as an error; answers `{results}` | §3 `POST .../nodes/:nodeId/auto-connect` |
50
50
  | `link_element` | `boardId`, `nodeId`, plus either `targetNodeId` or `timelineId` (+ `columnIndex?`, `lane?`) | Turn a node into a linked copy of `nodeId` — it receives a full copy of that node's meta plus `meta.linkedTo`. Name an existing `targetNodeId`, or pass `timelineId` to have the copy placed and linked in this one call (inheriting the original's type and title), which is what a translation or automation chain wants | §3 `POST .../nodes/:nodeId/link` |
@@ -66,6 +66,8 @@ Get the elements a scenario's given/when/then steps may legally reference (EVENT
66
66
  An ordered, branchable walkthrough of existing board elements ("beats"), stored alongside GWT
67
67
  scenarios on the same SCENARIO spec node, in a sibling `meta.storylines` collection.
68
68
 
69
+ > **Scenarios and storylines belong to the original element's column.** If the node has `meta.linkedTo` it is a copy: follow it to the origin and post to the origin's timeline/column. The API does not reject a copy's column, so the caller must resolve it.
70
+
69
71
  ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/columns/:columnId/storylines`
70
72
  Append one or more storylines to a column's spec node. The spec node is auto-created if missing
71
73
  (shared with scenarios).
@@ -346,6 +346,8 @@ if Conway's Law boundaries are not relevant to the project.
346
346
 
347
347
  Invoke `eventmodeling-elaborating-scenarios` with the Skill tool **before the first `add_scenario` call** — its scenario types, business-rules-only rule and payload shape are not repeated here, so scenarios written from this file alone come out wrong.
348
348
 
349
+ **Linked copies get no scenarios.** A command/read model with `meta.linkedTo` is a copy; add its scenarios and storylines to the origin's column (its own timeline), never the copy's.
350
+
349
351
  **Input**: Commands and read models.
350
352
  **Output to carry forward**: Given-When-Then specifications (or storylines, for
351
353
  walkthrough-style coverage) for every command **and every read model**, posted
@@ -128,6 +128,8 @@ After resolving `columnId` (from step 3, or from step 5 if a new column was just
128
128
 
129
129
  **Prefer MCP:**
130
130
 
131
+ > **Original only:** if the target COMMAND/READMODEL has `meta.linkedTo` it is a copy — resolve to the origin (no `linkedTo`) and use the origin's `timelineId`/`columnId`; never put a spec on a copy's column. Same for `add_storyline`. See `eventmodeling-core-rules`.
132
+
131
133
  ```
132
134
  mcp__eventmodelers__add_scenario {
133
135
  "boardId": "<BOARD_ID>",