@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 +10 -1
- package/package.json +1 -1
- package/shared/build-kit/lib/ralph.js +28 -2
- package/shared/skills/learn-eventmodelers-api/references/mcp-tool-catalog.md +2 -2
- package/shared/skills/learn-eventmodelers-api/references/s06-specifications.md +2 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +2 -0
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +2 -0
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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>",
|