@eventmodelers/cli 0.0.22 → 0.0.23

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
@@ -59,6 +59,30 @@ npx @eventmodelers/cli init-modeling --global
59
59
 
60
60
  Everything else (the kit dir, project scaffold, credentials, MCP registration) still targets the current directory as usual — `--global` only changes where skills land.
61
61
 
62
+ ## Bridging to another spec framework
63
+
64
+ If you drive development with a different spec/task framework (Spec Kitty today; more later) instead of build-kit's own code generation, a **bridge** kit keeps that framework's artifacts in sync with the board instead of writing application code:
65
+
66
+ ```bash
67
+ npx @eventmodelers/cli init --bridge --target spec-kitty
68
+ npx @eventmodelers/cli bridge
69
+ ```
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).
72
+
73
+ ### Overriding the executor with a hook
74
+
75
+ Claude is only the default — some teams don't want an AI agent in this loop at all, e.g. they'd rather just commit + push the board export and let a CI pipeline own the actual translation. `--hook` replaces the AI executor with an arbitrary shell command, run once per batch of slice changes:
76
+
77
+ ```bash
78
+ npx @eventmodelers/cli init --bridge --target spec-kitty --hook "git add .slices && git commit -m sync && git push"
79
+ npx @eventmodelers/cli bridge
80
+ ```
81
+
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.
83
+
84
+ 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
+
62
86
  ## Claude execution & config resolution
63
87
 
64
88
  During install you can optionally point the agent at a local LLM server (vLLM, Ollama) instead of the default Claude Code endpoint, and/or pin a specific model:
package/cli.js CHANGED
@@ -80,7 +80,32 @@ const MODELING_KIT = {
80
80
  needsBoardId: false,
81
81
  };
82
82
 
83
- const KIT_DIR_NAMES = [...new Set([...Object.values(STACKS), MODELING_KIT].map((s) => s.kitDirName))];
83
+ // Frameworks a bridge install can translate board slices into. Each key needs
84
+ // a matching `bridge-<key>-specify` skill under shared/bridge/ — see
85
+ // stacks/bridge/templates/bridge/lib/prompt.md for how the loop picks it up.
86
+ const BRIDGE_TARGETS = {
87
+ 'spec-kitty': { label: 'Spec Kitty' },
88
+ };
89
+
90
+ // Also not a stack — no backend scaffold, just the bridge-*/shared skills +
91
+ // the agent loop. Installed via `init --bridge --target <name>` instead of
92
+ // the `init --stack <name>` picker. useShared:true (unlike modeling-kit): a
93
+ // bridge agent reuses build-kit's cold-spawn/tasks.json engine as-is
94
+ // (lib/ralph.js) — it just reacts to every slice change instead of only
95
+ // "Planned" ones (see queueAllStatuses in lib/ralph.js) and translates
96
+ // instead of building. Its own templates/bridge overlay swaps in
97
+ // bridge-specific prompt.md/AGENT.md and a ralph-claude.js that omits
98
+ // onPlannedSlice entirely — see stacks/bridge/templates/bridge.
99
+ const BRIDGE_KIT = {
100
+ key: 'bridge',
101
+ label: 'Bridge — translate board slices into another spec framework, no backend scaffold',
102
+ kitSubdir: 'bridge',
103
+ kitDirName: '.bridge-kit',
104
+ useShared: true,
105
+ needsBoardId: true,
106
+ };
107
+
108
+ const KIT_DIR_NAMES = [...new Set([...Object.values(STACKS), MODELING_KIT, BRIDGE_KIT].map((s) => s.kitDirName))];
84
109
 
85
110
  // Same principle Playwright MCP uses per harness: one shared server, but each coding
86
111
  // agent has its own registration mechanism. Automate the ones with a real, verified
@@ -441,17 +466,24 @@ function readJsonSafe(path) {
441
466
  // Distinguishes this agent process from any other agent pinging the same
442
467
  // token/board — e.g. a build-kit and a modeling-kit install in the same project
443
468
  // share one root config.json, and without a per-agent id both would upsert the
444
- // same alive row and race each other. Written to the kit's OWN config.json
445
- // (inside kitDir, not the shared root one credentials live in) so a build agent
446
- // and a modeling agent never end up with the same id, and persisted so restarts
447
- // of this same kit keep reporting under the same identity.
448
- function ensureAgentId(kitDir) {
449
- const kitConfigPath = join(kitDir, '.eventmodelers', 'config.json');
450
- const existing = readJsonSafe(kitConfigPath);
451
- if (existing.agentId) return existing.agentId;
452
- const agentId = randomUUID();
453
- mkdirSync(dirname(kitConfigPath), { recursive: true });
454
- writeFileSync(kitConfigPath, JSON.stringify({ ...existing, agentId }, null, 2));
469
+ // same alive row and race each other. The platform already keys the alive-ping
470
+ // on the (agent_type, agent_id) pair, so one shared file works: agentIds is
471
+ // namespaced by agentType inside the project ROOT .eventmodelers/config.json
472
+ // the same file credentials already live in instead of each kit dir keeping
473
+ // its own separate config.json (mirrors shared/build-kit/lib/ralph.js's
474
+ // ensureAgentId, duplicated here since this file isn't copied into projects).
475
+ function ensureAgentId(kitDir, agentType) {
476
+ const rootConfigPath = join(dirname(kitDir), '.eventmodelers', 'config.json');
477
+ const rootCfg = readJsonSafe(rootConfigPath);
478
+ rootCfg.agentIds = rootCfg.agentIds || {};
479
+ if (rootCfg.agentIds[agentType]) return rootCfg.agentIds[agentType];
480
+
481
+ const legacyAgentId = readJsonSafe(join(kitDir, '.eventmodelers', 'config.json')).agentId;
482
+
483
+ const agentId = legacyAgentId || randomUUID();
484
+ rootCfg.agentIds[agentType] = agentId;
485
+ mkdirSync(dirname(rootConfigPath), { recursive: true });
486
+ writeFileSync(rootConfigPath, JSON.stringify(rootCfg, null, 2));
455
487
  return agentId;
456
488
  }
457
489
 
@@ -540,6 +572,14 @@ async function installStack(stackKey, stackCfg, options = {}) {
540
572
  // of sync with each other before (e.g. one stack's connect skill silently
541
573
  // missing a bugfix another stack's copy had).
542
574
  const sharedSkills = join(__dirname, 'shared', 'skills');
575
+ // Adapter skills that translate board slices for another spec framework —
576
+ // one subfolder per target (shared/bridge/spec-kitty/bridge-spec-kitty-*,
577
+ // shared/bridge/kiro/..., etc.), so a bridge install only ever pulls in
578
+ // the target it was actually configured for, not every framework's
579
+ // skills. Only relevant to a bridge install — never copied into the four
580
+ // backend stacks or modeling-kit.
581
+ const isBridge = stackKey === BRIDGE_KIT.key;
582
+ const sharedBridgeSkills = isBridge ? join(__dirname, 'shared', 'bridge', options.target) : null;
543
583
 
544
584
  if (!existsSync(templatesSource)) {
545
585
  console.error('❌ Templates directory not found at:', templatesSource);
@@ -552,6 +592,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
552
592
  const claudeSkillsSrc = join(templatesSource, '.claude', 'skills');
553
593
  const installedSkills = [
554
594
  ...(existsSync(sharedSkills) ? readdirSync(sharedSkills) : []),
595
+ ...(isBridge && existsSync(sharedBridgeSkills) ? readdirSync(sharedBridgeSkills) : []),
555
596
  ...(existsSync(claudeSkillsSrc) ? readdirSync(claudeSkillsSrc) : []),
556
597
  ];
557
598
  let claudeExtras = [];
@@ -560,11 +601,13 @@ async function installStack(stackKey, stackCfg, options = {}) {
560
601
  const globalSkillsDir = join(homedir(), '.claude', 'skills');
561
602
  console.log('📦 Installing skills globally...');
562
603
  copyDirContents(sharedSkills, globalSkillsDir);
604
+ if (isBridge) copyDirContents(sharedBridgeSkills, globalSkillsDir);
563
605
  copyDirContents(claudeSkillsSrc, globalSkillsDir);
564
606
  } else {
565
607
  console.log('📦 Installing skills...');
566
608
  copyDirContents(join(templatesSource, '.claude'), join(targetDir, '.claude'));
567
609
  copyDirContents(sharedSkills, join(targetDir, '.claude', 'skills'));
610
+ if (isBridge) copyDirContents(sharedBridgeSkills, join(targetDir, '.claude', 'skills'));
568
611
  claudeExtras = existsSync(join(templatesSource, '.claude'))
569
612
  ? readdirSync(join(templatesSource, '.claude')).filter((f) => f !== 'skills')
570
613
  : [];
@@ -648,7 +691,11 @@ async function installStack(stackKey, stackCfg, options = {}) {
648
691
  );
649
692
 
650
693
  console.log('\n✅ Done! Start your agent:\n');
651
- console.log(' npx @eventmodelers/cli run (--ollama or --bash for other runners)\n');
694
+ if (isBridge) {
695
+ console.log(' npx @eventmodelers/cli bridge\n');
696
+ } else {
697
+ console.log(' npx @eventmodelers/cli run (--ollama or --bash for other runners)\n');
698
+ }
652
699
  console.log('Connect this project to an MCP client (Claude Code, VS Code, ...):\n');
653
700
  console.log(` npx @eventmodelers/cli init-mcp\n`);
654
701
  console.log('Expose these skills to other AI agent hosts (Cursor, Windsurf, Gemini CLI, Copilot, Codex CLI, Kiro, ...):\n');
@@ -846,7 +893,7 @@ async function runModeling(kitDir, projectDir) {
846
893
  const { createClient } = await import('@supabase/supabase-js');
847
894
 
848
895
  const local = loadLocalConfig(kitDir);
849
- local.agentId = local.agentId ?? ensureAgentId(kitDir);
896
+ local.agentId = ensureAgentId(kitDir, 'MODELING');
850
897
  if (!local.token || !local.organizationId) {
851
898
  console.error('❌ --modeling needs platform credentials in .eventmodelers/config.json (token + organizationId) — run `/connect` once or paste your config first.');
852
899
  process.exit(1);
@@ -1075,6 +1122,7 @@ program.hook('preAction', (_thisCommand, actionCommand) => {
1075
1122
  console.error(' Run one of these first:');
1076
1123
  console.error(` npx @eventmodelers/cli init --stack <name> (${Object.keys(STACKS).join(', ')})`);
1077
1124
  console.error(' npx @eventmodelers/cli init --modeling');
1125
+ console.error(` npx @eventmodelers/cli init --bridge --target <name> (${Object.keys(BRIDGE_TARGETS).join(', ')})`);
1078
1126
  process.exit(1);
1079
1127
  });
1080
1128
 
@@ -1097,19 +1145,25 @@ function credentialOverridesFromOpts(opts) {
1097
1145
  credentialFlags(program
1098
1146
  .command('init')
1099
1147
  .alias('install')
1100
- .description('Scaffold a stack + install the agent kit into the current directory (or --modeling for skills + agent loop only, no backend scaffold)')
1148
+ .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)')
1101
1149
  .option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')})`)
1102
- .option('--modeling', 'Install skills + the agent loop only — no backend scaffold. Mutually exclusive with --stack.')
1150
+ .option('--modeling', 'Install skills + the agent loop only — no backend scaffold. Mutually exclusive with --stack/--bridge.')
1151
+ .option('--bridge', 'Install a bridge kit — translates board slices into another spec framework instead of building code. Mutually exclusive with --stack/--modeling. Requires --target.')
1152
+ .option('--target <name>', `Bridge target framework (${Object.keys(BRIDGE_TARGETS).join(', ')}) — only meaningful with --bridge`)
1153
+ .option('--hook <command>', 'Persist a default shell command hook for `bridge` to run per batch of slice changes instead of Claude/Ollama (e.g. commit + push .slices/ for a CI pipeline to pick up) — only meaningful with --bridge. Can also be set per-run with `bridge --hook`.')
1103
1154
  .option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
1104
1155
  .option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json'))
1105
1156
  .action(async (opts, command) => {
1106
1157
  const globalOpts = command.optsWithGlobals();
1107
1158
 
1108
- if (opts.modeling) {
1109
- if (opts.stack) {
1110
- console.error('❌ --modeling and --stack are mutually exclusive — pick one.');
1159
+ if (opts.modeling || opts.bridge) {
1160
+ if (opts.stack || (opts.modeling && opts.bridge)) {
1161
+ console.error('❌ --stack, --modeling, and --bridge are mutually exclusive — pick one.');
1111
1162
  process.exit(1);
1112
1163
  }
1164
+ }
1165
+
1166
+ if (opts.modeling) {
1113
1167
  await installStack(MODELING_KIT.key, MODELING_KIT, {
1114
1168
  configPath: globalOpts.config,
1115
1169
  print: globalOpts.print,
@@ -1120,6 +1174,37 @@ credentialFlags(program
1120
1174
  return;
1121
1175
  }
1122
1176
 
1177
+ if (opts.bridge) {
1178
+ if (!opts.target) {
1179
+ console.error(`❌ --bridge requires --target (${Object.keys(BRIDGE_TARGETS).join(', ')}).`);
1180
+ process.exit(1);
1181
+ }
1182
+ if (!BRIDGE_TARGETS[opts.target]) {
1183
+ console.error(`❌ Unknown bridge target "${opts.target}". Available: ${Object.keys(BRIDGE_TARGETS).join(', ')}`);
1184
+ process.exit(1);
1185
+ }
1186
+ await installStack(BRIDGE_KIT.key, BRIDGE_KIT, {
1187
+ configPath: globalOpts.config,
1188
+ print: globalOpts.print,
1189
+ global: opts.global,
1190
+ force: opts.force,
1191
+ credentialOverrides: credentialOverridesFromOpts(opts),
1192
+ target: opts.target,
1193
+ });
1194
+ // Deliberately NOT under .bridge-kit/.eventmodelers/ — that whole name is
1195
+ // gitignored (a bare `.eventmodelers` pattern matches at any depth, since
1196
+ // it protects the root credentials file), so anything written there is
1197
+ // per-machine only. target/hookCommand are project policy — how this repo
1198
+ // reacts to board changes — meant to be committed and shared by every
1199
+ // teammate and CI runner, so they live in a plain sibling file instead.
1200
+ const bridgeConfigPath = join(process.cwd(), BRIDGE_KIT.kitDirName, 'bridge.json');
1201
+ const existingBridgeCfg = readJsonSafe(bridgeConfigPath);
1202
+ mkdirSync(dirname(bridgeConfigPath), { recursive: true });
1203
+ writeFileSync(bridgeConfigPath, JSON.stringify({ ...existingBridgeCfg, target: opts.target, ...(opts.hook ? { hookCommand: opts.hook } : {}) }, null, 2));
1204
+ console.log(` ✓ Bridge target set to "${opts.target}"${opts.hook ? ` with hook: ${opts.hook}` : ''}`);
1205
+ return;
1206
+ }
1207
+
1123
1208
  const stackKey = await resolveStack(opts.stack);
1124
1209
  await installStack(stackKey, STACKS[stackKey], {
1125
1210
  configPath: globalOpts.config,
@@ -1224,7 +1309,12 @@ program
1224
1309
  // we resolve each stack's dir independently instead of relying on that order.
1225
1310
  const installedKitDirs = findAllInstalledKitDirs(cwd);
1226
1311
  const modelingKitDir = installedKitDirs.find((d) => d.endsWith(MODELING_KIT.kitDirName)) ?? null;
1227
- const buildKitDir = installedKitDirs.find((d) => d !== modelingKitDir) ?? null;
1312
+ const bridgeKitDir = installedKitDirs.find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
1313
+ // A bridge kit is not a build-kit stand-in even though it also reuses
1314
+ // lib/ralph.js — it has its own `eventmodelers bridge` entrypoint (no
1315
+ // onPlannedSlice/--ollama/--bash support), so it's excluded here rather
1316
+ // than falling through to the generic build-kit runner below.
1317
+ const buildKitDir = installedKitDirs.find((d) => d !== modelingKitDir && d !== bridgeKitDir) ?? null;
1228
1318
 
1229
1319
  // No overlap between the two stacks' runtimes: modeling-kit only ever runs the
1230
1320
  // warm, direct-dispatch loop (--modeling); build-kit only ever runs the
@@ -1258,6 +1348,8 @@ program
1258
1348
  if (!buildKitDir) {
1259
1349
  if (modelingKitDir) {
1260
1350
  console.error(`❌ A modeling-kit install (${MODELING_KIT.kitDirName}/) only runs via \`eventmodelers run --modeling\` — there is no cold-spawn/tasks.json loop for modeling-only projects.`);
1351
+ } else if (bridgeKitDir) {
1352
+ console.error(`❌ A bridge-kit install (${BRIDGE_KIT.kitDirName}/) only runs via \`eventmodelers bridge\` — it has no --modeling/--ollama/--bash modes.`);
1261
1353
  } else {
1262
1354
  console.error(`❌ No kit installed in ${cwd} — run \`eventmodelers install\` first.`);
1263
1355
  }
@@ -1291,6 +1383,53 @@ program
1291
1383
  }
1292
1384
  });
1293
1385
 
1386
+ program
1387
+ .command('bridge')
1388
+ .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.')
1389
+ .option('--ollama', 'Use ralph-ollama.js instead of the default Claude runner')
1390
+ .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')
1391
+ .action((opts) => {
1392
+ const cwd = process.cwd();
1393
+ const kitDir = findAllInstalledKitDirs(cwd).find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
1394
+ if (!kitDir) {
1395
+ console.error(`❌ No bridge-kit installed in ${cwd} — run \`eventmodelers init --bridge --target <name>\` first.`);
1396
+ process.exit(1);
1397
+ }
1398
+
1399
+ if (opts.ollama && opts.hook) {
1400
+ console.error('❌ --ollama and --hook are mutually exclusive — pick one executor.');
1401
+ process.exit(1);
1402
+ }
1403
+
1404
+ // A persisted default (from `init --bridge --hook`) lives in bridge.json, a
1405
+ // plain sibling file — NOT under .eventmodelers/, which is gitignored (see
1406
+ // the comment in `init`'s --bridge branch) and would otherwise make this
1407
+ // per-machine instead of a shared, checked-in team/CI convention.
1408
+ const persistedHook = readJsonSafe(join(kitDir, 'bridge.json')).hookCommand;
1409
+ const hookCmd = opts.hook || persistedHook;
1410
+
1411
+ // v1 has no --bash equivalent, unlike `run` — the default/--ollama path
1412
+ // needs an actual agent, not a plain shell script; --hook is the escape
1413
+ // hatch for teams who want a plain shell command instead of an AI agent.
1414
+ const runner = hookCmd ? 'ralph-hook.js' : opts.ollama ? 'ralph-ollama.js' : 'ralph-claude.js';
1415
+ const runnerPath = join(kitDir, runner);
1416
+ if (!existsSync(runnerPath)) {
1417
+ console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
1418
+ process.exit(1);
1419
+ }
1420
+
1421
+ console.log(`▶ Starting ${relative(cwd, runnerPath)}${hookCmd ? ` (hook: ${hookCmd})` : ''}...\n`);
1422
+ try {
1423
+ execSync(`node "${runnerPath}"`, {
1424
+ cwd: kitDir,
1425
+ stdio: 'inherit',
1426
+ env: hookCmd ? { ...process.env, BRIDGE_HOOK_CMD: hookCmd } : process.env,
1427
+ });
1428
+ } catch (err) {
1429
+ process.exit(err.status || 1);
1430
+ }
1431
+ });
1432
+
1294
1433
  program
1295
1434
  .command('listen')
1296
1435
  .description('Start the code-export listener (code-export.mjs) from the installed kit dir — receives slice/screen data pushed from the eventmodelers board UI and writes it into .slices/')
@@ -1409,14 +1548,16 @@ program
1409
1548
  .description('Remove everything init (with or without --modeling) installed: the kit dir, the skills it copied (project-local or ~/.claude/skills with --global), its MCP entry in .claude/settings.json, and any files written by init-agents. Leaves the root project scaffold untouched.')
1410
1549
  .option('--build-kit', `Remove ${STACKS.node.kitDirName}/ (the backend-stack kit dir)`)
1411
1550
  .option('--modeling-kit', `Remove ${MODELING_KIT.kitDirName}/ (the modeling-only kit dir)`)
1551
+ .option('--bridge-kit', `Remove ${BRIDGE_KIT.kitDirName}/ (the bridge kit dir)`)
1412
1552
  .action((opts) => {
1413
1553
  const cwd = process.cwd();
1414
1554
  let targets;
1415
1555
 
1416
- if (opts.buildKit || opts.modelingKit) {
1556
+ if (opts.buildKit || opts.modelingKit || opts.bridgeKit) {
1417
1557
  targets = [];
1418
1558
  if (opts.buildKit) targets.push(join(cwd, STACKS.node.kitDirName));
1419
1559
  if (opts.modelingKit) targets.push(join(cwd, MODELING_KIT.kitDirName));
1560
+ if (opts.bridgeKit) targets.push(join(cwd, BRIDGE_KIT.kitDirName));
1420
1561
  targets = targets.filter((p) => existsSync(p));
1421
1562
  if (!targets.length) {
1422
1563
  console.log('ℹ️ Nothing to remove for the requested option(s).');
@@ -1429,7 +1570,7 @@ program
1429
1570
  return;
1430
1571
  }
1431
1572
  if (targets.length > 1) {
1432
- console.log('⚠️ Multiple kit dirs found — re-run with --build-kit or --modeling-kit to pick one, or both to remove everything.');
1573
+ console.log('⚠️ Multiple kit dirs found — re-run with --build-kit, --modeling-kit, and/or --bridge-kit to pick which to remove.');
1433
1574
  targets.forEach((t) => console.log(` ${t}`));
1434
1575
  return;
1435
1576
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
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": {
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: bridge-spec-kitty-specify
3
+ description: "Adapter: generate a Spec Kitty mission spec.md (and auto-evaluate its quality checklist) from an Eventmodelers board's exported event model, instead of running the /spec-kitty.specify interview. Use when the user says \"use the event model as the spec\", \"sync the board to spec-kitty\", \"generate the spec from the event model\", or when a .slices/ export exists and a Spec Kitty mission needs to start or refresh from it."
4
+ ---
5
+
6
+ # bridge-spec-kitty-specify
7
+
8
+ Replaces the human interview in `/spec-kitty.specify` with a direct read of an
9
+ Eventmodelers board export. The event model is the source of truth; `spec.md`
10
+ becomes a **generated, read-only artifact** derived from it. Everything
11
+ downstream of specify (`/spec-kitty.plan`, `/spec-kitty.tasks`,
12
+ implement/review/merge) is untouched — this skill only produces the two
13
+ artifacts that phase gate expects: `spec.md` and `checklists/requirements.md`.
14
+
15
+ Do not use this to hand-author or hand-edit `spec.md` — if the event model is
16
+ wrong, fix it on the board and re-run this skill. A `spec.md` produced here
17
+ that gets manually edited afterward will be silently overwritten on the next
18
+ sync; there is no merge/diff in this version (see Limitations).
19
+
20
+ ## Inputs
21
+
22
+ The Eventmodelers `listen` server (`npx @eventmodelers/cli listen`) writes the
23
+ board export locally. Read it directly — do not call the board API:
24
+
25
+ - `.slices/current_context.json` → `{ "name": "<context>" }` — which context
26
+ to ingest if the user didn't name one.
27
+ - `.slices/<context>/index.json` → `{ "slices": [{ id, slice, index, context,
28
+ folder, status, group }] }` — the ordered slice list.
29
+ - `.slices/<context>/context.json` → context-level name/package info.
30
+ - `.slices/<context>/<folder>/slice.json` → full slice detail per the
31
+ [event-modeling-spec](https://github.com/dilgerma/event-modeling-spec)
32
+ contract: `commands[]`, `events[]`, `readModels[]` (projections/queries),
33
+ `screens[]`, `automations[]`/`processors[]`, `specifications[]` (given/when/
34
+ then), plus free-text `description`/`notes`.
35
+
36
+ If `.slices/` doesn't exist yet, stop and tell the user to run
37
+ `npx @eventmodelers/cli listen` (or `init-modeling` first if the kit isn't
38
+ installed) and push the board export before retrying.
39
+
40
+ ## Flow
41
+
42
+ 1. **Resolve the mission.** If an existing `kitty-specs/<slug>/meta.json`
43
+ already points at this context (check `purpose_tldr`/a stored context
44
+ reference), re-sync into it. Otherwise create a new mission dir the same
45
+ way `/spec-kitty.specify` does: kebab-case name + 8-char ULID suffix,
46
+ `mission_type: software-dev` (this adapter does not introduce a new
47
+ mission type — see project memory on why). Write `meta.json` matching the
48
+ shape in an existing mission (`mission_id`, `mission_slug`, `purpose_tldr`,
49
+ `purpose_context`, `target_branch`, etc.).
50
+ 2. **Load every slice** referenced in `index.json` by reading each
51
+ `<folder>/slice.json`. Do not skip slices with empty `specifications` —
52
+ surface them as gaps in the checklist instead (step 4).
53
+ 3. **Compose `spec.md`** using the standard Spec Kitty template sections,
54
+ sourced only from what the model actually states — never invent detail
55
+ the board doesn't have:
56
+ - **Purpose** ← `context.json` name/description, or ask the user for a
57
+ one-paragraph purpose if the board has none.
58
+ - **Primary User Story** ← walk the slices in `index.json` order (this is
59
+ already the chronological/timeline order from the board) and narrate
60
+ the SCREEN → COMMAND → EVENT → READMODEL flow across them.
61
+ - **Acceptance Scenarios** ← one bullet per `specifications[]` entry,
62
+ copied straight from its given/when/then — do not paraphrase away
63
+ precision the model already captured.
64
+ - **Edge Cases** ← `specifications[]` entries that describe rejection/
65
+ negative/boundary behavior (reject, invalid, already-exists, etc.).
66
+ - **Domain Language** ← distinct nouns from event/command/read-model field
67
+ names and titles, deduped.
68
+ - **Functional Requirements (FR-###)** ← one row per COMMAND (state-change
69
+ slice) and per AUTOMATION slice; wording from the command/automation
70
+ name + `description`.
71
+ - **Non-Functional Requirements / Constraints** ← only from explicit
72
+ `notes`/`description` content tagged as timing/authorization/volume
73
+ constraints. If the model states none, leave the table empty — do not
74
+ fabricate NFRs to fill it.
75
+ - **Key Entities** ← distinct read-model/event payload shapes.
76
+ - **Interfaces to Other Teams** ← any `TRANSLATION`-type slice or external
77
+ system mentioned in a slice's `description`.
78
+ 4. **Evaluate `checklists/requirements.md`** against the ingested model
79
+ instead of a human review pass. Check an item only if the model
80
+ demonstrably satisfies it:
81
+ - "No [NEEDS CLARIFICATION] markers remain" → fails if any slice has an
82
+ empty `specifications[]` or a `description` containing `TODO`/`TBD`.
83
+ - "Requirements are testable and unambiguous" → fails if an FR/NFR row has
84
+ no backing `specifications[]` entry.
85
+ - "Non-functional requirements include measurable thresholds" → fails
86
+ (leave unchecked, don't skip the row) if the NFR table is empty but the
87
+ model contains AUTOMATION slices implying a timing constraint the board
88
+ never made explicit.
89
+ - Leave any check unchecked with a `## Notes` explanation when the event
90
+ model genuinely doesn't provide enough to verify it — this checklist
91
+ gates `/spec-kitty.plan`, so a false pass defeats the point of the gate.
92
+ 5. **Hand off.** Once `spec.md` + `checklists/requirements.md` are written,
93
+ tell the user the mission is ready for `/spec-kitty.plan` as normal — do
94
+ not run plan/tasks yourself from this skill.
95
+
96
+ ## Re-sync behavior
97
+
98
+ Re-running this skill against the same mission regenerates `spec.md` and
99
+ `checklists/requirements.md` wholesale from the current `.slices/` state.
100
+ There is no field-level diff against the previous generation — if the spec
101
+ needs to reflect board changes, re-run this skill rather than hand-patching
102
+ `spec.md`.
103
+
104
+ ## Guardrail
105
+
106
+ If a required section can't be populated from the event model (e.g. no
107
+ Purpose anywhere on the board), ask the user rather than inventing one — an
108
+ invented Purpose defeats the reason this adapter exists (the event model,
109
+ not the LLM, is the source of truth for product intent).
110
+
111
+ ## Limitations (by design, v1)
112
+
113
+ - Does not itself generate `tasks/WP##-*.md` from slices — see the companion
114
+ `bridge-spec-kitty-tasks` skill, which enforces a 1:1
115
+ slice-to-WP mapping when `wps.yaml` is written during
116
+ `/spec-kitty.tasks-outline`.
117
+ - Does not introduce a new Spec Kitty mission type. Spec Kitty 3.2.5's
118
+ built-in mission types are loaded from a flat directory inside the
119
+ installed package (`doctrine/missions/mission_types/*.yaml`) via
120
+ `MissionTypeRepository.default()`, and org-pack extension for the
121
+ `mission_types` kind is unwired in this version (fragments only add
122
+ governance-graph metadata, not an executable type). A real native mission
123
+ type would require patching the installed `spec-kitty-cli` package itself.
124
+ This adapter avoids that entirely — it stays inside `mission_type:
125
+ software-dev` and only replaces the specify phase's *inputs*.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: bridge-spec-kitty-tasks
3
+ description: "Adapter: enforce a 1:1 mapping between Eventmodelers board slices and Spec Kitty work packages when writing wps.yaml. Use when the user says \"one slice should become one task/WP\", \"generate tasks from the event model\", or right after bridge-spec-kitty-specify, in place of freeform subtask-grouping judgment in /spec-kitty.tasks-outline."
4
+ ---
5
+
6
+ # bridge-spec-kitty-tasks
7
+
8
+ Companion to `bridge-spec-kitty-specify`. That skill turns the event model into
9
+ `spec.md`; this one enforces that each **slice** on the board becomes exactly
10
+ **one work package** — never split across WPs, never merged with another
11
+ slice — when Spec Kitty's task-planning pipeline builds `wps.yaml`.
12
+
13
+ This replaces only step 3–6 of `/spec-kitty.tasks-outline` (deriving
14
+ subtasks and rolling them into WPs). `/spec-kitty.tasks-packages` and
15
+ `/spec-kitty.tasks-finalize` run completely unmodified afterward — they only
16
+ ever consume `wps.yaml`, and don't care where its groupings came from.
17
+
18
+ ## Why a hard 1:1 boundary
19
+
20
+ Spec Kitty's default `tasks-outline` groups subtasks into WPs by
21
+ judgment (target 3–7 subtasks, merge small ones, split large ones). That's
22
+ the right default when subtask boundaries come from an LLM reading prose. It
23
+ is the *wrong* default here: the event model already drew the real
24
+ boundaries (each SLICE_BORDER is a reviewable, independently buildable unit
25
+ the board's `Planned → InProgress → Done` slice lifecycle already governs).
26
+ Re-grouping at the WP layer would create a second, competing notion of "unit
27
+ of work" that drifts from the board over time. So this skill trades away
28
+ `tasks-outline`'s sizing flexibility on purpose: WP boundary = slice
29
+ boundary, full stop.
30
+
31
+ ## Inputs
32
+
33
+ Same event-model export as `bridge-spec-kitty-specify`:
34
+ `.slices/<context>/index.json` (slice list, board/timeline order) and each
35
+ `.slices/<context>/<folder>/slice.json` (commands, events, readModels,
36
+ automations, `specifications`). Also read, from `feature_dir`:
37
+ `spec.md` (for FR/NFR ids — already generated by `bridge-spec-kitty-specify`) and
38
+ `plan.md` (for the tech-stack/file-layout context needed to fill
39
+ `owned_files`, since the event model doesn't know the codebase's directory
40
+ structure).
41
+
42
+ ## Flow
43
+
44
+ 1. **One WP per slice, in board order.** Assign `WP01`, `WP02`, ... following
45
+ `index.json`'s order (already the board's chronological/timeline order).
46
+ Do not reorder for "phase grouping" the way default `tasks-outline` does —
47
+ the board order already encodes the intended build sequence.
48
+ 2. **Subtasks come from inside the slice, not across slices.** For each
49
+ slice, break its own commands/read-models/screens/automations into T-ids
50
+ (one per command handler, one per read-model endpoint, one per
51
+ screen/automation, one for its tests) — all nested under that slice's one
52
+ WP. Every subtask must trace to exactly one slice; never let a subtask
53
+ span two slices.
54
+ 3. **Dependencies from event flow, not guesswork.** WP *B* depends on WP *A*
55
+ iff slice B's commands/queries/automations consume an event that slice
56
+ A's commands/automations produce. Derive this by diffing each slice's
57
+ emitted event names against every other slice's consumed event names —
58
+ this is mechanical, not judgment-based, so don't also apply
59
+ `tasks-outline`'s "Phase 2 typically depends on Phase 1" heuristic on top
60
+ of it.
61
+ 4. **`requirement_refs` from the spec's own FR/NFR table.** Match each
62
+ slice's command/read-model/automation names against `spec.md`'s FR/NFR
63
+ rows (written by `bridge-spec-kitty-specify` from these same slices) and cite
64
+ whichever ids trace to this slice. A slice with no matching row means
65
+ `bridge-spec-kitty-specify` needs a re-run, not a fabricated ref here.
66
+ 5. **Write `wps.yaml`** using the exact schema `/spec-kitty.tasks-outline`
67
+ expects (`id`, `title`, `dependencies`, `owned_files`, `requirement_refs`,
68
+ `plan_concern_refs` or `cross_cutting: true`, `subtasks`, `prompt_file:
69
+ null`) — so `tasks-packages`/`finalize-tasks` need no awareness that this
70
+ skill (rather than default `tasks-outline`) produced it.
71
+ 6. **Hand off.** Tell the user to continue with `/spec-kitty.tasks-packages`
72
+ then `/spec-kitty.tasks-finalize` (or `spec-kitty next`) exactly as normal.
73
+
74
+ ## Guardrail: don't fix boundary problems here
75
+
76
+ If a slice's own scope would blow past Spec Kitty's WP sizing ceiling (>10
77
+ subtasks / >700 lines), **do not silently split it into two WPs** — that
78
+ breaks the 1:1 invariant this skill exists to enforce, and produces the
79
+ exact spec/code drift it's meant to prevent. Instead, flag it to the user:
80
+ the slice is too coarse on the board itself, so split the SLICE_BORDER
81
+ there and re-run both `bridge-spec-kitty-specify` and this skill. The event model
82
+ stays the single place slice boundaries get decided.
83
+
84
+ The same holds in the other direction: never merge two small slices into
85
+ one WP for tidiness, even if `tasks-outline`'s default guidance would.
86
+
87
+ ## Limitations (v1)
88
+
89
+ - `owned_files`/file-layout ownership still comes from `plan.md`, which is
90
+ still produced by the normal, LLM-authored `/spec-kitty.plan` — the event
91
+ model has no notion of source-file structure, so this isn't something to
92
+ push onto it.
93
+ - Re-running this skill after the board changes regenerates `wps.yaml`
94
+ wholesale from the current slice set, same as `bridge-spec-kitty-specify`'s
95
+ re-sync behavior — no field-level diff against a prior run.
@@ -109,26 +109,42 @@ function hasCredentials(cfg) {
109
109
  }
110
110
 
111
111
  // Distinguishes this agent process from any other agent pinging the same
112
- // token/board — e.g. a build-kit and a modeling-kit install in the same project
112
+ // token/board — e.g. a build-kit and a bridge-kit install in the same project
113
113
  // share one root config.json, and without a per-agent id both would upsert the
114
- // same alive row and race each other. Written to the kit's OWN config.json
115
- // (inside kitDir, not the shared root one credentials live in) so a build agent
116
- // and a modeling agent never end up with the same id, and persisted so restarts
117
- // of this same kit keep reporting under the same identity.
118
- function ensureAgentId(kitDir) {
119
- const kitConfigPath = join(kitDir, '.eventmodelers', 'config.json');
120
- let existing = {};
121
- if (existsSync(kitConfigPath)) {
114
+ // same alive row and race each other. The platform already keys the alive-ping
115
+ // on the (agent_type, agent_id) pair, so one shared file works: agentIds is
116
+ // namespaced by agentType (BUILD/BRIDGE/MODELING/...) inside the project ROOT
117
+ // .eventmodelers/config.json the same file credentials already live in
118
+ // instead of each kit dir keeping its own separate config.json. Falls back to
119
+ // a pre-existing kit-local agentId (older installs, before this consolidation)
120
+ // so an upgrade doesn't mint a new identity the platform hasn't seen before.
121
+ function ensureAgentId(kitDir, agentType) {
122
+ const rootConfigPath = join(dirname(kitDir), '.eventmodelers', 'config.json');
123
+ let rootCfg = {};
124
+ if (existsSync(rootConfigPath)) {
122
125
  try {
123
- existing = JSON.parse(readFileSync(kitConfigPath, 'utf-8'));
126
+ rootCfg = JSON.parse(readFileSync(rootConfigPath, 'utf-8'));
124
127
  } catch {
125
- console.warn(`[ralph] Skipping invalid config at ${kitConfigPath}`);
128
+ console.warn(`[ralph] Skipping invalid config at ${rootConfigPath}`);
126
129
  }
127
130
  }
128
- if (existing.agentId) return existing.agentId;
129
- const agentId = randomUUID();
130
- mkdirSync(dirname(kitConfigPath), { recursive: true });
131
- writeFileSync(kitConfigPath, JSON.stringify({ ...existing, agentId }, null, 2));
131
+ rootCfg.agentIds = rootCfg.agentIds || {};
132
+ if (rootCfg.agentIds[agentType]) return rootCfg.agentIds[agentType];
133
+
134
+ const legacyKitConfigPath = join(kitDir, '.eventmodelers', 'config.json');
135
+ let legacyAgentId;
136
+ if (existsSync(legacyKitConfigPath)) {
137
+ try {
138
+ legacyAgentId = JSON.parse(readFileSync(legacyKitConfigPath, 'utf-8')).agentId;
139
+ } catch {
140
+ console.warn(`[ralph] Skipping invalid config at ${legacyKitConfigPath}`);
141
+ }
142
+ }
143
+
144
+ const agentId = legacyAgentId || randomUUID();
145
+ rootCfg.agentIds[agentType] = agentId;
146
+ mkdirSync(dirname(rootConfigPath), { recursive: true });
147
+ writeFileSync(rootConfigPath, JSON.stringify(rootCfg, null, 2));
132
148
  return agentId;
133
149
  }
134
150
 
@@ -226,7 +242,7 @@ async function writeTask(payload, kitDir) {
226
242
  console.log(`[agent] Task written — slice="${payload.sliceTitle}" status="${payload.sliceStatus}"`);
227
243
  }
228
244
 
229
- async function startRealtimeAgent(cfg, kitDir) {
245
+ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', queueAllStatuses = false } = {}) {
230
246
  let realtimeToken = await retryOn401('getRealtimeToken', () => getRealtimeToken(cfg));
231
247
 
232
248
  await retryOn401('fetchAndPersistSlices', () => fetchAndPersistSlices(cfg, kitDir)).catch((err) =>
@@ -254,8 +270,11 @@ async function startRealtimeAgent(cfg, kitDir) {
254
270
  await retryOn401('fetchAndPersistSlices', () => fetchAndPersistSlices(cfg, kitDir)).catch((err) =>
255
271
  console.error('[agent] Slice persist error:', err),
256
272
  );
257
- // Planned slices are handled by onPlannedSlice directly — no task needed
258
- if ((payload.sliceStatus || '').toLowerCase() !== 'planned') {
273
+ // Planned slices are handled by onPlannedSlice directly — no task needed.
274
+ // queueAllStatuses opts out of that split entirely (e.g. bridge has no
275
+ // onPlannedSlice consumer, so a lingering Planned slice would otherwise
276
+ // never naturally clear its own trigger — see lib/ralph.js callers).
277
+ if (queueAllStatuses || (payload.sliceStatus || '').toLowerCase() !== 'planned') {
259
278
  await writeTask(payload, kitDir).catch((err) => console.error('[agent] writeTask error:', err));
260
279
  }
261
280
  })
@@ -276,7 +295,7 @@ async function startRealtimeAgent(cfg, kitDir) {
276
295
  const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
277
296
  method: 'POST',
278
297
  headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
279
- body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: 'BUILD', agent_id: cfg.agentId }),
298
+ body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: agentType, agent_id: cfg.agentId }),
280
299
  signal: AbortSignal.timeout(10_000),
281
300
  });
282
301
  if (!res.ok) console.error(`[agent] Ping failed: ${res.status} ${await res.text().catch(() => '')}`);
@@ -379,9 +398,9 @@ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice) {
379
398
 
380
399
  export { loadLocalConfig, fetchPlatformConfig, retryOn401, startRealtimeAgent };
381
400
 
382
- export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice }) {
401
+ export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice, agentType = 'BUILD', queueAllStatuses = false }) {
383
402
  const local = loadLocalConfig(kitDir);
384
- local.agentId = local.agentId ?? ensureAgentId(kitDir);
403
+ local.agentId = ensureAgentId(kitDir, agentType);
385
404
 
386
405
  console.log(`Ralph — kit: ${kitDir}`);
387
406
  console.log(` project: ${projectDir}`);
@@ -396,7 +415,7 @@ export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice })
396
415
  console.log(` org=${cfg.organizationId}, board=${cfg.boardId}, base=${cfg.baseUrl}\n`);
397
416
 
398
417
  await Promise.all([
399
- startRealtimeAgent(cfg, kitDir),
418
+ startRealtimeAgent(cfg, kitDir, { agentType, queueAllStatuses }),
400
419
  ralphLoop(kitDir, cfg, onTask, onPlannedSlice),
401
420
  ]);
402
421
  }
@@ -0,0 +1,47 @@
1
+ # Agent Learnings
2
+
3
+ Patterns and gotchas discovered during task processing. Update this file
4
+ whenever you encounter something reusable.
5
+
6
+ ## tasks.json
7
+
8
+ - Tasks are objects with `id`, `createdAt`, and `payload` (a
9
+ `SliceChangedPayload`).
10
+ - Unlike build-kit, every status change is queued (not just non-`Planned`
11
+ ones) — see `queueAllStatuses` in `lib/ralph.js`.
12
+ - After completing a task, remove it from the array entirely — do not add a
13
+ status field.
14
+ - Write `[]` to `tasks.json` if the last task is completed.
15
+
16
+ ## SliceChangedPayload fields
17
+
18
+ ```
19
+ event always "slice:changed"
20
+ organizationId org UUID or null
21
+ boardId board UUID
22
+ sliceId SLICE_BORDER node UUID
23
+ sliceTitle human-readable slice name (may be null)
24
+ sliceStatus e.g. "Created", "Planned", "InProgress", "Done" (may be null)
25
+ timestamp unix ms when the change was emitted
26
+ ```
27
+
28
+ ## Bridge target
29
+
30
+ - The active target framework lives in `.bridge-kit/bridge.json`'s `target`
31
+ field, written at `init --bridge --target <name>` time. This file is
32
+ committed (unlike `.eventmodelers/config.json`, which is gitignored) since
33
+ it's project policy shared by every teammate and CI runner, not a
34
+ per-machine credential.
35
+ - Skill naming convention: `bridge-<target>-specify` (kept in sync every
36
+ task) and `bridge-<target>-tasks` (invoked separately, by that framework's
37
+ own task-planning phase — not from this loop).
38
+
39
+ ## Executors
40
+
41
+ - Claude (`ralph-claude.js`, this prompt) is the default. `bridge --ollama`
42
+ swaps in a local model instead — same prompt, different executor.
43
+ - `bridge --hook "<command>"` (or a `hookCommand` persisted in
44
+ `bridge.json`) bypasses this prompt entirely: `ralph-hook.js` runs an
45
+ arbitrary shell command per batch of changes instead of any AI agent — e.g.
46
+ commit + push `.slices/` and let a CI pipeline do the actual translation.
47
+ If you're reading this file, a hook wasn't configured for this run.
@@ -0,0 +1,111 @@
1
+ # Bridge Task Instructions
2
+
3
+ You are an autonomous agent reacting to slice status change events on an
4
+ Eventmodelers board. Unlike a build-kit agent, you do **not** write
5
+ application code — you translate the board's event model into artifacts for
6
+ another spec framework (e.g. Spec Kitty), keeping that framework's files in
7
+ sync with the board as it changes.
8
+
9
+ ## Your Loop
10
+
11
+ 1. Read `AGENT.md` to load accumulated learnings before doing anything else.
12
+ 2. Read `.bridge-kit/tasks.json`.
13
+ 3. If `tasks.json` is empty or missing, reply with:
14
+ <promise>IDLE</promise>
15
+ and stop.
16
+ 4. Pick the **oldest task** (earliest `createdAt`).
17
+ 5. Execute the task — see the Execution section below.
18
+ 6. After execution, remove that task from the array and write
19
+ `.bridge-kit/tasks.json` back.
20
+ 7. Append a progress entry to `progress.txt` (create if missing).
21
+ 8. Update `AGENT.md` with any new reusable learnings discovered this
22
+ iteration.
23
+ 9. Reply normally so the next iteration can pick up the next task.
24
+
25
+ ## Execution
26
+
27
+ Each task has a single `payload` of type `SliceChangedPayload`:
28
+
29
+ ```
30
+ {
31
+ event: "slice:changed"
32
+ organizationId: string | null
33
+ boardId: string
34
+ sliceId: string ← SLICE_BORDER node UUID
35
+ sliceTitle: string | null
36
+ sliceStatus: string | null ← e.g. "Planned", "InProgress", "Done"
37
+ timestamp: number
38
+ }
39
+ ```
40
+
41
+ Unlike a build-kit agent, a bridge agent reacts to **every** status change
42
+ (including `Planned`) — a task is queued regardless of `sliceStatus`. There
43
+ is no separate "claim and build" step, so don't invoke `/update-slice-status`
44
+ here; translation doesn't take ownership of a slice the way building does.
45
+
46
+ ### Step 1 — Load credentials
47
+
48
+ Run `/connect` to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL` from
49
+ `.eventmodelers/config.json`.
50
+
51
+ ### Step 2 — Resolve the bridge target
52
+
53
+ Read `.bridge-kit/bridge.json`'s `target` field (e.g. `"spec-kitty"`). The
54
+ matching translation skill is named `bridge-<target>-specify`.
55
+
56
+ ### Step 3 — Translate
57
+
58
+ Invoke the `bridge-<target>-specify` skill. It reads `.slices/` (written by
59
+ `npx @eventmodelers/cli listen`, which must be running separately — see that
60
+ skill's own guardrail if `.slices/` doesn't exist yet) and regenerates that
61
+ framework's spec artifacts **wholesale** from the current board state. Do
62
+ not hand-author or patch those artifacts yourself — if they're wrong, the
63
+ event model is wrong; fix it on the board and let the next slice change
64
+ re-trigger this skill.
65
+
66
+ Do **not** also invoke `bridge-<target>-tasks` from this loop. That
67
+ companion skill enforces the 1:1 slice-to-work-package mapping during that
68
+ framework's own task-planning phase (e.g. Spec Kitty's
69
+ `/spec-kitty.tasks-outline`) — it's triggered by that framework's own
70
+ tooling when the user reaches that phase, not by every board change.
71
+
72
+ ## Updating tasks.json
73
+
74
+ After completing a task, remove it from the array and write the updated
75
+ array back to `.bridge-kit/tasks.json`. If the array is now empty, write
76
+ `[]`.
77
+
78
+ ## Progress Report Format
79
+
80
+ APPEND to `progress.txt` (never replace):
81
+ ```
82
+ ## [ISO timestamp] — Task [task.id]
83
+
84
+ Slice: [sliceTitle] ([sliceId])
85
+ Status change: [sliceStatus]
86
+
87
+ Action taken:
88
+ - [what was translated / regenerated in response to the slice change]
89
+
90
+ Learnings:
91
+ - [any patterns, gotchas, or reusable knowledge discovered]
92
+ ---
93
+ ```
94
+
95
+ ## Stop Condition
96
+
97
+ If `.bridge-kit/tasks.json` is empty (`[]`) or does not exist, reply with:
98
+ <promise>IDLE</promise>
99
+
100
+ ## Updating AGENT.md
101
+
102
+ After completing a task, add any **reusable** learnings to `AGENT.md` —
103
+ patterns, gotchas, or skill behaviour that future iterations should know.
104
+ Only add things that are general and applicable beyond this single task. Do
105
+ not duplicate what is already there.
106
+
107
+ ## Important
108
+
109
+ - Process **one task per iteration**.
110
+ - Read `AGENT.md` first — it contains patterns from previous iterations.
111
+ - Always start with `/connect` if credentials are not yet loaded.
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // Bridge loop using Claude Code as the executor. Unlike build-kit's
3
+ // ralph-claude.js, there is no onPlannedSlice consumer — a bridge agent
4
+ // doesn't "build" a Planned slice, it just translates every slice change as
5
+ // it arrives via tasks.json (see lib/prompt.md and queueAllStatuses below).
6
+ // Usage: node ralph-claude.js [project_dir]
7
+
8
+ import { startRalph, loadLocalConfig } from './lib/ralph.js';
9
+ import { spawn } from 'child_process';
10
+ import { dirname, resolve } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const kitDir = dirname(fileURLToPath(import.meta.url));
14
+ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
15
+
16
+ const cfg = loadLocalConfig(kitDir);
17
+ const inlineHeader = cfg.boardId
18
+ ? `board=${cfg.boardId} token=${cfg.token} org=${cfg.organizationId} baseUrl=${cfg.baseUrl}\n\n`
19
+ : '';
20
+
21
+ const claudeArgs = ['--dangerously-skip-permissions'];
22
+ if (cfg.model) claudeArgs.push('--model', cfg.model);
23
+ const claudeEnv = cfg.anthropicBaseUrl
24
+ ? { ...process.env, ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl }
25
+ : process.env;
26
+
27
+ function runClaude(prompt) {
28
+ return new Promise((resolve, reject) => {
29
+ const proc = spawn('claude', [...claudeArgs, '-p', inlineHeader + prompt], {
30
+ cwd: projectDir,
31
+ stdio: 'inherit',
32
+ env: claudeEnv,
33
+ });
34
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Claude exited ${code}`))));
35
+ proc.on('error', reject);
36
+ });
37
+ }
38
+
39
+ startRalph({
40
+ kitDir,
41
+ projectDir,
42
+ onTask: runClaude,
43
+ agentType: 'BRIDGE',
44
+ queueAllStatuses: true,
45
+ }).catch((err) => {
46
+ console.error('[ralph] Fatal:', err);
47
+ process.exit(1);
48
+ });
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ // Bridge loop that hands each batch of slice changes to an arbitrary external
3
+ // command instead of an AI agent — e.g. commit + push .slices/ and let a CI
4
+ // pipeline take it from there. No Claude/Ollama call happens in this mode.
5
+ //
6
+ // Configure the hook with `bridge --hook "<command>"` (one-off) or persist a
7
+ // default with `init --bridge --target <name> --hook "<command>"`.
8
+ //
9
+ // Usage: node ralph-hook.js [project_dir]
10
+ // BRIDGE_HOOK_CMD="git add .slices && git commit -m sync && git push" node ralph-hook.js
11
+
12
+ import { startRalph } from './lib/ralph.js';
13
+ import { spawn } from 'child_process';
14
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
15
+ import { dirname, join, resolve } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const kitDir = dirname(fileURLToPath(import.meta.url));
19
+ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
20
+
21
+ // bridge.json (not .eventmodelers/config.json) — a plain, committed sibling
22
+ // file, since target/hookCommand are project policy meant to be shared with
23
+ // every teammate and CI runner, not gitignored per-machine state.
24
+ function loadBridgeConfig() {
25
+ const p = join(kitDir, 'bridge.json');
26
+ if (!existsSync(p)) return {};
27
+ try { return JSON.parse(readFileSync(p, 'utf-8')); } catch { return {}; }
28
+ }
29
+
30
+ const hookCmd = process.env.BRIDGE_HOOK_CMD || loadBridgeConfig().hookCommand;
31
+ if (!hookCmd) {
32
+ console.error('[bridge-hook] No hook command configured.');
33
+ console.error(' Set one for this run: eventmodelers bridge --hook "<command>"');
34
+ console.error(' Or persist a default: eventmodelers init --bridge --target <name> --hook "<command>"');
35
+ process.exit(1);
36
+ }
37
+
38
+ const tasksPath = join(kitDir, 'tasks.json');
39
+
40
+ function readTasks() {
41
+ try {
42
+ return JSON.parse(readFileSync(tasksPath, 'utf-8'));
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+
48
+ // Batched, not one-task-at-a-time: a hook like "commit + push .slices/" acts
49
+ // on the whole current board export in one shot (already fresh — lib/ralph.js
50
+ // re-syncs .slices/ before every task is queued), not on a single slice's
51
+ // translation the way an AI agent does. Only the tasks present at invocation
52
+ // time are cleared afterward — anything the realtime agent queues *while* the
53
+ // hook is still running is left in place for the next tick, so a slice change
54
+ // arriving mid-run is never silently dropped.
55
+ function runHook() {
56
+ const batch = readTasks();
57
+ if (!batch.length) return Promise.resolve();
58
+
59
+ const batchFilePath = join(kitDir, 'last-hook-batch.json');
60
+ mkdirSync(kitDir, { recursive: true });
61
+ writeFileSync(batchFilePath, JSON.stringify(batch, null, 2));
62
+
63
+ const latest = batch[batch.length - 1]?.payload ?? {};
64
+
65
+ return new Promise((resolvePromise, reject) => {
66
+ console.log(`[bridge-hook] Running hook for ${batch.length} change(s): ${hookCmd}`);
67
+ const proc = spawn(hookCmd, {
68
+ cwd: projectDir,
69
+ shell: true,
70
+ stdio: 'inherit',
71
+ env: {
72
+ ...process.env,
73
+ BRIDGE_BATCH_FILE: batchFilePath,
74
+ BRIDGE_TASK_COUNT: String(batch.length),
75
+ BRIDGE_SLICE_ID: latest.sliceId ?? '',
76
+ BRIDGE_SLICE_TITLE: latest.sliceTitle ?? '',
77
+ BRIDGE_SLICE_STATUS: latest.sliceStatus ?? '',
78
+ BRIDGE_BOARD_ID: latest.boardId ?? '',
79
+ BRIDGE_ORGANIZATION_ID: latest.organizationId ?? '',
80
+ },
81
+ });
82
+ proc.on('close', (code) => {
83
+ if (code !== 0) return reject(new Error(`hook exited ${code}`));
84
+ const handledIds = new Set(batch.map((t) => t.id));
85
+ const remaining = readTasks().filter((t) => !handledIds.has(t.id));
86
+ writeFileSync(tasksPath, JSON.stringify(remaining, null, 2), 'utf-8');
87
+ resolvePromise();
88
+ });
89
+ proc.on('error', reject);
90
+ });
91
+ }
92
+
93
+ startRalph({
94
+ kitDir,
95
+ projectDir,
96
+ onTask: runHook,
97
+ // onPlannedSlice omitted — see stacks/bridge/templates/bridge/ralph-claude.js
98
+ agentType: 'BRIDGE',
99
+ queueAllStatuses: true,
100
+ }).catch((err) => {
101
+ console.error('[ralph] Fatal:', err);
102
+ process.exit(1);
103
+ });
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ // Bridge loop using a local Ollama model as the executor. Same caveats as
3
+ // build-kit's ralph-ollama.js — lib/ollama-agent.js is shared as-is (see
4
+ // useShared in cli.js), unmodified for bridge.
5
+ // Run `ollama serve` first.
6
+ // Usage: node ralph-ollama.js [project_dir]
7
+ // OLLAMA_MODEL=qwen3.5:9b node ralph-ollama.js
8
+ // OLLAMA_URL=http://host:11434 node ralph-ollama.js
9
+
10
+ import { startRalph } from './lib/ralph.js';
11
+ import { spawn } from 'child_process';
12
+ import { dirname, join, resolve } from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const kitDir = dirname(fileURLToPath(import.meta.url));
16
+ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
17
+ const model = process.env.OLLAMA_MODEL || 'qwen3.5:9b';
18
+
19
+ console.log(`[ralph-ollama] model=${model}`);
20
+
21
+ function runOllama() {
22
+ return new Promise((resolve, reject) => {
23
+ const proc = spawn('node', [join(kitDir, 'lib', 'ollama-agent.js'), model], {
24
+ cwd: projectDir,
25
+ stdio: 'inherit',
26
+ env: process.env,
27
+ });
28
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`ollama-agent exited ${code}`))));
29
+ proc.on('error', reject);
30
+ });
31
+ }
32
+
33
+ startRalph({
34
+ kitDir,
35
+ projectDir,
36
+ onTask: runOllama,
37
+ // onPlannedSlice omitted — ollama-agent manages its own task queue
38
+ agentType: 'BRIDGE',
39
+ queueAllStatuses: true,
40
+ }).catch((err) => {
41
+ console.error('[ralph] Fatal:', err);
42
+ process.exit(1);
43
+ });