@eventmodelers/cli 1.0.161 → 1.0.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -680,6 +680,7 @@ Flow: `Ready → Planned → InProgress → Handover → InProgress → Done`. P
680
680
  - **Both sides need the new kit.** An already-installed backend kit keeps its old `ralph.js` and prompts and silently ignores `--handover` (slices still end at `Done`); `re-init` it to pick the feature up.
681
681
  - **The platform must know the `Ready` / `Handover` statuses.**
682
682
  - A kit declares what it claims in `lib/kit.json` — `{ "pickupStatus": "Handover" }`; absent means `Planned`.
683
+ - `run --status-trigger <status>` overrides that pickup status for a single run (e.g. `--status-trigger Ready`); nothing is saved.
683
684
 
684
685
  The contract itself is the shared `ui-contract` skill's job (see above); it also owns the rule for which status a slice ends at.
685
686
 
package/cli.js CHANGED
@@ -3718,6 +3718,7 @@ credentialFlags(program
3718
3718
  .option('--dangerously-skip-permissions', 'Run the modeling agent\'s Claude process with --dangerously-skip-permissions, as before the lockdown: every tool call is approved — any shell command, curl to anywhere, any file the user can read or write, every MCP server the machine configures. Off by default because the agent takes chat messages and snippet clicks from any collaborator on the board; pass it only for a board whose collaborators you trust with your shell. Default Claude runner of --modeling/--standalone/--worker/--global only — not with --agent, --exec or --local-ai, which bring their own permission flags.')
3719
3719
  .option('--browser', 'Give the locked-down modeling agent a browser: the Playwright MCP server (npx @playwright/mcp) next to the eventmodelers one, for discover-storyboard. Off by default because a browser can reach any site, which the lockdown otherwise rules out. Default Claude runner only; --dangerously-skip-permissions already loads every MCP server the machine configures.')
3720
3720
  .option('--handover', 'Backend build kit: for this run only, finish slices that have a UI at `Handover` instead of `Done` (same as the project setting `handover: true`, which `init --handover` / `config --handover on` save — this flag writes nothing). Without it the project setting applies; without both, slices end at `Done`.')
3721
+ .option('--status-trigger <status>', 'Build kit: the slice status the agent picks work up from, for this run only (e.g. Ready, Planned, Handover). Defaults to the kit\'s own pickup status — `Planned` for backend kits, `Handover` for the UI kits. Nothing is written to disk.')
3721
3722
  .option('--verbose', 'Log every tool call\'s full input (commands, skill args, file paths) and assistant reasoning text. Default is condensed, high-level per-step logging only.')
3722
3723
  .option('--id <id>', 'Pin the agent id this run identifies itself with on the platform. A project install otherwise mints one id per project and reuses it on every restart; --global/--standalone mints a fresh one per run, since two ad-hoc agents for one board must not share a row (the heartbeat is keyed on token + agent_id + agent_type, so the second would replace the first). Pass this when an agent has to keep ONE identity across restarts — a supervisor that already knows the id, or a board where it is the starred "preferred agent". Per-run only: nothing is written to disk.')
3723
3724
  .option('--name <name>', 'A human-readable name for this agent, sent with every heartbeat so the board shows which agent is live rather than a bare uuid (e.g. "ci-builder", "martins-laptop"). Per-run only, like --id: the persistent name is `agentName` in config.json (set via `init --name` / `init-config --name`), and this overrides it for one run without writing anything.')
@@ -3988,7 +3989,7 @@ credentialFlags(program
3988
3989
  // local-only branch even when .eventmodelers/config.json has valid credentials.
3989
3990
  // RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
3990
3991
  // they reach both node runners but not ralph.sh, which has no heartbeat to identify.
3991
- execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_HANDOVER: opts.handover ? '1' : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}), ...(typeof opts.exec === 'string' ? { RALPH_EXEC_CMD: opts.exec } : {}) } });
3992
+ execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_HANDOVER: opts.handover ? '1' : '', RALPH_STATUS_TRIGGER: opts.statusTrigger ? String(opts.statusTrigger).trim() : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}), ...(typeof opts.exec === 'string' ? { RALPH_EXEC_CMD: opts.exec } : {}) } });
3992
3993
  } catch (err) {
3993
3994
  process.exit(err.status || 1);
3994
3995
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.161",
3
+ "version": "1.0.163",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -521,8 +521,11 @@ function hasPendingTasks(kitDir) {
521
521
 
522
522
  // The slice status this kit claims work from. A stack declares it in `lib/kit.json`
523
523
  // ({ "pickupStatus": "Handover" } for the UI kits, which build what a backend kit handed over);
524
- // absent means "Planned", the backend default.
524
+ // absent means "Planned", the backend default. `run --status-trigger <status>` (RALPH_STATUS_TRIGGER)
525
+ // overrides either for that run.
525
526
  export function readPickupStatus(kitDir) {
527
+ const override = (process.env.RALPH_STATUS_TRIGGER || '').trim();
528
+ if (override) return override;
526
529
  try {
527
530
  const s = JSON.parse(readFileSync(join(kitDir, 'lib', 'kit.json'), 'utf-8')).pickupStatus;
528
531
  if (typeof s === 'string' && s.trim()) return s.trim();
@@ -550,8 +553,8 @@ function readTicketNumber(kitDir, ctx, folder) {
550
553
  // Returns the first Planned slice, preferring the current context so a context is finished
551
554
  // before the next one starts. When the current context has no planned work, the first other
552
555
  // context that does becomes the current one (current_context.json is rewritten), so a slice
553
- // set to Planned in any context is eventually built.
554
- function getFirstPlannedSlice(kitDir) {
556
+ // set to Planned in any context is eventually built. Ids in `skipIds` are passed over.
557
+ function getFirstPlannedSlice(kitDir, skipIds = new Set()) {
555
558
  const slicesDir = join(kitDir, '.slices');
556
559
  const currentCtx = readCurrentContext(kitDir);
557
560
  let others = [];
@@ -565,7 +568,7 @@ function getFirstPlannedSlice(kitDir) {
565
568
  if (!existsSync(indexPath)) continue;
566
569
  try {
567
570
  const { slices } = JSON.parse(readFileSync(indexPath, 'utf-8'));
568
- const planned = slices && slices.find((s) => (s.status || '').toLowerCase() === pickup);
571
+ const planned = slices && slices.find((s) => (s.status || '').toLowerCase() === pickup && !skipIds.has(s.id));
569
572
  if (!planned) continue;
570
573
  if (ctx !== currentCtx) {
571
574
  writeFileSync(join(slicesDir, 'current_context.json'), JSON.stringify({ name: ctx }, null, 2), 'utf-8');
@@ -577,15 +580,35 @@ function getFirstPlannedSlice(kitDir) {
577
580
  return null;
578
581
  }
579
582
 
580
- // If the exact same Planned slice (by id) comes back up this many times in a
581
- // row without its status ever leaving "Planned", onPlannedSlice is stuck on
583
+ // Ids of every slice currently waiting for pickup, across all contexts.
584
+ function listPlannedIds(kitDir) {
585
+ const slicesDir = join(kitDir, '.slices');
586
+ const pickup = readPickupStatus(kitDir).toLowerCase();
587
+ const ids = new Set();
588
+ try {
589
+ for (const d of readdirSync(slicesDir, { withFileTypes: true })) {
590
+ if (!d.isDirectory()) continue;
591
+ try {
592
+ const { slices } = JSON.parse(readFileSync(join(slicesDir, d.name, 'index.json'), 'utf-8'));
593
+ for (const s of slices ?? []) if ((s.status || '').toLowerCase() === pickup) ids.add(s.id);
594
+ } catch {}
595
+ }
596
+ } catch {}
597
+ return ids;
598
+ }
599
+
600
+ // If the exact same Planned slice (by id) has been built this many times without its status ever leaving "Planned", onPlannedSlice is stuck on
582
601
  // it — declining to build it, or building it but its own status change keeps
583
602
  // getting reverted (e.g. a failed check). Rather than retry it forever (or
584
603
  // crash the whole loop, which would take down every other slice with it),
585
604
  // mark it Blocked with a note explaining why and move on to other work.
586
605
  // Critical for unsupervised/CI runs, which have no human watching to notice
587
606
  // a stall. Configurable for teams that want more slack.
588
- const MAX_PLANNED_ATTEMPTS = Number(process.env.RALPH_MAX_PLANNED_ATTEMPTS) || 2;
607
+ const MAX_PLANNED_ATTEMPTS = Number(process.env.RALPH_MAX_PLANNED_ATTEMPTS) || 5;
608
+
609
+ // After this many builds that left a slice Planned, the loop builds any other Planned slice first
610
+ // and comes back to the stuck one only when nothing else is waiting (until MAX_PLANNED_ATTEMPTS).
611
+ const SWITCH_AFTER_ATTEMPTS = Math.min(2, MAX_PLANNED_ATTEMPTS);
589
612
 
590
613
  // Marks a stuck slice Blocked (locally, and on the board if credentialed) and
591
614
  // records why, so the loop can move on instead of looping or exiting.
@@ -661,6 +684,23 @@ async function blockStuckSlice(kitDir, cfg, credentialed, planned, attempts, las
661
684
  }
662
685
  }
663
686
 
687
+ // Always leave a comment on the slice saying why it was blocked — a Blocked slice with no
688
+ // explanation is a dead end for whoever finds it. Its own try block, so a failed status sync
689
+ // above can't skip it; without credentials there is no board to comment on, so say so loudly.
690
+ if (credentialed) {
691
+ try {
692
+ await fetchJSON(`${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes/${planned.id}/comments`, {
693
+ method: 'POST',
694
+ headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-user-id': 'ralph-loop', ...agentHeaders(cfg) },
695
+ body: JSON.stringify({ text: `Slice "${planned.title}" was auto-blocked by the Ralph loop. ${reason}`, type: 'COMMENT', author: 'ralph-loop' }),
696
+ });
697
+ } catch (err) {
698
+ console.error(`[ralph] Failed to post the auto-block comment on "${planned.title}" — the reason is only in progress.txt:`, err.message);
699
+ }
700
+ } else {
701
+ console.error(`[ralph] No board credentials — could not comment on "${planned.title}"; the reason is only in progress.txt.`);
702
+ }
703
+
664
704
  console.error(`[ralph] ${reason} Marked "${planned.title}" (id=${planned.id}) as Blocked — moving on.`);
665
705
  }
666
706
 
@@ -703,7 +743,7 @@ function taskRequest(planned = null, cfg = null, pickupStatus = 'Planned') {
703
743
  : 'process the pending tasks in tasks.json';
704
744
  // Stated explicitly so the agent never has to work it out from config.json: this run's effective
705
745
  // value, which is what the `ui-contract` skill (Step 4) follows. UI kits (pickup Handover) ignore it.
706
- const handover = planned && pickupStatus === 'Planned' ? `\nHandover: ${resolveHandover(cfg) ? 'on' : 'off'}\n` : '';
746
+ const handover = planned && pickupStatus.toLowerCase() !== 'handover' ? `\nHandover: ${resolveHandover(cfg) ? 'on' : 'off'}\n` : '';
707
747
  return `\n\n---\n\nYour task now: ${task}, following the instructions above. Start immediately.\n${handover}`;
708
748
  }
709
749
 
@@ -715,9 +755,9 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
715
755
  // this back on.
716
756
  const credentialed = !localOnly && hasCredentials(cfg);
717
757
  let lastIdleCtx;
718
- // Tracks consecutive sightings of the same Planned slice id — see
719
- // MAX_PLANNED_ATTEMPTS above.
720
- let stuckSlice = { id: null, count: 0 };
758
+ // Builds so far per slice id that is still Planned — see MAX_PLANNED_ATTEMPTS above. An id drops
759
+ // out as soon as its slice leaves Planned, so only builds that failed to move it count.
760
+ const plannedAttempts = new Map();
721
761
  // The build agent's final reply on the most recent planned-slice turn — quoted in the Blocked note.
722
762
  let lastReply = null;
723
763
 
@@ -732,18 +772,23 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
732
772
  didWork = true;
733
773
  }
734
774
 
735
- const planned = onPlannedSlice && getFirstPlannedSlice(kitDir);
775
+ let planned = null;
776
+ if (onPlannedSlice) {
777
+ const stillPlanned = listPlannedIds(kitDir);
778
+ for (const id of plannedAttempts.keys()) if (!stillPlanned.has(id)) plannedAttempts.delete(id);
779
+ const stuckIds = new Set([...plannedAttempts].filter(([, n]) => n >= SWITCH_AFTER_ATTEMPTS).map(([id]) => id));
780
+ // Prefer a slice that hasn't been tried to the switch point; fall back to a stuck one.
781
+ planned = getFirstPlannedSlice(kitDir, stuckIds) ?? (stuckIds.size ? getFirstPlannedSlice(kitDir) : null);
782
+ }
736
783
  if (planned) {
737
- stuckSlice = planned.id !== null && planned.id === stuckSlice.id
738
- ? { id: stuckSlice.id, count: stuckSlice.count + 1 }
739
- : { id: planned.id, count: 1 };
740
-
741
- if (stuckSlice.count > MAX_PLANNED_ATTEMPTS) {
742
- await blockStuckSlice(kitDir, cfg, credentialed, planned, stuckSlice.count, lastReply);
743
- stuckSlice = { id: null, count: 0 };
784
+ const attempts = plannedAttempts.get(planned.id) ?? 0;
785
+ if (planned.id !== null && attempts >= MAX_PLANNED_ATTEMPTS) {
786
+ await blockStuckSlice(kitDir, cfg, credentialed, planned, attempts, lastReply);
787
+ plannedAttempts.delete(planned.id);
744
788
  didWork = true;
745
789
  continue;
746
790
  }
791
+ if (planned.id !== null) plannedAttempts.set(planned.id, attempts + 1);
747
792
 
748
793
  const prompt = readFileSync(backendPromptFile, 'utf-8') + taskRequest(planned, cfg, readPickupStatus(kitDir));
749
794
  if (credentialed) await fetchFullSlice(cfg, kitDir, planned.id);
@@ -754,7 +799,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
754
799
  context: planned.ctx,
755
800
  ticketNumber: planned.ticketNumber,
756
801
  boardId: cfg.boardId ?? null,
757
- attempt: stuckSlice.count,
802
+ attempt: attempts + 1,
758
803
  }); });
759
804
  if (built) console.log(`[ralph] Slice build complete — waiting for next slice`);
760
805
  if (credentialed) await fetchAndPersistSlices(cfg, kitDir).catch(() => {});
@@ -41,9 +41,12 @@ has_pending_tasks() {
41
41
  [[ "$content" != "[]" && -n "$content" ]]
42
42
  }
43
43
 
44
- # Returns 0 if any JSON under .build-kit/.slices/ contains a "Planned" status
44
+ # Status this loop picks work up from — `run --status-trigger <status>` (RALPH_STATUS_TRIGGER), default Planned
45
+ PICKUP_STATUS="${RALPH_STATUS_TRIGGER:-Planned}"
46
+
47
+ # Returns 0 if any JSON under .build-kit/.slices/ contains a slice in the pickup status
45
48
  has_planned_slices() {
46
- grep -rqi '"status"[[:space:]]*:[[:space:]]*"planned"' "$KIT_DIR/.slices/" --include='index.json' 2>/dev/null
49
+ grep -rqi "\"status\"[[:space:]]*:[[:space:]]*\"$PICKUP_STATUS\"" "$KIT_DIR/.slices/" --include='index.json' 2>/dev/null
47
50
  }
48
51
 
49
52
  # Returns the title of the first "Planned" slice, or empty string
@@ -54,10 +57,10 @@ get_planned_slice_title() {
54
57
  title=$(node -e "
55
58
  try {
56
59
  const d = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf-8'));
57
- const s = (d.slices||[]).find(s => (s.status||'').toLowerCase() === 'planned');
60
+ const s = (d.slices||[]).find(s => (s.status||'').toLowerCase() === process.argv[2].toLowerCase());
58
61
  if (s) process.stdout.write(s.slice || s.id || '');
59
62
  } catch(e) {}
60
- " "$index_file" 2>/dev/null)
63
+ " "$index_file" "$PICKUP_STATUS" 2>/dev/null)
61
64
  if [[ -n "$title" ]]; then
62
65
  echo "$title"
63
66
  return
@@ -95,7 +98,7 @@ while [[ "$ITERATIONS" -eq 0 || "$cycle" -lt "$ITERATIONS" ]]; do
95
98
 
96
99
  if has_planned_slices; then
97
100
  slice_title=$(get_planned_slice_title)
98
- run_agent "onPlannedSlice: building \"$slice_title\"..." "$(cat "$BACKEND_PROMPT_FILE"; task_request "build the Planned slice \"$slice_title\"")"
101
+ run_agent "onPlannedSlice: building \"$slice_title\"..." "$(cat "$BACKEND_PROMPT_FILE"; task_request "build the $PICKUP_STATUS slice \"$slice_title\"")"
99
102
  echo "[$(date -u +%H:%M:%S)] Slice \"$slice_title\" build complete — waiting for next slice"
100
103
  ran_something=true
101
104
  fi
@@ -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 tells you what you may write to. `get_board_outline` returns `sliceStatus` per column — a slice owns a column, so every element in that column shares its status. `get_nodes` does **not** carry it, so take it from the outline before any write:
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
 
@@ -114,22 +114,6 @@ Stop traversal when:
114
114
 
115
115
  ---
116
116
 
117
- ## Step 3e — Check the slices first (before any write)
118
-
119
- Only elements in a slice with status `Created` may be changed — and an element in no slice at all is
120
- not locked. The chain you resolved does not carry that: `get_attribute_chain` and `get_node` do not
121
- report `sliceStatus`. Read it now, once: `mcp__eventmodelers__get_board_outline { "boardId": "$BOARD_ID", "chapterId": "$CHAPTER_ID" }`
122
- gives every column a `sliceStatus` when a slice spans it. Look up each chain node's column.
123
-
124
- - **Every node writable** (`Created`, or no `sliceStatus`) → continue with Step 4.
125
- - **Any node in a column with another status** (`Done`, `Review`, `Handover`, `InProgress`, `Blocked`, `Ready`, `Planned`, `Assigned`,
126
- `Informational`) → **do not write that node.** If the task text says the person already confirmed changing
127
- that locked slice (*"The person confirmed changing the locked slice '…'"*, with an `origin_message_id`), you may
128
- change those elements and only those. Otherwise write **nothing** — not even the nodes that are writable, the
129
- chain is one logical edit — and follow `CLAUDE.md` § *Locked slices in a chat*: ask in the chat with a
130
- `confirm` snippet (or a `changes` snippet listing the elements) naming the slice and its status, post no
131
- `COMMENT` on the slice, and end the turn. (No chat to ask in? Change nothing and post a `COMMENT` on the slice.)
132
-
133
117
  ## Step 4 — Apply the change to the whole chain in one write
134
118
 
135
119
  Compute the updated `fields` array for **every** node in the chain first, in order (TARGET_NODE first, then backwards to SOURCE_NODE), then submit them all in a **single** `submit_node_events` call. Do not write one node, check it, and move to the next — the chain is one logical edit and `events[]` takes the whole batch.
@@ -196,7 +180,7 @@ Tell the user:
196
180
  - **Operation**: add `"<name>"` / rename `"<old>"` → `"<new>"`
197
181
  - **Chain**: list each element in order (type + title + cell)
198
182
  - **Updated**: which nodes were changed
199
- - **Skipped**: which nodes were skipped and why (field already exists / field not found / slice not `Created` — asked to confirm)
183
+ - **Skipped**: which nodes were skipped and why (field already exists / field not found)
200
184
  - **Lineage**: where the attribute starts (`generated` on which element) and any `mapping` you set for a renamed hop
201
185
 
202
186
  Example output:
@@ -25,14 +25,6 @@ From `$ARGUMENTS`, extract:
25
25
 
26
26
  ---
27
27
 
28
- ## Step 1a — Locked slices
29
-
30
- Only elements in a slice with status `Created` may be changed (no slice at all = not locked).
31
- `get_board_outline` reports `sliceStatus` per column; read it for the chapter before writing, and for
32
- any target in a column with another status (`Done`, `Review`, `InProgress`, …) **write nothing to it**
33
- unless the task says the person already confirmed changing that locked slice. Otherwise ask in the chat
34
- with a `confirm` snippet — `CLAUDE.md` § *Locked slices in a chat* — and post no `COMMENT` on the slice.
35
-
36
28
  ## Step 1b — Many targets at once
37
29
 
38
30
  `target` is singular, but the common real request is "fill in the examples across this chapter". When you have more than one target, **do not run this skill once per element and do not fetch context a node at a time.**
@@ -89,8 +89,6 @@ In the text, name the **clusters** (elements hit by the same stressor pair), the
89
89
 
90
90
  Propose new slices (translations, reconciliation todo lists, grace periods, second entry paths …) and state the **new residue** for each stressor that hit the cluster. A `confirm` snippet: `headline` = *"Add these N slices for <cluster>?"*; the slices and their new residues go in the text, kept short.
91
91
 
92
- Never touch elements in non-`Created` slices without an explicit yes; name them instead ("Order Paid sits in a Done slice — I'd leave it and add a translation next to it").
93
-
94
92
  ## Step 4 — On yes
95
93
 
96
94
  One `create_prompt` per new slice (`originMessageId` = the yes), phrased so the prompt turn can do it on its own (chapter, slice name, its elements and fields). Reply in one line. A no: acknowledge it and offer the next cluster.
@@ -170,6 +170,6 @@ Impact mode, same shape with `status` (and `changes`, or `elements` for a new sl
170
170
  - `summary` is always there — the first page shows it under the slice count.
171
171
  - Every `nodeId` / `screenId` is an id you read in Steps 2–3 — never a guessed or remembered one. A `new` slice has no `nodeId`.
172
172
  - The snippet is at most 64 KB; keep descriptions short rather than dropping steps.
173
- - **Impact mode** ends there: the walkthrough is the proposal. If the person then asks for the changes, that is ordinary work (prompts per the kit's CLAUDE.md) — and a slice that is not `Created` still needs their confirmation first.
173
+ - **Impact mode** ends there: the walkthrough is the proposal. If the person then asks for the changes, that is ordinary work (prompts per the kit's CLAUDE.md).
174
174
 
175
175
  **No chat** (run from a terminal): print the slice count and the summary, then the steps as a numbered list with the same fields, instead of posting.
@@ -20,4 +20,3 @@ ones in a compressed, reusable form; only add if not already covered here.
20
20
  - Mint ids once per turn, in one shell: `for i in $(seq <n>); do uuidgen; done` — one per new node, plus one per event, since every event needs its own fresh `id` (required; not the node id). Nothing in it depends on anything being read, so splitting it across several calls buys round trips for nothing. Never mint a per-event `boardId` or `timestamp` — both are server-derived and ignored if sent.
21
21
  - A subagent is a fresh session: it gets `token=`/`org=`/`baseUrl=`/`board=` inline as already-resolved values (which satisfies `connect` at its Step 0 — tell it not to invoke `/connect`) plus the board state already read for it, inline. Handed bare node ids instead, it has exactly one way to recover the rest — re-reading the whole board, once per agent.
22
22
  - Dispatch executor agents with `model:` set to the session header's `subagent_model` (default `sonnet`). The judging happened before the dispatch, on this session's model; what's left is execution against a written brief. Keep an agent on the session model only where its piece re-derives modeling structure (a chain's shape, a slice boundary).
23
- - Only slices in status `Created` may be modified. `get_board_outline` returns `sliceStatus` per column (`get_nodes` does not carry it), so the outline read at `/connect` Step 5 already answers it — absent means the column is in no slice (writable), not locked. Never spend a `list_slices`/`get_slice_data` call just to check whether you may write.
@@ -84,22 +84,6 @@ header's `standalone=on|off` tells you which mode they come in.
84
84
 
85
85
  At the start of every session, read `.agent-modeling-kit/AGENTS.md` if it exists to load accumulated learnings.
86
86
 
87
- **Only touch elements in a slice whose status is `Created` — unless the person explicitly confirmed it.** Every other status — `Ready`, `Planned`,
88
- `Assigned`, `InProgress`, `Handover`, `Review`, `Blocked`, `Done`, `Informational` — means someone is working
89
- on that slice: read it for context, but never change, move, rename or delete its elements, and
90
- never add scenarios, fields or examples to them. An element in no slice at all is not locked.
91
- `get_board_outline` returns `sliceStatus` per column (`get_nodes` does not carry it), so the
92
- outline read `/connect` Step 5 already makes answers this — no `list_slices`/`get_slice_data` call needed.
93
- If only part of what you were asked to do is locked, do the rest and name what you skipped and
94
- why. **When the work came from a chat** (the prompt carries `origin_session_id=`, or you are in a
95
- `CHAT` turn), do not simply refuse and do not change it either: **ask the person to confirm**, in
96
- the chat, with a `confirm` snippet (see "Locked slices in a chat" below). Their yes is what unlocks
97
- exactly those elements — nothing else about the rule loosens. **No skill overrides this.** `/attributes`,
98
- `/examples`, `/place-element` and the rest write through their own steps; a skill that does not mention
99
- `sliceStatus` is not permission — check the outline before its first write, every time. A `COMMENT` on the slice is only for
100
- when there is no chat to ask in (a prompt that did not come from one, a standalone turn): then change
101
- nothing and comment which status blocked it. Where there is a chat, never comment — ask with the snippet.
102
-
103
87
  **Every field you add or rename must have a known source. This holds in every skill and every turn, not only in a full modeling run.**
104
88
  Follow `eventmodeling-information-completeness` before writing any field. It defines the three valid sources: a same-named upstream field, a `mapping`, or `generated: true`. A field with none of these is a gap to close upstream, never one to place unmapped.
105
89
 
@@ -249,19 +233,14 @@ for it. Instead:
249
233
  *"Run /<name> <the rest of the message>"*, with the target resolved from `context=` like any other
250
234
  (selected elements, chapter), and say so in one line. Don't re-interpret it as a question. A skill that
251
235
  needs something the message lacks (which attribute? which screen?) is a **clarify**: ask, with a
252
- snippet if the answer is a pick. A locked slice still needs the person's confirmation
253
- ("Locked slices in a chat" below).
236
+ snippet if the answer is a pick.
254
237
  - **a review in other words** ("what do you think?", "any questions on this?", "what's missing here?") —
255
238
  your call: a quick take is an **answer**, in prose; a thorough review of a whole context or chapter may
256
239
  be the same prompt as above. If a reply ends in a question the person answers with a click ("Want me to
257
240
  turn these into comments?"), that question is a snippet, not a sentence (step 4, Snippets).
258
241
  - **clarify** — too vague to act on even with the session behind it: do no work.
259
242
  - **work** — it asks for a change to the board: create the work now, as below — a request is its
260
- own go-ahead, in every mode — **except for a locked slice.** Before `create_prompt`, find out whether
261
- the elements it names (and, for a change that follows connections, the ones it reaches) sit in a slice
262
- that is not `Created`: `get_board_outline` for that chapter, `sliceStatus` per column. If so, no
263
- prompt for those: ask first (see "Locked slices in a chat"). A request is not a go-ahead to reopen
264
- finished work.
243
+ own go-ahead, in every mode.
265
244
  - **confirm** — the message says yes to a proposal you started after a board change (`--modeling`
266
245
  proposes instead of acting on board changes; see "Propose mode" in CLAUDE-STANDALONE.md). Any
267
246
  form counts, typed or spoken ("yes", "sure", "go ahead", "do it", "ok, but only the
@@ -365,31 +344,6 @@ the reason: *"Added the fields to Register User. I left Order Placed alone — s
365
344
  The work card shows "Done", which the person would otherwise read as everything went through. Never
366
345
  reply `DONE` to a chat-originated prompt without having posted a message.
367
346
 
368
- ### Locked slices in a chat
369
-
370
- A request that touches an element in a slice that is not `Created` (e.g. `Done`) is not refused and
371
- not silently turned into a comment: **you ask, explicitly, and wait for the yes.** Whichever turn
372
- notices it — the `CHAT` turn (the outline you read already has `sliceStatus`) or the prompt turn —
373
- does the same:
374
-
375
- 1. **Change nothing in the locked slice.** Do everything else the request asked for.
376
- 2. **Post one `post_chat_message`** (`replyTo` = this turn's `message_id` in a `CHAT` turn; `sessionId`
377
- = `origin_session_id` in a prompt turn) with a **`confirm` snippet**: the text names the slice, its
378
- status and what you would change — *"Order Placed is in the slice 'Place order', which is Done.
379
- Changing it reopens finished work."* — and the snippet's `headline` is the question, with a
380
- `yesLabel` that says what happens (*"Yes, change it"*, `noLabel` *"No, leave it"*). When the change is
381
- several edits, a `changes` snippet (title = element as link, slice, before/after, one confirm) shows
382
- exactly what will be touched instead.
383
- 3. **Reply `DONE`** — a turn that asked is finished; the question is the result. **No `COMMENT` on the
384
- slice, not now and not instead of asking** — the snippet in the chat is the only thing you post.
385
- 4. **The click arrives as a `CHAT` turn** (`Yes, change it – …` / `Apply changes – …` is a yes, `No, leave it
386
- – …` a no). On a yes, create the prompt for exactly the elements you asked about
387
- (`originMessageId` = the yes) and **write the confirmation into the prompt's text**: *"The person
388
- confirmed changing the locked slice 'Place order' (status Done): …"*. A prompt turn whose text carries
389
- that confirmation, with `origin_message_id` pointing at the yes, may change those elements — and only
390
- those; any other locked slice still needs its own yes. On a no, acknowledge in a line and drop it.
391
- Not clearly a yes? Ask again, don't change it.
392
-
393
347
  ## Standalone board-change turns — see `CLAUDE-STANDALONE.md`
394
348
 
395
349
  These turns start with `BOARD_CHANGE` (the board changed) or `BOARD_REVIEW` (nothing has changed