@stackmemoryai/stackmemory 1.6.0 → 1.6.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.
Files changed (34) hide show
  1. package/dist/src/cli/commands/orchestrate.js +334 -4
  2. package/dist/src/cli/commands/orchestrator.js +283 -24
  3. package/package.json +1 -2
  4. package/scripts/gepa/.before-optimize.md +0 -112
  5. package/scripts/gepa/README.md +0 -275
  6. package/scripts/gepa/config.json +0 -59
  7. package/scripts/gepa/evals/coding-tasks.jsonl +0 -5
  8. package/scripts/gepa/evals/fixtures/api-endpoint.ts +0 -31
  9. package/scripts/gepa/evals/fixtures/brittle-integration.ts +0 -38
  10. package/scripts/gepa/evals/fixtures/buggy-loop.js +0 -18
  11. package/scripts/gepa/evals/fixtures/callback-hell.js +0 -53
  12. package/scripts/gepa/evals/fixtures/fts5-triggers.sql +0 -23
  13. package/scripts/gepa/evals/fixtures/leaky-service.ts +0 -70
  14. package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +0 -39
  15. package/scripts/gepa/evals/fixtures/pr-diff.txt +0 -24
  16. package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +0 -34
  17. package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +0 -42
  18. package/scripts/gepa/evals/stackmemory-tasks.jsonl +0 -8
  19. package/scripts/gepa/generations/gen-000/baseline.md +0 -112
  20. package/scripts/gepa/generations/gen-001/baseline.md +0 -112
  21. package/scripts/gepa/generations/gen-001/variant-a.md +0 -107
  22. package/scripts/gepa/generations/gen-001/variant-b.md +0 -216
  23. package/scripts/gepa/generations/gen-001/variant-c.md +0 -83
  24. package/scripts/gepa/generations/gen-001/variant-d.md +0 -90
  25. package/scripts/gepa/hooks/auto-optimize.js +0 -494
  26. package/scripts/gepa/hooks/eval-tracker.js +0 -203
  27. package/scripts/gepa/hooks/reflect.js +0 -350
  28. package/scripts/gepa/optimize.js +0 -853
  29. package/scripts/gepa/results/eval-1-baseline.json +0 -218
  30. package/scripts/gepa/results/eval-1-variant-a.json +0 -218
  31. package/scripts/gepa/results/eval-1-variant-b.json +0 -218
  32. package/scripts/gepa/results/eval-1-variant-c.json +0 -218
  33. package/scripts/gepa/results/eval-1-variant-d.json +0 -218
  34. package/scripts/gepa/state.json +0 -49
@@ -38,6 +38,124 @@ function logAgentOutcome(entry) {
38
38
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
39
39
  appendFileSync(getOutcomesLogPath(), JSON.stringify(entry) + "\n");
40
40
  }
41
+ function createPullRequest(opts) {
42
+ try {
43
+ execSync(`git push -u origin "${opts.branch}"`, {
44
+ cwd: opts.workspacePath,
45
+ stdio: "pipe",
46
+ timeout: 6e4
47
+ });
48
+ const prTitle = `feat(conductor): ${opts.issueId} \u2014 ${opts.title}`;
49
+ const prBody = [
50
+ "## Summary",
51
+ "",
52
+ `Automated PR from conductor agent for **${opts.issueId}**.`,
53
+ "",
54
+ `- **Files modified:** ${opts.filesModified}`,
55
+ `- **Tool calls:** ${opts.toolCalls}`,
56
+ "",
57
+ "_This PR was auto-created by StackMemory Conductor._"
58
+ ].join("\n");
59
+ const result = execSync(
60
+ `gh pr create --base "${opts.baseBranch}" --head "${opts.branch}" --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}"`,
61
+ {
62
+ cwd: opts.workspacePath,
63
+ encoding: "utf-8",
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ timeout: 3e4
66
+ }
67
+ );
68
+ const prUrl = result.trim();
69
+ logger.info("Created PR", { issueId: opts.issueId, prUrl });
70
+ return prUrl;
71
+ } catch (err) {
72
+ logger.warn("Failed to create PR (best-effort)", {
73
+ issueId: opts.issueId,
74
+ error: err.message
75
+ });
76
+ return null;
77
+ }
78
+ }
79
+ function getRetryStrategy(issue, outcomes) {
80
+ if (!outcomes) {
81
+ const logPath = getOutcomesLogPath();
82
+ if (!existsSync(logPath)) {
83
+ return { shouldRetry: true, adjustments: [] };
84
+ }
85
+ try {
86
+ const lines = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
87
+ outcomes = lines.map((l) => JSON.parse(l));
88
+ } catch {
89
+ return { shouldRetry: true, adjustments: [] };
90
+ }
91
+ }
92
+ const issueOutcomes = outcomes.filter((o) => o.issue === issue);
93
+ const relevant = issueOutcomes.length > 0 ? issueOutcomes : outcomes;
94
+ const failures = relevant.filter(
95
+ (o) => o.outcome === "failure" || o.outcome === "partial"
96
+ );
97
+ if (failures.length === 0) {
98
+ return { shouldRetry: true, adjustments: [] };
99
+ }
100
+ const lastFailure = failures[failures.length - 1];
101
+ if (lastFailure.errorTail && /429|rate.?limit|too many requests|usage.?limit|overloaded/i.test(
102
+ lastFailure.errorTail
103
+ )) {
104
+ return {
105
+ shouldRetry: false,
106
+ reason: "Last failure was a rate limit (429) \u2014 retrying immediately will not help",
107
+ adjustments: []
108
+ };
109
+ }
110
+ if (issueOutcomes.length > 0) {
111
+ const issueFailures = issueOutcomes.filter(
112
+ (o) => o.outcome === "failure" || o.outcome === "partial"
113
+ );
114
+ const phaseFailCounts = /* @__PURE__ */ new Map();
115
+ for (const f of issueFailures) {
116
+ phaseFailCounts.set(f.phase, (phaseFailCounts.get(f.phase) || 0) + 1);
117
+ }
118
+ for (const [phase, count] of phaseFailCounts) {
119
+ if (count >= 2) {
120
+ return {
121
+ shouldRetry: false,
122
+ reason: `Issue has failed ${count} times in '${phase}' phase \u2014 likely a structural problem`,
123
+ adjustments: []
124
+ };
125
+ }
126
+ }
127
+ }
128
+ const adjustments = [];
129
+ if (lastFailure.errorTail) {
130
+ const tail = lastFailure.errorTail;
131
+ if (/timeout|timed?.?out|ETIMEDOUT|ESOCKETTIMEDOUT/i.test(tail)) {
132
+ adjustments.push(
133
+ "Previous attempt timed out. Work in smaller increments and commit partial progress early."
134
+ );
135
+ }
136
+ if (/lint|eslint|prettier|formatting|style.?error/i.test(tail)) {
137
+ adjustments.push(
138
+ "Previous attempt failed on linting. Run `npm run lint:fix` after each file change and fix all lint errors before committing."
139
+ );
140
+ }
141
+ if (/test.?fail|assertion|expect|vitest|jest|FAIL/i.test(tail)) {
142
+ adjustments.push(
143
+ "Previous attempt failed tests. Run tests incrementally after each change. Check existing test expectations before modifying code."
144
+ );
145
+ }
146
+ if (/build.?fail|tsc|type.?error|TS\d{4}|compile/i.test(tail)) {
147
+ adjustments.push(
148
+ "Previous attempt had build/type errors. Run `npm run build` after changes and fix type errors before committing."
149
+ );
150
+ }
151
+ if (/cannot find module|ERR_MODULE_NOT_FOUND|import/i.test(tail)) {
152
+ adjustments.push(
153
+ "Previous attempt had module resolution errors. Ensure all imports use .js extensions for relative paths (ESM)."
154
+ );
155
+ }
156
+ }
157
+ return { shouldRetry: true, adjustments };
158
+ }
41
159
  function findPackageRoot() {
42
160
  const currentFile = fileURLToPath(import.meta.url);
43
161
  let dir = dirname(currentFile);
@@ -136,6 +254,52 @@ function inferPhase(msg) {
136
254
  }
137
255
  return null;
138
256
  }
257
+ const SIMPLE_LABELS = ["bug", "fix", "typo", "chore", "docs", "hotfix"];
258
+ const COMPLEX_LABELS = [
259
+ "feature",
260
+ "refactor",
261
+ "architecture",
262
+ "migration",
263
+ "security",
264
+ "performance"
265
+ ];
266
+ function estimateIssueComplexity(issue, attempt) {
267
+ let score = 0;
268
+ const descLen = (issue.description || "").length;
269
+ if (descLen > 800) score += 2;
270
+ else if (descLen > 400) score += 1;
271
+ else if (descLen < 200) score -= 1;
272
+ const labelNames = (issue.labels || []).map((l) => l.name.toLowerCase());
273
+ for (const label of labelNames) {
274
+ if (SIMPLE_LABELS.some((s) => label.includes(s))) score -= 1;
275
+ if (COMPLEX_LABELS.some((c) => label.includes(c))) score += 2;
276
+ }
277
+ const titleLower = issue.title.toLowerCase();
278
+ if (SIMPLE_LABELS.some((s) => titleLower.includes(s))) score -= 1;
279
+ if (COMPLEX_LABELS.some((c) => titleLower.includes(c))) score += 1;
280
+ if (issue.priority === 1) score += 2;
281
+ else if (issue.priority === 2) score += 1;
282
+ if (issue.estimate) {
283
+ if (issue.estimate >= 5) score += 2;
284
+ else if (issue.estimate >= 3) score += 1;
285
+ else if (issue.estimate <= 1) score -= 1;
286
+ }
287
+ if (attempt && attempt > 1) score += 2;
288
+ if (score >= 3) return "complex";
289
+ if (score >= 1) return "moderate";
290
+ return "simple";
291
+ }
292
+ function selectModelForIssue(complexity, config) {
293
+ const modelSetting = config.model || "auto";
294
+ if (modelSetting !== "auto") return modelSetting;
295
+ switch (complexity) {
296
+ case "simple":
297
+ case "moderate":
298
+ return "claude-sonnet-4-20250514";
299
+ case "complex":
300
+ return "claude-opus-4-20250514";
301
+ }
302
+ }
139
303
  const DEFAULT_CONFIG = {
140
304
  activeStates: ["Todo"],
141
305
  terminalStates: ["Done", "Cancelled", "Canceled", "Closed"],
@@ -645,6 +809,24 @@ class Conductor {
645
809
  await this.runAgent(issue, run);
646
810
  run.status = "completed";
647
811
  this.completeCount++;
812
+ let prUrl;
813
+ if (this.config.autoPR !== false) {
814
+ const wsKey = this.sanitizeIdentifier(issue.identifier);
815
+ const branchName = `conductor/${wsKey}`;
816
+ const url = createPullRequest({
817
+ branch: branchName,
818
+ baseBranch: this.config.baseBranch,
819
+ issueId: issue.identifier,
820
+ title: issue.title,
821
+ filesModified: run.filesModified,
822
+ toolCalls: run.toolCalls,
823
+ workspacePath: run.workspacePath
824
+ });
825
+ if (url) {
826
+ prUrl = url;
827
+ console.log(`[${issue.identifier}] PR created: ${url}`);
828
+ }
829
+ }
648
830
  logAgentOutcome({
649
831
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
650
832
  issue: issue.identifier,
@@ -655,7 +837,9 @@ class Conductor {
655
837
  filesModified: run.filesModified,
656
838
  tokensUsed: run.tokensUsed,
657
839
  durationMs: Date.now() - run.startedAt,
658
- hasCommits: true
840
+ hasCommits: true,
841
+ labels: issue.labels.map((l) => l.name),
842
+ prUrl
659
843
  });
660
844
  await this.runHook(
661
845
  "after-run",
@@ -678,6 +862,20 @@ class Conductor {
678
862
  error: run.error,
679
863
  attempt: run.attempt
680
864
  });
865
+ logAgentOutcome({
866
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
867
+ issue: issue.identifier,
868
+ attempt: run.attempt,
869
+ outcome: "failure",
870
+ phase: run.phase,
871
+ toolCalls: run.toolCalls,
872
+ filesModified: run.filesModified,
873
+ tokensUsed: run.tokensUsed,
874
+ durationMs: Date.now() - run.startedAt,
875
+ hasCommits: false,
876
+ labels: issue.labels.map((l) => l.name),
877
+ errorTail: run.error?.slice(-500)
878
+ });
681
879
  if (this.handleRateLimitError(run.error, issue.identifier)) {
682
880
  throw err;
683
881
  }
@@ -691,6 +889,23 @@ class Conductor {
691
889
  });
692
890
  }
693
891
  if (run.attempt < maxAttempts && !this.stopping) {
892
+ const strategy = getRetryStrategy(issue.identifier);
893
+ if (!strategy.shouldRetry) {
894
+ console.log(
895
+ `[${issue.identifier}] Skipping retry: ${strategy.reason}`
896
+ );
897
+ logger.info("Retry skipped by intelligence", {
898
+ identifier: issue.identifier,
899
+ reason: strategy.reason
900
+ });
901
+ throw err;
902
+ }
903
+ if (strategy.adjustments.length > 0) {
904
+ run.retryAdjustments = strategy.adjustments;
905
+ console.log(
906
+ `[${issue.identifier}] Retry with adjustments: ${strategy.adjustments.length} hint(s)`
907
+ );
908
+ }
694
909
  console.log(
695
910
  `[${issue.identifier}] Failed (attempt ${run.attempt}), retrying...`
696
911
  );
@@ -1024,25 +1239,33 @@ class Conductor {
1024
1239
  */
1025
1240
  runAgentCLI(issue, run) {
1026
1241
  return new Promise((resolve, reject) => {
1027
- const prompt = this.buildPrompt(issue, run.attempt);
1242
+ const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
1243
+ const complexity = estimateIssueComplexity(issue, run.attempt);
1244
+ const selectedModel = selectModelForIssue(complexity, this.config);
1245
+ logger.info("Agent model selected", {
1246
+ identifier: issue.identifier,
1247
+ complexity,
1248
+ model: selectedModel || "(default)",
1249
+ reason: this.config.model && this.config.model !== "auto" ? "config override" : `auto: ${complexity} issue`
1250
+ });
1028
1251
  const claudeBin = this.findClaudeBinary();
1029
- const proc = spawn(
1030
- claudeBin,
1031
- [
1032
- "-p",
1033
- "--output-format",
1034
- "stream-json",
1035
- "--dangerously-skip-permissions",
1036
- "--settings",
1037
- '{"hooks":{}}',
1038
- prompt
1039
- ],
1040
- {
1041
- cwd: run.workspacePath,
1042
- env: this.buildAgentEnv(issue, run),
1043
- stdio: ["pipe", "pipe", "pipe"]
1044
- }
1045
- );
1252
+ const args = [
1253
+ "-p",
1254
+ "--output-format",
1255
+ "stream-json",
1256
+ "--dangerously-skip-permissions",
1257
+ "--settings",
1258
+ '{"hooks":{}}'
1259
+ ];
1260
+ if (selectedModel) {
1261
+ args.push("--model", selectedModel);
1262
+ }
1263
+ args.push(prompt);
1264
+ const proc = spawn(claudeBin, args, {
1265
+ cwd: run.workspacePath,
1266
+ env: this.buildAgentEnv(issue, run),
1267
+ stdio: ["pipe", "pipe", "pipe"]
1268
+ });
1046
1269
  run.process = proc;
1047
1270
  proc.stdin.end();
1048
1271
  const logStream = this.openAgentLogStream(issue.identifier);
@@ -1146,7 +1369,7 @@ class Conductor {
1146
1369
  */
1147
1370
  runAgentAdapter(issue, run) {
1148
1371
  return new Promise((resolve, reject) => {
1149
- const prompt = this.buildPrompt(issue, run.attempt);
1372
+ const prompt = this.buildPrompt(issue, run.attempt, run.retryAdjustments);
1150
1373
  const proc = spawn("node", [this.config.appServerPath], {
1151
1374
  cwd: run.workspacePath,
1152
1375
  env: this.buildAgentEnv(issue, run),
@@ -1301,7 +1524,7 @@ class Conductor {
1301
1524
  * Template variables: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}},
1302
1525
  * {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
1303
1526
  */
1304
- buildPrompt(issue, attempt) {
1527
+ buildPrompt(issue, attempt, retryAdjustments) {
1305
1528
  const templatePath = join(
1306
1529
  homedir(),
1307
1530
  ".stackmemory",
@@ -1310,7 +1533,20 @@ class Conductor {
1310
1533
  );
1311
1534
  const priority = ["None", "Urgent", "High", "Medium", "Low"][issue.priority] || "None";
1312
1535
  const labels = issue.labels.length > 0 ? issue.labels.map((l) => l.name).join(", ") : "";
1313
- const priorContext = attempt > 1 ? `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.` : "";
1536
+ const contextParts = [];
1537
+ if (attempt > 1) {
1538
+ contextParts.push(
1539
+ `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.`
1540
+ );
1541
+ }
1542
+ if (retryAdjustments && retryAdjustments.length > 0) {
1543
+ contextParts.push(
1544
+ "## Retry Adjustments (from prior failure analysis)",
1545
+ "",
1546
+ ...retryAdjustments.map((a) => `- ${a}`)
1547
+ );
1548
+ }
1549
+ const priorContext = contextParts.join("\n");
1314
1550
  if (existsSync(templatePath)) {
1315
1551
  try {
1316
1552
  let template = readFileSync(templatePath, "utf-8");
@@ -1535,6 +1771,24 @@ class Conductor {
1535
1771
  } catch {
1536
1772
  }
1537
1773
  }
1774
+ let prUrl;
1775
+ if (hasCommits && this.config.autoPR !== false) {
1776
+ const wsKey = this.sanitizeIdentifier(run.issue.identifier);
1777
+ const branchName = `conductor/${wsKey}`;
1778
+ const url = createPullRequest({
1779
+ branch: branchName,
1780
+ baseBranch: this.config.baseBranch,
1781
+ issueId: run.issue.identifier,
1782
+ title: run.issue.title,
1783
+ filesModified: run.filesModified,
1784
+ toolCalls: run.toolCalls,
1785
+ workspacePath: wsPath
1786
+ });
1787
+ if (url) {
1788
+ prUrl = url;
1789
+ console.log(`[${run.issue.identifier}] PR created: ${url}`);
1790
+ }
1791
+ }
1538
1792
  const durationMs = Date.now() - run.startedAt;
1539
1793
  logAgentOutcome({
1540
1794
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1547,7 +1801,9 @@ class Conductor {
1547
1801
  tokensUsed: run.tokensUsed,
1548
1802
  durationMs,
1549
1803
  hasCommits,
1550
- errorTail
1804
+ labels: run.issue.labels.map((l) => l.name),
1805
+ errorTail,
1806
+ prUrl
1551
1807
  });
1552
1808
  if (hasCommits) {
1553
1809
  logger.info("Stale agent had commits, transitioning to In Review", {
@@ -1631,6 +1887,9 @@ class Conductor {
1631
1887
  }
1632
1888
  export {
1633
1889
  Conductor,
1890
+ estimateIssueComplexity,
1634
1891
  getAgentStatusDir,
1635
- getOutcomesLogPath
1892
+ getOutcomesLogPath,
1893
+ getRetryStrategy,
1894
+ selectModelForIssue
1636
1895
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, and automatic hooks.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
@@ -20,7 +20,6 @@
20
20
  "files": [
21
21
  "bin",
22
22
  "dist/src",
23
- "scripts/gepa",
24
23
  "scripts/git-hooks",
25
24
  "scripts/hooks",
26
25
  "scripts/setup",
@@ -1,112 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- Sol is the monorepo for **Rize**, an automatic time tracking application. The stack consists of:
8
- - **api/** - Rails 7.1 GraphQL backend (Ruby 3.3.5)
9
- - **web/** - Next.js 14 React web app (Node 22)
10
- - **electron/** - Electron desktop app (Node 22)
11
- - **services/** - Bun-based TypeScript event consumers/workers
12
- - **vanity/** - Webflow marketing site scripts (deprecated)
13
- - **voyager/** - Marketing website for home and landing pageas (Next.js)
14
- - **puppet/** - Puppeteer server for images/PDFs
15
- - **chrome/** - Chrome browser extension
16
- - **docs/** - Docusaurus documentation site
17
- - **zapier/** - Zapier integration
18
-
19
- ## Development Commands
20
-
21
- ### Starting Development Environment
22
- ```bash
23
- # Start all services (requires iTerm2 on macOS)
24
- ./scripts/run-dev.sh
25
-
26
- # Or start individually:
27
- cd api && hivemind Procfile.dev # Rails + AnyCable + Sidekiq + Clockwork
28
- cd web && npm run dev # Next.js dev server
29
- cd electron && npm run dev # Electron with hot reload
30
- cd services && hivemind Procfile.dev # Bun services
31
- ```
32
-
33
- ### Docker Dependencies (api/docker-compose.yml)
34
- ```bash
35
- cd api && docker-compose up -d
36
- # TimescaleDB: localhost:15432
37
- # Redis: localhost:16379
38
- # Kafka: localhost:9092
39
- # MySQL: localhost:13306
40
- ```
41
-
42
- ### Testing
43
- ```bash
44
- # API (RSpec)
45
- cd api && bundle exec rspec
46
- cd api && bundle exec rspec spec/path/to/file_spec.rb # Single file
47
- cd api && bundle exec rspec spec/path/to/file_spec.rb:42 # Single test at line
48
-
49
- # Electron (Jest)
50
- cd electron && npm test
51
- cd electron && npm run test:watch
52
- cd electron && npm run test:coverage
53
-
54
- # Web - no active tests (exits 0)
55
- ```
56
-
57
- ### Building
58
- ```bash
59
- cd api && bundle install && rake db:migrate
60
- cd web && npm run build # Runs gql-gen, tailwind, next build
61
- cd electron && npm run build # Electron Forge make
62
- cd services && bun install
63
- ```
64
-
65
- ### GraphQL Code Generation
66
- ```bash
67
- cd web && npm run build # Includes gql codegen
68
- cd electron && npm run dev # Runs gql codegen as part of dev
69
- ```
70
-
71
- ## Architecture
72
-
73
- ### GraphQL API Structure
74
- The API exposes two GraphQL endpoints:
75
- - **api/v1** - Public API (OAuth consumers, Zapier)
76
- - **private/v1** - Private API (web, electron apps)
77
-
78
- Located at `api/app/graphql/{api,private}/v1/`
79
-
80
- ### Real-time Communication
81
- - **AnyCable** WebSocket server for subscriptions
82
- - ActionCable channels in `api/app/channels/`
83
- - WebSocket config: `api/config/cable.yml` and `api/config/anycable.yml`
84
-
85
- ### Background Jobs
86
- - **Sidekiq** for async job processing (`api/config/sidekiq.yml`)
87
- - **Clockwork** for scheduled jobs (`api/config/clock.rb`)
88
-
89
- ### Event Streaming
90
- - **Kafka** for event publishing/consumption
91
- - Services consume events via `services/consumers/`
92
- - Kafka config: `api/config/initializers/kafka.rb`
93
-
94
- ### Databases
95
- - **Primary PostgreSQL** - Main application data
96
- - **TimescaleDB** - Time-series data (separate connection in `database.yml`)
97
- - **MySQL** - Legacy/external integrations
98
- - **Redis** - Caching, ActionCable, Sidekiq
99
-
100
- ## Style Guidelines
101
-
102
- ### JavaScript/TypeScript
103
- - Use `test()` instead of `it()` in tests
104
- - Use `toBeCalled()` instead of `toHaveBeenCalledWith()` in jest assertions
105
-
106
- ## Key Configuration Files
107
-
108
- - `api/config/database.yml` - Database connections (primary + timescale)
109
- - `api/config/cable.yml` - AnyCable WebSocket config
110
- - `api/Procfile.dev` - Development processes (rails, anycable, sidekiq, clockwork)
111
- - `sol.code-workspace` - VS Code multi-folder workspace
112
- - Each project requires its own `.env` file (not in repo)