@eventmodelers/cli 0.0.21 → 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 +24 -0
- package/cli.js +166 -25
- package/package.json +1 -1
- package/shared/bridge/spec-kitty/bridge-spec-kitty-specify/SKILL.md +125 -0
- package/shared/bridge/spec-kitty/bridge-spec-kitty-tasks/SKILL.md +95 -0
- package/shared/build-kit/lib/ralph.js +41 -22
- package/stacks/bridge/templates/bridge/lib/AGENT.md +47 -0
- package/stacks/bridge/templates/bridge/lib/prompt.md +111 -0
- package/stacks/bridge/templates/bridge/ralph-claude.js +48 -0
- package/stacks/bridge/templates/bridge/ralph-hook.js +103 -0
- package/stacks/bridge/templates/bridge/ralph-ollama.js +43 -0
- package/stacks/modeling-kit/templates/root/claude-modeling.md +2 -2
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
|
-
|
|
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.
|
|
445
|
-
// (
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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');
|
|
@@ -835,7 +882,7 @@ async function configureMcp(options = {}) {
|
|
|
835
882
|
// read-only config resolution (`loadLocalConfig`/`fetchPlatformConfig`) is reused
|
|
836
883
|
// from the kit's lib/config.js, to avoid duplicating the config-file-walk logic.
|
|
837
884
|
// See `claude-modeling.md` in the kit's project root for the per-turn instructions
|
|
838
|
-
// this mode's
|
|
885
|
+
// this mode's modeling session follows.
|
|
839
886
|
async function runModeling(kitDir, projectDir) {
|
|
840
887
|
const configLibPath = join(kitDir, 'lib', 'config.js');
|
|
841
888
|
if (!existsSync(configLibPath)) {
|
|
@@ -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 =
|
|
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);
|
|
@@ -866,7 +913,7 @@ async function runModeling(kitDir, projectDir) {
|
|
|
866
913
|
'relevant slice or column node on the board, then continue with your best interpretation of the prompt.\n\n';
|
|
867
914
|
|
|
868
915
|
// Sent once, on the first turn only — it's what tells CLAUDE.md's dispatcher to
|
|
869
|
-
// follow claude-modeling.md instead of claude-ralph.md, and gives the
|
|
916
|
+
// follow claude-modeling.md instead of claude-ralph.md, and gives the modeling
|
|
870
917
|
// session its one-time connect credentials. Every later turn only carries the
|
|
871
918
|
// per-prompt fields that actually vary (board_id, comment_id, ...).
|
|
872
919
|
let firstTurn = true;
|
|
@@ -952,7 +999,7 @@ async function runModeling(kitDir, projectDir) {
|
|
|
952
999
|
turn.reject(new Error(`claude process exited (${code}) mid-turn`));
|
|
953
1000
|
}
|
|
954
1001
|
});
|
|
955
|
-
log('
|
|
1002
|
+
log('modeling session started');
|
|
956
1003
|
}
|
|
957
1004
|
|
|
958
1005
|
function runClaudeWarm(text) {
|
|
@@ -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 --
|
|
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
|
|
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 --
|
|
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
|
@@ -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
|
|
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.
|
|
115
|
-
// (
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
+
rootCfg = JSON.parse(readFileSync(rootConfigPath, 'utf-8'));
|
|
124
127
|
} catch {
|
|
125
|
-
console.warn(`[ralph] Skipping invalid config at ${
|
|
128
|
+
console.warn(`[ralph] Skipping invalid config at ${rootConfigPath}`);
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Modeling Direct-Dispatch —
|
|
1
|
+
# Modeling Direct-Dispatch — Modeling Session Mode
|
|
2
2
|
|
|
3
3
|
Used by `npx @eventmodelers/cli run --modeling`. The CLI itself subscribes to the board's realtime channel and writes each incoming prompt directly to your stdin as a new turn — there is **no `tasks.json` queue** in this mode. Each user message you receive already IS the one prompt to handle; there's nothing to read, pre-filter, or pick from.
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@ You are a long-lived process handling many turns in a row. Don't redo one-time s
|
|
|
12
12
|
- if this turn's `board_id` differs from the one you last connected with, or
|
|
13
13
|
- if the last API call returned `401`/`403`.
|
|
14
14
|
|
|
15
|
-
Otherwise skip straight to executing the prompt — re-running `/connect` every turn defeats the point of a
|
|
15
|
+
Otherwise skip straight to executing the prompt — re-running `/connect` every turn defeats the point of a modeling session.
|
|
16
16
|
3. **Resolve `BOARD_ID`** from this turn's `board_id` field; if absent, fall back to `boardId` in `.eventmodelers/config.json`.
|
|
17
17
|
4. Execute the prompt using the skill matched in CLAUDE.md's Skill Selection table.
|
|
18
18
|
**Questioning rule**: you are running autonomously — no human is available to answer questions. If you need clarification, do not pause or ask interactively — post a `QUESTION`-type comment (`/handle-comment` with `action=place`, `type=QUESTION`) on the most relevant node, then continue with your best interpretation.
|