@eventmodelers/cli 1.0.162 → 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.
- package/README.md +22 -27
- package/RELEASE_NOTES.md +20 -2
- package/cli.js +461 -225
- package/lib/adapters/spec-kitty-adapter.js +3 -7
- package/lib/global-agent-id.js +45 -0
- package/lib/modeling-local-ai.js +4 -2
- package/package.json +1 -1
- package/shared/build-kit/README.md +4 -19
- package/shared/build-kit/lib/agent-lock.js +75 -0
- package/shared/build-kit/lib/agent-name.js +27 -0
- package/shared/build-kit/lib/event-prune.js +58 -0
- package/shared/build-kit/lib/events-view.js +121 -0
- package/shared/build-kit/lib/local-ai-agent.js +14 -26
- package/shared/build-kit/lib/mcp-register.js +17 -1
- package/shared/build-kit/lib/prompt-journal.js +191 -0
- package/shared/build-kit/lib/prompt-replay.js +35 -0
- package/shared/build-kit/lib/ralph.js +250 -163
- package/shared/build-kit/lib/slice-curation.js +40 -0
- package/shared/build-kit/lib/slice-events.js +93 -0
- package/shared/build-kit/lib/slice-git.js +86 -0
- package/shared/build-kit/lib/slice-learnings.js +197 -0
- package/shared/build-kit/lib/slice-projection.js +101 -0
- package/shared/build-kit/lib/slice-run.js +181 -0
- package/shared/build-kit/lib/slice-state.js +182 -0
- package/shared/build-kit/lib/slice-tasks.js +35 -0
- package/shared/build-kit/ralph-claude.js +4 -2
- package/shared/build-kit/ralph-exec.js +4 -2
- package/shared/build-kit/ralph-local-ai.js +5 -3
- package/shared/skills/connect/SKILL.md +1 -5
- package/shared/skills/learn-eventmodelers-api/SKILL.md +13 -0
- package/shared/skills/request-feedback/SKILL.md +1 -2
- package/shared/skills/ui-contract/SKILL.md +5 -3
- package/stacks/axon/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/axon/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/axon/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/blank/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/blank/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +20 -37
- package/stacks/blank/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/classic/templates/.claude/skills/build-automation/SKILL.md +1 -1
- package/stacks/classic/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/classic/templates/.claude/skills/build-state-view/SKILL.md +2 -2
- package/stacks/classic/templates/.claude/skills/record-evidence/SKILL.md +2 -2
- package/stacks/classic/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/classic/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/classic/templates/build-kit/lib/backend-prompt.md +20 -38
- package/stacks/classic/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +3 -4
- package/stacks/kurrent/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/kurrent/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +1 -17
- package/stacks/modeling-kit/templates/.claude/skills/detect-model-drift/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +2 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +7 -3
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +0 -8
- package/stacks/modeling-kit/templates/.claude/skills/stressor-analysis/SKILL.md +0 -2
- package/stacks/modeling-kit/templates/.claude/skills/walkthrough/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/kit/AGENTS.md +0 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +3 -4
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +11 -72
- package/stacks/modeling-kit/templates/kit/README.md +1 -1
- package/stacks/modeling-kit/templates/kit/lib/config.js +1 -1
- package/stacks/node/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/node/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/node/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +3 -4
- package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/opencqrs/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +1 -1
- package/stacks/react/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/react/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +20 -37
- package/stacks/react/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/supabase/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/supabase/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/supabase/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +2 -2
- package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +2 -2
- package/stacks/supabase-react/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +20 -37
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/umadb/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/umadb/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/umadb/templates/build-kit/lib/prompt.md +18 -41
- package/shared/bridge/spec-kitty/bridge-spec-kitty-specify/SKILL.md +0 -125
- package/shared/bridge/spec-kitty/bridge-spec-kitty-tasks/SKILL.md +0 -95
- package/shared/build-kit/lib/agent.sh +0 -20
- package/shared/build-kit/ralph.sh +0 -111
- package/shared/build-kit/realtime-agent.js +0 -20
- package/stacks/bridge/templates/bridge/lib/AGENT.md +0 -47
- package/stacks/bridge/templates/bridge/lib/prompt.md +0 -111
- package/stacks/bridge/templates/bridge/ralph-claude.js +0 -50
- package/stacks/bridge/templates/bridge/ralph-hook.js +0 -103
- package/stacks/bridge/templates/bridge/ralph-local-ai.js +0 -43
- package/stacks/bridge/templates/bridge/ralph-static.js +0 -90
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The run loop's shell around the pure slice-state core: claim a slice, settle it after the build, and
|
|
2
|
+
// release claims a dead process left behind. Every step appends events and re-projects status for the
|
|
3
|
+
// build agent; `decide*` (slice-state.js) says what to do, this executes it.
|
|
4
|
+
//
|
|
5
|
+
// `board` is injected: { setStatus(sliceId, status), readStatus(sliceId) → status | null }. null means
|
|
6
|
+
// no board (--local): nothing is pushed or read, the log alone decides.
|
|
7
|
+
|
|
8
|
+
import { appendEvent, readEvents } from './slice-events.js';
|
|
9
|
+
import { fold, decideSettle, decideRecovery } from './slice-state.js';
|
|
10
|
+
import { adoptAgentEdits, projectStatus } from './slice-projection.js';
|
|
11
|
+
import { discardUnfinishedWork, gitBase } from './slice-git.js';
|
|
12
|
+
|
|
13
|
+
// Appends an event and re-projects status, so the files the agent reads never lag the log.
|
|
14
|
+
export function recordEvent(kitDir, event, meta) {
|
|
15
|
+
appendEvent(kitDir, event, meta);
|
|
16
|
+
const state = fold(readEvents(kitDir));
|
|
17
|
+
projectStatus(kitDir, state);
|
|
18
|
+
return state;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isProcessAlive(pid) {
|
|
22
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return err.code === 'EPERM'; // exists, just not ours; ESRCH means gone
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The build agent's side (behind `eventmodelers slice-status`): it records the outcome of the slice the
|
|
32
|
+
// loop has claimed, instead of editing index.json. The loop pushes it to the board when the build ends.
|
|
33
|
+
// The slice is the one open claim, or the one named by id or title. Appends `agent-recorded` and
|
|
34
|
+
// projects straight away — the file the agent reads must never lag the log, or the next refresh would
|
|
35
|
+
// take the stale file for an edit.
|
|
36
|
+
export function recordAgentStatus({ kitDir, status, sliceId, meta }) {
|
|
37
|
+
const claimed = Object.entries(fold(readEvents(kitDir)).slices).filter(([, s]) => s.claim);
|
|
38
|
+
let match;
|
|
39
|
+
if (sliceId) {
|
|
40
|
+
const wanted = sliceId.trim().toLowerCase();
|
|
41
|
+
match = claimed.find(([id, s]) => id === sliceId || s.claim.title?.toLowerCase() === wanted);
|
|
42
|
+
if (!match) throw new Error(`slice "${sliceId}" is not claimed by the run loop — only a slice the loop is building has its status recorded here`);
|
|
43
|
+
} else if (claimed.length === 1) {
|
|
44
|
+
match = claimed[0];
|
|
45
|
+
} else if (claimed.length > 1) {
|
|
46
|
+
throw new Error(`${claimed.length} slices are claimed (${claimed.map(([, s]) => s.claim.title).join(', ')}) — name one with --slice`);
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error('no slice is claimed by the run loop. For a task from tasks.json, set the status with the update-slice-status skill instead');
|
|
49
|
+
}
|
|
50
|
+
const [id, { claim }] = match;
|
|
51
|
+
recordEvent(kitDir, { type: 'agent-recorded', slice: id, status }, meta);
|
|
52
|
+
return { slice: id, title: claim.title ?? id, status };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Claims a slice before it is built. The log is the local arbiter (the first claim wins); the board is
|
|
56
|
+
// the cross-agent one — it rejects a move into the status a slice is already in, and a claim first checks the slice is
|
|
57
|
+
// still in the pickup status. Returns 'won', 'held'
|
|
58
|
+
// (another live process owns it in this kit) or 'lost' (the board refused; nothing was built).
|
|
59
|
+
export async function claimSlice({ kitDir, planned, board, meta, cleanTree, pid = process.pid, pickupStatus = 'Planned' }) {
|
|
60
|
+
const open = fold(readEvents(kitDir)).slices[planned.id]?.claim;
|
|
61
|
+
if (open && open.pid !== pid && isProcessAlive(open.pid)) return 'held';
|
|
62
|
+
// Look at the board before claiming. Its same-status guard only turns away an InProgress over an InProgress, so an
|
|
63
|
+
// agent working from a stale view (Planned, while another has already built and finished it) would otherwise move
|
|
64
|
+
// a Done slice back to InProgress and build it twice. What the board really shows is recorded, which also puts the
|
|
65
|
+
// loop's stale local view right. (Two claims in the same instant are still decided by that same-status guard.)
|
|
66
|
+
if (board) {
|
|
67
|
+
let current;
|
|
68
|
+
try {
|
|
69
|
+
current = await board.readStatus(planned.id);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(`[ralph] Could not read the board before claiming "${planned.title}":`, err.message);
|
|
72
|
+
return 'lost';
|
|
73
|
+
}
|
|
74
|
+
if (current && current.toLowerCase() !== pickupStatus.toLowerCase()) {
|
|
75
|
+
console.log(`[ralph] "${planned.title}" is ${current} on the board, not ${pickupStatus} — someone else got there first.`);
|
|
76
|
+
recordEvent(kitDir, { type: 'board-observed', slice: planned.id, status: current }, meta);
|
|
77
|
+
return 'lost';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const state = recordEvent(kitDir, {
|
|
81
|
+
type: 'claimed', slice: planned.id, ctx: planned.ctx, title: planned.title, pid, base: gitBase(kitDir), cleanTree,
|
|
82
|
+
// What the board says about the slice at build time, when it says anything: both can change between builds.
|
|
83
|
+
...(planned.ticketNumber ? { ticketNumber: planned.ticketNumber } : {}),
|
|
84
|
+
...(planned.assignee ? { assignee: planned.assignee } : {}),
|
|
85
|
+
}, meta);
|
|
86
|
+
if (state.slices[planned.id].claim?.pid !== pid) return 'held';
|
|
87
|
+
if (board) {
|
|
88
|
+
try {
|
|
89
|
+
await board.setStatus(planned.id, 'InProgress');
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`[ralph] Could not claim "${planned.title}" on the board:`, err.message);
|
|
92
|
+
recordEvent(kitDir, { type: 'released', slice: planned.id, reason: 'lost' }, meta);
|
|
93
|
+
// Counted like any attempt that did not move the slice, so one the board keeps refusing is switched away from.
|
|
94
|
+
recordEvent(kitDir, { type: 'attempt-failed', slice: planned.id, reason: 'claim-lost', lastReply: null }, meta);
|
|
95
|
+
return 'lost';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
recordEvent(kitDir, { type: 'claim-confirmed', slice: planned.id }, meta);
|
|
99
|
+
return 'won';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Executes what decideSettle / decideRecovery returned for one slice. false when the board refused the
|
|
103
|
+
// push — no `settled` is appended then, so the claim stays open and the next iteration settles it again.
|
|
104
|
+
async function runActions({ kitDir, planned, board, meta, actions, details }) {
|
|
105
|
+
const unfinished = actions.some((a) => a.type === 'discard');
|
|
106
|
+
let discarded = null;
|
|
107
|
+
for (const a of actions) {
|
|
108
|
+
if (a.type === 'discard') {
|
|
109
|
+
discarded = discardUnfinishedWork(kitDir, planned, !a.skip, a.base);
|
|
110
|
+
} else if (a.type === 'push') {
|
|
111
|
+
if (board) {
|
|
112
|
+
try {
|
|
113
|
+
await board.setStatus(planned.id, a.status);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(`[ralph] Could not settle the status of "${planned.title}":`, err.message);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
recordEvent(kitDir, { type: 'settled', slice: planned.id, status: a.status }, meta);
|
|
120
|
+
console.log(`[ralph] "${planned.title}" InProgress → ${a.status}.`);
|
|
121
|
+
} else if (a.type === 'release') {
|
|
122
|
+
recordEvent(kitDir, { type: 'released', slice: planned.id, reason: a.reason }, meta);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Counted once the slice is actually back, so a retried settle cannot count the attempt twice.
|
|
126
|
+
if (unfinished) {
|
|
127
|
+
recordEvent(kitDir, {
|
|
128
|
+
type: 'attempt-failed',
|
|
129
|
+
slice: planned.id,
|
|
130
|
+
reason: details.built === false ? 'turn-failed' : 'no-outcome',
|
|
131
|
+
status: actions.find((a) => a.type === 'push')?.status,
|
|
132
|
+
lastReply: details.lastReply ?? null,
|
|
133
|
+
...(discarded ?? {}),
|
|
134
|
+
}, meta);
|
|
135
|
+
console.log(`[ralph] "${planned.title}" ended without an outcome.`);
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// After a build: the outcome the agent recorded is pushed to the board; no outcome means unfinished —
|
|
141
|
+
// its work is discarded and the slice goes back to `pickupStatus`. Returns true once settled; false when
|
|
142
|
+
// it could not be (the claim stays open and the loop tries again at the top of its next iteration).
|
|
143
|
+
export async function settleSlice({ kitDir, planned, board, meta, pickupStatus = 'Planned', details = {} }) {
|
|
144
|
+
const state = adoptAgentEdits(kitDir, fold(readEvents(kitDir)), meta);
|
|
145
|
+
if (!state.slices[planned.id]?.claim) return true;
|
|
146
|
+
let boardStatus = null;
|
|
147
|
+
if (board) {
|
|
148
|
+
try {
|
|
149
|
+
boardStatus = await board.readStatus(planned.id);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`[ralph] Could not read the board to settle "${planned.title}":`, err.message);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const actions = decideSettle(state, planned.id, { boardStatus, pickupStatus });
|
|
156
|
+
return runActions({ kitDir, planned, board, meta, actions, details });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Claims still open at the top of an iteration are leftovers: the process died mid-build, or settling
|
|
160
|
+
// failed last time. Each is settled like a finished build; one held by another live process is left alone.
|
|
161
|
+
export async function settleLeftoverClaims({ kitDir, board, meta, pickupStatus = 'Planned', selfPid = process.pid, isAlive = isProcessAlive }) {
|
|
162
|
+
const state = adoptAgentEdits(kitDir, fold(readEvents(kitDir)), meta);
|
|
163
|
+
const boardStatuses = {};
|
|
164
|
+
const unreadable = new Set();
|
|
165
|
+
for (const [id, s] of Object.entries(state.slices)) {
|
|
166
|
+
if (!s.claim || !board || (s.claim.pid !== selfPid && isAlive(s.claim.pid))) continue;
|
|
167
|
+
try {
|
|
168
|
+
boardStatuses[id] = await board.readStatus(id);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(`[ralph] Could not read the board to settle the leftover claim on "${s.claim.title ?? id}":`, err.message);
|
|
171
|
+
unreadable.add(id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const { slice, actions } of decideRecovery(state, { isAlive, selfPid, boardStatuses, pickupStatus })) {
|
|
175
|
+
if (unreadable.has(slice)) continue;
|
|
176
|
+
const claim = state.slices[slice].claim;
|
|
177
|
+
const planned = { id: slice, title: claim.title ?? slice, ctx: claim.ctx };
|
|
178
|
+
console.log(`[ralph] Found an unsettled claim on "${planned.title}" — settling it.`);
|
|
179
|
+
await runActions({ kitDir, planned, board, meta, actions, details: { built: false } });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -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
|
-
|
|
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 —
|
|
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);
|
|
@@ -287,11 +287,7 @@ Connecting is also where the session's read discipline starts. Every skill that
|
|
|
287
287
|
|
|
288
288
|
### The slice status comes with the outline
|
|
289
289
|
|
|
290
|
-
The orientation read also
|
|
291
|
-
|
|
292
|
-
**Only a slice in `Created` may be written to.** Any other status — `Ready`, `Planned`, `Assigned`, `InProgress`, `Handover`, `Review`, `Blocked`, `Done`, `Informational` — means someone is working on that slice: read its elements for context, but never change, move, rename or delete them, and never add scenarios, fields or examples to them. An element in a column with **no** `sliceStatus` is in no slice at all, which is not the same as locked — that one is writable.
|
|
293
|
-
|
|
294
|
-
Never spend a `list_slices`/`get_slice_data` call to answer this; the outline already did.
|
|
290
|
+
The orientation read also gives each column's `sliceStatus` (`get_nodes` does **not** carry it) — a slice owns a column, so every element in that column shares its status. It is context, not a write lock: any slice may be changed, whatever its status.
|
|
295
291
|
|
|
296
292
|
`get_node` without a projection is for **one** node you did not already load — most often re-reading a node right after writing it. A step that issues it in a loop over nodes that were already in a list response is doing the same fetch N times; collapse it to the single chapter-scoped read above.
|
|
297
293
|
|
|
@@ -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
|
|
111
|
-
`update-slice-status` (
|
|
112
|
-
`
|
|
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
|
|
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
|
|
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
|
|
1
|
+
# Agent Reference
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Tasks
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
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.
|