@eventmodelers/cli 2.0.2 → 2.0.4

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.
Files changed (35) hide show
  1. package/README.md +1 -0
  2. package/cli.js +61 -10
  3. package/package.json +1 -1
  4. package/shared/build-kit/lib/build-log-share.js +156 -0
  5. package/shared/build-kit/lib/events-view.js +2 -0
  6. package/shared/build-kit/lib/ralph.js +8 -1
  7. package/shared/build-kit/lib/slice-events.js +42 -2
  8. package/shared/build-kit/lib/slice-info.js +52 -0
  9. package/shared/build-kit/lib/slice-state.js +1 -1
  10. package/shared/scaffold/supabase/generate.mjs +1 -1
  11. package/shared/scaffold/supabase/template/web/src/app/TimelinePage.tsx.tmpl +1 -1
  12. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +10 -0
  13. package/stacks/axon/templates/build-kit/lib/prompt.md +10 -0
  14. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +10 -0
  15. package/stacks/blank/templates/build-kit/lib/prompt.md +10 -0
  16. package/stacks/classic/templates/build-kit/lib/backend-prompt.md +10 -0
  17. package/stacks/classic/templates/build-kit/lib/prompt.md +10 -0
  18. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +10 -0
  19. package/stacks/kurrent/templates/build-kit/lib/prompt.md +10 -0
  20. package/stacks/modeling-kit/templates/.claude/skills/prototype/SKILL.md +2 -0
  21. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/prototype.mjs +7 -5
  22. package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +1 -1
  23. package/stacks/modeling-kit/templates/kit/CLAUDE.md +2 -1
  24. package/stacks/node/templates/build-kit/lib/backend-prompt.md +10 -0
  25. package/stacks/node/templates/build-kit/lib/prompt.md +10 -0
  26. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +10 -0
  27. package/stacks/opencqrs/templates/build-kit/lib/prompt.md +10 -0
  28. package/stacks/react/templates/build-kit/lib/backend-prompt.md +10 -0
  29. package/stacks/react/templates/build-kit/lib/prompt.md +10 -0
  30. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +10 -0
  31. package/stacks/supabase/templates/build-kit/lib/prompt.md +10 -0
  32. package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +10 -0
  33. package/stacks/supabase-react/templates/build-kit/lib/prompt.md +10 -0
  34. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +10 -0
  35. package/stacks/umadb/templates/build-kit/lib/prompt.md +10 -0
package/README.md CHANGED
@@ -166,6 +166,7 @@ npx @eventmodelers/cli run --agent <name> # pick the agent: claude (de
166
166
  npx @eventmodelers/cli run --agent <name> --model <id> # …on a given model: -m for opencode/codex/gemini, --model for Claude, LOCAL_AI_MODEL for --local-ai
167
167
  npx @eventmodelers/cli run --exec "<command>" # manual fallback for any other harness (ralph-exec.js) — agent tracing needs its JSON flag, e.g. "codex exec --json --full-auto"
168
168
  npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
169
+ npx @eventmodelers/cli run --sync # share this build agent's event log with the board, constantly, for a live dashboard (see agent-build-log.md)
169
170
  npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
170
171
  npx @eventmodelers/cli run --standalone # same, plus acting on board changes unprompted — and needs no install at all
171
172
  npx @eventmodelers/cli run --standalone --board-id <uuid> # …from any directory, against any board (see Power users)
package/cli.js CHANGED
@@ -46,6 +46,7 @@ import { filterEvents, baselineIds, formatEvent, formatPayload, sliceTitles, sum
46
46
  import { stableGlobalAgentId } from './lib/global-agent-id.js';
47
47
  import { ensureLearningsGitRules } from './shared/build-kit/lib/ralph.js';
48
48
  import { createCallTracker, heldPromptsRequest, journalPromptAction, shouldJournalCall, journalPromptFinished, journalPromptReceived, recoverOwnPrompts } from './shared/build-kit/lib/prompt-journal.js';
49
+ import { INFO_KINDS, infoComment, recordInfo, recordTurnInfos } from './shared/build-kit/lib/slice-info.js';
49
50
  import { listLearnings, mergeLearnings, recordLearning, recordTurnLearnings, retractLearning, syncLearnings } from './shared/build-kit/lib/slice-learnings.js';
50
51
 
51
52
  const __filename = fileURLToPath(import.meta.url);
@@ -2681,15 +2682,18 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
2681
2682
  try {
2682
2683
  turnPromptId = p.id;
2683
2684
  const reply = await runTurn(buildTurn(p));
2684
- // The agent cannot run `learn` (locked-down Bash), so it states learnings in its reply and they are recorded here.
2685
- // Best effort: a failed write never takes the prompt queue down.
2686
- if (keepsRecord) {
2687
- try {
2685
+ // The agent cannot run `learn` or `info` (locked-down Bash), so it states them in its reply and they are recorded here.
2686
+ // Best effort: a failed write never takes the prompt queue down. Infos are events stamped with their board, so every
2687
+ // run records them — standalone included; learnings are kept only where `keepsRecord` (they feed AGENTS.md).
2688
+ try {
2689
+ if (keepsRecord) {
2688
2690
  const n = recordTurnLearnings({ kitDir, reply, meta: journalMeta });
2689
2691
  if (n) log(`recorded ${n} learning${n === 1 ? '' : 's'} from the turn`);
2690
- } catch (err) {
2691
- log(`could not record the turn's learnings: ${err.message}`);
2692
2692
  }
2693
+ const infos = recordTurnInfos({ kitDir, reply, meta: journalMeta });
2694
+ if (infos) log(`recorded ${infos} info event${infos === 1 ? '' : 's'} from the turn`);
2695
+ } catch (err) {
2696
+ log(`could not record the turn's learnings or infos: ${err.message}`);
2693
2697
  }
2694
2698
  journal(() => journalPromptFinished({ kitDir, id: p.id, outcome: 'worked', meta: journalMeta }));
2695
2699
  } catch (err) {
@@ -3826,6 +3830,7 @@ credentialFlags(program
3826
3830
  .option('--browser', 'Give the locked-down modeling agent a browser: the Playwright MCP server (npx @playwright/mcp) next to the eventmodelers one, for discover-storyboard. Off by default because a browser can reach any site, which the lockdown otherwise rules out. Default Claude runner only; --dangerously-skip-permissions already loads every MCP server the machine configures.')
3827
3831
  .option('--handover', 'Backend build kit: for this run only, finish slices that have a UI at `Waiting` instead of `Done` (same as the project setting `handover: true`, which `init --handover` / `config --handover on` save — this flag writes nothing). Without it the project setting applies; without both, slices end at `Done`.')
3828
3832
  .option('--status-trigger <status>', 'Build kit: the slice status the agent picks work up from, for this run only (e.g. Ready, Planned, Waiting). Defaults to the kit\'s own pickup status — `Planned` for backend kits, `Handover` for the UI kits. Nothing is written to disk.')
3833
+ .option('--sync', 'Build kit: share this agent\'s event log with the board — events that name a slice are uploaded constantly (POST …/buildlog) for a live dashboard. The platform keeps one iteration per slice, 1000 entries at most. A no-op for --modeling/--standalone, and with --local.')
3829
3834
  .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.')
3830
3835
  .option('--id <id>', 'Pin the agent id this run identifies itself with on the platform. Without it an agent keeps one id: a project install per project, a --global/--standalone run per board and repo (the directory it is started in) — the same agent on every restart, with its chat, its star on the board and the prompts it holds. If another live process already runs under that id, this one gets a fresh id instead (two agents must not share a heartbeat row). Pass --id when a supervisor already knows the id you want, or for a second agent on purpose; it is used as given, with no such check. Per-run only: nothing is written to disk.')
3831
3836
  .option('--name <name>', 'A human-readable name for this agent, sent with every heartbeat so the board shows which agent is live rather than a bare uuid (e.g. "ci-builder", "martins-laptop"). Per-run only, like --id: the persistent name is `agentName` in config.json (set via `init --name` / `init-config --name`), and this overrides it for one run without writing anything.')
@@ -3858,8 +3863,10 @@ credentialFlags(program
3858
3863
  }
3859
3864
  // Asked once, for the kit in this directory (the first one found when there is none), and
3860
3865
  // handed to the others — the modeling agent reads this directory's config.
3861
- const rootDir = dirs.find((dir) => dirname(dir) === cwd) ?? dirs[0];
3862
- console.log(`\n🔑 Credentials for ${relative(cwd, rootDir) || '.'}, shared by all`);
3866
+ // With no build kit (--modeling alone), the credentials belong to this directory itself:
3867
+ // a kit dir that need not exist, since only its parent is used.
3868
+ const rootDir = dirs.find((dir) => dirname(dir) === cwd) ?? dirs[0] ?? join(cwd, '.agent-modeling-kit');
3869
+ console.log(`\n🔑 Credentials for ${dirs.length ? relative(cwd, rootDir) || '.' : 'the modeling agent'}, shared by all`);
3863
3870
  const { organizationId, boardId, token, baseUrl } = await forceBuildKitCredentials(rootDir, cwd, opts, globalOpts);
3864
3871
  for (const dir of dirs.filter((d) => d !== rootDir)) {
3865
3872
  await forceBuildKitCredentials(dir, cwd, opts, globalOpts, { organizationId, boardId, token, baseUrl });
@@ -3949,6 +3956,7 @@ credentialFlags(program
3949
3956
  // no meaning for a build kit, which is scaffolded per project by definition.
3950
3957
  if (opts.modeling || opts.standalone || opts.worker || opts.global) {
3951
3958
  const picked = opts.modeling ? '--modeling' : opts.standalone ? '--standalone' : opts.worker ? '--worker' : '--global';
3959
+ if (opts.sync) console.log(`ℹ️ --sync shares a build agent's log — it does nothing with ${picked}.`);
3952
3960
  // --local-ai and --exec name what does a turn, not a queue, and the modeling loop has its own
3953
3961
  // runner for each (lib/modeling-local-ai.js, lib/modeling-exec.js).
3954
3962
  const modelingLocalAi = resolveLocalAiTarget(opts);
@@ -4114,7 +4122,7 @@ credentialFlags(program
4114
4122
  // path with no stream-json parsing to gate. RALPH_LOCAL is read by ralph.js's startRalph to force
4115
4123
  // the local-only branch even when .eventmodelers/config.json has valid credentials.
4116
4124
  // RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read there too.
4117
- execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_HANDOVER: opts.handover ? '1' : '', RALPH_STATUS_TRIGGER: opts.statusTrigger ? String(opts.statusTrigger).trim() : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}), ...(typeof opts.exec === 'string' ? { RALPH_EXEC_CMD: opts.exec } : {}) } });
4125
+ execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_SYNC: opts.sync ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_HANDOVER: opts.handover ? '1' : '', RALPH_STATUS_TRIGGER: opts.statusTrigger ? String(opts.statusTrigger).trim() : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}), ...(typeof opts.exec === 'string' ? { RALPH_EXEC_CMD: opts.exec } : {}) } });
4118
4126
  } catch (err) {
4119
4127
  process.exit(err.status || 1);
4120
4128
  }
@@ -4740,6 +4748,42 @@ program
4740
4748
  }
4741
4749
  });
4742
4750
 
4751
+ program
4752
+ .command('info <text>')
4753
+ .description("For a build agent: record something that is not a reusable learning — what you cannot change and why (limitation), a decision you took, a note. It is an event in the log and a comment on the slice. Reusable knowledge about the codebase or model is a `learn`, not an info.")
4754
+ .option('--kind <kind>', `${INFO_KINDS.join(' · ')} (default note)`, 'note')
4755
+ .option('--slice <idOrTitle>', 'The slice it is about (default: the one the run loop has claimed)')
4756
+ .option('--tag <tag>', 'Label the info (repeatable)', (tag, tags) => [...tags, tag], [])
4757
+ .action(async (text, opts, command) => {
4758
+ const cwd = process.cwd();
4759
+ const kitDir = requireBuildKitDir(cwd);
4760
+ const meta = agentEventMeta(cwd, kitDir, command);
4761
+ try {
4762
+ let sliceId;
4763
+ if (opts.slice) sliceId = findSliceDir(kitDir, opts.slice)?.slice.id ?? opts.slice;
4764
+ else {
4765
+ const claimed = Object.entries(fold(readEvents(kitDir)).slices).filter(([, s]) => s.claim);
4766
+ if (claimed.length === 1) sliceId = claimed[0][0];
4767
+ }
4768
+ const info = recordInfo({ kitDir, text, kind: opts.kind, slice: sliceId, tags: opts.tag, meta });
4769
+ console.log(`✅ Recorded ${info.kind} (${info.id.slice(0, 8)}).`);
4770
+ if (!sliceId) return console.log('ℹ️ No slice named or claimed — not posted to the board.');
4771
+ // Best effort: the event is the record, the comment is how the board shows it.
4772
+ const cfg = loadEffectiveConfig(cwd, kitDir, command.optsWithGlobals().config).config;
4773
+ if (['organizationId', 'boardId', 'token'].some((f) => !cfg[f])) return console.log('ℹ️ No board credentials — not posted to the board.');
4774
+ const res = await fetch(`${cfg.baseUrl || DEFAULT_BASE_URL}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes/${sliceId}/comments`, {
4775
+ method: 'POST',
4776
+ headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-user-id': 'cli-info', ...agentHeaders(cfg) },
4777
+ body: JSON.stringify({ text: infoComment(info), type: 'COMMENT', author: 'build-agent' }),
4778
+ signal: AbortSignal.timeout(15_000),
4779
+ });
4780
+ console.log(res.ok ? '✅ Posted as a comment on the slice.' : `⚠️ Board returned HTTP ${res.status} — the info is only in the event log.`);
4781
+ } catch (err) {
4782
+ console.error(`❌ ${err.message}`);
4783
+ process.exit(1);
4784
+ }
4785
+ });
4786
+
4743
4787
  program
4744
4788
  .command('events')
4745
4789
  .description("Decode a kit's event log in the terminal: one line per event (claims, settles, learnings, tasks, prompts …) with the slice's title and the agent that wrote it. Filter it, print the raw events, or print the state the log folds to.")
@@ -5112,5 +5156,12 @@ program
5112
5156
  }
5113
5157
  });
5114
5158
 
5115
- await program.parseAsync();
5159
+ try {
5160
+ await program.parseAsync();
5161
+ } catch (err) {
5162
+ // A bug or bad input must end as one readable line, not a Node stack trace.
5163
+ console.error(`❌ ${err?.message ?? err}`);
5164
+ if (process.env.EVENTMODELERS_DEBUG && err?.stack) console.error(err.stack);
5165
+ process.exitCode = 1;
5166
+ }
5116
5167
  if (sharedRl) sharedRl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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": {
@@ -0,0 +1,156 @@
1
+ // A build agent shares its event log with the board — two capabilities, both build-agent only (a modeling
2
+ // agent gets no-ops):
3
+ //
4
+ // iterations every slice build turn is an iteration: `iteration-started` / `iteration-closed` events in the
5
+ // log, each carrying the slice, the iteration id, the board, the org and the date — and a
6
+ // `slice-done` after an iteration that settled the slice as Done. Always on.
7
+ // sync with --sync, the events that name a slice are uploaded to POST …/boards/:boardId/buildlog,
8
+ // numbered (only while the slice has an iteration open — an agent records only when it is active; the event's own `seq`, which defines the order) and tagged with the kit type (backend, or ui for a kit that picks up from Waiting) —
9
+ // constantly, on a short interval for as long as the loop runs, so a live dashboard follows the
10
+ // build, and once more at the start and end of each iteration.
11
+ //
12
+ // Best effort, like tracing: a cursor (the last event id sent) lives next to the log and only moves forward
13
+ // when the upload succeeded, so a failed upload is simply sent again with the next one. The server holds one
14
+ // iteration per slice and kit type — every `iteration-started` resets that log — and at most 1000 entries.
15
+
16
+ import { randomUUID } from 'crypto';
17
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { appendEvent, readEvents } from './slice-events.js';
20
+ import { statusKey } from './slice-state.js';
21
+
22
+ export const SYNC_STATE_FILE = 'build-log-sync.json';
23
+ export const SYNC_INTERVAL_MS = 2_000;
24
+ // Always shared, even with no iteration open: they are what opens and closes one.
25
+ const BOUNDARY = new Set(['iteration-started', 'iteration-closed', 'slice-done']);
26
+ const NOOP = { iterationStarted: () => null, iterationClosed: () => {}, flush: async () => {}, start: () => {}, stop: () => {} };
27
+
28
+ function readState(path) {
29
+ try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return {}; }
30
+ }
31
+
32
+ function writeState(path, state) {
33
+ const tmp = `${path}.${process.pid}.tmp`;
34
+ writeFileSync(tmp, JSON.stringify(state), 'utf-8');
35
+ renameSync(tmp, path);
36
+ }
37
+
38
+ // The wire shape of one log event: the envelope the endpoint reads, everything else as payload.
39
+ export function toBuildLogEntry(e, { agentId, iteration, boardId, organizationId, kitType, seq }) {
40
+ const { id, at, type, slice, meta: _meta, tags: _tags, ...rest } = e;
41
+ return { ...rest, id, type, sliceId: slice, agentId, iteration, occurredAt: at, boardId, organizationId, kitType, seq };
42
+ }
43
+
44
+ // A kit that picks slices up from Waiting builds the UI; any other builds the backend.
45
+ export const kitTypeOf = (pickupStatus) => (String(pickupStatus ?? '').trim().toLowerCase() === 'waiting' ? 'ui' : 'backend');
46
+
47
+ export function createBuildLogShare({ kitDir, cfg, meta, agentType = 'BUILD', kitType = 'backend', sync = false, enabled = true, intervalMs = SYNC_INTERVAL_MS, log = () => {} } = {}) {
48
+ if (agentType !== 'BUILD') return NOOP;
49
+ const statePath = join(kitDir, SYNC_STATE_FILE);
50
+ const canUpload = !!(sync && enabled && cfg?.baseUrl && cfg?.token && cfg?.organizationId && cfg?.boardId && cfg?.agentId);
51
+ const open = new Map(); // slice → the iteration in progress
52
+ let timer = null;
53
+ let inFlight = null;
54
+ let warned = false;
55
+
56
+ async function upload() {
57
+ const state = readState(statePath);
58
+ const events = readEvents(kitDir);
59
+ const from = state.cursor ? events.findIndex((e) => e.id === state.cursor) + 1 : 0; // a pruned cursor re-sends; the server drops duplicates
60
+ const fresh = events.slice(from);
61
+ if (!fresh.length) return;
62
+ const iterations = { ...(state.iterations ?? {}) };
63
+ const entries = [];
64
+ for (const e of fresh) {
65
+ if (!e.slice) continue;
66
+ // An agent records only while it is active: events of a slice with no iteration open are not shared. The kit mirrors
67
+ // every slice's board status as `board-observed`, whether or not it is working on it — that is not the agent's work.
68
+ // The boundaries themselves (the iteration's start and end, and slice-done after it) always are.
69
+ if (!iterations[e.slice] && !BOUNDARY.has(e.type)) continue;
70
+ if (e.type === 'iteration-started') iterations[e.slice] = e.iteration;
71
+ entries.push(toBuildLogEntry(e, { agentId: cfg.agentId, iteration: e.iteration ?? iterations[e.slice], boardId: cfg.boardId, organizationId: cfg.organizationId, kitType, seq: e.seq }));
72
+ if (e.type === 'iteration-closed') delete iterations[e.slice];
73
+ }
74
+ if (entries.length) {
75
+ const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/buildlog`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-agent-id': cfg.agentId },
78
+ body: JSON.stringify({ events: entries }),
79
+ signal: AbortSignal.timeout(15_000),
80
+ });
81
+ if (!res.ok) {
82
+ const reason = `HTTP ${res.status}${await res.text().then((t) => `: ${t}`).catch(() => '')}`;
83
+ // A batch the platform rejects as invalid can never succeed: skip it rather than resend it every interval.
84
+ if (res.status !== 400) throw new Error(reason);
85
+ log(`info: the platform did not take ${entries.length} build log entries (${reason}) — skipped`);
86
+ }
87
+ }
88
+ writeState(statePath, { cursor: fresh[fresh.length - 1].id, iterations });
89
+ }
90
+
91
+ // One upload at a time. A call that arrives while one runs asks for another pass right after it, so events
92
+ // appended in between are never left to wait for the interval.
93
+ let again = false;
94
+ function flush() {
95
+ if (!canUpload) return Promise.resolve();
96
+ if (inFlight) { again = true; return inFlight; }
97
+ inFlight = (async () => {
98
+ do {
99
+ again = false;
100
+ try { await upload(); warned = false; } catch (err) {
101
+ if (!warned) { warned = true; log(`info: build log upload failed (${err.message}) — will send it with the next one`); }
102
+ break;
103
+ }
104
+ } while (again);
105
+ })().finally(() => { inFlight = null; });
106
+ return inFlight;
107
+ }
108
+
109
+ return {
110
+ iterationStarted(planned) {
111
+ const iteration = randomUUID();
112
+ try {
113
+ appendEvent(kitDir, { type: 'iteration-started', slice: planned.id, iteration, title: planned.title, kitType, boardId: cfg?.boardId, organizationId: cfg?.organizationId }, meta);
114
+ } catch (err) {
115
+ log(`info: could not record the start of an iteration (${err.message}) — the build goes on`);
116
+ return null;
117
+ }
118
+ open.set(planned.id, iteration);
119
+ flush();
120
+ return iteration;
121
+ },
122
+ async iterationClosed(planned, status) {
123
+ const iteration = open.get(planned.id);
124
+ if (!iteration) return;
125
+ open.delete(planned.id);
126
+ try {
127
+ appendEvent(kitDir, { type: 'iteration-closed', slice: planned.id, iteration, status, kitType, boardId: cfg?.boardId, organizationId: cfg?.organizationId }, meta);
128
+ } catch (err) {
129
+ log(`info: could not record the end of an iteration (${err.message}) — the build goes on`);
130
+ return;
131
+ }
132
+ // The iteration that settled the slice as Done ends with `slice-done`; if it is the slice's newest entry when
133
+ // the next iteration starts, the platform knows the slice was reopened and starts its log over.
134
+ try {
135
+ const events = readEvents(kitDir);
136
+ const from = events.findLastIndex((e) => e.type === 'iteration-started' && e.iteration === iteration);
137
+ if (events.slice(from + 1).some((e) => e.type === 'settled' && e.slice === planned.id && statusKey(e.status) === 'done')) {
138
+ appendEvent(kitDir, { type: 'slice-done', slice: planned.id, iteration, kitType, boardId: cfg?.boardId, organizationId: cfg?.organizationId }, meta);
139
+ }
140
+ } catch (err) {
141
+ log(`info: could not record that the slice is done (${err.message}) — the build goes on`);
142
+ }
143
+ await flush();
144
+ },
145
+ flush,
146
+ start() {
147
+ if (!canUpload || timer) return;
148
+ timer = setInterval(flush, intervalMs);
149
+ timer.unref?.();
150
+ },
151
+ stop() {
152
+ if (timer) clearInterval(timer);
153
+ timer = null;
154
+ },
155
+ };
156
+ }
@@ -51,6 +51,8 @@ function describe(e) {
51
51
  return ['', `${plural(e.sources?.length ?? e.learnings?.length ?? 0, 'learning')} → ${oneLine(e.text, 120)}${e.reason ? ` — ${oneLine(e.reason, 80)}` : ''}`];
52
52
  case 'learnings-curated':
53
53
  return ['', ''];
54
+ case 'info-recorded':
55
+ return [e.slice, `[${e.kind ?? 'note'}] ${oneLine(e.text, 160)}`];
54
56
  case 'prompt-received':
55
57
  return [e.prompt?.id, `"${oneLine(e.prompt?.text, 80)}"${e.prompt?.priority ? ', priority' : ''}, pid ${e.pid}`];
56
58
  case 'prompt-action':
@@ -20,6 +20,7 @@ import { readEvents } from './slice-events.js';
20
20
  import { claimSlice, recordEvent, settleSlice, settleLeftoverClaims } from './slice-run.js';
21
21
  import { isTreeClean } from './slice-git.js';
22
22
  import { syncLearnings } from './slice-learnings.js';
23
+ import { createBuildLogShare, kitTypeOf, SYNC_STATE_FILE } from './build-log-share.js';
23
24
  import { curateIfDue } from './slice-curation.js';
24
25
  import { queueTask, workNextTask } from './slice-tasks.js';
25
26
  import { defaultAgentName, withInstanceSuffix } from './agent-name.js';
@@ -739,7 +740,7 @@ function ensureLearningsGitRules(kitDir) {
739
740
  const projectDir = dirname(kitDir);
740
741
  if (!existsSync(join(projectDir, '.git'))) return;
741
742
  const rules = [
742
- ['info/exclude', [`${basename(kitDir)}/events/`, `${basename(kitDir)}/events.log`]],
743
+ ['info/exclude', [`${basename(kitDir)}/events/`, `${basename(kitDir)}/events.log`, `${basename(kitDir)}/events.seq`, `${basename(kitDir)}/${SYNC_STATE_FILE}`]],
743
744
  ['info/attributes', [`${basename(kitDir)}/AGENTS.md merge=union`]],
744
745
  ];
745
746
  for (const [name, lines] of rules) {
@@ -769,6 +770,10 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false,
769
770
  const board = credentialed ? createBoard(cfg) : null;
770
771
  const meta = eventMeta(cfg);
771
772
  ensureLearningsGitRules(kitDir);
773
+ // Sharing the event log with the board (`run --sync`): needs the board, so never with --local.
774
+ const share = createBuildLogShare({ kitDir, cfg, meta, agentType: cfg?.agentType ?? 'BUILD', kitType: kitTypeOf(readPickupStatus(kitDir)), sync: process.env.RALPH_SYNC === '1', enabled: credentialed, log: (line) => console.log(`[ralph] ${line}`) });
775
+ if (process.env.RALPH_SYNC === '1' && !credentialed) console.log('[ralph] --sync needs board credentials — the build log stays local.');
776
+ share.start();
772
777
  let lastIdleCtx;
773
778
  // The build agent's final reply on the most recent planned-slice turn — quoted in the Blocked note.
774
779
  let lastReply = null;
@@ -831,6 +836,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false,
831
836
  }
832
837
  }
833
838
  lastReply = null;
839
+ if (planned.id) share.iterationStarted(planned);
834
840
  const built = await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, async () => { lastReply = await onPlannedSlice(prompt, {
835
841
  sliceId: planned.id,
836
842
  sliceTitle: planned.title,
@@ -842,6 +848,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false,
842
848
  }); });
843
849
  if (built) console.log(`[ralph] Slice build complete — waiting for next slice`);
844
850
  if (planned.id) await settleSlice({ kitDir, planned, board, meta, pickupStatus: readPickupStatus(kitDir), details: { built, lastReply } });
851
+ if (planned.id) await share.iterationClosed(planned, built ? 'ok' : 'error');
845
852
  if (credentialed) await fetchAndPersistSlices(cfg, kitDir).catch(() => {});
846
853
  didWork = true;
847
854
  }
@@ -11,6 +11,7 @@ import { join } from 'path';
11
11
 
12
12
  export const EVENTS_DIR = 'events';
13
13
  export const INDEX_FILE = 'events.log';
14
+ export const SEQ_FILE = 'events.seq';
14
15
 
15
16
  const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
16
17
 
@@ -49,10 +50,41 @@ export function normalizeMeta(meta) {
49
50
  return clean;
50
51
  }
51
52
 
53
+ // The last sequence number handed out is remembered next to the index (SEQ_FILE), so numbers keep climbing
54
+ // when events are pruned or the log is rebuilt. A kit that has no such file yet (a log written before events
55
+ // were numbered) continues from its newest event: its own `seq`, or how many events precede it. Two processes
56
+ // appending in the same instant can draw the same number — nothing enforces uniqueness, here or on the server:
57
+ // the index order decides, and readEvents renumbers so a reader still sees a strictly increasing sequence.
58
+ function newestEventSeq(kitDir) {
59
+ const index = join(kitDir, INDEX_FILE);
60
+ if (!existsSync(index)) return 0;
61
+ const ids = readFileSync(index, 'utf-8').split('\n').map((l) => l.trim()).filter((l) => ID.test(l));
62
+ for (let i = ids.length - 1; i >= 0; i--) {
63
+ try {
64
+ const { seq } = JSON.parse(readFileSync(join(kitDir, EVENTS_DIR, `${ids[i]}.json`), 'utf-8'));
65
+ return Number.isInteger(seq) ? seq : i + 1;
66
+ } catch { /* unreadable — look at the one before */ }
67
+ }
68
+ return 0;
69
+ }
70
+
71
+ // Takes the next number and remembers it.
72
+ function nextSeq(kitDir) {
73
+ const file = join(kitDir, SEQ_FILE);
74
+ let remembered = 0;
75
+ try { remembered = Number.parseInt(readFileSync(file, 'utf-8'), 10) || 0; } catch { /* none yet */ }
76
+ const seq = Math.max(remembered, newestEventSeq(kitDir)) + 1;
77
+ const tmp = `${file}.${randomUUID()}.tmp`;
78
+ writeFileSync(tmp, String(seq), 'utf-8');
79
+ renameSync(tmp, file);
80
+ return seq;
81
+ }
82
+
52
83
  // The event file is written whole (temp file, then rename) before its id is indexed, so a reader that
53
84
  // finds the id finds a complete file. The index line is one short append-mode write, so lines from
54
85
  // concurrent processes never interleave however large the event. After a torn index line the new id
55
- // starts on a fresh line instead of being glued onto the fragment. Returns the event id.
86
+ // starts on a fresh line instead of being glued onto the fragment. Every event gets the next sequence number
87
+ // (`seq`), which — not the timestamp — defines the order. Returns the event id.
56
88
  export function appendEvent(kitDir, event, meta) {
57
89
  const dir = join(kitDir, EVENTS_DIR);
58
90
  mkdirSync(dir, { recursive: true });
@@ -67,6 +99,7 @@ export function appendEvent(kitDir, event, meta) {
67
99
  ...rest,
68
100
  ...(cleanTags.length ? { tags: cleanTags } : {}),
69
101
  ...(Object.keys(cleanMeta).length ? { meta: cleanMeta } : {}),
102
+ seq: nextSeq(kitDir),
70
103
  id,
71
104
  }, null, 2), 'utf-8');
72
105
  renameSync(tmp, join(dir, `${id}.json`));
@@ -75,7 +108,7 @@ export function appendEvent(kitDir, event, meta) {
75
108
  return id;
76
109
  }
77
110
 
78
- // All events in index order; with `tag`, only those carrying it.
111
+ // All events in index order, each with its `seq`; with `tag`, only those carrying it.
79
112
  export function readEvents(kitDir, { tag } = {}) {
80
113
  const index = join(kitDir, INDEX_FILE);
81
114
  if (!existsSync(index)) return [];
@@ -89,5 +122,12 @@ export function readEvents(kitDir, { tag } = {}) {
89
122
  events.push(JSON.parse(readFileSync(join(kitDir, EVENTS_DIR, `${id}.json`), 'utf-8')));
90
123
  } catch { /* indexed but missing or unreadable — skipped */ }
91
124
  }
125
+ // Strictly increasing in index order: an event from before numbering, or one that drew the same number as its
126
+ // predecessor, takes the next free one.
127
+ let prev = 0;
128
+ for (const e of events) {
129
+ if (!Number.isInteger(e.seq) || e.seq <= prev) e.seq = prev + 1;
130
+ prev = e.seq;
131
+ }
92
132
  return tag === undefined ? events : events.filter((e) => e.tags?.includes(tag));
93
133
  }
@@ -0,0 +1,52 @@
1
+ // Info events: what an agent reports that is not a reusable learning — "I can't change X because Y", a
2
+ // decision it took, a note for whoever reads the board. A learning is true of the codebase or the model for
3
+ // every later build (slice-learnings.js); an info is about this slice, now. Infos feed no projection — the
4
+ // event is the record — and are also posted as a comment on the slice, so the board shows them.
5
+
6
+ import { appendEvent } from './slice-events.js';
7
+
8
+ export const INFO_KINDS = ['limitation', 'decision', 'note'];
9
+ const MAX_INFO_LENGTH = 500;
10
+
11
+ export function recordInfo({ kitDir, text, kind = 'note', slice, tags, meta }) {
12
+ const clean = String(text ?? '').trim();
13
+ if (!clean) throw new Error('the info is empty');
14
+ if (!INFO_KINDS.includes(kind)) throw new Error(`kind must be ${INFO_KINDS.join(', ')}, not "${kind}"`);
15
+ const id = appendEvent(kitDir, { type: 'info-recorded', kind, text: clean.slice(0, MAX_INFO_LENGTH), ...(slice ? { slice } : {}), tags }, meta);
16
+ return { id, text: clean.slice(0, MAX_INFO_LENGTH), kind };
17
+ }
18
+
19
+ // The comment the board shows for an info.
20
+ export function infoComment({ kind, text }) {
21
+ return `[${kind}] ${text}`;
22
+ }
23
+
24
+ // ── infos a turn states in its reply ────────────────────────────────────────────
25
+ // Same contract as learnings: the locked-down modeling agent ends a reply with `Info [<kind>]: <line>` and
26
+ // the CLI records it. At most five per reply, each capped, so what a board collaborator can talk the agent
27
+ // into saying stays small.
28
+ const MAX_TURN_INFOS = 5;
29
+ const TURN_INFO = /^\s*(?:[-*]\s+)?info(?:\s*\[([^\]]{1,20})\])?:\s*(.*?)\s*$/i;
30
+
31
+ export function parseTurnInfos(reply) {
32
+ const found = [];
33
+ let fenced = false;
34
+ for (const line of String(reply ?? '').split('\n')) {
35
+ if (line.trim().startsWith('```')) { fenced = !fenced; continue; }
36
+ if (fenced) continue;
37
+ const m = TURN_INFO.exec(line);
38
+ if (!m) continue;
39
+ const text = m[2];
40
+ if (!text || /^none\.?$/i.test(text) || text.length > MAX_INFO_LENGTH) continue;
41
+ const kind = m[1]?.trim().toLowerCase();
42
+ found.push({ kind: INFO_KINDS.includes(kind) ? kind : 'note', text });
43
+ if (found.length === MAX_TURN_INFOS) break;
44
+ }
45
+ return found;
46
+ }
47
+
48
+ export function recordTurnInfos({ kitDir, reply, meta }) {
49
+ const infos = parseTurnInfos(reply);
50
+ for (const { kind, text } of infos) recordInfo({ kitDir, text, kind, tags: ['modeling'], meta });
51
+ return infos.length;
52
+ }
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Events (one writer per type): board-observed, claimed, claim-confirmed, agent-recorded, settled,
6
6
  // released, attempt-failed, blocked, learning-recorded, learning-retracted, learning-merged, learnings-curated,
7
- // task-queued, task-done, prompt-received, prompt-finished. Unknown types are ignored, so a
7
+ // info-recorded (read by nothing here — it is the record), task-queued, task-done, prompt-received, prompt-finished. Unknown types are ignored, so a
8
8
  // newer loop's log is readable.
9
9
 
10
10
  const emptySlice = () => ({
@@ -164,7 +164,7 @@ const pageList = [...pages.values()].map(({name, title, route, hasParams, slices
164
164
  write('web/src/app/model.ts', `// GENERATED by "eventmodelers scaffold" from the event model. Do not edit by hand: re-run the command.
165
165
  // Navigation has one entry per timeline (chapter). Pages are one per distinct screen title (web kit convention).
166
166
 
167
- export type SliceInfo = { title: string; type: string; endpoint?: string };
167
+ export type SliceInfo = { title: string; type?: string; endpoint?: string };
168
168
  export type TimelineInfo = { title: string; slug: string; pages: string[]; slices: SliceInfo[] };
169
169
  export type PageInfo = { name: string; title: string; route: string; hasParams: boolean; slices: string[] };
170
170
 
@@ -60,7 +60,7 @@ export function TimelinePage() {
60
60
  {timeline.slices.map((s) => (
61
61
  <tr key={s.title}>
62
62
  <td>{s.title}</td>
63
- <td>{TYPE_LABEL[s.type] ?? s.type}</td>
63
+ <td>{s.type ? TYPE_LABEL[s.type] ?? s.type : '—'}</td>
64
64
  <td><code>{s.endpoint ?? '—'}</code></td>
65
65
  <td>{s.type === 'STATE_VIEW' && s.endpoint ? <ReadModelStatus slice={s} /> : <span className="has-text-grey">—</span>}</td>
66
66
  </tr>
@@ -64,6 +64,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
64
64
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
65
65
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
66
66
 
67
+ ### Recording Info
68
+
69
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
70
+
71
+ ```
72
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
73
+ ```
74
+
75
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
76
+
67
77
  ## Update AGENTS.md Files
68
78
 
69
79
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files:
@@ -112,6 +112,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
112
112
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
113
113
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
114
114
 
115
+ ### Recording Info
116
+
117
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
118
+
119
+ ```
120
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
121
+ ```
122
+
123
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
124
+
115
125
  ## Important
116
126
 
117
127
  - Process **one task per iteration**.
@@ -54,6 +54,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
54
54
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
55
55
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
56
56
 
57
+ ### Recording Info
58
+
59
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
60
+
61
+ ```
62
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
63
+ ```
64
+
65
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
66
+
57
67
  ## Update AGENTS.md Files
58
68
 
59
69
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files — API patterns/conventions, gotchas, dependencies between files, testing approaches, configuration/environment requirements.
@@ -112,6 +112,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
112
112
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
113
113
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
114
114
 
115
+ ### Recording Info
116
+
117
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
118
+
119
+ ```
120
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
121
+ ```
122
+
123
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
124
+
115
125
  ## Important
116
126
 
117
127
  - Process **one task per iteration**.
@@ -58,6 +58,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
58
58
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
59
59
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
60
60
 
61
+ ### Recording Info
62
+
63
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
64
+
65
+ ```
66
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
67
+ ```
68
+
69
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
70
+
61
71
  ## Update AGENTS.md Files
62
72
 
63
73
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files — API patterns/conventions, gotchas, dependencies between files, testing approaches, configuration/environment requirements.
@@ -116,6 +116,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
116
116
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
117
117
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
118
118
 
119
+ ### Recording Info
120
+
121
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
122
+
123
+ ```
124
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
125
+ ```
126
+
127
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
128
+
119
129
  ## Important
120
130
 
121
131
  - Process **one task per iteration**.
@@ -64,6 +64,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
64
64
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
65
65
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
66
66
 
67
+ ### Recording Info
68
+
69
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
70
+
71
+ ```
72
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
73
+ ```
74
+
75
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
76
+
67
77
  ## Update AGENTS.md Files
68
78
 
69
79
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files:
@@ -112,6 +112,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
112
112
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
113
113
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
114
114
 
115
+ ### Recording Info
116
+
117
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
118
+
119
+ ```
120
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
121
+ ```
122
+
123
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
124
+
115
125
  ## Important
116
126
 
117
127
  - Process **one task per iteration**.
@@ -35,6 +35,8 @@ From `$ARGUMENTS`: the context (a MODEL_CONTEXT name or a timeline name) and opt
35
35
 
36
36
  The unit is the **effective context**: a timeline assigned to or connected with a MODEL_CONTEXT belongs to that context; a timeline with neither is its own context. Either name returns the whole effective context — every timeline in it, each slice carrying its `chapter`.
37
37
 
38
+ **Pass the id, not the name, whenever you have it** — the turn's `timelineId` (or a MODEL_CONTEXT's id) goes straight into `--context "<id>"`. A chapter may have no name at all; never rename a chapter to make the lookup work, and never ask about it.
39
+
38
40
  No context given, or a name that matches nothing: list the options once —
39
41
 
40
42
  ```
@@ -97,10 +97,12 @@ async function status() {
97
97
  return res.json();
98
98
  };
99
99
 
100
- const ctx = encodeURIComponent(args.context);
100
+ // --context is a name, or the id of a MODEL_CONTEXT / timeline (a chapter without a name has only that).
101
+ const contextIsId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.context);
102
+ const ctx = `${contextIsId ? 'contextId' : 'contextName'}=${encodeURIComponent(args.context)}`;
101
103
  const [fields, specs] = await Promise.all([
102
- get(`/slicedata?contextName=${ctx}&projection=fields&format=json`),
103
- get(`/slicedata?contextName=${ctx}&projection=specs&format=json`),
104
+ get(`/slicedata?${ctx}&projection=fields&format=json`),
105
+ get(`/slicedata?${ctx}&projection=specs&format=json`),
104
106
  ]);
105
107
  const specsById = Object.fromEntries((specs.slices || []).map((s) => [s.id, s]));
106
108
 
@@ -127,7 +129,7 @@ async function status() {
127
129
  // The baseline whose blocks can be kept: the page the platform stores for this context — what everyone
128
130
  // plays, and there in any session or folder (its sanitizer leaves the blocks byte for byte) — else a
129
131
  // local one, from an earlier run whose upload failed. Neither: every slice is new.
130
- const contextId = args['context-id'] || (await contextIdOf(get, args.context));
132
+ const contextId = contextIsId ? args.context : await contextIdOf(get, args.context);
131
133
  const storedRes = await fetch(`${api}/prototype/${encodeURIComponent(contextId)}`, { headers });
132
134
  if (!storedRes.ok && storedRes.status !== 404) fail(`GET /prototype/${contextId} → ${storedRes.status} ${await storedRes.text()}`);
133
135
  const stored = storedRes.ok ? await storedRes.text() : null;
@@ -158,7 +160,7 @@ async function status() {
158
160
  const removed = existing.filter((b) => !current.has(b.id)).map(({ id, title }) => ({ id, title }));
159
161
  const cssFramework = Object.entries(frameworkVotes).sort((a, b) => b[1] - a[1])[0]?.[0] || 'bulma';
160
162
 
161
- const report = { context: args.context, contextId, baseline, cssFramework, mixedFrameworks: Object.keys(frameworkVotes).length > 1, slices, removed };
163
+ const report = { context: fields.contextName || args.context, contextId, baseline, cssFramework, mixedFrameworks: Object.keys(frameworkVotes).length > 1, slices, removed };
162
164
  writeFileSync(join(WORK, 'status.json'), JSON.stringify(report, null, 2));
163
165
  for (const s of slices) console.log(`${s.state.padEnd(9)} ${s.hash} ${s.id} ${s.title}`);
164
166
  for (const r of removed) console.log(`removed ${''.padEnd(16)} ${r.id} ${r.title}`);
@@ -199,7 +199,7 @@ Steps:
199
199
  ever get here in a `standalone=on` session, which is ad-hoc: its kit dir is shared across
200
200
  every board and nobody reads it afterwards. The board is the only place anything is kept, so
201
201
  anything reusable — including what a subagent reported back — goes there, as a comment on the
202
- node it concerns.
202
+ node it concerns. The exception: end the reply with `Info [limitation|decision|note]: <one terse line>` for anything you could not change (and why) or decided — the CLI records it as an event, standalone included.
203
203
  8. Reply `<promise>DONE</promise>`, naming what you dispatched and what each agent did, or —
204
204
  when step 2 turned up nothing worth doing — change nothing at all and reply
205
205
  `<promise>NOOP</promise>`. A NOOP is a perfectly good outcome, and the CLI widens the gap
@@ -19,7 +19,7 @@ session: it belongs to no project, its kit dir is a `~/.eventmodelers/kit` share
19
19
  and nobody goes looking in there afterwards. So in a `standalone=on` session the **board is the
20
20
  only place anything is kept** — comments, elements, slice statuses, scenarios. Write no file at
21
21
  all: no `Learning:` lines in your reply (step 8), no `.agent-modeling-kit/AGENTS.md`, and nothing a skill's
22
- own instructions suggest writing down either. Anything worth keeping goes on the board, as a
22
+ own instructions suggest writing down either. The one exception is `Info [kind]:` lines (step 8): the CLI records those as events in every session, standalone included. Anything worth keeping goes on the board, as a
23
23
  comment on the node it concerns. This overrides every "write it down" instruction elsewhere in this
24
24
  file and in any skill.
25
25
 
@@ -151,6 +151,7 @@ mention it in the `DONE` comment, and leave it for a self-directed turn (or for
151
151
  6. **Mark the prompt as finished** — invoke `/update-prompt-status` with this turn's `prompt_id`, `newStatus=DONE`, and a `comment` that summarizes what you actually did (e.g. "Added the OrderPlaced event and wired it to the read model"). Do this once, right after the work is done — not per skill call within the turn.
152
152
  7. If this turn has a `comment_id` field, invoke `/handle-comment` with `action=resolve`, `nodeId` from the resolved `NODE_ID` (step 3), `commentId` from `comment_id`.
153
153
  8. **`standalone=off` only** — if you noticed something reusable this turn (a pattern, a gotcha, useful context about this model), end your reply with one line per learning: `Learning [<topic>]: <one terse line>` (the topic is optional, at most five lines). The CLI records them — never write them into a file yourself, and add nothing that is already in `.agent-modeling-kit/AGENTS.md`. In a `standalone=on` session skip this: that session writes no files (see Mode), so note anything worth keeping as a board comment instead.
154
+ **In every session, `standalone=on` too:** anything that is *not* a reusable learning — something you could not change and why, a decision you took — goes the same way as `Info [limitation|decision|note]: <one terse line>` (at most five lines). The CLI records each as an event; never leave it only in prose.
154
155
  9. Reply `<promise>DONE</promise>` and wait for the next turn.
155
156
 
156
157
 
@@ -58,6 +58,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
58
58
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
59
59
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
60
60
 
61
+ ### Recording Info
62
+
63
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
64
+
65
+ ```
66
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
67
+ ```
68
+
69
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
70
+
61
71
  ## Update AGENTS.md Files
62
72
 
63
73
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files:
@@ -94,6 +94,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
94
94
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
95
95
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
96
96
 
97
+ ### Recording Info
98
+
99
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
100
+
101
+ ```
102
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
103
+ ```
104
+
105
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
106
+
97
107
  ## Important
98
108
 
99
109
  - Process **one task per iteration**.
@@ -64,6 +64,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
64
64
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
65
65
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
66
66
 
67
+ ### Recording Info
68
+
69
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
70
+
71
+ ```
72
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
73
+ ```
74
+
75
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
76
+
67
77
  ## Update AGENTS.md Files
68
78
 
69
79
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files:
@@ -112,6 +112,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
112
112
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
113
113
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
114
114
 
115
+ ### Recording Info
116
+
117
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
118
+
119
+ ```
120
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
121
+ ```
122
+
123
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
124
+
115
125
  ## Important
116
126
 
117
127
  - Process **one task per iteration**.
@@ -54,6 +54,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
54
54
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
55
55
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
56
56
 
57
+ ### Recording Info
58
+
59
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
60
+
61
+ ```
62
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
63
+ ```
64
+
65
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
66
+
57
67
  ## Update AGENTS.md Files
58
68
 
59
69
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files — API patterns/conventions, gotchas, dependencies between files, testing approaches, configuration/environment requirements.
@@ -125,6 +125,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
125
125
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
126
126
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
127
127
 
128
+ ### Recording Info
129
+
130
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
131
+
132
+ ```
133
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
134
+ ```
135
+
136
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
137
+
128
138
  ## Important
129
139
 
130
140
  - Process **one task per iteration**.
@@ -58,6 +58,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
58
58
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
59
59
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
60
60
 
61
+ ### Recording Info
62
+
63
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
64
+
65
+ ```
66
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
67
+ ```
68
+
69
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
70
+
61
71
  ## Update AGENTS.md Files
62
72
 
63
73
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files:
@@ -95,6 +95,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
95
95
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
96
96
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
97
97
 
98
+ ### Recording Info
99
+
100
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
101
+
102
+ ```
103
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
104
+ ```
105
+
106
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
107
+
98
108
  ## Important
99
109
 
100
110
  - Process **one task per iteration**.
@@ -54,6 +54,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
54
54
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
55
55
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
56
56
 
57
+ ### Recording Info
58
+
59
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
60
+
61
+ ```
62
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
63
+ ```
64
+
65
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
66
+
57
67
  ## Update AGENTS.md Files
58
68
 
59
69
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files — API patterns/conventions, gotchas, dependencies between files, testing approaches, configuration/environment requirements.
@@ -135,6 +135,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
135
135
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
136
136
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
137
137
 
138
+ ### Recording Info
139
+
140
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
141
+
142
+ ```
143
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
144
+ ```
145
+
146
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
147
+
138
148
  ## Important
139
149
 
140
150
  - Process **one task per iteration**.
@@ -64,6 +64,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
64
64
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
65
65
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
66
66
 
67
+ ### Recording Info
68
+
69
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
70
+
71
+ ```
72
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
73
+ ```
74
+
75
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
76
+
67
77
  ## Update AGENTS.md Files
68
78
 
69
79
  Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files:
@@ -112,6 +112,16 @@ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
112
112
  - `--scope system` for what is true of the modeled domain (a business rule, a naming convention of the model); `repo` (the default) for the codebase and its tooling. `--tag <tag>` labels it (repeatable).
113
113
  - One in `.build-kit/AGENTS.md` that turned out wrong or obsolete: `npx @eventmodelers/cli learnings --ids` lists each with its id, `npx @eventmodelers/cli learn --retract <id>` removes it.
114
114
 
115
+ ### Recording Info
116
+
117
+ Something that is **not** a reusable learning — you cannot change something and why, a decision you took, a note for whoever reads the board — is an info, not a learning. Never leave it only in your reply:
118
+
119
+ ```
120
+ npx @eventmodelers/cli info "<one terse line>" --kind limitation|decision|note
121
+ ```
122
+
123
+ It is recorded as an event and posted as a comment on the slice you are building (`--slice <id>` to name another). Reusable knowledge stays a `learn`.
124
+
115
125
  ## Important
116
126
 
117
127
  - Process **one task per iteration**.