@eventmodelers/cli 1.0.70 → 1.0.71

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
@@ -416,6 +416,18 @@ npx @eventmodelers/cli run --id <id> # pin ONE identit
416
416
 
417
417
  `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
418
 
419
+ #### Working only what you were addressed (`--exclusive`)
420
+
421
+ 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:
422
+
423
+ ```bash
424
+ npx @eventmodelers/cli run --standalone --board-id <uuid> --id <agent-uuid> --exclusive
425
+ ```
426
+
427
+ 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.
428
+
429
+ 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.
430
+
419
431
  ### Env vars and `--config` (scripted/CI installs)
420
432
 
421
433
  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
@@ -1672,7 +1672,13 @@ async function ensureGlobalKit(baseUrl) {
1672
1672
  // question, sketch a screen. Without the flag that channel is still subscribed on
1673
1673
  // the same connection and every event on it is dropped, so the two modes differ by
1674
1674
  // 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 = {}) {
1675
+ //
1676
+ // `exclusive` narrows the prompt lane to this agent alone: only a prompt the user
1677
+ // addressed to this agent id (the board's "preferred agent") is worked, and anything
1678
+ // untargeted is handed straight back to the queue for another agent to take. It says
1679
+ // nothing about the standalone lane — a self-directed turn is nobody's task, so an
1680
+ // exclusive standalone agent still works the board on its own initiative.
1681
+ async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {} } = {}) {
1676
1682
  const configLibPath = join(kitDir, 'lib', 'config.js');
1677
1683
  if (!existsSync(configLibPath)) {
1678
1684
  console.error(`❌ ${relative(process.cwd(), configLibPath)} not found — --modeling needs a kit installed via \`init --modeling\`.`);
@@ -1710,6 +1716,12 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1710
1716
  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
1717
  process.exit(1);
1712
1718
  }
1719
+ // Nothing can be addressed to an agent with no id, so an exclusive run without one would
1720
+ // hand every prompt back and sit idle forever — a silent no-op worth failing on instead.
1721
+ if (exclusive && !cfg.agentId) {
1722
+ 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.');
1723
+ process.exit(1);
1724
+ }
1713
1725
 
1714
1726
  const subagentModel = cfg.subagentModel || DEFAULT_SUBAGENT_MODEL;
1715
1727
 
@@ -1928,6 +1940,13 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1928
1940
  ? `standalone: ON — reacting to direct prompts AND to board changes on its own initiative (max ${maxAgents} subagent(s) per self-directed turn)`
1929
1941
  : 'standalone: off — reacting to direct prompts only (board changes are dropped)',
1930
1942
  );
1943
+ if (exclusive) {
1944
+ log(`exclusive: ON — only prompts addressed to ${cfg.agentId} are worked; every untargeted prompt is handed back to the queue`);
1945
+ // A global/standalone run mints its id per run (see resolveModelingCredentials), so an id
1946
+ // someone addressed a prompt to yesterday is not this agent — worth saying out loud here,
1947
+ // where the alternative is an agent that looks healthy and quietly works nothing.
1948
+ 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');
1949
+ }
1931
1950
  warmUpSession();
1932
1951
 
1933
1952
  async function getRealtimeToken() {
@@ -1947,15 +1966,47 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1947
1966
  return res.json();
1948
1967
  }
1949
1968
 
1969
+ // Puts a prompt this agent claimed but will not work back on the queue (CLAIMED -> ADDED),
1970
+ // so whichever agent it was actually open to can still take it. `x-token` only — the status
1971
+ // endpoint is meant to be called by the agent holding the prompt.
1972
+ async function releasePrompt(promptId) {
1973
+ const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/${promptId}/status`, {
1974
+ method: 'POST',
1975
+ headers: { 'x-token': cfg.token, 'Content-Type': 'application/json', ...agentHeaders(cfg) },
1976
+ body: JSON.stringify({ status: 'ADDED' }),
1977
+ });
1978
+ if (!res.ok) throw new Error(`prompts/${promptId}/status: HTTP ${res.status}`);
1979
+ }
1980
+
1950
1981
  let realtimeToken = await getRealtimeToken();
1951
1982
 
1952
1983
  let draining = false;
1953
1984
  async function drain() {
1954
1985
  if (draining) return;
1955
1986
  draining = true;
1987
+ // --exclusive only: prompts claimed in this pass that weren't addressed to this agent,
1988
+ // handed back once the pass is over (see below).
1989
+ const handBack = [];
1956
1990
  try {
1957
1991
  let p;
1958
1992
  while ((p = await fetchNextPrompt(realtimeToken)) !== null) {
1993
+ // The queue can't filter by addressee for us: `prompts/next` hands an agent both the
1994
+ // prompts addressed to it and every untargeted one (`agent_id IS NULL`) — claiming is
1995
+ // what reveals which kind arrived — so an exclusive run claims as usual and gives back
1996
+ // what wasn't meant for it.
1997
+ //
1998
+ // The hand-back is deferred to the end of the pass on purpose: a prompt released
1999
+ // mid-loop goes straight back to the head of the very queue this loop is reading, so
2000
+ // the next fetch would return the prompt just released instead of the addressed one
2001
+ // queued behind it, and the agent would never reach its own work. Holding them CLAIMED
2002
+ // until the queue runs dry walks past them instead. The cost is a brief CLAIMED blip on
2003
+ // someone else's prompt, and — if no other agent happens to be draining when the
2004
+ // hand-back lands — that prompt waiting for the next `prompt:created` to be noticed.
2005
+ if (exclusive && (p.agent_id ?? null) !== cfg.agentId) {
2006
+ log(`prompt ${p.id} ${p.agent_id ? `is addressed to agent ${p.agent_id}` : 'is addressed to no agent'} — handing it back (--exclusive)`);
2007
+ handBack.push(p.id);
2008
+ continue;
2009
+ }
1959
2010
  log(`prompt received: "${p.prompt}" (board=${p.board_id ?? cfg.boardId ?? 'n/a'}, priority=${p.priority})`);
1960
2011
  try {
1961
2012
  await runClaudeWarm(buildTurn(p));
@@ -1964,6 +2015,16 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1964
2015
  }
1965
2016
  }
1966
2017
  } finally {
2018
+ for (const id of handBack) {
2019
+ try {
2020
+ await releasePrompt(id);
2021
+ } catch (err) {
2022
+ // Left CLAIMED, which is worse for whoever sent it than a retry would be — but
2023
+ // retrying here risks wedging the loop, and the next pass claims nothing new
2024
+ // while this one is still unwinding. Say so and move on.
2025
+ log(`handing prompt ${id} back failed, it stays CLAIMED: ${err.message}`);
2026
+ }
2027
+ }
1967
2028
  draining = false;
1968
2029
  // A prompt turn counts as activity: the board isn't idle just because nobody edited
1969
2030
  // it while the agent was busy answering someone.
@@ -2806,6 +2867,7 @@ credentialFlags(program
2806
2867
  .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
2868
  .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
2869
  .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')
2870
+ .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
2871
  .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
2872
  .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
2873
  .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 +2884,11 @@ credentialFlags(program
2822
2884
  if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
2823
2885
  console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
2824
2886
  }
2887
+ // The build-kit runners claim their work from the same queue but have no addressee
2888
+ // filter, so the flag would silently do nothing there rather than half of what it says.
2889
+ if (opts.exclusive && !(opts.modeling || opts.standalone || opts.global)) {
2890
+ console.log('ℹ️ --exclusive only applies to the modeling loop (--modeling/--standalone/--global); ignoring it here.');
2891
+ }
2825
2892
  // --id/--name are what the platform will see for this run, so a blank one is a
2826
2893
  // mistake worth failing on rather than silently falling back to the stored identity.
2827
2894
  const identity = {
@@ -2902,7 +2969,7 @@ credentialFlags(program
2902
2969
  const shown = relative(cwd, kitDir);
2903
2970
  await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (warm Claude process) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
2904
2971
  try {
2905
- await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents, identity);
2972
+ await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, overrides, maxAgents, identity });
2906
2973
  } catch (err) {
2907
2974
  console.error('[modeling] Fatal:', err);
2908
2975
  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.71",
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