@eventmodelers/cli 1.0.163 → 2.0.2
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 +48 -36
- package/RELEASE_NOTES.md +20 -2
- package/cli.js +774 -247
- package/lib/adapters/spec-kitty-adapter.js +3 -7
- package/lib/build-kits.js +23 -0
- package/lib/global-agent-id.js +45 -0
- package/lib/modeling-local-ai.js +4 -2
- package/lib/scaffold-model.js +71 -0
- package/lib/scaffold.js +224 -0
- package/lib/supervise.js +60 -0
- package/lib/ui-baseline.js +123 -0
- 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 +134 -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 +218 -169
- 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 +184 -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/scaffold/LICENSE-em-scaffold +21 -0
- package/shared/scaffold/supabase/generate.mjs +290 -0
- package/shared/scaffold/supabase/template/scripts/create-user.mjs.tmpl +29 -0
- package/shared/scaffold/supabase/template/scripts/dev.mjs.tmpl +145 -0
- package/shared/scaffold/supabase/template/scripts/write-env.mjs.tmpl +44 -0
- package/shared/scaffold/supabase/template/server.ts.tmpl +168 -0
- package/shared/scaffold/supabase/template/src/common/extraEndpoints.ts.tmpl +5 -0
- package/shared/scaffold/supabase/template/src/common/notImplemented.test.ts.tmpl +58 -0
- package/shared/scaffold/supabase/template/src/common/notImplemented.ts.tmpl +29 -0
- package/shared/scaffold/supabase/template/supabase/migrations/_V0__supabase_stubs.sql.tmpl +34 -0
- package/shared/scaffold/supabase/template/web/src/App.tsx.tmpl +38 -0
- package/shared/scaffold/supabase/template/web/src/app/Layout.tsx.tmpl +48 -0
- package/shared/scaffold/supabase/template/web/src/app/NotFound.tsx.tmpl +12 -0
- package/shared/scaffold/supabase/template/web/src/app/SignIn.tsx.tmpl +42 -0
- package/shared/scaffold/supabase/template/web/src/app/TimelinePage.tsx.tmpl +73 -0
- package/shared/scaffold/supabase/template/web/src/app/pages.ts.tmpl +26 -0
- package/shared/scaffold/supabase/template/web/src/app/skeleton.css.tmpl +25 -0
- package/shared/scaffold/supabase/template/web/src/lib/NotBuilt.tsx.tmpl +12 -0
- package/shared/scaffold/supabase/template/web/src/lib/api.ts.tmpl +226 -0
- package/shared/scaffold/supabase/template/web/src/main.tsx.tmpl +13 -0
- package/shared/skills/learn-eventmodelers-api/SKILL.md +17 -4
- package/shared/skills/request-feedback/SKILL.md +1 -2
- package/shared/skills/ui-contract/SKILL.md +7 -5
- package/shared/skills/update-slice-status/SKILL.md +1 -1
- package/stacks/axon/templates/build-kit/CLAUDE.md +7 -6
- 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 +32 -41
- package/stacks/blank/templates/build-kit/CLAUDE.md +7 -6
- 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 +33 -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 +9 -8
- 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 +33 -42
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +7 -7
- 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 +32 -41
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +1 -1
- 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/wdyt/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +3 -4
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +18 -28
- 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 +7 -6
- 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 +7 -7
- 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 +32 -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 +7 -6
- package/stacks/react/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +25 -42
- package/stacks/react/templates/build-kit/lib/kit.json +1 -1
- package/stacks/react/templates/build-kit/lib/prompt.md +47 -43
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +15 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +7 -6
- 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 +80 -8
- package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +66 -13
- package/stacks/supabase-react/templates/build-kit/CLAUDE.md +8 -7
- package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +6 -7
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +25 -42
- package/stacks/supabase-react/templates/build-kit/lib/kit.json +1 -1
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +48 -44
- package/stacks/umadb/templates/build-kit/CLAUDE.md +7 -6
- 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 +32 -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,
|
|
@@ -19,6 +20,10 @@ import { createInterface, emitKeypressEvents, moveCursor, clearScreenDown } from
|
|
|
19
20
|
import { homedir, tmpdir } from 'os';
|
|
20
21
|
import { randomUUID } from 'crypto';
|
|
21
22
|
import { runFetch, FetchAuthError } from './lib/fetch.js';
|
|
23
|
+
import { SCAFFOLD_TYPES, scaffoldTypeNames, missingPrerequisites, runScaffold, builtInTemplates, initTemplates } from './lib/scaffold.js';
|
|
24
|
+
import { supervise } from './lib/supervise.js';
|
|
25
|
+
import { findBuildKitDirs } from './lib/build-kits.js';
|
|
26
|
+
import { buildModel } from './lib/scaffold-model.js';
|
|
22
27
|
import { createModelingLocalAiRunner } from './lib/modeling-local-ai.js';
|
|
23
28
|
import { run as runSpecKittyAdapter } from './lib/adapters/spec-kitty-adapter.js';
|
|
24
29
|
import { latestPublishedVersion, updateHints } from './lib/update-check.js';
|
|
@@ -29,14 +34,27 @@ import { modelingPermissions } from './lib/agent-permissions.js';
|
|
|
29
34
|
// copyDirContents in installStack) for ralph.js to import standalone — see
|
|
30
35
|
// shared/build-kit/lib/adapters/realtime-adapter.js for why it lives there instead.
|
|
31
36
|
import { createRealtimeAdapter, keepSubscribed } from './shared/build-kit/lib/adapters/realtime-adapter.js';
|
|
37
|
+
import { recordAgentStatus } from './shared/build-kit/lib/slice-run.js';
|
|
38
|
+
import { findSliceDir, fingerprint, readBaseline, writeBaseline, compare } from './lib/ui-baseline.js';
|
|
39
|
+
import { defaultAgentName, withInstanceSuffix } from './shared/build-kit/lib/agent-name.js';
|
|
40
|
+
import { acquireAgentId } from './shared/build-kit/lib/agent-lock.js';
|
|
41
|
+
import { readEvents } from './shared/build-kit/lib/slice-events.js';
|
|
42
|
+
import { findPromptEvent, replayRequest } from './shared/build-kit/lib/prompt-replay.js';
|
|
43
|
+
import { prunableEvents, pruneLog, rememberPruneAnswer, shouldOfferPrune } from './shared/build-kit/lib/event-prune.js';
|
|
44
|
+
import { fold } from './shared/build-kit/lib/slice-state.js';
|
|
45
|
+
import { filterEvents, baselineIds, formatEvent, formatPayload, sliceTitles, summarize } from './shared/build-kit/lib/events-view.js';
|
|
46
|
+
import { stableGlobalAgentId } from './lib/global-agent-id.js';
|
|
47
|
+
import { ensureLearningsGitRules } from './shared/build-kit/lib/ralph.js';
|
|
48
|
+
import { createCallTracker, heldPromptsRequest, journalPromptAction, shouldJournalCall, journalPromptFinished, journalPromptReceived, recoverOwnPrompts } from './shared/build-kit/lib/prompt-journal.js';
|
|
49
|
+
import { listLearnings, mergeLearnings, recordLearning, recordTurnLearnings, retractLearning, syncLearnings } from './shared/build-kit/lib/slice-learnings.js';
|
|
32
50
|
|
|
33
51
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
52
|
const __dirname = dirname(__filename);
|
|
35
53
|
|
|
36
54
|
// Each stack is a template set under stacks/<key>/templates/{.claude,root,<kitSubdir>}.
|
|
37
55
|
// 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/
|
|
56
|
+
// (ralph.js, ralph-claude.js, ralph-local-ai.js, ralph-exec.js,
|
|
57
|
+
// code-export.mjs, lib/local-ai-agent.js, package.json, README.md) —
|
|
40
58
|
// those files have no per-stack content, so they live once instead of being
|
|
41
59
|
// copy-pasted into every stack (that copy-pasting is exactly how they drifted out
|
|
42
60
|
// of sync before: a bugfix or default landing in one stack's copy but not another's).
|
|
@@ -44,7 +62,7 @@ const __dirname = dirname(__filename);
|
|
|
44
62
|
// (lib/prompt.md, lib/backend-prompt.md, lib/AGENT.md, checks, etc.); shared/build-kit/*
|
|
45
63
|
// is copied after it, so the shared runtime always wins over any copy a stack still ships.
|
|
46
64
|
// modeling-kit (below) is the one kit that opts out of all of this (useShared:false)
|
|
47
|
-
// — it has no cold-spawn/
|
|
65
|
+
// — it has no cold-spawn/task-queue runtime at all, so none of shared/build-kit/*
|
|
48
66
|
// applies to it; see its own templates/kit for its (much smaller) self-contained set.
|
|
49
67
|
// --- Local-AI runner selection ------------------------------------------------
|
|
50
68
|
// One runner drives every local/self-hosted backend. What differs between Ollama,
|
|
@@ -117,8 +135,8 @@ function resolveAgentPreset(opts, loop) {
|
|
|
117
135
|
console.error('❌ --agent and --exec are mutually exclusive — --agent is a shortcut for --exec.');
|
|
118
136
|
process.exit(1);
|
|
119
137
|
}
|
|
120
|
-
if (resolveLocalAiTarget(opts) !== null
|
|
121
|
-
console.error('❌ --agent is mutually exclusive with --local-ai
|
|
138
|
+
if (resolveLocalAiTarget(opts) !== null) {
|
|
139
|
+
console.error('❌ --agent is mutually exclusive with --local-ai — pick one runner.');
|
|
122
140
|
process.exit(1);
|
|
123
141
|
}
|
|
124
142
|
const cmd = AGENT_PRESETS[name]?.[loop] ?? null;
|
|
@@ -212,7 +230,7 @@ const STACKS = {
|
|
|
212
230
|
|
|
213
231
|
// Not a stack — no backend scaffold, just skills + the agent loop. Installed via
|
|
214
232
|
// `init --modeling` instead of the `init --stack <name>` picker.
|
|
215
|
-
// useShared:false — unlike build-kit, modeling-kit has no cold-spawn/
|
|
233
|
+
// useShared:false — unlike build-kit, modeling-kit has no cold-spawn/task-queue
|
|
216
234
|
// runtime to reuse from shared/build-kit/*; its only runtime mode is the CLI's
|
|
217
235
|
// own warm, direct-dispatch loop (`run --modeling`), so its kit dir just needs
|
|
218
236
|
// lib/config.js for config resolution — see stacks/modeling-kit/templates/kit.
|
|
@@ -225,34 +243,9 @@ const MODELING_KIT = {
|
|
|
225
243
|
needsBoardId: false,
|
|
226
244
|
};
|
|
227
245
|
|
|
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
246
|
// Also not a stack — installed via `init --build-kit` instead of `init --stack <name>`.
|
|
254
247
|
// useShared:true, same as any real backend stack: it reuses build-kit's cold-spawn/
|
|
255
|
-
//
|
|
248
|
+
// task-queue engine as-is (lib/ralph.js). Its templates/build-kit/CLAUDE.md,
|
|
256
249
|
// lib/{prompt,backend-prompt}.md, and templates/.claude/skills/build-*/SKILL.md are
|
|
257
250
|
// TODO-marked placeholders instead of real stack content (see stacks/blank/templates)
|
|
258
251
|
// — this is for a stack that isn't built into this CLI yet: fill in the TODOs against
|
|
@@ -267,7 +260,7 @@ const BLANK_BUILD_KIT = {
|
|
|
267
260
|
needsBoardId: true,
|
|
268
261
|
};
|
|
269
262
|
|
|
270
|
-
const KIT_DIR_NAMES = [...new Set([...Object.values(STACKS), MODELING_KIT,
|
|
263
|
+
const KIT_DIR_NAMES = [...new Set([...Object.values(STACKS), MODELING_KIT, BLANK_BUILD_KIT].map((s) => s.kitDirName))];
|
|
271
264
|
|
|
272
265
|
// What `init --demo` installs, for the messages it prints. Kept in step with
|
|
273
266
|
// shared/demo-slices/ (context.json's name, and the slice folders beside it).
|
|
@@ -475,6 +468,30 @@ function getSharedRl() {
|
|
|
475
468
|
return sharedRl;
|
|
476
469
|
}
|
|
477
470
|
|
|
471
|
+
// --force on a build kit: the credentials step of `init-config` against the project-root config the
|
|
472
|
+
// runner reads. Interactive, so under `run --all` the parent runs it for each kit one after the other —
|
|
473
|
+
// the supervised children have no stdin to ask on. `shared` (another kit's answer) is applied
|
|
474
|
+
// instead of asking, so every kit of one run ends up on the same credentials.
|
|
475
|
+
async function forceBuildKitCredentials(kitDir, cwd, opts, globalOpts, shared = null) {
|
|
476
|
+
const projectDir = dirname(kitDir);
|
|
477
|
+
const configPath = globalOpts.config
|
|
478
|
+
? resolve(projectDir, globalOpts.config)
|
|
479
|
+
: join(projectDir, '.eventmodelers', 'config.json');
|
|
480
|
+
const cfg = await configureCredentials({
|
|
481
|
+
config: loadEffectiveConfig(cwd, kitDir, globalOpts.config).config,
|
|
482
|
+
configPath,
|
|
483
|
+
targetDir: projectDir,
|
|
484
|
+
requiredFields: ['organizationId', 'boardId', 'token'],
|
|
485
|
+
overrides: shared ?? credentialOverridesFromOpts(opts),
|
|
486
|
+
print: false,
|
|
487
|
+
force: !shared,
|
|
488
|
+
});
|
|
489
|
+
ensureMcpRegistered(projectDir, cfg.baseUrl || DEFAULT_BASE_URL);
|
|
490
|
+
ensureEnvToken(projectDir, cfg.token);
|
|
491
|
+
ensureMcpEnabled(projectDir);
|
|
492
|
+
return cfg;
|
|
493
|
+
}
|
|
494
|
+
|
|
478
495
|
// Pulls one line from the shared readline's own async iterator rather than calling
|
|
479
496
|
// its `.question()` — `.question()` attaches a one-shot 'line' listener *after* the
|
|
480
497
|
// prompt is issued, but when stdin is piped (a file, `<<<`, scripted/CI input) readline
|
|
@@ -953,7 +970,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
953
970
|
// install TO, versus where we install FROM.
|
|
954
971
|
const targetDir = options.targetDir ? resolve(options.targetDir) : process.cwd();
|
|
955
972
|
// `init --git <url>` passes a resolved clone dir's templates/ here instead — every
|
|
956
|
-
// other input (STACKS, MODELING_KIT
|
|
973
|
+
// other input (STACKS, MODELING_KIT) keeps using the built-in path.
|
|
957
974
|
const templatesSource = options.templatesSource || join(__dirname, 'stacks', stackKey, 'templates');
|
|
958
975
|
const sharedBuildKit = join(__dirname, 'shared', 'build-kit');
|
|
959
976
|
// Skills with no stack-specific content (connect, learn-eventmodelers-api,
|
|
@@ -962,15 +979,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
962
979
|
// of sync with each other before (e.g. one stack's connect skill silently
|
|
963
980
|
// missing a bugfix another stack's copy had).
|
|
964
981
|
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
982
|
const isModelingKit = stackKey === MODELING_KIT.key;
|
|
973
|
-
const sharedBridgeSkills = isBridge ? join(__dirname, 'shared', 'bridge', options.target) : null;
|
|
974
983
|
|
|
975
984
|
if (!existsSync(templatesSource)) {
|
|
976
985
|
console.error('❌ Templates directory not found at:', templatesSource);
|
|
@@ -983,7 +992,6 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
983
992
|
const claudeSkillsSrc = join(templatesSource, '.claude', 'skills');
|
|
984
993
|
const installedSkills = [
|
|
985
994
|
...(existsSync(sharedSkills) ? readdirSync(sharedSkills) : []),
|
|
986
|
-
...(isBridge && existsSync(sharedBridgeSkills) ? readdirSync(sharedBridgeSkills) : []),
|
|
987
995
|
...(existsSync(claudeSkillsSrc) ? readdirSync(claudeSkillsSrc) : []),
|
|
988
996
|
];
|
|
989
997
|
let claudeExtras = [];
|
|
@@ -992,14 +1000,12 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
992
1000
|
const globalSkillsDir = join(homedir(), '.claude', 'skills');
|
|
993
1001
|
console.log('📦 Installing skills globally...');
|
|
994
1002
|
copyDirContents(sharedSkills, globalSkillsDir);
|
|
995
|
-
if (isBridge) copyDirContents(sharedBridgeSkills, globalSkillsDir);
|
|
996
1003
|
copyDirContents(claudeSkillsSrc, globalSkillsDir);
|
|
997
1004
|
} else {
|
|
998
1005
|
console.log('📦 Installing skills...');
|
|
999
1006
|
// Shared skills first, the stack's own templates last — so a build kit can override
|
|
1000
1007
|
// a shared skill (e.g. `ui-contract`) just by shipping a skill of the same name.
|
|
1001
1008
|
copyDirContents(sharedSkills, join(targetDir, '.claude', 'skills'));
|
|
1002
|
-
if (isBridge) copyDirContents(sharedBridgeSkills, join(targetDir, '.claude', 'skills'));
|
|
1003
1009
|
copyDirContents(join(templatesSource, '.claude'), join(targetDir, '.claude'));
|
|
1004
1010
|
claudeExtras = existsSync(join(templatesSource, '.claude'))
|
|
1005
1011
|
? readdirSync(join(templatesSource, '.claude')).filter((f) => f !== 'skills')
|
|
@@ -1041,7 +1047,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1041
1047
|
}
|
|
1042
1048
|
}
|
|
1043
1049
|
|
|
1044
|
-
// A modeling-kit + build-kit
|
|
1050
|
+
// A modeling-kit + build-kit combo in the same project is supported
|
|
1045
1051
|
// (they share one config.json — see ensureAgentId) but each kit's own instructions
|
|
1046
1052
|
// now live in its own kit dir (.build-kit/CLAUDE.md, .agent-modeling-kit/CLAUDE.md)
|
|
1047
1053
|
// instead of root/CLAUDE.md, precisely so a second kit's install can never clobber
|
|
@@ -1131,16 +1137,8 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1131
1137
|
}
|
|
1132
1138
|
}
|
|
1133
1139
|
|
|
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
1140
|
// Make scripts executable
|
|
1143
|
-
for (const script of ['ralph
|
|
1141
|
+
for (const script of ['ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
|
|
1144
1142
|
const p = join(kitDir, script);
|
|
1145
1143
|
if (existsSync(p)) {
|
|
1146
1144
|
try { execSync(`chmod +x "${p}"`); } catch {}
|
|
@@ -1246,12 +1244,10 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
1246
1244
|
if (options.skipEpilogue) return;
|
|
1247
1245
|
|
|
1248
1246
|
console.log('\n✅ Done! Start your agent:\n');
|
|
1249
|
-
if (
|
|
1250
|
-
console.log(' npx @eventmodelers/cli bridge\n');
|
|
1251
|
-
} else if (isModelingKit) {
|
|
1247
|
+
if (isModelingKit) {
|
|
1252
1248
|
console.log(' npx @eventmodelers/cli run --modeling\n');
|
|
1253
1249
|
} else {
|
|
1254
|
-
console.log(' npx @eventmodelers/cli run (--local-ai or --
|
|
1250
|
+
console.log(' npx @eventmodelers/cli run (--local-ai or --exec for other runners)\n');
|
|
1255
1251
|
}
|
|
1256
1252
|
console.log('Connect this project to an MCP client (Claude Code, VS Code, ...):\n');
|
|
1257
1253
|
console.log(` npx @eventmodelers/cli init-mcp\n`);
|
|
@@ -1390,7 +1386,7 @@ async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
|
|
|
1390
1386
|
if (existsSync(manifestPath)) {
|
|
1391
1387
|
writeFileSync(manifestPath, JSON.stringify({ ...readJsonSafe(manifestPath), version: CLI_VERSION }, null, 2));
|
|
1392
1388
|
}
|
|
1393
|
-
for (const script of ['ralph
|
|
1389
|
+
for (const script of ['ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
|
|
1394
1390
|
const p = join(kitDir, script);
|
|
1395
1391
|
if (existsSync(p)) {
|
|
1396
1392
|
try { execSync(`chmod +x "${p}"`); } catch {}
|
|
@@ -1750,6 +1746,22 @@ async function fetchDefaultBoardId(baseUrl, token) {
|
|
|
1750
1746
|
}
|
|
1751
1747
|
}
|
|
1752
1748
|
|
|
1749
|
+
// The board whose credentials were written most recently — the best guess for "the board I
|
|
1750
|
+
// was just driving" when nothing in this directory names one. Only offered as the default
|
|
1751
|
+
// of the interactive question, never applied silently.
|
|
1752
|
+
function lastUsedBoardId() {
|
|
1753
|
+
try {
|
|
1754
|
+
const dir = join(GLOBAL_DIR, 'boards');
|
|
1755
|
+
return readdirSync(dir)
|
|
1756
|
+
.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))
|
|
1757
|
+
.map((f) => ({ id: f.slice(0, -5), mtime: statSync(join(dir, f)).mtimeMs, creds: readJsonSafe(join(dir, f)) }))
|
|
1758
|
+
.filter((b) => b.creds.token)
|
|
1759
|
+
.sort((a, b) => b.mtime - a.mtime)[0]?.id ?? null;
|
|
1760
|
+
} catch {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1753
1765
|
// Per-run credentials for the global install. Precedence is this CLI's usual one, with the
|
|
1754
1766
|
// per-board file slotted in as the most specific *file*: explicit flags beat
|
|
1755
1767
|
// EVENTMODELERS_* env vars beat ~/.eventmodelers/boards/<board>.json beat the nearest
|
|
@@ -1789,6 +1801,7 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
|
|
|
1789
1801
|
let boardChosen = !!(explicit.boardId || process.env.EVENTMODELERS_BOARD_ID);
|
|
1790
1802
|
let pasted = null;
|
|
1791
1803
|
if (!boardChosen && interactive) {
|
|
1804
|
+
boardId = boardId || lastUsedBoardId();
|
|
1792
1805
|
const answer = await prompt(boardId ? `\n Board ID [${boardId}]: ` : '\n Board ID: ');
|
|
1793
1806
|
if (answer) {
|
|
1794
1807
|
pasted = parseCredentialsPaste(answer, ['organizationId', 'token', 'boardId']);
|
|
@@ -1916,16 +1929,12 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print,
|
|
|
1916
1929
|
process.exit(1);
|
|
1917
1930
|
}
|
|
1918
1931
|
|
|
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();
|
|
1932
|
+
// One identity per board and repo — the directory the run is started in — kept in ~/.eventmodelers/agent-ids.json, so a
|
|
1933
|
+
// standalone agent is the same agent on every start: its chat sessions, its star on the board and the prompts it holds
|
|
1934
|
+
// stay addressed to it. This is the id it keeps; whether this process may use it (another live run may already be
|
|
1935
|
+
// under it) is decided in the run action, which also lets `run --id` pin a different one. Not written into the
|
|
1936
|
+
// per-board credentials file below — the id is not a credential.
|
|
1937
|
+
config.agentId = stableGlobalAgentId({ file: join(GLOBAL_DIR, 'agent-ids.json'), boardId: config.boardId, dir: cwd });
|
|
1929
1938
|
writeBoardCredentials(stored.useGlobal
|
|
1930
1939
|
? { boardId: config.boardId, useGlobal: true, agentName: config.agentName }
|
|
1931
1940
|
: config);
|
|
@@ -1963,7 +1972,7 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1963
1972
|
}
|
|
1964
1973
|
|
|
1965
1974
|
// `run --modeling`: modeling-kit's one and only runtime mode — there is no
|
|
1966
|
-
// cold-spawn/
|
|
1975
|
+
// cold-spawn/task-queue loop for this kit (that's a build-kit concept; see the
|
|
1967
1976
|
// `run` command's build-kit-vs-modeling-kit gate above). It keeps ONE Claude
|
|
1968
1977
|
// process warm across turns via `--input-format stream-json`, subscribes to the
|
|
1969
1978
|
// org's realtime channel itself, and writes each prompt straight to that
|
|
@@ -1990,6 +1999,27 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1990
1999
|
// untargeted is handed straight back to the queue for another agent to take. It says
|
|
1991
2000
|
// nothing about the standalone lane — a self-directed turn is nobody's task, so an
|
|
1992
2001
|
// exclusive standalone agent still works the board on its own initiative.
|
|
2002
|
+
// A new instance of a loop starts by checking how big its event log has grown: at every multiple of 1000 events it
|
|
2003
|
+
// asks whether to prune it (`events --prune` does the same on demand). Only asked where someone can answer.
|
|
2004
|
+
async function offerPrune(kitDir, { interactive }) {
|
|
2005
|
+
let count;
|
|
2006
|
+
try { count = readEvents(kitDir).length; } catch { return; }
|
|
2007
|
+
if (!shouldOfferPrune(kitDir, count)) return;
|
|
2008
|
+
const prunable = prunableEvents(readEvents(kitDir)).length;
|
|
2009
|
+
if (!interactive) {
|
|
2010
|
+
console.error(`ℹ️ The event log has ${count} events (${prunable} of them prunable) — \`eventmodelers events --prune\` removes them.`);
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
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();
|
|
2014
|
+
if (answer === 'y' || answer === 'yes') {
|
|
2015
|
+
const { removed, kept } = pruneLog(kitDir);
|
|
2016
|
+
rememberPruneAnswer(kitDir, kept);
|
|
2017
|
+
console.log(`Pruned ${removed} events, ${kept} left.\n`);
|
|
2018
|
+
} else {
|
|
2019
|
+
rememberPruneAnswer(kitDir, count);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1993
2023
|
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
2024
|
// --worker: only prompts — no chat, and no board-change turns (it has nowhere to propose).
|
|
1995
2025
|
const chat = !worker;
|
|
@@ -2017,7 +2047,17 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2017
2047
|
// `run --id` overrides that for this run only — ensureAgentId is skipped rather than
|
|
2018
2048
|
// overwritten, so the project's own stable id stays on disk and the next run without the
|
|
2019
2049
|
// flag is the same agent the platform saw before.
|
|
2020
|
-
if (!overrides)
|
|
2050
|
+
if (!overrides) {
|
|
2051
|
+
if (identity.agentId) {
|
|
2052
|
+
local.agentId = identity.agentId;
|
|
2053
|
+
} else {
|
|
2054
|
+
// The project's stable id — unless a live process already runs under it (two `run --modeling` in one
|
|
2055
|
+
// project), in which case this one gets a fresh id (agent-lock.js).
|
|
2056
|
+
const { agentId, fellBack } = acquireAgentId({ lockDir: join(projectDir, '.eventmodelers', 'locks'), stableId: ensureAgentId(kitDir, 'MODELING') });
|
|
2057
|
+
local.agentId = agentId;
|
|
2058
|
+
local.agentIdFellBack = fellBack;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2021
2061
|
if (!local.token || !local.organizationId) {
|
|
2022
2062
|
console.error('❌ --modeling needs platform credentials in .eventmodelers/config.json (token + organizationId) — run `/connect` once or paste your config first.');
|
|
2023
2063
|
process.exit(1);
|
|
@@ -2026,6 +2066,17 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2026
2066
|
// resolveModelingCredentials just minted for this run, which would otherwise win back over
|
|
2027
2067
|
// an explicit --id.
|
|
2028
2068
|
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
|
|
2069
|
+
// No name given (--name, agentName in the config): the repository's, so the board shows which agent is live.
|
|
2070
|
+
// (On the global install projectDir is ~/.eventmodelers/kit; the repo is the directory the run was started in.)
|
|
2071
|
+
if (!cfg.agentName) {
|
|
2072
|
+
const base = defaultAgentName(codeDir ?? projectDir, 'MODELING');
|
|
2073
|
+
cfg.agentName = cfg.agentIdFellBack ? withInstanceSuffix(base, cfg.agentId) : base;
|
|
2074
|
+
}
|
|
2075
|
+
// Learnings are kept only on a project install: the global install's kit dir (~/.eventmodelers/kit) is shared by every
|
|
2076
|
+
// board and account, so what one board taught the agent would end up in another's AGENTS.md. The prompt journal has no
|
|
2077
|
+
// such problem — every entry is stamped with its board (`events --board`) — so every run keeps it, standalone included.
|
|
2078
|
+
const keepsRecord = !standalone && !overrides;
|
|
2079
|
+
if (keepsRecord) ensureLearningsGitRules(kitDir);
|
|
2029
2080
|
if (!cfg.boardId) {
|
|
2030
2081
|
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
2082
|
process.exit(1);
|
|
@@ -2051,7 +2102,7 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2051
2102
|
let localRunner = null;
|
|
2052
2103
|
if (localAi) {
|
|
2053
2104
|
try {
|
|
2054
|
-
localRunner = createModelingLocalAiRunner({ cfg, target: localAi, log, verbose, standalone });
|
|
2105
|
+
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
2106
|
} catch (err) {
|
|
2056
2107
|
console.error(`❌ --local-ai: ${err.message}`);
|
|
2057
2108
|
process.exit(1);
|
|
@@ -2190,6 +2241,19 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2190
2241
|
let proc = null;
|
|
2191
2242
|
let stdoutBuffer = '';
|
|
2192
2243
|
let pending = null; // one in-flight turn at a time
|
|
2244
|
+
let turnPromptId = null; // the prompt the current turn works, so the board writes it makes are journaled against it
|
|
2245
|
+
// A call is journaled when it ends, so its entry carries how long it took (see journalPromptAction). Only the calls the
|
|
2246
|
+
// journal will keep are tracked at all.
|
|
2247
|
+
const calls = createCallTracker();
|
|
2248
|
+
let localCallSeq = 0;
|
|
2249
|
+
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 }));
|
|
2250
|
+
const noteToolStart = (id, name, input) => {
|
|
2251
|
+
if (turnPromptId && shouldJournalCall(name, input, verbose)) calls.begin(id, { name, input });
|
|
2252
|
+
};
|
|
2253
|
+
const noteToolEnd = (id, outcome) => {
|
|
2254
|
+
const call = calls.end(id, outcome);
|
|
2255
|
+
if (call) journalCall(call);
|
|
2256
|
+
};
|
|
2193
2257
|
// The last message of each session this process has handed to Claude. A session the process has
|
|
2194
2258
|
// never seen is read whole; after that only what came since — each message enters the warm
|
|
2195
2259
|
// session's context once. Cleared whenever the process is (re)spawned: a fresh process has read
|
|
@@ -2248,10 +2312,16 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2248
2312
|
let msg;
|
|
2249
2313
|
try { msg = JSON.parse(line); } catch { return; }
|
|
2250
2314
|
|
|
2315
|
+
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
|
2316
|
+
for (const block of msg.message.content) if (block.type === 'tool_result') noteToolEnd(block.tool_use_id, { isError: block.is_error });
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2251
2320
|
if (msg.type === 'assistant') {
|
|
2252
2321
|
for (const block of msg.message?.content ?? []) {
|
|
2253
2322
|
if (block.type === 'text' && block.text && verbose) log(block.text);
|
|
2254
2323
|
if (block.type === 'tool_use') {
|
|
2324
|
+
noteToolStart(block.id, block.name, block.input);
|
|
2255
2325
|
if (verbose) log(`→ ${describeToolUse(block)}`);
|
|
2256
2326
|
else if (block.name === 'Skill') log(`→ Skill: ${block.input?.skill ?? ''}`);
|
|
2257
2327
|
else log(`→ ${block.name}`);
|
|
@@ -2381,10 +2451,10 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2381
2451
|
log(chat ? 'chat: ON — answering chat messages addressed to this agent' : 'chat: off (--worker)');
|
|
2382
2452
|
if (exclusive) {
|
|
2383
2453
|
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
|
|
2454
|
+
// A run keeps its stable id unless another live process is already running under it (agent-lock.js), in which
|
|
2455
|
+
// case it got a fresh one — and an id someone addressed a prompt to is not this agent. Worth saying out loud here,
|
|
2386
2456
|
// where the alternative is an agent that looks healthy and quietly works nothing.
|
|
2387
|
-
if (
|
|
2457
|
+
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
2458
|
}
|
|
2389
2459
|
// Both warm-ups are the same bet — pay the session's fixed setup cost before a turn arrives
|
|
2390
2460
|
// rather than making whoever sends the first prompt wait for it. For a local model that cost
|
|
@@ -2420,7 +2490,16 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2420
2490
|
headers: { 'x-token': cfg.token, 'Content-Type': 'application/json', ...agentHeaders(cfg) },
|
|
2421
2491
|
body: JSON.stringify({ status: 'ADDED' }),
|
|
2422
2492
|
});
|
|
2423
|
-
if (!res.ok) throw new Error(`prompts/${promptId}/status: HTTP ${res.status}`);
|
|
2493
|
+
if (!res.ok) throw Object.assign(new Error(`prompts/${promptId}/status: HTTP ${res.status}`), { status: res.status });
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// The prompts the platform shows as held (CLAIMED / IN_PROGRESS) under this agent's id — GET /prompts?claimed_by=,
|
|
2497
|
+
// which reads and claims nothing. At start an agent holds none, so these are what an earlier run left behind.
|
|
2498
|
+
async function fetchHeldPrompts(agentId) {
|
|
2499
|
+
const { url, headers } = heldPromptsRequest({ baseUrl: cfg.baseUrl, organizationId: cfg.organizationId, boardId: cfg.boardId, token: cfg.token, jwt: realtimeToken, agentId });
|
|
2500
|
+
const res = await fetch(url, { headers });
|
|
2501
|
+
if (!res.ok) throw new Error(`prompts list: HTTP ${res.status}`);
|
|
2502
|
+
return ((await res.json()).prompts ?? []).map((p) => ({ id: p.id, status: p.status }));
|
|
2424
2503
|
}
|
|
2425
2504
|
|
|
2426
2505
|
// ── Chat ───────────────────────────────────────────────────────────────────
|
|
@@ -2547,6 +2626,21 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2547
2626
|
|
|
2548
2627
|
let realtimeToken = await getRealtimeToken();
|
|
2549
2628
|
|
|
2629
|
+
// What this agent writes into its own record: events in the kit, stamped with who and where (learnings: see keepsRecord).
|
|
2630
|
+
const journalMeta = { agent_id: cfg.agentId, agent_type: 'MODELING', organization_id: cfg.organizationId, board_id: cfg.boardId };
|
|
2631
|
+
const journal = (write) => {
|
|
2632
|
+
try { write(); } catch (err) { log(`could not write the prompt journal: ${err.message}`); }
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2635
|
+
// Prompts an earlier run of this agent was holding are still CLAIMED / IN_PROGRESS — the platform never re-queues
|
|
2636
|
+
// them by itself. This run holds none yet, so whatever the platform shows as held under this agent's id goes back.
|
|
2637
|
+
{
|
|
2638
|
+
const recovered = await recoverOwnPrompts({ kitDir, meta: journalMeta, agentId: cfg.agentId, fetchHeld: fetchHeldPrompts, release: releasePrompt });
|
|
2639
|
+
if (recovered.released || recovered.settled || recovered.left) {
|
|
2640
|
+
log(`prompts from an earlier run: ${recovered.released} handed back, ${recovered.settled} already settled${recovered.left ? `, ${recovered.left} left for the next start` : ''}`);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2550
2644
|
let draining = false;
|
|
2551
2645
|
async function drain() {
|
|
2552
2646
|
if (draining) return;
|
|
@@ -2583,10 +2677,28 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2583
2677
|
continue;
|
|
2584
2678
|
}
|
|
2585
2679
|
log(`prompt received: "${p.prompt}" (board=${p.board_id ?? cfg.boardId ?? 'n/a'}, priority=${p.priority})`);
|
|
2680
|
+
journal(() => journalPromptReceived({ kitDir, prompt: p, meta: journalMeta }));
|
|
2586
2681
|
try {
|
|
2587
|
-
|
|
2682
|
+
turnPromptId = p.id;
|
|
2683
|
+
const reply = await runTurn(buildTurn(p));
|
|
2684
|
+
// The agent cannot run `learn` (locked-down Bash), so it states learnings in its reply and they are recorded here.
|
|
2685
|
+
// Best effort: a failed write never takes the prompt queue down.
|
|
2686
|
+
if (keepsRecord) {
|
|
2687
|
+
try {
|
|
2688
|
+
const n = recordTurnLearnings({ kitDir, reply, meta: journalMeta });
|
|
2689
|
+
if (n) log(`recorded ${n} learning${n === 1 ? '' : 's'} from the turn`);
|
|
2690
|
+
} catch (err) {
|
|
2691
|
+
log(`could not record the turn's learnings: ${err.message}`);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
journal(() => journalPromptFinished({ kitDir, id: p.id, outcome: 'worked', meta: journalMeta }));
|
|
2588
2695
|
} catch (err) {
|
|
2696
|
+
// Not finished: the entry stays open, so the next start asks the platform about it and hands it back
|
|
2697
|
+
// (or closes it if the agent had set DONE before the turn failed).
|
|
2589
2698
|
log(`turn failed: ${err.message}`);
|
|
2699
|
+
} finally {
|
|
2700
|
+
for (const call of calls.drain()) journalCall(call); // never ended: the turn died mid-call, so no duration
|
|
2701
|
+
turnPromptId = null;
|
|
2590
2702
|
}
|
|
2591
2703
|
}
|
|
2592
2704
|
} finally {
|
|
@@ -3203,11 +3315,40 @@ program
|
|
|
3203
3315
|
// dir: a modeling run falls back to the global install (see ensureGlobalKit) rather than
|
|
3204
3316
|
// requiring one here, and the build-kit branch reports a better-targeted error of its own
|
|
3205
3317
|
// 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']);
|
|
3318
|
+
const NO_INIT_REQUIRED = new Set(['init', 'scaffold', 'init-templates', 'init-config', 'stacks', 'status', 'config', 'uninstall', 'fetch', 'activate-context', 'set-slice-status', 'release-notes', 'run', 'listen', 'events', 'replay', 'bridge']);
|
|
3207
3319
|
|
|
3208
3320
|
// Commands that install or refresh the kit themselves — a "your kit is outdated" hint in
|
|
3209
3321
|
// front of them would only be noise.
|
|
3210
|
-
const NO_KIT_UPDATE_HINT = new Set(['init', 're-init', 'uninstall']);
|
|
3322
|
+
const NO_KIT_UPDATE_HINT = new Set(['init', 'scaffold', 'init-templates', 're-init', 'uninstall']);
|
|
3323
|
+
|
|
3324
|
+
// Retired: still recognised, so an old script or muscle memory gets an explanation instead of "unknown option".
|
|
3325
|
+
// The hidden options and the `bridge` command exist only to say so; they do nothing else.
|
|
3326
|
+
const RETIRED_BASH = [
|
|
3327
|
+
'❌ `run --bash` (the bash-only loop, ralph.sh) has been retired.',
|
|
3328
|
+
' Its task queue (tasks.json) is now an event log inside the kit, which the bash scripts cannot read or write.',
|
|
3329
|
+
' Use the default loop instead: npx @eventmodelers/cli run',
|
|
3330
|
+
' (add --exec "<command>" to drive another agent harness, or --local-ai for a local model).',
|
|
3331
|
+
];
|
|
3332
|
+
const RETIRED_BRIDGE = [
|
|
3333
|
+
'❌ The bridge kit (`init --bridge`, `bridge`) has been retired — it ran on the same tasks.json queue, which is now an event log.',
|
|
3334
|
+
' To hand slices to Spec Kitty, `fetch --spec-kitty` still works on its own, without a standing loop.',
|
|
3335
|
+
];
|
|
3336
|
+
function retire(lines) {
|
|
3337
|
+
console.error(lines.join('\n'));
|
|
3338
|
+
process.exit(1);
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
program
|
|
3342
|
+
.command('bridge', { hidden: true })
|
|
3343
|
+
.allowUnknownOption()
|
|
3344
|
+
.allowExcessArguments()
|
|
3345
|
+
.action(() => retire(RETIRED_BRIDGE));
|
|
3346
|
+
|
|
3347
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
3348
|
+
const o = actionCommand.opts();
|
|
3349
|
+
if (actionCommand.name() === 'run' && o.bash) retire(RETIRED_BASH);
|
|
3350
|
+
if (actionCommand.name() === 'init' && (o.bridge || o.target || o.hook)) retire(RETIRED_BRIDGE);
|
|
3351
|
+
});
|
|
3211
3352
|
|
|
3212
3353
|
// Startup hint when this CLI is behind npm, or an installed kit here is behind this CLI.
|
|
3213
3354
|
// Best effort and on stderr, so it never breaks piped output; EVENTMODELERS_NO_UPDATE_CHECK
|
|
@@ -3228,7 +3369,6 @@ program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
|
3228
3369
|
console.error(' Run one of these first:');
|
|
3229
3370
|
console.error(` npx @eventmodelers/cli init --stack <name> (${Object.keys(STACKS).join(', ')})`);
|
|
3230
3371
|
console.error(' npx @eventmodelers/cli init --modeling');
|
|
3231
|
-
console.error(` npx @eventmodelers/cli init --bridge --target <name> (${Object.keys(BRIDGE_TARGETS).join(', ')})`);
|
|
3232
3372
|
process.exit(1);
|
|
3233
3373
|
});
|
|
3234
3374
|
|
|
@@ -3261,13 +3401,13 @@ const AGENT_NAME_OPTION = [
|
|
|
3261
3401
|
];
|
|
3262
3402
|
|
|
3263
3403
|
// `--handover` is the project policy for how a backend build kit finishes a slice with a UI:
|
|
3264
|
-
// set, it ends STATE_VIEW / STATE_CHANGE slices at `
|
|
3404
|
+
// set, it ends STATE_VIEW / STATE_CHANGE slices at `Waiting` (the UI kit builds the rest and sets
|
|
3265
3405
|
// `Done`); unset, it ends at `Done`, exactly as before. Persisted as `handover: true` in the project's
|
|
3266
3406
|
// config.json — the `ui-contract` skill (Step 4) reads it there. Switch it later with
|
|
3267
3407
|
// `eventmodelers config --handover on|off`.
|
|
3268
3408
|
const HANDOVER_OPTION = [
|
|
3269
3409
|
'--handover',
|
|
3270
|
-
'Backend build kit: finish slices that have a UI at status `
|
|
3410
|
+
'Backend build kit: finish slices that have a UI at status `Waiting` (UI build kit takes over, then sets `Done`) instead of `Done`. Saved as `handover: true` in .eventmodelers/config.json; without it slices end at `Done`. Turn it off later with `eventmodelers config --handover off`.',
|
|
3271
3411
|
];
|
|
3272
3412
|
|
|
3273
3413
|
function identityOverridesFromOpts(opts) {
|
|
@@ -3278,15 +3418,15 @@ function identityOverridesFromOpts(opts) {
|
|
|
3278
3418
|
credentialFlags(program
|
|
3279
3419
|
.command('init')
|
|
3280
3420
|
.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 --
|
|
3421
|
+
.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
3422
|
.option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')}), or a name of your choosing when combined with --git`)
|
|
3283
3423
|
.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
3424
|
.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
|
|
3425
|
+
.option('--modeling', 'Install skills + the agent loop only — no backend scaffold. Mutually exclusive with --stack/--build-kit.')
|
|
3426
|
+
.addOption(new Option('--bridge').hideHelp())
|
|
3427
|
+
.addOption(new Option('--target <name>').hideHelp())
|
|
3428
|
+
.addOption(new Option('--hook <command>').hideHelp())
|
|
3429
|
+
.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
3430
|
.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
3431
|
.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
3432
|
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
|
|
@@ -3296,10 +3436,10 @@ credentialFlags(program
|
|
|
3296
3436
|
.action(async (opts, command) => {
|
|
3297
3437
|
const globalOpts = command.optsWithGlobals();
|
|
3298
3438
|
|
|
3299
|
-
if (opts.modeling || opts.
|
|
3300
|
-
const modeCount = [opts.modeling, opts.
|
|
3439
|
+
if (opts.modeling || opts.buildKit) {
|
|
3440
|
+
const modeCount = [opts.modeling, opts.buildKit].filter(Boolean).length;
|
|
3301
3441
|
if (opts.stack || opts.git || modeCount > 1) {
|
|
3302
|
-
console.error('❌ --stack/--git, --modeling
|
|
3442
|
+
console.error('❌ --stack/--git, --modeling and --build-kit are mutually exclusive — pick one.');
|
|
3303
3443
|
process.exit(1);
|
|
3304
3444
|
}
|
|
3305
3445
|
}
|
|
@@ -3328,38 +3468,6 @@ credentialFlags(program
|
|
|
3328
3468
|
return;
|
|
3329
3469
|
}
|
|
3330
3470
|
|
|
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
3471
|
if (opts.branch && !opts.git) {
|
|
3364
3472
|
console.error('❌ --branch only applies to --git — nothing to clone without it.');
|
|
3365
3473
|
process.exit(1);
|
|
@@ -3561,13 +3669,11 @@ program
|
|
|
3561
3669
|
.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
3670
|
.action(() => {
|
|
3563
3671
|
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.
|
|
3672
|
+
// Mirrors `run`'s buildKitDir resolution: a modeling-kit install shares KIT_DIR_NAMES but never ships
|
|
3673
|
+
// check-commit-scope.cjs, so prefer whichever installed dir isn't the modeling kit.
|
|
3567
3674
|
const installedKitDirs = findAllInstalledKitDirs(cwd);
|
|
3568
3675
|
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];
|
|
3676
|
+
const kitDir = installedKitDirs.find((d) => d !== modelingKitDir) ?? installedKitDirs[0];
|
|
3571
3677
|
|
|
3572
3678
|
const checkScript = join(kitDir, 'lib', 'check-commit-scope.cjs');
|
|
3573
3679
|
if (!existsSync(checkScript)) {
|
|
@@ -3702,10 +3808,11 @@ credentialFlags(program
|
|
|
3702
3808
|
.command('run')
|
|
3703
3809
|
.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
3810
|
.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
|
-
.
|
|
3811
|
+
.addOption(new Option('--bash').hideHelp())
|
|
3812
|
+
.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
3813
|
.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
3814
|
.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('--
|
|
3815
|
+
.option('--all', 'Run every build kit in this project at once — the one in this directory and any up to two levels below it (a scaffolded app has `.build-kit` at the root and in `web/`, so this works from the app or from the folder holding it) — each from its own directory, output prefixed with the directory, Ctrl+c stops them all. Every other flag is passed on to each run. Not interactive: the kits\' credentials must already be configured (except with --force, which asks once per kit up front, here, before the runs start). With --modeling, the modeling agent runs alongside the build kits. Not with --standalone, --worker, --global, --local or --id (one id cannot belong to several agents).')
|
|
3709
3816
|
.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
3817
|
.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
3818
|
.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).')
|
|
@@ -3717,15 +3824,58 @@ credentialFlags(program
|
|
|
3717
3824
|
.option('--local', 'Skip platform config/credential lookup entirely and run the local-only loop (no board sync, no realtime agent) — even if .eventmodelers/config.json has credentials (build-kit stacks only)')
|
|
3718
3825
|
.option('--dangerously-skip-permissions', 'Run the modeling agent\'s Claude process with --dangerously-skip-permissions, as before the lockdown: every tool call is approved — any shell command, curl to anywhere, any file the user can read or write, every MCP server the machine configures. Off by default because the agent takes chat messages and snippet clicks from any collaborator on the board; pass it only for a board whose collaborators you trust with your shell. Default Claude runner of --modeling/--standalone/--worker/--global only — not with --agent, --exec or --local-ai, which bring their own permission flags.')
|
|
3719
3826
|
.option('--browser', 'Give the locked-down modeling agent a browser: the Playwright MCP server (npx @playwright/mcp) next to the eventmodelers one, for discover-storyboard. Off by default because a browser can reach any site, which the lockdown otherwise rules out. Default Claude runner only; --dangerously-skip-permissions already loads every MCP server the machine configures.')
|
|
3720
|
-
.option('--handover', 'Backend build kit: for this run only, finish slices that have a UI at `
|
|
3721
|
-
.option('--status-trigger <status>', 'Build kit: the slice status the agent picks work up from, for this run only (e.g. Ready, Planned,
|
|
3827
|
+
.option('--handover', 'Backend build kit: for this run only, finish slices that have a UI at `Waiting` 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`.')
|
|
3828
|
+
.option('--status-trigger <status>', 'Build kit: the slice status the agent picks work up from, for this run only (e.g. Ready, Planned, Waiting). Defaults to the kit\'s own pickup status — `Planned` for backend kits, `Handover` for the UI kits. Nothing is written to disk.')
|
|
3722
3829
|
.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.
|
|
3830
|
+
.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
3831
|
.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
3832
|
.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
3833
|
.action(async (opts, command) => {
|
|
3727
3834
|
const globalOpts = command.optsWithGlobals();
|
|
3728
3835
|
const cwd = process.cwd();
|
|
3836
|
+
if (opts.all) {
|
|
3837
|
+
const clashing = [['--standalone', opts.standalone], ['--worker', opts.worker], ['--global', opts.global], ['--local', opts.local], ['--id', opts.id !== undefined]]
|
|
3838
|
+
.filter(([, set]) => set).map(([flag]) => flag);
|
|
3839
|
+
if (clashing.length) {
|
|
3840
|
+
console.error(`❌ --all runs build kits and cannot be combined with ${clashing.join(', ')}.`);
|
|
3841
|
+
process.exit(1);
|
|
3842
|
+
}
|
|
3843
|
+
const kitDirNames = [...new Set(Object.values(STACKS).map((st) => st.kitDirName))];
|
|
3844
|
+
const dirs = findBuildKitDirs(cwd, kitDirNames);
|
|
3845
|
+
if (!dirs.length && !opts.modeling) {
|
|
3846
|
+
console.error(`❌ No build kit found in this directory or up to two levels below it (looked for ${kitDirNames.join(', ')}).`);
|
|
3847
|
+
process.exit(1);
|
|
3848
|
+
}
|
|
3849
|
+
// Each kit runs as its own `run` with whatever else was typed; only --all is dropped, and
|
|
3850
|
+
// --modeling too — that one is its own run, below, not something a build kit takes.
|
|
3851
|
+
// --force is the one flag that asks questions, and the children have no terminal to ask on
|
|
3852
|
+
// (the supervisor pipes their stdin, so the first prompt dies with ERR_USE_AFTER_CLOSE). The
|
|
3853
|
+
// credentials step runs here once per kit instead, then --force is not forwarded.
|
|
3854
|
+
if (opts.force) {
|
|
3855
|
+
if (opts.nonInteractive || globalOpts.print) {
|
|
3856
|
+
console.error('❌ --force asks for credentials again, which --non-interactive/--print rule out — pick one.');
|
|
3857
|
+
process.exit(1);
|
|
3858
|
+
}
|
|
3859
|
+
// Asked once, for the kit in this directory (the first one found when there is none), and
|
|
3860
|
+
// handed to the others — the modeling agent reads this directory's config.
|
|
3861
|
+
const rootDir = dirs.find((dir) => dirname(dir) === cwd) ?? dirs[0];
|
|
3862
|
+
console.log(`\n🔑 Credentials for ${relative(cwd, rootDir) || '.'}, shared by all`);
|
|
3863
|
+
const { organizationId, boardId, token, baseUrl } = await forceBuildKitCredentials(rootDir, cwd, opts, globalOpts);
|
|
3864
|
+
for (const dir of dirs.filter((d) => d !== rootDir)) {
|
|
3865
|
+
await forceBuildKitCredentials(dir, cwd, opts, globalOpts, { organizationId, boardId, token, baseUrl });
|
|
3866
|
+
}
|
|
3867
|
+
if (sharedRl) sharedRl.close();
|
|
3868
|
+
console.log('');
|
|
3869
|
+
}
|
|
3870
|
+
const forwarded = process.argv.slice(2).filter((a) => a !== '--all' && a !== '--modeling' && !(opts.force && a === '--force'));
|
|
3871
|
+
const cliJs = join(__dirname, 'cli.js');
|
|
3872
|
+
const specs = dirs.map((dir) => ({ name: relative(cwd, dir) || '.', file: process.execPath, args: [cliJs, ...forwarded], cwd: dir }));
|
|
3873
|
+
// --modeling adds the modeling agent next to the build kits: one process, from this directory
|
|
3874
|
+
// (its modeling kit, or the global install when there is none), like a plain `run --modeling`.
|
|
3875
|
+
if (opts.modeling) specs.push({ name: 'modeling', file: process.execPath, args: [cliJs, ...forwarded, '--modeling'], cwd });
|
|
3876
|
+
console.log(`▶ Running ${dirs.length} build kit(s)${opts.modeling ? ' + the modeling agent' : ''}: ${specs.map((sp) => sp.name).join(', ')} (Ctrl+c stops all)\n`);
|
|
3877
|
+
process.exit(await supervise(specs));
|
|
3878
|
+
}
|
|
3729
3879
|
// Validated up front, before any runner is selected: a cost guard given as garbage
|
|
3730
3880
|
// should fail on the spot, not once the loop is already up — and a cap passed where
|
|
3731
3881
|
// nothing will read it is worth saying out loud rather than ignoring silently.
|
|
@@ -3779,11 +3929,6 @@ credentialFlags(program
|
|
|
3779
3929
|
process.exit(1);
|
|
3780
3930
|
}
|
|
3781
3931
|
}
|
|
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
3932
|
// Both kit dirs can be installed side by side (e.g. running a build-kit and a
|
|
3788
3933
|
// modeling-kit agent from the same project). findInstalledKitDir only ever
|
|
3789
3934
|
// returns its first fixed-order match, which would silently prefer one stack
|
|
@@ -3791,16 +3936,11 @@ credentialFlags(program
|
|
|
3791
3936
|
// we resolve each stack's dir independently instead of relying on that order.
|
|
3792
3937
|
const installedKitDirs = findAllInstalledKitDirs(cwd);
|
|
3793
3938
|
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;
|
|
3939
|
+
const buildKitDir = installedKitDirs.find((d) => d !== modelingKitDir) ?? null;
|
|
3800
3940
|
|
|
3801
3941
|
// No overlap between the two stacks' runtimes: modeling-kit only ever runs the
|
|
3802
3942
|
// warm, direct-dispatch loop (--modeling); build-kit only ever runs the
|
|
3803
|
-
// cold-spawn/
|
|
3943
|
+
// cold-spawn/task-queue loop (default, or --local-ai/--exec). Neither falls back
|
|
3804
3944
|
// to the other's mechanism, so each side is gated explicitly below rather than
|
|
3805
3945
|
// just being left to fail on a missing file.
|
|
3806
3946
|
// --standalone implies --modeling: it already refused every other runner, so there
|
|
@@ -3809,14 +3949,8 @@ credentialFlags(program
|
|
|
3809
3949
|
// no meaning for a build kit, which is scaffolded per project by definition.
|
|
3810
3950
|
if (opts.modeling || opts.standalone || opts.worker || opts.global) {
|
|
3811
3951
|
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
|
-
}
|
|
3952
|
+
// --local-ai and --exec name what does a turn, not a queue, and the modeling loop has its own
|
|
3953
|
+
// runner for each (lib/modeling-local-ai.js, lib/modeling-exec.js).
|
|
3820
3954
|
const modelingLocalAi = resolveLocalAiTarget(opts);
|
|
3821
3955
|
if (modelingLocalAi !== null && opts.exec) {
|
|
3822
3956
|
console.error('❌ --local-ai and --exec are mutually exclusive — pick one runner.');
|
|
@@ -3865,6 +3999,13 @@ credentialFlags(program
|
|
|
3865
3999
|
...Object.fromEntries(Object.entries(credentialOverridesFromOpts(opts)).filter(([, v]) => v)),
|
|
3866
4000
|
};
|
|
3867
4001
|
const config = await resolveModelingCredentials(cwd, flags, globalOpts.config, globalOpts.print, !!opts.nonInteractive, !!opts.force);
|
|
4002
|
+
// The stable id of this board and repo — unless `--id` pins another, or a live process already runs under it,
|
|
4003
|
+
// in which case this run gets a fresh one (agent-lock.js).
|
|
4004
|
+
if (!identity.agentId) {
|
|
4005
|
+
const { agentId, fellBack } = acquireAgentId({ lockDir: join(GLOBAL_DIR, 'locks'), stableId: config.agentId });
|
|
4006
|
+
config.agentId = agentId;
|
|
4007
|
+
config.agentIdFellBack = fellBack;
|
|
4008
|
+
}
|
|
3868
4009
|
projectDir = await ensureGlobalKit(config.baseUrl);
|
|
3869
4010
|
kitDir = join(projectDir, MODELING_KIT.kitDirName);
|
|
3870
4011
|
overrides = config;
|
|
@@ -3896,6 +4037,7 @@ credentialFlags(program
|
|
|
3896
4037
|
// right after (dynamic imports, config reads) can eat the event-loop tick this write
|
|
3897
4038
|
// needed to drain, so a piped watcher sees the ping arrive after runModeling's own
|
|
3898
4039
|
// [modeling] log lines instead of before them.
|
|
4040
|
+
await offerPrune(kitDir, { interactive: !globalOpts.print && !opts.nonInteractive && !!process.stdin.isTTY });
|
|
3899
4041
|
const shown = relative(cwd, kitDir);
|
|
3900
4042
|
const runnerLabel = modelingLocalAi ? 'local model' : opts.exec ? 'one agent process per turn' : 'warm Claude process';
|
|
3901
4043
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (${runnerLabel}) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
@@ -3910,9 +4052,7 @@ credentialFlags(program
|
|
|
3910
4052
|
|
|
3911
4053
|
if (!buildKitDir) {
|
|
3912
4054
|
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.`);
|
|
4055
|
+
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
4056
|
} else {
|
|
3917
4057
|
console.error(`❌ No kit installed in ${cwd} — run \`eventmodelers install\` first.`);
|
|
3918
4058
|
console.error(' (A modeling agent needs no install at all: eventmodelers run --standalone --board-id <uuid>)');
|
|
@@ -3922,17 +4062,19 @@ credentialFlags(program
|
|
|
3922
4062
|
const kitDir = buildKitDir;
|
|
3923
4063
|
|
|
3924
4064
|
const localAiTarget = resolveLocalAiTarget(opts);
|
|
3925
|
-
const pickedCount = [
|
|
4065
|
+
const pickedCount = [localAiTarget !== null, !!opts.exec].filter(Boolean).length;
|
|
3926
4066
|
if (pickedCount > 1) {
|
|
3927
|
-
console.error('❌ --
|
|
4067
|
+
console.error('❌ --local-ai and --exec are mutually exclusive — pick one runner.');
|
|
3928
4068
|
process.exit(1);
|
|
3929
4069
|
}
|
|
3930
4070
|
|
|
4071
|
+
await offerPrune(kitDir, { interactive: !globalOpts.print && !opts.nonInteractive && !!process.stdin.isTTY });
|
|
4072
|
+
|
|
3931
4073
|
// The actual agent loop lives in the scaffolded kit dir, not in this package — this
|
|
3932
4074
|
// is just a thin dispatcher so users don't have to remember the kit-dir name or which
|
|
3933
4075
|
// runner file to invoke. Users (and the agent itself, via AGENT.md) may customize these
|
|
3934
4076
|
// files freely; `run` always executes whatever is currently on disk.
|
|
3935
|
-
const runner = opts.
|
|
4077
|
+
const runner = opts.exec ? 'ralph-exec.js' : localAiTarget !== null ? 'ralph-local-ai.js' : 'ralph-claude.js';
|
|
3936
4078
|
const runnerPath = join(kitDir, runner);
|
|
3937
4079
|
if (!existsSync(runnerPath)) {
|
|
3938
4080
|
console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
|
|
@@ -3950,22 +4092,7 @@ credentialFlags(program
|
|
|
3950
4092
|
console.error('❌ --force asks for credentials, which --local (no platform config) never uses — pick one.');
|
|
3951
4093
|
process.exit(1);
|
|
3952
4094
|
}
|
|
3953
|
-
|
|
3954
|
-
const configPath = globalOpts.config
|
|
3955
|
-
? resolve(projectDir, globalOpts.config)
|
|
3956
|
-
: join(projectDir, '.eventmodelers', 'config.json');
|
|
3957
|
-
const cfg = await configureCredentials({
|
|
3958
|
-
config: loadEffectiveConfig(cwd, kitDir, globalOpts.config).config,
|
|
3959
|
-
configPath,
|
|
3960
|
-
targetDir: projectDir,
|
|
3961
|
-
requiredFields: ['organizationId', 'boardId', 'token'],
|
|
3962
|
-
overrides: credentialOverridesFromOpts(opts),
|
|
3963
|
-
print: false,
|
|
3964
|
-
force: true,
|
|
3965
|
-
});
|
|
3966
|
-
ensureMcpRegistered(projectDir, cfg.baseUrl || DEFAULT_BASE_URL);
|
|
3967
|
-
ensureEnvToken(projectDir, cfg.token);
|
|
3968
|
-
ensureMcpEnabled(projectDir);
|
|
4095
|
+
await forceBuildKitCredentials(kitDir, cwd, opts, globalOpts);
|
|
3969
4096
|
}
|
|
3970
4097
|
|
|
3971
4098
|
// ralph-claude.js hands every turn to a `claude` that only discovers MCP servers from
|
|
@@ -3983,77 +4110,16 @@ credentialFlags(program
|
|
|
3983
4110
|
console.log(`▶ Starting ${relative(cwd, runnerPath)}...\n`);
|
|
3984
4111
|
const cmd = runner.endsWith('.sh') ? `"${runnerPath}"` : `node "${runnerPath}"`;
|
|
3985
4112
|
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.
|
|
4113
|
+
// Only ralph-claude.js reads RALPH_VERBOSE — the local-AI executor has its own separate output
|
|
4114
|
+
// path with no stream-json parsing to gate. RALPH_LOCAL is read by ralph.js's startRalph to force
|
|
4115
|
+
// the local-only branch even when .eventmodelers/config.json has valid credentials.
|
|
4116
|
+
// RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read there too.
|
|
3992
4117
|
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
4118
|
} catch (err) {
|
|
3994
4119
|
process.exit(err.status || 1);
|
|
3995
4120
|
}
|
|
3996
4121
|
});
|
|
3997
4122
|
|
|
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
4123
|
program
|
|
4058
4124
|
.command('listen')
|
|
4059
4125
|
.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/')
|
|
@@ -4152,6 +4218,162 @@ program
|
|
|
4152
4218
|
}
|
|
4153
4219
|
});
|
|
4154
4220
|
|
|
4221
|
+
program
|
|
4222
|
+
.command('scaffold <target> [args...]')
|
|
4223
|
+
.description('`scaffold add-user <email> <password> [dir]` adds a sign-in user to a scaffolded app (run in its folder, or pass dir). Otherwise: generate a runnable app skeleton from the board — sign-in, navigation with one entry per timeline, a page shell per screen, 501 for every unbuilt endpoint, hot reload — and install the build kits of the chosen --type into it, ready for `run` to add slices. <target> is an empty or new directory, or a skeleton made earlier (which is refreshed from the current model).')
|
|
4224
|
+
.option('--type <name>', `Kind of app to scaffold (${scaffoldTypeNames().join(', ')})`, 'supabase')
|
|
4225
|
+
.option('--no-install', 'Stop after generating the skeleton: no npm install, checks or commit')
|
|
4226
|
+
.option('--no-check', 'Install dependencies but skip the type-check, tests and web build/lint')
|
|
4227
|
+
.option('--templates <dir>', 'Folder of *.tmpl files that replace the built-in skeleton template of the same path (or add files). Default: the target\'s own templates/ folder when it exists, so customisations survive a refresh')
|
|
4228
|
+
.option('--port <name=value>', "Pin one of the type's ports instead of picking a free one, e.g. --port api=3200 (repeatable; the names are listed in the error when one is unknown). A refresh keeps its ports unless pinned here", (v, all) => [...all, v], [])
|
|
4229
|
+
.action(async (target, args, opts, command) => {
|
|
4230
|
+
if (target === 'add-user') {
|
|
4231
|
+
const [email, password, dir] = args;
|
|
4232
|
+
if (!email || !password) {
|
|
4233
|
+
console.error('❌ usage: eventmodelers scaffold add-user <email> <password> [dir]');
|
|
4234
|
+
process.exit(1);
|
|
4235
|
+
}
|
|
4236
|
+
const addUser = SCAFFOLD_TYPES[opts.type]?.addUser;
|
|
4237
|
+
if (!addUser) {
|
|
4238
|
+
console.log(`ℹ️ The "${opts.type}" scaffold has no users — nothing to do.`);
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
const appDir = resolve(process.cwd(), dir ?? '.');
|
|
4242
|
+
if (!existsSync(join(appDir, addUser.requires))) {
|
|
4243
|
+
console.error(`❌ ${appDir} is not a scaffolded "${opts.type}" app (no ${addUser.requires}) — run this in its folder or pass the folder as the last argument.`);
|
|
4244
|
+
process.exit(1);
|
|
4245
|
+
}
|
|
4246
|
+
const [file, ...fileArgs] = addUser.command(email, password);
|
|
4247
|
+
try {
|
|
4248
|
+
execFileSync(file, fileArgs, { cwd: appDir, stdio: 'inherit' });
|
|
4249
|
+
} catch {
|
|
4250
|
+
process.exit(1);
|
|
4251
|
+
}
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
if (args.length) {
|
|
4255
|
+
console.error(`❌ Unexpected argument(s): ${args.join(' ')}`);
|
|
4256
|
+
process.exit(1);
|
|
4257
|
+
}
|
|
4258
|
+
const type = SCAFFOLD_TYPES[opts.type];
|
|
4259
|
+
if (!type) {
|
|
4260
|
+
console.error(`❌ Unknown --type "${opts.type}". Available: ${scaffoldTypeNames().join(', ')}`);
|
|
4261
|
+
process.exit(1);
|
|
4262
|
+
}
|
|
4263
|
+
const missing = missingPrerequisites(type);
|
|
4264
|
+
if (missing.length) {
|
|
4265
|
+
console.error(`❌ Missing on this machine:\n${missing.map((m) => ` - ${m}`).join('\n')}`);
|
|
4266
|
+
process.exit(1);
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
const cwd = process.cwd();
|
|
4270
|
+
const globalOpts = command.optsWithGlobals();
|
|
4271
|
+
let cfg = loadEffectiveConfig(cwd, findInstalledKitDir(cwd), globalOpts.config).config;
|
|
4272
|
+
const explicit = Object.fromEntries(Object.entries(credentialOverridesFromOpts(opts)).filter(([, v]) => v));
|
|
4273
|
+
cfg = { ...cfg, ...explicit };
|
|
4274
|
+
const configPath = globalOpts.config ? resolve(cwd, globalOpts.config) : join(cwd, '.eventmodelers', 'config.json');
|
|
4275
|
+
const requiredFields = ['organizationId', 'boardId', 'token'];
|
|
4276
|
+
const promptForCredentials = async () => {
|
|
4277
|
+
cfg = await configureCredentials({ config: cfg, configPath, targetDir: cwd, requiredFields, boardIdOptional: false, overrides: {}, print: globalOpts.print });
|
|
4278
|
+
if (requiredFields.some((f) => !cfg[f])) {
|
|
4279
|
+
console.error('❌ Still missing token/organizationId/boardId — re-run `eventmodelers scaffold` once configured.');
|
|
4280
|
+
process.exit(1);
|
|
4281
|
+
}
|
|
4282
|
+
};
|
|
4283
|
+
if (requiredFields.some((f) => !cfg[f])) await promptForCredentials();
|
|
4284
|
+
|
|
4285
|
+
const readModel = async () => {
|
|
4286
|
+
const baseUrl = cfg.baseUrl || 'https://api.eventmodelers.ai';
|
|
4287
|
+
const headers = { 'x-token': cfg.token, 'x-user-id': 'cli-scaffold', 'content-type': 'application/json' };
|
|
4288
|
+
const call = async (url, what, init) => {
|
|
4289
|
+
const res = await fetch(url, { headers, ...init }).catch((err) => {
|
|
4290
|
+
console.error(`❌ Request failed (${what}): ${err.message}`);
|
|
4291
|
+
process.exit(1);
|
|
4292
|
+
});
|
|
4293
|
+
if (res.status === 401 || res.status === 403) throw new FetchAuthError(res.status, `${what}: ${res.status === 401 ? 'invalid or expired token' : "token's organization does not match this board"}`);
|
|
4294
|
+
if (!res.ok) {
|
|
4295
|
+
console.error(`❌ ${what}: HTTP ${res.status}`);
|
|
4296
|
+
process.exit(1);
|
|
4297
|
+
}
|
|
4298
|
+
return res.json();
|
|
4299
|
+
};
|
|
4300
|
+
const boardBase = `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}`;
|
|
4301
|
+
const boards = await call(`${baseUrl}/api/boards`, 'boards').catch((err) => { if (err instanceof FetchAuthError) throw err; return []; });
|
|
4302
|
+
const board = (Array.isArray(boards) ? boards : boards.boards ?? []).find((b) => b.id === cfg.boardId);
|
|
4303
|
+
return buildModel({
|
|
4304
|
+
appTitle: board?.name ?? board?.title,
|
|
4305
|
+
api: {
|
|
4306
|
+
get: (path) => call(`${boardBase}${path}`, path.split('?')[0]),
|
|
4307
|
+
post: (path, body) => call(`${boardBase}${path}`, path, { method: 'POST', body: JSON.stringify(body) }),
|
|
4308
|
+
},
|
|
4309
|
+
});
|
|
4310
|
+
};
|
|
4311
|
+
|
|
4312
|
+
console.log(`▶ Reading the model of board ${cfg.boardId}...`);
|
|
4313
|
+
let model;
|
|
4314
|
+
try {
|
|
4315
|
+
model = await readModel();
|
|
4316
|
+
} catch (err) {
|
|
4317
|
+
if (!(err instanceof FetchAuthError)) throw err;
|
|
4318
|
+
console.error(`❌ ${err.message}`);
|
|
4319
|
+
if (err.status === 404) delete cfg.boardId;
|
|
4320
|
+
else delete cfg.token;
|
|
4321
|
+
await promptForCredentials();
|
|
4322
|
+
model = await readModel();
|
|
4323
|
+
}
|
|
4324
|
+
console.log(` ${model.timelines.length} timeline(s), ${model.slices.length} slice(s), ${model.screens.length} screen(s), css: ${model.css}`);
|
|
4325
|
+
|
|
4326
|
+
try {
|
|
4327
|
+
const { root, ports } = await runScaffold({
|
|
4328
|
+
typeName: opts.type,
|
|
4329
|
+
target,
|
|
4330
|
+
cfg,
|
|
4331
|
+
model,
|
|
4332
|
+
cliPath: join(__dirname, 'cli.js'),
|
|
4333
|
+
install: opts.install,
|
|
4334
|
+
check: opts.check,
|
|
4335
|
+
templates: opts.templates ? resolve(cwd, opts.templates) : undefined,
|
|
4336
|
+
ports: Object.fromEntries(opts.port.map((kv) => kv.split(/=(.*)/s).slice(0, 2))),
|
|
4337
|
+
});
|
|
4338
|
+
console.log(`\n✅ Skeleton ready in ${root}`);
|
|
4339
|
+
console.log(` cd ${relative(cwd, root) || '.'} && npm run up # ${Object.entries(ports).map(([n, v]) => `${n} :${v}`).join(', ')}`);
|
|
4340
|
+
console.log(' npx @eventmodelers/cli scaffold add-user <email> <password> # a user to sign in with (no sign-up screen)');
|
|
4341
|
+
console.log(' npx @eventmodelers/cli run --all # then build: every kit in the project, in this terminal');
|
|
4342
|
+
} catch (err) {
|
|
4343
|
+
console.error(`❌ ${err.message}`);
|
|
4344
|
+
process.exit(1);
|
|
4345
|
+
}
|
|
4346
|
+
});
|
|
4347
|
+
|
|
4348
|
+
program
|
|
4349
|
+
.command('init-templates [paths...]')
|
|
4350
|
+
.description('Copy the built-in scaffold templates into ./templates/ (or --dir) so you can edit them: `scaffold` uses a template found there and falls back to the built-in one for everything else. Name paths (a file like web/src/app/Layout.tsx, or a folder like web/src/app) to copy only those — a copied template no longer follows the CLI\'s own updates, so take only what you mean to change. Existing files are kept unless --force.')
|
|
4351
|
+
.option('--type <name>', `Scaffold type whose templates to copy (${scaffoldTypeNames().join(', ')})`, 'supabase')
|
|
4352
|
+
.option('--dir <dir>', 'Where to copy them', 'templates')
|
|
4353
|
+
.option('--list', 'Only list the built-in template paths')
|
|
4354
|
+
.option('-f, --force', 'Overwrite templates already in the folder')
|
|
4355
|
+
.action((paths, opts) => {
|
|
4356
|
+
const type = SCAFFOLD_TYPES[opts.type];
|
|
4357
|
+
if (!type) {
|
|
4358
|
+
console.error(`❌ Unknown --type "${opts.type}". Available: ${scaffoldTypeNames().join(', ')}`);
|
|
4359
|
+
process.exit(1);
|
|
4360
|
+
}
|
|
4361
|
+
if (opts.list) {
|
|
4362
|
+
for (const rel of builtInTemplates(type).keys()) console.log(rel);
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
try {
|
|
4366
|
+
const dest = resolve(process.cwd(), opts.dir);
|
|
4367
|
+
const { copied, kept } = initTemplates(type, dest, { only: paths, force: opts.force });
|
|
4368
|
+
for (const rel of copied) console.log(` ✓ ${relative(process.cwd(), join(dest, `${rel}.tmpl`))}`);
|
|
4369
|
+
if (kept.length) console.log(` ${kept.length} already there, kept (use --force to overwrite)`);
|
|
4370
|
+
console.log(`\n✅ ${copied.length} template(s) copied. Edit them, then run \`eventmodelers scaffold <dir>\` — it uses ${relative(process.cwd(), dest) || '.'}/ when that is the target's templates/ folder, or pass --templates.`);
|
|
4371
|
+
} catch (err) {
|
|
4372
|
+
console.error(`❌ ${err.message}`);
|
|
4373
|
+
process.exit(1);
|
|
4374
|
+
}
|
|
4375
|
+
});
|
|
4376
|
+
|
|
4155
4377
|
program
|
|
4156
4378
|
.command('activate-context')
|
|
4157
4379
|
.description('Choose which fetched context is active — writes .slices/current_context.json, which `run` works through first; once it has no planned slices left, `run` moves on to the next context that does')
|
|
@@ -4209,7 +4431,7 @@ const SLICE_STATUSES = [
|
|
|
4209
4431
|
{ label: '🚫 Blocked', value: 'Blocked' },
|
|
4210
4432
|
{ label: '🧭 Ready', value: 'Ready' },
|
|
4211
4433
|
{ label: '📅 Planned', value: 'Planned' },
|
|
4212
|
-
{ label: '🤝
|
|
4434
|
+
{ label: '🤝 Waiting', value: 'Waiting' },
|
|
4213
4435
|
{ label: 'ℹ️ Informational', value: 'Informational' },
|
|
4214
4436
|
];
|
|
4215
4437
|
|
|
@@ -4360,6 +4582,313 @@ program
|
|
|
4360
4582
|
console.log(`✅ Pushed status change to the board (node ${slice.id}).`);
|
|
4361
4583
|
});
|
|
4362
4584
|
|
|
4585
|
+
// The kit a command that records events works against: a build kit if one is installed, else the modeling
|
|
4586
|
+
// kit. The build kit wins when both are installed — the modeling agent never runs these commands itself
|
|
4587
|
+
// (its Bash is an allowlist); it states its learnings in its reply and `runModeling` records them.
|
|
4588
|
+
function requireBuildKitDir(cwd) {
|
|
4589
|
+
const installed = findAllInstalledKitDirs(cwd);
|
|
4590
|
+
const kitDir = installed.find((d) => !d.endsWith(MODELING_KIT.kitDirName)) ?? installed.find((d) => d.endsWith(MODELING_KIT.kitDirName));
|
|
4591
|
+
if (!kitDir) {
|
|
4592
|
+
console.error('❌ This command needs a build or modeling kit (npx @eventmodelers/cli init --stack <name> or init --modeling).');
|
|
4593
|
+
process.exit(1);
|
|
4594
|
+
}
|
|
4595
|
+
return kitDir;
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
// What the build agent's events are stamped with: the loop exports the agent id into its environment.
|
|
4599
|
+
function agentEventMeta(cwd, kitDir, command) {
|
|
4600
|
+
const { config: cfg } = loadEffectiveConfig(cwd, kitDir, command.optsWithGlobals().config);
|
|
4601
|
+
return {
|
|
4602
|
+
agent_id: process.env.RALPH_AGENT_ID || process.env.EVENTMODELERS_AGENT_ID,
|
|
4603
|
+
agent_type: 'BUILD', // `learn` and `slice-status` are the build agent's commands
|
|
4604
|
+
organization_id: cfg.organizationId,
|
|
4605
|
+
board_id: cfg.boardId,
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
program
|
|
4610
|
+
.command('slice-status <status>')
|
|
4611
|
+
.description("For a build agent: record the outcome of the slice the run loop is building (Done, Waiting, 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.")
|
|
4612
|
+
.option('--slice <idOrTitle>', 'The slice, when the loop has claimed more than one (default: the one it is building)')
|
|
4613
|
+
.action((status, opts, command) => {
|
|
4614
|
+
const cwd = process.cwd();
|
|
4615
|
+
// `Handover` is the old name of `Waiting`; older skills and prompts still say it
|
|
4616
|
+
const wanted = status.toLowerCase() === 'handover' ? 'waiting' : status.toLowerCase();
|
|
4617
|
+
const canonical = SLICE_STATUSES.find((s) => s.value.toLowerCase() === wanted)?.value;
|
|
4618
|
+
if (!canonical) {
|
|
4619
|
+
console.error(`❌ "${status}" is not a slice status. Valid: ${SLICE_STATUSES.map((s) => s.value).join(', ')}`);
|
|
4620
|
+
process.exit(1);
|
|
4621
|
+
}
|
|
4622
|
+
const kitDir = requireBuildKitDir(cwd);
|
|
4623
|
+
const meta = agentEventMeta(cwd, kitDir, command);
|
|
4624
|
+
try {
|
|
4625
|
+
const result = recordAgentStatus({ kitDir, status: canonical, sliceId: opts.slice, meta });
|
|
4626
|
+
console.log(`✅ "${result.title}": recorded ${result.status} — the run loop pushes it to the board when the build ends.`);
|
|
4627
|
+
} catch (err) {
|
|
4628
|
+
console.error(`❌ ${err.message}`);
|
|
4629
|
+
process.exit(1);
|
|
4630
|
+
}
|
|
4631
|
+
});
|
|
4632
|
+
|
|
4633
|
+
program
|
|
4634
|
+
.command('ui-baseline <action>')
|
|
4635
|
+
.alias('build-baseline')
|
|
4636
|
+
.description("For a build agent: decide whether a slice's UI needs work. `check` compares the board's hashes of the slice's screen and of every element of the slice and its codeGen.uiPrompts with what was recorded after the last UI build and prints NO_BASELINE, UNCHANGED or CHANGED (UNCHANGED = skip all UI work). `record` stores the current state once the UI is built — commit the resulting ui-baseline.json with the slice.")
|
|
4637
|
+
.option('--slice <idOrTitle>', 'The slice (default: the one the run loop has claimed)')
|
|
4638
|
+
.option('--scope <scope>', 'ui (default): screen, elements and UI prompts · backend: elements only, for a kit that builds no UI', 'ui')
|
|
4639
|
+
.action(async (action, opts, command) => {
|
|
4640
|
+
if (!['ui', 'backend'].includes(opts.scope)) {
|
|
4641
|
+
console.error('❌ --scope must be `ui` or `backend`.');
|
|
4642
|
+
process.exit(1);
|
|
4643
|
+
}
|
|
4644
|
+
if (!['check', 'record'].includes(action)) {
|
|
4645
|
+
console.error('❌ Action must be `check` or `record`.');
|
|
4646
|
+
process.exit(1);
|
|
4647
|
+
}
|
|
4648
|
+
const cwd = process.cwd();
|
|
4649
|
+
const kitDir = requireBuildKitDir(cwd);
|
|
4650
|
+
let wanted = opts.slice;
|
|
4651
|
+
if (!wanted) {
|
|
4652
|
+
const claimed = Object.entries(fold(readEvents(kitDir)).slices).filter(([, s]) => s.claim);
|
|
4653
|
+
if (claimed.length !== 1) {
|
|
4654
|
+
console.error(`❌ ${claimed.length ? `${claimed.length} slices are claimed` : 'No slice is claimed by the run loop'} — name the slice with --slice.`);
|
|
4655
|
+
process.exit(1);
|
|
4656
|
+
}
|
|
4657
|
+
wanted = claimed[0][0];
|
|
4658
|
+
}
|
|
4659
|
+
const found = findSliceDir(kitDir, wanted);
|
|
4660
|
+
if (!found) {
|
|
4661
|
+
console.error(`❌ Slice "${wanted}" not found under ${relative(cwd, join(kitDir, '.slices'))}.`);
|
|
4662
|
+
process.exit(1);
|
|
4663
|
+
}
|
|
4664
|
+
const globalOpts = command.optsWithGlobals();
|
|
4665
|
+
const cfg = loadEffectiveConfig(cwd, kitDir, globalOpts.config).config;
|
|
4666
|
+
const fetchNodes = async (ids) => {
|
|
4667
|
+
if (['organizationId', 'boardId', 'token'].some((f) => !cfg[f])) throw new Error('token/organizationId/boardId are not configured (.eventmodelers/config.json)');
|
|
4668
|
+
const res = await fetch(`${cfg.baseUrl || DEFAULT_BASE_URL}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes/query`, {
|
|
4669
|
+
method: 'POST',
|
|
4670
|
+
headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-user-id': 'cli-ui-baseline', ...agentHeaders(cfg) },
|
|
4671
|
+
body: JSON.stringify({ ids }),
|
|
4672
|
+
});
|
|
4673
|
+
if (!res.ok) throw new Error(`board returned HTTP ${res.status}`);
|
|
4674
|
+
return res.json();
|
|
4675
|
+
};
|
|
4676
|
+
let fp;
|
|
4677
|
+
try {
|
|
4678
|
+
fp = await fingerprint({ slice: found.slice, fetchNodes, scope: opts.scope });
|
|
4679
|
+
} catch (err) {
|
|
4680
|
+
// Cannot tell — the safe answer is the full check, never a skip.
|
|
4681
|
+
console.log('UNKNOWN');
|
|
4682
|
+
console.log(`Could not read the screen from the board (${err.message}) — do the full screen diff.`);
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
if (action === 'record') {
|
|
4686
|
+
writeBaseline(found.path, fp, opts.scope);
|
|
4687
|
+
console.log(`✅ Recorded the ${opts.scope === 'backend' ? 'build' : 'UI'} baseline for "${found.slice.title}" (${relative(cwd, join(found.path, 'ui-baseline.json'))}) — commit it with the slice.`);
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
const result = compare(readBaseline(found.path), fp, opts.scope);
|
|
4691
|
+
console.log(result.verdict);
|
|
4692
|
+
if (result.verdict === 'UNCHANGED') {
|
|
4693
|
+
console.log('Screen, elements and UI prompts are as of the last UI build — skip all UI work.');
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
if (result.verdict === 'NO_BASELINE') console.log('No UI build recorded for this slice yet — do one full screen diff, apply every UI prompt, then `ui-baseline record`.');
|
|
4697
|
+
const nodeLine = (label, list) => (list.length ? `\n ${label}: ${list.map((n) => `${n.title} (${n.kind === 'screen' ? 'screen' : n.kind.replace(/s$/, '')})`).join(', ')}` : '');
|
|
4698
|
+
const nodeDetail = nodeLine('changed', result.changedNodes) + nodeLine('added', result.addedNodes) + nodeLine('removed', result.removedNodes);
|
|
4699
|
+
if (nodeDetail) console.log(`Since the last recorded build:${nodeDetail}\nWork on exactly these; leave what matches alone.`);
|
|
4700
|
+
if (result.screenChanged && result.verdict === 'CHANGED') console.log('SCREEN_CHANGED: re-fetch the screen node, diff the components against it and adjust the UI.');
|
|
4701
|
+
if (result.elementsChanged && result.verdict === 'CHANGED') console.log('ELEMENTS_CHANGED: an element of the slice (command, event, read model, processor, table, actor, aggregate, specification, ...) changed on the board — diff slice.json against the code field by field.');
|
|
4702
|
+
if (result.promptsChanged) console.log(`PROMPTS_CHANGED: apply ${result.newPrompts.length ? 'these new/changed UI prompts' : 'the UI prompts (they were edited or removed — re-read them)'}:${result.newPrompts.map((p) => `\n - ${p.trim()}`).join('')}`);
|
|
4703
|
+
console.log('When the UI is done, run `ui-baseline record`.');
|
|
4704
|
+
});
|
|
4705
|
+
|
|
4706
|
+
program
|
|
4707
|
+
.command('learn [text]')
|
|
4708
|
+
.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`).")
|
|
4709
|
+
.option('--topic <topic>', 'Group the learning under this heading in AGENTS.md')
|
|
4710
|
+
.option('--tag <tag>', 'Label the learning (repeatable)', (tag, tags) => [...tags, tag], [])
|
|
4711
|
+
.option('--scope <scope>', 'system: true of the modeled domain · repo: about the codebase and its tooling (default repo)')
|
|
4712
|
+
.option('--slice <slice>', 'The slice it was learned on')
|
|
4713
|
+
.option('--retract <id>', 'Withdraw the learning with this id (or a unique prefix of it) instead of recording one')
|
|
4714
|
+
.option('--merge <ids>', 'Replace these learnings (comma-separated ids) by one with the given text — the curator\'s tool')
|
|
4715
|
+
.option('--reason <reason>', 'Why — recorded with a retraction or a merge')
|
|
4716
|
+
.action((text, opts, command) => {
|
|
4717
|
+
const cwd = process.cwd();
|
|
4718
|
+
const kitDir = requireBuildKitDir(cwd);
|
|
4719
|
+
const meta = agentEventMeta(cwd, kitDir, command);
|
|
4720
|
+
try {
|
|
4721
|
+
if (opts.retract !== undefined) {
|
|
4722
|
+
const gone = retractLearning({ kitDir, id: opts.retract, reason: opts.reason, meta });
|
|
4723
|
+
console.log(`✅ Retracted: ${gone.text}`);
|
|
4724
|
+
return;
|
|
4725
|
+
}
|
|
4726
|
+
if (opts.merge !== undefined) {
|
|
4727
|
+
const ids = opts.merge.split(',').map((id) => id.trim()).filter(Boolean);
|
|
4728
|
+
const merged = mergeLearnings({ kitDir, ids, text, topic: opts.topic, scope: opts.scope, tags: opts.tag, reason: opts.reason, meta });
|
|
4729
|
+
console.log(`✅ Merged ${ids.length} learnings into one (${merged.id.slice(0, 8)}).`);
|
|
4730
|
+
return;
|
|
4731
|
+
}
|
|
4732
|
+
if (text === undefined) throw new Error('give the learning as text, or --retract <id>, or --merge <ids> <text>');
|
|
4733
|
+
// The curator may only merge and retract — an LLM asked to tidy must not add facts of its own.
|
|
4734
|
+
if (process.env.EVENTMODELERS_CURATING === '1') throw new Error('a curation turn only merges and retracts — new learnings are not recorded during it');
|
|
4735
|
+
const result = recordLearning({ kitDir, text, topic: opts.topic, tags: opts.tag, scope: opts.scope ?? 'repo', slice: opts.slice, meta });
|
|
4736
|
+
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.`);
|
|
4737
|
+
} catch (err) {
|
|
4738
|
+
console.error(`❌ ${err.message}`);
|
|
4739
|
+
process.exit(1);
|
|
4740
|
+
}
|
|
4741
|
+
});
|
|
4742
|
+
|
|
4743
|
+
program
|
|
4744
|
+
.command('events')
|
|
4745
|
+
.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.")
|
|
4746
|
+
.option('--kit <dir>', "Read this kit directory's log (default: the build kit of the current project, else its modeling kit)")
|
|
4747
|
+
.option('--global', 'Read the log of the global install (~/.eventmodelers/kit) — what runs started without a project install wrote')
|
|
4748
|
+
.option('--board <id>', "Only events stamped with this board (an id prefix) — the global install's log mixes every board")
|
|
4749
|
+
.option('--type <types>', 'Only these event types, comma-separated (e.g. claimed,settled)')
|
|
4750
|
+
.option('--slice <slice>', 'Only events of the slice with this id (prefix) or a title containing this text')
|
|
4751
|
+
.option('--tag <tag>', 'Only events carrying this tag')
|
|
4752
|
+
.option('--last <n>', 'Only the last n events (after the other filters)')
|
|
4753
|
+
.option('--json', 'Print the raw events, one JSON object per line')
|
|
4754
|
+
.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). Also shows the baseline events: the kit\'s first look at each slice (a fresh kit records every slice already on the board), which are hidden by default. `--type board-observed` and `--json` show them too.')
|
|
4755
|
+
.option('--state', 'Print the state the log folds to (slices, queue, open prompts, learnings) instead of the events')
|
|
4756
|
+
.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.')
|
|
4757
|
+
.option('--follow', 'Keep running and print events as they are appended')
|
|
4758
|
+
.option('--watch', 'Same as --follow')
|
|
4759
|
+
.action(async (opts) => {
|
|
4760
|
+
const cwd = process.cwd();
|
|
4761
|
+
if (opts.global && opts.kit) {
|
|
4762
|
+
console.error('❌ --global and --kit are alternatives — pick one.');
|
|
4763
|
+
process.exit(1);
|
|
4764
|
+
}
|
|
4765
|
+
const globalLog = join(GLOBAL_KIT_DIR, MODELING_KIT.kitDirName);
|
|
4766
|
+
const kitDir = opts.global ? globalLog : opts.kit ? resolve(cwd, opts.kit) : requireBuildKitDir(cwd);
|
|
4767
|
+
if (opts.prune) {
|
|
4768
|
+
try {
|
|
4769
|
+
const { removed, kept } = pruneLog(kitDir);
|
|
4770
|
+
rememberPruneAnswer(kitDir, kept);
|
|
4771
|
+
console.log(removed ? `Pruned ${removed} events, ${kept} left.` : `Nothing to prune — ${kept} events, all of them carry state.`);
|
|
4772
|
+
} catch (err) {
|
|
4773
|
+
console.error(`❌ ${err.message}`);
|
|
4774
|
+
process.exit(1);
|
|
4775
|
+
}
|
|
4776
|
+
return;
|
|
4777
|
+
}
|
|
4778
|
+
let last;
|
|
4779
|
+
if (opts.last !== undefined) {
|
|
4780
|
+
last = Number(opts.last);
|
|
4781
|
+
if (!Number.isInteger(last) || last < 1) {
|
|
4782
|
+
console.error('❌ --last needs a positive number.');
|
|
4783
|
+
process.exit(1);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
const filter = { types: opts.type?.split(',').map((t) => t.trim()).filter(Boolean), slice: opts.slice, tag: opts.tag, board: opts.board };
|
|
4787
|
+
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4788
|
+
const paint = (code, text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
4789
|
+
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');
|
|
4790
|
+
const print = (events, titles) => {
|
|
4791
|
+
for (const e of events) {
|
|
4792
|
+
if (opts.json) { console.log(JSON.stringify(e)); continue; }
|
|
4793
|
+
const [time, ...rest] = formatEvent(e, { titles }).split(' ');
|
|
4794
|
+
const type = rest.shift();
|
|
4795
|
+
console.log([paint('2', time), paint(family(e.type), type), ...rest].join(' '));
|
|
4796
|
+
if (opts.verbose) { const payload = formatPayload(e); if (payload) console.log(paint('2', payload)); }
|
|
4797
|
+
}
|
|
4798
|
+
};
|
|
4799
|
+
|
|
4800
|
+
const follow = opts.follow || opts.watch;
|
|
4801
|
+
const events = readEvents(kitDir);
|
|
4802
|
+
// The baseline (each slice's first board-observed) is noise unless asked for: --verbose, the raw --json, or the type itself.
|
|
4803
|
+
const hideBaseline = !opts.verbose && !opts.json && !filter.types?.includes('board-observed');
|
|
4804
|
+
const titles = sliceTitles(events);
|
|
4805
|
+
if (opts.state) {
|
|
4806
|
+
if (follow) console.error('ℹ️ --follow/--watch does not apply to --state.');
|
|
4807
|
+
console.log(events.length ? summarize(fold(events), titles) : 'No events yet in this kit.');
|
|
4808
|
+
return;
|
|
4809
|
+
}
|
|
4810
|
+
if (!events.length && !follow) {
|
|
4811
|
+
console.log(`No events yet in ${opts.global ? '~/.eventmodelers/kit' : relative(cwd, kitDir) || kitDir}.`);
|
|
4812
|
+
// Runs started without a project install keep their log in the global kit — say so when it has one.
|
|
4813
|
+
if (!opts.global && !opts.kit && readEvents(globalLog).length) console.log('The global install has a log of its own: `eventmodelers events --global`.');
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4816
|
+
print(filterEvents(events, { ...filter, last, omit: hideBaseline ? baselineIds(events) : undefined }, titles), titles);
|
|
4817
|
+
if (!follow) return;
|
|
4818
|
+
|
|
4819
|
+
// Follow: what has been printed is remembered by event id, and the log is read again twice a second.
|
|
4820
|
+
const seen = new Set(events.map((e) => e.id));
|
|
4821
|
+
setInterval(() => {
|
|
4822
|
+
const all = readEvents(kitDir);
|
|
4823
|
+
const fresh = all.filter((e) => !seen.has(e.id));
|
|
4824
|
+
for (const e of fresh) seen.add(e.id);
|
|
4825
|
+
if (fresh.length) print(filterEvents(fresh, { ...filter, omit: hideBaseline ? baselineIds(all) : undefined }, sliceTitles(all)), sliceTitles(all));
|
|
4826
|
+
}, 500);
|
|
4827
|
+
});
|
|
4828
|
+
|
|
4829
|
+
program
|
|
4830
|
+
.command('replay <id>')
|
|
4831
|
+
.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.")
|
|
4832
|
+
.option('--kit <dir>', "Read this kit directory's log (default: the build kit of the current project, else its modeling kit)")
|
|
4833
|
+
.option('--global', 'Read the log of the global install (~/.eventmodelers/kit)')
|
|
4834
|
+
.option('--dry-run', 'Show the request that would be sent and stop — nothing is submitted')
|
|
4835
|
+
.option('--token <uuid>', 'API token (overrides config file / env var)')
|
|
4836
|
+
.option('--organization-id <uuid>', 'Organization ID (overrides config file / env var)')
|
|
4837
|
+
.option('--base-url <url>', 'Platform base URL (overrides config file / env var)')
|
|
4838
|
+
.action(async (id, opts) => {
|
|
4839
|
+
const cwd = process.cwd();
|
|
4840
|
+
if (opts.global && opts.kit) {
|
|
4841
|
+
console.error('❌ --global and --kit are alternatives — pick one.');
|
|
4842
|
+
process.exit(1);
|
|
4843
|
+
}
|
|
4844
|
+
const kitDir = opts.global ? join(GLOBAL_KIT_DIR, MODELING_KIT.kitDirName) : opts.kit ? resolve(cwd, opts.kit) : requireBuildKitDir(cwd);
|
|
4845
|
+
try {
|
|
4846
|
+
const event = findPromptEvent(readEvents(kitDir), id);
|
|
4847
|
+
const boardId = event.prompt.board_id ?? event.meta?.board_id;
|
|
4848
|
+
// The credentials of the board the prompt was for: flags and env vars over the board's own file over the account's.
|
|
4849
|
+
const stored = boardId ? readJsonSafe(boardCredentialsPath(boardId)) : {};
|
|
4850
|
+
const explicit = Object.fromEntries(Object.entries(credentialOverridesFromOpts(opts)).filter(([, v]) => v));
|
|
4851
|
+
const cfg = applyEnvOverrides({ ...loadEffectiveConfig(cwd, null, null).config, ...(stored.useGlobal || stored.useBoard ? {} : stored), ...explicit });
|
|
4852
|
+
cfg.baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
|
|
4853
|
+
if (!cfg.token || !cfg.organizationId) throw new Error('no credentials for this board — pass --token/--organization-id, or run the agent once for it');
|
|
4854
|
+
|
|
4855
|
+
const tokenRes = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/realtime-token`, { headers: { 'x-token': cfg.token } });
|
|
4856
|
+
if (!tokenRes.ok) throw new Error(`realtime-token: HTTP ${tokenRes.status}`);
|
|
4857
|
+
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 } } });
|
|
4858
|
+
if (opts.dryRun) {
|
|
4859
|
+
console.log(`POST ${url}\n${JSON.stringify(body, null, 2)}`);
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4862
|
+
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
4863
|
+
if (!res.ok) throw new Error(`prompts: HTTP ${res.status} ${(await res.text()).slice(0, 200)}`);
|
|
4864
|
+
const created = await res.json();
|
|
4865
|
+
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}"`);
|
|
4866
|
+
} catch (err) {
|
|
4867
|
+
console.error(`❌ ${err.message}`);
|
|
4868
|
+
process.exit(1);
|
|
4869
|
+
}
|
|
4870
|
+
});
|
|
4871
|
+
|
|
4872
|
+
program
|
|
4873
|
+
.command('learnings')
|
|
4874
|
+
.description('Print the recorded learnings — what .build-kit/AGENTS.md says. With --ids, one line each with the id to retract it by.')
|
|
4875
|
+
.option('--ids', 'One line per learning: id, topic, text')
|
|
4876
|
+
.action((opts, command) => {
|
|
4877
|
+
const cwd = process.cwd();
|
|
4878
|
+
const kitDir = requireBuildKitDir(cwd);
|
|
4879
|
+
syncLearnings({ kitDir, meta: agentEventMeta(cwd, kitDir, command) });
|
|
4880
|
+
const all = listLearnings(kitDir);
|
|
4881
|
+
if (!all.length) {
|
|
4882
|
+
console.log('No learnings recorded yet.');
|
|
4883
|
+
return;
|
|
4884
|
+
}
|
|
4885
|
+
if (!opts.ids) {
|
|
4886
|
+
console.log(readFileSync(join(kitDir, 'AGENTS.md'), 'utf-8').trimEnd());
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
for (const l of all) console.log(` ${l.id.slice(0, 8)} [${l.topic ?? 'General'}] ${l.text}`);
|
|
4890
|
+
});
|
|
4891
|
+
|
|
4363
4892
|
program
|
|
4364
4893
|
.command('stacks')
|
|
4365
4894
|
.description('List available stacks (for `init --stack`)')
|
|
@@ -4455,16 +4984,14 @@ program
|
|
|
4455
4984
|
.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
4985
|
.option('--build-kit', `Remove ${STACKS.node.kitDirName}/ (the backend-stack kit dir)`)
|
|
4457
4986
|
.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
4987
|
.action((opts) => {
|
|
4460
4988
|
const cwd = process.cwd();
|
|
4461
4989
|
let targets;
|
|
4462
4990
|
|
|
4463
|
-
if (opts.buildKit || opts.modelingKit
|
|
4991
|
+
if (opts.buildKit || opts.modelingKit) {
|
|
4464
4992
|
targets = [];
|
|
4465
4993
|
if (opts.buildKit) targets.push(join(cwd, STACKS.node.kitDirName));
|
|
4466
4994
|
if (opts.modelingKit) targets.push(join(cwd, MODELING_KIT.kitDirName));
|
|
4467
|
-
if (opts.bridgeKit) targets.push(join(cwd, BRIDGE_KIT.kitDirName));
|
|
4468
4995
|
targets = targets.filter((p) => existsSync(p));
|
|
4469
4996
|
if (!targets.length) {
|
|
4470
4997
|
console.log('ℹ️ Nothing to remove for the requested option(s).');
|
|
@@ -4477,7 +5004,7 @@ program
|
|
|
4477
5004
|
return;
|
|
4478
5005
|
}
|
|
4479
5006
|
if (targets.length > 1) {
|
|
4480
|
-
console.log('⚠️ Multiple kit dirs found — re-run with --build-kit
|
|
5007
|
+
console.log('⚠️ Multiple kit dirs found — re-run with --build-kit and/or --modeling-kit to pick which to remove.');
|
|
4481
5008
|
targets.forEach((t) => console.log(` ${t}`));
|
|
4482
5009
|
return;
|
|
4483
5010
|
}
|