@eventmodelers/cli 0.0.28 → 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
 
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.`);
@@ -1467,6 +1487,7 @@ program
1467
1487
  .requiredOption('--context <name>', 'Name of the MODEL_CONTEXT to fetch')
1468
1488
  .option('--slice-id <id>', 'After fetching, print just the slice with this id')
1469
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.")
1470
1491
  .action(async (opts, command) => {
1471
1492
  const cwd = process.cwd();
1472
1493
  const kitDir = findInstalledKitDir(cwd);
@@ -1521,6 +1542,15 @@ program
1521
1542
  await promptForCredentials();
1522
1543
  await runFetch({ cwd, kitDir: slicesKitDir, cfg, opts });
1523
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
+ }
1524
1554
  });
1525
1555
 
1526
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 {
@@ -56,7 +66,13 @@ 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,42 +95,49 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
74
95
  return res.json();
75
96
  }
76
97
 
77
- console.log(`▶ Fetching context "${opts.context}" from ${baseUrl} (board ${cfg.boardId})...`);
78
-
79
98
  // Falls back to cwd when no kit is installed — fetch doesn't need kit-specific
80
99
  // files, just somewhere to write .slices/.
81
100
  const SLICES_DIR = join(kitDir || cwd, '.slices');
82
101
 
102
+ const contextInput = opts.context;
103
+
104
+ console.log(`▶ Fetching context "${contextInput}" from ${baseUrl} (board ${cfg.boardId})...`);
105
+
83
106
  // /slicedata (buildSliceData) is per-context and returns full slice detail —
84
- // commands/events/readmodels/screens/processors/specifications/comments. It
85
- // resolves contextName by exact match, so no separate "list contexts" lookup
86
- // is needed first.
87
- const contextQuery = `contextName=${encodeURIComponent(opts.context)}`;
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)}`;
88
115
  const payload = await fetchJson(
89
116
  `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${contextQuery}`,
90
117
  `slicedata?${contextQuery}`,
91
118
  );
92
119
  const { slices: allSlices } = payload;
120
+ const displayContext = allSlices[0]?.context || contextInput;
93
121
 
94
122
  if (allSlices.length) {
95
123
  // Mirrors code-export.mjs's /api/generate: a raw per-context payload dump at
96
124
  // .slices/<context>/config.json, alongside the per-slice output below — kept
97
125
  // for parity with `listen` even though nothing in this repo reads it back.
98
- const contextSlug = slugify(allSlices[0]?.context || opts.context || 'default') || 'default';
126
+ const contextSlug = slugify(displayContext || 'default') || 'default';
99
127
  const baseFolder = join(SLICES_DIR, contextSlug);
100
128
  mkdirSync(baseFolder, { recursive: true });
101
129
  writeFileSync(join(baseFolder, 'config.json'), JSON.stringify(payload, null, 2));
102
130
  }
103
131
 
104
132
  if (!allSlices.length) {
105
- console.log(`ℹ️ No slices found in context "${opts.context}".`);
133
+ console.log(`ℹ️ No slices found in context "${displayContext}".`);
106
134
  return;
107
135
  }
108
136
 
109
137
  for (const slice of allSlices) {
110
138
  // buildSliceData names this field `context`, not `contextName` (that's the
111
139
  // /slicedata/slices summary endpoint's field) — read the one this endpoint sends.
112
- const contextName = slice.context || opts.context || 'default';
140
+ const contextName = slice.context || displayContext || 'default';
113
141
  const contextSlug = slugify(contextName) || 'default';
114
142
  const baseFolder = join(SLICES_DIR, contextSlug);
115
143
  const sliceFolder = sliceFolderName(slice.title);
@@ -144,9 +172,12 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
144
172
  writeFileSync(indexFile, JSON.stringify(sliceIndices, null, 2));
145
173
  }
146
174
 
147
- writeFileSync(join(SLICES_DIR, 'current_context.json'), JSON.stringify({ name: opts.context }, null, 2));
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));
148
179
 
149
- console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} from context "${opts.context}" → ${relative(cwd, SLICES_DIR)}/`);
180
+ console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} from context "${displayContext}" → ${relative(cwd, SLICES_DIR)}/`);
150
181
 
151
182
  // --slice-id/--slice-title mirror load-slice's Step 4/5 — fetch+persist everything
152
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.28",
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.