@eventmodelers/cli 0.0.27 → 0.0.29

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
@@ -68,7 +68,17 @@ npx @eventmodelers/cli init --bridge --target spec-kitty
68
68
  npx @eventmodelers/cli bridge
69
69
  ```
70
70
 
71
- `init --bridge` installs a `.bridge-kit/` (mirrors `.build-kit/`'s realtime + task-queue loop) plus only the skills for the chosen `--target` (`shared/bridge/<target>/`) — a `spec-kitty` bridge never installs Kiro's skills, and vice versa. `bridge` starts the loop: on every board slice change (not just "Planned", unlike build-kit), it re-runs `bridge-<target>-specify` to regenerate that framework's spec artifacts from the current `.slices/` export. It doesn't build code and doesn't claim slices. Pass `--ollama` for the local-Ollama runner instead of Claude (same caveat as build-kit's `--ollama`: `lib/ollama-agent.js` is shared as-is).
71
+ `init --bridge` installs a `.bridge-kit/` (mirrors `.build-kit/`'s realtime + task-queue loop) plus only the skills for the chosen `--target` (`shared/bridge/<target>/`) — a `spec-kitty` bridge never installs Kiro's skills, and vice versa. `bridge` starts the loop: on every board slice change (not just "Planned", unlike build-kit), it regenerates that framework's spec artifacts from the current board state. It doesn't build code and doesn't claim slices.
72
+
73
+ For `spec-kitty`, that sync is deterministic and stops well short of writing Spec Kitty's own artifacts — `lib/adapters/spec-kitty-adapter.js` fetches full slice detail and restates it as a plain markdown mission brief (one section per slice, its scenarios verbatim, nothing invented), then calls `spec-kitty intake --force` to install it at `.kittify/mission-brief.md`. It deliberately doesn't create the mission, write `spec.md`, or author work packages — Spec Kitty's own `/spec-kitty.specify` → `/spec-kitty.plan` → `/spec-kitty.tasks` pipeline does that, because those steps need real judgment (work package boundaries, which files a WP owns, which agent profile fits) that only makes sense with actual codebase context, which this adapter doesn't have. What it replaces is Spec Kitty's *interactive discovery interview*: `/spec-kitty.specify`'s own "Brief Context Detection" step reads `.kittify/mission-brief.md` when present and extracts requirements from it instead of asking the user, so the event model — not a live Q&A — becomes the input. No LLM call happens in this adapter's own path, and `bridge` picks it automatically whenever a target has one (`--claude` forces the Claude runner instead). Targets without a static adapter yet fall back to Claude re-running `bridge-<target>-specify`; pass `--ollama` for the local-Ollama runner instead (same caveat as build-kit's `--ollama`: `lib/ollama-agent.js` is shared as-is).
74
+
75
+ Don't want the standing loop at all? `fetch` can call the same adapter for a single one-shot sync, no `.bridge-kit/` install required:
76
+
77
+ ```bash
78
+ npx @eventmodelers/cli fetch --context Ticketing --spec-kitty
79
+ ```
80
+
81
+ Either way, `spec-kitty init` (Spec Kitty's own project setup) has to have already been run in the project root — the adapter checks for `.kittify/` first and stops with the exact command to run if it's missing, rather than failing deep inside a cryptic `spec-kitty` CLI error. After the sync, run `/spec-kitty.specify` in your coding agent to turn the brief into an actual mission (the plain `spec-kitty specify` CLI command only scaffolds — brief detection is in the agent-driven prompt).
72
82
 
73
83
  ### Overriding the executor with a hook
74
84
 
@@ -79,7 +89,7 @@ npx @eventmodelers/cli init --bridge --target spec-kitty --hook "git add .slices
79
89
  npx @eventmodelers/cli bridge
80
90
  ```
81
91
 
82
- `init --bridge --hook` persists the command to `.bridge-kit/bridge.json` — a plain, **committed** file (unlike `.eventmodelers/config.json`, which is gitignored for credentials) since the hook is project policy meant to be shared by every teammate and CI runner, not per-machine state. `bridge --hook "<command>"` overrides it for a single run without touching that file. Only one executor runs per invocation — `--ollama` and `--hook` are mutually exclusive.
92
+ `init --bridge --hook` persists the command to `.bridge-kit/bridge.json` — a plain, **committed** file (unlike `.eventmodelers/config.json`, which is gitignored for credentials) since the hook is project policy meant to be shared by every teammate and CI runner, not per-machine state. `bridge --hook "<command>"` overrides it for a single run without touching that file. Only one executor runs per invocation — `--ollama`, `--hook`, and `--claude` are mutually exclusive.
83
93
 
84
94
  The hook command runs with `BRIDGE_TASK_COUNT`, `BRIDGE_SLICE_ID`/`_TITLE`/`_STATUS` (the most recent change in the batch), and `BRIDGE_BATCH_FILE` (path to the full batch as JSON) in its environment. It's invoked once per batch, not once per slice — any change that arrives while the hook is still running is left queued for the next batch rather than dropped.
85
95
 
@@ -225,9 +235,9 @@ npx @eventmodelers/cli run --ollama # same, via local Ollama (ra
225
235
  npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
226
236
  npx @eventmodelers/cli listen # start the code-export listener (code-export.mjs) from the installed kit dir
227
237
  npx @eventmodelers/cli listen --port 4000 # same, on a different port
228
- npx @eventmodelers/cli fetch # pull full slice detail from every context on the board into <kit-dir>/.slices/ (project root if no kit, or a modeling-kit, is installed)
229
- npx @eventmodelers/cli fetch --slice-id <id> # same, then print just that slice
230
- npx @eventmodelers/cli fetch --slice-title <title> # same, then print just the slice matching this title
238
+ npx @eventmodelers/cli fetch --context <name> # pull full slice detail for one context on the board into <kit-dir>/.slices/ (project root if no kit, or a modeling-kit, is installed)
239
+ npx @eventmodelers/cli fetch --context <name> --slice-id <id> # same, then print just that slice
240
+ npx @eventmodelers/cli fetch --context <name> --slice-title <title> # same, then print just the slice matching this title
231
241
  npx @eventmodelers/cli stacks # list available stacks
232
242
  npx @eventmodelers/cli status # check what's installed
233
243
  npx @eventmodelers/cli config # print the fully resolved config (file + env), token masked
@@ -238,7 +248,7 @@ npx @eventmodelers/cli uninstall # remove everything init/ini
238
248
 
239
249
  `listen` is the same kind of dispatcher, but for `<kit-dir>/code-export.mjs` — a local HTTP server (port 3001 by default) that the eventmodelers board UI posts slice/screen data to, which then gets written under `<kit-dir>/.slices/`.
240
250
 
241
- `fetch` is the pull-based counterpart to `listen`: instead of waiting for the board UI to push data to a running listener, it lists every `MODEL_CONTEXT` node on the board, calls `slicedata?contextId=<id>` for each (full slice detail — commands/events/readmodels/screens/processors/specifications/comments), and writes the same `.slices/<context>/<slice>/slice.json`, `index.json`, and `context.json` layout — useful in CI or any context where nothing is listening on a port. It does not fetch screen images (those only arrive via `listen`'s push). Unlike every other command, `fetch` also works with no kit installed at all — it only needs credentials, not kit-specific files — and, unique to modeling-kit, writes `.slices/` to the project root instead of nesting it under `.agent-modeling-kit/`, since nothing reads it from there (modeling-kit has no `code-export.mjs`/`listen`). If credentials are missing, it prompts the same way `init-config` does. `--slice-id`/`--slice-title` still fetch and persist everything, then just print the one you asked about.
251
+ `fetch` is the pull-based counterpart to `listen`: instead of waiting for the board UI to push data to a running listener, it calls `slicedata?contextName=<name>` for the required `--context` (full slice detail — commands/events/readmodels/screens/processors/specifications/comments), and writes the same `.slices/<context>/<slice>/slice.json`, `index.json`, and `context.json` layout — useful in CI or any context where nothing is listening on a port. It does not fetch screen images (those only arrive via `listen`'s push). Unlike every other command, `fetch` also works with no kit installed at all — it only needs credentials, not kit-specific files — and, unique to modeling-kit, writes `.slices/` to the project root instead of nesting it under `.agent-modeling-kit/`, since nothing reads it from there (modeling-kit has no `code-export.mjs`/`listen`). If credentials are missing, it prompts the same way `init-config` does. `--slice-id`/`--slice-title` still fetch and persist the whole context, then just print the one slice you asked about.
242
252
 
243
253
  ### Uninstall
244
254
 
package/cli.js CHANGED
@@ -19,6 +19,7 @@ import { createInterface, emitKeypressEvents, moveCursor, clearScreenDown } from
19
19
  import { homedir } from 'os';
20
20
  import { randomUUID } from 'crypto';
21
21
  import { runFetch, FetchAuthError } from './lib/fetch.js';
22
+ import { run as runSpecKittyAdapter } from './lib/adapters/spec-kitty-adapter.js';
22
23
 
23
24
  const __filename = fileURLToPath(import.meta.url);
24
25
  const __dirname = dirname(__filename);
@@ -631,6 +632,14 @@ async function installStack(stackKey, stackCfg, options = {}) {
631
632
  }
632
633
  copyDirContents(join(templatesSource, stackCfg.kitSubdir), kitDir, { skip: ['.eventmodelers'] });
633
634
 
635
+ // Static (no-LLM) bridge adapters live once in this package's own lib/
636
+ // adapters/ — `fetch --spec-kitty` imports them directly, and a bridge
637
+ // install gets its own copy here so ralph-static.js can run standalone
638
+ // with no access back to the published package.
639
+ if (isBridge) {
640
+ copyDirContents(join(__dirname, 'lib', 'adapters'), join(kitDir, 'adapters'));
641
+ }
642
+
634
643
  // Make scripts executable
635
644
  for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-ollama.js']) {
636
645
  const p = join(kitDir, script);
@@ -1393,9 +1402,10 @@ program
1393
1402
 
1394
1403
  program
1395
1404
  .command('bridge')
1396
- .description('Start the bridge agent loop from the installed .bridge-kit/ — translates board slice changes into another spec framework instead of building code. Claude is the default executor; --ollama or --hook override it.')
1397
- .option('--ollama', 'Use ralph-ollama.js instead of the default Claude runner')
1405
+ .description('Start the bridge agent loop from the installed .bridge-kit/ — translates board slice changes into another spec framework instead of building code. A deterministic adapter runs with no LLM call if one exists for the configured target (e.g. spec-kitty); otherwise Claude is the default executor. --ollama, --hook, or --claude override the pick.')
1406
+ .option('--ollama', 'Use ralph-ollama.js instead of the default runner')
1398
1407
  .option('--hook <command>', 'Run this shell command instead of an AI agent for each batch of slice changes (e.g. commit + push .slices/ for a CI pipeline to pick up) — overrides any hook persisted via `init --bridge --hook` for this run only')
1408
+ .option('--claude', 'Force the Claude runner even if a static adapter exists for this target')
1399
1409
  .action((opts) => {
1400
1410
  const cwd = process.cwd();
1401
1411
  const kitDir = findAllInstalledKitDirs(cwd).find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
@@ -1404,8 +1414,8 @@ program
1404
1414
  process.exit(1);
1405
1415
  }
1406
1416
 
1407
- if (opts.ollama && opts.hook) {
1408
- console.error('❌ --ollama and --hook are mutually exclusive — pick one executor.');
1417
+ if ([opts.ollama, opts.hook, opts.claude].filter(Boolean).length > 1) {
1418
+ console.error('❌ --ollama, --hook, and --claude are mutually exclusive — pick one executor.');
1409
1419
  process.exit(1);
1410
1420
  }
1411
1421
 
@@ -1413,13 +1423,23 @@ program
1413
1423
  // plain sibling file — NOT under .eventmodelers/, which is gitignored (see
1414
1424
  // the comment in `init`'s --bridge branch) and would otherwise make this
1415
1425
  // per-machine instead of a shared, checked-in team/CI convention.
1416
- const persistedHook = readJsonSafe(join(kitDir, 'bridge.json')).hookCommand;
1426
+ const bridgeCfg = readJsonSafe(join(kitDir, 'bridge.json'));
1427
+ const persistedHook = bridgeCfg.hookCommand;
1417
1428
  const hookCmd = opts.hook || persistedHook;
1418
1429
 
1419
- // v1 has no --bash equivalent, unlike `run` — the default/--ollama path
1420
- // needs an actual agent, not a plain shell script; --hook is the escape
1421
- // hatch for teams who want a plain shell command instead of an AI agent.
1422
- const runner = hookCmd ? 'ralph-hook.js' : opts.ollama ? 'ralph-ollama.js' : 'ralph-claude.js';
1430
+ // A static adapter (adapters/<target>-adapter.js, e.g. spec-kitty-adapter.js)
1431
+ // is deterministic and costs no LLM call, so it wins over the Claude default
1432
+ // whenever one exists for the configured target --claude opts back out.
1433
+ const staticAdapterPath = join(kitDir, 'adapters', `${bridgeCfg.target}-adapter.js`);
1434
+ const hasStaticAdapter = bridgeCfg.target && existsSync(staticAdapterPath);
1435
+
1436
+ const runner = hookCmd
1437
+ ? 'ralph-hook.js'
1438
+ : opts.ollama
1439
+ ? 'ralph-ollama.js'
1440
+ : !opts.claude && hasStaticAdapter
1441
+ ? 'ralph-static.js'
1442
+ : 'ralph-claude.js';
1423
1443
  const runnerPath = join(kitDir, runner);
1424
1444
  if (!existsSync(runnerPath)) {
1425
1445
  console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
@@ -1463,9 +1483,11 @@ program
1463
1483
 
1464
1484
  program
1465
1485
  .command('fetch')
1466
- .description('Pull full slice detail from every context on the board via the slicedata API and write it into .slices/ — the pull-based counterpart to `listen`, without screen images')
1486
+ .description('Pull full slice detail for one context on the board via the slicedata API and write it into .slices/ — the pull-based counterpart to `listen`, without screen images')
1487
+ .requiredOption('--context <name>', 'Name of the MODEL_CONTEXT to fetch')
1467
1488
  .option('--slice-id <id>', 'After fetching, print just the slice with this id')
1468
1489
  .option('--slice-title <title>', 'After fetching, print just the slice with this title (case-insensitive)')
1490
+ .option('--spec-kitty', "After fetching, also restate this context as a Spec Kitty mission brief (.kittify/mission-brief.md via `spec-kitty intake`) — deterministic, no LLM call, no mission/spec.md/tasks created. Run `/spec-kitty.specify` afterward to turn the brief into a mission. Requires `spec-kitty init` to already be set up in this project (see lib/adapters/spec-kitty-adapter.js). One-shot: does not start a loop.")
1469
1491
  .action(async (opts, command) => {
1470
1492
  const cwd = process.cwd();
1471
1493
  const kitDir = findInstalledKitDir(cwd);
@@ -1520,6 +1542,15 @@ program
1520
1542
  await promptForCredentials();
1521
1543
  await runFetch({ cwd, kitDir: slicesKitDir, cfg, opts });
1522
1544
  }
1545
+
1546
+ if (opts.specKitty) {
1547
+ try {
1548
+ await runSpecKittyAdapter({ cfg, projectDir: cwd, contextName: opts.context });
1549
+ } catch (err) {
1550
+ console.error(`❌ ${err.message}`);
1551
+ process.exit(1);
1552
+ }
1553
+ }
1523
1554
  });
1524
1555
 
1525
1556
  program
@@ -0,0 +1,165 @@
1
+ // Spec Kitty adapter — turns an Eventmodelers context into a Spec Kitty
2
+ // mission brief, deterministically, with no LLM call. It deliberately stops
3
+ // there: Spec Kitty's own `/spec-kitty.specify` → `/spec-kitty.plan` →
4
+ // `/spec-kitty.tasks` pipeline is what creates the mission, spec.md, and work
5
+ // packages, because those steps require real judgment (WP boundaries, which
6
+ // files a WP owns, which agent profile fits) that only make sense with actual
7
+ // codebase context — this adapter has none of that, only the event model.
8
+ //
9
+ // Spec Kitty already has a first-class way to accept structured input instead
10
+ // of running its interactive discovery interview: `spec-kitty intake <path>`
11
+ // writes .kittify/mission-brief.md, and /spec-kitty.specify's own "Brief
12
+ // Context Detection" step reads that file and extracts requirements from it
13
+ // (asking 0-3 gap-filling questions instead of a full interview) — this
14
+ // adapter's whole job is producing a good brief and calling `intake`, nothing
15
+ // more.
16
+ //
17
+ // Lives here (not only inside a bridge-kit install) so it has exactly one
18
+ // entry point regardless of caller: `eventmodelers fetch --spec-kitty` calls
19
+ // it directly for a one-shot sync; `eventmodelers bridge` (via
20
+ // ralph-static.js) calls the same run() from a copy the bridge-kit installer
21
+ // places at .bridge-kit/adapters/ — see installStack in cli.js.
22
+ //
23
+ // Exports run({ cfg, projectDir, contextName }).
24
+
25
+ import { execFileSync } from 'child_process';
26
+ import { existsSync, unlinkSync, writeFileSync } from 'fs';
27
+ import { tmpdir } from 'os';
28
+ import { join } from 'path';
29
+
30
+ const DEFAULT_BASE_URL = 'https://api.eventmodelers.ai';
31
+
32
+ function slugify(text) {
33
+ return (text ?? '')
34
+ .toString()
35
+ .toLowerCase()
36
+ .trim()
37
+ .replace(/\s+/g, '-')
38
+ .replace(/[^\w-]+/g, '')
39
+ .replace(/-+/g, '-')
40
+ .replace(/^-+/, '')
41
+ .replace(/-+$/, '');
42
+ }
43
+
44
+ // Mirrors lib/fetch.js's per-context call rather than importing runFetch
45
+ // directly — this fetches raw JSON only, it never touches .slices/ (fetch.js
46
+ // owns writing that), and a copy of this file also has to run standalone
47
+ // inside an installed .bridge-kit/ with no access to lib/fetch.js at all.
48
+ async function fetchFullSliceData(cfg, contextName) {
49
+ const baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
50
+ const url = `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?contextName=${encodeURIComponent(contextName)}`;
51
+ const res = await fetch(url, {
52
+ headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'spec-kitty-adapter' },
53
+ });
54
+ if (!res.ok) throw new Error(`slicedata fetch failed for context "${contextName}": HTTP ${res.status}`);
55
+ return res.json();
56
+ }
57
+
58
+ // Set on errors that no amount of retrying will fix (missing setup, missing
59
+ // binary) — callers should stop instead of retrying on these: ralph-static.js
60
+ // exits instead of looping forever, `fetch --spec-kitty` just reports and exits.
61
+ function fatal(message) {
62
+ const err = new Error(message);
63
+ err.fatal = true;
64
+ return err;
65
+ }
66
+
67
+ function assertSpecKittyInitialized(projectDir) {
68
+ if (!existsSync(join(projectDir, '.kittify'))) {
69
+ throw fatal(
70
+ `Spec Kitty isn't initialized in ${projectDir} (no .kittify/ found).\n` +
71
+ ' Run this once from the project root, then re-sync:\n' +
72
+ ' spec-kitty init --ai claude --non-interactive',
73
+ );
74
+ }
75
+ }
76
+
77
+ // given/when/then are each arrays of element objects (command/event/etc.,
78
+ // each with its own `title`), not plain strings — confirmed against a real
79
+ // slicedata payload. `title` is the specification's own natural-language
80
+ // summary (e.g. "Cannot move on the opponent's turn") and is populated
81
+ // whenever the board has one, so it's the primary source; the given/when/then
82
+ // element names are only a fallback for specs that somehow lack a title.
83
+ function elementNames(elements) {
84
+ return (elements || []).map((e) => e?.title).filter(Boolean).join(', ');
85
+ }
86
+
87
+ function formatScenario(spec) {
88
+ if (spec?.title) return spec.title;
89
+ const given = elementNames(spec?.given);
90
+ const when = elementNames(spec?.when);
91
+ const then = elementNames(spec?.then);
92
+ if (given || when || then) {
93
+ return `Given ${given || '…'}, when ${when || '…'}, then ${then || '…'}.`;
94
+ }
95
+ return spec?.description || null;
96
+ }
97
+
98
+ // Plain prose, in board/timeline order — nothing here is invented. Priority,
99
+ // WP boundaries, and requirement IDs are exactly the judgment calls left to
100
+ // /spec-kitty.specify; this only restates what the event model already says.
101
+ export function buildBrief({ contextName, slices }) {
102
+ const lines = [
103
+ `# Event model: ${contextName}`,
104
+ '',
105
+ `This is a Spec Kitty mission brief generated from the "${contextName}" context on the Eventmodelers board — not free-text from a user. It restates the event model as-is; it does not add scope, priority, or requirements the board doesn't state.`,
106
+ '',
107
+ ];
108
+
109
+ for (const slice of slices) {
110
+ lines.push(`## ${slice.title}`);
111
+ lines.push('');
112
+ if (slice.description?.trim()) {
113
+ lines.push(slice.description.trim());
114
+ lines.push('');
115
+ }
116
+
117
+ const scenarios = (slice.specifications || []).map(formatScenario).filter(Boolean);
118
+ if (scenarios.length) {
119
+ for (const s of scenarios) lines.push(`- ${s}`);
120
+ } else {
121
+ lines.push('_No specifications captured for this slice on the board yet._');
122
+ }
123
+ lines.push('');
124
+ }
125
+
126
+ return lines.join('\n');
127
+ }
128
+
129
+ export async function run({ cfg, projectDir, contextName }) {
130
+ assertSpecKittyInitialized(projectDir);
131
+
132
+ console.log(`[spec-kitty-adapter] Fetching context "${contextName}"...`);
133
+ const payload = await fetchFullSliceData(cfg, contextName);
134
+ const slices = payload.slices || [];
135
+ if (!slices.length) {
136
+ console.log(`[spec-kitty-adapter] No slices in context "${contextName}" — nothing to write.`);
137
+ return;
138
+ }
139
+
140
+ const contextSlug = slugify(contextName) || 'default';
141
+ const briefPath = join(tmpdir(), `eventmodelers-brief-${contextSlug}.md`);
142
+ writeFileSync(briefPath, buildBrief({ contextName, slices }));
143
+
144
+ try {
145
+ execFileSync('spec-kitty', ['intake', briefPath, '--force'], { cwd: projectDir, stdio: 'inherit' });
146
+ } catch (err) {
147
+ if (err.code === 'ENOENT') {
148
+ throw fatal('`spec-kitty` CLI not found on PATH — install spec-kitty-cli first (see https://github.com/dilgerma/spec-kitty).');
149
+ }
150
+ throw new Error(`spec-kitty intake failed: ${err.stderr || err.message}`);
151
+ } finally {
152
+ try {
153
+ unlinkSync(briefPath);
154
+ } catch {
155
+ // Scratch file in tmpdir — not worth failing the sync over.
156
+ }
157
+ }
158
+
159
+ console.log(
160
+ `[spec-kitty-adapter] Brief synced from ${slices.length} slice(s) in "${contextName}" → .kittify/mission-brief.md.\n` +
161
+ ' Run the `/spec-kitty.specify` slash command in your coding agent to turn it into a mission ' +
162
+ '(the plain `spec-kitty specify` CLI command only scaffolds — brief detection and requirement ' +
163
+ 'extraction happen in the agent-driven prompt, not the bare CLI).',
164
+ );
165
+ }
package/lib/fetch.js CHANGED
@@ -15,6 +15,16 @@ export class FetchAuthError extends Error {
15
15
  }
16
16
  }
17
17
 
18
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+
20
+ // `--context` accepts any of: a MODEL_CONTEXT name or id, or a timeline (CHAPTER) name or id.
21
+ // /slicedata's contextId/contextName params now both resolve against MODEL_CONTEXT nodes first,
22
+ // then timelines (id matched exactly, name case-insensitively) — including a timeline with no
23
+ // assigned/connected context, which is its own context, not an error (same rule the canvas
24
+ // frontend's resolveContext applies: "an unassigned timeline is its own context"). So there's
25
+ // nothing left to resolve here — just route a uuid-shaped input to contextId, everything else to
26
+ // contextName, and let the server do the actual lookup.
27
+
18
28
  function readJsonSafe(path) {
19
29
  if (!path || !existsSync(path)) return {};
20
30
  try {
@@ -45,18 +55,24 @@ function sliceFolderName(title) {
45
55
  return (title ?? '').replaceAll(' ', '').replaceAll('slice:', '').toLowerCase();
46
56
  }
47
57
 
48
- // Pulls full slice detail from every context on a board and writes it into
58
+ // Pulls full slice detail from a single context on a board and writes it into
49
59
  // .slices/, mirroring the layout code-export.mjs's /api/generate handler produces
50
60
  // (minus screen images, which only ever arrive via that push-based listener).
51
61
  //
52
62
  // kitDir here is null for modeling-kit (see cli.js's fetch action) — nothing
53
63
  // nests .slices/ under .agent-modeling-kit/, so it lands at cwd instead.
54
- // { cwd, kitDir, cfg: { token, organizationId, boardId, baseUrl }, opts: { sliceId?, sliceTitle? } }
64
+ // { cwd, kitDir, cfg: { token, organizationId, boardId, baseUrl }, opts: { context, sliceId?, sliceTitle? } }
55
65
  export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
56
66
  const baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
57
67
  const headers = { 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'cli-fetch' };
58
68
 
59
- async function fetchJson(url, what) {
69
+ // assertBoardAccess (the guard every one of these routes runs behind) only ever
70
+ // answers 401/403 for credential/board-access problems — a 404 here always means
71
+ // "the thing at this path doesn't exist" (e.g. no MODEL_CONTEXT with that name),
72
+ // never "board not found". Only 401/403 are credential problems worth the
73
+ // reconfigure-and-retry dance in cli.js; 404 gets reported and the process exits,
74
+ // same as any other non-auth error.
75
+ async function fetchJson(url, what, { allow404 = false } = {}) {
60
76
  let res;
61
77
  try {
62
78
  res = await fetch(url, { headers });
@@ -66,7 +82,12 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
66
82
  }
67
83
  if (res.status === 401) throw new FetchAuthError(401, `${what}: invalid or expired token`);
68
84
  if (res.status === 403) throw new FetchAuthError(403, `${what}: token's organization does not match this board`);
69
- if (res.status === 404) throw new FetchAuthError(404, `${what}: board not found`);
85
+ if (res.status === 404) {
86
+ if (allow404) return null;
87
+ const body = await res.json().catch(() => null);
88
+ console.error(`❌ ${what}: ${body?.error || 'not found'}`);
89
+ process.exit(1);
90
+ }
70
91
  if (!res.ok) {
71
92
  console.error(`❌ ${what}: HTTP ${res.status}`);
72
93
  process.exit(1);
@@ -74,63 +95,49 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
74
95
  return res.json();
75
96
  }
76
97
 
77
- console.log(`▶ Fetching slices from ${baseUrl} (board ${cfg.boardId})...`);
78
-
79
- // No dedicated "list contexts" endpoint — MODEL_CONTEXT nodes are the contexts,
80
- // same as how the connect skill lists CHAPTER nodes for its health check.
81
- const contextNodes = await fetchJson(
82
- `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes?type=MODEL_CONTEXT`,
83
- 'nodes?type=MODEL_CONTEXT',
84
- );
85
- if (!contextNodes?.length) {
86
- console.log('ℹ️ No contexts found on this board.');
87
- return;
88
- }
89
-
90
98
  // Falls back to cwd when no kit is installed — fetch doesn't need kit-specific
91
99
  // files, just somewhere to write .slices/.
92
100
  const SLICES_DIR = join(kitDir || cwd, '.slices');
93
101
 
102
+ const contextInput = opts.context;
103
+
104
+ console.log(`▶ Fetching context "${contextInput}" from ${baseUrl} (board ${cfg.boardId})...`);
105
+
94
106
  // /slicedata (buildSliceData) is per-context and returns full slice detail —
95
- // commands/events/readmodels/screens/processors/specifications/comments unlike
96
- // the lightweight /slicedata/slices summary. There's no "all contexts in one
97
- // call" variant, so fetch each context's full data and merge client-side.
98
- const allSlices = [];
99
- for (const node of contextNodes) {
100
- const contextName = node.meta?.title ?? node.node?.data?.title ?? '';
101
- // contextId is the normal path (we already have the node id); contextName is
102
- // the fallback the endpoint itself supports when an id can't be resolved.
103
- const contextQuery = node.id ? `contextId=${encodeURIComponent(node.id)}` : `contextName=${encodeURIComponent(contextName)}`;
104
- const payload = await fetchJson(
105
- `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${contextQuery}`,
106
- `slicedata?${contextQuery}`,
107
- );
108
- const { slices } = payload;
109
- allSlices.push(...slices);
110
-
111
- if (slices.length) {
112
- // Mirrors code-export.mjs's /api/generate: a raw per-context payload dump at
113
- // .slices/<context>/config.json, alongside the per-slice output below kept
114
- // for parity with `listen` even though nothing in this repo reads it back.
115
- const contextSlug = slugify(slices[0]?.context || contextName || 'default') || 'default';
116
- const baseFolder = join(SLICES_DIR, contextSlug);
117
- mkdirSync(baseFolder, { recursive: true });
118
- writeFileSync(join(baseFolder, 'config.json'), JSON.stringify(payload, null, 2));
119
- }
107
+ // commands/events/readmodels/screens/processors/specifications/comments. It resolves
108
+ // contextId/contextName against MODEL_CONTEXT nodes first, then timelines, entirely
109
+ // server-side including the self-context fallback for a timeline with no assigned
110
+ // context so all that's left here is routing a uuid-shaped input to contextId and
111
+ // everything else to contextName. A 404 (no match) surfaces via fetchJson below.
112
+ const contextQuery = UUID_RE.test(contextInput)
113
+ ? `contextId=${encodeURIComponent(contextInput)}`
114
+ : `contextName=${encodeURIComponent(contextInput)}`;
115
+ const payload = await fetchJson(
116
+ `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${contextQuery}`,
117
+ `slicedata?${contextQuery}`,
118
+ );
119
+ const { slices: allSlices } = payload;
120
+ const displayContext = allSlices[0]?.context || contextInput;
121
+
122
+ if (allSlices.length) {
123
+ // Mirrors code-export.mjs's /api/generate: a raw per-context payload dump at
124
+ // .slices/<context>/config.json, alongside the per-slice output below — kept
125
+ // for parity with `listen` even though nothing in this repo reads it back.
126
+ const contextSlug = slugify(displayContext || 'default') || 'default';
127
+ const baseFolder = join(SLICES_DIR, contextSlug);
128
+ mkdirSync(baseFolder, { recursive: true });
129
+ writeFileSync(join(baseFolder, 'config.json'), JSON.stringify(payload, null, 2));
120
130
  }
121
131
 
122
132
  if (!allSlices.length) {
123
- console.log('ℹ️ No slices found on this board.');
133
+ console.log(`ℹ️ No slices found in context "${displayContext}".`);
124
134
  return;
125
135
  }
126
136
 
127
- const contextNames = new Set();
128
-
129
137
  for (const slice of allSlices) {
130
138
  // buildSliceData names this field `context`, not `contextName` (that's the
131
139
  // /slicedata/slices summary endpoint's field) — read the one this endpoint sends.
132
- const contextName = slice.context || 'default';
133
- contextNames.add(contextName);
140
+ const contextName = slice.context || displayContext || 'default';
134
141
  const contextSlug = slugify(contextName) || 'default';
135
142
  const baseFolder = join(SLICES_DIR, contextSlug);
136
143
  const sliceFolder = sliceFolderName(slice.title);
@@ -165,14 +172,12 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
165
172
  writeFileSync(indexFile, JSON.stringify(sliceIndices, null, 2));
166
173
  }
167
174
 
168
- // A single shared current_context.json only makes sense when everything fetched
169
- // belongs to one context with several, any one choice would be arbitrary, so
170
- // leave whatever `listen`/a prior fetch already wrote there untouched.
171
- if (contextNames.size === 1) {
172
- writeFileSync(join(SLICES_DIR, 'current_context.json'), JSON.stringify({ name: [...contextNames][0] }, null, 2));
173
- }
175
+ // Persist the *resolved* context name (not the raw --context input, which may
176
+ // have been a timeline name/id or context id) so the next run's assigned-context
177
+ // fallback, and shared/build-kit/lib/ralph.js's readCurrentContext, see a real name.
178
+ writeFileSync(join(SLICES_DIR, 'current_context.json'), JSON.stringify({ name: displayContext }, null, 2));
174
179
 
175
- console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} across ${contextNames.size} context${contextNames.size === 1 ? '' : 's'} → ${relative(cwd, SLICES_DIR)}/`);
180
+ console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} from context "${displayContext}" → ${relative(cwd, SLICES_DIR)}/`);
176
181
 
177
182
  // --slice-id/--slice-title mirror load-slice's Step 4/5 — fetch+persist everything
178
183
  // regardless, then just report the one the caller asked about.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -140,4 +140,4 @@ All slices (<count>) — context: <contextSlug>:
140
140
  - ...
141
141
  ```
142
142
 
143
- Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
143
+ Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // Bridge loop that hands each batch of slice changes to a deterministic JS
3
+ // adapter instead of an AI agent or an arbitrary shell hook — no LLM call
4
+ // happens in this mode. Which adapter runs is picked by bridge.json's target
5
+ // (adapters/<target>-adapter.js); if the installed target has no adapter file
6
+ // yet, this exits with a clear error rather than silently doing nothing.
7
+ //
8
+ // Usage: node ralph-static.js [project_dir]
9
+
10
+ import { startRalph, loadLocalConfig, fetchPlatformConfig } from './lib/ralph.js';
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import { dirname, join, resolve } from 'path';
13
+ import { fileURLToPath, pathToFileURL } from 'url';
14
+
15
+ const kitDir = dirname(fileURLToPath(import.meta.url));
16
+ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
17
+
18
+ function loadBridgeConfig() {
19
+ const p = join(kitDir, 'bridge.json');
20
+ if (!existsSync(p)) return {};
21
+ try {
22
+ return JSON.parse(readFileSync(p, 'utf-8'));
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ const target = loadBridgeConfig().target;
29
+ if (!target) {
30
+ console.error('[bridge-static] No target configured in bridge.json — run `eventmodelers init --bridge --target <name>` first.');
31
+ process.exit(1);
32
+ }
33
+
34
+ const adapterPath = join(kitDir, 'adapters', `${target}-adapter.js`);
35
+ if (!existsSync(adapterPath)) {
36
+ console.error(`[bridge-static] No static adapter for target "${target}" (expected ${adapterPath}).`);
37
+ console.error(' Use the default Claude runner for this target instead: `eventmodelers bridge`.');
38
+ process.exit(1);
39
+ }
40
+
41
+ const local = loadLocalConfig(kitDir);
42
+
43
+ // The adapter itself doesn't know about the bridge loop's .slices/ convention
44
+ // (fetch --spec-kitty calls the same run() with a context name of its own) —
45
+ // this is the one place that convention still applies, so resolve it here.
46
+ function readCurrentContext() {
47
+ const ctxPath = join(kitDir, '.slices', 'current_context.json');
48
+ if (!existsSync(ctxPath)) return null;
49
+ try {
50
+ return JSON.parse(readFileSync(ctxPath, 'utf-8')).name || null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ async function runAdapter() {
57
+ const contextName = readCurrentContext();
58
+ if (!contextName) {
59
+ console.log('[bridge-static] No .slices/current_context.json yet — waiting for the first board sync.');
60
+ return;
61
+ }
62
+
63
+ const { run } = await import(pathToFileURL(adapterPath).href);
64
+ const cfg = await fetchPlatformConfig(local);
65
+ try {
66
+ await run({ cfg, projectDir, contextName });
67
+ } catch (err) {
68
+ // lib/ralph.js retries any onTask failure after 60s forever — right for
69
+ // transient errors (network blips, a slow spec-kitty command), wrong for
70
+ // a missing one-time setup step that retrying can never fix on its own.
71
+ // Adapters flag those with `err.fatal` so we stop instead of looping.
72
+ if (err.fatal) {
73
+ console.error(`[bridge-static] ${err.message}`);
74
+ process.exit(1);
75
+ }
76
+ throw err;
77
+ }
78
+ }
79
+
80
+ startRalph({
81
+ kitDir,
82
+ projectDir,
83
+ onTask: runAdapter,
84
+ // onPlannedSlice omitted — see ralph-claude.js in this same directory.
85
+ agentType: 'BRIDGE',
86
+ queueAllStatuses: true,
87
+ }).catch((err) => {
88
+ console.error('[ralph] Fatal:', err);
89
+ process.exit(1);
90
+ });
@@ -49,4 +49,6 @@ Outcome: [what changed on the board]
49
49
  - `/wdyt` posts QUESTION comments onto nodes — use for analysis only, not modifications.
50
50
  - The `board_id`, `timeline_id`, and `organization_id` from each prompt provide full context — pass them to skills that need them.
51
51
  - Node events POST to `/api/boards/:boardId/nodes/events` using `node:created`, `node:changed`, `node:deleted`.
52
- - `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard so two agents can't both claim the same slice. Treat this as `ALREADY_IN_STATUS`, not a task failure: drop the prompt, move on to the next task, and do not retry the same update.
52
+ - `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard so two agents can't both claim the same slice. Treat this as `ALREADY_IN_STATUS`, not a task failure: drop the prompt, move on to the next task, and do not retry the same update.
53
+ - macOS/BSD `date` silently ignores GNU-only format specifiers like `%N`/`%3N` (sub-second precision) instead of erroring — it prints the literal characters, producing a malformed timestamp that only fails downstream. Don't shell out to `date` for sub-second precision; use `$(( $(date +%s) * 1000 ))` for whole-second-in-ms, or a runtime call (`Date.now()`, `process.hrtime()`) instead.
54
+ - Before retrying a failed shell command a second time, diagnose why it failed (e.g. a GNU/BSD flag mismatch) rather than re-running it unchanged — repeating the same command produces the same failure and just burns retries.
@@ -1,141 +0,0 @@
1
- ---
2
- name: load-slice
3
- description: Load all slices from the board via the slicedata API and persist them to the .build-kit/.slices/ directory hierarchy (index.json with full definitions, per-slice folders). Returns data for a specific slice by ID or title.
4
- ---
5
-
6
- # Load Slice
7
-
8
- > **Before doing anything else**, invoke the `connect` skill to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL`. Do not proceed until the connect skill has completed.
9
-
10
- ---
11
-
12
- ## Step 1 — Parse arguments
13
-
14
- From `$ARGUMENTS`, extract:
15
-
16
- | Field | How to find it | Default |
17
- |-------|---------------|---------|
18
- | `sliceId` | UUID of the slice (SLICE_BORDER node ID) | optional — prefer over title |
19
- | `sliceTitle` | slice title (case-insensitive match) | optional — used if sliceId missing |
20
-
21
- If neither is provided, load and persist all slices without filtering.
22
-
23
- ---
24
-
25
- ## Step 2 — Fetch all slices from the slicedata API
26
-
27
- ```bash
28
- curl -s \
29
- -H "x-token: <TOKEN>" \
30
- -H "x-board-id: <BOARD_ID>" \
31
- -H "x-user-id: load-slice-skill" \
32
- "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/slicedata/slices"
33
- ```
34
-
35
- Response shape: `{ "slices": [ { "id": "...", "title": "...", "status": "...", "context": "...", "comments": ["..."], ... } ] }`
36
-
37
- Save the full array as `ALL_SLICES`.
38
-
39
- ---
40
-
41
- ## Step 3 — Persist slices to .build-kit/.slices/ directory
42
-
43
- Apply the following logic for every slice in `ALL_SLICES`.
44
-
45
- ### Derive paths
46
-
47
- - `contextName` = `slice.context` if present, otherwise `"default"` — **preserve original casing** (e.g. `"Beta"`, not `"beta"`)
48
- - `sliceFolder` = `slice.title` lowercased, with all spaces removed and the prefix `"slice:"` stripped
49
- e.g. `"Beta Enable User for Beta Test"` → `"betaenableuserforbetatest"`
50
- - `baseFolder` = `.build-kit/.slices/<contextName>/`
51
- - `sliceDir` = `.build-kit/.slices/<contextName>/<sliceFolder>/`
52
-
53
- ### Write files
54
-
55
- ```bash
56
- mkdir -p ".build-kit/.slices/<contextName>/<sliceFolder>"
57
- ```
58
-
59
- **`.build-kit/.slices/current_context.json`** — always overwrite:
60
-
61
- ```json
62
- { "name": "Beta" }
63
- ```
64
-
65
- **`.build-kit/.slices/<contextName>/context.json`** — write once per context:
66
-
67
- ```json
68
- { "name": "Beta" }
69
- ```
70
-
71
- **`.build-kit/.slices/<contextName>/<sliceFolder>/slice.json`** — the full slice object with the `index` field removed.
72
-
73
- ### Maintain `.build-kit/.slices/<contextName>/index.json`
74
-
75
- Read the file if it exists, otherwise start with `{ "slices": [] }`.
76
-
77
- Each entry in `index.json` contains the index metadata **plus** the complete slice definition fetched from the API:
78
-
79
- ```json
80
- {
81
- "slices": [
82
- {
83
- "id": "d0dbc70c-f244-4048-886b-1d11e461f466",
84
- "slice": "Beta Enable User for Beta Test",
85
- "index": 0,
86
- "context": "Beta",
87
- "folder": "betaenableuserforbetatest",
88
- "status": "Created",
89
- "definition": {
90
- "id": "d0dbc70c-f244-4048-886b-1d11e461f466",
91
- "title": "Beta Enable User for Beta Test",
92
- "status": "Created",
93
- "context": "Beta"
94
- }
95
- }
96
- ]
97
- }
98
- ```
99
-
100
- The `definition` field holds the full object returned by the API for that slice (all fields as-is).
101
-
102
- **Merge rules:**
103
- - If an entry with the same `id` already exists: update all fields and refresh `definition`; preserve any existing `assigned` field.
104
- - If not found: append the new entry.
105
-
106
- Write the updated object back to `.build-kit/.slices/<contextName>/index.json`.
107
-
108
- ---
109
-
110
- ## Step 4 — Return the requested slice
111
-
112
- If `sliceId` was given: find the entry in `ALL_SLICES` where `id === sliceId`.
113
- If `sliceTitle` was given: find the entry where `title` matches case-insensitively.
114
- If neither: return all slices.
115
-
116
- If a specific slice was requested but not found, stop and list the available titles.
117
-
118
- ---
119
-
120
- ## Step 5 — Output
121
-
122
- ```
123
- Slices loaded: <count> total
124
- Persisted to: .build-kit/.slices/<contextName>/
125
-
126
- Requested slice:
127
- Title: <title>
128
- ID: <id>
129
- Status: <status>
130
- Folder: .build-kit/.slices/<contextName>/<sliceFolder>/slice.json
131
- ```
132
-
133
- Or if no filter was given:
134
-
135
- ```
136
- All slices (<count>) — context: <contextName>:
137
- - <title> [<status>] → .build-kit/.slices/<contextName>/<sliceFolder>/
138
- - ...
139
- ```
140
-
141
- Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
@@ -1,141 +0,0 @@
1
- ---
2
- name: load-slice
3
- description: Load all slices from the board via the slicedata API and persist them to the .build-kit/.slices/ directory hierarchy (index.json with full definitions, per-slice folders). Returns data for a specific slice by ID or title.
4
- ---
5
-
6
- # Load Slice
7
-
8
- > **Before doing anything else**, invoke the `connect` skill to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL`. Do not proceed until the connect skill has completed.
9
-
10
- ---
11
-
12
- ## Step 1 — Parse arguments
13
-
14
- From `$ARGUMENTS`, extract:
15
-
16
- | Field | How to find it | Default |
17
- |-------|---------------|---------|
18
- | `sliceId` | UUID of the slice (SLICE_BORDER node ID) | optional — prefer over title |
19
- | `sliceTitle` | slice title (case-insensitive match) | optional — used if sliceId missing |
20
-
21
- If neither is provided, load and persist all slices without filtering.
22
-
23
- ---
24
-
25
- ## Step 2 — Fetch all slices from the slicedata API
26
-
27
- ```bash
28
- curl -s \
29
- -H "x-token: <TOKEN>" \
30
- -H "x-board-id: <BOARD_ID>" \
31
- -H "x-user-id: load-slice-skill" \
32
- "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/slicedata/slices"
33
- ```
34
-
35
- Response shape: `{ "slices": [ { "id": "...", "title": "...", "status": "...", "context": "...", "comments": ["..."], ... } ] }`
36
-
37
- Save the full array as `ALL_SLICES`.
38
-
39
- ---
40
-
41
- ## Step 3 — Persist slices to .build-kit/.slices/ directory
42
-
43
- Apply the following logic for every slice in `ALL_SLICES`.
44
-
45
- ### Derive paths
46
-
47
- - `contextName` = `slice.context` if present, otherwise `"default"` — **preserve original casing** (e.g. `"Beta"`, not `"beta"`)
48
- - `sliceFolder` = `slice.title` lowercased, with all spaces removed and the prefix `"slice:"` stripped
49
- e.g. `"Beta Enable User for Beta Test"` → `"betaenableuserforbetatest"`
50
- - `baseFolder` = `.build-kit/.slices/<contextName>/`
51
- - `sliceDir` = `.build-kit/.slices/<contextName>/<sliceFolder>/`
52
-
53
- ### Write files
54
-
55
- ```bash
56
- mkdir -p ".build-kit/.slices/<contextName>/<sliceFolder>"
57
- ```
58
-
59
- **`.build-kit/.slices/current_context.json`** — always overwrite:
60
-
61
- ```json
62
- { "name": "Beta" }
63
- ```
64
-
65
- **`.build-kit/.slices/<contextName>/context.json`** — write once per context:
66
-
67
- ```json
68
- { "name": "Beta" }
69
- ```
70
-
71
- **`.build-kit/.slices/<contextName>/<sliceFolder>/slice.json`** — the full slice object with the `index` field removed.
72
-
73
- ### Maintain `.build-kit/.slices/<contextName>/index.json`
74
-
75
- Read the file if it exists, otherwise start with `{ "slices": [] }`.
76
-
77
- Each entry in `index.json` contains the index metadata **plus** the complete slice definition fetched from the API:
78
-
79
- ```json
80
- {
81
- "slices": [
82
- {
83
- "id": "d0dbc70c-f244-4048-886b-1d11e461f466",
84
- "slice": "Beta Enable User for Beta Test",
85
- "index": 0,
86
- "context": "Beta",
87
- "folder": "betaenableuserforbetatest",
88
- "status": "Created",
89
- "definition": {
90
- "id": "d0dbc70c-f244-4048-886b-1d11e461f466",
91
- "title": "Beta Enable User for Beta Test",
92
- "status": "Created",
93
- "context": "Beta"
94
- }
95
- }
96
- ]
97
- }
98
- ```
99
-
100
- The `definition` field holds the full object returned by the API for that slice (all fields as-is).
101
-
102
- **Merge rules:**
103
- - If an entry with the same `id` already exists: update all fields and refresh `definition`; preserve any existing `assigned` field.
104
- - If not found: append the new entry.
105
-
106
- Write the updated object back to `.build-kit/.slices/<contextName>/index.json`.
107
-
108
- ---
109
-
110
- ## Step 4 — Return the requested slice
111
-
112
- If `sliceId` was given: find the entry in `ALL_SLICES` where `id === sliceId`.
113
- If `sliceTitle` was given: find the entry where `title` matches case-insensitively.
114
- If neither: return all slices.
115
-
116
- If a specific slice was requested but not found, stop and list the available titles.
117
-
118
- ---
119
-
120
- ## Step 5 — Output
121
-
122
- ```
123
- Slices loaded: <count> total
124
- Persisted to: .build-kit/.slices/<contextName>/
125
-
126
- Requested slice:
127
- Title: <title>
128
- ID: <id>
129
- Status: <status>
130
- Folder: .build-kit/.slices/<contextName>/<sliceFolder>/slice.json
131
- ```
132
-
133
- Or if no filter was given:
134
-
135
- ```
136
- All slices (<count>) — context: <contextName>:
137
- - <title> [<status>] → .build-kit/.slices/<contextName>/<sliceFolder>/
138
- - ...
139
- ```
140
-
141
- Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.
@@ -1,143 +0,0 @@
1
- ---
2
- name: load-slice
3
- description: Load all slices from the board via the slicedata API and persist them to the .build-kit/.slices/ directory hierarchy (index.json with full definitions, per-slice folders). Returns data for a specific slice by ID or title.
4
- ---
5
-
6
- # Load Slice
7
-
8
- > **Before doing anything else**, invoke the `connect` skill to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL`. Do not proceed until the connect skill has completed.
9
-
10
- ---
11
-
12
- ## Step 1 — Parse arguments
13
-
14
- From `$ARGUMENTS`, extract:
15
-
16
- | Field | How to find it | Default |
17
- |-------|---------------|---------|
18
- | `sliceId` | UUID of the slice (SLICE_BORDER node ID) | optional — prefer over title |
19
- | `sliceTitle` | slice title (case-insensitive match) | optional — used if sliceId missing |
20
-
21
- If neither is provided, load and persist all slices without filtering.
22
-
23
- ---
24
-
25
- ## Step 2 — Fetch all slices from the slicedata API
26
-
27
- ```bash
28
- curl -s \
29
- -H "x-token: <TOKEN>" \
30
- -H "x-board-id: <BOARD_ID>" \
31
- -H "x-user-id: load-slice-skill" \
32
- "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/slicedata/slices"
33
- ```
34
-
35
- Response shape: `{ "slices": [ { "id": "...", "title": "...", "status": "...", "contextName": "...", "contextId": "...", "comments": ["..."], ... } ] }`
36
-
37
- Save the full array as `ALL_SLICES`.
38
-
39
- ---
40
-
41
- ## Step 3 — Persist slices to .build-kit/.slices/ directory
42
-
43
- Apply the following logic for every slice in `ALL_SLICES`.
44
-
45
- ### Derive paths
46
-
47
- - `contextSlug` = slugify `slice.contextName` if present, otherwise `"default"` — lowercase, spaces to hyphens, non-alphanumeric removed (e.g. `"My Ctx"` → `"my-ctx"`)
48
- - `sliceFolder` = `slice.title` lowercased, with all spaces removed and the prefix `"slice:"` stripped
49
- e.g. `"Beta Enable User for Beta Test"` → `"betaenableuserforbetatest"`
50
- - `baseFolder` = `.build-kit/.slices/<contextSlug>/`
51
- - `sliceDir` = `.build-kit/.slices/<contextSlug>/<sliceFolder>/`
52
-
53
- ### Write files
54
-
55
- ```bash
56
- mkdir -p ".build-kit/.slices/<contextSlug>/<sliceFolder>"
57
- ```
58
-
59
- **`.build-kit/.slices/current_context.json`** — always overwrite:
60
-
61
- ```json
62
- { "name": "Beta" }
63
- ```
64
-
65
- **`.build-kit/.slices/<contextSlug>/context.json`** — write once per context:
66
-
67
- ```json
68
- { "name": "Beta" }
69
- ```
70
-
71
- **`.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json`** — the full slice object with the `index` field removed.
72
-
73
- ### Maintain `.build-kit/.slices/<contextSlug>/index.json`
74
-
75
- Read the file if it exists, otherwise start with `{ "slices": [] }`.
76
-
77
- Each entry in `index.json` contains the index metadata **plus** the complete slice definition fetched from the API:
78
-
79
- ```json
80
- {
81
- "slices": [
82
- {
83
- "id": "d0dbc70c-f244-4048-886b-1d11e461f466",
84
- "slice": "Beta Enable User for Beta Test",
85
- "index": 0,
86
- "contextName": "Beta",
87
- "contextSlug": "beta",
88
- "folder": "betaenableuserforbetatest",
89
- "status": "Created",
90
- "definition": {
91
- "id": "d0dbc70c-f244-4048-886b-1d11e461f466",
92
- "title": "Beta Enable User for Beta Test",
93
- "status": "Created",
94
- "contextName": "Beta",
95
- "contextId": "..."
96
- }
97
- }
98
- ]
99
- }
100
- ```
101
-
102
- The `definition` field holds the full object returned by the API for that slice (all fields as-is).
103
-
104
- **Merge rules:**
105
- - If an entry with the same `id` already exists: update all fields and refresh `definition`; preserve any existing `assigned` field.
106
- - If not found: append the new entry.
107
-
108
- Write the updated object back to `.build-kit/.slices/<contextSlug>/index.json`.
109
-
110
- ---
111
-
112
- ## Step 4 — Return the requested slice
113
-
114
- If `sliceId` was given: find the entry in `ALL_SLICES` where `id === sliceId`.
115
- If `sliceTitle` was given: find the entry where `title` matches case-insensitively.
116
- If neither: return all slices.
117
-
118
- If a specific slice was requested but not found, stop and list the available titles.
119
-
120
- ---
121
-
122
- ## Step 5 — Output
123
-
124
- ```
125
- Slices loaded: <count> total
126
- Persisted to: .build-kit/.slices/<contextSlug>/
127
-
128
- Requested slice:
129
- Title: <title>
130
- ID: <id>
131
- Status: <status>
132
- Folder: .build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json
133
- ```
134
-
135
- Or if no filter was given:
136
-
137
- ```
138
- All slices (<count>) — context: <contextSlug>:
139
- - <title> [<status>] → .build-kit/.slices/<contextSlug>/<sliceFolder>/
140
- - ...
141
- ```
142
-
143
- Make the matched slice's `id`, `title`, `status`, and local folder path available to subsequent steps in the same session.