@eventmodelers/cli 1.0.163 → 2.0.1

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 (98) hide show
  1. package/README.md +22 -27
  2. package/RELEASE_NOTES.md +20 -2
  3. package/cli.js +461 -225
  4. package/lib/adapters/spec-kitty-adapter.js +3 -7
  5. package/lib/global-agent-id.js +45 -0
  6. package/lib/modeling-local-ai.js +4 -2
  7. package/package.json +1 -1
  8. package/shared/build-kit/README.md +4 -19
  9. package/shared/build-kit/lib/agent-lock.js +75 -0
  10. package/shared/build-kit/lib/agent-name.js +27 -0
  11. package/shared/build-kit/lib/event-prune.js +58 -0
  12. package/shared/build-kit/lib/events-view.js +121 -0
  13. package/shared/build-kit/lib/local-ai-agent.js +14 -26
  14. package/shared/build-kit/lib/mcp-register.js +17 -1
  15. package/shared/build-kit/lib/prompt-journal.js +191 -0
  16. package/shared/build-kit/lib/prompt-replay.js +35 -0
  17. package/shared/build-kit/lib/ralph.js +201 -156
  18. package/shared/build-kit/lib/slice-curation.js +40 -0
  19. package/shared/build-kit/lib/slice-events.js +93 -0
  20. package/shared/build-kit/lib/slice-git.js +86 -0
  21. package/shared/build-kit/lib/slice-learnings.js +197 -0
  22. package/shared/build-kit/lib/slice-projection.js +101 -0
  23. package/shared/build-kit/lib/slice-run.js +181 -0
  24. package/shared/build-kit/lib/slice-state.js +182 -0
  25. package/shared/build-kit/lib/slice-tasks.js +35 -0
  26. package/shared/build-kit/ralph-claude.js +4 -2
  27. package/shared/build-kit/ralph-exec.js +4 -2
  28. package/shared/build-kit/ralph-local-ai.js +5 -3
  29. package/shared/skills/learn-eventmodelers-api/SKILL.md +13 -0
  30. package/shared/skills/request-feedback/SKILL.md +1 -2
  31. package/shared/skills/ui-contract/SKILL.md +5 -3
  32. package/stacks/axon/templates/build-kit/CLAUDE.md +3 -3
  33. package/stacks/axon/templates/build-kit/lib/AGENT.md +6 -7
  34. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +20 -42
  35. package/stacks/axon/templates/build-kit/lib/prompt.md +18 -41
  36. package/stacks/blank/templates/build-kit/CLAUDE.md +3 -3
  37. package/stacks/blank/templates/build-kit/lib/AGENT.md +6 -7
  38. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +20 -37
  39. package/stacks/blank/templates/build-kit/lib/prompt.md +19 -42
  40. package/stacks/classic/templates/.claude/skills/build-automation/SKILL.md +1 -1
  41. package/stacks/classic/templates/.claude/skills/build-state-change/SKILL.md +1 -1
  42. package/stacks/classic/templates/.claude/skills/build-state-view/SKILL.md +2 -2
  43. package/stacks/classic/templates/.claude/skills/record-evidence/SKILL.md +2 -2
  44. package/stacks/classic/templates/build-kit/CLAUDE.md +3 -3
  45. package/stacks/classic/templates/build-kit/lib/AGENT.md +6 -7
  46. package/stacks/classic/templates/build-kit/lib/backend-prompt.md +20 -38
  47. package/stacks/classic/templates/build-kit/lib/prompt.md +19 -42
  48. package/stacks/kurrent/templates/build-kit/CLAUDE.md +3 -4
  49. package/stacks/kurrent/templates/build-kit/lib/AGENT.md +6 -7
  50. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +20 -42
  51. package/stacks/kurrent/templates/build-kit/lib/prompt.md +18 -41
  52. package/stacks/modeling-kit/templates/.claude/skills/detect-model-drift/SKILL.md +1 -1
  53. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +2 -0
  54. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +7 -3
  55. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
  56. package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +3 -4
  57. package/stacks/modeling-kit/templates/kit/CLAUDE.md +9 -24
  58. package/stacks/modeling-kit/templates/kit/README.md +1 -1
  59. package/stacks/modeling-kit/templates/kit/lib/config.js +1 -1
  60. package/stacks/node/templates/build-kit/CLAUDE.md +3 -3
  61. package/stacks/node/templates/build-kit/lib/AGENT.md +6 -7
  62. package/stacks/node/templates/build-kit/lib/backend-prompt.md +20 -42
  63. package/stacks/node/templates/build-kit/lib/prompt.md +18 -41
  64. package/stacks/opencqrs/templates/build-kit/CLAUDE.md +3 -4
  65. package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +6 -7
  66. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +20 -42
  67. package/stacks/opencqrs/templates/build-kit/lib/prompt.md +18 -41
  68. package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +1 -1
  69. package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +1 -1
  70. package/stacks/react/templates/build-kit/CLAUDE.md +3 -3
  71. package/stacks/react/templates/build-kit/lib/AGENT.md +6 -7
  72. package/stacks/react/templates/build-kit/lib/backend-prompt.md +20 -37
  73. package/stacks/react/templates/build-kit/lib/prompt.md +19 -42
  74. package/stacks/supabase/templates/build-kit/CLAUDE.md +3 -3
  75. package/stacks/supabase/templates/build-kit/lib/AGENT.md +6 -7
  76. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +20 -42
  77. package/stacks/supabase/templates/build-kit/lib/prompt.md +18 -41
  78. package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +2 -2
  79. package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +2 -2
  80. package/stacks/supabase-react/templates/build-kit/CLAUDE.md +3 -3
  81. package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +6 -7
  82. package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +20 -37
  83. package/stacks/supabase-react/templates/build-kit/lib/prompt.md +19 -42
  84. package/stacks/umadb/templates/build-kit/CLAUDE.md +3 -3
  85. package/stacks/umadb/templates/build-kit/lib/AGENT.md +6 -7
  86. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +20 -42
  87. package/stacks/umadb/templates/build-kit/lib/prompt.md +18 -41
  88. package/shared/bridge/spec-kitty/bridge-spec-kitty-specify/SKILL.md +0 -125
  89. package/shared/bridge/spec-kitty/bridge-spec-kitty-tasks/SKILL.md +0 -95
  90. package/shared/build-kit/lib/agent.sh +0 -20
  91. package/shared/build-kit/ralph.sh +0 -111
  92. package/shared/build-kit/realtime-agent.js +0 -20
  93. package/stacks/bridge/templates/bridge/lib/AGENT.md +0 -47
  94. package/stacks/bridge/templates/bridge/lib/prompt.md +0 -111
  95. package/stacks/bridge/templates/bridge/ralph-claude.js +0 -50
  96. package/stacks/bridge/templates/bridge/ralph-hook.js +0 -103
  97. package/stacks/bridge/templates/bridge/ralph-local-ai.js +0 -43
  98. package/stacks/bridge/templates/bridge/ralph-static.js +0 -90
@@ -0,0 +1,182 @@
1
+ // Local slice state as an event log: `fold` derives it, `decide*` turn it into actions. Both are pure —
2
+ // the run loop appends the events, executes the actions and appends what came of them.
3
+ // Design and the precedence rule: slice-state-test-design.md.
4
+ //
5
+ // Events (one writer per type): board-observed, claimed, claim-confirmed, agent-recorded, settled,
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
8
+ // newer loop's log is readable.
9
+
10
+ const emptySlice = () => ({
11
+ status: undefined,
12
+ claim: null,
13
+ pendingOutcome: null,
14
+ lastBoardStatus: undefined,
15
+ attempts: 0,
16
+ retryStatus: undefined,
17
+ lastFailure: null,
18
+ blocked: null,
19
+ });
20
+
21
+ const same = (a, b) => typeof a === 'string' && typeof b === 'string' && a.toLowerCase() === b.toLowerCase();
22
+
23
+ const apply = {
24
+ // While a claim is open the board's view is recorded but does not touch `status`: a refresh that
25
+ // still shows InProgress must not revert the outcome the agent recorded. Attempts reset only between
26
+ // builds — our own claim moving the board to InProgress is not the slice leaving Planned.
27
+ 'board-observed'(s, e) {
28
+ s.lastBoardStatus = e.status;
29
+ if (s.claim) return;
30
+ s.status = e.status;
31
+ // Attempts count builds that failed to move the slice off the status it was put back to (Planned, or
32
+ // Handover for UI kits); any other status means it moved. Any status but Blocked lifts a block.
33
+ if (!same(e.status, s.retryStatus ?? 'Planned')) { s.attempts = 0; s.lastFailure = null; }
34
+ if (!same(e.status, 'Blocked')) s.blocked = null;
35
+ },
36
+ // First claim wins: while one is open a second `claimed` for the slice is ignored.
37
+ claimed(s, e) {
38
+ if (s.claim) return;
39
+ s.claim = { pid: e.pid, confirmed: false, base: e.base ?? null, cleanTree: e.cleanTree, ctx: e.ctx, title: e.title, prevStatus: s.status };
40
+ s.pendingOutcome = null;
41
+ s.status = 'InProgress';
42
+ },
43
+ 'claim-confirmed'(s) {
44
+ if (s.claim) s.claim.confirmed = true;
45
+ },
46
+ 'agent-recorded'(s, e) {
47
+ s.status = e.status;
48
+ if (s.claim) s.pendingOutcome = e.status;
49
+ },
50
+ settled(s, e) {
51
+ s.claim = null;
52
+ s.pendingOutcome = null;
53
+ s.status = e.status;
54
+ s.lastBoardStatus = e.status;
55
+ },
56
+ // The claim ended without anything pushed: the slice shows what the board last said (someone else may
57
+ // have moved it meanwhile), or what it was before the claim if the board was never observed.
58
+ released(s) {
59
+ if (!s.claim) return;
60
+ s.status = s.lastBoardStatus ?? s.claim.prevStatus;
61
+ s.claim = null;
62
+ s.pendingOutcome = null;
63
+ },
64
+ 'attempt-failed'(s, e) {
65
+ s.attempts += 1;
66
+ s.retryStatus = e.status ?? 'Planned';
67
+ // What the next build is told about the one that failed.
68
+ s.lastFailure = { at: e.at, reason: e.reason, lastReply: e.lastReply ?? null, branch: e.branch ?? null, created: e.created ?? [], changed: e.changed ?? '' };
69
+ },
70
+ // A block starts the count afresh: an unblocked slice gets a full budget again.
71
+ blocked(s, e) {
72
+ s.blocked = { reason: e.reason, at: e.at };
73
+ s.status = 'Blocked';
74
+ s.attempts = 0;
75
+ s.lastFailure = null;
76
+ },
77
+ };
78
+
79
+ // The curator's merge: the known sources are replaced by one learning, in the slot of the first of them.
80
+ function mergeLearnings(learnings, e) {
81
+ const sources = learnings.filter((l) => e.learnings?.includes(l.id));
82
+ if (!sources.length) return learnings;
83
+ const tags = [...new Set([...sources.flatMap((l) => l.tags), ...(e.tags ?? [])])];
84
+ const merged = { id: e.id, text: e.text, topic: e.topic ?? sources[0].topic, slice: undefined, scope: e.scope ?? sources[0].scope, tags, curated: true };
85
+ return learnings.flatMap((l) => (l === sources[0] ? [merged] : sources.includes(l) ? [] : [l]));
86
+ }
87
+
88
+ export function fold(events) {
89
+ const slices = {};
90
+ let learnings = [];
91
+ let tasks = [];
92
+ const prompts = new Map(); // board prompts the modeling agent took and has not finished, in the order received
93
+ for (const e of events) {
94
+ if (!e) continue;
95
+ if (e.type === 'prompt-received' && e.prompt?.id) {
96
+ const { id, text, board_id, priority, agent_id } = e.prompt;
97
+ prompts.set(id, { id, pid: e.pid, at: e.at, text, board_id, priority, agent_id: agent_id ?? null });
98
+ } else if (e.type === 'prompt-finished') {
99
+ prompts.delete(e.prompt);
100
+ }
101
+ // The task queue: slice changes the agent has to react to, oldest first. A newer change to the same
102
+ // slice replaces its queued task (and goes to the back); `task-done` names a task by id, so a change that
103
+ // arrived while the agent was working the old one is a new task and survives.
104
+ if (e.type === 'task-queued') {
105
+ const sliceId = e.payload?.sliceId;
106
+ tasks = tasks.filter((t) => sliceId === undefined || t.payload?.sliceId !== sliceId);
107
+ tasks.push({ id: e.id, at: e.at, payload: e.payload });
108
+ } else if (e.type === 'task-done') {
109
+ tasks = tasks.filter((t) => t.id !== e.task);
110
+ }
111
+ // Learnings are not about one slice (the slice is just where it was learned), so they sit beside them.
112
+ if (e.type === 'learning-recorded') learnings.push({ id: e.id, text: e.text, topic: e.topic, slice: e.slice, scope: e.scope ?? 'repo', tags: e.tags ?? [], curated: false });
113
+ else if (e.type === 'learning-retracted') learnings = learnings.filter((l) => l.id !== e.learning);
114
+ else if (e.type === 'learning-merged') learnings = mergeLearnings(learnings, e);
115
+ else if (e.type === 'learnings-curated') learnings = learnings.map((l) => ({ ...l, curated: true }));
116
+ if (!e.slice || !Object.hasOwn(apply, e.type)) continue;
117
+ apply[e.type](slices[e.slice] ??= emptySlice(), e);
118
+ }
119
+ return { slices, learnings, tasks, prompts: [...prompts.values()] };
120
+ }
121
+
122
+ // The curator (an LLM, writing events) is worth a turn once this many learnings have been recorded since
123
+ // it last reviewed them.
124
+ export function needsCuration(state, threshold) {
125
+ return state.learnings.filter((l) => !l.curated).length >= threshold && threshold > 0;
126
+ }
127
+
128
+ // Two learnings that differ only in case or whitespace are the same learning.
129
+ export function learningKey(text) {
130
+ return String(text).replace(/\s+/g, ' ').trim().toLowerCase();
131
+ }
132
+
133
+ // AGENTS.md: the learnings, grouped by topic (in order of first appearance), one line each, duplicates
134
+ // dropped. Nothing is written into the file by hand — it is regenerated from the log.
135
+ export function renderLearnings(state) {
136
+ const topics = new Map();
137
+ const seen = new Set();
138
+ for (const { text, topic } of state.learnings) {
139
+ const key = learningKey(text);
140
+ if (!key || seen.has(key)) continue;
141
+ seen.add(key);
142
+ const name = topic?.trim() || 'General';
143
+ if (!topics.has(name)) topics.set(name, []);
144
+ topics.get(name).push(String(text).trim());
145
+ }
146
+ if (!topics.size) return '';
147
+ const body = [...topics].map(([name, lines]) => `## ${name}\n${lines.map((l) => `- ${l}`).join('\n')}\n`).join('\n');
148
+ return `<!-- Generated from the slice-state event log. Record learnings with the learn command; do not edit. -->\n\n${body}`;
149
+ }
150
+
151
+ // What to do with a claimed slice once its build is over. `boardStatus` is the board's current status
152
+ // for it (null when unknown or there is no board); `pickupStatus` is what an unfinished slice goes back
153
+ // to (Planned, or Handover for UI kits). Actions: discard {base, skip}, push {status}, release {reason} —
154
+ // the loop appends `settled` after a successful push and `released` for a release. A failed push appends
155
+ // nothing, so the claim stays open and the next iteration settles it again.
156
+ export function decideSettle(state, sliceId, { boardStatus = null, pickupStatus = 'Planned' } = {}) {
157
+ const s = state.slices[sliceId];
158
+ const claim = s?.claim;
159
+ if (!claim) return [];
160
+ // Never confirmed on the board: nothing of ours to push there — the slice may belong to the winner of the race.
161
+ if (!claim.confirmed) return [{ type: 'release', reason: 'unconfirmed' }];
162
+ // Someone else moved it (a person, request-feedback's Blocked) — theirs to keep.
163
+ if (boardStatus && !same(boardStatus, 'InProgress')) return [{ type: 'release', reason: 'moved' }];
164
+ const outcome = s.pendingOutcome;
165
+ const unfinished = !outcome || same(outcome, 'InProgress') || same(outcome, pickupStatus);
166
+ if (!unfinished) return [{ type: 'push', status: outcome }];
167
+ // `skip`: the tree already held someone's edits at claim time, so none of it is the build's debris.
168
+ return [{ type: 'discard', base: claim.base, skip: !claim.cleanTree }, { type: 'push', status: pickupStatus }];
169
+ }
170
+
171
+ // Claims still open at the top of a loop iteration are leftovers: the process died mid-build, or settling
172
+ // failed last time. Each is settled the way a finished build is. A claim held by another live process
173
+ // belongs to it — left alone.
174
+ export function decideRecovery(state, { isAlive, selfPid, boardStatuses = {}, pickupStatus = 'Planned' }) {
175
+ const result = [];
176
+ for (const [slice, s] of Object.entries(state.slices)) {
177
+ if (!s.claim) continue;
178
+ if (s.claim.pid !== selfPid && isAlive(s.claim.pid)) continue;
179
+ result.push({ slice, actions: decideSettle(state, slice, { boardStatus: boardStatuses[slice] ?? null, pickupStatus }) });
180
+ }
181
+ return result;
182
+ }
@@ -0,0 +1,35 @@
1
+ // The task queue is the event log: the realtime agent queues a slice change as `task-queued`, the loop replays
2
+ // the log to find the oldest task still waiting, hands it to the agent, and appends `task-done` when the turn is
3
+ // over. Nobody reads or rewrites a queue file, so there is nothing for two writers to collide on — a change that
4
+ // lands mid-turn is just another event, and `task-done` names the task by id.
5
+
6
+ import { appendEvent, readEvents } from './slice-events.js';
7
+ import { fold } from './slice-state.js';
8
+
9
+ // `payload` is the slice change ({ sliceId, sliceTitle, sliceStatus, ... }). Returns { id } of the task.
10
+ export function queueTask({ kitDir, payload, meta }) {
11
+ return { id: appendEvent(kitDir, { type: 'task-queued', payload }, meta) };
12
+ }
13
+
14
+ export function pendingTasks(kitDir) {
15
+ return fold(readEvents(kitDir)).tasks;
16
+ }
17
+
18
+ // The oldest task waiting, or null. Handing it out does not consume it — `completeTask` does.
19
+ export function nextTask(kitDir) {
20
+ return pendingTasks(kitDir)[0] ?? null;
21
+ }
22
+
23
+ export function completeTask({ kitDir, task, meta }) {
24
+ appendEvent(kitDir, { type: 'task-done', task }, meta);
25
+ }
26
+
27
+ // One turn for the oldest waiting task. `run(task)` is the agent turn and says whether it finished (false: it
28
+ // timed out — the task stays queued and is handed out again; a turn that throws leaves it queued too). The
29
+ // task is completed only after a finished turn. Returns whether a task was worked.
30
+ export async function workNextTask({ kitDir, meta, run }) {
31
+ const task = nextTask(kitDir);
32
+ if (!task) return false;
33
+ if (await run(task)) completeTask({ kitDir, task: task.id, meta });
34
+ return true;
35
+ }
@@ -78,7 +78,7 @@ function describeToolUse(block) {
78
78
  }
79
79
 
80
80
  // `slice` is set for a planned-slice build (see startRalph) — the only turns that are traced.
81
- function runClaude(prompt, slice = null) {
81
+ function runClaude(prompt, slice = null, extraEnv = {}) {
82
82
  return new Promise((resolve, reject) => {
83
83
  // The prompt goes in on stdin, never as a `-p` argument: on Windows a multi-line argv entry
84
84
  // is cut at its first newline, so `claude` saw only the connect header, answered "I don't see
@@ -86,7 +86,7 @@ function runClaude(prompt, slice = null) {
86
86
  const proc = spawn('claude', [...claudeArgs, '-p'], {
87
87
  cwd: projectDir,
88
88
  stdio: ['pipe', 'pipe', 'inherit'],
89
- env: claudeEnv,
89
+ env: { ...claudeEnv, ...extraEnv },
90
90
  });
91
91
  proc.stdin.end(inlineHeader + prompt);
92
92
  const turn = superviseTurn(proc, { timeoutMs, label: 'Claude turn', log: (line) => console.error(`[ralph] ${line}`) });
@@ -134,6 +134,8 @@ startRalph({
134
134
  projectDir,
135
135
  onTask: runClaude,
136
136
  onPlannedSlice: runClaude,
137
+ // The curation turn: the learnings tidied by an agent that may only merge and retract (see lib/slice-curation.js).
138
+ onCurate: (prompt, { env } = {}) => runClaude(prompt, null, env),
137
139
  localOnly,
138
140
  }).catch((err) => {
139
141
  console.error('[ralph] Fatal:', err);
@@ -102,7 +102,7 @@ function collectUsage(stream) {
102
102
  }
103
103
 
104
104
  // `slice` is set for a planned-slice build (see startRalph) — the only turns that are traced.
105
- function runExec(prompt, slice = null) {
105
+ function runExec(prompt, slice = null, extraEnv = {}) {
106
106
  return new Promise((resolvePromise, reject) => {
107
107
  const full = inlineHeader + prompt;
108
108
  const promptFile = join(promptDir, 'prompt.md');
@@ -118,7 +118,7 @@ function runExec(prompt, slice = null) {
118
118
  cwd: projectDir,
119
119
  stdio: ['inherit', traced ? 'pipe' : 'inherit', 'inherit'],
120
120
  shell: true,
121
- env: { ...childEnv, RALPH_PROMPT_FILE: promptFile },
121
+ env: { ...childEnv, RALPH_PROMPT_FILE: promptFile, ...extraEnv },
122
122
  });
123
123
  const turn = superviseTurn(proc, { timeoutMs, label: 'exec turn', viaShell: true, log: (line) => console.error(`[ralph-exec] ${line}`) });
124
124
  const usage = traced ? collectUsage(proc.stdout) : null;
@@ -141,6 +141,8 @@ startRalph({
141
141
  projectDir,
142
142
  onTask: runExec,
143
143
  onPlannedSlice: runExec,
144
+ // The curation turn: the learnings tidied by an agent that may only merge and retract (see lib/slice-curation.js).
145
+ onCurate: (prompt, { env } = {}) => runExec(prompt, null, env),
144
146
  localOnly,
145
147
  }).catch((err) => {
146
148
  console.error('[ralph] Fatal:', err);
@@ -16,13 +16,15 @@ const kitDir = dirname(fileURLToPath(import.meta.url));
16
16
  const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
17
17
 
18
18
 
19
- function runLocalAi() {
19
+ // The task goes in on stdin, as it does for the Claude runner: the loop has already picked it.
20
+ function runLocalAi(prompt) {
20
21
  return new Promise((resolve, reject) => {
21
22
  const proc = spawn('node', [join(kitDir, 'lib', 'local-ai-agent.js')], {
22
23
  cwd: projectDir,
23
- stdio: 'inherit',
24
+ stdio: ['pipe', 'inherit', 'inherit'],
24
25
  env: process.env,
25
26
  });
27
+ proc.stdin.end(prompt);
26
28
  proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`local-ai-agent exited ${code}`))));
27
29
  proc.on('error', reject);
28
30
  });
@@ -32,7 +34,7 @@ startRalph({
32
34
  kitDir,
33
35
  projectDir,
34
36
  onTask: runLocalAi,
35
- // onPlannedSlice omitted — local-ai-agent manages its own task queue
37
+ // onPlannedSlice omitted — this runner works the queued tasks only
36
38
  localOnly: process.env.RALPH_LOCAL === '1',
37
39
  }).catch((err) => {
38
40
  console.error('[ralph] Fatal:', err);
@@ -1066,11 +1066,22 @@ Send `x-agent-id` here too: a prompt the user addressed to one preferred agent (
1066
1066
 
1067
1067
  Note what this endpoint does *not* do: an agent that sends its id still gets every untargeted prompt on top of its own. A caller that wants only what was addressed to it has to hand the rest back itself (`POST /prompts/:id/status` with `status: 'ADDED'`), which is what `eventmodelers run --exclusive` does.
1068
1068
 
1069
+ The claim records its holder: `claimed_by` is the caller's `x-agent-id` (`null` if it sent none) and `claimed_at` the time. That is what tells a prompt an agent is working from one a crashed agent left behind. Before claiming, this endpoint puts prompts whose known holder has **no live heartbeat** (and that were claimed more than 45 s ago) back to `ADDED`; a prompt with no recorded holder is never handed back, and an agent that is still pinging keeps its prompts however long a turn takes. An agent that restarts under the same id finds what it was holding with `GET /prompts?claimed_by=<its id>&status=CLAIMED,IN_PROGRESS` and hands it back itself.
1070
+
1069
1071
  **Query params**: `board_id` (required)
1070
1072
  **Response**: `200` — the claimed row (now `status: "CLAIMED"`), including its parsed `context` and the `hidden` flag · `404` — no `ADDED` prompts available
1071
1073
 
1072
1074
  ---
1073
1075
 
1076
+ ### GET `/api/org/:orgId/prompts`
1077
+ List a board's prompts **with their status** — the only way to read a prompt's status. Read-only: nothing is claimed. Auth: `x-token` **and** a Supabase JWT (`Authorization: Bearer`) together, like `/prompts/next` — an agent uses the realtime token it already holds. A status write is unconditional (`CLAIMED` → `ADDED` works on a `DONE` prompt too), so an agent that wants to hand back prompts a crash left it holding looks here first and releases only the ones still `CLAIMED` / `IN_PROGRESS`.
1078
+
1079
+ **Query params**: `board_id` (required) · `status` (optional, comma-separated subset of `ADDED`,`CLAIMED`,`IN_PROGRESS`,`DONE`) · `ids` (optional, comma-separated prompt ids, at most 200 — only these prompts of the board come back; an empty value asks about none, and the id of another board's prompt matches nothing) · `claimed_by` (optional, an agent id — only the prompts that agent holds) · `limit` (optional, default 200, max 500)
1080
+ **Response**: `200` — `{ "prompts": [ … ] }`, each row with `id`, `status`, `agent_id` (who it is addressed to), `claimed_by` / `claimed_at` (who holds it and since when), `priority`, `creation_time`, `node_id`, `comment_id`, the progress `comment` and the parsed `context`. Ordered like the claim: priority first, then oldest first.
1081
+ **Errors**: `400` missing `board_id`, unknown status (`PROMPT_STATUS_INVALID`), more than 200 ids or a bad `limit` · `403` the board is not in the token's organization
1082
+
1083
+ ---
1084
+
1074
1085
  ### POST `/api/org/:orgId/prompts/:id/status`
1075
1086
  Set a prompt's status, optionally attaching a progress comment. Auth: `x-token` only (bot token — no user JWT needed, this is meant to be called directly by the agent working the prompt).
1076
1087
 
@@ -1084,6 +1095,8 @@ Set a prompt's status, optionally attaching a progress comment. Auth: `x-token`
1084
1095
  }
1085
1096
  ```
1086
1097
 
1098
+ Setting `ADDED` hands the prompt back to the queue and clears `claimed_by` / `claimed_at`; the other statuses leave the holder as it was.
1099
+
1087
1100
  **Response**: `200` — the updated row
1088
1101
  **Errors**: `400` invalid/missing `status` · `403` token not for this prompt's org · `404` prompt not found · `409` (`PROMPT_STATUS_ALREADY_SET`) the prompt already carries that status — another agent already made this transition, so don't retry it
1089
1102
 
@@ -16,8 +16,7 @@ Prefer `mcp__eventmodelers__*` tools when available (registered by the `connect`
16
16
  **The default is to assume and build.** A missing example value, an unclear field type, a status
17
17
  mapping, a referenced event that isn't modeled yet, a dependency slice that isn't built yet — none of
18
18
  these is a reason to stop. Pick the most sensible interpretation from `slice.json`, its
19
- `specifications[]` and the surrounding model, build the slice, and record each assumption in one line
20
- in `progress.txt` and as a code comment. **Read `slice.json` fully, and read the build skill's own
19
+ `specifications[]` and the surrounding model, build the slice, and record each assumption in one line as a code comment. **Read `slice.json` fully, and read the build skill's own
21
20
  instructions and reference docs, before concluding anything is missing.**
22
21
 
23
22
  Invoke `request-feedback` **only if the slice literally cannot be built**: even with sensible
@@ -107,9 +107,11 @@ runner resolves `run --handover` over the project setting), else `"handover": tr
107
107
  `.eventmodelers/config.json` (saved by `init --handover` / `eventmodelers config --handover on|off`).
108
108
 
109
109
  - **On** — the build kit's "set the slice to `Done`" for a STATE_VIEW / STATE_CHANGE slice means
110
- **`Handover`** instead: after the contract is published and the commit/merge succeeded, via
111
- `update-slice-status` (mirrored in the local `index.json`). The UI kit claims `Handover` slices and sets
112
- `Done` when its UI is built.
110
+ **`Handover`** instead: after the contract is published and the commit/merge succeeded. Where the build
111
+ kit's prompt says to call `update-slice-status` (a queued task), set `Handover` through it,
112
+ mirrored in the local `index.json`. For a run-loop build (the task request names the slice), record it
113
+ with `npx @eventmodelers/cli slice-status Handover` — the run loop pushes it to the board; do not call
114
+ `update-slice-status`. The UI kit claims `Handover` slices and sets `Done` when its UI is built.
113
115
  - **Off / unset** — nothing changes: `Done`.
114
116
  - Automations and translations have no UI → always `Done`.
115
117
 
@@ -33,16 +33,16 @@ Ignore case for files and slices in prompts. "CartItems" slice is the same as "c
33
33
 
34
34
  Do not change files with tests unless explicitely instructed, or the change brings the test in line with slice.json (e.g. step 4's field/spec diff): *.test.ts
35
35
 
36
- At the start of every session, read `.build-kit/AGENTS.md` if it exists to load accumulated project learnings.
36
+ At the start of every session, read `.build-kit/AGENTS.md` if it exists to load accumulated project learnings. It is generated from the learnings recorded so far — record a new one with `npx @eventmodelers/cli learn "<one terse line>" --topic <topic>`, never by editing the file.
37
37
 
38
- When starting to work on a slice, invoke the `update-slice-status` skill with `InProgress` status before doing anything else.
38
+ When working from a queued task, invoke the `update-slice-status` skill with `InProgress` status before doing anything else. When the task request names the slice to build (a run-loop build), the run loop has already set it to `InProgress` and pushes the final status itself — do not call `update-slice-status`; record the outcome with `npx @eventmodelers/cli slice-status <status>`.
39
39
 
40
40
  ## Building a Slice
41
41
 
42
42
  **CRITICAL: You MUST always use the provided skills to build slices. NEVER implement a slice manually.**
43
43
  **ALL fields, event names, command names, and business rules MUST come exclusively from slice.json. Do NOT invent, assume, or guess any field or logic not present in the slice definition.**
44
44
 
45
- **Default: make a reasonable assumption and build the slice.** If a detail is unclear or missing (an example value, a field type, a status mapping, a referenced event that isn't modeled yet, ...), pick the most sensible interpretation from `slice.json`, its specifications and the surrounding model, build it, and record each assumption in one line in `progress.txt` and as a code comment. Ambiguity alone is never a reason to stop. **Only if the slice literally cannot be built** — nothing runnable can be produced even with sensible assumptions — invoke `/request-feedback` with the specific question; it posts the question as a comment on the slice and marks it `Blocked`. That must be the absolute exception; a `Blocked` slice should mean "impossible without a human", never "the agent preferred to ask".
45
+ **Default: make a reasonable assumption and build the slice.** If a detail is unclear or missing (an example value, a field type, a status mapping, a referenced event that isn't modeled yet, ...), pick the most sensible interpretation from `slice.json`, its specifications and the surrounding model, build it, and record each assumption in one line as a code comment. Ambiguity alone is never a reason to stop. **Only if the slice literally cannot be built** — nothing runnable can be produced even with sensible assumptions — invoke `/request-feedback` with the specific question; it posts the question as a comment on the slice and marks it `Blocked`. That must be the absolute exception; a `Blocked` slice should mean "impossible without a human", never "the agent preferred to ask".
46
46
 
47
47
  When asked to build a slice, always follow this flow:
48
48
 
@@ -1,12 +1,11 @@
1
- # Agent Learnings
1
+ # Agent Reference
2
2
 
3
- Patterns and gotchas discovered during task processing. Update this file whenever you encounter something reusable.
3
+ What task processing relies on: the shape of a task and the API facts the skills use. A reusable learning is recorded with `npx @eventmodelers/cli learn "<one terse line>"`, not added here.
4
4
 
5
- ## tasks.json
5
+ ## Tasks
6
6
 
7
- - Tasks are objects with `id`, `createdAt`, and `payload` (a `SliceChangedPayload`).
8
- - After completing a task, remove it from the array entirely — do not add a status field.
9
- - Write `[]` to `tasks.json` if the last task is completed.
7
+ - A task is a slice change the run loop queued: an `id` and a `payload` (a `SliceChangedPayload`). The task request names it.
8
+ - You never read or write a queue file: the run loop completes the task when your turn ends.
10
9
 
11
10
  ## SliceChangedPayload fields
12
11
 
@@ -44,4 +43,4 @@ These files are always up to date — read them directly before invoking any ski
44
43
  - The `boardId` and `organizationId` from each payload provide full context — pass them to skills.
45
44
  - Node events use `node:created`, `node:changed`, `node:deleted` — always POST to `/api/org/:orgId/boards/:boardId/nodes/events`.
46
45
  - Slice metadata (title, status) lives on the SLICE_BORDER node under `meta.sliceStatus` and `meta.title`.
47
- - `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `Planned` one instead of erroring out.
46
+ - `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `Planned` one instead of erroring out. This applies to queued tasks only: when the task request names the slice to build, the run loop owns its status — never call `/update-slice-status` there.
@@ -17,20 +17,19 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
20
+ 2. Read `.build-kit/AGENTS.md` **if it exists** — the project's accumulated learnings, generated from what earlier builds recorded. If the slice's `slice.json` has a `lastAttempt`, an earlier attempt at it failed: read its `reason`, `lastReply`, `branch` and `changed`, and do not repeat what failed.
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
- 5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
- **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
22
+ 5. Your slice is the one the task request names (the run loop picked the highest priority slice with status "Planned"); it becomes your PRD. The run loop has already claimed it — its status is "InProgress" in index.json and on the board. Build that slice and do not call `update-slice-status`: the run loop owns the slice's status on the board.
23
+ **IMPORTANT: Only work on the slice named in the task request. Never switch to another slice, whatever its status. If the task request names no slice and no slice has status "Planned" in the current context, reply with:**
24
24
  <promise>NO_TASKS</promise> and stop immediately. Do not work on other slices and do not switch to another context.
25
- **Claim conflict**: the board rejects the status update if the slice is already in the target status — this is expected: another agent claimed it first, racing you for the same slice. This is NOT an error. Do not stop, do not retry the same slice. Re-read `index.json` (or re-fetch via `load-slice`), pick the next-highest-priority slice still "Planned", and try claiming that one instead. Repeat until a claim succeeds or no "Planned" slice remains, in which case reply `<promise>NO_TASKS</promise>`.
26
25
  6. Pick the slice definition from `.build-kit/.slices/<contextName>/<folder>/slice.json` as defined in the prd. Never work on more than one slice per iteration.
27
- 7. A slice can define additional prompts as codegen/backendPrompt. any additional prompts defined in backend are hints for the implementation of the slice and have to be taken into account. If you use the additional prompt, add a line in progress.txt
26
+ 7. A slice can define additional prompts as codegen/backendPrompt. any additional prompts defined in backend are hints for the implementation of the slice and have to be taken into account.
28
27
  7. Define the slice type and load the matching skill:
29
28
  - Write slice (has commands, no processors) → `build-state-change`
30
29
  - Read slice (has readModel / information flow) → `build-state-view`
31
30
  - Translation slice (`sliceType === "TRANSLATION"`) → read `description` and `notes` from slice.json for hints; default to `build-automation` if nothing else is specified
32
31
  - Automation slice (processors-array is not empty) → `build-automation`
33
- 8. Write a short progress one liner after each step to progress.txt
32
+ 8. Record each learning the moment you have it (see **Recording Learnings**), not at the end.
34
33
  9. Analyze and Implement that single slice, make use of the skills in the skills directory, but also your previsously collected
35
34
  knowledge. Make a list TODO list for what needs to be done. Also make sure to adjust the implementation according to the json definition. Carefully inspect events, fields and compare against the implemented slice. JSON is the desired state. ATTENTION: A "planned" task can also be just added specifications. So always look at the slice itself, but also the specifications. If specifications were added in json, which are not on code, you need to add them in code.
36
35
  10. The slice in the json is always true, the code follows what is defined in the json
@@ -43,48 +42,27 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
43
42
  If the tests for the slice are not yet named predictably, run `./mvnw test -q` and check for failures.
44
43
  15. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge ( update
45
44
  first )
46
- 16. Update the PRD to set `status: Done` for the completed story in index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available). For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to set is `Handover`, not `Done` — see the `ui-contract` skill, Step 4.
47
- 17. Append your progress to `progress.txt` after each step in the iteration.
48
- 18. append your new learnings to `.build-kit/AGENTS.md` in a compressed form, reusable for future iterations. Only add learnings if they are not already there.
45
+ 16. Record the outcome with `npx @eventmodelers/cli slice-status Done` — not by editing index.json, and do not call `update-slice-status`: the run loop pushes the status to the board when your turn ends. For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to record is `Handover`, not `Done` — see the `ui-contract` skill, Step 4. A slice you could not finish needs no status: leave it as it is and the run loop discards the attempt.
46
+ 17. Record any reusable learning you have not recorded yet (see **Recording Learnings**).
47
+ 18. If an entry in `.build-kit/AGENTS.md` turned out wrong or obsolete, retract it (see **Recording Learnings**).
49
48
  19. Finish the iteration.
50
49
 
51
50
  ## Escalating Ambiguity
52
51
 
53
- **Default: make a reasonable assumption and build the slice.** If a detail is unclear or missing (an example value, a field type, a status mapping, a referenced event that isn't modeled yet, ...), pick the most sensible interpretation from `slice.json`, its specifications and the surrounding model, build it, and record each assumption in one line in `progress.txt` and as a code comment. Ambiguity alone is never a reason to stop. **Only if the slice literally cannot be built** — nothing runnable can be produced even with sensible assumptions — invoke `/request-feedback` with the specific question; it posts the question as a comment on the slice and marks it `Blocked`. That must be the absolute exception; a `Blocked` slice should mean "impossible without a human", never "the agent preferred to ask".
52
+ **Default: make a reasonable assumption and build the slice.** If a detail is unclear or missing (an example value, a field type, a status mapping, a referenced event that isn't modeled yet, ...), pick the most sensible interpretation from `slice.json`, its specifications and the surrounding model, build it, and record each assumption in one line as a code comment. Ambiguity alone is never a reason to stop. **Only if the slice literally cannot be built** — nothing runnable can be produced even with sensible assumptions — invoke `/request-feedback` with the specific question; it posts the question as a comment on the slice and marks it `Blocked`. That must be the absolute exception; a `Blocked` slice should mean "impossible without a human", never "the agent preferred to ask".
54
53
 
55
- ## Progress Report Format
54
+ ## Recording Learnings
56
55
 
57
- APPEND to progress.txt (never replace, always append):
56
+ `.build-kit/AGENTS.md` is generated from the learnings recorded so far — read it at the start of every session, never edit it by hand. Record a learning the moment you have it:
58
57
 
59
58
  ```
60
- ## [Date/Time] - [Slice]
61
-
62
- - What was implemented
63
- - Files changed
64
- - **Learnings for future iterations:**
65
- - Patterns discovered (e.g., "this codebase uses X for Y")
66
- - Gotchas encountered (e.g., "don't forget to update Z when changing W")
67
- - Useful context (e.g., "the evaluation panel is in component X")
68
- ---
59
+ npx @eventmodelers/cli learn "<one terse line>" --topic "<topic>"
69
60
  ```
70
61
 
71
- The learnings section is critical - it helps future iterations avoid repeating mistakes and understand the codebase
72
- better.
73
-
74
- ## Consolidate Patterns
75
-
76
- If you discover a **reusable pattern** that future iterations should know, add it to the `## Codebase Patterns` section
77
- at the TOP of progress.txt (create it if it doesn't exist). This section should consolidate the most important
78
- learnings:
79
-
80
- ```
81
- ## Codebase Patterns
82
- - Example: Use event sourcing aggregate patterns for all state changes
83
- - Example: Always use @CommandHandler on the aggregate for write slices
84
- - Example: Export query result types from the slice package
85
- ```
86
-
87
- Only add patterns that are **general and reusable**, not story-specific details.
62
+ - Only what is genuinely reusable: codebase patterns, gotchas, commands that work or don't, conventions — and, after a failed approach, what was tried and why it failed. Not slice-specific details, and nothing the code or git log already says.
63
+ - One learning per call, one line each. A learning that is already in `.build-kit/AGENTS.md` is skipped for you.
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
+ - 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.
88
66
 
89
67
  ## Update AGENTS.md Files
90
68
 
@@ -110,7 +88,7 @@ Before committing, check if any edited files have learnings worth preserving in
110
88
  - Slice specific implementation details
111
89
  - Story-specific implementation details
112
90
  - Temporary debugging notes
113
- - Information already in progress.txt
91
+ - Information already recorded as a learning in `.build-kit/AGENTS.md`
114
92
  - Task specific learnings
115
93
 
116
94
  Only update AGENTS.md if you have **genuinely reusable knowledge** that would help future work
@@ -154,9 +132,9 @@ If ALL slices in the current context are Done, reply with:
154
132
  - If no `.eventmodelers/config.json` exists anywhere (project root or an ancestor directory, or `~/.eventmodelers/config.json` — the one inside `.build-kit/` is only an optional override, so its absence means nothing), skip all platform communication (MCP calls, `update-slice-status`, board sync) and continue working locally.
155
133
  - Work on ONE slice per iteration
156
134
  - Commit frequently
157
- - update progress.txt frequently
158
- - Read the Codebase Patterns section in progress.txt before starting
135
+ - Record learnings as you go with `learn`
136
+ - Read `.build-kit/AGENTS.md` before starting
159
137
 
160
138
  ## When an iteration completes
161
139
 
162
- Use all the key learnings from the progress.txt and update the `.build-kit/AGENTS.md` file with those learnings.
140
+ Make sure every reusable learning is recorded with `learn`; `.build-kit/AGENTS.md` is generated from them — never edit it by hand.