@eventmodelers/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -0
- package/cli.js +676 -0
- package/package.json +35 -0
- package/shared/build-kit/code-export.mjs +560 -0
- package/shared/build-kit/lib/ollama-agent.js +147 -0
- package/shared/build-kit/package.json +11 -0
- package/shared/build-kit/ralph-ollama.js +39 -0
- package/shared/build-kit/realtime-agent.js +18 -0
- package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +407 -0
- package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +114 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/SKILL.md +496 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/axon-workflow-api.md +279 -0
- package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/feature-flag-patterns.md +114 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +396 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/axon-test-fixture-patterns.md +162 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +56 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +115 -0
- package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +282 -0
- package/stacks/axon/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +611 -0
- package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +141 -0
- package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
- package/stacks/axon/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/axon/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +158 -0
- package/stacks/axon/templates/build-kit/lib/prompt.md +126 -0
- package/stacks/axon/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/axon/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/axon/templates/build-kit/ralph.sh +98 -0
- package/stacks/axon/templates/root/.env.example +5 -0
- package/stacks/axon/templates/root/CLAUDE.md +60 -0
- package/stacks/axon/templates/root/README.md +44 -0
- package/stacks/axon/templates/root/docker-compose.yml +64 -0
- package/stacks/axon/templates/root/mvnw +259 -0
- package/stacks/axon/templates/root/mvnw.cmd +149 -0
- package/stacks/axon/templates/root/pom.xml +125 -0
- package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/QuickstartApplication.java +128 -0
- package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/config/WebConfig.java +155 -0
- package/stacks/axon/templates/root/src/main/resources/application.properties +2 -0
- package/stacks/axon/templates/root/src/main/resources/static/bg-body.png +0 -0
- package/stacks/axon/templates/root/src/main/resources/static/bg-header.png +0 -0
- package/stacks/axon/templates/root/src/main/resources/static/index.html +596 -0
- package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +251 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +99 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +115 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +163 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +234 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +120 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +166 -0
- package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +169 -0
- package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
- package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +141 -0
- package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +58 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +129 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +124 -0
- package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +302 -0
- package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +37 -0
- package/stacks/cratis-csharp/templates/build-kit/ralph.sh +98 -0
- package/stacks/cratis-csharp/templates/root/.frontend/index.css +29 -0
- package/stacks/cratis-csharp/templates/root/.frontend/index.html +17 -0
- package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +18 -0
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +42 -0
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +11 -0
- package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +56 -0
- package/stacks/cratis-csharp/templates/root/App.tsx +23 -0
- package/stacks/cratis-csharp/templates/root/CLAUDE.md +62 -0
- package/stacks/cratis-csharp/templates/root/CratisApp.csproj +25 -0
- package/stacks/cratis-csharp/templates/root/CratisApp.sln +18 -0
- package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +3 -0
- package/stacks/cratis-csharp/templates/root/Home.tsx +102 -0
- package/stacks/cratis-csharp/templates/root/Program.cs +26 -0
- package/stacks/cratis-csharp/templates/root/README.md +192 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +47 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +11 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +12 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +17 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +51 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +18 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +27 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +22 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +3 -0
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +1 -0
- package/stacks/cratis-csharp/templates/root/appsettings.Development.json +9 -0
- package/stacks/cratis-csharp/templates/root/appsettings.json +26 -0
- package/stacks/cratis-csharp/templates/root/docker-compose.yml +23 -0
- package/stacks/cratis-csharp/templates/root/package.json +33 -0
- package/stacks/cratis-csharp/templates/root/tsconfig.json +3 -0
- package/stacks/modeling-kit/templates/.claude/settings.local.json +13 -0
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +215 -0
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +176 -0
- package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +182 -0
- package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +383 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +465 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +587 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/facilitating-event-modeling-workshops.md +626 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +542 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +497 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +783 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +604 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +699 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +674 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/SKILL.md +231 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/domain-patterns.md +150 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/patterns.md +329 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/snapshotting.md +204 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +352 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/project-planning-with-event-modeling.md +341 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +153 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +136 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +78 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/patterns.md +85 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +655 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +479 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +234 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +353 -0
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +200 -0
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +94 -0
- package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +441 -0
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +290 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +212 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +123 -0
- package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +353 -0
- package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +134 -0
- package/stacks/modeling-kit/templates/kit/lib/agent.sh +20 -0
- package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +147 -0
- package/stacks/modeling-kit/templates/kit/lib/ralph.js +286 -0
- package/stacks/modeling-kit/templates/kit/package.json +12 -0
- package/stacks/modeling-kit/templates/kit/ralph-claude.js +50 -0
- package/stacks/modeling-kit/templates/kit/ralph-ollama.js +38 -0
- package/stacks/modeling-kit/templates/kit/ralph.sh +61 -0
- package/stacks/modeling-kit/templates/kit/realtime-agent.js +18 -0
- package/stacks/modeling-kit/templates/root/CLAUDE.md +56 -0
- package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +266 -0
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +344 -0
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +412 -0
- package/stacks/node/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/node/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
- package/stacks/node/templates/.claude/skills/load-slice/SKILL.md +143 -0
- package/stacks/node/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/node/templates/build-kit/README.md +86 -0
- package/stacks/node/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/node/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +152 -0
- package/stacks/node/templates/build-kit/lib/prompt.md +122 -0
- package/stacks/node/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/node/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/node/templates/build-kit/ralph.sh +98 -0
- package/stacks/node/templates/root/.env.example +15 -0
- package/stacks/node/templates/root/CLAUDE.md +60 -0
- package/stacks/node/templates/root/docker-compose.yml +15 -0
- package/stacks/node/templates/root/flyway.conf +17 -0
- package/stacks/node/templates/root/migrations/V1__schema.sql.example +12 -0
- package/stacks/node/templates/root/package.json +50 -0
- package/stacks/node/templates/root/server.ts +130 -0
- package/stacks/node/templates/root/setup-env.sh +53 -0
- package/stacks/node/templates/root/src/common/assertions.ts +6 -0
- package/stacks/node/templates/root/src/common/db.ts +32 -0
- package/stacks/node/templates/root/src/common/loadPostgresEventstore.ts +23 -0
- package/stacks/node/templates/root/src/common/parseEndpoint.ts +51 -0
- package/stacks/node/templates/root/src/common/processorDlq.ts +28 -0
- package/stacks/node/templates/root/src/common/realtimeBroadcast.ts +12 -0
- package/stacks/node/templates/root/src/common/replay.ts +16 -0
- package/stacks/node/templates/root/src/common/routes.ts +19 -0
- package/stacks/node/templates/root/src/common/testHelpers.ts +44 -0
- package/stacks/node/templates/root/src/swagger.ts +34 -0
- package/stacks/node/templates/root/src/util/assertions.ts +6 -0
- package/stacks/node/templates/root/src/util/hash.ts +9 -0
- package/stacks/node/templates/root/src/util/sanitize.ts +23 -0
- package/stacks/node/templates/root/tsconfig.json +32 -0
- package/stacks/node/templates/root/vercel.json +8 -0
- package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +266 -0
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +344 -0
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +390 -0
- package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +303 -0
- package/stacks/supabase/templates/.claude/skills/connect/SKILL.md +178 -0
- package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
- package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +143 -0
- package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
- package/stacks/supabase/templates/build-kit/README.md +86 -0
- package/stacks/supabase/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/supabase/templates/build-kit/lib/agent.sh +20 -0
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +152 -0
- package/stacks/supabase/templates/build-kit/lib/prompt.md +123 -0
- package/stacks/supabase/templates/build-kit/lib/ralph.js +369 -0
- package/stacks/supabase/templates/build-kit/ralph-claude.js +44 -0
- package/stacks/supabase/templates/build-kit/ralph.sh +98 -0
- package/stacks/supabase/templates/root/.env.example +22 -0
- package/stacks/supabase/templates/root/CLAUDE.md +61 -0
- package/stacks/supabase/templates/root/flyway.conf +17 -0
- package/stacks/supabase/templates/root/package.json +52 -0
- package/stacks/supabase/templates/root/server.ts +177 -0
- package/stacks/supabase/templates/root/setup-env.sh +53 -0
- package/stacks/supabase/templates/root/src/common/assertions.ts +6 -0
- package/stacks/supabase/templates/root/src/common/db.ts +32 -0
- package/stacks/supabase/templates/root/src/common/loadPostgresEventstore.ts +23 -0
- package/stacks/supabase/templates/root/src/common/parseEndpoint.ts +51 -0
- package/stacks/supabase/templates/root/src/common/processorDlq.ts +28 -0
- package/stacks/supabase/templates/root/src/common/realtimeBroadcast.ts +19 -0
- package/stacks/supabase/templates/root/src/common/replay.ts +16 -0
- package/stacks/supabase/templates/root/src/common/routes.ts +23 -0
- package/stacks/supabase/templates/root/src/common/testHelpers.ts +54 -0
- package/stacks/supabase/templates/root/src/supabase/README.md +141 -0
- package/stacks/supabase/templates/root/src/supabase/api.ts +56 -0
- package/stacks/supabase/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
- package/stacks/supabase/templates/root/src/supabase/requireSysUser.ts +40 -0
- package/stacks/supabase/templates/root/src/supabase/requireUser.ts +72 -0
- package/stacks/supabase/templates/root/src/swagger.ts +34 -0
- package/stacks/supabase/templates/root/src/util/assertions.ts +6 -0
- package/stacks/supabase/templates/root/src/util/hash.ts +9 -0
- package/stacks/supabase/templates/root/src/util/sanitize.ts +23 -0
- package/stacks/supabase/templates/root/supabase/config.toml +295 -0
- package/stacks/supabase/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
- package/stacks/supabase/templates/root/supabase/seed.sql +1 -0
- package/stacks/supabase/templates/root/tsconfig.json +32 -0
- package/stacks/supabase/templates/root/vercel.json +8 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// Common runtime for the ralph loop + realtime agent.
|
|
2
|
+
// Not meant to be run directly — use ralph-claude.js or ralph-ollama.js.
|
|
3
|
+
//
|
|
4
|
+
// startRalph({ kitDir, projectDir, onTask, onPlannedSlice })
|
|
5
|
+
// onTask(prompt) — called when tasks.json has entries
|
|
6
|
+
// onPlannedSlice(prompt) — called when .slices/ has a "Planned" entry (omit to skip)
|
|
7
|
+
|
|
8
|
+
import { createClient } from '@supabase/supabase-js';
|
|
9
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
|
|
13
|
+
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
class HttpError extends Error {
|
|
16
|
+
constructor(status, body) {
|
|
17
|
+
super(`HTTP ${status}: ${body}`);
|
|
18
|
+
this.status = status;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchJSON(url, options) {
|
|
23
|
+
const res = await fetch(url, options);
|
|
24
|
+
if (!res.ok) throw new HttpError(res.status, await res.text());
|
|
25
|
+
return res.json();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function retryOn401(label, fn, maxRetries = 3) {
|
|
29
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err instanceof HttpError && err.status === 401) {
|
|
34
|
+
if (attempt < maxRetries) {
|
|
35
|
+
console.warn(`[agent] ${label} — 401, retrying (${attempt}/${maxRetries})...`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
console.error(`[agent] ${label} — 401 after ${maxRetries} retries, shutting down`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
// Config is resolved by walking from the kit dir up through every ancestor
|
|
49
|
+
// directory's .eventmodelers/config.json, merging fields as we go — a value
|
|
50
|
+
// set by a closer (more specific) directory always wins over a farther one.
|
|
51
|
+
// The walk stops as soon as the merged config has full connection credentials
|
|
52
|
+
// (see hasCredentials); anthropicBaseUrl/model are picked up opportunistically
|
|
53
|
+
// along the way but never force the walk to continue further up.
|
|
54
|
+
function* configCandidates(kitDir) {
|
|
55
|
+
yield join(kitDir, '.eventmodelers', 'config.json');
|
|
56
|
+
let dir = dirname(kitDir);
|
|
57
|
+
while (true) {
|
|
58
|
+
yield join(dir, '.eventmodelers', 'config.json');
|
|
59
|
+
const parent = dirname(dir);
|
|
60
|
+
if (parent === dir) return;
|
|
61
|
+
dir = parent;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadLocalConfig(kitDir) {
|
|
66
|
+
const merged = {};
|
|
67
|
+
const sources = [];
|
|
68
|
+
|
|
69
|
+
for (const candidate of configCandidates(kitDir)) {
|
|
70
|
+
if (!existsSync(candidate)) continue;
|
|
71
|
+
let cfg;
|
|
72
|
+
try {
|
|
73
|
+
cfg = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
74
|
+
} catch {
|
|
75
|
+
console.warn(`[ralph] Skipping invalid config at ${candidate}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const [key, value] of Object.entries(cfg)) {
|
|
79
|
+
if (merged[key] === undefined) merged[key] = value;
|
|
80
|
+
}
|
|
81
|
+
sources.push(candidate);
|
|
82
|
+
if (hasCredentials(merged)) break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.env.BASE_URL) merged.baseUrl = process.env.BASE_URL;
|
|
86
|
+
|
|
87
|
+
if (sources.length > 1) {
|
|
88
|
+
console.log(`[ralph] Merged config from: ${sources.join(', ')}`);
|
|
89
|
+
} else if (sources.length === 1 && sources[0] !== join(kitDir, '.eventmodelers', 'config.json')) {
|
|
90
|
+
console.log(`[ralph] Using credentials from ${sources[0]}`);
|
|
91
|
+
} else if (sources.length === 0) {
|
|
92
|
+
console.warn(`[ralph] Note: no .eventmodelers/config.json found — platform sync disabled.`);
|
|
93
|
+
console.warn(` To enable board sync, follow: https://app.eventmodelers.ai/documentation#build-axon`);
|
|
94
|
+
console.warn(` Code generation from local slice definitions will still run.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return merged;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hasCredentials(cfg) {
|
|
101
|
+
return !!(cfg.token && cfg.organizationId && cfg.boardId && cfg.baseUrl);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function fetchPlatformConfig(local) {
|
|
105
|
+
const remote = await fetchJSON(`${local.baseUrl}/api/config`, {
|
|
106
|
+
headers: { 'x-token': local.token },
|
|
107
|
+
});
|
|
108
|
+
return { ...local, ...remote };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Realtime agent ────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async function getRealtimeToken(cfg) {
|
|
114
|
+
const { token } = await fetchJSON(
|
|
115
|
+
`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/realtime-token`,
|
|
116
|
+
{ headers: { 'x-token': cfg.token } },
|
|
117
|
+
);
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function slugify(str) {
|
|
122
|
+
return str.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function fetchAndPersistSlices(cfg, kitDir) {
|
|
126
|
+
const url = `${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata/slices`;
|
|
127
|
+
const { slices } = await fetchJSON(url, {
|
|
128
|
+
headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId },
|
|
129
|
+
});
|
|
130
|
+
const slicesDir = join(kitDir, '.slices');
|
|
131
|
+
mkdirSync(slicesDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
// Group by context slug
|
|
134
|
+
const contexts = {};
|
|
135
|
+
for (const slice of slices) {
|
|
136
|
+
const contextSlug = slice.contextName ? slugify(slice.contextName) : 'default';
|
|
137
|
+
if (!contexts[contextSlug]) contexts[contextSlug] = { name: slice.contextName || 'default', slices: [] };
|
|
138
|
+
contexts[contextSlug].slices.push(slice);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// current_context.json is STICKY. We work within ONE context at a time and must
|
|
142
|
+
// not auto-jump to another context just because it happens to have planned work.
|
|
143
|
+
// Keep the existing context if it still exists; only seed it when absent or stale.
|
|
144
|
+
const ctxPath = join(slicesDir, 'current_context.json');
|
|
145
|
+
let activeCtx = null;
|
|
146
|
+
if (existsSync(ctxPath)) {
|
|
147
|
+
try { activeCtx = JSON.parse(readFileSync(ctxPath, 'utf-8')).name; } catch {}
|
|
148
|
+
}
|
|
149
|
+
if (!activeCtx || !contexts[activeCtx]) {
|
|
150
|
+
// First run (or the current context disappeared): seed with a context that
|
|
151
|
+
// has planned work, else the first one. This is the ONLY place we choose it.
|
|
152
|
+
const plannedCtx = Object.keys(contexts).find(c => contexts[c].slices.some(s => (s.status || '').toLowerCase() === 'planned'));
|
|
153
|
+
activeCtx = plannedCtx || Object.keys(contexts)[0] || 'default';
|
|
154
|
+
writeFileSync(ctxPath, JSON.stringify({ name: activeCtx }, null, 2), 'utf-8');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Write per-context index.json and per-slice slice.json
|
|
158
|
+
for (const [contextSlug, { slices: ctxSlices }] of Object.entries(contexts)) {
|
|
159
|
+
const contextDir = join(slicesDir, contextSlug);
|
|
160
|
+
mkdirSync(contextDir, { recursive: true });
|
|
161
|
+
|
|
162
|
+
const indexSlices = ctxSlices.map((s, i) => {
|
|
163
|
+
const folder = (s.title ?? s.id).replaceAll(' ', '').toLowerCase();
|
|
164
|
+
return {
|
|
165
|
+
id: s.id,
|
|
166
|
+
slice: s.title,
|
|
167
|
+
index: i,
|
|
168
|
+
contextName: s.contextName || contextSlug,
|
|
169
|
+
contextSlug,
|
|
170
|
+
folder,
|
|
171
|
+
status: s.status,
|
|
172
|
+
definition: { id: s.id, title: s.title, status: s.status },
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
writeFileSync(join(contextDir, 'index.json'), JSON.stringify({ slices: indexSlices }, null, 2), 'utf-8');
|
|
176
|
+
|
|
177
|
+
for (const slice of ctxSlices) {
|
|
178
|
+
const folder = (slice.title ?? slice.id).replaceAll(' ', '').toLowerCase();
|
|
179
|
+
const sliceDir = join(contextDir, folder);
|
|
180
|
+
mkdirSync(sliceDir, { recursive: true });
|
|
181
|
+
writeFileSync(join(sliceDir, 'slice.json'), JSON.stringify(slice, null, 2), 'utf-8');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(`[agent] Persisted ${slices.length} slice(s)`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function writeTask(payload, kitDir) {
|
|
189
|
+
const tasksPath = join(kitDir, 'tasks.json');
|
|
190
|
+
const existing = existsSync(tasksPath) ? JSON.parse(readFileSync(tasksPath, 'utf-8')) : [];
|
|
191
|
+
const filtered = existing.filter(t => t.payload?.sliceId !== payload.sliceId);
|
|
192
|
+
const task = { id: randomUUID(), createdAt: new Date().toISOString(), payload };
|
|
193
|
+
filtered.push(task);
|
|
194
|
+
writeFileSync(tasksPath, JSON.stringify(filtered, null, 2), 'utf-8');
|
|
195
|
+
console.log(`[agent] Task written — slice="${payload.sliceTitle}" status="${payload.sliceStatus}"`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function startRealtimeAgent(cfg, kitDir) {
|
|
199
|
+
let realtimeToken = await retryOn401('getRealtimeToken', () => getRealtimeToken(cfg));
|
|
200
|
+
|
|
201
|
+
await retryOn401('fetchAndPersistSlices', () => fetchAndPersistSlices(cfg, kitDir)).catch((err) =>
|
|
202
|
+
console.error('[agent] Initial slice fetch error:', err),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const supabase = createClient(cfg.supabaseUrl, cfg.supabaseAnonKey, {
|
|
206
|
+
realtime: { params: { apikey: cfg.supabaseAnonKey } },
|
|
207
|
+
});
|
|
208
|
+
await supabase.realtime.setAuth(realtimeToken);
|
|
209
|
+
|
|
210
|
+
const channelName = `board:${cfg.boardId}-slicechanged`;
|
|
211
|
+
|
|
212
|
+
supabase
|
|
213
|
+
.channel(channelName, { config: { private: true } })
|
|
214
|
+
.on('broadcast', { event: 'message' }, (msg) => {
|
|
215
|
+
if (msg.payload === 'Exit') {
|
|
216
|
+
console.log('[agent] Received "Exit" — shutting down');
|
|
217
|
+
process.exit(0);
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
.on('broadcast', { event: 'slice:changed' }, async (msg) => {
|
|
221
|
+
const payload = msg.payload;
|
|
222
|
+
console.log(`[agent] slice:changed — slice="${payload.sliceTitle}" status="${payload.sliceStatus}"`);
|
|
223
|
+
await retryOn401('fetchAndPersistSlices', () => fetchAndPersistSlices(cfg, kitDir)).catch((err) =>
|
|
224
|
+
console.error('[agent] Slice persist error:', err),
|
|
225
|
+
);
|
|
226
|
+
// Planned slices are handled by onPlannedSlice directly — no task needed
|
|
227
|
+
if ((payload.sliceStatus || '').toLowerCase() !== 'planned') {
|
|
228
|
+
await writeTask(payload, kitDir).catch((err) => console.error('[agent] writeTask error:', err));
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
.subscribe((status) => console.log(`[agent] Channel "${channelName}": ${status}`));
|
|
232
|
+
|
|
233
|
+
setInterval(async () => {
|
|
234
|
+
try {
|
|
235
|
+
realtimeToken = await retryOn401('getRealtimeToken (refresh)', () => getRealtimeToken(cfg));
|
|
236
|
+
supabase.realtime.setAuth(realtimeToken);
|
|
237
|
+
console.log('[agent] Token refreshed');
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error('[agent] Token refresh failed:', err);
|
|
240
|
+
}
|
|
241
|
+
}, 10 * 60 * 1000);
|
|
242
|
+
|
|
243
|
+
const ping = async () => {
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ token: cfg.token }),
|
|
249
|
+
});
|
|
250
|
+
if (!res.ok) console.error(`[agent] Ping failed: ${res.status}`);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error('[agent] Ping error:', err);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
await ping();
|
|
256
|
+
setInterval(ping, 30_000);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Ralph loop ────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
function hasPendingTasks(kitDir) {
|
|
262
|
+
const tasksPath = join(kitDir, 'tasks.json');
|
|
263
|
+
if (!existsSync(tasksPath)) return false;
|
|
264
|
+
try {
|
|
265
|
+
const tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
|
|
266
|
+
return Array.isArray(tasks) && tasks.length > 0;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function readCurrentContext(kitDir) {
|
|
273
|
+
const ctxPath = join(kitDir, '.slices', 'current_context.json');
|
|
274
|
+
if (!existsSync(ctxPath)) return null;
|
|
275
|
+
try { return JSON.parse(readFileSync(ctxPath, 'utf-8')).name || null; } catch { return null; }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Returns the first Planned slice IN THE CURRENT CONTEXT ONLY. If the current
|
|
279
|
+
// context has no planned work, returns null so the loop waits — it must NEVER
|
|
280
|
+
// cross into another context to find something to build.
|
|
281
|
+
function getFirstPlannedSliceTitle(kitDir) {
|
|
282
|
+
const currentCtx = readCurrentContext(kitDir);
|
|
283
|
+
if (!currentCtx) return null;
|
|
284
|
+
const indexPath = join(kitDir, '.slices', currentCtx, 'index.json');
|
|
285
|
+
if (!existsSync(indexPath)) return null;
|
|
286
|
+
try {
|
|
287
|
+
const { slices } = JSON.parse(readFileSync(indexPath, 'utf-8'));
|
|
288
|
+
const planned = slices && slices.find((s) => (s.status || '').toLowerCase() === 'planned');
|
|
289
|
+
if (planned) return planned.slice || planned.id || null;
|
|
290
|
+
} catch {}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function runWithRetry(label, fn) {
|
|
295
|
+
while (true) {
|
|
296
|
+
try {
|
|
297
|
+
console.log(`[ralph] ${label}`);
|
|
298
|
+
await fn();
|
|
299
|
+
return;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(`[ralph] Error — retrying in 60s:`, err.message);
|
|
302
|
+
await new Promise((r) => setTimeout(r, 60_000));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice) {
|
|
308
|
+
const promptFile = join(kitDir, 'lib', 'prompt.md');
|
|
309
|
+
const backendPromptFile = join(kitDir, 'lib', 'backend-prompt.md');
|
|
310
|
+
const credentialed = hasCredentials(cfg);
|
|
311
|
+
let lastIdleCtx;
|
|
312
|
+
|
|
313
|
+
while (true) {
|
|
314
|
+
let didWork = false;
|
|
315
|
+
|
|
316
|
+
if (credentialed && hasPendingTasks(kitDir)) {
|
|
317
|
+
const prompt = readFileSync(promptFile, 'utf-8');
|
|
318
|
+
await runWithRetry('onTask: loading slice from board...', () => onTask(prompt));
|
|
319
|
+
await fetchAndPersistSlices(cfg, kitDir).catch(() => {});
|
|
320
|
+
didWork = true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const plannedTitle = onPlannedSlice && getFirstPlannedSliceTitle(kitDir);
|
|
324
|
+
if (plannedTitle) {
|
|
325
|
+
const prompt = readFileSync(backendPromptFile, 'utf-8');
|
|
326
|
+
await runWithRetry(`onPlannedSlice: building slice "${plannedTitle}"...`, () => onPlannedSlice(prompt));
|
|
327
|
+
console.log(`[ralph] Slice build complete — waiting for next slice`);
|
|
328
|
+
if (credentialed) await fetchAndPersistSlices(cfg, kitDir).catch(() => {});
|
|
329
|
+
didWork = true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!didWork) {
|
|
333
|
+
// No planned work in the current context — wait, do NOT switch contexts.
|
|
334
|
+
const ctx = readCurrentContext(kitDir);
|
|
335
|
+
if (ctx !== lastIdleCtx) {
|
|
336
|
+
console.log(`[ralph] No planned slices in current context "${ctx}" — waiting. Switch context on the board to continue.`);
|
|
337
|
+
lastIdleCtx = ctx;
|
|
338
|
+
}
|
|
339
|
+
await new Promise((r) => setTimeout(r, 10_000));
|
|
340
|
+
} else {
|
|
341
|
+
lastIdleCtx = undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
export { loadLocalConfig, fetchPlatformConfig, retryOn401, startRealtimeAgent };
|
|
349
|
+
|
|
350
|
+
export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice }) {
|
|
351
|
+
const local = loadLocalConfig(kitDir);
|
|
352
|
+
|
|
353
|
+
console.log(`Ralph — kit: ${kitDir}`);
|
|
354
|
+
console.log(` project: ${projectDir}`);
|
|
355
|
+
|
|
356
|
+
if (!hasCredentials(local)) {
|
|
357
|
+
console.log(` mode: local-only (no platform sync)\n`);
|
|
358
|
+
await ralphLoop(kitDir, local, onTask, onPlannedSlice);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const cfg = await retryOn401('fetchPlatformConfig', () => fetchPlatformConfig(local));
|
|
363
|
+
console.log(` org=${cfg.organizationId}, board=${cfg.boardId}, base=${cfg.baseUrl}\n`);
|
|
364
|
+
|
|
365
|
+
await Promise.all([
|
|
366
|
+
startRealtimeAgent(cfg, kitDir),
|
|
367
|
+
ralphLoop(kitDir, cfg, onTask, onPlannedSlice),
|
|
368
|
+
]);
|
|
369
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Ralph loop + realtime agent using Claude Code as the executor.
|
|
3
|
+
// Usage: node ralph-claude.js [project_dir]
|
|
4
|
+
|
|
5
|
+
import { startRalph, loadLocalConfig } from './lib/ralph.js';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { dirname, resolve } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const kitDir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
|
|
12
|
+
|
|
13
|
+
const cfg = loadLocalConfig(kitDir);
|
|
14
|
+
const inlineHeader = cfg.boardId
|
|
15
|
+
? `board=${cfg.boardId} token=${cfg.token} org=${cfg.organizationId} baseUrl=${cfg.baseUrl}\n\n`
|
|
16
|
+
: '';
|
|
17
|
+
|
|
18
|
+
const claudeArgs = ['--dangerously-skip-permissions'];
|
|
19
|
+
if (cfg.model) claudeArgs.push('--model', cfg.model);
|
|
20
|
+
const claudeEnv = cfg.anthropicBaseUrl
|
|
21
|
+
? { ...process.env, ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl }
|
|
22
|
+
: process.env;
|
|
23
|
+
|
|
24
|
+
function runClaude(prompt) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const proc = spawn('claude', [...claudeArgs, '-p', inlineHeader + prompt], {
|
|
27
|
+
cwd: projectDir,
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
env: claudeEnv,
|
|
30
|
+
});
|
|
31
|
+
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Claude exited ${code}`))));
|
|
32
|
+
proc.on('error', reject);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
startRalph({
|
|
37
|
+
kitDir,
|
|
38
|
+
projectDir,
|
|
39
|
+
onTask: runClaude,
|
|
40
|
+
onPlannedSlice: runClaude,
|
|
41
|
+
}).catch((err) => {
|
|
42
|
+
console.error('[ralph] Fatal:', err);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Ralph agent loop — two independent loops, each triggered by their own condition
|
|
3
|
+
#
|
|
4
|
+
# onTask: tasks.json has entries → load slice from board, update .build-kit/.slices/
|
|
5
|
+
# onPlannedSlice: .build-kit/.slices/ has a "Planned" slice → build it
|
|
6
|
+
#
|
|
7
|
+
# The loops are NOT causally linked — either can trigger on its own.
|
|
8
|
+
#
|
|
9
|
+
# Usage: ./ralph.sh [iterations] [project_dir]
|
|
10
|
+
# iterations — number of loop cycles to run; 0 or omitted means run forever
|
|
11
|
+
# project_dir — path to the project root; defaults to the parent of .build-kit
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
KIT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
16
|
+
ITERATIONS="${1:-0}"
|
|
17
|
+
PROJECT_DIR="${2:-"$KIT_DIR/.."}"
|
|
18
|
+
TASKS_FILE="$KIT_DIR/tasks.json"
|
|
19
|
+
PROMPT_FILE="$KIT_DIR/lib/prompt.md"
|
|
20
|
+
BACKEND_PROMPT_FILE="$KIT_DIR/lib/backend-prompt.md"
|
|
21
|
+
AGENT_SCRIPT="$KIT_DIR/lib/agent.sh"
|
|
22
|
+
|
|
23
|
+
HAS_CREDENTIALS=true
|
|
24
|
+
if [[ ! -f "$KIT_DIR/.eventmodelers/config.json" ]]; then
|
|
25
|
+
echo "[ralph] Note: no .eventmodelers/config.json found — platform sync disabled." >&2
|
|
26
|
+
echo " To enable board sync, follow: https://app.eventmodelers.ai/documentation#build-axon" >&2
|
|
27
|
+
echo " Code generation from local slice definitions will still run." >&2
|
|
28
|
+
HAS_CREDENTIALS=false
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "Ralph — kit: $KIT_DIR project: $PROJECT_DIR"
|
|
32
|
+
|
|
33
|
+
# Returns 0 if tasks.json has at least one task
|
|
34
|
+
has_pending_tasks() {
|
|
35
|
+
[[ -f "$TASKS_FILE" ]] || return 1
|
|
36
|
+
local content
|
|
37
|
+
content=$(cat "$TASKS_FILE")
|
|
38
|
+
[[ "$content" != "[]" && -n "$content" ]]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Returns 0 if any JSON under .build-kit/.slices/ contains a "Planned" status
|
|
42
|
+
has_planned_slices() {
|
|
43
|
+
grep -rqi '"status"[[:space:]]*:[[:space:]]*"planned"' "$KIT_DIR/.slices/" --include='index.json' 2>/dev/null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Returns the title of the first "Planned" slice, or empty string
|
|
47
|
+
get_planned_slice_title() {
|
|
48
|
+
for index_file in "$KIT_DIR/.slices/"*/index.json; do
|
|
49
|
+
[[ -f "$index_file" ]] || continue
|
|
50
|
+
local title
|
|
51
|
+
title=$(node -e "
|
|
52
|
+
try {
|
|
53
|
+
const d = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf-8'));
|
|
54
|
+
const s = (d.slices||[]).find(s => (s.status||'').toLowerCase() === 'planned');
|
|
55
|
+
if (s) process.stdout.write(s.slice || s.id || '');
|
|
56
|
+
} catch(e) {}
|
|
57
|
+
" "$index_file" 2>/dev/null)
|
|
58
|
+
if [[ -n "$title" ]]; then
|
|
59
|
+
echo "$title"
|
|
60
|
+
return
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Runs agent.sh with the given prompt; retries on non-zero exit
|
|
66
|
+
run_agent() {
|
|
67
|
+
local label="$1"
|
|
68
|
+
local prompt="$2"
|
|
69
|
+
while true; do
|
|
70
|
+
echo "[$(date -u +%H:%M:%S)] $label"
|
|
71
|
+
(cd "$PROJECT_DIR" && bash "$AGENT_SCRIPT" "$prompt") 2>&1 && return 0
|
|
72
|
+
echo "[$(date -u +%H:%M:%S)] Agent error — retrying in 60s..."
|
|
73
|
+
sleep 60
|
|
74
|
+
done
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
cycle=0
|
|
78
|
+
while [[ "$ITERATIONS" -eq 0 || "$cycle" -lt "$ITERATIONS" ]]; do
|
|
79
|
+
ran_something=false
|
|
80
|
+
|
|
81
|
+
if [[ "$HAS_CREDENTIALS" == true ]] && has_pending_tasks; then
|
|
82
|
+
run_agent "onTask: loading slice from board..." "$(cat "$PROMPT_FILE")"
|
|
83
|
+
ran_something=true
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
if has_planned_slices; then
|
|
87
|
+
slice_title=$(get_planned_slice_title)
|
|
88
|
+
run_agent "onPlannedSlice: building \"$slice_title\"..." "$(cat "$BACKEND_PROMPT_FILE")"
|
|
89
|
+
echo "[$(date -u +%H:%M:%S)] Slice \"$slice_title\" build complete — waiting for next slice"
|
|
90
|
+
ran_something=true
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [[ "$ran_something" == false ]]; then
|
|
94
|
+
sleep 3
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
(( cycle++ )) || true
|
|
98
|
+
done
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Copy this file to .env and fill in your AxonIQ credentials.
|
|
2
|
+
# Get these from https://console.axoniq.io
|
|
3
|
+
AXONIQ_PLATFORM_CREDENTIALS=your-org-id:your-credential-token
|
|
4
|
+
AXONIQ_PLATFORM_AUTHENTICATION=your-org-id-asc:your-authentication-token
|
|
5
|
+
AXONIQ_INSIGHTS_AUTHENTICATION=your-org-id:your-insights-token
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Project Configuration
|
|
2
|
+
|
|
3
|
+
Read Events in src/events to understand the global structure.
|
|
4
|
+
|
|
5
|
+
## File Structure Constraints
|
|
6
|
+
|
|
7
|
+
- **Strict Path Limitation**: if not instructed otherwise, only check `src/slices/{slicename}/*.ts`
|
|
8
|
+
- **Slice Organization**: Each feature/domain should be organized as a separate slice
|
|
9
|
+
|
|
10
|
+
## Code Standards
|
|
11
|
+
|
|
12
|
+
- **Language**: TypeScript only
|
|
13
|
+
- **Module System**: Use ES modules (import/export)
|
|
14
|
+
- **Type Safety**: Ensure all code is properly typed
|
|
15
|
+
|
|
16
|
+
## Development Guidelines
|
|
17
|
+
|
|
18
|
+
1. Each slice should be self-contained and focused on a specific domain
|
|
19
|
+
2. Maintain clear separation of concerns within each slice
|
|
20
|
+
3. Follow TypeScript best practices for type definitions and interfaces
|
|
21
|
+
|
|
22
|
+
Only check src/slices/{slice}/*.ts, do not check subfolders unless explicitely tasked to.
|
|
23
|
+
If not tasked explicitely to change routes, ignore routes*.ts
|
|
24
|
+
|
|
25
|
+
Ignore case for files and slices in prompts. "CartItems" slice is the same as "cartitems"
|
|
26
|
+
|
|
27
|
+
Do not change files with tests unless explicitely instructed: *.test.ts
|
|
28
|
+
|
|
29
|
+
At the start of every session, read `AGENTS.md` if it exists to load accumulated project learnings.
|
|
30
|
+
|
|
31
|
+
When starting to work on a slice, invoke the `update-slice-status` skill with `InProgress` status before doing anything else.
|
|
32
|
+
|
|
33
|
+
## Building a Slice
|
|
34
|
+
|
|
35
|
+
**CRITICAL: You MUST always use the provided skills to build slices. NEVER implement a slice manually.**
|
|
36
|
+
**ALL fields, event names, command names, and business rules MUST come exclusively from slice.json. Do NOT invent, assume, or guess any field or logic not present in the slice definition.**
|
|
37
|
+
|
|
38
|
+
When asked to build a slice, always follow this flow:
|
|
39
|
+
|
|
40
|
+
1. Read the slice definition from `.build-kit/.slices/<context>/<slicename>/slice.json`.
|
|
41
|
+
2. Determine the slice type:
|
|
42
|
+
- **Translation** — `sliceType === "TRANSLATION"` → read `description` and `notes` from slice.json for hints; default to `/build-automation` if nothing else is specified
|
|
43
|
+
- **Automation** — `processors` array is non-empty → invoke `/build-automation`
|
|
44
|
+
- **State-view** — `projections` or `queries` array is non-empty → invoke `/build-state-view`
|
|
45
|
+
- **State-change** — default (has `commands` / `events`) → invoke `/build-state-change`
|
|
46
|
+
3. Invoke the matching skill and follow its instructions completely. Do not deviate.
|
|
47
|
+
4. **Verify against slice.json**: After the skill completes, check that every command field, event field, and specification in slice.json appears in the implementation. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
48
|
+
5. Run quality checks (`./mvnw compile -q`, then the slice tests only).
|
|
49
|
+
6. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done`.
|
|
50
|
+
|
|
51
|
+
After you are done, automatically run the tests for the slice that was edited.
|
|
52
|
+
|
|
53
|
+
## Example Slice Structure
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
src/slices/
|
|
57
|
+
├── {slice-name}/
|
|
58
|
+
│ ├── CommandHandler.ts
|
|
59
|
+
│ └── routes.ts
|
|
60
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Axoniq Platform Quickstart
|
|
2
|
+
|
|
3
|
+
This simple GiftCard application demonstrates the core concepts of the Axoniq Platform, including Command Handling,
|
|
4
|
+
Event Sourcing, and Projections. It is built using the Axoniq Framework and Spring Boot.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
- Java 21 or higher
|
|
8
|
+
- Maven 3.6 or higher
|
|
9
|
+
- Docker (for running the Axon Server)
|
|
10
|
+
- An IDE like IntelliJ IDEA or Eclipse (optional)
|
|
11
|
+
|
|
12
|
+
## Getting Started
|
|
13
|
+
|
|
14
|
+
Run docker-compose to start Axon Server:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
docker-compose up -d
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Now, you can run the application using Maven:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
mvn spring-boot:run
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Alternatively, you can run the application from your IDE by running the `QuickstartApplication` class.
|
|
27
|
+
|
|
28
|
+
## Using the Application
|
|
29
|
+
You can interact with the application via the basic UI available on [http://localhost:8080](http://localhost:8080).
|
|
30
|
+
The events are stored in Axon Server, which you can access at [http://localhost:8024](http://localhost:8024).
|
|
31
|
+
You can run analytics queries using AI on your event store with Axoniq Insights, accessible at [https://localhost:8090](https://localhost:8090).
|
|
32
|
+
|
|
33
|
+
Your application and Axon Server will both connect to the Axoniq Platform. You can monitor and manage your application from there: [https://monitor.axoniq.io/workspace/e6953958/env/e6953958-0](https://monitor.axoniq.io/workspace/e6953958/env/e6953958-0).
|
|
34
|
+
|
|
35
|
+
## Useful resources
|
|
36
|
+
|
|
37
|
+
You can access a lot of resources through the [Axoniq Platform](https://platform.axoniq.io/). This includes:
|
|
38
|
+
|
|
39
|
+
- Extensive Documentation
|
|
40
|
+
- Axoniq Academy with free courses
|
|
41
|
+
- AI-powered Development Agent to create your applications faster
|
|
42
|
+
- Community Forum to ask questions and share knowledge
|
|
43
|
+
- Monitor your applications
|
|
44
|
+
- And much more!
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:17
|
|
4
|
+
ports:
|
|
5
|
+
- "5432:5432"
|
|
6
|
+
environment:
|
|
7
|
+
- POSTGRES_DB=quickstart
|
|
8
|
+
- POSTGRES_USER=quickstart
|
|
9
|
+
- POSTGRES_PASSWORD=quickstart
|
|
10
|
+
volumes:
|
|
11
|
+
- postgres-data:/var/lib/postgresql/data
|
|
12
|
+
healthcheck:
|
|
13
|
+
test: ["CMD-SHELL", "pg_isready -U quickstart"]
|
|
14
|
+
interval: 10s
|
|
15
|
+
timeout: 5s
|
|
16
|
+
retries: 10
|
|
17
|
+
|
|
18
|
+
axon-server:
|
|
19
|
+
image: axoniq/axonserver:latest-jdk-17
|
|
20
|
+
ports:
|
|
21
|
+
- "8024:8024"
|
|
22
|
+
- "8124:8124"
|
|
23
|
+
- '8224:8224'
|
|
24
|
+
hostname: axonserver-1
|
|
25
|
+
environment:
|
|
26
|
+
- axoniq_platform_authentication=${AXONIQ_PLATFORM_AUTHENTICATION}
|
|
27
|
+
- axoniq.axonserver.autocluster.dcb=true
|
|
28
|
+
volumes:
|
|
29
|
+
- data:/axonserver/data
|
|
30
|
+
- events:/axonserver/events
|
|
31
|
+
networks:
|
|
32
|
+
- axon-server
|
|
33
|
+
healthcheck:
|
|
34
|
+
test: ["CMD", "curl", "-f", "http://localhost:8024/actuator/health"]
|
|
35
|
+
interval: 10s
|
|
36
|
+
timeout: 5s
|
|
37
|
+
retries: 10
|
|
38
|
+
start_period: 30s
|
|
39
|
+
|
|
40
|
+
axoniq-insights:
|
|
41
|
+
image: docker.axoniq.io/axoniq/axoniq-insights:1.0.0
|
|
42
|
+
ports:
|
|
43
|
+
- "8090:8080"
|
|
44
|
+
environment:
|
|
45
|
+
- insights.platform.authentication=${AXONIQ_INSIGHTS_AUTHENTICATION}
|
|
46
|
+
- insights.axonserver.routing-servers=axonserver-1:8124
|
|
47
|
+
- insights.axonserver.auto-subscribe=true
|
|
48
|
+
depends_on:
|
|
49
|
+
axon-server:
|
|
50
|
+
condition: service_healthy
|
|
51
|
+
networks:
|
|
52
|
+
- axon-server
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
networks:
|
|
56
|
+
axon-server:
|
|
57
|
+
|
|
58
|
+
volumes:
|
|
59
|
+
data:
|
|
60
|
+
driver: local
|
|
61
|
+
events:
|
|
62
|
+
driver: local
|
|
63
|
+
postgres-data:
|
|
64
|
+
driver: local
|