@eventmodelers/cli 1.0.153 → 1.0.154
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 +35 -1
- package/cli.js +51 -8
- package/package.json +1 -1
- package/shared/build-kit/lib/adapters/pocketbase-realtime-adapter.js +46 -20
- package/shared/build-kit/lib/ralph.js +31 -8
- package/shared/skills/connect/SKILL.md +1 -1
- package/shared/skills/learn-eventmodelers-api/SKILL.md +10 -4
- package/shared/skills/ui-contract/SKILL.md +151 -0
- package/shared/skills/update-slice-status/SKILL.md +3 -1
- package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +6 -0
- package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +6 -0
- package/stacks/axon/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +2 -2
- package/stacks/blank/templates/.claude/skills/build-state-change/SKILL.md +12 -0
- package/stacks/blank/templates/.claude/skills/build-state-view/SKILL.md +12 -0
- package/stacks/blank/templates/build-kit/CLAUDE.md +8 -1
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +2 -2
- package/stacks/classic/templates/.claude/skills/build-state-change/SKILL.md +6 -0
- package/stacks/classic/templates/.claude/skills/build-state-view/SKILL.md +6 -0
- package/stacks/classic/templates/build-kit/lib/backend-prompt.md +2 -2
- package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +6 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +6 -0
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +4 -4
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +8 -0
- package/stacks/node/templates/.claude/skills/build-state-change/references/ui-contract-from-openapi.md +17 -0
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +8 -0
- package/stacks/node/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +2 -2
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +6 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +6 -0
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +2 -2
- package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +5 -0
- package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +5 -0
- package/stacks/react/templates/build-kit/lib/AGENT.md +1 -1
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +11 -11
- package/stacks/react/templates/build-kit/lib/kit.json +3 -0
- package/stacks/react/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +8 -0
- package/stacks/supabase/templates/.claude/skills/build-state-change/references/ui-contract-from-openapi.md +17 -0
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +8 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +2 -2
- package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +10 -1
- package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +4 -0
- package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +1 -1
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +11 -11
- package/stacks/supabase-react/templates/build-kit/lib/kit.json +3 -0
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +2 -2
- package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +6 -0
- package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +6 -0
- package/stacks/umadb/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +2 -2
package/README.md
CHANGED
|
@@ -155,6 +155,8 @@ npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude
|
|
|
155
155
|
npx @eventmodelers/cli run --standalone # same, plus acting on board changes unprompted — and needs no install at all
|
|
156
156
|
npx @eventmodelers/cli run --standalone --board-id <uuid> # …from any directory, against any board (see Power users)
|
|
157
157
|
npx @eventmodelers/cli run --id <id> --name <label> # override this run's agent identity (id + display name); `init --name` persists a name instead
|
|
158
|
+
npx @eventmodelers/cli run --handover # backend kit, this run only: finish slices that have a UI at `Handover` instead of `Done` (see Backend → UI handover)
|
|
159
|
+
npx @eventmodelers/cli config --handover on|off # persist that choice for the project (`handover` in .eventmodelers/config.json)
|
|
158
160
|
npx @eventmodelers/cli fetch --context <name> # pull full slice detail for one context on the board into <kit-dir>/.slices/
|
|
159
161
|
npx @eventmodelers/cli fetch --context <name> --slice-id <id> # same, then print just that slice
|
|
160
162
|
npx @eventmodelers/cli fetch --context <name> --slice-title <title> # same, then print just the slice matching this title
|
|
@@ -198,6 +200,10 @@ npx @eventmodelers/cli run --local
|
|
|
198
200
|
|
|
199
201
|
and the agent starts building — `--local` skips platform config and credential lookup entirely, so it works straight out of `init` with nothing connected. Every TODO you fill in gets exercised on the next iteration against real slice data covering all four slice types (state change, state view, automation, translation), which is exactly the coverage a new stack's `build-*` skills need before it's worth promoting.
|
|
200
202
|
|
|
203
|
+
**Backend → UI handover.** Besides the usual statuses a slice can be `Ready` (modeling done, waiting to be planned) and `Handover` (backend built, UI contract published, waiting for the UI kit). The flow is `Ready → Planned → InProgress → Handover → InProgress → Done`. Install with `init --handover` (or `re-init --handover`; switch later with `eventmodelers config --handover on|off`; for a single run `run --handover` overrides the setting without saving anything), which saves `"handover": true` in `.eventmodelers/config.json` — then a backend kit ends STATE_VIEW / STATE_CHANGE slices at `Handover` instead of `Done` (the `ui-contract` skill, Step 4, owns that rule); without the flag nothing changes: slices end at `Done`, exactly as before. A kit claims work from the status named in its `lib/kit.json` — `{ "pickupStatus": "Handover" }` for the `react` / `supabase-react` UI kits, absent (= `Planned`) for backend kits. A new UI kit ships that file and sets `Done` when its half is built; a new backend kit just invokes `ui-contract`. Needs a platform with the `Ready` / `Handover` statuses.
|
|
204
|
+
|
|
205
|
+
**UI contract.** The scaffolded `build-state-view` / `build-state-change` skills already end by invoking the shared `ui-contract` skill, which publishes each slice's read (`GET`) or write (`POST`) contract as a `## UI Contract` section in the slice's feedback-lane markdown node (exported in `slice.json` `notes[]`, so a UI build kit can look it up directly). Two things are yours to decide: *where the contract is derived from* (point the `UI Contract` section of those skills at your stack's machine-readable API description, if it has one — the `node`/`supabase` stacks use their `@openapi` blocks via a `ui-contract-from-openapi.md` reference — otherwise the skill reads the route code), and, if you need a different shape or location, a whole-skill override: ship `.claude/skills/ui-contract/SKILL.md` in the stack's templates and it replaces the shared one (stack templates are applied after `shared/skills/`).
|
|
206
|
+
|
|
201
207
|
Installing both a build stack and `init-modeling` into the same project reuses this one `.eventmodelers/config.json` — run whichever `init` command second and it finds the existing config already satisfies the required fields and skips straight past the credential prompt.
|
|
202
208
|
|
|
203
209
|
### The modeling agent — `run --modeling` and `--standalone`
|
|
@@ -649,6 +655,34 @@ Only prompts carrying this agent's id are worked. Anything untargeted is handed
|
|
|
649
655
|
|
|
650
656
|
Pair it with `--id`: a `--standalone`/`--global` run mints a fresh id per run, so prompts addressed to the previous run's id are never claimed. `--exclusive` applies to the prompt queue only — a `--standalone` agent's self-directed turns are nobody's prompt, and it keeps taking them.
|
|
651
657
|
|
|
658
|
+
### Backend → UI handover (`--handover`)
|
|
659
|
+
|
|
660
|
+
A slice with a UI is only finished once two kits have each built their half: the backend kit builds the endpoint and publishes its contract, the UI kit builds the frontend against it. `--handover` makes the first kit stop at `Handover` instead of `Done`, so the second one can claim the slice.
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
npx @eventmodelers/cli init --stack node --handover # persisted: "handover": true in .eventmodelers/config.json (also re-init)
|
|
664
|
+
npx @eventmodelers/cli config --handover on # switch it on later…
|
|
665
|
+
npx @eventmodelers/cli config --handover off # …or back off
|
|
666
|
+
npx @eventmodelers/cli run --handover # this run only — writes nothing
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
| | Handover off (default) | Handover on |
|
|
670
|
+
|---|---|---|
|
|
671
|
+
| Backend kit, STATE_VIEW / STATE_CHANGE slice | ends at `Done` | publishes the UI contract, ends at `Handover` |
|
|
672
|
+
| Backend kit, automation / translation | `Done` | `Done` — no UI counterpart |
|
|
673
|
+
| UI kit (`react`, `supabase-react`) | claims `Handover` only | claims `Handover` only, sets `Done` when its UI is built |
|
|
674
|
+
|
|
675
|
+
Flow: `Ready → Planned → InProgress → Handover → InProgress → Done`. Precedence for a run is `run --handover` over the project's `handover` setting; the runner states the effective value to the agent as a `Handover: on|off` line in its task, so the agent never has to work it out from `config.json`. `config --handover` writes to the project's config (the installed kit's project, else the nearest `.eventmodelers/config.json` up the tree — never the account-wide one in `~/`).
|
|
676
|
+
|
|
677
|
+
**What to know before relying on it**
|
|
678
|
+
|
|
679
|
+
- **UI kits only ever claim `Handover`.** Against a board whose backend finished slices at `Done`, a `react` / `supabase-react` kit has nothing to do and replies `NO_TASKS` — the backend side must run with handover on.
|
|
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
|
+
- **The platform must know the `Ready` / `Handover` statuses.**
|
|
682
|
+
- A kit declares what it claims in `lib/kit.json` — `{ "pickupStatus": "Handover" }`; absent means `Planned`.
|
|
683
|
+
|
|
684
|
+
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
|
+
|
|
652
686
|
### Env vars and `--config` (scripted/CI installs)
|
|
653
687
|
|
|
654
688
|
Every config field can be set via an `EVENTMODELERS_*` env var instead of the interactive prompts — these always win over whatever's in `config.json`, so a fully env-driven install never prompts for credentials or Claude execution settings:
|
|
@@ -759,7 +793,7 @@ npx @eventmodelers/cli uninstall --modeling-kit # remove .agent-modeling-ki
|
|
|
759
793
|
|
|
760
794
|
### Adding a stack
|
|
761
795
|
|
|
762
|
-
Each stack lives under `stacks/<name>/templates/` with `.claude/` (skills), `root/` (spread into the project root), and either `build-kit/` (backend stacks) or `kit/` (modeling-only) for the agent runner. Files identical across all backend stacks live once in `shared/build-kit/` and get layered in automatically — only put stack-specific overrides under `stacks/<name>/templates/build-kit/`. Skills with no stack-specific content (`connect`, `learn-eventmodelers-api`, `update-slice-status`, `request-feedback`) work the same way via `shared/skills/` — a new stack gets them for free without copying anything; add a skill there only once it needs a stack-specific fork.
|
|
796
|
+
Each stack lives under `stacks/<name>/templates/` with `.claude/` (skills), `root/` (spread into the project root), and either `build-kit/` (backend stacks) or `kit/` (modeling-only) for the agent runner. Files identical across all backend stacks live once in `shared/build-kit/` and get layered in automatically — only put stack-specific overrides under `stacks/<name>/templates/build-kit/`. Skills with no stack-specific content (`connect`, `learn-eventmodelers-api`, `update-slice-status`, `request-feedback`, `ui-contract`) work the same way via `shared/skills/` — a new stack gets them for free without copying anything; add a skill there only once it needs a stack-specific fork. A stack overrides a shared skill by shipping one of the same name under its own `.claude/skills/` (e.g. a custom `ui-contract`).
|
|
763
797
|
|
|
764
798
|
The demo model `init --demo` installs is shared the same way, in `shared/demo-slices/` — a verbatim `fetch --format json` tree (`current_context.json` plus `<context>/{context,index,config}.json` and `<slice>/slice.json`), copied as-is into the kit's `.slices/`. It's stack-agnostic board data, so a new stack gets `--demo` for free. To replace or extend it, `fetch` a context into an empty directory and copy the result in, then update `DEMO_CONTEXT_NAME`/`DEMO_SLICE_COUNT` in `cli.js` (used only for the line `init` prints).
|
|
765
799
|
|
package/cli.js
CHANGED
|
@@ -710,6 +710,18 @@ function findConfigInParents(startDir) {
|
|
|
710
710
|
return existsSync(globalCandidate) ? globalCandidate : null;
|
|
711
711
|
}
|
|
712
712
|
|
|
713
|
+
// Where a project-level setting (`config --handover`, `--agent-tracing`) is written: the explicit
|
|
714
|
+
// --config, else the installed kit's project, else the nearest project config up the tree — so it
|
|
715
|
+
// lands in the file `run` merges in, even from a subdirectory — else this directory. The
|
|
716
|
+
// account-wide ~/.eventmodelers/config.json is never a target: these are per-project policies.
|
|
717
|
+
function projectConfigPath(cwd, kitDir, explicitConfig) {
|
|
718
|
+
if (explicitConfig) return resolve(explicitConfig);
|
|
719
|
+
if (kitDir) return join(dirname(kitDir), '.eventmodelers', 'config.json');
|
|
720
|
+
const found = findConfigInParents(cwd);
|
|
721
|
+
const globalPath = join(homedir(), '.eventmodelers', 'config.json');
|
|
722
|
+
return found && found !== globalPath ? found : join(cwd, '.eventmodelers', 'config.json');
|
|
723
|
+
}
|
|
724
|
+
|
|
713
725
|
function readJsonSafe(path) {
|
|
714
726
|
if (!path || !existsSync(path)) return {};
|
|
715
727
|
try {
|
|
@@ -983,9 +995,11 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
983
995
|
copyDirContents(claudeSkillsSrc, globalSkillsDir);
|
|
984
996
|
} else {
|
|
985
997
|
console.log('📦 Installing skills...');
|
|
986
|
-
|
|
998
|
+
// Shared skills first, the stack's own templates last — so a build kit can override
|
|
999
|
+
// a shared skill (e.g. `ui-contract`) just by shipping a skill of the same name.
|
|
987
1000
|
copyDirContents(sharedSkills, join(targetDir, '.claude', 'skills'));
|
|
988
1001
|
if (isBridge) copyDirContents(sharedBridgeSkills, join(targetDir, '.claude', 'skills'));
|
|
1002
|
+
copyDirContents(join(templatesSource, '.claude'), join(targetDir, '.claude'));
|
|
989
1003
|
claudeExtras = existsSync(join(templatesSource, '.claude'))
|
|
990
1004
|
? readdirSync(join(templatesSource, '.claude')).filter((f) => f !== 'skills')
|
|
991
1005
|
: [];
|
|
@@ -1357,6 +1371,10 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
|
|
|
1357
1371
|
if (!skipWrite) {
|
|
1358
1372
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1359
1373
|
console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
|
|
1374
|
+
} else if (overrides.handover) {
|
|
1375
|
+
// A policy flag, not a credential — it must stick even when credentials were skipped.
|
|
1376
|
+
writeFileSync(configPath, JSON.stringify({ ...readJsonSafe(configPath), handover: true }, null, 2));
|
|
1377
|
+
console.log(` ✓ Handover on — saved to ${relative(targetDir, configPath)}`);
|
|
1360
1378
|
}
|
|
1361
1379
|
return config;
|
|
1362
1380
|
}
|
|
@@ -3241,9 +3259,19 @@ const AGENT_NAME_OPTION = [
|
|
|
3241
3259
|
'Human-readable name for the agent this kit runs (e.g. "ci-builder", "martins-laptop"), saved to config.json as `agentName` and sent with every heartbeat so the board shows a name instead of a bare uuid. Also settable via EVENTMODELERS_AGENT_NAME, and overridable for a single run with `run --name`.',
|
|
3242
3260
|
];
|
|
3243
3261
|
|
|
3262
|
+
// `--handover` is the project policy for how a backend build kit finishes a slice with a UI:
|
|
3263
|
+
// set, it ends STATE_VIEW / STATE_CHANGE slices at `Handover` (the UI kit builds the rest and sets
|
|
3264
|
+
// `Done`); unset, it ends at `Done`, exactly as before. Persisted as `handover: true` in the project's
|
|
3265
|
+
// config.json — the `ui-contract` skill (Step 4) reads it there. Switch it later with
|
|
3266
|
+
// `eventmodelers config --handover on|off`.
|
|
3267
|
+
const HANDOVER_OPTION = [
|
|
3268
|
+
'--handover',
|
|
3269
|
+
'Backend build kit: finish slices that have a UI at status `Handover` (UI build kit takes over, then sets `Done`) instead of `Done`. Saved as `handover: true` in .eventmodelers/config.json; without it slices end at `Done`. Turn it off later with `eventmodelers config --handover off`.',
|
|
3270
|
+
];
|
|
3271
|
+
|
|
3244
3272
|
function identityOverridesFromOpts(opts) {
|
|
3245
3273
|
const agentName = typeof opts.name === 'string' ? opts.name.trim() : undefined;
|
|
3246
|
-
return agentName ? { agentName } : {};
|
|
3274
|
+
return { ...(agentName ? { agentName } : {}), ...(opts.handover ? { handover: true } : {}) };
|
|
3247
3275
|
}
|
|
3248
3276
|
|
|
3249
3277
|
credentialFlags(program
|
|
@@ -3262,7 +3290,8 @@ credentialFlags(program
|
|
|
3262
3290
|
.option('--demo', 'Install a ready-made demo model into the kit\'s .slices/ — the "Understanding Eventsourcing" context (16 shopping-cart slices covering every slice type), in exactly the shape `fetch` writes, so the build skills and the agent loop have something real to work on before this project is connected to a board. Skipped if .slices/ already holds fetched slices. Off by default.')
|
|
3263
3291
|
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
|
|
3264
3292
|
.option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json')
|
|
3265
|
-
.option(...AGENT_NAME_OPTION)
|
|
3293
|
+
.option(...AGENT_NAME_OPTION)
|
|
3294
|
+
.option(...HANDOVER_OPTION))
|
|
3266
3295
|
.action(async (opts, command) => {
|
|
3267
3296
|
const globalOpts = command.optsWithGlobals();
|
|
3268
3297
|
|
|
@@ -3384,7 +3413,8 @@ credentialFlags(program
|
|
|
3384
3413
|
.option('--hooks', 'Install the slice commit-scope guard (.githooks/pre-commit) and wire it up via `git config core.hooksPath .githooks` — same as `init --hooks`, for turning it on after the fact without a full re-scaffold. Off by default.')
|
|
3385
3414
|
.option('--global', 'Re-install skills into ~/.claude/skills/ instead of the project — defaults to however they were originally installed')
|
|
3386
3415
|
.option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json')
|
|
3387
|
-
.option(...AGENT_NAME_OPTION)
|
|
3416
|
+
.option(...AGENT_NAME_OPTION)
|
|
3417
|
+
.option(...HANDOVER_OPTION))
|
|
3388
3418
|
.action(async (opts, command) => {
|
|
3389
3419
|
const globalOpts = command.optsWithGlobals();
|
|
3390
3420
|
const targetDir = process.cwd();
|
|
@@ -3686,6 +3716,7 @@ credentialFlags(program
|
|
|
3686
3716
|
.option('--local', 'Skip platform config/credential lookup entirely and run the local-only loop (no board sync, no realtime agent) — even if .eventmodelers/config.json has credentials (build-kit stacks only)')
|
|
3687
3717
|
.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.')
|
|
3688
3718
|
.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.')
|
|
3719
|
+
.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`.')
|
|
3689
3720
|
.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.')
|
|
3690
3721
|
.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.')
|
|
3691
3722
|
.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.')
|
|
@@ -3956,7 +3987,7 @@ credentialFlags(program
|
|
|
3956
3987
|
// local-only branch even when .eventmodelers/config.json has valid credentials.
|
|
3957
3988
|
// RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
|
|
3958
3989
|
// they reach both node runners but not ralph.sh, which has no heartbeat to identify.
|
|
3959
|
-
execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '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 } : {}) } });
|
|
3990
|
+
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 } : {}) } });
|
|
3960
3991
|
} catch (err) {
|
|
3961
3992
|
process.exit(err.status || 1);
|
|
3962
3993
|
}
|
|
@@ -4174,7 +4205,9 @@ const SLICE_STATUSES = [
|
|
|
4174
4205
|
{ label: '🔄 InProgress', value: 'InProgress' },
|
|
4175
4206
|
{ label: '🔍 Review', value: 'Review' },
|
|
4176
4207
|
{ label: '🚫 Blocked', value: 'Blocked' },
|
|
4208
|
+
{ label: '🧭 Ready', value: 'Ready' },
|
|
4177
4209
|
{ label: '📅 Planned', value: 'Planned' },
|
|
4210
|
+
{ label: '🤝 Handover', value: 'Handover' },
|
|
4178
4211
|
{ label: 'ℹ️ Informational', value: 'Informational' },
|
|
4179
4212
|
];
|
|
4180
4213
|
|
|
@@ -4504,21 +4537,31 @@ program
|
|
|
4504
4537
|
program
|
|
4505
4538
|
.command('config')
|
|
4506
4539
|
.description('Print the fully resolved config (merged across the directory hierarchy + EVENTMODELERS_* env vars), with the token masked')
|
|
4540
|
+
.option('--handover <on|off>', "Turn the backend→UI handover on or off for this project (saved as `handover` in the project's .eventmodelers/config.json). On: a backend build kit ends slices that have a UI at `Handover`; off: at `Done`.")
|
|
4507
4541
|
.option('--agent-tracing <on|off>', "Turn agent tracing on or off for this project (saved as `agentTracing` in the project's .eventmodelers/config.json). Off: the build agent sends no slice cost data to the platform at all. A non-Claude agent via run --exec is only traced when its command includes the harness's JSON output flag, e.g. \"codex exec --json --full-auto\".")
|
|
4508
4542
|
.action((opts, command) => {
|
|
4509
4543
|
const cwd = process.cwd();
|
|
4510
4544
|
const kitDir = findInstalledKitDir(cwd);
|
|
4511
4545
|
const explicitConfig = command.optsWithGlobals().config;
|
|
4512
4546
|
|
|
4547
|
+
if (opts.handover !== undefined) {
|
|
4548
|
+
const value = String(opts.handover).toLowerCase();
|
|
4549
|
+
if (!['on', 'off'].includes(value)) {
|
|
4550
|
+
console.error('❌ --handover takes "on" or "off"');
|
|
4551
|
+
process.exit(1);
|
|
4552
|
+
}
|
|
4553
|
+
const configPath = projectConfigPath(cwd, kitDir, explicitConfig);
|
|
4554
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
4555
|
+
writeFileSync(configPath, JSON.stringify({ ...readJsonSafe(configPath), handover: value === 'on' }, null, 2));
|
|
4556
|
+
console.log(`✓ Handover ${value} — saved to ${relative(cwd, configPath) || configPath}\n`);
|
|
4557
|
+
}
|
|
4513
4558
|
if (opts.agentTracing !== undefined) {
|
|
4514
4559
|
const value = String(opts.agentTracing).toLowerCase();
|
|
4515
4560
|
if (!['on', 'off'].includes(value)) {
|
|
4516
4561
|
console.error('❌ --agent-tracing takes "on" or "off"');
|
|
4517
4562
|
process.exit(1);
|
|
4518
4563
|
}
|
|
4519
|
-
const configPath = explicitConfig
|
|
4520
|
-
? resolve(explicitConfig)
|
|
4521
|
-
: join(kitDir ? dirname(kitDir) : cwd, '.eventmodelers', 'config.json');
|
|
4564
|
+
const configPath = projectConfigPath(cwd, kitDir, explicitConfig);
|
|
4522
4565
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
4523
4566
|
writeFileSync(configPath, JSON.stringify({ ...readJsonSafe(configPath), agentTracing: value === 'on' }, null, 2));
|
|
4524
4567
|
console.log(`✓ Agent tracing ${value} — saved to ${relative(cwd, configPath) || configPath}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.154",
|
|
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": {
|
|
@@ -24,33 +24,59 @@ export async function createPocketBaseRealtimeAdapter(cfg, initialToken, deps =
|
|
|
24
24
|
const pb = new PocketBase(cfg.pocketbaseUrl);
|
|
25
25
|
pb.authStore.save(initialToken, null);
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// The SDK adds one EventSource listener per subscribe() call — and keeps it when the call
|
|
28
|
+
// fails — so a flaky connection retrying per topic piles listeners up (Node warns at 11).
|
|
29
|
+
// Hence a single SDK subscription for the whole adapter, fanned out to topics here.
|
|
30
|
+
// subscribe() replaces the topic's previous handlers (see realtime-adapter.js).
|
|
31
|
+
const topics = new Map();
|
|
32
|
+
let sdkUnsubscribe = null;
|
|
33
|
+
let connecting = null;
|
|
34
|
+
|
|
35
|
+
const connect = async () => {
|
|
36
|
+
for (let attempt = 0; ; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
sdkUnsubscribe = await pb.collection(REALTIME_EVENTS_COLLECTION).subscribe('*', (e) => {
|
|
39
|
+
if (e.action !== 'create') return;
|
|
40
|
+
const entry = topics.get(e.record.topic);
|
|
41
|
+
entry?.handlers[e.record.event]?.(e.record.payload);
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Drop the listener the failed call left behind before trying again.
|
|
46
|
+
await pb.collection(REALTIME_EVENTS_COLLECTION).unsubscribe('*').catch(() => {});
|
|
47
|
+
if (attempt >= RECONNECT_INTERVALS_MS.length) throw err;
|
|
48
|
+
topics.forEach((t) => t.onStatus?.(`RECONNECTING (attempt ${attempt + 1}/${RECONNECT_INTERVALS_MS.length}): ${err.message}`));
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, RECONNECT_INTERVALS_MS[attempt]));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Concurrent subscribe() calls share one in-flight connect — sdkUnsubscribe is only set
|
|
55
|
+
// after the await, so without this each would open its own SDK subscription.
|
|
56
|
+
const ensureSdkSubscription = () => {
|
|
57
|
+
if (sdkUnsubscribe) return Promise.resolve();
|
|
58
|
+
connecting ??= connect().finally(() => { connecting = null; });
|
|
59
|
+
return connecting;
|
|
60
|
+
};
|
|
61
|
+
|
|
29
62
|
const remove = async (topic) => {
|
|
30
|
-
|
|
31
|
-
if (!
|
|
32
|
-
|
|
63
|
+
topics.delete(topic);
|
|
64
|
+
if (topics.size || !sdkUnsubscribe) return;
|
|
65
|
+
const unsubscribe = sdkUnsubscribe;
|
|
66
|
+
sdkUnsubscribe = null;
|
|
33
67
|
await unsubscribe().catch(() => {});
|
|
34
68
|
};
|
|
35
69
|
|
|
36
70
|
return {
|
|
37
71
|
async subscribe(topic, handlers, onStatus) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
listeners.set(topic, unsubscribe);
|
|
46
|
-
onStatus?.('SUBSCRIBED');
|
|
47
|
-
return;
|
|
48
|
-
} catch (err) {
|
|
49
|
-
if (attempt >= RECONNECT_INTERVALS_MS.length) throw err;
|
|
50
|
-
onStatus?.(`RECONNECTING (attempt ${attempt + 1}/${RECONNECT_INTERVALS_MS.length}): ${err.message}`);
|
|
51
|
-
await new Promise((resolve) => setTimeout(resolve, RECONNECT_INTERVALS_MS[attempt]));
|
|
52
|
-
}
|
|
72
|
+
topics.set(topic, { handlers, onStatus });
|
|
73
|
+
try {
|
|
74
|
+
await ensureSdkSubscription();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
topics.delete(topic);
|
|
77
|
+
throw err;
|
|
53
78
|
}
|
|
79
|
+
onStatus?.('SUBSCRIBED');
|
|
54
80
|
},
|
|
55
81
|
unsubscribe(topic) {
|
|
56
82
|
return remove(topic);
|
|
@@ -242,7 +242,8 @@ async function fetchAndPersistSlices(cfg, kitDir) {
|
|
|
242
242
|
if (!activeCtx || !contexts[activeCtx]) {
|
|
243
243
|
// First run (or the current context disappeared): seed with a context that
|
|
244
244
|
// has planned work, else the first one. This is the ONLY place we choose it.
|
|
245
|
-
const
|
|
245
|
+
const pickup = readPickupStatus(kitDir).toLowerCase();
|
|
246
|
+
const plannedCtx = Object.keys(contexts).find(c => contexts[c].slices.some(s => (s.status || '').toLowerCase() === pickup));
|
|
246
247
|
activeCtx = plannedCtx || Object.keys(contexts)[0] || 'default';
|
|
247
248
|
writeFileSync(ctxPath, JSON.stringify({ name: activeCtx }, null, 2), 'utf-8');
|
|
248
249
|
}
|
|
@@ -400,11 +401,12 @@ async function handleSliceChanged(payload, cfg, kitDir, queueAllStatuses) {
|
|
|
400
401
|
await fetchAndPersistSlices(cfg, kitDir).catch((err) =>
|
|
401
402
|
console.error('[agent] Slice persist error:', err),
|
|
402
403
|
);
|
|
403
|
-
//
|
|
404
|
+
// Slices in the kit's pickup status (Planned, or Handover for UI kits) are handled by
|
|
405
|
+
// onPlannedSlice directly — no task needed.
|
|
404
406
|
// queueAllStatuses opts out of that split entirely (e.g. bridge has no
|
|
405
407
|
// onPlannedSlice consumer, so a lingering Planned slice would otherwise
|
|
406
408
|
// never naturally clear its own trigger — see lib/ralph.js callers).
|
|
407
|
-
if (queueAllStatuses || (payload.sliceStatus || '').toLowerCase() !==
|
|
409
|
+
if (queueAllStatuses || (payload.sliceStatus || '').toLowerCase() !== readPickupStatus(kitDir).toLowerCase()) {
|
|
408
410
|
await writeTask(payload, kitDir).catch((err) => console.error('[agent] writeTask error:', err));
|
|
409
411
|
}
|
|
410
412
|
}
|
|
@@ -513,6 +515,17 @@ function hasPendingTasks(kitDir) {
|
|
|
513
515
|
return readTasks(join(kitDir, 'tasks.json')).length > 0;
|
|
514
516
|
}
|
|
515
517
|
|
|
518
|
+
// The slice status this kit claims work from. A stack declares it in `lib/kit.json`
|
|
519
|
+
// ({ "pickupStatus": "Handover" } for the UI kits, which build what a backend kit handed over);
|
|
520
|
+
// absent means "Planned", the backend default.
|
|
521
|
+
export function readPickupStatus(kitDir) {
|
|
522
|
+
try {
|
|
523
|
+
const s = JSON.parse(readFileSync(join(kitDir, 'lib', 'kit.json'), 'utf-8')).pickupStatus;
|
|
524
|
+
if (typeof s === 'string' && s.trim()) return s.trim();
|
|
525
|
+
} catch {}
|
|
526
|
+
return 'Planned';
|
|
527
|
+
}
|
|
528
|
+
|
|
516
529
|
function readCurrentContext(kitDir) {
|
|
517
530
|
const ctxPath = join(kitDir, '.slices', 'current_context.json');
|
|
518
531
|
if (!existsSync(ctxPath)) return null;
|
|
@@ -541,13 +554,14 @@ function getFirstPlannedSlice(kitDir) {
|
|
|
541
554
|
try {
|
|
542
555
|
others = readdirSync(slicesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
543
556
|
} catch {}
|
|
557
|
+
const pickup = readPickupStatus(kitDir).toLowerCase();
|
|
544
558
|
const candidates = [...(currentCtx ? [currentCtx] : []), ...others.filter((c) => c !== currentCtx)];
|
|
545
559
|
for (const ctx of candidates) {
|
|
546
560
|
const indexPath = join(slicesDir, ctx, 'index.json');
|
|
547
561
|
if (!existsSync(indexPath)) continue;
|
|
548
562
|
try {
|
|
549
563
|
const { slices } = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
550
|
-
const planned = slices && slices.find((s) => (s.status || '').toLowerCase() ===
|
|
564
|
+
const planned = slices && slices.find((s) => (s.status || '').toLowerCase() === pickup);
|
|
551
565
|
if (!planned) continue;
|
|
552
566
|
if (ctx !== currentCtx) {
|
|
553
567
|
writeFileSync(join(slicesDir, 'current_context.json'), JSON.stringify({ name: ctx }, null, 2), 'utf-8');
|
|
@@ -673,11 +687,20 @@ async function runWithRetry(label, fn) {
|
|
|
673
687
|
// alone, the agent read it as background, answered "what would you like me to help with?" with
|
|
674
688
|
// no tool calls, and a Planned slice stayed Planned until the stuck-slice guard blocked it. So
|
|
675
689
|
// every turn ends with the concrete request — here, once, for every runner.
|
|
676
|
-
|
|
690
|
+
// Whether a backend kit finishes UI-bearing slices at `Handover` instead of `Done`: this run's
|
|
691
|
+
// `run --handover` (RALPH_HANDOVER) over the project's `handover` setting in config.json.
|
|
692
|
+
export function resolveHandover(cfg) {
|
|
693
|
+
return process.env.RALPH_HANDOVER === '1' || cfg?.handover === true;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function taskRequest(planned = null, cfg = null, pickupStatus = 'Planned') {
|
|
677
697
|
const task = planned
|
|
678
|
-
? `build the
|
|
698
|
+
? `build the ${pickupStatus} slice "${planned.title}" (id=${planned.id}, context="${planned.ctx}")`
|
|
679
699
|
: 'process the pending tasks in tasks.json';
|
|
680
|
-
|
|
700
|
+
// Stated explicitly so the agent never has to work it out from config.json: this run's effective
|
|
701
|
+
// value, which is what the `ui-contract` skill (Step 4) follows. UI kits (pickup Handover) ignore it.
|
|
702
|
+
const handover = planned && pickupStatus === 'Planned' ? `\nHandover: ${resolveHandover(cfg) ? 'on' : 'off'}\n` : '';
|
|
703
|
+
return `\n\n---\n\nYour task now: ${task}, following the instructions above. Start immediately.\n${handover}`;
|
|
681
704
|
}
|
|
682
705
|
|
|
683
706
|
async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false) {
|
|
@@ -718,7 +741,7 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false)
|
|
|
718
741
|
continue;
|
|
719
742
|
}
|
|
720
743
|
|
|
721
|
-
const prompt = readFileSync(backendPromptFile, 'utf-8') + taskRequest(planned);
|
|
744
|
+
const prompt = readFileSync(backendPromptFile, 'utf-8') + taskRequest(planned, cfg, readPickupStatus(kitDir));
|
|
722
745
|
if (credentialed) await fetchFullSlice(cfg, kitDir, planned.id);
|
|
723
746
|
lastReply = null;
|
|
724
747
|
const built = await runWithRetry(`onPlannedSlice: building slice "${planned.title}"...`, async () => { lastReply = await onPlannedSlice(prompt, {
|
|
@@ -289,7 +289,7 @@ Connecting is also where the session's read discipline starts. Every skill that
|
|
|
289
289
|
|
|
290
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
291
|
|
|
292
|
-
**Only a slice in `Created` may be written to.** Any other status — `Planned`, `Assigned`, `InProgress`, `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.
|
|
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
293
|
|
|
294
294
|
Never spend a `list_slices`/`get_slice_data` call to answer this; the outline already did.
|
|
295
295
|
|
|
@@ -644,7 +644,7 @@ Create a complete slice (1 column + its nodes automatically placed).
|
|
|
644
644
|
interaction?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
|
|
645
645
|
swimlane?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
|
|
646
646
|
}
|
|
647
|
-
status?: 'Created' | 'Planned' | 'InProgress' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
|
|
647
|
+
status?: 'Created' | 'Ready' | 'Planned' | 'InProgress' | 'Handover' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
|
|
648
648
|
}
|
|
649
649
|
```
|
|
650
650
|
|
|
@@ -671,7 +671,7 @@ Create a standalone SLICE_BORDER node spanning an **existing** column. Unlike th
|
|
|
671
671
|
{
|
|
672
672
|
columnId: string // id of an existing column on this timeline
|
|
673
673
|
title: string // slice title — always taken from this field, never derived
|
|
674
|
-
status?: 'Created' | 'Planned' | 'InProgress' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
|
|
674
|
+
status?: 'Created' | 'Ready' | 'Planned' | 'InProgress' | 'Handover' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
|
|
675
675
|
data?: Record<string, unknown> // optional node.data payload
|
|
676
676
|
meta?: Record<string, unknown> // optional extra meta fields (type, colId, title are always set explicitly and cannot be overridden here)
|
|
677
677
|
}
|
|
@@ -845,7 +845,8 @@ How much each slice of one context has been reworked — read-only, derived from
|
|
|
845
845
|
for it.
|
|
846
846
|
|
|
847
847
|
A slice's rework is read from its own SLICE_BORDER history. Going backwards means a step down the progress order
|
|
848
|
-
(Created → Planned → Assigned → InProgress → Review → Done), or reaching `Blocked` from
|
|
848
|
+
(Created → Ready → Planned → Assigned → InProgress → Handover → Review → Done), or reaching `Blocked` from
|
|
849
|
+
`Handover`/`Review`/`Done`;
|
|
849
850
|
`Informational` is never scored. `reopens` counts transitions leading away from `Done`, `changes` counts writes to the
|
|
850
851
|
slice itself. This says nothing about edits to the elements inside a slice.
|
|
851
852
|
|
|
@@ -854,6 +855,7 @@ context (the same rule `slicedata` uses), so one that inherits a context is repo
|
|
|
854
855
|
none is its own. Any other node type is rejected. Always scoped to one context; there is no board-wide form.
|
|
855
856
|
|
|
856
857
|
**Response**: `200` — `{ contextId, contextName, metrics, slices[] }`, slices most reworked first.
|
|
858
|
+
`metrics.progress` is where the slices stand now: `done`, `inProgress` (Assigned/InProgress/Review), `handover`, `ready`, `blocked`, `planned` (Planned/Created) as counts plus a `…Rate` share each, over the non-Informational slices, summing to 1.
|
|
857
859
|
`slices[]` = `{ sliceId, title, status, changes, reopens, regressions, everReachedDone }`.
|
|
858
860
|
`metrics` = `{ slices, everReachedDone, reopenedAfterDone, firstTimeRightRate, currentlyReopened, avgReopensPerSlice,
|
|
859
861
|
avgChangesPerSlice }` — `firstTimeRightRate` is the share of the slices that reached Done and never came back, or
|
|
@@ -1172,11 +1174,12 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
|
|
|
1172
1174
|
| to show several screens at once — a list, a gallery, a storyboard ("show me all screens of X") | `walkthrough`, one step per screen | nothing — selecting a step pans the canvas |
|
|
1173
1175
|
| to answer where one piece of information comes from and where it ends up ("where is the customer email used?") | `dataJourney` | nothing — element names pan the canvas |
|
|
1174
1176
|
| to show a diagram you drew yourself — a context map, a domain story, a sketch, any picture that isn't a chart | `svg` | nothing |
|
|
1177
|
+
| to show a UML or flow diagram you write as Mermaid — a sequence, flowchart, state, ER or class diagram | `mermaid` | nothing |
|
|
1175
1178
|
| to lay out rows and columns — a comparison, a matrix (stressors × elements), a short list with a few attributes | `table` | nothing — linked cells pan the canvas |
|
|
1176
1179
|
| to show a prototype you just built and uploaded (the `prototype` skill), running in the chat | `prototype` | nothing — **Full screen** opens the board's prototype player |
|
|
1177
1180
|
| something the customer defined their own snippet type for (a risk card, a sign-off — see `list_snippet_types`) | `custom` | the clicked action's `answer`; `context.snippetReply.then` says what to do |
|
|
1178
1181
|
|
|
1179
|
-
`confirm`, `poll`, `tasks` and `changes` are questions: a person answers once, then they show as answered. `link`, `image`, `code`, `report`, `walkthrough`, `screen`, `dataJourney`, `svg`, `table` and `prototype` are display only and never wait for anything.
|
|
1182
|
+
`confirm`, `poll`, `tasks` and `changes` are questions: a person answers once, then they show as answered. `link`, `image`, `code`, `report`, `walkthrough`, `screen`, `dataJourney`, `svg`, `mermaid`, `table` and `prototype` are display only and never wait for anything.
|
|
1180
1183
|
|
|
1181
1184
|
### Shapes
|
|
1182
1185
|
```typescript
|
|
@@ -1225,6 +1228,8 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
|
|
|
1225
1228
|
title?: string, caption?: string,
|
|
1226
1229
|
links?: Array<{ nodeId: string, x: number, y: number, // ≤ 200 clickable areas over the picture, in the svg's viewBox coordinates;
|
|
1227
1230
|
width: number, height: number, title?: string }> } // a click zooms the canvas to nodeId; title = tooltip (else the element's title)
|
|
1231
|
+
{ kind: 'mermaid', source: string, // Mermaid source, no ``` fence (≤ 60000 chars); the app draws it
|
|
1232
|
+
title?: string, caption?: string }
|
|
1228
1233
|
{ kind: 'table', title?: string, caption?: string,
|
|
1229
1234
|
columns: Array<string | { label: string, nodeId?: string, // 1–12; nodeId makes the header a link (a matrix with one column per element)
|
|
1230
1235
|
align?: 'left' | 'center' | 'right' }>,
|
|
@@ -1247,6 +1252,7 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
|
|
|
1247
1252
|
- **`dataJourney`** answers "where does X come from / where is X used?" for **one piece of information**, as a table: one stop per element it passes through, in flow order, each with its `role`, the field name there and the slice. A headline counts the roles ("Originates in 1 place, stored in 2 elements and used in 3 places"). Build it from the board — the `data-journey` skill (modeling kit) has the procedure; every `nodeId` must be a real id from this board, and a field the board doesn't carry is not a stop. Keep `text` to one line naming the data and where it originates.
|
|
1248
1253
|
- **`svg`** is for a picture you draw yourself: give the root a `viewBox` (and `width`/`height` for its natural size), inline colours and fonts — it is shown as an image, so scripts, `<a>` links and external resources do nothing. To make parts of it clickable, list them in `links`: a rectangle in the viewBox coordinates you drew in plus the element it stands for (only real ids from this board); the app lays a clickable area over each that zooms the canvas there. Prefer `report` for numbers. A domain story (who does what with which work object) is an `svg` you draw — the `domain-story` skill (modeling kit) has the procedure and the drawing style. Every SVG snippet has a download-as-PNG button.
|
|
1249
1254
|
- **"Place it on the board" after you drew an SVG always means: put that picture on the board as an image** — never rebuild it from elements (stickies, lanes, a timeline), never a SCREEN, never a new snippet. `place_image` with `imageBase64` = the SVG document itself, base64-encoded, `mimeType: "image/svg+xml"`, `title` = the snippet's title: it becomes an OUTLINE (the drawing tools' image element), free-floating at `x`/`y`, in no chapter and connected to nothing. Place it where the person says, else in free space next to the chapter it is about (`get_chapter_bounds`: right of or below it). Add a white background `<rect>` first if the SVG has none, or it is unreadable on a dark board. One SVG = one image. The image on the board has no clickable areas — `links` stay in the chat.
|
|
1255
|
+
- **`mermaid`** is for a diagram you can write as text: a sequence diagram (`sequenceDiagram`), a flow (`flowchart LR`), states (`stateDiagram-v2`), an ER or class diagram. Send just the source; the app draws it, with a *Show source* toggle and a copy button, and shows the source when it has a syntax error — so keep the syntax plain (quote labels with special characters). Prefer it over a `code` block for any diagram: a `code` block with `language: "mermaid"` is drawn too, but `mermaid` says what you mean. It has no element links; for clickable parts draw an `svg`.
|
|
1250
1256
|
- **`table`** is for anything that is rows and columns and not a chart: a comparison of options, an incidence matrix (rows = stressors, columns = elements, `true` where it hits), a short list with a few attributes per item. It computes nothing — totals are a row you add. Put element ids in `nodeId` (header or cell) so names are links; a row longer than the columns is cut, a shorter one padded, an empty one dropped. Prefer `report` for numbers you want *seen* as a trend or share, `dataJourney` for one piece of data's path.
|
|
1251
1257
|
- **`prototype`** runs a prototype in the chat: the page the platform stored for `timelineId` (the `contextId` the `prototype` skill uploaded under — any timeline of that context works too), in a sandboxed iframe with **Restart** (empty event log) and **Full screen** (the same player as the timeline's ▶). Send it only after `upload` succeeded — before that it shows "No prototype has been built for this yet". The `text` is the short report of what was built; the prototype itself is the snippet.
|
|
1252
1258
|
- A snippet is shown exactly as you send it — keep secrets and tokens out of `code` and `image`.
|