@jaimevalasek/aioson 1.4.0 → 1.5.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.
Files changed (199) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/LICENSE +661 -21
  3. package/README.md +3 -1
  4. package/docs/en/squad-dashboard.md +372 -0
  5. package/docs/openclaw-bridge.md +308 -0
  6. package/docs/pt/agentes.md +124 -10
  7. package/docs/pt/cenarios.md +46 -2
  8. package/docs/pt/comandos-cli.md +60 -1
  9. package/docs/pt/inicio-rapido.md +18 -2
  10. package/docs/pt/squad-dashboard.md +373 -0
  11. package/docs/testing/genome-2.0-matrix.md +5 -5
  12. package/docs/testing/genome-2.0-rollout.md +9 -9
  13. package/package.json +2 -2
  14. package/src/backup-local.js +74 -0
  15. package/src/cli.js +98 -0
  16. package/src/commands/backup-local-cmd.js +25 -0
  17. package/src/commands/runtime.js +242 -0
  18. package/src/commands/setup-context.js +7 -2
  19. package/src/commands/squad-daemon.js +209 -0
  20. package/src/commands/squad-dashboard.js +39 -0
  21. package/src/commands/squad-deploy.js +64 -0
  22. package/src/commands/squad-doctor.js +52 -0
  23. package/src/commands/squad-mcp.js +270 -0
  24. package/src/commands/squad-processes.js +56 -0
  25. package/src/commands/squad-recovery.js +42 -0
  26. package/src/commands/squad-roi.js +291 -0
  27. package/src/commands/squad-score.js +250 -0
  28. package/src/commands/squad-status.js +37 -1
  29. package/src/commands/squad-validate.js +62 -1
  30. package/src/commands/squad-webhook.js +160 -0
  31. package/src/commands/squad-worker.js +191 -0
  32. package/src/commands/squad-worktrees.js +75 -0
  33. package/src/commands/web-map.js +70 -0
  34. package/src/commands/web-scrape.js +71 -0
  35. package/src/constants.js +8 -0
  36. package/src/context-writer.js +45 -1
  37. package/src/i18n/messages/en.js +127 -1
  38. package/src/i18n/messages/es.js +117 -0
  39. package/src/i18n/messages/fr.js +117 -0
  40. package/src/i18n/messages/pt-BR.js +126 -1
  41. package/src/lib/webhook-server.js +328 -0
  42. package/src/mcp-connectors/registry.js +602 -0
  43. package/src/runtime-store.js +259 -2
  44. package/src/squad/external-session.js +180 -0
  45. package/src/squad/inter-squad.js +74 -0
  46. package/src/squad/recovery-context.js +201 -0
  47. package/src/squad/worktree-manager.js +114 -0
  48. package/src/squad-daemon.js +490 -0
  49. package/src/squad-dashboard/api.js +223 -0
  50. package/src/squad-dashboard/attachment-handler.js +93 -0
  51. package/src/squad-dashboard/context-monitor.js +157 -0
  52. package/src/squad-dashboard/execution-logs.js +115 -0
  53. package/src/squad-dashboard/hunk-review.js +209 -0
  54. package/src/squad-dashboard/metrics.js +133 -0
  55. package/src/squad-dashboard/process-monitor.js +125 -0
  56. package/src/squad-dashboard/renderer.js +858 -0
  57. package/src/squad-dashboard/server.js +232 -0
  58. package/src/squad-dashboard/styles.js +525 -0
  59. package/src/squad-dashboard/token-tracker.js +99 -0
  60. package/src/web.js +284 -0
  61. package/src/worker-runner.js +339 -0
  62. package/template/.aioson/agents/analyst.md +4 -0
  63. package/template/.aioson/agents/architect.md +4 -0
  64. package/template/.aioson/agents/dev.md +120 -11
  65. package/template/.aioson/agents/deyvin.md +8 -0
  66. package/template/.aioson/agents/neo.md +152 -0
  67. package/template/.aioson/agents/orache.md +17 -0
  68. package/template/.aioson/agents/orchestrator.md +26 -0
  69. package/template/.aioson/agents/product.md +60 -12
  70. package/template/.aioson/agents/qa.md +1 -0
  71. package/template/.aioson/agents/setup.md +63 -19
  72. package/template/.aioson/agents/sheldon.md +603 -0
  73. package/template/.aioson/agents/squad.md +191 -0
  74. package/template/.aioson/agents/tester.md +254 -0
  75. package/template/.aioson/agents/ux-ui.md +12 -0
  76. package/template/.aioson/config.md +6 -0
  77. package/template/.aioson/locales/en/agents/analyst.md +8 -0
  78. package/template/.aioson/locales/en/agents/architect.md +8 -0
  79. package/template/.aioson/locales/en/agents/dev.md +66 -7
  80. package/template/.aioson/locales/en/agents/deyvin.md +8 -0
  81. package/template/.aioson/locales/en/agents/neo.md +8 -0
  82. package/template/.aioson/locales/en/agents/orchestrator.md +26 -0
  83. package/template/.aioson/locales/en/agents/qa.md +49 -0
  84. package/template/.aioson/locales/en/agents/setup.md +2 -1
  85. package/template/.aioson/locales/en/agents/sheldon.md +340 -0
  86. package/template/.aioson/locales/en/agents/ux-ui.md +8 -0
  87. package/template/.aioson/locales/es/agents/analyst.md +8 -0
  88. package/template/.aioson/locales/es/agents/architect.md +8 -0
  89. package/template/.aioson/locales/es/agents/dev.md +66 -7
  90. package/template/.aioson/locales/es/agents/deyvin.md +8 -0
  91. package/template/.aioson/locales/es/agents/neo.md +48 -0
  92. package/template/.aioson/locales/es/agents/orchestrator.md +26 -0
  93. package/template/.aioson/locales/es/agents/qa.md +26 -0
  94. package/template/.aioson/locales/es/agents/setup.md +2 -1
  95. package/template/.aioson/locales/es/agents/sheldon.md +192 -0
  96. package/template/.aioson/locales/es/agents/squad.md +63 -0
  97. package/template/.aioson/locales/es/agents/ux-ui.md +8 -0
  98. package/template/.aioson/locales/fr/agents/analyst.md +8 -0
  99. package/template/.aioson/locales/fr/agents/architect.md +8 -0
  100. package/template/.aioson/locales/fr/agents/dev.md +66 -7
  101. package/template/.aioson/locales/fr/agents/deyvin.md +8 -0
  102. package/template/.aioson/locales/fr/agents/neo.md +48 -0
  103. package/template/.aioson/locales/fr/agents/orchestrator.md +26 -0
  104. package/template/.aioson/locales/fr/agents/qa.md +26 -0
  105. package/template/.aioson/locales/fr/agents/setup.md +2 -1
  106. package/template/.aioson/locales/fr/agents/sheldon.md +192 -0
  107. package/template/.aioson/locales/fr/agents/squad.md +63 -0
  108. package/template/.aioson/locales/fr/agents/ux-ui.md +8 -0
  109. package/template/.aioson/locales/pt-BR/agents/analyst.md +19 -0
  110. package/template/.aioson/locales/pt-BR/agents/architect.md +19 -0
  111. package/template/.aioson/locales/pt-BR/agents/dev.md +75 -12
  112. package/template/.aioson/locales/pt-BR/agents/deyvin.md +8 -0
  113. package/template/.aioson/locales/pt-BR/agents/neo.md +147 -0
  114. package/template/.aioson/locales/pt-BR/agents/orchestrator.md +26 -0
  115. package/template/.aioson/locales/pt-BR/agents/product.md +8 -3
  116. package/template/.aioson/locales/pt-BR/agents/qa.md +60 -0
  117. package/template/.aioson/locales/pt-BR/agents/setup.md +2 -1
  118. package/template/.aioson/locales/pt-BR/agents/sheldon.md +192 -0
  119. package/template/.aioson/locales/pt-BR/agents/squad.md +105 -0
  120. package/template/.aioson/locales/pt-BR/agents/ux-ui.md +8 -0
  121. package/template/.aioson/schemas/squad-blueprint.schema.json +21 -0
  122. package/template/.aioson/schemas/squad-manifest.schema.json +178 -1
  123. package/template/.aioson/skills/design/bold-editorial-ui/SKILL.md +205 -0
  124. package/template/.aioson/skills/design/bold-editorial-ui/references/art-direction.md +338 -0
  125. package/template/.aioson/skills/design/bold-editorial-ui/references/components.md +977 -0
  126. package/template/.aioson/skills/design/bold-editorial-ui/references/dashboards.md +218 -0
  127. package/template/.aioson/skills/design/bold-editorial-ui/references/design-tokens.md +326 -0
  128. package/template/.aioson/skills/design/bold-editorial-ui/references/motion.md +461 -0
  129. package/template/.aioson/skills/design/bold-editorial-ui/references/patterns.md +293 -0
  130. package/template/.aioson/skills/design/bold-editorial-ui/references/websites.md +352 -0
  131. package/template/.aioson/skills/design/clean-saas-ui/SKILL.md +210 -0
  132. package/template/.aioson/skills/design/clean-saas-ui/references/art-direction.md +319 -0
  133. package/template/.aioson/skills/design/clean-saas-ui/references/components.md +365 -0
  134. package/template/.aioson/skills/design/clean-saas-ui/references/dashboards.md +196 -0
  135. package/template/.aioson/skills/design/clean-saas-ui/references/design-tokens.md +244 -0
  136. package/template/.aioson/skills/design/clean-saas-ui/references/motion.md +235 -0
  137. package/template/.aioson/skills/design/clean-saas-ui/references/patterns.md +215 -0
  138. package/template/.aioson/skills/design/clean-saas-ui/references/websites.md +295 -0
  139. package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +55 -9
  140. package/template/.aioson/skills/design/cognitive-core-ui/references/art-direction.md +339 -0
  141. package/template/.aioson/skills/design/cognitive-core-ui/references/components.md +1 -1
  142. package/template/.aioson/skills/design/cognitive-core-ui/references/dashboards.md +100 -0
  143. package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +43 -9
  144. package/template/.aioson/skills/design/cognitive-core-ui/references/motion.md +40 -0
  145. package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +1 -1
  146. package/template/.aioson/skills/design/cognitive-core-ui/references/websites.md +99 -12
  147. package/template/.aioson/skills/design/warm-craft-ui/SKILL.md +209 -0
  148. package/template/.aioson/skills/design/warm-craft-ui/references/art-direction.md +324 -0
  149. package/template/.aioson/skills/design/warm-craft-ui/references/components.md +508 -0
  150. package/template/.aioson/skills/design/warm-craft-ui/references/dashboards.md +223 -0
  151. package/template/.aioson/skills/design/warm-craft-ui/references/design-tokens.md +374 -0
  152. package/template/.aioson/skills/design/warm-craft-ui/references/motion.md +356 -0
  153. package/template/.aioson/skills/design/warm-craft-ui/references/patterns.md +288 -0
  154. package/template/.aioson/skills/design/warm-craft-ui/references/websites.md +289 -0
  155. package/template/.aioson/skills/premium-visual-design/SKILL.md +83 -0
  156. package/template/.aioson/skills/premium-visual-design/components/agent-badge.md +92 -0
  157. package/template/.aioson/skills/premium-visual-design/components/dependency-node.md +102 -0
  158. package/template/.aioson/skills/premium-visual-design/components/mention-autocomplete.md +136 -0
  159. package/template/.aioson/skills/premium-visual-design/components/notification-center.md +136 -0
  160. package/template/.aioson/skills/premium-visual-design/components/review-action-bar.md +188 -0
  161. package/template/.aioson/skills/premium-visual-design/components/team-switcher.md +131 -0
  162. package/template/.aioson/skills/premium-visual-design/patterns/agent-message-thread.md +198 -0
  163. package/template/.aioson/skills/premium-visual-design/patterns/notification-panel.md +275 -0
  164. package/template/.aioson/skills/premium-visual-design/patterns/review-workflow-ui.md +234 -0
  165. package/template/.aioson/skills/premium-visual-design/patterns/task-dependency-graph.md +147 -0
  166. package/template/.aioson/skills/premium-visual-design/tokens/status-extended.md +142 -0
  167. package/template/.aioson/skills/squad/formats/catalog.json +15 -0
  168. package/template/.aioson/skills/squad/formats/content/blog-post.md +47 -0
  169. package/template/.aioson/skills/squad/formats/content/newsletter.md +47 -0
  170. package/template/.aioson/skills/squad/formats/creative/podcast-script.md +43 -0
  171. package/template/.aioson/skills/squad/formats/creative/video-script.md +41 -0
  172. package/template/.aioson/skills/squad/formats/social/instagram-feed.md +42 -0
  173. package/template/.aioson/skills/squad/formats/social/linkedin-post.md +42 -0
  174. package/template/.aioson/skills/squad/formats/social/tiktok.md +39 -0
  175. package/template/.aioson/skills/squad/formats/social/twitter-thread.md +39 -0
  176. package/template/.aioson/skills/squad/formats/social/youtube-long.md +47 -0
  177. package/template/.aioson/skills/squad/formats/social/youtube-shorts.md +39 -0
  178. package/template/.aioson/skills/squad/patterns/multi-platform-pattern.md +108 -0
  179. package/template/.aioson/skills/squad/patterns/persona-based-pattern.md +98 -0
  180. package/template/.aioson/skills/squad/patterns/pipeline-pattern.md +106 -0
  181. package/template/.aioson/skills/squad/patterns/review-loop-pattern.md +81 -0
  182. package/template/.aioson/skills/squad/references/checklist-templates.md +122 -0
  183. package/template/.aioson/skills/squad/references/executor-archetypes.md +123 -0
  184. package/template/.aioson/skills/squad/references/workflow-templates.md +169 -0
  185. package/template/.aioson/skills/static/debugging-protocol.md +42 -0
  186. package/template/.aioson/skills/static/git-worktrees.md +36 -0
  187. package/template/.aioson/tasks/implementation-plan.md +19 -0
  188. package/template/.aioson/tasks/squad-design.md +28 -0
  189. package/template/.aioson/tasks/squad-profile.md +48 -0
  190. package/template/.aioson/tasks/squad-review.md +61 -0
  191. package/template/.aioson/tasks/squad-task-decompose.md +66 -0
  192. package/template/.claude/commands/aioson/agent/neo.md +5 -0
  193. package/template/.claude/commands/aioson/agent/tester.md +5 -0
  194. package/template/.gemini/GEMINI.md +1 -0
  195. package/template/.gemini/commands/aios-neo.toml +4 -0
  196. package/template/.gemini/commands/aios-tester.toml +6 -0
  197. package/template/AGENTS.md +3 -0
  198. package/template/CLAUDE.md +5 -2
  199. package/template/OPENCODE.md +2 -0
@@ -496,6 +496,100 @@ async function openRuntimeDb(targetDir, options = {}) {
496
496
  CREATE INDEX IF NOT EXISTS idx_squad_learnings_status ON squad_learnings(status);
497
497
  CREATE INDEX IF NOT EXISTS idx_project_learnings_type ON project_learnings(type);
498
498
  CREATE INDEX IF NOT EXISTS idx_project_learnings_status ON project_learnings(status);
499
+
500
+ CREATE TABLE IF NOT EXISTS squad_metrics (
501
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
502
+ squad_slug TEXT NOT NULL,
503
+ metric_key TEXT NOT NULL,
504
+ metric_value REAL NOT NULL,
505
+ metric_unit TEXT,
506
+ period TEXT,
507
+ baseline REAL,
508
+ target REAL,
509
+ source TEXT DEFAULT 'manual',
510
+ notes TEXT,
511
+ created_at TEXT DEFAULT (datetime('now')),
512
+ UNIQUE(squad_slug, metric_key, period)
513
+ );
514
+ CREATE INDEX IF NOT EXISTS idx_squad_metrics_squad ON squad_metrics(squad_slug, period DESC);
515
+
516
+ CREATE TABLE IF NOT EXISTS worker_runs (
517
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
518
+ squad_slug TEXT NOT NULL,
519
+ worker_slug TEXT NOT NULL,
520
+ trigger_type TEXT NOT NULL DEFAULT 'manual',
521
+ input_json TEXT,
522
+ output_json TEXT,
523
+ status TEXT DEFAULT 'running',
524
+ error_message TEXT,
525
+ duration_ms INTEGER,
526
+ attempt INTEGER DEFAULT 1,
527
+ created_at TEXT DEFAULT (datetime('now')),
528
+ completed_at TEXT
529
+ );
530
+ CREATE INDEX IF NOT EXISTS idx_worker_runs_squad ON worker_runs(squad_slug, created_at DESC);
531
+
532
+ CREATE TABLE IF NOT EXISTS squad_daemons (
533
+ squad_slug TEXT PRIMARY KEY,
534
+ status TEXT DEFAULT 'stopped',
535
+ pid INTEGER,
536
+ port INTEGER,
537
+ started_at TEXT,
538
+ last_heartbeat TEXT,
539
+ config_json TEXT,
540
+ error_message TEXT
541
+ );
542
+
543
+ CREATE TABLE IF NOT EXISTS mcp_status (
544
+ squad_slug TEXT NOT NULL,
545
+ mcp_slug TEXT NOT NULL,
546
+ connector TEXT NOT NULL,
547
+ status TEXT DEFAULT 'unconfigured',
548
+ last_check TEXT,
549
+ last_error TEXT,
550
+ calls_total INTEGER DEFAULT 0,
551
+ calls_failed INTEGER DEFAULT 0,
552
+ PRIMARY KEY (squad_slug, mcp_slug)
553
+ );
554
+
555
+ CREATE TABLE IF NOT EXISTS workflow_reviews (
556
+ review_id TEXT PRIMARY KEY,
557
+ squad_slug TEXT NOT NULL,
558
+ workflow_slug TEXT NOT NULL,
559
+ phase_id TEXT NOT NULL,
560
+ attempt_number INTEGER DEFAULT 1,
561
+ reviewer_slug TEXT NOT NULL,
562
+ verdict TEXT NOT NULL,
563
+ score REAL,
564
+ feedback TEXT,
565
+ veto_triggered TEXT,
566
+ created_at TEXT DEFAULT (datetime('now'))
567
+ );
568
+ CREATE INDEX IF NOT EXISTS idx_workflow_reviews_squad ON workflow_reviews(squad_slug, workflow_slug);
569
+
570
+ CREATE TABLE IF NOT EXISTS squad_scores (
571
+ squad_slug TEXT NOT NULL,
572
+ dimension TEXT NOT NULL,
573
+ score INTEGER NOT NULL,
574
+ max_score INTEGER NOT NULL,
575
+ details_json TEXT,
576
+ scored_at TEXT DEFAULT (datetime('now')),
577
+ PRIMARY KEY (squad_slug, dimension, scored_at)
578
+ );
579
+ CREATE INDEX IF NOT EXISTS idx_squad_scores_squad ON squad_scores(squad_slug);
580
+
581
+ CREATE TABLE IF NOT EXISTS squad_roi_config (
582
+ squad_slug TEXT PRIMARY KEY,
583
+ pricing_model TEXT DEFAULT 'fixed',
584
+ setup_fee REAL,
585
+ monthly_fee REAL,
586
+ percentage_fee REAL,
587
+ percentage_base TEXT,
588
+ currency TEXT DEFAULT 'BRL',
589
+ contract_months INTEGER DEFAULT 12,
590
+ created_at TEXT DEFAULT (datetime('now')),
591
+ updated_at TEXT DEFAULT (datetime('now'))
592
+ );
499
593
  `);
500
594
 
501
595
  ensureLegacyColumns(db);
@@ -597,6 +691,8 @@ function ensureLegacyColumns(db) {
597
691
  if (!contentItemColumnNames.has('used_skills_json')) {
598
692
  db.exec('ALTER TABLE content_items ADD COLUMN used_skills_json TEXT');
599
693
  }
694
+
695
+ try { db.exec('ALTER TABLE worker_runs ADD COLUMN conversation_id TEXT'); } catch { /* já existe */ }
600
696
  }
601
697
 
602
698
  function insertEvent(db, record) {
@@ -2019,7 +2115,7 @@ function getSquadPlanRounds(db, planSlug) {
2019
2115
  // --- Squad Learnings CRUD ---
2020
2116
 
2021
2117
  function insertSquadLearning(db, options = {}) {
2022
- const learningId = options.learningId || `sl-${slugify(options.squadSlug || 'squad')}-${Date.now()}`;
2118
+ const learningId = options.learningId || `sl-${slugify(options.squadSlug || 'squad')}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
2023
2119
  const now = nowIso();
2024
2120
  db.prepare(`
2025
2121
  INSERT OR REPLACE INTO squad_learnings
@@ -2185,6 +2281,150 @@ function getProjectLearningStats(db) {
2185
2281
  `).all();
2186
2282
  }
2187
2283
 
2284
+ // --- Squad Metrics CRUD ---
2285
+
2286
+ function upsertSquadMetric(db, { squadSlug, metricKey, value, unit, period, baseline, target, source, notes }) {
2287
+ db.prepare(`
2288
+ INSERT INTO squad_metrics (squad_slug, metric_key, metric_value, metric_unit, period, baseline, target, source, notes)
2289
+ VALUES (@squad_slug, @metric_key, @metric_value, @metric_unit, @period, @baseline, @target, @source, @notes)
2290
+ ON CONFLICT(squad_slug, metric_key, period) DO UPDATE SET
2291
+ metric_value = excluded.metric_value,
2292
+ metric_unit = excluded.metric_unit,
2293
+ baseline = COALESCE(excluded.baseline, squad_metrics.baseline),
2294
+ target = COALESCE(excluded.target, squad_metrics.target),
2295
+ source = excluded.source,
2296
+ notes = COALESCE(excluded.notes, squad_metrics.notes)
2297
+ `).run({
2298
+ squad_slug: String(squadSlug).trim(),
2299
+ metric_key: String(metricKey).trim(),
2300
+ metric_value: Number(value),
2301
+ metric_unit: unit ? String(unit).trim() : null,
2302
+ period: period ? String(period).trim() : null,
2303
+ baseline: baseline != null ? Number(baseline) : null,
2304
+ target: target != null ? Number(target) : null,
2305
+ source: source ? String(source).trim() : 'manual',
2306
+ notes: notes ? String(notes).trim() : null
2307
+ });
2308
+ }
2309
+
2310
+ function listSquadMetrics(db, squadSlug, period) {
2311
+ if (period) {
2312
+ return db.prepare(
2313
+ 'SELECT * FROM squad_metrics WHERE squad_slug = ? AND period = ? ORDER BY metric_key ASC'
2314
+ ).all(squadSlug, period);
2315
+ }
2316
+ return db.prepare(
2317
+ 'SELECT * FROM squad_metrics WHERE squad_slug = ? ORDER BY period DESC, metric_key ASC'
2318
+ ).all(squadSlug);
2319
+ }
2320
+
2321
+ function deleteSquadMetric(db, squadSlug, metricKey, period) {
2322
+ db.prepare(
2323
+ 'DELETE FROM squad_metrics WHERE squad_slug = ? AND metric_key = ? AND period = ?'
2324
+ ).run(squadSlug, metricKey, period);
2325
+ }
2326
+
2327
+ // --- Worker Runs CRUD ---
2328
+
2329
+ function insertWorkerRun(db, { squadSlug, workerSlug, triggerType, inputJson, outputJson, status, errorMessage, durationMs, attempt, conversationId }) {
2330
+ const now = nowIso();
2331
+ return db.prepare(`
2332
+ INSERT INTO worker_runs (squad_slug, worker_slug, trigger_type, input_json, output_json, status, error_message, duration_ms, attempt, conversation_id, created_at, completed_at)
2333
+ VALUES (@squad_slug, @worker_slug, @trigger_type, @input_json, @output_json, @status, @error_message, @duration_ms, @attempt, @conversation_id, @created_at, @completed_at)
2334
+ `).run({
2335
+ squad_slug: String(squadSlug).trim(),
2336
+ worker_slug: String(workerSlug).trim(),
2337
+ trigger_type: String(triggerType || 'manual').trim(),
2338
+ input_json: inputJson || null,
2339
+ output_json: outputJson || null,
2340
+ status: String(status || 'running').trim(),
2341
+ error_message: errorMessage || null,
2342
+ duration_ms: durationMs != null ? Number(durationMs) : null,
2343
+ attempt: Number(attempt || 1),
2344
+ conversation_id: conversationId || null,
2345
+ created_at: now,
2346
+ completed_at: (status === 'completed' || status === 'failed') ? now : null
2347
+ });
2348
+ }
2349
+
2350
+ function listWorkerRuns(db, squadSlug, limit = 50) {
2351
+ return db.prepare(
2352
+ 'SELECT * FROM worker_runs WHERE squad_slug = ? ORDER BY created_at DESC, id DESC LIMIT ?'
2353
+ ).all(squadSlug, limit);
2354
+ }
2355
+
2356
+ function getWorkerRunStats(db, squadSlug) {
2357
+ return db.prepare(`
2358
+ SELECT worker_slug, status, COUNT(*) as count,
2359
+ AVG(duration_ms) as avg_duration_ms
2360
+ FROM worker_runs WHERE squad_slug = ?
2361
+ GROUP BY worker_slug, status
2362
+ ORDER BY worker_slug, status
2363
+ `).all(squadSlug);
2364
+ }
2365
+
2366
+ // --- MCP Status CRUD ---
2367
+
2368
+ function upsertMcpStatus(db, { squadSlug, mcpSlug, connector, status, lastError }) {
2369
+ db.prepare(`
2370
+ INSERT INTO mcp_status (squad_slug, mcp_slug, connector, status, last_check, last_error)
2371
+ VALUES (?, ?, ?, ?, datetime('now'), ?)
2372
+ ON CONFLICT(squad_slug, mcp_slug) DO UPDATE SET
2373
+ connector = excluded.connector,
2374
+ status = excluded.status,
2375
+ last_check = excluded.last_check,
2376
+ last_error = excluded.last_error
2377
+ `).run(squadSlug, mcpSlug, connector, status || 'unconfigured', lastError || null);
2378
+ }
2379
+
2380
+ function incrementMcpCalls(db, squadSlug, mcpSlug, failed) {
2381
+ if (failed) {
2382
+ db.prepare(`
2383
+ UPDATE mcp_status SET calls_total = calls_total + 1, calls_failed = calls_failed + 1
2384
+ WHERE squad_slug = ? AND mcp_slug = ?
2385
+ `).run(squadSlug, mcpSlug);
2386
+ } else {
2387
+ db.prepare(`
2388
+ UPDATE mcp_status SET calls_total = calls_total + 1
2389
+ WHERE squad_slug = ? AND mcp_slug = ?
2390
+ `).run(squadSlug, mcpSlug);
2391
+ }
2392
+ }
2393
+
2394
+ function listMcpStatus(db, squadSlug) {
2395
+ return db.prepare('SELECT * FROM mcp_status WHERE squad_slug = ? ORDER BY mcp_slug').all(squadSlug);
2396
+ }
2397
+
2398
+ function getMcpStatus(db, squadSlug, mcpSlug) {
2399
+ return db.prepare('SELECT * FROM mcp_status WHERE squad_slug = ? AND mcp_slug = ?').get(squadSlug, mcpSlug);
2400
+ }
2401
+
2402
+ // --- ROI Config CRUD ---
2403
+
2404
+ function upsertROIConfig(db, { squadSlug, pricingModel, setupFee, monthlyFee, percentageFee, percentageBase, currency, contractMonths }) {
2405
+ db.prepare(`
2406
+ INSERT INTO squad_roi_config (squad_slug, pricing_model, setup_fee, monthly_fee, percentage_fee, percentage_base, currency, contract_months)
2407
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2408
+ ON CONFLICT(squad_slug) DO UPDATE SET
2409
+ pricing_model = excluded.pricing_model,
2410
+ setup_fee = excluded.setup_fee,
2411
+ monthly_fee = excluded.monthly_fee,
2412
+ percentage_fee = excluded.percentage_fee,
2413
+ percentage_base = excluded.percentage_base,
2414
+ currency = excluded.currency,
2415
+ contract_months = excluded.contract_months,
2416
+ updated_at = datetime('now')
2417
+ `).run(squadSlug, pricingModel || 'fixed', setupFee || null, monthlyFee || null, percentageFee || null, percentageBase || null, currency || 'BRL', contractMonths || 12);
2418
+ }
2419
+
2420
+ function getROIConfig(db, squadSlug) {
2421
+ return db.prepare('SELECT * FROM squad_roi_config WHERE squad_slug = ?').get(squadSlug);
2422
+ }
2423
+
2424
+ function deleteROIConfig(db, squadSlug) {
2425
+ return db.prepare('DELETE FROM squad_roi_config WHERE squad_slug = ?').run(squadSlug);
2426
+ }
2427
+
2188
2428
  module.exports = {
2189
2429
  resolveRuntimePaths,
2190
2430
  runtimeStoreExists,
@@ -2261,5 +2501,22 @@ module.exports = {
2261
2501
  updateProjectLearningStatus,
2262
2502
  reinforceProjectLearning,
2263
2503
  promoteProjectLearning,
2264
- getProjectLearningStats
2504
+ getProjectLearningStats,
2505
+ // Squad Metrics CRUD
2506
+ upsertSquadMetric,
2507
+ listSquadMetrics,
2508
+ deleteSquadMetric,
2509
+ // Worker Runs CRUD
2510
+ insertWorkerRun,
2511
+ listWorkerRuns,
2512
+ getWorkerRunStats,
2513
+ // MCP Status CRUD
2514
+ upsertMcpStatus,
2515
+ incrementMcpCalls,
2516
+ listMcpStatus,
2517
+ getMcpStatus,
2518
+ // ROI Config CRUD
2519
+ upsertROIConfig,
2520
+ getROIConfig,
2521
+ deleteROIConfig
2265
2522
  };
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+
6
+ const SESSIONS_DIR = path.join('.aioson', 'sessions');
7
+ const DEFAULT_TTL_HOURS = 24;
8
+
9
+ function sanitizeSessionId(sessionId) {
10
+ // Replace anything that isn't alphanumeric, hyphen, or underscore with a dash
11
+ return String(sessionId)
12
+ .replace(/[^a-zA-Z0-9_-]/g, '-')
13
+ .replace(/-{2,}/g, '-')
14
+ .replace(/^-|-$/g, '')
15
+ .slice(0, 128);
16
+ }
17
+
18
+ function sessionPath(projectDir, sessionId) {
19
+ return path.join(projectDir, SESSIONS_DIR, `${sanitizeSessionId(sessionId)}.json`);
20
+ }
21
+
22
+ async function ensureSessionsDir(projectDir) {
23
+ const dir = path.join(projectDir, SESSIONS_DIR);
24
+ await fs.mkdir(dir, { recursive: true });
25
+ return dir;
26
+ }
27
+
28
+ /**
29
+ * Load a session by ID. Returns null if not found or expired.
30
+ * @param {string} projectDir
31
+ * @param {string} sessionId
32
+ * @param {number} [ttlHours]
33
+ * @returns {Promise<object|null>}
34
+ */
35
+ async function loadSession(projectDir, sessionId, ttlHours = DEFAULT_TTL_HOURS) {
36
+ const filePath = sessionPath(projectDir, sessionId);
37
+ let raw;
38
+ try {
39
+ raw = await fs.readFile(filePath, 'utf8');
40
+ } catch {
41
+ return null;
42
+ }
43
+
44
+ let session;
45
+ try {
46
+ session = JSON.parse(raw);
47
+ } catch {
48
+ return null;
49
+ }
50
+
51
+ // Check TTL
52
+ if (ttlHours > 0 && session.last_active) {
53
+ const idleMs = Date.now() - new Date(session.last_active).getTime();
54
+ if (idleMs > ttlHours * 3_600_000) {
55
+ // Expired — delete and return null
56
+ await fs.unlink(filePath).catch(() => {});
57
+ return null;
58
+ }
59
+ }
60
+
61
+ return session;
62
+ }
63
+
64
+ /**
65
+ * Save (overwrite) a session file atomically.
66
+ * @param {string} projectDir
67
+ * @param {object} session Must have session.session_id
68
+ */
69
+ async function saveSession(projectDir, session) {
70
+ await ensureSessionsDir(projectDir);
71
+ const filePath = sessionPath(projectDir, session.session_id);
72
+ const tmpPath = filePath + '.tmp';
73
+ await fs.writeFile(tmpPath, JSON.stringify(session, null, 2), 'utf8');
74
+ await fs.rename(tmpPath, filePath);
75
+ }
76
+
77
+ /**
78
+ * Append a turn to a session. Creates the session file if it doesn't exist.
79
+ * @param {string} projectDir
80
+ * @param {string} sessionId
81
+ * @param {'user'|'assistant'} role
82
+ * @param {string} content
83
+ * @param {object} [metadata] Merged into session.metadata on first creation
84
+ * @param {number} [ttlHours]
85
+ * @returns {Promise<object>} Updated session
86
+ */
87
+ async function appendTurn(projectDir, sessionId, role, content, metadata = {}, ttlHours = DEFAULT_TTL_HOURS) {
88
+ let session = await loadSession(projectDir, sessionId, ttlHours);
89
+ const now = new Date().toISOString();
90
+
91
+ if (!session) {
92
+ session = {
93
+ session_id: sessionId,
94
+ channel: metadata.channel || 'webhook',
95
+ created_at: now,
96
+ last_active: now,
97
+ turns: [],
98
+ metadata: { ...metadata }
99
+ };
100
+ }
101
+
102
+ session.last_active = now;
103
+ session.turns.push({ role, content, ts: now });
104
+
105
+ // Merge any new metadata fields without overwriting existing ones
106
+ if (metadata && typeof metadata === 'object') {
107
+ for (const [k, v] of Object.entries(metadata)) {
108
+ if (session.metadata[k] === undefined) session.metadata[k] = v;
109
+ }
110
+ }
111
+
112
+ await saveSession(projectDir, session);
113
+ return session;
114
+ }
115
+
116
+ /**
117
+ * Build a context string from session history to inject into the squad input.
118
+ * @param {object} session
119
+ * @param {string} currentInput
120
+ * @returns {string}
121
+ */
122
+ function buildContextualInput(session, currentInput) {
123
+ if (!session || !session.turns || session.turns.length === 0) {
124
+ return currentInput;
125
+ }
126
+
127
+ const history = session.turns
128
+ .map(t => `${t.role}: ${t.content}`)
129
+ .join('\n');
130
+
131
+ return `[Conversation history]\n${history}\n\n[Current message]\n${currentInput}`;
132
+ }
133
+
134
+ /**
135
+ * Delete all session files that have been inactive for longer than ttlHours.
136
+ * @param {string} projectDir
137
+ * @param {number} [ttlHours]
138
+ * @returns {Promise<number>} Number of sessions deleted
139
+ */
140
+ async function cleanExpiredSessions(projectDir, ttlHours = DEFAULT_TTL_HOURS) {
141
+ const dir = path.join(projectDir, SESSIONS_DIR);
142
+ let entries;
143
+ try {
144
+ entries = await fs.readdir(dir, { withFileTypes: true });
145
+ } catch {
146
+ return 0;
147
+ }
148
+
149
+ let deleted = 0;
150
+ const cutoff = Date.now() - ttlHours * 3_600_000;
151
+
152
+ for (const entry of entries) {
153
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
154
+ const filePath = path.join(dir, entry.name);
155
+ try {
156
+ const raw = await fs.readFile(filePath, 'utf8');
157
+ const session = JSON.parse(raw);
158
+ const lastActive = session.last_active ? new Date(session.last_active).getTime() : 0;
159
+ if (lastActive < cutoff) {
160
+ await fs.unlink(filePath);
161
+ deleted++;
162
+ }
163
+ } catch {
164
+ // Corrupt file — remove it
165
+ await fs.unlink(filePath).catch(() => {});
166
+ deleted++;
167
+ }
168
+ }
169
+
170
+ return deleted;
171
+ }
172
+
173
+ module.exports = {
174
+ loadSession,
175
+ saveSession,
176
+ appendTurn,
177
+ buildContextualInput,
178
+ cleanExpiredSessions,
179
+ sanitizeSessionId
180
+ };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+ const { randomUUID } = require('node:crypto');
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { openRuntimeDb, insertWorkerRun } = require('../runtime-store');
6
+
7
+ const INBOX_DIR = (projectDir, squadSlug) =>
8
+ path.join(projectDir, '.aioson', 'squads', squadSlug, 'inbox');
9
+
10
+ async function callSquad({ projectDir, from, to, worker, payload, conversationId, depth = 0 }) {
11
+ // 1. Cascade guard
12
+ if (depth > 5) return { ok: false, error: 'cascade_guard' };
13
+
14
+ conversationId = conversationId || randomUUID();
15
+
16
+ // 2. Resolver porta do squad destino no SQLite (manter DB aberto para log)
17
+ const handle = await openRuntimeDb(projectDir);
18
+ const db = handle?.db;
19
+ const port = db
20
+ ?.prepare("SELECT port FROM squad_daemons WHERE squad_slug = ? AND status = 'running'")
21
+ .get(to)?.port;
22
+
23
+ let result;
24
+
25
+ // 3. Tentar chamada direta
26
+ if (port) {
27
+ try {
28
+ const res = await fetch(`http://127.0.0.1:${port}/webhook/${worker}`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ ...payload, _inter_squad: { from, conversationId, depth: depth + 1 } }),
32
+ signal: AbortSignal.timeout(10000)
33
+ });
34
+ const json = await res.json();
35
+ result = { ok: res.ok, result: json, conversationId };
36
+ } catch {
37
+ // cai para inbox
38
+ }
39
+ }
40
+
41
+ // 4. Enfileirar na inbox do squad destino
42
+ if (!result) {
43
+ const inboxDir = INBOX_DIR(projectDir, to);
44
+ await fs.mkdir(inboxDir, { recursive: true });
45
+ const id = randomUUID();
46
+ await fs.writeFile(
47
+ path.join(inboxDir, `${id}.json`),
48
+ JSON.stringify({ id, from, to, worker, payload, conversationId, depth, created_at: new Date().toISOString() })
49
+ );
50
+ result = { ok: false, error: 'offline_queued', conversationId };
51
+ }
52
+
53
+ // 5. Gravar chamada emitida no runtime store (trigger_type = 'inter-squad')
54
+ if (db) {
55
+ try {
56
+ insertWorkerRun(db, {
57
+ squadSlug: from,
58
+ workerSlug: worker,
59
+ triggerType: 'inter-squad',
60
+ inputJson: JSON.stringify({ to, payload, conversationId }),
61
+ outputJson: result.ok ? JSON.stringify(result.result) : null,
62
+ status: result.ok ? 'completed' : 'failed',
63
+ errorMessage: result.ok ? null : result.error,
64
+ durationMs: 0,
65
+ attempt: 1
66
+ });
67
+ } catch { /* ignore */ }
68
+ db.close();
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ module.exports = { callSquad, INBOX_DIR };