@eventmodelers/cli 0.0.28 → 0.0.30

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
@@ -14,11 +14,12 @@ import {
14
14
  readFileSync,
15
15
  appendFileSync,
16
16
  } from 'fs';
17
- import { execSync, spawn } from 'child_process';
17
+ import { execSync, execFileSync, spawn } from 'child_process';
18
18
  import { createInterface, emitKeypressEvents, moveCursor, clearScreenDown } from 'readline';
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);
@@ -545,6 +546,55 @@ function copyDirContents(srcDir, destDir, { skip = [] } = {}) {
545
546
  if (count) console.log(` ✓ Installed ${count} item${count === 1 ? '' : 's'} into ${relative(process.cwd(), destDir) || '.'}`);
546
547
  }
547
548
 
549
+ // Community/custom build kits (`init --stack <name> --git <url>`) are cloned fresh on
550
+ // every run rather than pulled/updated in place — there's no local state worth
551
+ // preserving between installs, and re-cloning from scratch means a broken or partial
552
+ // previous clone can never linger and cause a confusing stale-file bug. Cached under
553
+ // ~/.eventmodelers (not inside the target project) so it's reusable across projects and
554
+ // definitely not something `uninstall` or the project's own .gitignore need to know about.
555
+ function cloneGitStack(url, branch) {
556
+ const dest = join(homedir(), '.eventmodelers', 'git-stacks', `${url}${branch ? `#${branch}` : ''}`.replace(/[^a-zA-Z0-9._-]/g, '_'));
557
+ rmSync(dest, { recursive: true, force: true });
558
+ mkdirSync(dirname(dest), { recursive: true });
559
+ console.log(`📥 Cloning ${url}${branch ? ` (branch: ${branch})` : ''}...`);
560
+ const cloneArgs = ['clone', '--depth', '1', ...(branch ? ['--branch', branch] : []), url, dest];
561
+ try {
562
+ execFileSync('git', cloneArgs, { stdio: ['ignore', 'inherit', 'inherit'] });
563
+ } catch {
564
+ console.error(`❌ Failed to clone "${url}"${branch ? ` (branch: ${branch})` : ''} — check the URL, the branch name, your git access, and that git is installed, then try again.`);
565
+ process.exit(1);
566
+ }
567
+ return dest;
568
+ }
569
+
570
+ // A community stack repo must mirror the internal stacks/<key>/templates layout exactly
571
+ // (templates/.claude, templates/root, templates/<kitSubdir>) so installStack() can treat
572
+ // it identically to a built-in stack — see STACKS above for the shape. An optional
573
+ // stack.json at the repo root can declare label/kitSubdir/useShared/needsBoardId;
574
+ // kitDirName is always forced to .build-kit (the same runtime contract every built-in
575
+ // stack already uses), so run/status/uninstall recognize a git-installed stack with no
576
+ // changes of their own.
577
+ function resolveGitStackConfig(clonedDir, name) {
578
+ const templatesSource = join(clonedDir, 'templates');
579
+ if (!existsSync(templatesSource)) {
580
+ console.error(`❌ ${relative(process.cwd(), clonedDir) || clonedDir} has no templates/ directory — a build kit repo needs templates/.claude, templates/root, and templates/<kitSubdir>, the same layout this CLI's own stacks/<name>/templates use.`);
581
+ process.exit(1);
582
+ }
583
+ const manifest = readJsonSafe(join(clonedDir, 'stack.json'));
584
+ const kitSubdir = manifest.kitSubdir || 'build-kit';
585
+ if (!existsSync(join(templatesSource, kitSubdir))) {
586
+ console.error(`❌ ${relative(process.cwd(), templatesSource) || templatesSource} is missing its kit subdirectory "${kitSubdir}/" (from stack.json, or the "build-kit" default) — nothing to install.`);
587
+ process.exit(1);
588
+ }
589
+ return {
590
+ label: manifest.label || name,
591
+ kitSubdir,
592
+ kitDirName: '.build-kit',
593
+ useShared: manifest.useShared !== false,
594
+ needsBoardId: manifest.needsBoardId !== false,
595
+ };
596
+ }
597
+
548
598
  async function resolveStack(cliStack) {
549
599
  if (cliStack) {
550
600
  if (!STACKS[cliStack]) {
@@ -565,7 +615,9 @@ async function installStack(stackKey, stackCfg, options = {}) {
565
615
  console.log(`Using: ${stackKey} (${stackCfg.label})\n`);
566
616
 
567
617
  const targetDir = process.cwd();
568
- const templatesSource = join(__dirname, 'stacks', stackKey, 'templates');
618
+ // `init --git <url>` passes a resolved clone dir's templates/ here instead — every
619
+ // other input (STACKS, MODELING_KIT, BRIDGE_KIT) keeps using the built-in path.
620
+ const templatesSource = options.templatesSource || join(__dirname, 'stacks', stackKey, 'templates');
569
621
  const sharedBuildKit = join(__dirname, 'shared', 'build-kit');
570
622
  // Skills with no stack-specific content (connect, learn-eventmodelers-api,
571
623
  // update-slice-status, ...) live once here instead of being copy-pasted into
@@ -631,6 +683,14 @@ async function installStack(stackKey, stackCfg, options = {}) {
631
683
  }
632
684
  copyDirContents(join(templatesSource, stackCfg.kitSubdir), kitDir, { skip: ['.eventmodelers'] });
633
685
 
686
+ // Static (no-LLM) bridge adapters live once in this package's own lib/
687
+ // adapters/ — `fetch --spec-kitty` imports them directly, and a bridge
688
+ // install gets its own copy here so ralph-static.js can run standalone
689
+ // with no access back to the published package.
690
+ if (isBridge) {
691
+ copyDirContents(join(__dirname, 'lib', 'adapters'), join(kitDir, 'adapters'));
692
+ }
693
+
634
694
  // Make scripts executable
635
695
  for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-ollama.js']) {
636
696
  const p = join(kitDir, script);
@@ -1153,8 +1213,10 @@ function credentialOverridesFromOpts(opts) {
1153
1213
  credentialFlags(program
1154
1214
  .command('init')
1155
1215
  .alias('install')
1156
- .description('Scaffold a stack + install the agent kit into the current directory (or --modeling for skills + agent loop only, no backend scaffold; or --bridge to translate board slices into another spec framework)')
1157
- .option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')})`)
1216
+ .description('Scaffold a stack + install the agent kit into the current directory (or --modeling for skills + agent loop only, no backend scaffold; or --bridge to translate board slices into another spec framework; or --git to install a community/custom build kit from a git repo)')
1217
+ .option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')}), or a name of your choosing when combined with --git`)
1218
+ .option('--git <url>', 'Install a build kit not built into this CLI by cloning this git repo (used with --stack <name> to name it) — the repo must mirror the templates/.claude, templates/root, templates/<kitSubdir> layout of this CLI\'s own stacks/<name>/templates, optionally with a stack.json declaring label/kitSubdir/useShared/needsBoardId')
1219
+ .option('--branch <name>', 'Branch to clone — only meaningful with --git (defaults to the repo\'s default branch)')
1158
1220
  .option('--modeling', 'Install skills + the agent loop only — no backend scaffold. Mutually exclusive with --stack/--bridge.')
1159
1221
  .option('--bridge', 'Install a bridge kit — translates board slices into another spec framework instead of building code. Mutually exclusive with --stack/--modeling. Requires --target.')
1160
1222
  .option('--target <name>', `Bridge target framework (${Object.keys(BRIDGE_TARGETS).join(', ')}) — only meaningful with --bridge`)
@@ -1165,8 +1227,8 @@ credentialFlags(program
1165
1227
  const globalOpts = command.optsWithGlobals();
1166
1228
 
1167
1229
  if (opts.modeling || opts.bridge) {
1168
- if (opts.stack || (opts.modeling && opts.bridge)) {
1169
- console.error('❌ --stack, --modeling, and --bridge are mutually exclusive — pick one.');
1230
+ if (opts.stack || opts.git || (opts.modeling && opts.bridge)) {
1231
+ console.error('❌ --stack/--git, --modeling, and --bridge are mutually exclusive — pick one.');
1170
1232
  process.exit(1);
1171
1233
  }
1172
1234
  }
@@ -1213,6 +1275,33 @@ credentialFlags(program
1213
1275
  return;
1214
1276
  }
1215
1277
 
1278
+ if (opts.branch && !opts.git) {
1279
+ console.error('❌ --branch only applies to --git — nothing to clone without it.');
1280
+ process.exit(1);
1281
+ }
1282
+
1283
+ if (opts.git) {
1284
+ if (!opts.stack) {
1285
+ console.error('❌ --git requires --stack <name> to name the installed stack.');
1286
+ process.exit(1);
1287
+ }
1288
+ if (STACKS[opts.stack]) {
1289
+ console.error(`❌ "${opts.stack}" is already a built-in stack (${Object.keys(STACKS).join(', ')}) — --git is only for installing a stack that isn't built in.`);
1290
+ process.exit(1);
1291
+ }
1292
+ const clonedDir = cloneGitStack(opts.git, opts.branch);
1293
+ const stackCfg = resolveGitStackConfig(clonedDir, opts.stack);
1294
+ await installStack(opts.stack, stackCfg, {
1295
+ configPath: globalOpts.config,
1296
+ print: globalOpts.print,
1297
+ global: opts.global,
1298
+ force: opts.force,
1299
+ credentialOverrides: credentialOverridesFromOpts(opts),
1300
+ templatesSource: join(clonedDir, 'templates'),
1301
+ });
1302
+ return;
1303
+ }
1304
+
1216
1305
  const stackKey = await resolveStack(opts.stack);
1217
1306
  await installStack(stackKey, STACKS[stackKey], {
1218
1307
  configPath: globalOpts.config,
@@ -1393,9 +1482,10 @@ program
1393
1482
 
1394
1483
  program
1395
1484
  .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')
1485
+ .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.')
1486
+ .option('--ollama', 'Use ralph-ollama.js instead of the default runner')
1398
1487
  .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')
1488
+ .option('--claude', 'Force the Claude runner even if a static adapter exists for this target')
1399
1489
  .action((opts) => {
1400
1490
  const cwd = process.cwd();
1401
1491
  const kitDir = findAllInstalledKitDirs(cwd).find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
@@ -1404,8 +1494,8 @@ program
1404
1494
  process.exit(1);
1405
1495
  }
1406
1496
 
1407
- if (opts.ollama && opts.hook) {
1408
- console.error('❌ --ollama and --hook are mutually exclusive — pick one executor.');
1497
+ if ([opts.ollama, opts.hook, opts.claude].filter(Boolean).length > 1) {
1498
+ console.error('❌ --ollama, --hook, and --claude are mutually exclusive — pick one executor.');
1409
1499
  process.exit(1);
1410
1500
  }
1411
1501
 
@@ -1413,13 +1503,23 @@ program
1413
1503
  // plain sibling file — NOT under .eventmodelers/, which is gitignored (see
1414
1504
  // the comment in `init`'s --bridge branch) and would otherwise make this
1415
1505
  // per-machine instead of a shared, checked-in team/CI convention.
1416
- const persistedHook = readJsonSafe(join(kitDir, 'bridge.json')).hookCommand;
1506
+ const bridgeCfg = readJsonSafe(join(kitDir, 'bridge.json'));
1507
+ const persistedHook = bridgeCfg.hookCommand;
1417
1508
  const hookCmd = opts.hook || persistedHook;
1418
1509
 
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';
1510
+ // A static adapter (adapters/<target>-adapter.js, e.g. spec-kitty-adapter.js)
1511
+ // is deterministic and costs no LLM call, so it wins over the Claude default
1512
+ // whenever one exists for the configured target --claude opts back out.
1513
+ const staticAdapterPath = join(kitDir, 'adapters', `${bridgeCfg.target}-adapter.js`);
1514
+ const hasStaticAdapter = bridgeCfg.target && existsSync(staticAdapterPath);
1515
+
1516
+ const runner = hookCmd
1517
+ ? 'ralph-hook.js'
1518
+ : opts.ollama
1519
+ ? 'ralph-ollama.js'
1520
+ : !opts.claude && hasStaticAdapter
1521
+ ? 'ralph-static.js'
1522
+ : 'ralph-claude.js';
1423
1523
  const runnerPath = join(kitDir, runner);
1424
1524
  if (!existsSync(runnerPath)) {
1425
1525
  console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
@@ -1467,6 +1567,7 @@ program
1467
1567
  .requiredOption('--context <name>', 'Name of the MODEL_CONTEXT to fetch')
1468
1568
  .option('--slice-id <id>', 'After fetching, print just the slice with this id')
1469
1569
  .option('--slice-title <title>', 'After fetching, print just the slice with this title (case-insensitive)')
1570
+ .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
1571
  .action(async (opts, command) => {
1471
1572
  const cwd = process.cwd();
1472
1573
  const kitDir = findInstalledKitDir(cwd);
@@ -1521,6 +1622,15 @@ program
1521
1622
  await promptForCredentials();
1522
1623
  await runFetch({ cwd, kitDir: slicesKitDir, cfg, opts });
1523
1624
  }
1625
+
1626
+ if (opts.specKitty) {
1627
+ try {
1628
+ await runSpecKittyAdapter({ cfg, projectDir: cwd, contextName: opts.context });
1629
+ } catch (err) {
1630
+ console.error(`❌ ${err.message}`);
1631
+ process.exit(1);
1632
+ }
1633
+ }
1524
1634
  });
1525
1635
 
1526
1636
  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.30",
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.