@eventmodelers/cli 1.0.118 → 1.0.120

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
@@ -436,6 +436,21 @@ different thing, and `anthropicBaseUrl` below is that path — but note that it
436
436
  speaking Anthropic's own `/v1/messages`, which Ollama does not serve. Pointing it straight at
437
437
  `localhost:11434` gets you a 404; a translating proxy has to sit in between.
438
438
 
439
+ ### Asking for credentials again
440
+
441
+ A modeling kit installed in the project never asks on its own: `run --modeling` takes whatever
442
+ the `.eventmodelers/config.json` walk turns up. To replace those credentials (another account,
443
+ a rotated token, a different board) pass `--force`:
444
+
445
+ ```bash
446
+ npx @eventmodelers/cli run --modeling --force
447
+ ```
448
+
449
+ It re-runs the same credentials prompt as `init --modeling`, overwrites the project's
450
+ `.eventmodelers/config.json`, and then starts the loop with the new values. With the global
451
+ install, `--force` drops the "keep the stored credentials" choice so the board's credentials have
452
+ to be answered afresh. `init-config` (no flags) does the same re-ask without starting a loop.
453
+
439
454
  ### Running headless, in a container
440
455
 
441
456
  `run` normally confirms which board it is about to drive and where that board's credentials
package/cli.js CHANGED
@@ -1716,7 +1716,7 @@ async function fetchDefaultBoardId(baseUrl, token) {
1716
1716
  // .eventmodelers/config.json up the tree beat ~/.eventmodelers/config.json. So
1717
1717
  // `run --standalone --board-id <uuid>` is enough for a board used before, and any run can
1718
1718
  // be pointed somewhere else entirely with --token/--organization-id.
1719
- async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print, nonInteractive = false) {
1719
+ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print, nonInteractive = false, force = false) {
1720
1720
  const walked = loadEffectiveConfig(cwd, null, explicitConfigPath).config;
1721
1721
  const explicit = Object.fromEntries(Object.entries(flags ?? {}).filter(([, v]) => v));
1722
1722
 
@@ -1795,7 +1795,8 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
1795
1795
  { label: 'The account-wide credentials (~/.eventmodelers/config.json)', value: 'global' },
1796
1796
  { label: 'Credentials of its own — paste them now', value: 'board' },
1797
1797
  ];
1798
- if (existing) choices.unshift({ label: `Keep the credentials already stored for this board (${existing.from})`, value: 'keep' });
1798
+ // --force: the stored answer is exactly what the user wants to replace, so don't offer it.
1799
+ if (existing && !force) choices.unshift({ label: `Keep the credentials already stored for this board (${existing.from})`, value: 'keep' });
1799
1800
 
1800
1801
  const configured = existing ? 'is already configured on this machine' : "hasn't been configured on this machine yet";
1801
1802
  const choice = await selectPrompt(
@@ -1805,7 +1806,7 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
1805
1806
  choices,
1806
1807
  // 'keep' when there is something to keep, else the pre-existing default: account-wide
1807
1808
  // when it actually holds credentials, otherwise the paste.
1808
- existing || hasAccountWide ? 0 : 1,
1809
+ (existing && !force) || hasAccountWide ? 0 : 1,
1809
1810
  );
1810
1811
 
1811
1812
  if (choice === 'keep') {
@@ -3639,6 +3640,7 @@ credentialFlags(program
3639
3640
  .option('--exec [command]', 'Fallback for any harness --agent does not cover, or other flags than its preset. Hand each prompt to an external agent command instead of the default Claude runner — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto", "gemini --yolo -p" or "opencode run". One process per prompt; the prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent. A build kit runs it via ralph-exec.js; there, with agent tracing on, include the harness\'s JSON output flag (codex exec --json, opencode run --format json, gemini --output-format stream-json) — without it no slice cost is recorded. With --modeling/--standalone/--worker it is how any agent takes board prompts and chats: the loop claims each prompt or chat message and runs the command once for it. Keep the harness in its plain-text output mode there — stdout is the turn\'s answer — the eventmodelers MCP server is registered for it on every run (opencode.json, .gemini/settings.json, or -c flags for codex; any other command logs what to register by hand), authenticating with $EVENTMODELERS_TOKEN from the env the command inherits, and `init-agents --hosts <harness>` installs the skills for a harness that does not read .claude/skills. No subagents: self-directed work is done inline.')
3640
3641
  .option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
3641
3642
  .option('--modeling', 'Connect a modeling agent to the board: it takes prompts and chat messages and hands each to an AI — by default one Claude process kept warm across prompts, for low-latency voice/live use; --agent (or --local-ai, or --exec as the manual fallback) picks another. 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.')
3643
+ .option('-f, --force', 'Ask for credentials again before the modeling loop starts, even when a config already has everything required. With a modeling kit in this directory it re-runs the same prompt as `init --modeling`, overwriting the project\'s .eventmodelers/config.json; with the global install it drops the "keep the stored credentials" choice, so a board\'s credentials have to be answered afresh. Only affects the modeling loop (--modeling/--standalone/--worker/--global); cannot be combined with --non-interactive or --print.')
3642
3644
  .option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal — right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--worker/--global).')
3643
3645
  .option('--standalone', 'Let the modeling agent act on board changes on its own initiative, without asking: it subscribes to the board\'s change channel and, whenever the board goes quiet after an edit — or has simply been idle for a while — it takes a turn nobody asked for, 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). Filling that detail in while the human keeps modeling is the point. A plain --modeling agent only proposes, and only to the people chatting with it: after their edits it says in the chat what it would do and waits for a yes; everyone else\'s edits are ignored. Implies --modeling.')
3644
3646
  .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')
@@ -3685,6 +3687,9 @@ credentialFlags(program
3685
3687
  }
3686
3688
  // The build-kit runners claim their work from the same queue but have no addressee
3687
3689
  // filter, so the flag would silently do nothing there rather than half of what it says.
3690
+ if (opts.force && !(opts.modeling || opts.standalone || opts.worker || opts.global)) {
3691
+ console.log('ℹ️ --force only applies to the modeling loop (--modeling/--standalone/--worker/--global); for a build kit use `re-init --force` or `init-config`. Ignoring it here.');
3692
+ }
3688
3693
  if (opts.exclusive && !(opts.modeling || opts.standalone || opts.worker || opts.global)) {
3689
3694
  console.log('ℹ️ --exclusive only applies to the modeling loop (--modeling/--standalone/--worker/--global); ignoring it here.');
3690
3695
  }
@@ -3749,6 +3754,12 @@ credentialFlags(program
3749
3754
  console.error('❌ --local-ai and --exec are mutually exclusive — pick one runner.');
3750
3755
  process.exit(1);
3751
3756
  }
3757
+ // --force means "ask me", --non-interactive/--print mean "never ask" — one has to lose,
3758
+ // and silently dropping either would surprise whoever passed it.
3759
+ if (opts.force && (opts.nonInteractive || globalOpts.print)) {
3760
+ console.error(`❌ --force asks for credentials again, which --non-interactive/--print rule out — pick one.`);
3761
+ process.exit(1);
3762
+ }
3752
3763
  if (opts.local) {
3753
3764
  console.error(`❌ ${picked} has no local-only mode — it is always driven by the org-wide realtime prompt queue, so --local has no use for it.`);
3754
3765
  process.exit(1);
@@ -3777,10 +3788,31 @@ credentialFlags(program
3777
3788
  ...(opts.credentials ? parseCredentialsArg(opts.credentials) : {}),
3778
3789
  ...Object.fromEntries(Object.entries(credentialOverridesFromOpts(opts)).filter(([, v]) => v)),
3779
3790
  };
3780
- const config = await resolveModelingCredentials(cwd, flags, globalOpts.config, globalOpts.print, !!opts.nonInteractive);
3791
+ const config = await resolveModelingCredentials(cwd, flags, globalOpts.config, globalOpts.print, !!opts.nonInteractive, !!opts.force);
3781
3792
  projectDir = await ensureGlobalKit(config.baseUrl);
3782
3793
  kitDir = join(projectDir, MODELING_KIT.kitDirName);
3783
3794
  overrides = config;
3795
+ } else if (opts.force) {
3796
+ // A project install never asks on its own — runModeling just walks the config files.
3797
+ // --force re-runs init's own credentials step against the same project-root config,
3798
+ // so the loop below picks up the fresh answer through that very walk.
3799
+ const configPath = globalOpts.config
3800
+ ? resolve(projectDir, globalOpts.config)
3801
+ : join(projectDir, '.eventmodelers', 'config.json');
3802
+ const effective = loadEffectiveConfig(projectDir, kitDir, globalOpts.config);
3803
+ const cfg = await configureCredentials({
3804
+ config: effective.config,
3805
+ configPath,
3806
+ targetDir: projectDir,
3807
+ requiredFields: ['organizationId', 'token'],
3808
+ boardIdOptional: true,
3809
+ overrides: credentialOverridesFromOpts(opts),
3810
+ print: false,
3811
+ force: true,
3812
+ });
3813
+ ensureMcpRegistered(projectDir, cfg.baseUrl || DEFAULT_BASE_URL);
3814
+ ensureEnvToken(projectDir, cfg.token);
3815
+ ensureMcpEnabled(projectDir);
3784
3816
  }
3785
3817
 
3786
3818
  // Writes to a stdout pipe are asynchronous on POSIX — without waiting for this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.118",
3
+ "version": "1.0.120",
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": {
@@ -5,6 +5,7 @@
5
5
  // onTask(prompt) — called when tasks.json has entries
6
6
  // onPlannedSlice(prompt, slice) — called when .slices/ has a "Planned" entry (omit to skip);
7
7
  // `slice` ({sliceId, sliceTitle, context, ticketNumber, boardId, attempt}) lets a runner record what building it cost.
8
+ // May resolve with the agent's final reply — quoted in the note if the slice ends up auto-blocked.
8
9
 
9
10
  import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync, renameSync } from 'fs';
10
11
  import { join, dirname } from 'path';
@@ -590,11 +591,14 @@ const MAX_PLANNED_ATTEMPTS = Number(process.env.RALPH_MAX_PLANNED_ATTEMPTS) || 2
590
591
 
591
592
  // Marks a stuck slice Blocked (locally, and on the board if credentialed) and
592
593
  // records why, so the loop can move on instead of looping or exiting.
593
- async function blockStuckSlice(kitDir, cfg, credentialed, planned, attempts) {
594
+ // `lastReply` is the build agent's final message on the last attempt (when the runner reports one),
595
+ // so the note says what actually happened instead of only listing what might have.
596
+ async function blockStuckSlice(kitDir, cfg, credentialed, planned, attempts, lastReply) {
594
597
  const now = new Date().toISOString();
595
598
  const reason = `Ralph loop picked up this slice ${attempts} times in a row without its status ever leaving ` +
596
599
  `"Planned" — the build agent kept declining to build it, or kept building it but its own status change kept ` +
597
- `getting reverted (e.g. a failed check). Auto-blocked to stop the loop from retrying it forever.`;
600
+ `getting reverted (e.g. a failed check). Auto-blocked to stop the loop from retrying it forever.` +
601
+ (lastReply ? ` Build agent's last reply: "${String(lastReply).replace(/\s+/g, ' ').trim().slice(0, 500)}"` : '');
598
602
 
599
603
  const indexPath = join(kitDir, '.slices', planned.ctx, 'index.json');
600
604
  let folder;
@@ -662,7 +666,7 @@ async function blockStuckSlice(kitDir, cfg, credentialed, planned, attempts) {
662
666
  console.error(`[ralph] ${reason} Marked "${planned.title}" (id=${planned.id}) as Blocked — moving on.`);
663
667
  }
664
668
 
665
- // Returns true once fn succeeds; false for a turn that timed out (see lib/turn.js). A timeout
669
+ // Returns true once fn succeeds (fn's own result is the caller's to capture); false for a turn that timed out (see lib/turn.js). A timeout
666
670
  // is not retried in place: re-running the same hung turn every 60s would spin forever without
667
671
  // the stuck-slice guard ever seeing it, so it goes back to ralphLoop, which recounts the slice.
668
672
  // Any other failure is retried in place, with backoff (30s → 10min) so a harness that keeps
@@ -696,6 +700,8 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
696
700
  // Tracks consecutive sightings of the same Planned slice id — see
697
701
  // MAX_PLANNED_ATTEMPTS above.
698
702
  let stuckSlice = { id: null, count: 0 };
703
+ // The build agent's final reply on the most recent planned-slice turn — quoted in the Blocked note.
704
+ let lastReply = null;
699
705
 
700
706
  while (true) {
701
707
  let didWork = false;
@@ -715,7 +721,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
715
721
  : { id: planned.id, count: 1 };
716
722
 
717
723
  if (stuckSlice.count > MAX_PLANNED_ATTEMPTS) {
718
- await blockStuckSlice(kitDir, cfg, credentialed, planned, stuckSlice.count);
724
+ await blockStuckSlice(kitDir, cfg, credentialed, planned, stuckSlice.count, lastReply);
719
725
  stuckSlice = { id: null, count: 0 };
720
726
  didWork = true;
721
727
  continue;
@@ -723,14 +729,15 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
723
729
 
724
730
  const prompt = readFileSync(backendPromptFile, 'utf-8');
725
731
  if (credentialed) await fetchFullSlice(cfg, kitDir, planned.id);
726
- const built = await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, () => onPlannedSlice(prompt, {
732
+ lastReply = null;
733
+ const built = await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, async () => { lastReply = await onPlannedSlice(prompt, {
727
734
  sliceId: planned.id,
728
735
  sliceTitle: planned.title,
729
736
  context: planned.ctx,
730
737
  ticketNumber: planned.ticketNumber,
731
738
  boardId: cfg.boardId ?? null,
732
739
  attempt: stuckSlice.count,
733
- }));
740
+ }); });
734
741
  if (built) console.log(`[ralph] Slice build complete — waiting for next slice`);
735
742
  if (credentialed) await fetchAndPersistSlices(cfg, kitDir).catch(() => {});
736
743
  didWork = true;
@@ -80,14 +80,19 @@ function describeToolUse(block) {
80
80
  // `slice` is set for a planned-slice build (see startRalph) — the only turns that are traced.
81
81
  function runClaude(prompt, slice = null) {
82
82
  return new Promise((resolve, reject) => {
83
- const proc = spawn('claude', [...claudeArgs, '-p', inlineHeader + prompt], {
83
+ // The prompt goes in on stdin, never as a `-p` argument: on Windows a multi-line argv entry
84
+ // is cut at its first newline, so `claude` saw only the connect header, answered "I don't see
85
+ // a request" and the slice stayed Planned until the stuck-slice guard blocked it.
86
+ const proc = spawn('claude', [...claudeArgs, '-p'], {
84
87
  cwd: projectDir,
85
- stdio: ['inherit', 'pipe', 'inherit'],
88
+ stdio: ['pipe', 'pipe', 'inherit'],
86
89
  env: claudeEnv,
87
90
  });
91
+ proc.stdin.end(inlineHeader + prompt);
88
92
  const turn = superviseTurn(proc, { timeoutMs, label: 'Claude turn', log: (line) => console.error(`[ralph] ${line}`) });
89
93
 
90
94
  let buffer = '';
95
+ let reply = null;
91
96
  proc.stdout.on('data', (chunk) => {
92
97
  buffer += chunk.toString();
93
98
  const lines = buffer.split('\n');
@@ -107,6 +112,7 @@ function runClaude(prompt, slice = null) {
107
112
  }
108
113
  }
109
114
  } else if (msg.type === 'result') {
115
+ reply = msg.result ?? null;
110
116
  console.log(`done (${msg.duration_ms}ms${msg.total_cost_usd ? `, $${msg.total_cost_usd.toFixed(4)}` : ''})`);
111
117
  if (slice) tracer.record({...slice, status: msg.is_error ? 'error' : 'ok'}, usageFromClaudeResult(msg));
112
118
  }
@@ -116,7 +122,7 @@ function runClaude(prompt, slice = null) {
116
122
  proc.on('close', (code) => {
117
123
  const timeout = turn.timeoutError();
118
124
  if (timeout) reject(timeout);
119
- else if (code === 0) resolve();
125
+ else if (code === 0) resolve(reply);
120
126
  else reject(new Error(`Claude exited ${code}`));
121
127
  });
122
128
  proc.on('error', reject);
@@ -377,6 +377,26 @@ Place a free-text markdown note in that lane the same way any other node is plac
377
377
 
378
378
  The node's content lives in `meta.description` (a plain string of markdown source) — **not** `meta.content`; that field is silently accepted and stored but never rendered, producing a visibly empty note. There is no `fields[]` array on this element type, and no separate render/sketch call is needed (unlike SCREEN/HTML_SCREEN). `node.type` comes back as `"markdown"` (lowercase) on read.
379
379
 
380
+ ### Linking to elements in markdown: `ref:<nodeId>`
381
+
382
+ Any markdown the app renders can point at an element on the board. Clicking the link zooms the canvas to that element. This works in MARKDOWN notes, element `meta.description`s (doc view and slice view) and timeline info notes.
383
+
384
+ | Write | Renders as |
385
+ |-------|------------|
386
+ | `ref:<nodeId>` | the element's **current** title, so it stays right after a rename. Use this one by default. |
387
+ | `[Visit Booked](ref:<nodeId>)` | your own label, for a different wording or a phrase in a sentence |
388
+
389
+ ```markdown
390
+ | # | Decision | Element(s) |
391
+ |---|----------|------------|
392
+ | 1 | Insert into `visits` → event | ref:3f2b8c1e-1a2b-4c3d-8e9f-0123456789ab |
393
+ ```
394
+
395
+ - `<nodeId>` is the full node uuid, taken from a read or from your own `node:created`. Never make one up: an id that isn't on the open board renders as a greyed-out `ref:3f2b8c1e…` link.
396
+ - A slice can be linked through its slice border's node id.
397
+ - Write it as a separate word (`see ref:<id>`, not `xref:<id>`). Inside backticks or code blocks it stays plain text.
398
+ - Use it only where the app renders markdown. Chat message `text` is plain text, so use a snippet's `nodeId` there (`link`, `tasks`, `changes`). Comments are plain text too.
399
+
380
400
  ---
381
401
 
382
402
  ## 3. Nodes
@@ -26,11 +26,13 @@ const claudeEnv = cfg.anthropicBaseUrl
26
26
 
27
27
  function runClaude(prompt) {
28
28
  return new Promise((resolve, reject) => {
29
- const proc = spawn('claude', [...claudeArgs, '-p', inlineHeader + prompt], {
29
+ // Prompt on stdin, not as a `-p` argument — Windows cuts a multi-line argv entry at its first newline.
30
+ const proc = spawn('claude', [...claudeArgs, '-p'], {
30
31
  cwd: projectDir,
31
- stdio: 'inherit',
32
+ stdio: ['pipe', 'inherit', 'inherit'],
32
33
  env: claudeEnv,
33
34
  });
35
+ proc.stdin.end(inlineHeader + prompt);
34
36
  proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Claude exited ${code}`))));
35
37
  proc.on('error', reject);
36
38
  });
@@ -40,7 +40,9 @@ Either way, append a row to the chapter's *Analysis history* (see *The analysis
40
40
 
41
41
  ## Step 1 — Is there a running UI? (once, when the analysis starts)
42
42
 
43
- Ask once, before reading the sources, and only when starting fresh (no chapters from this analysis yet): *"Is there a running instance I can inspect? URL, and a login or test account if needed."* Wait for the answer. Record it (URL or "no running UI") in each chapter's `Legacy Sources` note and reuse it on later passes. Skip it when the request already answers it.
43
+ Ask once, when starting fresh (no chapters from this analysis yet): *"Is there a running instance I can inspect? URL, and a login or test account if needed."* The question is **optional, not a gate**: post it at the start, then **continue straight away** with the code analysis (Pass 0) without waiting for the answer. Until an answer arrives, work from the code alone and record `running UI: not answered yet` in each chapter's `Legacy Sources` note. Skip the question when the request already answers it.
44
+
45
+ When the answer arrives later: a URL (and login) → start the UI analysis then — walk it via `discover-storyboard` (see the *UI* lens), add its screens as evidence and use its click path to confirm or correct the order already derived from the code; "no running UI" → nothing to do. Either way, update the `Legacy Sources` note with the answer and reuse it on later passes.
44
46
 
45
47
  Ask again mid-way only when it helps — typically when the order isn't clear from the code: *"I can't tell the order of these steps from the code. Is there a running UI I can use to check? Otherwise, here are my questions: …"*
46
48
 
@@ -63,12 +65,14 @@ Naming, what to model and the anti-patterns are in `eventmodeling-core-rules` (r
63
65
  Every pass reads the code through the same four lenses; each produces *hypotheses* to put to the person, never elements on its own. Read only as deep as the current layer needs.
64
66
 
65
67
  1. **API** (look hardest here) — REST controllers, routes, handlers, message listeners, scheduled jobs, outbound clients. Each entry point is a candidate **screen + command** (a person writes), **screen + read model** (a person reads) or **automation** (jobs, listeners, calls to other systems). Request/response payloads hint at fields; the caller hints at who triggers it.
66
- 2. **Tests** — test names, fixtures, assertions. Names reveal the business vocabulary; arrange/act/assert becomes Given/When/Then; error cases reveal the rules that matter. Fixtures are the source of realistic example values.
68
+ 2. **Tests** — test names, fixtures, assertions. Names reveal the business vocabulary; arrange/act/assert is a *hint* for Given/When/Then, not its shape — which element goes into Given, When and Then is decided by `eventmodeling-elaborating-scenarios`, never by the test's structure (see *Scenarios* below); error cases reveal the rules that matter. Fixtures are the source of realistic example values.
67
69
  3. **Persistence** (look hardest here, too — *what gets saved*) — entities, tables, migrations, repositories. Every write is a candidate **event**. Writes show which state changes exist (→ events); relations show aggregate boundaries; status/enum columns show lifecycles; derived or joined queries show read models. Tables are evidence, never elements.
68
- 4. **UI** — templates, views, components, forms, navigation. Forms and buttons are where a person triggers a command; list and detail pages are read models; the page order is the flow; visible labels are the business words. Whether a running UI exists is asked in **Step 1**. Without one, rely on the other three lenses and ask where the order stays unclear.
70
+ 4. **UI** — templates, views, components, forms, navigation. Forms and buttons are where a person triggers a command; list and detail pages are read models; the page order is the flow; visible labels are the business words. Whether a running UI exists is asked in **Step 1** — without waiting for the answer. Until there is a URL, rely on the other three lenses and ask where the order stays unclear.
69
71
 
70
72
  **With a URL, walk it via `discover-storyboard`** (URL already known — don't ask again; pass the flow as guidance and ask for screenshots). Its click path is the *proposed* order and its screens go into the chapter as evidence; confirm both against the API and persistence lenses. Submit forms only against a local or test system, or after the person agrees.
71
73
 
74
+ **Screens: HTML, except real screenshots.** A plain `SCREEN` node is used for exactly one thing — a screenshot Playwright took of the running UI (`contentType: "image"`). Every other screen — built from the templates, views and controllers, or for a step the running UI didn't show — is an `HTML_SCREEN` via `/html-screen` (`create_screen` with `contentType: "html"` and real pages: the template's labels, fields and buttons). Never a sketch, never a plain `SCREEN` without a screenshot on it.
75
+
72
76
  If the code is a generic engine (templates, schemas, configurable entities), model one real case it handles, not the engine (see *Concrete Over Generic* in `eventmodeling-core-rules`).
73
77
 
74
78
  Where the lenses disagree (a route nobody calls, a table never written, a test for removed behaviour, a screen with no backend), that is a question for the person — not something to model.
@@ -94,17 +98,17 @@ Rules of thumb: every API call is either a screen-driven command/read or an auto
94
98
  ## Progressive elaboration — all chapters first, then one chapter per pass
95
99
 
96
100
  ### Pass 0 — all chapters, high level
97
- 1. Read the sources broadly through the four lenses to spot the business workflows — walking the running UI from Step 1 via `discover-storyboard` if there is one.
101
+ 1. Read the sources broadly through the four lenses to spot the business workflows — don't wait for the Step 1 answer; walk the running UI via `discover-storyboard` once a URL has been given (also when it arrives after Pass 0).
98
102
  2. Group them into chapters and create them as `eventmodeling-brainstorming-events` (§ *Group events by workflow* / *Create one chapter per group*) does — a chapter is a user journey, never one endpoint, controller or CRUD operation. Code is split by operation (`OwnerController#initCreationForm`, `#processFindForm`, `#showOwner`, `#processUpdateOwnerForm`); the story is not — registering, finding, viewing and updating an owner are **one** chapter. Order the chapters per `eventmodeling-plotting-events`; ask where it is unclear. Add the **Decisions** lane, put the `Legacy Sources` note into **column 0** (see *The analysis log*), then sketch only the milestones (titles only) **from column 1 on**.
99
103
  3. Report the chapters and **ask which chapter to start with**. Never pick for them.
100
104
 
101
105
  ### Pass N — detail the chosen chapter
102
106
  1. Work **in the chapter the person picked**, reusing what is already there. Model the **whole workflow**, not its first slice, by running the step skills on it in order:
103
107
  - `eventmodeling-plotting-events` — the events in business order, start to outcome;
104
- - `eventmodeling-storyboarding-events` — what the person sees after each event (e.g. *Owner Registered* → owner details page → add a pet);
108
+ - `eventmodeling-storyboarding-events` — what the person sees after each event (e.g. *Owner Registered* → owner details page → add a pet). Every screen it places is an `HTML_SCREEN` (see *Screens* in the *UI* lens): skip its interview (the code answers it) and never take its sketch path;
105
109
  - `eventmodeling-identifying-inputs` / `eventmodeling-identifying-outputs` — commands and read models;
106
110
  - `eventmodeling-designing-automation-chains` / `eventmodeling-translating-external-events` — jobs, listeners, webhooks, outbound calls;
107
- - `eventmodeling-elaborating-scenarios` — rules from the tests (layer 1+ only).
111
+ - `eventmodeling-elaborating-scenarios` — rules from the tests (layer 1+ only), built exactly as that skill says (see *Scenarios* below).
108
112
  Feed each skill what the lenses and the mapping table give you; ask only where the code stops (see *Step 1* when the order is unclear).
109
113
  2. Stay within the layer's detail budget (below). Every placement batch carries its decision rows (see *Recording decisions*).
110
114
  3. Re-space the chapters (see *Keeping chapters apart*) after every batch.
@@ -119,7 +123,7 @@ Repeat until the person says stop. Chapters not chosen stay at high level.
119
123
  |-------|----------|--------|----------|-----------|---------|
120
124
  | **0 — High level** | one chapter per flow, business milestones as slices | none | none | none | none |
121
125
  | **1 — Flow** | real command / event / read model sequence of one flow | only identifying and business-critical | one per key element | happy path + the one or two rules that matter | only where a person decides |
122
- | **2+ — Deeper** | sub-flows, alternate paths, automations | full business field set incl. optional/list | realistic values from tests and fixtures | error cases and edge rules from the tests | sketches where a decision needs them |
126
+ | **2+ — Deeper** | sub-flows, alternate paths, automations | full business field set incl. optional/list | realistic values from tests and fixtures | error cases and edge rules from the tests | HTML screens where a decision needs them (screenshots only from the running UI) |
123
127
 
124
128
  Never add detail that belongs to a deeper layer. Every element must trace to sources you read or an answer the person gave. **Deeper never means more technical** — it means more business: decisions, rules, failure modes.
125
129
 
@@ -160,6 +164,8 @@ Create notes with `submit_node_events` (`node:created`, `meta.type: "MARKDOWN"`,
160
164
 
161
165
  **Use tables, not prose.** Each note is mostly markdown tables. Prose is allowed only for a one-line intro, never for something that fits in a row.
162
166
 
167
+ **Link elements, don't just name them.** Wherever a note mentions an element on the board, write `ref:<nodeId>`. It renders as the element's current title, and a click zooms the canvas to it (see `/learn-eventmodelers-api` *Linking to elements in markdown*). Use `[label](ref:<nodeId>)` only when the wording must differ. Take the id from your `node:created` or from a read. An element that isn't placed yet keeps its plain *italic* name until it is, and then the row is updated.
168
+
163
169
  **References must be precise and resolvable.** Paths are relative to the analysed code root, with a line number where one exists: `src/main/java/org/acme/visit/VisitController.java:57`, plus the class and method (`VisitController#processNewVisitForm`), table, endpoint (`POST /owners/{id}/pets/{petId}/visits/new`), test (`VisitControllerTests#testProcessNewVisitFormSuccess`) or template (`templates/pets/createOrUpdateVisitForm.html`). Use folders and packages for anything bigger than one class. Never "the visit code". A line number is valid for the commit of the *Analysis history* row that wrote it; when a later pass re-reads a changed file, it updates the references into that file.
164
170
 
165
171
  ### The analysis log — column 0 of every chapter
@@ -194,7 +200,7 @@ Code root `petclinic/` · running UI: http://localhost:8080 (test account `georg
194
200
  ## Chapter decisions
195
201
  | # | Decision | Why | Source | Confirmed by |
196
202
  |---|----------|-----|--------|--------------|
197
- | 1 | Flow starts at *Pet Added* | A visit needs a pet (`Visit.petId` not null) | `Visit.java:34` | person |
203
+ | 1 | Flow starts at ref:<petAddedId> | A visit needs a pet (`Visit.petId` not null) | `Visit.java:34` | person |
198
204
 
199
205
  ## Not modeled
200
206
  | Code | Reason |
@@ -207,8 +213,8 @@ Code root `petclinic/` · running UI: http://localhost:8080 (test account `georg
207
213
  ```markdown
208
214
  | # | Decision | Element(s) | Why | Source | Confirmed by |
209
215
  |---|----------|------------|-----|--------|--------------|
210
- | 1 | Insert into `visits` → event | *Visit Booked* | the only write of the booking | `VisitController.java:57` `#processNewVisitForm` | code |
211
- | 2 | Title *Book Visit*, not *New Visit Form* | *Book Visit* | the business word | person, 2nd answer | person |
216
+ | 1 | Insert into `visits` → event | ref:<visitBookedId> | the only write of the booking | `VisitController.java:57` `#processNewVisitForm` | code |
217
+ | 2 | Title *Book Visit*, not *New Visit Form* | ref:<bookVisitId> | the business word | person, 2nd answer | person |
212
218
 
213
219
  ## Code vs. business terms
214
220
  | Code | Board |
@@ -226,7 +232,7 @@ Element `description`s still carry their own short code reference. The feedback
226
232
 
227
233
  **When this run has a chat** (`CHAT_SESSION_ID` is set — the request came from the board's chat), a question the person answers with a pick is a **snippet** (`post_chat_message` with `snippet`; shapes in `/learn-eventmodelers-api` § 16 *Chat snippets*); an open question stays text. Without a chat, ask in prose.
228
234
 
229
- This overrides the `tasks`-snippet chat mode of `/timeline`. **Snippets are not a confirmation gate.** Never post a `tasks` snippet to let the person tick what you derived from the code — you build that directly (see *Principles*). Use a snippet only when you genuinely need the person (the Step 1 question about a running UI comes first, as plain text, and only when the analysis starts):
235
+ This overrides the `tasks`-snippet chat mode of `/timeline`. **Snippets are not a confirmation gate.** Never post a `tasks` snippet to let the person tick what you derived from the code — you build that directly (see *Principles*). Use a snippet only when you genuinely need the person (the Step 1 question about a running UI comes first, as plain text, when the analysis starts — then continue with the code analysis without waiting for the answer):
230
236
 
231
237
  1. **Order** — *"Which comes first?"* → a `poll` with the candidate orders; *"I'd put A before B — right?"* → a `confirm`.
232
238
  2. **Unclear business requirement** — the code leaves it open or contradicts itself (*"Can a visit be cancelled?"*, *"Is the specialty optional?"*) → a `confirm` or `poll` with your hypothesis; an open answer stays text.
@@ -251,7 +257,7 @@ One question per message, with the best hypothesis attached. The answer arrives
251
257
  | Given/When/Then scenarios from the tests | `/eventmodeling-elaborating-scenarios` |
252
258
  | Add or rename attributes along a chain | `/attributes` |
253
259
  | Realistic example data from tests and fixtures | `/examples` |
254
- | A screen where a person decides | `/html-screen` (wireframe sketch only on explicit request: `/storyboard-screen`) |
260
+ | A screen where a person decides | `/html-screen` — always HTML; a plain SCREEN only for a Playwright screenshot of the running UI (see *UI* lens), never `/storyboard-screen` |
255
261
  | Set a slice's status | `/update-slice-status` |
256
262
  | Record a decision with its code reference | *Recording decisions* (MARKDOWN note in the **Decisions** feedback lane) |
257
263
  | Open questions, unresolved code-vs-user conflicts | `/wdyt`, `/handle-comment` |
@@ -259,7 +265,16 @@ One question per message, with the best hypothesis attached. The answer arrives
259
265
  | Validate before reporting completion | `/eventmodeling-validating-event-models-checklist` |
260
266
  | Any endpoint or element type not covered above | `/learn-eventmodelers-api` |
261
267
 
262
- Scenarios: only business rules found in tests and comments — not simple validations ("must be a number"). Only create a storyline when the person explicitly asks.
268
+ ### Scenarios
269
+
270
+ *Which* rules to write is specific to legacy code: only business rules found in tests and comments — not simple validations ("must be a number"). Only create a storyline when the person explicitly asks.
271
+
272
+ *How* to write them is not — read `eventmodeling-elaborating-scenarios` before the first scenario and follow it; a test translated line by line is how wrong scenarios end up on the board. In particular:
273
+
274
+ - **What goes into Given, When and Then** — per command and per read model: § *1. Command Scenarios*, § *2. Command Scenarios - State Validation*, § *3. View Scenarios*, § *3b. List-type Read Model Scenarios*, § *4. Error Path Scenarios* (worked examples in its `references/examples.md`).
275
+ - **Which elements may appear in a step** — only those `get_spec_info` returns for the timeline: § *Post Scenarios to Board* → *Step 2 — Load valid step elements* and *Step 3 — Resolve step IDs*. A test's mocks, repositories or setup calls are never steps.
276
+ - **Rejections** — § *Step 4c — Error/rejection scenarios*.
277
+ - **Before reporting** — its § *Quality Checklist*.
263
278
 
264
279
  ---
265
280
 
@@ -147,16 +147,18 @@ The answer comes as a `CHAT` turn (`Please do these:` + ticked titles or ids). R
147
147
 
148
148
  Print the report below, with every finding as a `- [ ]` checkbox carrying its proposed fix, and ask which to apply. Wait for the answer.
149
149
 
150
+ Name each slice and element as `[<title>](ref:<nodeId>)` (the slice border's id for a slice). The terminal shows the title, and once the report is in a board note or an element description, a click zooms the canvas to it (see `/learn-eventmodelers-api` *Linking to elements in markdown*). A slice that exists only in code has no node, so it stays plain text.
151
+
150
152
  ```
151
153
  ## Architecture Drift — <scope: board | timeline "<title>" | context "<name>">
152
154
  Compared: <n> slices on the board, <n> slice folders in code Analysed: <ISO timestamp>
153
155
 
154
156
  ### Slices
155
157
  - [ ] <slice> — in code, not on the board → add it to the model
156
- - [ ] <slice> — board says Done, no code → set back to Planned
158
+ - [ ] [<slice>](ref:<sliceBorderId>) — board says Done, no code → set back to Planned
157
159
 
158
160
  ### Fields
159
- - [ ] <slice> / <element>: model `customerId: String`, code `custoemrId` → fix model typo
161
+ - [ ] [<slice>](ref:<sliceBorderId>) / [<element>](ref:<nodeId>): model `customerId: String`, code `custoemrId` → fix model typo
160
162
 
161
163
  ### Specifications / Screens / Edges
162
164
  - [ ] ...
@@ -448,7 +448,7 @@ Not delegated to a separate skill — performed directly by this orchestrating s
448
448
 
449
449
  The note's body lives in **`meta.description`** as plain markdown source — headings, lists, bold, code fences, tables all render. **Not `meta.content`** — that field is accepted and stored without error but never rendered by the board UI, producing a visibly empty note; this was caught by comparing against a note authored directly in the UI, so treat it as confirmed, not a guess. There is no separate render/sketch call (unlike SCREEN/HTML_SCREEN) and no `fields[]` array on this element type.
450
450
 
451
- **What the note should actually contain** — write for the next person (or next session) who opens this board cold, not for whoever just built it:
451
+ **What the note should actually contain** — write for the next person (or next session) who opens this board cold, not for whoever just built it. Wherever it names an element on the board, write `ref:<nodeId>`: it renders as the element's title and a click zooms the canvas to it (see `/learn-eventmodelers-api` *Linking to elements in markdown*):
452
452
  - **Scope**: what business process this chapter covers, and its entities (identity keys).
453
453
  - **Assumptions added beyond the literal brief** — anything invented to fill a gap the requirements left open, and why (e.g. adding a resolution event so a state isn't a one-way trap door).
454
454
  - **Business rules deliberately encoded as scenarios, not new events** — so a reader doesn't mistake a missing event for an oversight.
@@ -171,7 +171,7 @@ The summary lives **on the board, not in a file**: one MARKDOWN note per chapter
171
171
  2. Take the leftmost entry of `meta.timelineData.columns` as the first column (re-read the chapter after modeling — columns may have been added or reordered).
172
172
  3. Create the note: `node:created`, `meta.type: "MARKDOWN"`, `cellId = "<feedbackLaneId>-<firstColumnId>"`, title `Summary — <Chapter Name>`, body as plain markdown in **`meta.description`** (not `meta.content` — it is stored but never rendered).
173
173
 
174
- Exactly one summary note per chapter; on a re-run, update the existing one. Write it **after** the chapter's model, scenarios and review are complete, so it describes the finished shape. Use the source document's language (or `language`). Each note must read standalone for someone who has not seen the source, and carry enough detail to start specifying and building that chapter. Tables over prose, no filler.
174
+ Name every board element by link: `ref:<nodeId>` renders as the element's title and zooms the canvas to it on click (see `/learn-eventmodelers-api` *Linking to elements in markdown*). Exactly one summary note per chapter; on a re-run, update the existing one. Write it **after** the chapter's model, scenarios and review are complete, so it describes the finished shape. Use the source document's language (or `language`). Each note must read standalone for someone who has not seen the source, and carry enough detail to start specifying and building that chapter. Tables over prose, no filler.
175
175
 
176
176
  **Every chapter's note:**
177
177
 
@@ -183,7 +183,7 @@ Exactly one summary note per chapter; on a re-run, update the existing one. Writ
183
183
  | What | When | Source | Consequence of missing it |
184
184
 
185
185
  ## Business rules
186
- | Rule (verbatim thresholds) | Modeled in (command / read model) | Scenario | Source |
186
+ | Rule (verbatim thresholds) | Modeled in (`ref:<nodeId>` of the command / read model) | Scenario | Source |
187
187
 
188
188
  ## Requirements & deliverables
189
189
  Mandatory vs. optional (Muss/Kann), each with the board element that tracks it.
@@ -192,7 +192,7 @@ Mandatory vs. optional (Muss/Kann), each with the board element that tracks it.
192
192
  Forms/documents exchanged and their fields, where known.
193
193
 
194
194
  ## Elements
195
- Counts and names of the key events, commands, read models, automations, screens, and the slice list (state-change / state-view / automation). Note external parties and their translated events.
195
+ Counts and names of the key events, commands, read models, automations, screens, and the slice list (state-change / state-view / automation), each as `ref:<nodeId>`. Note external parties and their translated events.
196
196
 
197
197
  ## Open questions & risks
198
198
  | Question / risk | Source(s) | Why it matters |