@eventmodelers/cli 1.0.162 → 2.0.1
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 +22 -27
- package/RELEASE_NOTES.md +20 -2
- package/cli.js +461 -225
- package/lib/adapters/spec-kitty-adapter.js +3 -7
- package/lib/global-agent-id.js +45 -0
- package/lib/modeling-local-ai.js +4 -2
- package/package.json +1 -1
- package/shared/build-kit/README.md +4 -19
- package/shared/build-kit/lib/agent-lock.js +75 -0
- package/shared/build-kit/lib/agent-name.js +27 -0
- package/shared/build-kit/lib/event-prune.js +58 -0
- package/shared/build-kit/lib/events-view.js +121 -0
- package/shared/build-kit/lib/local-ai-agent.js +14 -26
- package/shared/build-kit/lib/mcp-register.js +17 -1
- package/shared/build-kit/lib/prompt-journal.js +191 -0
- package/shared/build-kit/lib/prompt-replay.js +35 -0
- package/shared/build-kit/lib/ralph.js +250 -163
- package/shared/build-kit/lib/slice-curation.js +40 -0
- package/shared/build-kit/lib/slice-events.js +93 -0
- package/shared/build-kit/lib/slice-git.js +86 -0
- package/shared/build-kit/lib/slice-learnings.js +197 -0
- package/shared/build-kit/lib/slice-projection.js +101 -0
- package/shared/build-kit/lib/slice-run.js +181 -0
- package/shared/build-kit/lib/slice-state.js +182 -0
- package/shared/build-kit/lib/slice-tasks.js +35 -0
- package/shared/build-kit/ralph-claude.js +4 -2
- package/shared/build-kit/ralph-exec.js +4 -2
- package/shared/build-kit/ralph-local-ai.js +5 -3
- package/shared/skills/connect/SKILL.md +1 -5
- package/shared/skills/learn-eventmodelers-api/SKILL.md +13 -0
- package/shared/skills/request-feedback/SKILL.md +1 -2
- package/shared/skills/ui-contract/SKILL.md +5 -3
- package/stacks/axon/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/axon/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/axon/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/blank/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/blank/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +20 -37
- package/stacks/blank/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/classic/templates/.claude/skills/build-automation/SKILL.md +1 -1
- package/stacks/classic/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/classic/templates/.claude/skills/build-state-view/SKILL.md +2 -2
- package/stacks/classic/templates/.claude/skills/record-evidence/SKILL.md +2 -2
- package/stacks/classic/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/classic/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/classic/templates/build-kit/lib/backend-prompt.md +20 -38
- package/stacks/classic/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +3 -4
- package/stacks/kurrent/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/kurrent/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +1 -17
- package/stacks/modeling-kit/templates/.claude/skills/detect-model-drift/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +2 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +7 -3
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +0 -8
- package/stacks/modeling-kit/templates/.claude/skills/stressor-analysis/SKILL.md +0 -2
- package/stacks/modeling-kit/templates/.claude/skills/walkthrough/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/kit/AGENTS.md +0 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +3 -4
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +11 -72
- package/stacks/modeling-kit/templates/kit/README.md +1 -1
- package/stacks/modeling-kit/templates/kit/lib/config.js +1 -1
- package/stacks/node/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/node/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/node/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +3 -4
- package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/opencqrs/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +1 -1
- package/stacks/react/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/react/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +20 -37
- package/stacks/react/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/supabase/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/supabase/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/supabase/templates/build-kit/lib/prompt.md +18 -41
- package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +2 -2
- package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +2 -2
- package/stacks/supabase-react/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +20 -37
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +19 -42
- package/stacks/umadb/templates/build-kit/CLAUDE.md +3 -3
- package/stacks/umadb/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +20 -42
- package/stacks/umadb/templates/build-kit/lib/prompt.md +18 -41
- package/shared/bridge/spec-kitty/bridge-spec-kitty-specify/SKILL.md +0 -125
- package/shared/bridge/spec-kitty/bridge-spec-kitty-tasks/SKILL.md +0 -95
- package/shared/build-kit/lib/agent.sh +0 -20
- package/shared/build-kit/ralph.sh +0 -111
- package/shared/build-kit/realtime-agent.js +0 -20
- package/stacks/bridge/templates/bridge/lib/AGENT.md +0 -47
- package/stacks/bridge/templates/bridge/lib/prompt.md +0 -111
- package/stacks/bridge/templates/bridge/ralph-claude.js +0 -50
- package/stacks/bridge/templates/bridge/ralph-hook.js +0 -103
- package/stacks/bridge/templates/bridge/ralph-local-ai.js +0 -43
- package/stacks/bridge/templates/bridge/ralph-static.js +0 -90
package/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { Command } from 'commander';
|
|
3
|
+
import { Command, Option } from 'commander';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
5
|
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'path';
|
|
6
6
|
import {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
copyFileSync,
|
|
11
11
|
rmSync,
|
|
12
12
|
readdirSync,
|
|
13
|
+
statSync,
|
|
13
14
|
writeFileSync,
|
|
14
15
|
readFileSync,
|
|
15
16
|
appendFileSync,
|
|
@@ -29,14 +30,26 @@ import { modelingPermissions } from './lib/agent-permissions.js';
|
|
|
29
30
|
// copyDirContents in installStack) for ralph.js to import standalone — see
|
|
30
31
|
// shared/build-kit/lib/adapters/realtime-adapter.js for why it lives there instead.
|
|
31
32
|
import { createRealtimeAdapter, keepSubscribed } from './shared/build-kit/lib/adapters/realtime-adapter.js';
|
|
33
|
+
import { recordAgentStatus } from './shared/build-kit/lib/slice-run.js';
|
|
34
|
+
import { defaultAgentName, withInstanceSuffix } from './shared/build-kit/lib/agent-name.js';
|
|
35
|
+
import { acquireAgentId } from './shared/build-kit/lib/agent-lock.js';
|
|
36
|
+
import { readEvents } from './shared/build-kit/lib/slice-events.js';
|
|
37
|
+
import { findPromptEvent, replayRequest } from './shared/build-kit/lib/prompt-replay.js';
|
|
38
|
+
import { prunableEvents, pruneLog, rememberPruneAnswer, shouldOfferPrune } from './shared/build-kit/lib/event-prune.js';
|
|
39
|
+
import { fold } from './shared/build-kit/lib/slice-state.js';
|
|
40
|
+
import { filterEvents, formatEvent, formatPayload, sliceTitles, summarize } from './shared/build-kit/lib/events-view.js';
|
|
41
|
+
import { stableGlobalAgentId } from './lib/global-agent-id.js';
|
|
42
|
+
import { ensureLearningsGitRules } from './shared/build-kit/lib/ralph.js';
|
|
43
|
+
import { createCallTracker, heldPromptsRequest, journalPromptAction, shouldJournalCall, journalPromptFinished, journalPromptReceived, recoverOwnPrompts } from './shared/build-kit/lib/prompt-journal.js';
|
|
44
|
+
import { listLearnings, mergeLearnings, recordLearning, recordTurnLearnings, retractLearning, syncLearnings } from './shared/build-kit/lib/slice-learnings.js';
|
|
32
45
|
|
|
33
46
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
47
|
const __dirname = dirname(__filename);
|
|
35
48
|
|
|
36
49
|
// Each stack is a template set under stacks/<key>/templates/{.claude,root,<kitSubdir>}.
|
|
37
50
|
// Stacks with useShared:true also get shared/build-kit/* copied into their kit dir
|
|
38
|
-
// (ralph.js, ralph-claude.js, ralph-local-ai.js, ralph-exec.js,
|
|
39
|
-
// code-export.mjs, lib/
|
|
51
|
+
// (ralph.js, ralph-claude.js, ralph-local-ai.js, ralph-exec.js,
|
|
52
|
+
// code-export.mjs, lib/local-ai-agent.js, package.json, README.md) —
|
|
40
53
|
// those files have no per-stack content, so they live once instead of being
|
|
41
54
|
// copy-pasted into every stack (that copy-pasting is exactly how they drifted out
|
|
42
55
|
// of sync before: a bugfix or default landing in one stack's copy but not another's).
|
|
@@ -44,7 +57,7 @@ const __dirname = dirname(__filename);
|
|
|
44
57
|
// (lib/prompt.md, lib/backend-prompt.md, lib/AGENT.md, checks, etc.); shared/build-kit/*
|
|
45
58
|
// is copied after it, so the shared runtime always wins over any copy a stack still ships.
|
|
46
59
|
// modeling-kit (below) is the one kit that opts out of all of this (useShared:false)
|
|
47
|
-
// — it has no cold-spawn/
|
|
60
|
+
// — it has no cold-spawn/task-queue runtime at all, so none of shared/build-kit/*
|
|
48
61
|
// applies to it; see its own templates/kit for its (much smaller) self-contained set.
|
|
49
62
|
// --- Local-AI runner selection ------------------------------------------------
|
|
50
63
|
// One runner drives every local/self-hosted backend. What differs between Ollama,
|
|
@@ -117,8 +130,8 @@ function resolveAgentPreset(opts, loop) {
|
|
|
117
130
|
console.error('❌ --agent and --exec are mutually exclusive — --agent is a shortcut for --exec.');
|
|
118
131
|
process.exit(1);
|
|
119
132
|
}
|
|
120
|
-
if (resolveLocalAiTarget(opts) !== null
|
|
121
|
-
console.error('❌ --agent is mutually exclusive with --local-ai
|
|
133
|
+
if (resolveLocalAiTarget(opts) !== null) {
|
|
134
|
+
console.error('❌ --agent is mutually exclusive with --local-ai — pick one runner.');
|
|
122
135
|
process.exit(1);
|
|
123
136
|
}
|
|
124
137
|
const cmd = AGENT_PRESETS[name]?.[loop] ?? null;
|
|
@@ -212,7 +225,7 @@ const STACKS = {
|
|
|
212
225
|
|
|
213
226
|
// Not a stack — no backend scaffold, just skills + the agent loop. Installed via
|
|
214
227
|
// `init --modeling` instead of the `init --stack <name>` picker.
|
|
215
|
-
// useShared:false — unlike build-kit, modeling-kit has no cold-spawn/
|
|
228
|
+
// useShared:false — unlike build-kit, modeling-kit has no cold-spawn/task-queue
|
|
216
229
|
// runtime to reuse from shared/build-kit/*; its only runtime mode is the CLI's
|
|
217
230
|
// own warm, direct-dispatch loop (`run --modeling`), so its kit dir just needs
|
|
218
231
|
// lib/config.js for config resolution — see stacks/modeling-kit/templates/kit.
|
|
@@ -225,34 +238,9 @@ const MODELING_KIT = {
|
|
|
225
238
|
needsBoardId: false,
|
|
226
239
|
};
|
|
227
240
|
|
|
228
|
-
// Frameworks a bridge install can translate board slices into. Each key needs
|
|
229
|
-
// a matching `bridge-<key>-specify` skill under shared/bridge/ — see
|
|
230
|
-
// stacks/bridge/templates/bridge/lib/prompt.md for how the loop picks it up.
|
|
231
|
-
const BRIDGE_TARGETS = {
|
|
232
|
-
'spec-kitty': { label: 'Spec Kitty' },
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
// Also not a stack — no backend scaffold, just the bridge-*/shared skills +
|
|
236
|
-
// the agent loop. Installed via `init --bridge --target <name>` instead of
|
|
237
|
-
// the `init --stack <name>` picker. useShared:true (unlike modeling-kit): a
|
|
238
|
-
// bridge agent reuses build-kit's cold-spawn/tasks.json engine as-is
|
|
239
|
-
// (lib/ralph.js) — it just reacts to every slice change instead of only
|
|
240
|
-
// "Planned" ones (see queueAllStatuses in lib/ralph.js) and translates
|
|
241
|
-
// instead of building. Its own templates/bridge overlay swaps in
|
|
242
|
-
// bridge-specific prompt.md/AGENT.md and a ralph-claude.js that omits
|
|
243
|
-
// onPlannedSlice entirely — see stacks/bridge/templates/bridge.
|
|
244
|
-
const BRIDGE_KIT = {
|
|
245
|
-
key: 'bridge',
|
|
246
|
-
label: 'Bridge — translate board slices into another spec framework, no backend scaffold',
|
|
247
|
-
kitSubdir: 'bridge',
|
|
248
|
-
kitDirName: '.bridge-kit',
|
|
249
|
-
useShared: true,
|
|
250
|
-
needsBoardId: true,
|
|
251
|
-
};
|
|
252
|
-
|
|
253
241
|
// Also not a stack — installed via `init --build-kit` instead of `init --stack <name>`.
|
|
254
242
|
// useShared:true, same as any real backend stack: it reuses build-kit's cold-spawn/
|
|
255
|
-
//
|
|
243
|
+
// task-queue engine as-is (lib/ralph.js). Its templates/build-kit/CLAUDE.md,
|
|
256
244
|
// lib/{prompt,backend-prompt}.md, and templates/.claude/skills/build-*/SKILL.md are
|
|
257
245
|
// TODO-marked placeholders instead of real stack content (see stacks/blank/templates)
|
|
258
246
|
// — this is for a stack that isn't built into this CLI yet: fill in the TODOs against
|
|
@@ -267,7 +255,7 @@ const BLANK_BUILD_KIT = {
|
|
|
267
255
|
needsBoardId: true,
|
|
268
256
|
};
|
|
269
257
|
|
|
270
|
-
const KIT_DIR_NAMES = [...new Set([...Object.values(STACKS), MODELING_KIT,
|
|
258
|
+
const KIT_DIR_NAMES = [...new Set([...Object.values(STACKS), MODELING_KIT, BLANK_BUILD_KIT].map((s) => s.kitDirName))];
|
|
271
259
|
|
|
272
260
|
// What `init --demo` installs, for the messages it prints. Kept in step with
|
|
273
261
|
// shared/demo-slices/ (context.json's name, and the slice folders beside it).
|
|
@@ -953,7 +941,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
953
941
|
// install TO, versus where we install FROM.
|
|
954
942
|
const targetDir = options.targetDir ? resolve(options.targetDir) : process.cwd();
|
|
955
943
|
// `init --git <url>` passes a resolved clone dir's templates/ here instead — every
|
|
956
|
-
// other input (STACKS, MODELING_KIT
|
|
944
|
+
// other input (STACKS, MODELING_KIT) keeps using the built-in path.
|
|
957
945
|
const templatesSource = options.templatesSource || join(__dirname, 'stacks', stackKey, 'templates');
|
|
958
946
|
const sharedBuildKit = join(__dirname, 'shared', 'build-kit');
|
|
959
947
|
// Skills with no stack-specific content (connect, learn-eventmodelers-api,
|
|
@@ -962,15 +950,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
962
950
|
// of sync with each other before (e.g. one stack's connect skill silently
|
|
963
951
|
// missing a bugfix another stack's copy had).
|
|
964
952
|
const sharedSkills = join(__dirname, 'shared', 'skills');
|
|
965
|
-
// Adapter skills that translate board slices for another spec framework —
|
|
966
|
-
// one subfolder per target (shared/bridge/spec-kitty/bridge-spec-kitty-*,
|
|
967
|
-
// shared/bridge/kiro/..., etc.), so a bridge install only ever pulls in
|
|
968
|
-
// the target it was actually configured for, not every framework's
|
|
969
|
-
// skills. Only relevant to a bridge install — never copied into the four
|
|
970
|
-
// backend stacks or modeling-kit.
|
|
971
|
-
const isBridge = stackKey === BRIDGE_KIT.key;
|
|
972
953
|
const isModelingKit = stackKey === MODELING_KIT.key;
|
|
973
|
-
const sharedBridgeSkills = isBridge ? join(__dirname, 'shared', 'bridge', options.target) : null;
|
|
974
954
|
|
|
975
955
|
if (!existsSync(templatesSource)) {
|
|
976
956
|
console.error('❌ Templates directory not found at:', templatesSource);
|
|
@@ -983,7 +963,6 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
983
963
|
const claudeSkillsSrc = join(templatesSource, '.claude', 'skills');
|
|
984
964
|
const installedSkills = [
|
|
985
965
|
...(existsSync(sharedSkills) ? readdirSync(sharedSkills) : []),
|
|
986
|
-
...(isBridge && existsSync(sharedBridgeSkills) ? readdirSync(sharedBridgeSkills) : []),
|
|
987
966
|
...(existsSync(claudeSkillsSrc) ? readdirSync(claudeSkillsSrc) : []),
|
|
988
967
|
];
|
|
989
968
|
let claudeExtras = [];
|
|
@@ -992,14 +971,12 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
992
971
|
const globalSkillsDir = join(homedir(), '.claude', 'skills');
|
|
993
972
|
console.log('📦 Installing skills globally...');
|
|
994
973
|
copyDirContents(sharedSkills, globalSkillsDir);
|
|
995
|
-
if (isBridge) copyDirContents(sharedBridgeSkills, globalSkillsDir);
|
|
996
974
|
copyDirContents(claudeSkillsSrc, globalSkillsDir);
|
|
997
975
|
} else {
|
|
998
976
|
console.log('📦 Installing skills...');
|
|
999
977
|
// Shared skills first, the stack's own templates last — so a build kit can override
|
|
1000
978
|
// a shared skill (e.g. `ui-contract`) just by shipping a skill of the same name.
|
|
1001
979
|
copyDirContents(sharedSkills, join(targetDir, '.claude', 'skills'));
|
|
1002
|
-
if (isBridge) copyDirContents(sharedBridgeSkills, join(targetDir, '.claude', 'skills'));
|
|
1003
980
|
copyDirContents(join(templatesSource, '.claude'), join(targetDir, '.claude'));
|
|
1004
981
|
claudeExtras = existsSync(join(templatesSource, '.claude'))
|
|
1005
982
|
? readdirSync(join(templatesSource, '.claude')).filter((f) => f !== 'skills')
|
|
@@ -1041,7 +1018,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1041
1018
|
}
|
|
1042
1019
|
}
|
|
1043
1020
|
|
|
1044
|
-
// A modeling-kit + build-kit
|
|
1021
|
+
// A modeling-kit + build-kit combo in the same project is supported
|
|
1045
1022
|
// (they share one config.json — see ensureAgentId) but each kit's own instructions
|
|
1046
1023
|
// now live in its own kit dir (.build-kit/CLAUDE.md, .agent-modeling-kit/CLAUDE.md)
|
|
1047
1024
|
// instead of root/CLAUDE.md, precisely so a second kit's install can never clobber
|
|
@@ -1131,16 +1108,8 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1131
1108
|
}
|
|
1132
1109
|
}
|
|
1133
1110
|
|
|
1134
|
-
// Static (no-LLM) bridge adapters live once in this package's own lib/
|
|
1135
|
-
// adapters/ — `fetch --spec-kitty` imports them directly, and a bridge
|
|
1136
|
-
// install gets its own copy here so ralph-static.js can run standalone
|
|
1137
|
-
// with no access back to the published package.
|
|
1138
|
-
if (isBridge) {
|
|
1139
|
-
copyDirContents(join(__dirname, 'lib', 'adapters'), join(kitDir, 'adapters'));
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
1111
|
// Make scripts executable
|
|
1143
|
-
for (const script of ['ralph
|
|
1112
|
+
for (const script of ['ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
|
|
1144
1113
|
const p = join(kitDir, script);
|
|
1145
1114
|
if (existsSync(p)) {
|
|
1146
1115
|
try { execSync(`chmod +x "${p}"`); } catch {}
|
|
@@ -1246,12 +1215,10 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1246
1215
|
if (options.skipEpilogue) return;
|
|
1247
1216
|
|
|
1248
1217
|
console.log('\n✅ Done! Start your agent:\n');
|
|
1249
|
-
if (
|
|
1250
|
-
console.log(' npx @eventmodelers/cli bridge\n');
|
|
1251
|
-
} else if (isModelingKit) {
|
|
1218
|
+
if (isModelingKit) {
|
|
1252
1219
|
console.log(' npx @eventmodelers/cli run --modeling\n');
|
|
1253
1220
|
} else {
|
|
1254
|
-
console.log(' npx @eventmodelers/cli run (--local-ai or --
|
|
1221
|
+
console.log(' npx @eventmodelers/cli run (--local-ai or --exec for other runners)\n');
|
|
1255
1222
|
}
|
|
1256
1223
|
console.log('Connect this project to an MCP client (Claude Code, VS Code, ...):\n');
|
|
1257
1224
|
console.log(` npx @eventmodelers/cli init-mcp\n`);
|
|
@@ -1390,7 +1357,7 @@ async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
|
|
|
1390
1357
|
if (existsSync(manifestPath)) {
|
|
1391
1358
|
writeFileSync(manifestPath, JSON.stringify({ ...readJsonSafe(manifestPath), version: CLI_VERSION }, null, 2));
|
|
1392
1359
|
}
|
|
1393
|
-
for (const script of ['ralph
|
|
1360
|
+
for (const script of ['ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
|
|
1394
1361
|
const p = join(kitDir, script);
|
|
1395
1362
|
if (existsSync(p)) {
|
|
1396
1363
|
try { execSync(`chmod +x "${p}"`); } catch {}
|
|
@@ -1750,6 +1717,22 @@ async function fetchDefaultBoardId(baseUrl, token) {
|
|
|
1750
1717
|
}
|
|
1751
1718
|
}
|
|
1752
1719
|
|
|
1720
|
+
// The board whose credentials were written most recently — the best guess for "the board I
|
|
1721
|
+
// was just driving" when nothing in this directory names one. Only offered as the default
|
|
1722
|
+
// of the interactive question, never applied silently.
|
|
1723
|
+
function lastUsedBoardId() {
|
|
1724
|
+
try {
|
|
1725
|
+
const dir = join(GLOBAL_DIR, 'boards');
|
|
1726
|
+
return readdirSync(dir)
|
|
1727
|
+
.filter((f) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/.test(f))
|
|
1728
|
+
.map((f) => ({ id: f.slice(0, -5), mtime: statSync(join(dir, f)).mtimeMs, creds: readJsonSafe(join(dir, f)) }))
|
|
1729
|
+
.filter((b) => b.creds.token)
|
|
1730
|
+
.sort((a, b) => b.mtime - a.mtime)[0]?.id ?? null;
|
|
1731
|
+
} catch {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1753
1736
|
// Per-run credentials for the global install. Precedence is this CLI's usual one, with the
|
|
1754
1737
|
// per-board file slotted in as the most specific *file*: explicit flags beat
|
|
1755
1738
|
// EVENTMODELERS_* env vars beat ~/.eventmodelers/boards/<board>.json beat the nearest
|
|
@@ -1789,6 +1772,7 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
|
|
|
1789
1772
|
let boardChosen = !!(explicit.boardId || process.env.EVENTMODELERS_BOARD_ID);
|
|
1790
1773
|
let pasted = null;
|
|
1791
1774
|
if (!boardChosen && interactive) {
|
|
1775
|
+
boardId = boardId || lastUsedBoardId();
|
|
1792
1776
|
const answer = await prompt(boardId ? `\n Board ID [${boardId}]: ` : '\n Board ID: ');
|
|
1793
1777
|
if (answer) {
|
|
1794
1778
|
pasted = parseCredentialsPaste(answer, ['organizationId', 'token', 'boardId']);
|
|
@@ -1916,16 +1900,12 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
|
|
|
1916
1900
|
process.exit(1);
|
|
1917
1901
|
}
|
|
1918
1902
|
|
|
1919
|
-
//
|
|
1920
|
-
// is
|
|
1921
|
-
//
|
|
1922
|
-
//
|
|
1923
|
-
//
|
|
1924
|
-
|
|
1925
|
-
// restarted agent is a new row, and the old one lingers until its 45s window lapses) — pass
|
|
1926
|
-
// `run --id <uuid>` when an agent needs to keep one identity, which is also what makes
|
|
1927
|
-
// "preferred agent" on the board stick to it.
|
|
1928
|
-
config.agentId = randomUUID();
|
|
1903
|
+
// One identity per board and repo — the directory the run is started in — kept in ~/.eventmodelers/agent-ids.json, so a
|
|
1904
|
+
// standalone agent is the same agent on every start: its chat sessions, its star on the board and the prompts it holds
|
|
1905
|
+
// stay addressed to it. This is the id it keeps; whether this process may use it (another live run may already be
|
|
1906
|
+
// under it) is decided in the run action, which also lets `run --id` pin a different one. Not written into the
|
|
1907
|
+
// per-board credentials file below — the id is not a credential.
|
|
1908
|
+
config.agentId = stableGlobalAgentId({ file: join(GLOBAL_DIR, 'agent-ids.json'), boardId: config.boardId, dir: cwd });
|
|
1929
1909
|
writeBoardCredentials(stored.useGlobal
|
|
1930
1910
|
? { boardId: config.boardId, useGlobal: true, agentName: config.agentName }
|
|
1931
1911
|
: config);
|
|
@@ -1963,7 +1943,7 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1963
1943
|
}
|
|
1964
1944
|
|
|
1965
1945
|
// `run --modeling`: modeling-kit's one and only runtime mode — there is no
|
|
1966
|
-
// cold-spawn/
|
|
1946
|
+
// cold-spawn/task-queue loop for this kit (that's a build-kit concept; see the
|
|
1967
1947
|
// `run` command's build-kit-vs-modeling-kit gate above). It keeps ONE Claude
|
|
1968
1948
|
// process warm across turns via `--input-format stream-json`, subscribes to the
|
|
1969
1949
|
// org's realtime channel itself, and writes each prompt straight to that
|
|
@@ -1990,6 +1970,27 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1990
1970
|
// untargeted is handed straight back to the queue for another agent to take. It says
|
|
1991
1971
|
// nothing about the standalone lane — a self-directed turn is nobody's task, so an
|
|
1992
1972
|
// exclusive standalone agent still works the board on its own initiative.
|
|
1973
|
+
// A new instance of a loop starts by checking how big its event log has grown: at every multiple of 1000 events it
|
|
1974
|
+
// asks whether to prune it (`events --prune` does the same on demand). Only asked where someone can answer.
|
|
1975
|
+
async function offerPrune(kitDir, { interactive }) {
|
|
1976
|
+
let count;
|
|
1977
|
+
try { count = readEvents(kitDir).length; } catch { return; }
|
|
1978
|
+
if (!shouldOfferPrune(kitDir, count)) return;
|
|
1979
|
+
const prunable = prunableEvents(readEvents(kitDir)).length;
|
|
1980
|
+
if (!interactive) {
|
|
1981
|
+
console.error(`ℹ️ The event log has ${count} events (${prunable} of them prunable) — \`eventmodelers events --prune\` removes them.`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const answer = (await prompt(`The event log has ${count} events. Should we prune the log? Removes the journal of finished prompts (${prunable} events); slice state and learnings stay. [y/N] `)).toLowerCase();
|
|
1985
|
+
if (answer === 'y' || answer === 'yes') {
|
|
1986
|
+
const { removed, kept } = pruneLog(kitDir);
|
|
1987
|
+
rememberPruneAnswer(kitDir, kept);
|
|
1988
|
+
console.log(`Pruned ${removed} events, ${kept} left.\n`);
|
|
1989
|
+
} else {
|
|
1990
|
+
rememberPruneAnswer(kitDir, count);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1993
1994
|
async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null, exec = null, model = null, skipPermissions = false, browser = false } = {}) {
|
|
1994
1995
|
// --worker: only prompts — no chat, and no board-change turns (it has nowhere to propose).
|
|
1995
1996
|
const chat = !worker;
|
|
@@ -2017,7 +2018,17 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2017
2018
|
// `run --id` overrides that for this run only — ensureAgentId is skipped rather than
|
|
2018
2019
|
// overwritten, so the project's own stable id stays on disk and the next run without the
|
|
2019
2020
|
// flag is the same agent the platform saw before.
|
|
2020
|
-
if (!overrides)
|
|
2021
|
+
if (!overrides) {
|
|
2022
|
+
if (identity.agentId) {
|
|
2023
|
+
local.agentId = identity.agentId;
|
|
2024
|
+
} else {
|
|
2025
|
+
// The project's stable id — unless a live process already runs under it (two `run --modeling` in one
|
|
2026
|
+
// project), in which case this one gets a fresh id (agent-lock.js).
|
|
2027
|
+
const { agentId, fellBack } = acquireAgentId({ lockDir: join(projectDir, '.eventmodelers', 'locks'), stableId: ensureAgentId(kitDir, 'MODELING') });
|
|
2028
|
+
local.agentId = agentId;
|
|
2029
|
+
local.agentIdFellBack = fellBack;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2021
2032
|
if (!local.token || !local.organizationId) {
|
|
2022
2033
|
console.error('❌ --modeling needs platform credentials in .eventmodelers/config.json (token + organizationId) — run `/connect` once or paste your config first.');
|
|
2023
2034
|
process.exit(1);
|
|
@@ -2026,6 +2037,17 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2026
2037
|
// resolveModelingCredentials just minted for this run, which would otherwise win back over
|
|
2027
2038
|
// an explicit --id.
|
|
2028
2039
|
const cfg = { ...applyEnvOverrides(await fetchPlatformConfig(local)), ...(overrides ?? {}), ...(identity.agentId ? { agentId: identity.agentId } : {}), ...(identity.agentName ? { agentName: identity.agentName } : {}), ...(model ? { model } : {}) }; // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
|
|
2040
|
+
// No name given (--name, agentName in the config): the repository's, so the board shows which agent is live.
|
|
2041
|
+
// (On the global install projectDir is ~/.eventmodelers/kit; the repo is the directory the run was started in.)
|
|
2042
|
+
if (!cfg.agentName) {
|
|
2043
|
+
const base = defaultAgentName(codeDir ?? projectDir, 'MODELING');
|
|
2044
|
+
cfg.agentName = cfg.agentIdFellBack ? withInstanceSuffix(base, cfg.agentId) : base;
|
|
2045
|
+
}
|
|
2046
|
+
// Learnings are kept only on a project install: the global install's kit dir (~/.eventmodelers/kit) is shared by every
|
|
2047
|
+
// board and account, so what one board taught the agent would end up in another's AGENTS.md. The prompt journal has no
|
|
2048
|
+
// such problem — every entry is stamped with its board (`events --board`) — so every run keeps it, standalone included.
|
|
2049
|
+
const keepsRecord = !standalone && !overrides;
|
|
2050
|
+
if (keepsRecord) ensureLearningsGitRules(kitDir);
|
|
2029
2051
|
if (!cfg.boardId) {
|
|
2030
2052
|
console.error('❌ --modeling needs a boardId — a modeling agent always runs for exactly one board. Run `/connect board=<uuid>` once, or add boardId to .eventmodelers/config.json.');
|
|
2031
2053
|
process.exit(1);
|
|
@@ -2051,7 +2073,7 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2051
2073
|
let localRunner = null;
|
|
2052
2074
|
if (localAi) {
|
|
2053
2075
|
try {
|
|
2054
|
-
localRunner = createModelingLocalAiRunner({ cfg, target: localAi, log, verbose, standalone });
|
|
2076
|
+
localRunner = createModelingLocalAiRunner({ cfg, target: localAi, log, verbose, standalone, onToolUse: (name, input) => { const id = `local-${++localCallSeq}`; noteToolStart(id, name, input); return (outcome) => noteToolEnd(id, outcome); } });
|
|
2055
2077
|
} catch (err) {
|
|
2056
2078
|
console.error(`❌ --local-ai: ${err.message}`);
|
|
2057
2079
|
process.exit(1);
|
|
@@ -2190,6 +2212,19 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2190
2212
|
let proc = null;
|
|
2191
2213
|
let stdoutBuffer = '';
|
|
2192
2214
|
let pending = null; // one in-flight turn at a time
|
|
2215
|
+
let turnPromptId = null; // the prompt the current turn works, so the board writes it makes are journaled against it
|
|
2216
|
+
// A call is journaled when it ends, so its entry carries how long it took (see journalPromptAction). Only the calls the
|
|
2217
|
+
// journal will keep are tracked at all.
|
|
2218
|
+
const calls = createCallTracker();
|
|
2219
|
+
let localCallSeq = 0;
|
|
2220
|
+
const journalCall = (call) => journal(() => journalPromptAction({ kitDir, id: turnPromptId, toolName: call.name, input: call.input, verbose, detail: verbose ? describeToolUse(call) : undefined, secrets: [cfg.token], startedAt: call.startedAt, durationMs: call.durationMs, error: call.error, meta: journalMeta }));
|
|
2221
|
+
const noteToolStart = (id, name, input) => {
|
|
2222
|
+
if (turnPromptId && shouldJournalCall(name, input, verbose)) calls.begin(id, { name, input });
|
|
2223
|
+
};
|
|
2224
|
+
const noteToolEnd = (id, outcome) => {
|
|
2225
|
+
const call = calls.end(id, outcome);
|
|
2226
|
+
if (call) journalCall(call);
|
|
2227
|
+
};
|
|
2193
2228
|
// The last message of each session this process has handed to Claude. A session the process has
|
|
2194
2229
|
// never seen is read whole; after that only what came since — each message enters the warm
|
|
2195
2230
|
// session's context once. Cleared whenever the process is (re)spawned: a fresh process has read
|
|
@@ -2248,10 +2283,16 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2248
2283
|
let msg;
|
|
2249
2284
|
try { msg = JSON.parse(line); } catch { return; }
|
|
2250
2285
|
|
|
2286
|
+
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
|
2287
|
+
for (const block of msg.message.content) if (block.type === 'tool_result') noteToolEnd(block.tool_use_id, { isError: block.is_error });
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2251
2291
|
if (msg.type === 'assistant') {
|
|
2252
2292
|
for (const block of msg.message?.content ?? []) {
|
|
2253
2293
|
if (block.type === 'text' && block.text && verbose) log(block.text);
|
|
2254
2294
|
if (block.type === 'tool_use') {
|
|
2295
|
+
noteToolStart(block.id, block.name, block.input);
|
|
2255
2296
|
if (verbose) log(`→ ${describeToolUse(block)}`);
|
|
2256
2297
|
else if (block.name === 'Skill') log(`→ Skill: ${block.input?.skill ?? ''}`);
|
|
2257
2298
|
else log(`→ ${block.name}`);
|
|
@@ -2381,10 +2422,10 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2381
2422
|
log(chat ? 'chat: ON — answering chat messages addressed to this agent' : 'chat: off (--worker)');
|
|
2382
2423
|
if (exclusive) {
|
|
2383
2424
|
log(`exclusive: ON — only prompts addressed to ${cfg.agentId} are worked; every untargeted prompt is handed back to the queue`);
|
|
2384
|
-
// A
|
|
2385
|
-
// someone addressed a prompt to
|
|
2425
|
+
// A run keeps its stable id unless another live process is already running under it (agent-lock.js), in which
|
|
2426
|
+
// case it got a fresh one — and an id someone addressed a prompt to is not this agent. Worth saying out loud here,
|
|
2386
2427
|
// where the alternative is an agent that looks healthy and quietly works nothing.
|
|
2387
|
-
if (
|
|
2428
|
+
if (cfg.agentIdFellBack) log('exclusive: another process is already running under this agent\'s usual id, so this run got a fresh one — star it on the board now, or stop the other process and restart');
|
|
2388
2429
|
}
|
|
2389
2430
|
// Both warm-ups are the same bet — pay the session's fixed setup cost before a turn arrives
|
|
2390
2431
|
// rather than making whoever sends the first prompt wait for it. For a local model that cost
|
|
@@ -2420,7 +2461,16 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2420
2461
|
headers: { 'x-token': cfg.token, 'Content-Type': 'application/json', ...agentHeaders(cfg) },
|
|
2421
2462
|
body: JSON.stringify({ status: 'ADDED' }),
|
|
2422
2463
|
});
|
|
2423
|
-
if (!res.ok) throw new Error(`prompts/${promptId}/status: HTTP ${res.status}`);
|
|
2464
|
+
if (!res.ok) throw Object.assign(new Error(`prompts/${promptId}/status: HTTP ${res.status}`), { status: res.status });
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// The prompts the platform shows as held (CLAIMED / IN_PROGRESS) under this agent's id — GET /prompts?claimed_by=,
|
|
2468
|
+
// which reads and claims nothing. At start an agent holds none, so these are what an earlier run left behind.
|
|
2469
|
+
async function fetchHeldPrompts(agentId) {
|
|
2470
|
+
const { url, headers } = heldPromptsRequest({ baseUrl: cfg.baseUrl, organizationId: cfg.organizationId, boardId: cfg.boardId, token: cfg.token, jwt: realtimeToken, agentId });
|
|
2471
|
+
const res = await fetch(url, { headers });
|
|
2472
|
+
if (!res.ok) throw new Error(`prompts list: HTTP ${res.status}`);
|
|
2473
|
+
return ((await res.json()).prompts ?? []).map((p) => ({ id: p.id, status: p.status }));
|
|
2424
2474
|
}
|
|
2425
2475
|
|
|
2426
2476
|
// ── Chat ───────────────────────────────────────────────────────────────────
|
|
@@ -2547,6 +2597,21 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2547
2597
|
|
|
2548
2598
|
let realtimeToken = await getRealtimeToken();
|
|
2549
2599
|
|
|
2600
|
+
// What this agent writes into its own record: events in the kit, stamped with who and where (learnings: see keepsRecord).
|
|
2601
|
+
const journalMeta = { agent_id: cfg.agentId, agent_type: 'MODELING', organization_id: cfg.organizationId, board_id: cfg.boardId };
|
|
2602
|
+
const journal = (write) => {
|
|
2603
|
+
try { write(); } catch (err) { log(`could not write the prompt journal: ${err.message}`); }
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// Prompts an earlier run of this agent was holding are still CLAIMED / IN_PROGRESS — the platform never re-queues
|
|
2607
|
+
// them by itself. This run holds none yet, so whatever the platform shows as held under this agent's id goes back.
|
|
2608
|
+
{
|
|
2609
|
+
const recovered = await recoverOwnPrompts({ kitDir, meta: journalMeta, agentId: cfg.agentId, fetchHeld: fetchHeldPrompts, release: releasePrompt });
|
|
2610
|
+
if (recovered.released || recovered.settled || recovered.left) {
|
|
2611
|
+
log(`prompts from an earlier run: ${recovered.released} handed back, ${recovered.settled} already settled${recovered.left ? `, ${recovered.left} left for the next start` : ''}`);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2550
2615
|
let draining = false;
|
|
2551
2616
|
async function drain() {
|
|
2552
2617
|
if (draining) return;
|
|
@@ -2583,10 +2648,28 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2583
2648
|
continue;
|
|
2584
2649
|
}
|
|
2585
2650
|
log(`prompt received: "${p.prompt}" (board=${p.board_id ?? cfg.boardId ?? 'n/a'}, priority=${p.priority})`);
|
|
2651
|
+
journal(() => journalPromptReceived({ kitDir, prompt: p, meta: journalMeta }));
|
|
2586
2652
|
try {
|
|
2587
|
-
|
|
2653
|
+
turnPromptId = p.id;
|
|
2654
|
+
const reply = await runTurn(buildTurn(p));
|
|
2655
|
+
// The agent cannot run `learn` (locked-down Bash), so it states learnings in its reply and they are recorded here.
|
|
2656
|
+
// Best effort: a failed write never takes the prompt queue down.
|
|
2657
|
+
if (keepsRecord) {
|
|
2658
|
+
try {
|
|
2659
|
+
const n = recordTurnLearnings({ kitDir, reply, meta: journalMeta });
|
|
2660
|
+
if (n) log(`recorded ${n} learning${n === 1 ? '' : 's'} from the turn`);
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
log(`could not record the turn's learnings: ${err.message}`);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
journal(() => journalPromptFinished({ kitDir, id: p.id, outcome: 'worked', meta: journalMeta }));
|
|
2588
2666
|
} catch (err) {
|
|
2667
|
+
// Not finished: the entry stays open, so the next start asks the platform about it and hands it back
|
|
2668
|
+
// (or closes it if the agent had set DONE before the turn failed).
|
|
2589
2669
|
log(`turn failed: ${err.message}`);
|
|
2670
|
+
} finally {
|
|
2671
|
+
for (const call of calls.drain()) journalCall(call); // never ended: the turn died mid-call, so no duration
|
|
2672
|
+
turnPromptId = null;
|
|
2590
2673
|
}
|
|
2591
2674
|
}
|
|
2592
2675
|
} finally {
|
|
@@ -3203,12 +3286,41 @@ program
|
|
|
3203
3286
|
// dir: a modeling run falls back to the global install (see ensureGlobalKit) rather than
|
|
3204
3287
|
// requiring one here, and the build-kit branch reports a better-targeted error of its own
|
|
3205
3288
|
// than this generic gate can.
|
|
3206
|
-
const NO_INIT_REQUIRED = new Set(['init', 'init-config', 'stacks', 'status', 'config', 'uninstall', 'fetch', 'activate-context', 'set-slice-status', 'release-notes', 'run', 'listen']);
|
|
3289
|
+
const NO_INIT_REQUIRED = new Set(['init', 'init-config', 'stacks', 'status', 'config', 'uninstall', 'fetch', 'activate-context', 'set-slice-status', 'release-notes', 'run', 'listen', 'events', 'replay', 'bridge']);
|
|
3207
3290
|
|
|
3208
3291
|
// Commands that install or refresh the kit themselves — a "your kit is outdated" hint in
|
|
3209
3292
|
// front of them would only be noise.
|
|
3210
3293
|
const NO_KIT_UPDATE_HINT = new Set(['init', 're-init', 'uninstall']);
|
|
3211
3294
|
|
|
3295
|
+
// Retired: still recognised, so an old script or muscle memory gets an explanation instead of "unknown option".
|
|
3296
|
+
// The hidden options and the `bridge` command exist only to say so; they do nothing else.
|
|
3297
|
+
const RETIRED_BASH = [
|
|
3298
|
+
'❌ `run --bash` (the bash-only loop, ralph.sh) has been retired.',
|
|
3299
|
+
' Its task queue (tasks.json) is now an event log inside the kit, which the bash scripts cannot read or write.',
|
|
3300
|
+
' Use the default loop instead: npx @eventmodelers/cli run',
|
|
3301
|
+
' (add --exec "<command>" to drive another agent harness, or --local-ai for a local model).',
|
|
3302
|
+
];
|
|
3303
|
+
const RETIRED_BRIDGE = [
|
|
3304
|
+
'❌ The bridge kit (`init --bridge`, `bridge`) has been retired — it ran on the same tasks.json queue, which is now an event log.',
|
|
3305
|
+
' To hand slices to Spec Kitty, `fetch --spec-kitty` still works on its own, without a standing loop.',
|
|
3306
|
+
];
|
|
3307
|
+
function retire(lines) {
|
|
3308
|
+
console.error(lines.join('\n'));
|
|
3309
|
+
process.exit(1);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
program
|
|
3313
|
+
.command('bridge', { hidden: true })
|
|
3314
|
+
.allowUnknownOption()
|
|
3315
|
+
.allowExcessArguments()
|
|
3316
|
+
.action(() => retire(RETIRED_BRIDGE));
|
|
3317
|
+
|
|
3318
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
3319
|
+
const o = actionCommand.opts();
|
|
3320
|
+
if (actionCommand.name() === 'run' && o.bash) retire(RETIRED_BASH);
|
|
3321
|
+
if (actionCommand.name() === 'init' && (o.bridge || o.target || o.hook)) retire(RETIRED_BRIDGE);
|
|
3322
|
+
});
|
|
3323
|
+
|
|
3212
3324
|
// Startup hint when this CLI is behind npm, or an installed kit here is behind this CLI.
|
|
3213
3325
|
// Best effort and on stderr, so it never breaks piped output; EVENTMODELERS_NO_UPDATE_CHECK
|
|
3214
3326
|
// (or CI) turns it off.
|
|
@@ -3228,7 +3340,6 @@ program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
|
3228
3340
|
console.error(' Run one of these first:');
|
|
3229
3341
|
console.error(` npx @eventmodelers/cli init --stack <name> (${Object.keys(STACKS).join(', ')})`);
|
|
3230
3342
|
console.error(' npx @eventmodelers/cli init --modeling');
|
|
3231
|
-
console.error(` npx @eventmodelers/cli init --bridge --target <name> (${Object.keys(BRIDGE_TARGETS).join(', ')})`);
|
|
3232
3343
|
process.exit(1);
|
|
3233
3344
|
});
|
|
3234
3345
|
|
|
@@ -3278,15 +3389,15 @@ function identityOverridesFromOpts(opts) {
|
|
|
3278
3389
|
credentialFlags(program
|
|
3279
3390
|
.command('init')
|
|
3280
3391
|
.alias('install')
|
|
3281
|
-
.description('Scaffold a stack + install the agent kit into the current directory (or --modeling for skills + agent loop only, no backend scaffold; or --
|
|
3392
|
+
.description('Scaffold a stack + install the agent kit into the current directory (or --modeling for skills + agent loop only, no backend scaffold; or --build-kit for a blank build-kit scaffold to fill in for a stack not built into this CLI yet; or --git to install a community/custom build kit from a git repo)')
|
|
3282
3393
|
.option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')}), or a name of your choosing when combined with --git`)
|
|
3283
3394
|
.option('--git <url>', 'Install a build kit not built into this CLI by cloning this git repo (used with --stack <name> to name it) — the repo must mirror the templates/.claude, templates/root, templates/<kitSubdir> layout of this CLI\'s own stacks/<name>/templates, optionally with a stack.json declaring label/kitSubdir/useShared/needsBoardId')
|
|
3284
3395
|
.option('--branch <name>', 'Branch to clone — only meaningful with --git (defaults to the repo\'s default branch)')
|
|
3285
|
-
.option('--modeling', 'Install skills + the agent loop only — no backend scaffold. Mutually exclusive with --stack/--
|
|
3286
|
-
.
|
|
3287
|
-
.
|
|
3288
|
-
.
|
|
3289
|
-
.option('--build-kit', 'Install a blank build-kit scaffold (.build-kit/ + .claude/skills/build-*/SKILL.md placeholders, all TODO-marked) for a stack not built into this CLI yet — no fixed backend. Mutually exclusive with --stack/--modeling
|
|
3396
|
+
.option('--modeling', 'Install skills + the agent loop only — no backend scaffold. Mutually exclusive with --stack/--build-kit.')
|
|
3397
|
+
.addOption(new Option('--bridge').hideHelp())
|
|
3398
|
+
.addOption(new Option('--target <name>').hideHelp())
|
|
3399
|
+
.addOption(new Option('--hook <command>').hideHelp())
|
|
3400
|
+
.option('--build-kit', 'Install a blank build-kit scaffold (.build-kit/ + .claude/skills/build-*/SKILL.md placeholders, all TODO-marked) for a stack not built into this CLI yet — no fixed backend. Mutually exclusive with --stack/--modeling.')
|
|
3290
3401
|
.option('--hooks', 'Install the slice commit-scope guard (.githooks/pre-commit, running .build-kit/lib/check-commit-scope.cjs) and wire it up via `git config core.hooksPath .githooks` — only meaningful with --stack (build-kit stacks). Off by default.')
|
|
3291
3402
|
.option('--demo', 'Install a ready-made demo model into the kit\'s .slices/ — the "Understanding Eventsourcing" context (16 shopping-cart slices covering every slice type), in exactly the shape `fetch` writes, so the build skills and the agent loop have something real to work on before this project is connected to a board. Skipped if .slices/ already holds fetched slices. Off by default.')
|
|
3292
3403
|
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
|
|
@@ -3296,10 +3407,10 @@ credentialFlags(program
|
|
|
3296
3407
|
.action(async (opts, command) => {
|
|
3297
3408
|
const globalOpts = command.optsWithGlobals();
|
|
3298
3409
|
|
|
3299
|
-
if (opts.modeling || opts.
|
|
3300
|
-
const modeCount = [opts.modeling, opts.
|
|
3410
|
+
if (opts.modeling || opts.buildKit) {
|
|
3411
|
+
const modeCount = [opts.modeling, opts.buildKit].filter(Boolean).length;
|
|
3301
3412
|
if (opts.stack || opts.git || modeCount > 1) {
|
|
3302
|
-
console.error('❌ --stack/--git, --modeling
|
|
3413
|
+
console.error('❌ --stack/--git, --modeling and --build-kit are mutually exclusive — pick one.');
|
|
3303
3414
|
process.exit(1);
|
|
3304
3415
|
}
|
|
3305
3416
|
}
|
|
@@ -3328,38 +3439,6 @@ credentialFlags(program
|
|
|
3328
3439
|
return;
|
|
3329
3440
|
}
|
|
3330
3441
|
|
|
3331
|
-
if (opts.bridge) {
|
|
3332
|
-
if (!opts.target) {
|
|
3333
|
-
console.error(`❌ --bridge requires --target (${Object.keys(BRIDGE_TARGETS).join(', ')}).`);
|
|
3334
|
-
process.exit(1);
|
|
3335
|
-
}
|
|
3336
|
-
if (!BRIDGE_TARGETS[opts.target]) {
|
|
3337
|
-
console.error(`❌ Unknown bridge target "${opts.target}". Available: ${Object.keys(BRIDGE_TARGETS).join(', ')}`);
|
|
3338
|
-
process.exit(1);
|
|
3339
|
-
}
|
|
3340
|
-
await installStack(BRIDGE_KIT.key, BRIDGE_KIT, {
|
|
3341
|
-
configPath: globalOpts.config,
|
|
3342
|
-
print: globalOpts.print,
|
|
3343
|
-
global: opts.global,
|
|
3344
|
-
force: opts.force,
|
|
3345
|
-
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
3346
|
-
demo: opts.demo,
|
|
3347
|
-
target: opts.target,
|
|
3348
|
-
});
|
|
3349
|
-
// Deliberately NOT under .bridge-kit/.eventmodelers/ — that whole name is
|
|
3350
|
-
// gitignored (a bare `.eventmodelers` pattern matches at any depth, since
|
|
3351
|
-
// it protects the root credentials file), so anything written there is
|
|
3352
|
-
// per-machine only. target/hookCommand are project policy — how this repo
|
|
3353
|
-
// reacts to board changes — meant to be committed and shared by every
|
|
3354
|
-
// teammate and CI runner, so they live in a plain sibling file instead.
|
|
3355
|
-
const bridgeConfigPath = join(process.cwd(), BRIDGE_KIT.kitDirName, 'bridge.json');
|
|
3356
|
-
const existingBridgeCfg = readJsonSafe(bridgeConfigPath);
|
|
3357
|
-
mkdirSync(dirname(bridgeConfigPath), { recursive: true });
|
|
3358
|
-
writeFileSync(bridgeConfigPath, JSON.stringify({ ...existingBridgeCfg, target: opts.target, ...(opts.hook ? { hookCommand: opts.hook } : {}) }, null, 2));
|
|
3359
|
-
console.log(` ✓ Bridge target set to "${opts.target}"${opts.hook ? ` with hook: ${opts.hook}` : ''}`);
|
|
3360
|
-
return;
|
|
3361
|
-
}
|
|
3362
|
-
|
|
3363
3442
|
if (opts.branch && !opts.git) {
|
|
3364
3443
|
console.error('❌ --branch only applies to --git — nothing to clone without it.');
|
|
3365
3444
|
process.exit(1);
|
|
@@ -3561,13 +3640,11 @@ program
|
|
|
3561
3640
|
.description("Run this project's commit-scope checks (.build-kit/lib/check-commit-scope.cjs, the same runner .githooks/pre-commit calls) against the currently staged changeset. Works for any build-kit stack — stacks/installs that ship no checks yet report that and exit 0, so this is safe to call unconditionally (e.g. from an agent loop) without checking the stack first.")
|
|
3562
3641
|
.action(() => {
|
|
3563
3642
|
const cwd = process.cwd();
|
|
3564
|
-
// Mirrors `run`'s buildKitDir resolution: modeling-kit
|
|
3565
|
-
//
|
|
3566
|
-
// installed dir isn't one of those over blindly taking the first match.
|
|
3643
|
+
// Mirrors `run`'s buildKitDir resolution: a modeling-kit install shares KIT_DIR_NAMES but never ships
|
|
3644
|
+
// check-commit-scope.cjs, so prefer whichever installed dir isn't the modeling kit.
|
|
3567
3645
|
const installedKitDirs = findAllInstalledKitDirs(cwd);
|
|
3568
3646
|
const modelingKitDir = installedKitDirs.find((d) => d.endsWith(MODELING_KIT.kitDirName)) ?? null;
|
|
3569
|
-
const
|
|
3570
|
-
const kitDir = installedKitDirs.find((d) => d !== modelingKitDir && d !== bridgeKitDir) ?? installedKitDirs[0];
|
|
3647
|
+
const kitDir = installedKitDirs.find((d) => d !== modelingKitDir) ?? installedKitDirs[0];
|
|
3571
3648
|
|
|
3572
3649
|
const checkScript = join(kitDir, 'lib', 'check-commit-scope.cjs');
|
|
3573
3650
|
if (!existsSync(checkScript)) {
|
|
@@ -3702,10 +3779,10 @@ credentialFlags(program
|
|
|
3702
3779
|
.command('run')
|
|
3703
3780
|
.description('Start the agent loop from the installed kit dir — build-kit stacks: ralph-claude.js (default); modeling-kit: --modeling, or --standalone, which needs no install at all')
|
|
3704
3781
|
.option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner: a build kit runs it via ralph-local-ai.js, and --modeling/--standalone via lib/modeling-local-ai.js (board tools over MCP, but no skills and no subagent fan-out — those are Claude Code features). Optional target preset picks the URL and wire dialect: ${LOCAL_AI_TARGETS.join(', ')} — bare --local-ai means ollama. Anything OpenAI-compatible (vLLM, LM Studio, llama.cpp, TGI) works by pointing LOCAL_AI_URL at it; see LOCAL_AI_* in the docs. Board work only: --local-ai never builds a Planned slice (no file, shell or git tools). To build code with a local model, use --exec with a coding harness pointed at it, e.g. "codex exec --oss --full-auto". Claude remains the default when this flag is absent.`)
|
|
3705
|
-
.
|
|
3782
|
+
.addOption(new Option('--bash').hideHelp())
|
|
3783
|
+
.option('--agent [name]', `Pick the agent that does the work by name: ${Object.keys(AGENT_PRESETS).join(', ')}. claude is the default runner; any other is a shortcut for --exec with that harness's headless command filled in — plain-text output for --modeling/--standalone/--worker, JSON output (for agent tracing) for a build kit. Bare --agent lists the agents and the exact command each runs, then stops. Mutually exclusive with --exec and --local-ai.`)
|
|
3706
3784
|
.option('--model <id>', 'The model the agent runs on, for this run only: `-m <id>` on the --agent opencode/codex/gemini command (e.g. opencode/big-pickle, gpt-5-codex, gemini-2.5-pro), `--model <id>` for Claude (overriding `model` from config), LOCAL_AI_MODEL for --local-ai. Not with --exec — put the harness\'s own model flag in the command there.')
|
|
3707
3785
|
.option('--exec [command]', 'Fallback for any harness --agent does not cover, or other flags than its preset. Hand each prompt to an external agent command instead of the default Claude runner — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto", "gemini --yolo -p" or "opencode run". One process per prompt; the prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent. A build kit runs it via ralph-exec.js; there, with agent tracing on, include the harness\'s JSON output flag (codex exec --json, opencode run --format json, gemini --output-format stream-json) — without it no slice cost is recorded. With --modeling/--standalone/--worker it is how any agent takes board prompts and chats: the loop claims each prompt or chat message and runs the command once for it. Keep the harness in its plain-text output mode there — stdout is the turn\'s answer — the eventmodelers MCP server is registered for it on every run (opencode.json, .gemini/settings.json, or -c flags for codex; any other command logs what to register by hand), authenticating with $EVENTMODELERS_TOKEN from the env the command inherits, and `init-agents --hosts <harness>` installs the skills for a harness that does not read .claude/skills. No subagents: self-directed work is done inline.')
|
|
3708
|
-
.option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
|
|
3709
3786
|
.option('--modeling', 'Connect a modeling agent to the board: it takes prompts and chat messages and hands each to an AI — by default one Claude process kept warm across prompts, for low-latency voice/live use; --agent (or --local-ai, or --exec as the manual fallback) picks another. Runs from a modeling-kit install in this directory, or from the global install (~/.eventmodelers/kit) when there is none. Built into the CLI, not a per-project file.')
|
|
3710
3787
|
.option('-f, --force', 'Ask for credentials again before the modeling loop starts, even when a config already has everything required. With a modeling kit in this directory it re-runs the same prompt as `init --modeling`, overwriting the project\'s .eventmodelers/config.json; with the global install it drops the "keep the stored credentials" choice, so a board\'s credentials have to be answered afresh. Applies to build-kit runs too, where it re-asks against the project\'s .eventmodelers/config.json. Cannot be combined with --non-interactive, --print or --local.')
|
|
3711
3788
|
.option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal — right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--worker/--global).')
|
|
@@ -3720,7 +3797,7 @@ credentialFlags(program
|
|
|
3720
3797
|
.option('--handover', 'Backend build kit: for this run only, finish slices that have a UI at `Handover` instead of `Done` (same as the project setting `handover: true`, which `init --handover` / `config --handover on` save — this flag writes nothing). Without it the project setting applies; without both, slices end at `Done`.')
|
|
3721
3798
|
.option('--status-trigger <status>', 'Build kit: the slice status the agent picks work up from, for this run only (e.g. Ready, Planned, Handover). Defaults to the kit\'s own pickup status — `Planned` for backend kits, `Handover` for the UI kits. Nothing is written to disk.')
|
|
3722
3799
|
.option('--verbose', 'Log every tool call\'s full input (commands, skill args, file paths) and assistant reasoning text. Default is condensed, high-level per-step logging only.')
|
|
3723
|
-
.option('--id <id>', 'Pin the agent id this run identifies itself with on the platform.
|
|
3800
|
+
.option('--id <id>', 'Pin the agent id this run identifies itself with on the platform. Without it an agent keeps one id: a project install per project, a --global/--standalone run per board and repo (the directory it is started in) — the same agent on every restart, with its chat, its star on the board and the prompts it holds. If another live process already runs under that id, this one gets a fresh id instead (two agents must not share a heartbeat row). Pass --id when a supervisor already knows the id you want, or for a second agent on purpose; it is used as given, with no such check. Per-run only: nothing is written to disk.')
|
|
3724
3801
|
.option('--name <name>', 'A human-readable name for this agent, sent with every heartbeat so the board shows which agent is live rather than a bare uuid (e.g. "ci-builder", "martins-laptop"). Per-run only, like --id: the persistent name is `agentName` in config.json (set via `init --name` / `init-config --name`), and this overrides it for one run without writing anything.')
|
|
3725
3802
|
.option('--credentials <values>', 'Credentials as the comma-separated blob from app.eventmodelers.ai/account (token=...,boardId=...,organizationId=...,baseUrl=...), the equivalent JSON, or - to read either from stdin. Saved to ~/.eventmodelers/boards/<board>.json, so it is only needed once per board, and passing it skips the first-run question. The individual flags below override single fields of it.'))
|
|
3726
3803
|
.action(async (opts, command) => {
|
|
@@ -3779,11 +3856,6 @@ credentialFlags(program
|
|
|
3779
3856
|
process.exit(1);
|
|
3780
3857
|
}
|
|
3781
3858
|
}
|
|
3782
|
-
// ralph.sh has no realtime agent and never pings /api/agent-alive, so there is no
|
|
3783
|
-
// identity for either flag to override there.
|
|
3784
|
-
if ((identity.agentId || identity.agentName) && opts.bash) {
|
|
3785
|
-
console.log('ℹ️ --id/--name only apply to agents that ping the platform; the --bash loop does not, so they are ignored here.');
|
|
3786
|
-
}
|
|
3787
3859
|
// Both kit dirs can be installed side by side (e.g. running a build-kit and a
|
|
3788
3860
|
// modeling-kit agent from the same project). findInstalledKitDir only ever
|
|
3789
3861
|
// returns its first fixed-order match, which would silently prefer one stack
|
|
@@ -3791,16 +3863,11 @@ credentialFlags(program
|
|
|
3791
3863
|
// we resolve each stack's dir independently instead of relying on that order.
|
|
3792
3864
|
const installedKitDirs = findAllInstalledKitDirs(cwd);
|
|
3793
3865
|
const modelingKitDir = installedKitDirs.find((d) => d.endsWith(MODELING_KIT.kitDirName)) ?? null;
|
|
3794
|
-
const
|
|
3795
|
-
// A bridge kit is not a build-kit stand-in even though it also reuses
|
|
3796
|
-
// lib/ralph.js — it has its own `eventmodelers bridge` entrypoint (no
|
|
3797
|
-
// onPlannedSlice/--local-ai/--bash support), so it's excluded here rather
|
|
3798
|
-
// than falling through to the generic build-kit runner below.
|
|
3799
|
-
const buildKitDir = installedKitDirs.find((d) => d !== modelingKitDir && d !== bridgeKitDir) ?? null;
|
|
3866
|
+
const buildKitDir = installedKitDirs.find((d) => d !== modelingKitDir) ?? null;
|
|
3800
3867
|
|
|
3801
3868
|
// No overlap between the two stacks' runtimes: modeling-kit only ever runs the
|
|
3802
3869
|
// warm, direct-dispatch loop (--modeling); build-kit only ever runs the
|
|
3803
|
-
// cold-spawn/
|
|
3870
|
+
// cold-spawn/task-queue loop (default, or --local-ai/--exec). Neither falls back
|
|
3804
3871
|
// to the other's mechanism, so each side is gated explicitly below rather than
|
|
3805
3872
|
// just being left to fail on a missing file.
|
|
3806
3873
|
// --standalone implies --modeling: it already refused every other runner, so there
|
|
@@ -3809,14 +3876,8 @@ credentialFlags(program
|
|
|
3809
3876
|
// no meaning for a build kit, which is scaffolded per project by definition.
|
|
3810
3877
|
if (opts.modeling || opts.standalone || opts.worker || opts.global) {
|
|
3811
3878
|
const picked = opts.modeling ? '--modeling' : opts.standalone ? '--standalone' : opts.worker ? '--worker' : '--global';
|
|
3812
|
-
// --
|
|
3813
|
-
//
|
|
3814
|
-
// queue, and the modeling loop has its own runner for each (lib/modeling-local-ai.js,
|
|
3815
|
-
// lib/modeling-exec.js).
|
|
3816
|
-
if (opts.bash) {
|
|
3817
|
-
console.error(`❌ ${picked} is mutually exclusive with --bash — that selects a build-kit runner, which the modeling loop has no use for.`);
|
|
3818
|
-
process.exit(1);
|
|
3819
|
-
}
|
|
3879
|
+
// --local-ai and --exec name what does a turn, not a queue, and the modeling loop has its own
|
|
3880
|
+
// runner for each (lib/modeling-local-ai.js, lib/modeling-exec.js).
|
|
3820
3881
|
const modelingLocalAi = resolveLocalAiTarget(opts);
|
|
3821
3882
|
if (modelingLocalAi !== null && opts.exec) {
|
|
3822
3883
|
console.error('❌ --local-ai and --exec are mutually exclusive — pick one runner.');
|
|
@@ -3865,6 +3926,13 @@ credentialFlags(program
|
|
|
3865
3926
|
...Object.fromEntries(Object.entries(credentialOverridesFromOpts(opts)).filter(([, v]) => v)),
|
|
3866
3927
|
};
|
|
3867
3928
|
const config = await resolveModelingCredentials(cwd, flags, globalOpts.config, globalOpts.print, !!opts.nonInteractive, !!opts.force);
|
|
3929
|
+
// The stable id of this board and repo — unless `--id` pins another, or a live process already runs under it,
|
|
3930
|
+
// in which case this run gets a fresh one (agent-lock.js).
|
|
3931
|
+
if (!identity.agentId) {
|
|
3932
|
+
const { agentId, fellBack } = acquireAgentId({ lockDir: join(GLOBAL_DIR, 'locks'), stableId: config.agentId });
|
|
3933
|
+
config.agentId = agentId;
|
|
3934
|
+
config.agentIdFellBack = fellBack;
|
|
3935
|
+
}
|
|
3868
3936
|
projectDir = await ensureGlobalKit(config.baseUrl);
|
|
3869
3937
|
kitDir = join(projectDir, MODELING_KIT.kitDirName);
|
|
3870
3938
|
overrides = config;
|
|
@@ -3896,6 +3964,7 @@ credentialFlags(program
|
|
|
3896
3964
|
// right after (dynamic imports, config reads) can eat the event-loop tick this write
|
|
3897
3965
|
// needed to drain, so a piped watcher sees the ping arrive after runModeling's own
|
|
3898
3966
|
// [modeling] log lines instead of before them.
|
|
3967
|
+
await offerPrune(kitDir, { interactive: !globalOpts.print && !opts.nonInteractive && !!process.stdin.isTTY });
|
|
3899
3968
|
const shown = relative(cwd, kitDir);
|
|
3900
3969
|
const runnerLabel = modelingLocalAi ? 'local model' : opts.exec ? 'one agent process per turn' : 'warm Claude process';
|
|
3901
3970
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (${runnerLabel}) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
@@ -3910,9 +3979,7 @@ credentialFlags(program
|
|
|
3910
3979
|
|
|
3911
3980
|
if (!buildKitDir) {
|
|
3912
3981
|
if (modelingKitDir) {
|
|
3913
|
-
console.error(`❌ A modeling-kit install (${MODELING_KIT.kitDirName}/) only runs via \`eventmodelers run --modeling\` — there is no cold-spawn/
|
|
3914
|
-
} else if (bridgeKitDir) {
|
|
3915
|
-
console.error(`❌ A bridge-kit install (${BRIDGE_KIT.kitDirName}/) only runs via \`eventmodelers bridge\` — it has no --modeling/--local-ai/--bash modes.`);
|
|
3982
|
+
console.error(`❌ A modeling-kit install (${MODELING_KIT.kitDirName}/) only runs via \`eventmodelers run --modeling\` — there is no cold-spawn/task-queue loop for modeling-only projects.`);
|
|
3916
3983
|
} else {
|
|
3917
3984
|
console.error(`❌ No kit installed in ${cwd} — run \`eventmodelers install\` first.`);
|
|
3918
3985
|
console.error(' (A modeling agent needs no install at all: eventmodelers run --standalone --board-id <uuid>)');
|
|
@@ -3922,17 +3989,19 @@ credentialFlags(program
|
|
|
3922
3989
|
const kitDir = buildKitDir;
|
|
3923
3990
|
|
|
3924
3991
|
const localAiTarget = resolveLocalAiTarget(opts);
|
|
3925
|
-
const pickedCount = [
|
|
3992
|
+
const pickedCount = [localAiTarget !== null, !!opts.exec].filter(Boolean).length;
|
|
3926
3993
|
if (pickedCount > 1) {
|
|
3927
|
-
console.error('❌ --
|
|
3994
|
+
console.error('❌ --local-ai and --exec are mutually exclusive — pick one runner.');
|
|
3928
3995
|
process.exit(1);
|
|
3929
3996
|
}
|
|
3930
3997
|
|
|
3998
|
+
await offerPrune(kitDir, { interactive: !globalOpts.print && !opts.nonInteractive && !!process.stdin.isTTY });
|
|
3999
|
+
|
|
3931
4000
|
// The actual agent loop lives in the scaffolded kit dir, not in this package — this
|
|
3932
4001
|
// is just a thin dispatcher so users don't have to remember the kit-dir name or which
|
|
3933
4002
|
// runner file to invoke. Users (and the agent itself, via AGENT.md) may customize these
|
|
3934
4003
|
// files freely; `run` always executes whatever is currently on disk.
|
|
3935
|
-
const runner = opts.
|
|
4004
|
+
const runner = opts.exec ? 'ralph-exec.js' : localAiTarget !== null ? 'ralph-local-ai.js' : 'ralph-claude.js';
|
|
3936
4005
|
const runnerPath = join(kitDir, runner);
|
|
3937
4006
|
if (!existsSync(runnerPath)) {
|
|
3938
4007
|
console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
|
|
@@ -3983,77 +4052,16 @@ credentialFlags(program
|
|
|
3983
4052
|
console.log(`▶ Starting ${relative(cwd, runnerPath)}...\n`);
|
|
3984
4053
|
const cmd = runner.endsWith('.sh') ? `"${runnerPath}"` : `node "${runnerPath}"`;
|
|
3985
4054
|
try {
|
|
3986
|
-
// Only ralph-claude.js reads RALPH_VERBOSE — the
|
|
3987
|
-
//
|
|
3988
|
-
//
|
|
3989
|
-
//
|
|
3990
|
-
// RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
|
|
3991
|
-
// they reach both node runners but not ralph.sh, which has no heartbeat to identify.
|
|
4055
|
+
// Only ralph-claude.js reads RALPH_VERBOSE — the local-AI executor has its own separate output
|
|
4056
|
+
// path with no stream-json parsing to gate. RALPH_LOCAL is read by ralph.js's startRalph to force
|
|
4057
|
+
// the local-only branch even when .eventmodelers/config.json has valid credentials.
|
|
4058
|
+
// RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read there too.
|
|
3992
4059
|
execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_HANDOVER: opts.handover ? '1' : '', RALPH_STATUS_TRIGGER: opts.statusTrigger ? String(opts.statusTrigger).trim() : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}), ...(typeof opts.exec === 'string' ? { RALPH_EXEC_CMD: opts.exec } : {}) } });
|
|
3993
4060
|
} catch (err) {
|
|
3994
4061
|
process.exit(err.status || 1);
|
|
3995
4062
|
}
|
|
3996
4063
|
});
|
|
3997
4064
|
|
|
3998
|
-
program
|
|
3999
|
-
.command('bridge')
|
|
4000
|
-
.description('Start the bridge agent loop from the installed .bridge-kit/ — translates board slice changes into another spec framework instead of building code. A deterministic adapter runs with no LLM call if one exists for the configured target (e.g. spec-kitty); otherwise Claude is the default executor. --local-ai, --hook, or --claude override the pick.')
|
|
4001
|
-
.option('--local-ai [target]', `Use ralph-local-ai.js instead of the default runner — a local or self-hosted model. Optional target preset: ${LOCAL_AI_TARGETS.join(', ')} (bare flag means ollama).`)
|
|
4002
|
-
.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')
|
|
4003
|
-
.option('--claude', 'Force the Claude runner even if a static adapter exists for this target')
|
|
4004
|
-
.action((opts) => {
|
|
4005
|
-
const cwd = process.cwd();
|
|
4006
|
-
const kitDir = findAllInstalledKitDirs(cwd).find((d) => d.endsWith(BRIDGE_KIT.kitDirName)) ?? null;
|
|
4007
|
-
if (!kitDir) {
|
|
4008
|
-
console.error(`❌ No bridge-kit installed in ${cwd} — run \`eventmodelers init --bridge --target <name>\` first.`);
|
|
4009
|
-
process.exit(1);
|
|
4010
|
-
}
|
|
4011
|
-
|
|
4012
|
-
const localAiTarget = resolveLocalAiTarget(opts);
|
|
4013
|
-
if ([localAiTarget !== null, opts.hook, opts.claude].filter(Boolean).length > 1) {
|
|
4014
|
-
console.error('❌ --local-ai, --hook, and --claude are mutually exclusive — pick one executor.');
|
|
4015
|
-
process.exit(1);
|
|
4016
|
-
}
|
|
4017
|
-
|
|
4018
|
-
// A persisted default (from `init --bridge --hook`) lives in bridge.json, a
|
|
4019
|
-
// plain sibling file — NOT under .eventmodelers/, which is gitignored (see
|
|
4020
|
-
// the comment in `init`'s --bridge branch) and would otherwise make this
|
|
4021
|
-
// per-machine instead of a shared, checked-in team/CI convention.
|
|
4022
|
-
const bridgeCfg = readJsonSafe(join(kitDir, 'bridge.json'));
|
|
4023
|
-
const persistedHook = bridgeCfg.hookCommand;
|
|
4024
|
-
const hookCmd = opts.hook || persistedHook;
|
|
4025
|
-
|
|
4026
|
-
// A static adapter (adapters/<target>-adapter.js, e.g. spec-kitty-adapter.js)
|
|
4027
|
-
// is deterministic and costs no LLM call, so it wins over the Claude default
|
|
4028
|
-
// whenever one exists for the configured target — --claude opts back out.
|
|
4029
|
-
const staticAdapterPath = join(kitDir, 'adapters', `${bridgeCfg.target}-adapter.js`);
|
|
4030
|
-
const hasStaticAdapter = bridgeCfg.target && existsSync(staticAdapterPath);
|
|
4031
|
-
|
|
4032
|
-
const runner = hookCmd
|
|
4033
|
-
? 'ralph-hook.js'
|
|
4034
|
-
: localAiTarget !== null
|
|
4035
|
-
? 'ralph-local-ai.js'
|
|
4036
|
-
: !opts.claude && hasStaticAdapter
|
|
4037
|
-
? 'ralph-static.js'
|
|
4038
|
-
: 'ralph-claude.js';
|
|
4039
|
-
const runnerPath = join(kitDir, runner);
|
|
4040
|
-
if (!existsSync(runnerPath)) {
|
|
4041
|
-
console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
|
|
4042
|
-
process.exit(1);
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4045
|
-
console.log(`▶ Starting ${relative(cwd, runnerPath)}${hookCmd ? ` (hook: ${hookCmd})` : ''}...\n`);
|
|
4046
|
-
try {
|
|
4047
|
-
execSync(`node "${runnerPath}"`, {
|
|
4048
|
-
cwd: kitDir,
|
|
4049
|
-
stdio: 'inherit',
|
|
4050
|
-
env: hookCmd ? { ...process.env, BRIDGE_HOOK_CMD: hookCmd } : process.env,
|
|
4051
|
-
});
|
|
4052
|
-
} catch (err) {
|
|
4053
|
-
process.exit(err.status || 1);
|
|
4054
|
-
}
|
|
4055
|
-
});
|
|
4056
|
-
|
|
4057
4065
|
program
|
|
4058
4066
|
.command('listen')
|
|
4059
4067
|
.description('Start the code-export listener (code-export.mjs) from the installed kit dir, or the bundled copy when no kit is installed — receives slice/screen data pushed from the eventmodelers board UI and writes it into .slices/')
|
|
@@ -4360,6 +4368,236 @@ program
|
|
|
4360
4368
|
console.log(`✅ Pushed status change to the board (node ${slice.id}).`);
|
|
4361
4369
|
});
|
|
4362
4370
|
|
|
4371
|
+
// The kit a command that records events works against: a build kit if one is installed, else the modeling
|
|
4372
|
+
// kit. The build kit wins when both are installed — the modeling agent never runs these commands itself
|
|
4373
|
+
// (its Bash is an allowlist); it states its learnings in its reply and `runModeling` records them.
|
|
4374
|
+
function requireBuildKitDir(cwd) {
|
|
4375
|
+
const installed = findAllInstalledKitDirs(cwd);
|
|
4376
|
+
const kitDir = installed.find((d) => !d.endsWith(MODELING_KIT.kitDirName)) ?? installed.find((d) => d.endsWith(MODELING_KIT.kitDirName));
|
|
4377
|
+
if (!kitDir) {
|
|
4378
|
+
console.error('❌ This command needs a build or modeling kit (npx @eventmodelers/cli init --stack <name> or init --modeling).');
|
|
4379
|
+
process.exit(1);
|
|
4380
|
+
}
|
|
4381
|
+
return kitDir;
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
// What the build agent's events are stamped with: the loop exports the agent id into its environment.
|
|
4385
|
+
function agentEventMeta(cwd, kitDir, command) {
|
|
4386
|
+
const { config: cfg } = loadEffectiveConfig(cwd, kitDir, command.optsWithGlobals().config);
|
|
4387
|
+
return {
|
|
4388
|
+
agent_id: process.env.RALPH_AGENT_ID || process.env.EVENTMODELERS_AGENT_ID,
|
|
4389
|
+
agent_type: 'BUILD', // `learn` and `slice-status` are the build agent's commands
|
|
4390
|
+
organization_id: cfg.organizationId,
|
|
4391
|
+
board_id: cfg.boardId,
|
|
4392
|
+
};
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
program
|
|
4396
|
+
.command('slice-status <status>')
|
|
4397
|
+
.description("For a build agent: record the outcome of the slice the run loop is building (Done, Handover, Blocked, ...). The loop owns the slice's status on the board and pushes it when the build ends — never edit index.json or call update-slice-status for a loop build.")
|
|
4398
|
+
.option('--slice <idOrTitle>', 'The slice, when the loop has claimed more than one (default: the one it is building)')
|
|
4399
|
+
.action((status, opts, command) => {
|
|
4400
|
+
const cwd = process.cwd();
|
|
4401
|
+
const canonical = SLICE_STATUSES.find((s) => s.value.toLowerCase() === status.toLowerCase())?.value;
|
|
4402
|
+
if (!canonical) {
|
|
4403
|
+
console.error(`❌ "${status}" is not a slice status. Valid: ${SLICE_STATUSES.map((s) => s.value).join(', ')}`);
|
|
4404
|
+
process.exit(1);
|
|
4405
|
+
}
|
|
4406
|
+
const kitDir = requireBuildKitDir(cwd);
|
|
4407
|
+
const meta = agentEventMeta(cwd, kitDir, command);
|
|
4408
|
+
try {
|
|
4409
|
+
const result = recordAgentStatus({ kitDir, status: canonical, sliceId: opts.slice, meta });
|
|
4410
|
+
console.log(`✅ "${result.title}": recorded ${result.status} — the run loop pushes it to the board when the build ends.`);
|
|
4411
|
+
} catch (err) {
|
|
4412
|
+
console.error(`❌ ${err.message}`);
|
|
4413
|
+
process.exit(1);
|
|
4414
|
+
}
|
|
4415
|
+
});
|
|
4416
|
+
|
|
4417
|
+
program
|
|
4418
|
+
.command('learn [text]')
|
|
4419
|
+
.description("For a build agent: record one reusable learning (a codebase pattern, a gotcha, a command that works or doesn't, what a failed approach tried and why it failed). .build-kit/AGENTS.md is generated from the recorded learnings — never edit it by hand. --retract withdraws a wrong or obsolete one (ids: `learnings --ids`).")
|
|
4420
|
+
.option('--topic <topic>', 'Group the learning under this heading in AGENTS.md')
|
|
4421
|
+
.option('--tag <tag>', 'Label the learning (repeatable)', (tag, tags) => [...tags, tag], [])
|
|
4422
|
+
.option('--scope <scope>', 'system: true of the modeled domain · repo: about the codebase and its tooling (default repo)')
|
|
4423
|
+
.option('--slice <slice>', 'The slice it was learned on')
|
|
4424
|
+
.option('--retract <id>', 'Withdraw the learning with this id (or a unique prefix of it) instead of recording one')
|
|
4425
|
+
.option('--merge <ids>', 'Replace these learnings (comma-separated ids) by one with the given text — the curator\'s tool')
|
|
4426
|
+
.option('--reason <reason>', 'Why — recorded with a retraction or a merge')
|
|
4427
|
+
.action((text, opts, command) => {
|
|
4428
|
+
const cwd = process.cwd();
|
|
4429
|
+
const kitDir = requireBuildKitDir(cwd);
|
|
4430
|
+
const meta = agentEventMeta(cwd, kitDir, command);
|
|
4431
|
+
try {
|
|
4432
|
+
if (opts.retract !== undefined) {
|
|
4433
|
+
const gone = retractLearning({ kitDir, id: opts.retract, reason: opts.reason, meta });
|
|
4434
|
+
console.log(`✅ Retracted: ${gone.text}`);
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
4437
|
+
if (opts.merge !== undefined) {
|
|
4438
|
+
const ids = opts.merge.split(',').map((id) => id.trim()).filter(Boolean);
|
|
4439
|
+
const merged = mergeLearnings({ kitDir, ids, text, topic: opts.topic, scope: opts.scope, tags: opts.tag, reason: opts.reason, meta });
|
|
4440
|
+
console.log(`✅ Merged ${ids.length} learnings into one (${merged.id.slice(0, 8)}).`);
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
if (text === undefined) throw new Error('give the learning as text, or --retract <id>, or --merge <ids> <text>');
|
|
4444
|
+
// The curator may only merge and retract — an LLM asked to tidy must not add facts of its own.
|
|
4445
|
+
if (process.env.EVENTMODELERS_CURATING === '1') throw new Error('a curation turn only merges and retracts — new learnings are not recorded during it');
|
|
4446
|
+
const result = recordLearning({ kitDir, text, topic: opts.topic, tags: opts.tag, scope: opts.scope ?? 'repo', slice: opts.slice, meta });
|
|
4447
|
+
console.log(result.duplicate ? `ℹ️ Already recorded (${result.id.slice(0, 8)}) — nothing to add.` : `✅ Recorded (${result.id.slice(0, 8)}) — .build-kit/AGENTS.md is updated.`);
|
|
4448
|
+
} catch (err) {
|
|
4449
|
+
console.error(`❌ ${err.message}`);
|
|
4450
|
+
process.exit(1);
|
|
4451
|
+
}
|
|
4452
|
+
});
|
|
4453
|
+
|
|
4454
|
+
program
|
|
4455
|
+
.command('events')
|
|
4456
|
+
.description("Decode a kit's event log in the terminal: one line per event (claims, settles, learnings, tasks, prompts …) with the slice's title and the agent that wrote it. Filter it, print the raw events, or print the state the log folds to.")
|
|
4457
|
+
.option('--kit <dir>', "Read this kit directory's log (default: the build kit of the current project, else its modeling kit)")
|
|
4458
|
+
.option('--global', 'Read the log of the global install (~/.eventmodelers/kit) — what runs started without a project install wrote')
|
|
4459
|
+
.option('--board <id>', "Only events stamped with this board (an id prefix) — the global install's log mixes every board")
|
|
4460
|
+
.option('--type <types>', 'Only these event types, comma-separated (e.g. claimed,settled)')
|
|
4461
|
+
.option('--slice <slice>', 'Only events of the slice with this id (prefix) or a title containing this text')
|
|
4462
|
+
.option('--tag <tag>', 'Only events carrying this tag')
|
|
4463
|
+
.option('--last <n>', 'Only the last n events (after the other filters)')
|
|
4464
|
+
.option('--json', 'Print the raw events, one JSON object per line')
|
|
4465
|
+
.option('--verbose', 'Print each event\'s payload under its line — for a tool call, the input it was sent (recorded by runs started with --verbose)')
|
|
4466
|
+
.option('--state', 'Print the state the log folds to (slices, queue, open prompts, learnings) instead of the events')
|
|
4467
|
+
.option('--prune', 'Delete the events nothing reads any more — the journal of finished prompts — and stop. Slice state, learnings and open prompts are kept; the same prune a run offers at every 1000 events.')
|
|
4468
|
+
.option('--follow', 'Keep running and print events as they are appended')
|
|
4469
|
+
.option('--watch', 'Same as --follow')
|
|
4470
|
+
.action(async (opts) => {
|
|
4471
|
+
const cwd = process.cwd();
|
|
4472
|
+
if (opts.global && opts.kit) {
|
|
4473
|
+
console.error('❌ --global and --kit are alternatives — pick one.');
|
|
4474
|
+
process.exit(1);
|
|
4475
|
+
}
|
|
4476
|
+
const globalLog = join(GLOBAL_KIT_DIR, MODELING_KIT.kitDirName);
|
|
4477
|
+
const kitDir = opts.global ? globalLog : opts.kit ? resolve(cwd, opts.kit) : requireBuildKitDir(cwd);
|
|
4478
|
+
if (opts.prune) {
|
|
4479
|
+
try {
|
|
4480
|
+
const { removed, kept } = pruneLog(kitDir);
|
|
4481
|
+
rememberPruneAnswer(kitDir, kept);
|
|
4482
|
+
console.log(removed ? `Pruned ${removed} events, ${kept} left.` : `Nothing to prune — ${kept} events, all of them carry state.`);
|
|
4483
|
+
} catch (err) {
|
|
4484
|
+
console.error(`❌ ${err.message}`);
|
|
4485
|
+
process.exit(1);
|
|
4486
|
+
}
|
|
4487
|
+
return;
|
|
4488
|
+
}
|
|
4489
|
+
let last;
|
|
4490
|
+
if (opts.last !== undefined) {
|
|
4491
|
+
last = Number(opts.last);
|
|
4492
|
+
if (!Number.isInteger(last) || last < 1) {
|
|
4493
|
+
console.error('❌ --last needs a positive number.');
|
|
4494
|
+
process.exit(1);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
const filter = { types: opts.type?.split(',').map((t) => t.trim()).filter(Boolean), slice: opts.slice, tag: opts.tag, board: opts.board };
|
|
4498
|
+
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4499
|
+
const paint = (code, text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
4500
|
+
const family = (type) => (/^(claim|settled|agent-recorded|task-done|prompt-finished)/.test(type) ? '32' : /^(attempt-failed|blocked|released)/.test(type) ? '31' : /^(learning|learnings)/.test(type) ? '36' : /^(prompt|task)/.test(type) ? '35' : '33');
|
|
4501
|
+
const print = (events, titles) => {
|
|
4502
|
+
for (const e of events) {
|
|
4503
|
+
if (opts.json) { console.log(JSON.stringify(e)); continue; }
|
|
4504
|
+
const [time, ...rest] = formatEvent(e, { titles }).split(' ');
|
|
4505
|
+
const type = rest.shift();
|
|
4506
|
+
console.log([paint('2', time), paint(family(e.type), type), ...rest].join(' '));
|
|
4507
|
+
if (opts.verbose) { const payload = formatPayload(e); if (payload) console.log(paint('2', payload)); }
|
|
4508
|
+
}
|
|
4509
|
+
};
|
|
4510
|
+
|
|
4511
|
+
const follow = opts.follow || opts.watch;
|
|
4512
|
+
const events = readEvents(kitDir);
|
|
4513
|
+
const titles = sliceTitles(events);
|
|
4514
|
+
if (opts.state) {
|
|
4515
|
+
if (follow) console.error('ℹ️ --follow/--watch does not apply to --state.');
|
|
4516
|
+
console.log(events.length ? summarize(fold(events), titles) : 'No events yet in this kit.');
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
if (!events.length && !follow) {
|
|
4520
|
+
console.log(`No events yet in ${opts.global ? '~/.eventmodelers/kit' : relative(cwd, kitDir) || kitDir}.`);
|
|
4521
|
+
// Runs started without a project install keep their log in the global kit — say so when it has one.
|
|
4522
|
+
if (!opts.global && !opts.kit && readEvents(globalLog).length) console.log('The global install has a log of its own: `eventmodelers events --global`.');
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
print(filterEvents(events, { ...filter, last }, titles), titles);
|
|
4526
|
+
if (!follow) return;
|
|
4527
|
+
|
|
4528
|
+
// Follow: what has been printed is remembered by event id, and the log is read again twice a second.
|
|
4529
|
+
const seen = new Set(events.map((e) => e.id));
|
|
4530
|
+
setInterval(() => {
|
|
4531
|
+
const all = readEvents(kitDir);
|
|
4532
|
+
const fresh = all.filter((e) => !seen.has(e.id));
|
|
4533
|
+
for (const e of fresh) seen.add(e.id);
|
|
4534
|
+
if (fresh.length) print(filterEvents(fresh, filter, sliceTitles(all)), sliceTitles(all));
|
|
4535
|
+
}, 500);
|
|
4536
|
+
});
|
|
4537
|
+
|
|
4538
|
+
program
|
|
4539
|
+
.command('replay <id>')
|
|
4540
|
+
.description("Submit a prompt from the event log again: puts it back on its board's queue as a new prompt, with the same text, node, comment, priority and canvas context, for an agent to work. <id> is a prompt id from `events` (a unique prefix will do). Writes to the platform.")
|
|
4541
|
+
.option('--kit <dir>', "Read this kit directory's log (default: the build kit of the current project, else its modeling kit)")
|
|
4542
|
+
.option('--global', 'Read the log of the global install (~/.eventmodelers/kit)')
|
|
4543
|
+
.option('--dry-run', 'Show the request that would be sent and stop — nothing is submitted')
|
|
4544
|
+
.option('--token <uuid>', 'API token (overrides config file / env var)')
|
|
4545
|
+
.option('--organization-id <uuid>', 'Organization ID (overrides config file / env var)')
|
|
4546
|
+
.option('--base-url <url>', 'Platform base URL (overrides config file / env var)')
|
|
4547
|
+
.action(async (id, opts) => {
|
|
4548
|
+
const cwd = process.cwd();
|
|
4549
|
+
if (opts.global && opts.kit) {
|
|
4550
|
+
console.error('❌ --global and --kit are alternatives — pick one.');
|
|
4551
|
+
process.exit(1);
|
|
4552
|
+
}
|
|
4553
|
+
const kitDir = opts.global ? join(GLOBAL_KIT_DIR, MODELING_KIT.kitDirName) : opts.kit ? resolve(cwd, opts.kit) : requireBuildKitDir(cwd);
|
|
4554
|
+
try {
|
|
4555
|
+
const event = findPromptEvent(readEvents(kitDir), id);
|
|
4556
|
+
const boardId = event.prompt.board_id ?? event.meta?.board_id;
|
|
4557
|
+
// The credentials of the board the prompt was for: flags and env vars over the board's own file over the account's.
|
|
4558
|
+
const stored = boardId ? readJsonSafe(boardCredentialsPath(boardId)) : {};
|
|
4559
|
+
const explicit = Object.fromEntries(Object.entries(credentialOverridesFromOpts(opts)).filter(([, v]) => v));
|
|
4560
|
+
const cfg = applyEnvOverrides({ ...loadEffectiveConfig(cwd, null, null).config, ...(stored.useGlobal || stored.useBoard ? {} : stored), ...explicit });
|
|
4561
|
+
cfg.baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
|
|
4562
|
+
if (!cfg.token || !cfg.organizationId) throw new Error('no credentials for this board — pass --token/--organization-id, or run the agent once for it');
|
|
4563
|
+
|
|
4564
|
+
const tokenRes = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/realtime-token`, { headers: { 'x-token': cfg.token } });
|
|
4565
|
+
if (!tokenRes.ok) throw new Error(`realtime-token: HTTP ${tokenRes.status}`);
|
|
4566
|
+
const { url, headers, body } = replayRequest({ baseUrl: cfg.baseUrl, organizationId: cfg.organizationId, token: cfg.token, jwt: (await tokenRes.json()).token, event: { ...event, prompt: { ...event.prompt, board_id: boardId } } });
|
|
4567
|
+
if (opts.dryRun) {
|
|
4568
|
+
console.log(`POST ${url}\n${JSON.stringify(body, null, 2)}`);
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
4572
|
+
if (!res.ok) throw new Error(`prompts: HTTP ${res.status} ${(await res.text()).slice(0, 200)}`);
|
|
4573
|
+
const created = await res.json();
|
|
4574
|
+
console.log(`Re-submitted prompt ${String(event.prompt.id).slice(0, 8)} as ${String(created.id ?? '').slice(0, 8)}: "${event.prompt.text.length > 80 ? `${event.prompt.text.slice(0, 79)}…` : event.prompt.text}"`);
|
|
4575
|
+
} catch (err) {
|
|
4576
|
+
console.error(`❌ ${err.message}`);
|
|
4577
|
+
process.exit(1);
|
|
4578
|
+
}
|
|
4579
|
+
});
|
|
4580
|
+
|
|
4581
|
+
program
|
|
4582
|
+
.command('learnings')
|
|
4583
|
+
.description('Print the recorded learnings — what .build-kit/AGENTS.md says. With --ids, one line each with the id to retract it by.')
|
|
4584
|
+
.option('--ids', 'One line per learning: id, topic, text')
|
|
4585
|
+
.action((opts, command) => {
|
|
4586
|
+
const cwd = process.cwd();
|
|
4587
|
+
const kitDir = requireBuildKitDir(cwd);
|
|
4588
|
+
syncLearnings({ kitDir, meta: agentEventMeta(cwd, kitDir, command) });
|
|
4589
|
+
const all = listLearnings(kitDir);
|
|
4590
|
+
if (!all.length) {
|
|
4591
|
+
console.log('No learnings recorded yet.');
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
if (!opts.ids) {
|
|
4595
|
+
console.log(readFileSync(join(kitDir, 'AGENTS.md'), 'utf-8').trimEnd());
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
for (const l of all) console.log(` ${l.id.slice(0, 8)} [${l.topic ?? 'General'}] ${l.text}`);
|
|
4599
|
+
});
|
|
4600
|
+
|
|
4363
4601
|
program
|
|
4364
4602
|
.command('stacks')
|
|
4365
4603
|
.description('List available stacks (for `init --stack`)')
|
|
@@ -4455,16 +4693,14 @@ program
|
|
|
4455
4693
|
.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.')
|
|
4456
4694
|
.option('--build-kit', `Remove ${STACKS.node.kitDirName}/ (the backend-stack kit dir)`)
|
|
4457
4695
|
.option('--modeling-kit', `Remove ${MODELING_KIT.kitDirName}/ (the modeling-only kit dir)`)
|
|
4458
|
-
.option('--bridge-kit', `Remove ${BRIDGE_KIT.kitDirName}/ (the bridge kit dir)`)
|
|
4459
4696
|
.action((opts) => {
|
|
4460
4697
|
const cwd = process.cwd();
|
|
4461
4698
|
let targets;
|
|
4462
4699
|
|
|
4463
|
-
if (opts.buildKit || opts.modelingKit
|
|
4700
|
+
if (opts.buildKit || opts.modelingKit) {
|
|
4464
4701
|
targets = [];
|
|
4465
4702
|
if (opts.buildKit) targets.push(join(cwd, STACKS.node.kitDirName));
|
|
4466
4703
|
if (opts.modelingKit) targets.push(join(cwd, MODELING_KIT.kitDirName));
|
|
4467
|
-
if (opts.bridgeKit) targets.push(join(cwd, BRIDGE_KIT.kitDirName));
|
|
4468
4704
|
targets = targets.filter((p) => existsSync(p));
|
|
4469
4705
|
if (!targets.length) {
|
|
4470
4706
|
console.log('ℹ️ Nothing to remove for the requested option(s).');
|
|
@@ -4477,7 +4713,7 @@ program
|
|
|
4477
4713
|
return;
|
|
4478
4714
|
}
|
|
4479
4715
|
if (targets.length > 1) {
|
|
4480
|
-
console.log('⚠️ Multiple kit dirs found — re-run with --build-kit
|
|
4716
|
+
console.log('⚠️ Multiple kit dirs found — re-run with --build-kit and/or --modeling-kit to pick which to remove.');
|
|
4481
4717
|
targets.forEach((t) => console.log(` ${t}`));
|
|
4482
4718
|
return;
|
|
4483
4719
|
}
|