@eventmodelers/cli 1.0.70 → 1.0.72

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 CHANGED
@@ -272,9 +272,12 @@ the turn's own instructions rather than being enforced from outside — the `cla
272
272
  what spawns the agents — so it's a budget the agent is told to keep, not a hard ceiling.
273
273
 
274
274
  Every event that arrives is remembered until a turn carries it — including events that land
275
- while a turn is running. Its own writes come back on that same channel and the platform can't
276
- tell them apart from a human's, so those are *labelled* for the agent rather than discarded,
277
- and the lane is damped on timing instead: it waits for a quiet period (but not forever), waits
275
+ while a turn is running. Its own writes come back on that same channel, and each event says who
276
+ made it (`agent_id` from the writer's `x-agent-id` header, `user_id` for a browser), so the
277
+ agent's own echo is recognized exactly: a burst that is nothing but its own writes is dropped
278
+ without spending a turn, and one that is mixed lists its own lines marked as such. Only a write
279
+ that reached the platform with neither id falls back to the echo window's guess and is labelled
280
+ "possibly your own". The lane is damped on timing on top of that: it waits for a quiet period (but not forever), waits
278
281
  out the echo window of its last turn, never fires twice in quick succession, and widens that
279
282
  floor each time it answers `NOOP`, so a finished board goes quiet by itself. Override the
280
283
  windows if the defaults don't suit your board:
@@ -283,7 +286,7 @@ windows if the defaults don't suit your board:
283
286
  |---|---|---|
284
287
  | `EVENTMODELERS_STANDALONE_DEBOUNCE_MS` | `8000` | quiet period before buffered board changes turn into a turn |
285
288
  | `EVENTMODELERS_STANDALONE_MAX_WAIT_MS` | `90000` | cap on that quiet period, so a board being edited continuously still gets a turn |
286
- | `EVENTMODELERS_STANDALONE_ECHO_WINDOW_MS` | `20000` | after a turn, how long incoming changes are labelled as probably the agent's own echo |
289
+ | `EVENTMODELERS_STANDALONE_ECHO_WINDOW_MS` | `20000` | after a turn, how long an *unattributed* incoming change is labelled as probably the agent's own echo (attributed ones are identified outright) |
287
290
  | `EVENTMODELERS_STANDALONE_MIN_INTERVAL_MS` | `60000` | floor between two self-directed turns |
288
291
  | `EVENTMODELERS_STANDALONE_BACKOFF_CAP_MS` | `900000` | ceiling that floor doubles up to while turns keep answering `NOOP` |
289
292
  | `EVENTMODELERS_STANDALONE_IDLE_MS` | `900000` | with nothing happening at all, how long before the agent reviews the model anyway (`0` disables it) |
@@ -416,6 +419,18 @@ npx @eventmodelers/cli run --id <id> # pin ONE identit
416
419
 
417
420
  `run --id` is what you want when an agent has to keep the same identity every time it starts: a supervisor that already knows the id, a second agent of the same type in one *project* (which would otherwise share the project's single minted id), or an agent a board has starred as its **preferred agent** — that star addresses prompts to one id, so an agent whose id changes per run loses it on restart. `run --id`/`run --name` are per-run only: nothing is written to disk.
418
421
 
422
+ #### Working only what you were addressed (`--exclusive`)
423
+
424
+ By default an agent claims two kinds of prompt: the ones addressed to its own id, and every prompt nobody addressed to anyone. That's right for the single agent on a board, and wrong for a dedicated one — a specialist sitting next to a general agent, or an agent a supervisor drives by id, ends up answering whatever the queue happens to hold. `--exclusive` drops that second kind:
425
+
426
+ ```bash
427
+ npx @eventmodelers/cli run --standalone --board-id <uuid> --id <agent-uuid> --exclusive
428
+ ```
429
+
430
+ Only prompts carrying this agent's id are worked. Anything untargeted is handed straight back to the queue (status `ADDED`) for another agent to take — the addressee filter lives in the queue's claim query, which hands an agent its own prompts *and* the untargeted ones, so claiming is the only way to find out which arrived. An exclusive run therefore claims as usual and gives back what wasn't meant for it, once it has walked past it to its own work.
431
+
432
+ Pair it with `--id`: a `--standalone`/`--global` run mints a fresh id per run, so prompts addressed to the previous run's id are never claimed. `--exclusive` applies to the prompt queue only — a `--standalone` agent's self-directed turns are nobody's prompt, and it keeps taking them.
433
+
419
434
  ### Env vars and `--config` (scripted/CI installs)
420
435
 
421
436
  Every config field can be set via an `EVENTMODELERS_*` env var instead of the interactive prompts — these always win over whatever's in `config.json`, so a fully env-driven install never prompts for credentials or Claude execution settings:
package/cli.js CHANGED
@@ -1310,7 +1310,15 @@ function configureHooks({ hooksSrc, targetDir }) {
1310
1310
  // per task, or a long-lived warm process) must call this first — a `.mcp.json`
1311
1311
  // written mid-session by the process itself is too late for that same process.
1312
1312
  // The token itself is never written to disk here — `${EVENTMODELERS_TOKEN}` is
1313
- // resolved by `claude` from its own process env, which the caller must set.
1313
+ // resolved by `claude` from its own process env, which the caller must set. Same for
1314
+ // `${EVENTMODELERS_AGENT_ID}`: without it on the transport, every MCP write this agent makes
1315
+ // reaches the platform unattributed (board_events.agent_id null), and the board shows one
1316
+ // anonymous robot for it instead of this agent's name. It belongs here rather than only in the
1317
+ // skill's Step 3.5 because this entry is rewritten on every run — it would otherwise overwrite
1318
+ // the header the skill just added — and because a `.mcp.json` fixed mid-session comes too late
1319
+ // for the `claude` process already running. An unset var arrives at the server as the literal
1320
+ // `${EVENTMODELERS_AGENT_ID}` text, which it drops (it only accepts a uuid), so the entry is the
1321
+ // same one for a human's session and an agent's.
1314
1322
  function ensureMcpRegistered(projectDir, baseUrl) {
1315
1323
  const mcpConfigPath = join(projectDir, '.mcp.json');
1316
1324
  const mcpConfig = readJsonSafe(mcpConfigPath);
@@ -1318,7 +1326,7 @@ function ensureMcpRegistered(projectDir, baseUrl) {
1318
1326
  mcpConfig.mcpServers.eventmodelers = {
1319
1327
  type: 'http',
1320
1328
  url: `${baseUrl}/mcp`,
1321
- headers: { 'x-token': '${EVENTMODELERS_TOKEN}' },
1329
+ headers: { 'x-token': '${EVENTMODELERS_TOKEN}', 'x-agent-id': '${EVENTMODELERS_AGENT_ID}' },
1322
1330
  };
1323
1331
  writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
1324
1332
  }
@@ -1672,7 +1680,13 @@ async function ensureGlobalKit(baseUrl) {
1672
1680
  // question, sketch a screen. Without the flag that channel is still subscribed on
1673
1681
  // the same connection and every event on it is dropped, so the two modes differ by
1674
1682
  // one filter rather than by a whole second realtime stack.
1675
- async function runModeling(kitDir, projectDir, verbose = false, standalone = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}) {
1683
+ //
1684
+ // `exclusive` narrows the prompt lane to this agent alone: only a prompt the user
1685
+ // addressed to this agent id (the board's "preferred agent") is worked, and anything
1686
+ // untargeted is handed straight back to the queue for another agent to take. It says
1687
+ // nothing about the standalone lane — a self-directed turn is nobody's task, so an
1688
+ // exclusive standalone agent still works the board on its own initiative.
1689
+ async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {} } = {}) {
1676
1690
  const configLibPath = join(kitDir, 'lib', 'config.js');
1677
1691
  if (!existsSync(configLibPath)) {
1678
1692
  console.error(`❌ ${relative(process.cwd(), configLibPath)} not found — --modeling needs a kit installed via \`init --modeling\`.`);
@@ -1710,6 +1724,12 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1710
1724
  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.');
1711
1725
  process.exit(1);
1712
1726
  }
1727
+ // Nothing can be addressed to an agent with no id, so an exclusive run without one would
1728
+ // hand every prompt back and sit idle forever — a silent no-op worth failing on instead.
1729
+ if (exclusive && !cfg.agentId) {
1730
+ console.error('❌ --exclusive needs an agent id — that is what a prompt is addressed to. Pass `run --id <uuid>` (or let the kit mint one) and address the prompt to it on the board.');
1731
+ process.exit(1);
1732
+ }
1713
1733
 
1714
1734
  const subagentModel = cfg.subagentModel || DEFAULT_SUBAGENT_MODEL;
1715
1735
 
@@ -1928,6 +1948,13 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1928
1948
  ? `standalone: ON — reacting to direct prompts AND to board changes on its own initiative (max ${maxAgents} subagent(s) per self-directed turn)`
1929
1949
  : 'standalone: off — reacting to direct prompts only (board changes are dropped)',
1930
1950
  );
1951
+ if (exclusive) {
1952
+ log(`exclusive: ON — only prompts addressed to ${cfg.agentId} are worked; every untargeted prompt is handed back to the queue`);
1953
+ // A global/standalone run mints its id per run (see resolveModelingCredentials), so an id
1954
+ // someone addressed a prompt to yesterday is not this agent — worth saying out loud here,
1955
+ // where the alternative is an agent that looks healthy and quietly works nothing.
1956
+ if (overrides && !identity.agentId) log('exclusive: this run minted a fresh agent id — star it on the board now, or restart with `--id <uuid>` to keep one addressable identity');
1957
+ }
1931
1958
  warmUpSession();
1932
1959
 
1933
1960
  async function getRealtimeToken() {
@@ -1947,15 +1974,47 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1947
1974
  return res.json();
1948
1975
  }
1949
1976
 
1977
+ // Puts a prompt this agent claimed but will not work back on the queue (CLAIMED -> ADDED),
1978
+ // so whichever agent it was actually open to can still take it. `x-token` only — the status
1979
+ // endpoint is meant to be called by the agent holding the prompt.
1980
+ async function releasePrompt(promptId) {
1981
+ const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/${promptId}/status`, {
1982
+ method: 'POST',
1983
+ headers: { 'x-token': cfg.token, 'Content-Type': 'application/json', ...agentHeaders(cfg) },
1984
+ body: JSON.stringify({ status: 'ADDED' }),
1985
+ });
1986
+ if (!res.ok) throw new Error(`prompts/${promptId}/status: HTTP ${res.status}`);
1987
+ }
1988
+
1950
1989
  let realtimeToken = await getRealtimeToken();
1951
1990
 
1952
1991
  let draining = false;
1953
1992
  async function drain() {
1954
1993
  if (draining) return;
1955
1994
  draining = true;
1995
+ // --exclusive only: prompts claimed in this pass that weren't addressed to this agent,
1996
+ // handed back once the pass is over (see below).
1997
+ const handBack = [];
1956
1998
  try {
1957
1999
  let p;
1958
2000
  while ((p = await fetchNextPrompt(realtimeToken)) !== null) {
2001
+ // The queue can't filter by addressee for us: `prompts/next` hands an agent both the
2002
+ // prompts addressed to it and every untargeted one (`agent_id IS NULL`) — claiming is
2003
+ // what reveals which kind arrived — so an exclusive run claims as usual and gives back
2004
+ // what wasn't meant for it.
2005
+ //
2006
+ // The hand-back is deferred to the end of the pass on purpose: a prompt released
2007
+ // mid-loop goes straight back to the head of the very queue this loop is reading, so
2008
+ // the next fetch would return the prompt just released instead of the addressed one
2009
+ // queued behind it, and the agent would never reach its own work. Holding them CLAIMED
2010
+ // until the queue runs dry walks past them instead. The cost is a brief CLAIMED blip on
2011
+ // someone else's prompt, and — if no other agent happens to be draining when the
2012
+ // hand-back lands — that prompt waiting for the next `prompt:created` to be noticed.
2013
+ if (exclusive && (p.agent_id ?? null) !== cfg.agentId) {
2014
+ log(`prompt ${p.id} ${p.agent_id ? `is addressed to agent ${p.agent_id}` : 'is addressed to no agent'} — handing it back (--exclusive)`);
2015
+ handBack.push(p.id);
2016
+ continue;
2017
+ }
1959
2018
  log(`prompt received: "${p.prompt}" (board=${p.board_id ?? cfg.boardId ?? 'n/a'}, priority=${p.priority})`);
1960
2019
  try {
1961
2020
  await runClaudeWarm(buildTurn(p));
@@ -1964,6 +2023,16 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1964
2023
  }
1965
2024
  }
1966
2025
  } finally {
2026
+ for (const id of handBack) {
2027
+ try {
2028
+ await releasePrompt(id);
2029
+ } catch (err) {
2030
+ // Left CLAIMED, which is worse for whoever sent it than a retry would be — but
2031
+ // retrying here risks wedging the loop, and the next pass claims nothing new
2032
+ // while this one is still unwinding. Say so and move on.
2033
+ log(`handing prompt ${id} back failed, it stays CLAIMED: ${err.message}`);
2034
+ }
2035
+ }
1967
2036
  draining = false;
1968
2037
  // A prompt turn counts as activity: the board isn't idle just because nobody edited
1969
2038
  // it while the agent was busy answering someone.
@@ -1983,20 +2052,22 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1983
2052
  // later adds to these payloads lands here without a client change.
1984
2053
  const BOARD_CHANGE_EVENTS = ['node:created', 'node:changed', 'node:deleted', 'edge:added', 'edge:removed', 'board:cleared'];
1985
2054
 
1986
- // Four knobs, because a board event can't tell you who caused it: the platform
1987
- // attributes an API token's writes to the org owner's user_id, so on this channel the
1988
- // agent's own edits are indistinguishable from the human's. They all govern *when* a
1989
- // self-directed turn fires — never whether an event is remembered. Everything that
1990
- // arrives is buffered (see onBoardEvent): an event seen while a turn runs, or inside the
1991
- // echo window, is only *marked* as possibly the agent's own write, so a burst that
2055
+ // Four knobs. They govern *when* a self-directed turn fires never whether an event is
2056
+ // remembered: everything that arrives is buffered (see onBoardEvent), so a burst that
1992
2057
  // straddles a turn boundary still reaches the next turn instead of being thrown away and
1993
2058
  // leaving the agent looking at whichever single event happened to land last.
2059
+ // Who wrote an event is *read off the event*, not inferred from these windows: the payload
2060
+ // carries `agent_id` (stamped from the writer's `x-agent-id` header) and `user_id` (a
2061
+ // browser session), so this agent's own echo is identified exactly and dropped without
2062
+ // costing a turn. The windows below only cover the one case attribution can't: a write that
2063
+ // carries neither id.
1994
2064
  // DEBOUNCE — one gesture (place a node, drag a column) fans out into several
1995
2065
  // events; wait for the board to fall quiet, then send a single turn.
1996
2066
  // MAX_WAIT — cap on that quiet period: a board someone keeps editing never falls
1997
2067
  // quiet, and the debounce alone would slide forever.
1998
- // ECHO_WINDOW — how long after a turn its own writes are expected back; changes in
1999
- // that window are labelled, and the next turn waits it out.
2068
+ // ECHO_WINDOW — how long after a turn its own writes are expected back; an
2069
+ // *unattributed* change in that window is labelled a possible echo, and
2070
+ // the next turn waits it out so a write and its echo don't each get one.
2000
2071
  // MIN_INTERVAL — a floor between self-directed turns, so a mistake upstream can't
2001
2072
  // become a self-feeding loop burning tokens unattended. Doubles per
2002
2073
  // consecutive NOOP up to BACKOFF_CAP, and resets as soon as a turn
@@ -2016,10 +2087,11 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2016
2087
  const STANDALONE_BACKOFF_CAP_MS = envMs('EVENTMODELERS_STANDALONE_BACKOFF_CAP_MS', 15 * 60_000);
2017
2088
  const STANDALONE_IDLE_MS = envMs('EVENTMODELERS_STANDALONE_IDLE_MS', 15 * 60_000);
2018
2089
 
2019
- // node_id (or '(board)') -> { types: Set<string>, count: number, maybeOwn: boolean }
2020
- // for everything seen since the last self-directed turn. `maybeOwn` stays true only
2021
- // while every event for that node arrived while a turn was running or inside the echo
2022
- // window one event from outside that window and the node is a real change again.
2090
+ // node_id (or '(board)') -> { types: Set<string>, count, own, other, maybe } for
2091
+ // everything seen since the last self-directed turn, each event counted into exactly one
2092
+ // origin bucket: `own` = attributed to this agent's own id, `other` = attributed to a human
2093
+ // or another agent, `maybe` = carries no attribution at all and landed inside the echo
2094
+ // window, so it *might* be this agent's. Only `maybe` is a guess.
2023
2095
  const observed = new Map();
2024
2096
  let observedCount = 0;
2025
2097
  let seqLo = null;
@@ -2036,6 +2108,16 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2036
2108
  return Math.min(STANDALONE_MIN_INTERVAL_MS * 2 ** noopStreak, STANDALONE_BACKOFF_CAP_MS);
2037
2109
  }
2038
2110
 
2111
+ // Empties the buffer — every field of it, which is why it is one function and not five
2112
+ // lines repeated at each place a burst stops being pending.
2113
+ function resetObserved() {
2114
+ observed.clear();
2115
+ observedCount = 0;
2116
+ seqLo = null;
2117
+ seqHi = null;
2118
+ firstObservedAt = 0;
2119
+ }
2120
+
2039
2121
  function onBoardEvent(type, payload) {
2040
2122
  if (!standalone) {
2041
2123
  if (verbose) log(`board event ${type} dropped — not running with --standalone`);
@@ -2046,12 +2128,26 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2046
2128
  // The warm-up turn is read-only, so a change that lands while it runs is somebody
2047
2129
  // else's — labelling it "possibly your own write" would only teach the agent to
2048
2130
  // discount the very edits it just came up to work on.
2049
- const maybeOwn = (!!pending && !warmingUp) || draining || inEchoWindow;
2131
+ const guessOwn = (!!pending && !warmingUp) || draining || inEchoWindow;
2132
+ // Attribution beats the clock in both directions. `agent_id === ours` is this agent's own
2133
+ // write, certainly, whenever it comes back. Any *other* id — a person's user_id, another
2134
+ // agent's — is certainly not ours, which is the half the timer used to get wrong: a human
2135
+ // editing while this agent worked had their change written off as an echo of it.
2136
+ const writerAgent = payload?.agent_id || null;
2137
+ const writerUser = payload?.user_id || null;
2138
+ const origin =
2139
+ writerAgent && cfg.agentId && writerAgent === cfg.agentId
2140
+ ? 'own'
2141
+ : writerAgent || writerUser
2142
+ ? 'other'
2143
+ : guessOwn
2144
+ ? 'maybe'
2145
+ : 'other';
2050
2146
  const nodeId = payload?.node_id ?? '(board)';
2051
- const entry = observed.get(nodeId) ?? { types: new Set(), count: 0, maybeOwn };
2147
+ const entry = observed.get(nodeId) ?? { types: new Set(), count: 0, own: 0, other: 0, maybe: 0 };
2052
2148
  entry.types.add(type);
2053
2149
  entry.count += 1;
2054
- if (!maybeOwn) entry.maybeOwn = false;
2150
+ entry[origin] += 1;
2055
2151
  observed.set(nodeId, entry);
2056
2152
  observedCount += 1;
2057
2153
  if (!firstObservedAt) firstObservedAt = Date.now();
@@ -2060,7 +2156,17 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2060
2156
  if (seqLo === null || seq < seqLo) seqLo = seq;
2061
2157
  if (seqHi === null || seq > seqHi) seqHi = seq;
2062
2158
  }
2063
- log(`board change: ${type} node=${nodeId}${Number.isFinite(seq) ? ` seq=${seq}` : ''}${maybeOwn ? ' (maybe own write)' : ''}`);
2159
+ const writtenBy =
2160
+ origin === 'own'
2161
+ ? ' — own write'
2162
+ : origin === 'maybe'
2163
+ ? ' — unattributed, maybe own write'
2164
+ : writerUser
2165
+ ? ' — by a person'
2166
+ : writerAgent
2167
+ ? ` — by agent ${writerAgent.slice(0, 8)}`
2168
+ : '';
2169
+ log(`board change: ${type} node=${nodeId}${Number.isFinite(seq) ? ` seq=${seq}` : ''}${writtenBy}`);
2064
2170
  armStandaloneTurn(nextDelayMs());
2065
2171
  }
2066
2172
 
@@ -2125,10 +2231,17 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2125
2231
  'nothing, change nothing and reply <promise>NOOP</promise>.';
2126
2232
 
2127
2233
  function buildStandaloneTurn() {
2128
- const lines = [...observed.entries()].map(
2129
- ([nodeId, entry]) =>
2130
- `- ${nodeId}: ${[...entry.types].join(', ')} (${entry.count}×)${entry.maybeOwn ? ' — possibly your own earlier write' : ''}`,
2131
- );
2234
+ const lines = [...observed.entries()].map(([nodeId, entry]) => {
2235
+ const origin =
2236
+ entry.own === entry.count
2237
+ ? ' — YOUR OWN earlier write, echoed back'
2238
+ : entry.own
2239
+ ? ` — ${entry.own} of ${entry.count} are YOUR OWN earlier writes, the rest are not`
2240
+ : entry.maybe === entry.count
2241
+ ? ' — unattributed, possibly your own earlier write'
2242
+ : '';
2243
+ return `- ${nodeId}: ${[...entry.types].join(', ')} (${entry.count}×)${origin}`;
2244
+ });
2132
2245
  const header = [
2133
2246
  'BOARD_CHANGE',
2134
2247
  `board_id=${cfg.boardId}`,
@@ -2177,6 +2290,15 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2177
2290
 
2178
2291
  async function dispatchStandaloneTurn({ idle = false } = {}) {
2179
2292
  if (!idle && !observed.size) return;
2293
+ // Every buffered event is provably this agent's own write coming back — attribution says
2294
+ // so, not a timer. There is nothing new on the board, so it costs no turn; if the board
2295
+ // then stays quiet the idle review still comes around.
2296
+ if (!idle && [...observed.values()].every((entry) => entry.own === entry.count)) {
2297
+ log(`standalone turn skipped: ${observedCount} board event(s) on ${observed.size} node(s), all own writes`);
2298
+ resetObserved();
2299
+ armIdleReview();
2300
+ return;
2301
+ }
2180
2302
  // A direct message always outranks the agent's own initiative — re-arm instead of
2181
2303
  // queueing behind the prompt lane, so the buffer just keeps collecting meanwhile.
2182
2304
  if (pending || draining) {
@@ -2194,17 +2316,20 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2194
2316
  if (idle) {
2195
2317
  log(`standalone review turn: board quiet for ${Math.round(STANDALONE_IDLE_MS / 1000)}s`);
2196
2318
  } else {
2197
- const ownOnly = [...observed.values()].every((entry) => entry.maybeOwn);
2319
+ const totals = [...observed.values()].reduce(
2320
+ (acc, entry) => ({ own: acc.own + entry.own, maybe: acc.maybe + entry.maybe }),
2321
+ { own: 0, maybe: 0 },
2322
+ );
2323
+ const breakdown = [
2324
+ totals.own ? `${totals.own} own` : null,
2325
+ totals.maybe ? `${totals.maybe} unattributed` : null,
2326
+ ].filter(Boolean);
2198
2327
  log(
2199
2328
  `standalone turn: ${observedCount} board event(s) on ${observed.size} node(s)` +
2200
- `${ownOnly ? ' (all possibly own writes)' : ''}`,
2329
+ `${breakdown.length ? ` (${breakdown.join(', ')})` : ''}`,
2201
2330
  );
2202
2331
  }
2203
- observed.clear();
2204
- observedCount = 0;
2205
- seqLo = null;
2206
- seqHi = null;
2207
- firstObservedAt = 0;
2332
+ resetObserved();
2208
2333
  lastStandaloneAt = Date.now();
2209
2334
  try {
2210
2335
  const result = await runClaudeWarm(text);
@@ -2806,6 +2931,7 @@ credentialFlags(program
2806
2931
  .option('--modeling', 'Keep one Claude process warm across prompts instead of spawning a fresh one per task, for low-latency voice/live use. 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.')
2807
2932
  .option('--standalone', 'Let the modeling agent work the board in the background, on its own initiative: on top of direct prompts it subscribes to the board\'s change channel (like the build agents do) and, whenever the board goes quiet after an edit — or has simply been idle for a while — it takes a turn nobody asked for. Changed nodes are a notification, not the task: it judges the model as a whole and fans the work out over parallel subagents, one per changed area (examples on a new node, specs for a new command or read model, a missing attribute along a chain, a screen, a question comment). Filling that detail in while the human keeps modeling is the point — it does not wait for the board to be finished. Implies --modeling.')
2808
2933
  .option('--max-agents <n>', 'Cap how many subagents a self-directed --standalone turn may dispatch at once, to bound what an unattended agent can spend per turn. The agent merges work that shares a slice or chain first, then takes the most valuable pieces up to this many and leaves the rest for a later turn. 1 makes it do the single most valuable piece itself, without spawning anything. Default 5. Ignored without --standalone — prompt turns are one piece of work by definition.', '5')
2934
+ .option('--exclusive', 'Work only the prompts addressed to this agent\'s id — the board\'s "preferred agent" (the star in the prompts panel) — and hand every untargeted prompt straight back to the queue for another agent to take. Without it an agent also works everything nobody addressed to anyone, which is what you want for a single agent and exactly what you do not want for a dedicated one (a board with a general agent plus a specialist, or an agent a supervisor drives by id). Pair it with --id so the same agent is addressable across restarts — --global/--standalone otherwise mint a fresh id per run, and prompts addressed to the previous run\'s id are never claimed. Leaves --standalone alone: a self-directed turn is nobody\'s prompt, so an exclusive standalone agent still works the board on its own initiative.')
2809
2935
  .option('--global', 'Run the modeling agent from the global install (~/.eventmodelers/kit), initializing it on first use, and ignore any kit in this directory. This is also what --modeling/--standalone fall back to on their own when nothing is installed here — pass it explicitly to prefer the global install over a local one. Credentials come from the flags below, EVENTMODELERS_* env vars, or ~/.eventmodelers/boards/<board>.json, so nothing is written into the current directory.')
2810
2936
  .option('--local', 'Skip platform config/credential lookup entirely and run the local-only loop (no board sync, no realtime agent) — even if .eventmodelers/config.json has credentials (build-kit stacks only)')
2811
2937
  .option('--verbose', 'Log every tool call\'s full input (commands, skill args, file paths) and assistant reasoning text. Default is condensed, high-level per-step logging only.')
@@ -2822,6 +2948,11 @@ credentialFlags(program
2822
2948
  if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
2823
2949
  console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
2824
2950
  }
2951
+ // The build-kit runners claim their work from the same queue but have no addressee
2952
+ // filter, so the flag would silently do nothing there rather than half of what it says.
2953
+ if (opts.exclusive && !(opts.modeling || opts.standalone || opts.global)) {
2954
+ console.log('ℹ️ --exclusive only applies to the modeling loop (--modeling/--standalone/--global); ignoring it here.');
2955
+ }
2825
2956
  // --id/--name are what the platform will see for this run, so a blank one is a
2826
2957
  // mistake worth failing on rather than silently falling back to the stored identity.
2827
2958
  const identity = {
@@ -2902,7 +3033,7 @@ credentialFlags(program
2902
3033
  const shown = relative(cwd, kitDir);
2903
3034
  await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (warm Claude process) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
2904
3035
  try {
2905
- await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents, identity);
3036
+ await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, overrides, maxAgents, identity });
2906
3037
  } catch (err) {
2907
3038
  console.error('[modeling] Fatal:', err);
2908
3039
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.70",
3
+ "version": "1.0.72",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -860,6 +860,8 @@ Claim the next pending (`ADDED`) prompt for a board — atomically flips it to `
860
860
 
861
861
  Send `x-agent-id` here too: a prompt the user addressed to one preferred agent (`prompts.agent_id`) is only ever handed to the agent claiming with that id, and a caller without the header claims untargeted prompts only. Addressing one is `POST /api/org/:orgId/prompts` with `agent_id: "<uuid>"` — the board's prompts panel does it when someone stars an agent.
862
862
 
863
+ Note what this endpoint does *not* do: an agent that sends its id still gets every untargeted prompt on top of its own. A caller that wants only what was addressed to it has to hand the rest back itself (`POST /prompts/:id/status` with `status: 'ADDED'`), which is what `eventmodelers run --exclusive` does.
864
+
863
865
  **Query params**: `board_id` (required)
864
866
  **Response**: `200` — the claimed row (now `status: "CLAIMED"`), including its parsed `context` and the `hidden` flag · `404` — no `ADDED` prompts available
865
867
 
@@ -879,7 +881,7 @@ Set a prompt's status, optionally attaching a progress comment. Auth: `x-token`
879
881
  ```
880
882
 
881
883
  **Response**: `200` — the updated row
882
- **Errors**: `400` invalid/missing `status` · `403` token not for this prompt's org · `404` prompt not found
884
+ **Errors**: `400` invalid/missing `status` · `403` token not for this prompt's org · `404` prompt not found · `409` (`PROMPT_STATUS_ALREADY_SET`) the prompt already carries that status — another agent already made this transition, so don't retry it
883
885
 
884
886
  ---
885
887
 
@@ -52,6 +52,7 @@ Omit `comment` entirely when there isn't one — don't pass an empty string.
52
52
  |----------|---------|--------|
53
53
  | `400` | `status` missing or not a valid value | Fix the value and retry — do not retry with the same bad value. |
54
54
  | `404` | Prompt not found | The prompt may have been deleted by its author while you were working. Report this and move on — do not treat it as a failure of your actual task work. |
55
+ | `409` | The prompt already carries the status you asked for (`PROMPT_STATUS_ALREADY_SET`) | Somebody else — usually another agent working the same queue — already made this transition. Do not retry it; treat the transition as done and carry on. |
55
56
  | `401`/`403` | Token invalid or wrong organization | Re-run `connect` to refresh credentials, then retry once. |
56
57
 
57
58
  ---
@@ -199,6 +199,9 @@ Steps:
199
199
 
200
200
  Keep these turns finished within the turn: wait for the subagents you dispatched, don't leave
201
201
  work trailing. Everything you and they write to the board comes back on this same channel as
202
- another change; the CLI labels changes that arrive in that echo window rather than dropping
203
- them, so you'll see your own writes listed on a later turn recognize them and don't rework
204
- them.
202
+ another change. A burst that is *only* your own writes never becomes a turn at all, so a
203
+ change list you are handed always contains something that isn't yoursbut it may still list
204
+ yours alongside it, marked `YOUR OWN earlier write`. Take that mark literally: those lines are
205
+ there for context, not to be reworked. A line marked `unattributed, possibly your own earlier
206
+ write` is the one uncertain case (a write that reached the platform without an agent id);
207
+ anything unmarked was written by someone else and is real work to look at.