@jaimevalasek/aioson 1.3.0

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 (288) hide show
  1. package/CHANGELOG.md +456 -0
  2. package/CODE_OF_CONDUCT.md +12 -0
  3. package/CONTRIBUTING.md +13 -0
  4. package/LICENSE +21 -0
  5. package/README.md +254 -0
  6. package/bin/aioson.js +4 -0
  7. package/docs/en/cli-reference.md +398 -0
  8. package/docs/en/i18n.md +52 -0
  9. package/docs/en/json-schemas.md +41 -0
  10. package/docs/en/mcp.md +56 -0
  11. package/docs/en/parallel.md +82 -0
  12. package/docs/en/qa-browser.md +339 -0
  13. package/docs/en/release-flow.md +22 -0
  14. package/docs/en/release-notes-template.md +41 -0
  15. package/docs/en/release.md +28 -0
  16. package/docs/en/schemas/agent-prompt.schema.json +17 -0
  17. package/docs/en/schemas/agents.schema.json +32 -0
  18. package/docs/en/schemas/context-validate.schema.json +36 -0
  19. package/docs/en/schemas/doctor.schema.json +89 -0
  20. package/docs/en/schemas/error.schema.json +24 -0
  21. package/docs/en/schemas/i18n-add.schema.json +15 -0
  22. package/docs/en/schemas/index.json +116 -0
  23. package/docs/en/schemas/info.schema.json +39 -0
  24. package/docs/en/schemas/init.schema.json +48 -0
  25. package/docs/en/schemas/install.schema.json +60 -0
  26. package/docs/en/schemas/locale-apply.schema.json +30 -0
  27. package/docs/en/schemas/mcp-doctor.schema.json +95 -0
  28. package/docs/en/schemas/mcp-init.schema.json +122 -0
  29. package/docs/en/schemas/package-test.schema.json +24 -0
  30. package/docs/en/schemas/parallel-assign.schema.json +57 -0
  31. package/docs/en/schemas/parallel-doctor.schema.json +86 -0
  32. package/docs/en/schemas/parallel-init.schema.json +53 -0
  33. package/docs/en/schemas/parallel-status.schema.json +94 -0
  34. package/docs/en/schemas/setup-context.schema.json +39 -0
  35. package/docs/en/schemas/smoke.schema.json +23 -0
  36. package/docs/en/schemas/update.schema.json +48 -0
  37. package/docs/en/schemas/workflow-plan.schema.json +30 -0
  38. package/docs/en/web3.md +54 -0
  39. package/docs/pt/README.md +46 -0
  40. package/docs/pt/advisor-spec.md +335 -0
  41. package/docs/pt/agentes.md +453 -0
  42. package/docs/pt/cenarios.md +1230 -0
  43. package/docs/pt/clientes-ai.md +224 -0
  44. package/docs/pt/comandos-cli.md +511 -0
  45. package/docs/pt/genome-3.0-spec.md +296 -0
  46. package/docs/pt/guia-engineer.md +226 -0
  47. package/docs/pt/inicio-rapido.md +138 -0
  48. package/docs/pt/profiler-system.md +214 -0
  49. package/docs/pt/runtime-observability.md +72 -0
  50. package/docs/pt/squad-genoma.md +777 -0
  51. package/docs/pt/web3.md +797 -0
  52. package/docs/testing/genome-2.0-manual-regression.md +23 -0
  53. package/docs/testing/genome-2.0-matrix.md +36 -0
  54. package/docs/testing/genome-2.0-rollout.md +184 -0
  55. package/package.json +50 -0
  56. package/src/agents.js +56 -0
  57. package/src/cli.js +497 -0
  58. package/src/commands/agents.js +142 -0
  59. package/src/commands/cloud.js +1767 -0
  60. package/src/commands/config.js +90 -0
  61. package/src/commands/context-validate.js +91 -0
  62. package/src/commands/doctor.js +123 -0
  63. package/src/commands/genome-doctor.js +41 -0
  64. package/src/commands/genome-migrate.js +49 -0
  65. package/src/commands/i18n-add.js +56 -0
  66. package/src/commands/info.js +41 -0
  67. package/src/commands/init.js +75 -0
  68. package/src/commands/install.js +68 -0
  69. package/src/commands/locale-apply.js +51 -0
  70. package/src/commands/locale-diff.js +126 -0
  71. package/src/commands/mcp-doctor.js +406 -0
  72. package/src/commands/mcp-init.js +379 -0
  73. package/src/commands/package-e2e.js +273 -0
  74. package/src/commands/parallel-assign.js +403 -0
  75. package/src/commands/parallel-doctor.js +437 -0
  76. package/src/commands/parallel-init.js +249 -0
  77. package/src/commands/parallel-status.js +290 -0
  78. package/src/commands/qa-doctor.js +185 -0
  79. package/src/commands/qa-init.js +161 -0
  80. package/src/commands/qa-report.js +58 -0
  81. package/src/commands/qa-run.js +873 -0
  82. package/src/commands/qa-scan.js +337 -0
  83. package/src/commands/runtime.js +948 -0
  84. package/src/commands/scan-project.js +1107 -0
  85. package/src/commands/setup-context.js +650 -0
  86. package/src/commands/smoke.js +426 -0
  87. package/src/commands/squad-doctor.js +358 -0
  88. package/src/commands/squad-export.js +46 -0
  89. package/src/commands/squad-pipeline.js +97 -0
  90. package/src/commands/squad-repair-genomes.js +39 -0
  91. package/src/commands/squad-status.js +424 -0
  92. package/src/commands/squad-validate.js +230 -0
  93. package/src/commands/test-agents.js +194 -0
  94. package/src/commands/update.js +55 -0
  95. package/src/commands/workflow-next.js +594 -0
  96. package/src/commands/workflow-plan.js +108 -0
  97. package/src/constants.js +314 -0
  98. package/src/context-parse-reason.js +22 -0
  99. package/src/context-writer.js +150 -0
  100. package/src/context.js +217 -0
  101. package/src/detector.js +261 -0
  102. package/src/doctor.js +289 -0
  103. package/src/execution-gateway.js +461 -0
  104. package/src/genome-files.js +198 -0
  105. package/src/genome-format.js +442 -0
  106. package/src/genome-schema.js +215 -0
  107. package/src/genomes/bindings.js +281 -0
  108. package/src/genomes.js +467 -0
  109. package/src/i18n/index.js +103 -0
  110. package/src/i18n/messages/en.js +784 -0
  111. package/src/i18n/messages/es.js +718 -0
  112. package/src/i18n/messages/fr.js +725 -0
  113. package/src/i18n/messages/pt-BR.js +818 -0
  114. package/src/i18n/scaffold.js +64 -0
  115. package/src/installer.js +232 -0
  116. package/src/lib/genomes/compat.js +206 -0
  117. package/src/lib/genomes/migrate.js +90 -0
  118. package/src/lib/squads/genome-repair.js +49 -0
  119. package/src/locales.js +84 -0
  120. package/src/onboarding.js +305 -0
  121. package/src/parser.js +53 -0
  122. package/src/prompt-tool.js +20 -0
  123. package/src/qa-html-report.js +472 -0
  124. package/src/runtime-store.js +1527 -0
  125. package/src/squads/apply-genome.js +21 -0
  126. package/src/squads/genome-binding-service.js +154 -0
  127. package/src/updater.js +32 -0
  128. package/src/utils.js +46 -0
  129. package/src/version.js +50 -0
  130. package/template/.aioson/advisors/.gitkeep +1 -0
  131. package/template/.aioson/agents/analyst.md +225 -0
  132. package/template/.aioson/agents/architect.md +221 -0
  133. package/template/.aioson/agents/dev.md +201 -0
  134. package/template/.aioson/agents/discovery-design-doc.md +196 -0
  135. package/template/.aioson/agents/genoma.md +300 -0
  136. package/template/.aioson/agents/orchestrator.md +107 -0
  137. package/template/.aioson/agents/pm.md +89 -0
  138. package/template/.aioson/agents/product.md +361 -0
  139. package/template/.aioson/agents/profiler-enricher.md +266 -0
  140. package/template/.aioson/agents/profiler-forge.md +188 -0
  141. package/template/.aioson/agents/profiler-researcher.md +245 -0
  142. package/template/.aioson/agents/qa.md +344 -0
  143. package/template/.aioson/agents/setup.md +381 -0
  144. package/template/.aioson/agents/squad.md +837 -0
  145. package/template/.aioson/agents/ux-ui.md +416 -0
  146. package/template/.aioson/config.md +56 -0
  147. package/template/.aioson/context/.gitkeep +0 -0
  148. package/template/.aioson/context/parallel/.gitkeep +0 -0
  149. package/template/.aioson/context/spec.md.template +37 -0
  150. package/template/.aioson/genomas/.gitkeep +0 -0
  151. package/template/.aioson/locales/en/agents/analyst.md +214 -0
  152. package/template/.aioson/locales/en/agents/architect.md +210 -0
  153. package/template/.aioson/locales/en/agents/dev.md +187 -0
  154. package/template/.aioson/locales/en/agents/discovery-design-doc.md +27 -0
  155. package/template/.aioson/locales/en/agents/genoma.md +212 -0
  156. package/template/.aioson/locales/en/agents/orchestrator.md +105 -0
  157. package/template/.aioson/locales/en/agents/pm.md +77 -0
  158. package/template/.aioson/locales/en/agents/product.md +310 -0
  159. package/template/.aioson/locales/en/agents/profiler-enricher.md +5 -0
  160. package/template/.aioson/locales/en/agents/profiler-forge.md +5 -0
  161. package/template/.aioson/locales/en/agents/profiler-researcher.md +5 -0
  162. package/template/.aioson/locales/en/agents/qa.md +214 -0
  163. package/template/.aioson/locales/en/agents/setup.md +342 -0
  164. package/template/.aioson/locales/en/agents/squad.md +247 -0
  165. package/template/.aioson/locales/en/agents/ux-ui.md +320 -0
  166. package/template/.aioson/locales/es/agents/analyst.md +203 -0
  167. package/template/.aioson/locales/es/agents/architect.md +208 -0
  168. package/template/.aioson/locales/es/agents/dev.md +183 -0
  169. package/template/.aioson/locales/es/agents/discovery-design-doc.md +19 -0
  170. package/template/.aioson/locales/es/agents/genoma.md +102 -0
  171. package/template/.aioson/locales/es/agents/orchestrator.md +108 -0
  172. package/template/.aioson/locales/es/agents/pm.md +81 -0
  173. package/template/.aioson/locales/es/agents/product.md +310 -0
  174. package/template/.aioson/locales/es/agents/profiler-enricher.md +5 -0
  175. package/template/.aioson/locales/es/agents/profiler-forge.md +5 -0
  176. package/template/.aioson/locales/es/agents/profiler-researcher.md +5 -0
  177. package/template/.aioson/locales/es/agents/qa.md +163 -0
  178. package/template/.aioson/locales/es/agents/setup.md +347 -0
  179. package/template/.aioson/locales/es/agents/squad.md +247 -0
  180. package/template/.aioson/locales/es/agents/ux-ui.md +201 -0
  181. package/template/.aioson/locales/fr/agents/analyst.md +203 -0
  182. package/template/.aioson/locales/fr/agents/architect.md +208 -0
  183. package/template/.aioson/locales/fr/agents/dev.md +183 -0
  184. package/template/.aioson/locales/fr/agents/discovery-design-doc.md +19 -0
  185. package/template/.aioson/locales/fr/agents/genoma.md +102 -0
  186. package/template/.aioson/locales/fr/agents/orchestrator.md +108 -0
  187. package/template/.aioson/locales/fr/agents/pm.md +81 -0
  188. package/template/.aioson/locales/fr/agents/product.md +310 -0
  189. package/template/.aioson/locales/fr/agents/profiler-enricher.md +5 -0
  190. package/template/.aioson/locales/fr/agents/profiler-forge.md +5 -0
  191. package/template/.aioson/locales/fr/agents/profiler-researcher.md +5 -0
  192. package/template/.aioson/locales/fr/agents/qa.md +163 -0
  193. package/template/.aioson/locales/fr/agents/setup.md +347 -0
  194. package/template/.aioson/locales/fr/agents/squad.md +247 -0
  195. package/template/.aioson/locales/fr/agents/ux-ui.md +201 -0
  196. package/template/.aioson/locales/pt-BR/agents/analyst.md +217 -0
  197. package/template/.aioson/locales/pt-BR/agents/architect.md +213 -0
  198. package/template/.aioson/locales/pt-BR/agents/dev.md +198 -0
  199. package/template/.aioson/locales/pt-BR/agents/discovery-design-doc.md +198 -0
  200. package/template/.aioson/locales/pt-BR/agents/genoma.md +297 -0
  201. package/template/.aioson/locales/pt-BR/agents/orchestrator.md +108 -0
  202. package/template/.aioson/locales/pt-BR/agents/pm.md +81 -0
  203. package/template/.aioson/locales/pt-BR/agents/product.md +316 -0
  204. package/template/.aioson/locales/pt-BR/agents/profiler-enricher.md +5 -0
  205. package/template/.aioson/locales/pt-BR/agents/profiler-forge.md +5 -0
  206. package/template/.aioson/locales/pt-BR/agents/profiler-researcher.md +5 -0
  207. package/template/.aioson/locales/pt-BR/agents/qa.md +217 -0
  208. package/template/.aioson/locales/pt-BR/agents/setup.md +371 -0
  209. package/template/.aioson/locales/pt-BR/agents/squad.md +772 -0
  210. package/template/.aioson/locales/pt-BR/agents/ux-ui.md +322 -0
  211. package/template/.aioson/mcp/servers.md +24 -0
  212. package/template/.aioson/profiler-reports/.gitkeep +1 -0
  213. package/template/.aioson/schemas/content-blueprint.schema.json +30 -0
  214. package/template/.aioson/schemas/genome-meta.schema.json +150 -0
  215. package/template/.aioson/schemas/genome.schema.json +115 -0
  216. package/template/.aioson/schemas/readiness.schema.json +27 -0
  217. package/template/.aioson/schemas/squad-blueprint.schema.json +172 -0
  218. package/template/.aioson/schemas/squad-manifest.schema.json +276 -0
  219. package/template/.aioson/skills/dynamic/README.md +30 -0
  220. package/template/.aioson/skills/dynamic/cardano-docs.md +16 -0
  221. package/template/.aioson/skills/dynamic/ethereum-docs.md +17 -0
  222. package/template/.aioson/skills/dynamic/flux-ui-docs.md +13 -0
  223. package/template/.aioson/skills/dynamic/laravel-docs.md +41 -0
  224. package/template/.aioson/skills/dynamic/npm-packages.md +16 -0
  225. package/template/.aioson/skills/dynamic/solana-docs.md +16 -0
  226. package/template/.aioson/skills/references/premium-command-center-ui/master-application-prompt.md +79 -0
  227. package/template/.aioson/skills/references/premium-command-center-ui/operational-ux-playbook.md +253 -0
  228. package/template/.aioson/skills/references/premium-command-center-ui/quality-validation-checklist.md +82 -0
  229. package/template/.aioson/skills/references/premium-command-center-ui/visual-system-and-component-patterns.md +270 -0
  230. package/template/.aioson/skills/static/django-patterns.md +342 -0
  231. package/template/.aioson/skills/static/fastapi-patterns.md +344 -0
  232. package/template/.aioson/skills/static/filament-patterns.md +267 -0
  233. package/template/.aioson/skills/static/flux-ui-components.md +262 -0
  234. package/template/.aioson/skills/static/git-conventions.md +227 -0
  235. package/template/.aioson/skills/static/interface-design.md +372 -0
  236. package/template/.aioson/skills/static/jetstream-setup.md +200 -0
  237. package/template/.aioson/skills/static/laravel-conventions.md +491 -0
  238. package/template/.aioson/skills/static/nextjs-patterns.md +321 -0
  239. package/template/.aioson/skills/static/node-express-patterns.md +317 -0
  240. package/template/.aioson/skills/static/node-typescript-patterns.md +282 -0
  241. package/template/.aioson/skills/static/premium-command-center-ui.md +190 -0
  242. package/template/.aioson/skills/static/rails-conventions.md +307 -0
  243. package/template/.aioson/skills/static/react-motion-patterns.md +577 -0
  244. package/template/.aioson/skills/static/static-html-patterns.md +1935 -0
  245. package/template/.aioson/skills/static/tall-stack-patterns.md +286 -0
  246. package/template/.aioson/skills/static/ui-ux-modern.md +75 -0
  247. package/template/.aioson/skills/static/web3-cardano-patterns.md +337 -0
  248. package/template/.aioson/skills/static/web3-ethereum-patterns.md +310 -0
  249. package/template/.aioson/skills/static/web3-security-checklist.md +284 -0
  250. package/template/.aioson/skills/static/web3-solana-patterns.md +324 -0
  251. package/template/.aioson/squads/.artisan/.gitkeep +0 -0
  252. package/template/.aioson/squads/.gitkeep +0 -0
  253. package/template/.aioson/squads/memory.md +5 -0
  254. package/template/.aioson/tasks/squad-analyze.md +83 -0
  255. package/template/.aioson/tasks/squad-create.md +99 -0
  256. package/template/.aioson/tasks/squad-design.md +100 -0
  257. package/template/.aioson/tasks/squad-export.md +20 -0
  258. package/template/.aioson/tasks/squad-extend.md +68 -0
  259. package/template/.aioson/tasks/squad-pipeline.md +122 -0
  260. package/template/.aioson/tasks/squad-repair.md +85 -0
  261. package/template/.aioson/tasks/squad-validate.md +58 -0
  262. package/template/.aioson/templates/squads/content-basic/template.json +21 -0
  263. package/template/.aioson/templates/squads/media-channel/template.json +24 -0
  264. package/template/.aioson/templates/squads/research-analysis/template.json +22 -0
  265. package/template/.aioson/templates/squads/software-delivery/template.json +21 -0
  266. package/template/.claude/commands/aioson/analyst.md +5 -0
  267. package/template/.claude/commands/aioson/architect.md +5 -0
  268. package/template/.claude/commands/aioson/dev.md +5 -0
  269. package/template/.claude/commands/aioson/orchestrator.md +5 -0
  270. package/template/.claude/commands/aioson/pm.md +5 -0
  271. package/template/.claude/commands/aioson/qa.md +5 -0
  272. package/template/.claude/commands/aioson/setup.md +5 -0
  273. package/template/.claude/commands/aioson/ux-ui.md +5 -0
  274. package/template/.gemini/GEMINI.md +10 -0
  275. package/template/.gemini/commands/aios-analyst.toml +4 -0
  276. package/template/.gemini/commands/aios-architect.toml +7 -0
  277. package/template/.gemini/commands/aios-dev.toml +8 -0
  278. package/template/.gemini/commands/aios-discovery-design-doc.toml +4 -0
  279. package/template/.gemini/commands/aios-orchestrator.toml +8 -0
  280. package/template/.gemini/commands/aios-pm.toml +8 -0
  281. package/template/.gemini/commands/aios-product.toml +4 -0
  282. package/template/.gemini/commands/aios-qa.toml +6 -0
  283. package/template/.gemini/commands/aios-setup.toml +3 -0
  284. package/template/.gemini/commands/aios-ux-ui.toml +8 -0
  285. package/template/AGENTS.md +67 -0
  286. package/template/CLAUDE.md +31 -0
  287. package/template/OPENCODE.md +24 -0
  288. package/template/aioson-models.json +40 -0
@@ -0,0 +1,1527 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const Database = require('better-sqlite3');
6
+ const { ensureDir, exists } = require('./utils');
7
+ const {
8
+ attachBindingsToExecutors,
9
+ flattenGenomeBindings,
10
+ mergeGenomeBindings
11
+ } = require('./genomes/bindings');
12
+
13
+ const RUNTIME_DIR = path.join('.aioson', 'runtime');
14
+ const DB_FILE = 'aios.sqlite';
15
+ const SESSIONS_DIR = '.sessions';
16
+ const VALID_STATUSES = new Set(['queued', 'running', 'completed', 'failed']);
17
+ const VALID_TASK_STATUSES = new Set(['queued', 'running', 'completed', 'failed']);
18
+
19
+ function slugify(value) {
20
+ return String(value || 'run')
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, '-')
23
+ .replace(/^-+|-+$/g, '')
24
+ .slice(0, 60) || 'run';
25
+ }
26
+
27
+ function nowIso() {
28
+ return new Date().toISOString();
29
+ }
30
+
31
+ function resolveRuntimePaths(targetDir) {
32
+ const runtimeDir = path.join(targetDir, RUNTIME_DIR);
33
+ return {
34
+ runtimeDir,
35
+ dbPath: path.join(runtimeDir, DB_FILE)
36
+ };
37
+ }
38
+
39
+ async function runtimeStoreExists(targetDir) {
40
+ const { dbPath } = resolveRuntimePaths(targetDir);
41
+ return exists(dbPath);
42
+ }
43
+
44
+ async function openRuntimeDb(targetDir, options = {}) {
45
+ const { runtimeDir, dbPath } = resolveRuntimePaths(targetDir);
46
+ const mustExist = Boolean(options.mustExist);
47
+
48
+ if (mustExist && !(await exists(dbPath))) {
49
+ return null;
50
+ }
51
+
52
+ await ensureDir(runtimeDir);
53
+
54
+ const db = new Database(dbPath);
55
+ db.pragma('journal_mode = WAL');
56
+ db.pragma('foreign_keys = ON');
57
+
58
+ db.exec(`
59
+ CREATE TABLE IF NOT EXISTS squads (
60
+ squad_slug TEXT PRIMARY KEY,
61
+ name TEXT NOT NULL,
62
+ mode TEXT NOT NULL DEFAULT 'content',
63
+ mission TEXT,
64
+ goal TEXT,
65
+ status TEXT NOT NULL DEFAULT 'active',
66
+ visibility TEXT NOT NULL DEFAULT 'private',
67
+ manifest_json TEXT,
68
+ context_json TEXT,
69
+ package_dir TEXT,
70
+ agents_dir TEXT,
71
+ output_dir TEXT,
72
+ logs_dir TEXT,
73
+ media_dir TEXT,
74
+ latest_session_path TEXT,
75
+ created_at TEXT NOT NULL,
76
+ updated_at TEXT NOT NULL
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS squad_executors (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ squad_slug TEXT NOT NULL,
82
+ executor_slug TEXT NOT NULL,
83
+ title TEXT,
84
+ role TEXT,
85
+ file_path TEXT,
86
+ skills_json TEXT,
87
+ genomes_json TEXT,
88
+ created_at TEXT NOT NULL,
89
+ updated_at TEXT NOT NULL,
90
+ UNIQUE (squad_slug, executor_slug),
91
+ FOREIGN KEY (squad_slug) REFERENCES squads(squad_slug) ON DELETE CASCADE
92
+ );
93
+
94
+ CREATE TABLE IF NOT EXISTS squad_skills (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ squad_slug TEXT NOT NULL,
97
+ skill_slug TEXT NOT NULL,
98
+ title TEXT,
99
+ description TEXT,
100
+ UNIQUE (squad_slug, skill_slug),
101
+ FOREIGN KEY (squad_slug) REFERENCES squads(squad_slug) ON DELETE CASCADE
102
+ );
103
+
104
+ CREATE TABLE IF NOT EXISTS squad_mcps (
105
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
106
+ squad_slug TEXT NOT NULL,
107
+ mcp_slug TEXT NOT NULL,
108
+ required INTEGER NOT NULL DEFAULT 0,
109
+ purpose TEXT,
110
+ UNIQUE (squad_slug, mcp_slug),
111
+ FOREIGN KEY (squad_slug) REFERENCES squads(squad_slug) ON DELETE CASCADE
112
+ );
113
+
114
+ CREATE TABLE IF NOT EXISTS squad_genomes (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ squad_slug TEXT NOT NULL,
117
+ genome_slug TEXT NOT NULL,
118
+ scope_type TEXT NOT NULL DEFAULT 'squad',
119
+ agent_slug TEXT,
120
+ UNIQUE (squad_slug, genome_slug, scope_type, agent_slug),
121
+ FOREIGN KEY (squad_slug) REFERENCES squads(squad_slug) ON DELETE CASCADE
122
+ );
123
+
124
+ CREATE TABLE IF NOT EXISTS tasks (
125
+ task_key TEXT PRIMARY KEY,
126
+ squad_slug TEXT,
127
+ session_key TEXT,
128
+ title TEXT NOT NULL,
129
+ goal TEXT,
130
+ status TEXT NOT NULL,
131
+ created_by TEXT,
132
+ created_at TEXT NOT NULL,
133
+ updated_at TEXT NOT NULL,
134
+ finished_at TEXT
135
+ );
136
+
137
+ CREATE TABLE IF NOT EXISTS agent_runs (
138
+ run_key TEXT PRIMARY KEY,
139
+ task_key TEXT,
140
+ agent_name TEXT NOT NULL,
141
+ agent_kind TEXT NOT NULL DEFAULT 'official',
142
+ squad_slug TEXT,
143
+ session_key TEXT,
144
+ source TEXT NOT NULL DEFAULT 'direct',
145
+ workflow_id TEXT,
146
+ workflow_stage TEXT,
147
+ parent_run_key TEXT,
148
+ title TEXT,
149
+ status TEXT NOT NULL,
150
+ summary TEXT,
151
+ used_skills_json TEXT,
152
+ output_path TEXT,
153
+ started_at TEXT NOT NULL,
154
+ updated_at TEXT NOT NULL,
155
+ finished_at TEXT,
156
+ FOREIGN KEY (task_key) REFERENCES tasks(task_key) ON DELETE SET NULL
157
+ );
158
+
159
+ CREATE TABLE IF NOT EXISTS agent_events (
160
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
161
+ run_key TEXT NOT NULL,
162
+ event_type TEXT NOT NULL,
163
+ message TEXT NOT NULL DEFAULT '',
164
+ payload_json TEXT,
165
+ created_at TEXT NOT NULL,
166
+ FOREIGN KEY (run_key) REFERENCES agent_runs(run_key) ON DELETE CASCADE
167
+ );
168
+
169
+ CREATE TABLE IF NOT EXISTS execution_events (
170
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
171
+ task_key TEXT,
172
+ run_key TEXT,
173
+ agent_name TEXT,
174
+ agent_kind TEXT,
175
+ squad_slug TEXT,
176
+ session_key TEXT,
177
+ source TEXT,
178
+ workflow_id TEXT,
179
+ workflow_stage TEXT,
180
+ parent_run_key TEXT,
181
+ event_type TEXT NOT NULL,
182
+ phase TEXT,
183
+ status TEXT,
184
+ tool_name TEXT,
185
+ message TEXT NOT NULL DEFAULT '',
186
+ payload_json TEXT,
187
+ sequence_no INTEGER NOT NULL DEFAULT 1,
188
+ parent_event_id INTEGER,
189
+ created_at TEXT NOT NULL,
190
+ FOREIGN KEY (task_key) REFERENCES tasks(task_key) ON DELETE SET NULL,
191
+ FOREIGN KEY (run_key) REFERENCES agent_runs(run_key) ON DELETE CASCADE
192
+ );
193
+
194
+ CREATE TABLE IF NOT EXISTS artifacts (
195
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
196
+ task_key TEXT,
197
+ run_key TEXT,
198
+ squad_slug TEXT,
199
+ agent_name TEXT,
200
+ kind TEXT NOT NULL,
201
+ title TEXT,
202
+ file_path TEXT NOT NULL,
203
+ created_at TEXT NOT NULL,
204
+ FOREIGN KEY (task_key) REFERENCES tasks(task_key) ON DELETE SET NULL,
205
+ FOREIGN KEY (run_key) REFERENCES agent_runs(run_key) ON DELETE SET NULL
206
+ );
207
+
208
+ CREATE TABLE IF NOT EXISTS content_items (
209
+ content_key TEXT PRIMARY KEY,
210
+ task_key TEXT,
211
+ run_key TEXT,
212
+ squad_slug TEXT NOT NULL,
213
+ session_key TEXT,
214
+ title TEXT NOT NULL,
215
+ content_type TEXT NOT NULL,
216
+ layout_type TEXT NOT NULL DEFAULT 'document',
217
+ status TEXT NOT NULL DEFAULT 'completed',
218
+ summary TEXT,
219
+ blueprint_slug TEXT,
220
+ used_skills_json TEXT,
221
+ payload_json TEXT,
222
+ json_path TEXT,
223
+ html_path TEXT,
224
+ created_by_agent TEXT,
225
+ created_at TEXT NOT NULL,
226
+ updated_at TEXT NOT NULL,
227
+ FOREIGN KEY (task_key) REFERENCES tasks(task_key) ON DELETE SET NULL,
228
+ FOREIGN KEY (run_key) REFERENCES agent_runs(run_key) ON DELETE SET NULL
229
+ );
230
+
231
+ CREATE INDEX IF NOT EXISTS idx_squads_updated ON squads(updated_at DESC);
232
+ CREATE INDEX IF NOT EXISTS idx_squad_executors_squad ON squad_executors(squad_slug, updated_at DESC);
233
+ CREATE INDEX IF NOT EXISTS idx_squad_skills_squad ON squad_skills(squad_slug);
234
+ CREATE INDEX IF NOT EXISTS idx_squad_mcps_squad ON squad_mcps(squad_slug);
235
+ CREATE INDEX IF NOT EXISTS idx_squad_genomes_squad ON squad_genomes(squad_slug);
236
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status, updated_at DESC);
237
+ CREATE INDEX IF NOT EXISTS idx_tasks_squad ON tasks(squad_slug, updated_at DESC);
238
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_status ON agent_runs(status, updated_at DESC);
239
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_task ON agent_runs(task_key, updated_at DESC);
240
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_squad ON agent_runs(squad_slug, updated_at DESC);
241
+ CREATE INDEX IF NOT EXISTS idx_agent_events_run ON agent_events(run_key, created_at DESC);
242
+ CREATE INDEX IF NOT EXISTS idx_execution_events_run ON execution_events(run_key, sequence_no DESC, created_at DESC);
243
+ CREATE INDEX IF NOT EXISTS idx_execution_events_task ON execution_events(task_key, created_at DESC);
244
+ CREATE INDEX IF NOT EXISTS idx_execution_events_created ON execution_events(created_at DESC);
245
+ CREATE INDEX IF NOT EXISTS idx_artifacts_task ON artifacts(task_key, created_at DESC);
246
+ CREATE INDEX IF NOT EXISTS idx_content_items_squad ON content_items(squad_slug, updated_at DESC);
247
+ CREATE INDEX IF NOT EXISTS idx_content_items_task ON content_items(task_key, updated_at DESC);
248
+
249
+ CREATE TABLE IF NOT EXISTS squad_analyses (
250
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
251
+ squad_slug TEXT NOT NULL,
252
+ coverage_json TEXT,
253
+ suggestions_json TEXT,
254
+ metrics_json TEXT,
255
+ created_at TEXT NOT NULL,
256
+ FOREIGN KEY (squad_slug) REFERENCES squads(squad_slug)
257
+ );
258
+
259
+ CREATE INDEX IF NOT EXISTS idx_squad_analyses_squad ON squad_analyses(squad_slug, created_at DESC);
260
+
261
+ CREATE TABLE IF NOT EXISTS squad_ports (
262
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
263
+ squad_slug TEXT NOT NULL,
264
+ port_type TEXT NOT NULL CHECK(port_type IN ('input', 'output')),
265
+ port_key TEXT NOT NULL,
266
+ data_type TEXT DEFAULT 'any',
267
+ description TEXT,
268
+ required INTEGER DEFAULT 0,
269
+ content_blueprint_slug TEXT,
270
+ FOREIGN KEY (squad_slug) REFERENCES squads(squad_slug),
271
+ UNIQUE(squad_slug, port_type, port_key)
272
+ );
273
+
274
+ CREATE TABLE IF NOT EXISTS squad_pipelines (
275
+ slug TEXT PRIMARY KEY,
276
+ name TEXT NOT NULL,
277
+ description TEXT,
278
+ status TEXT DEFAULT 'draft' CHECK(status IN ('draft', 'active', 'paused', 'archived')),
279
+ trigger_mode TEXT DEFAULT 'manual' CHECK(trigger_mode IN ('manual', 'on_output', 'scheduled')),
280
+ created_at TEXT NOT NULL,
281
+ updated_at TEXT NOT NULL
282
+ );
283
+
284
+ CREATE TABLE IF NOT EXISTS pipeline_nodes (
285
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
286
+ pipeline_slug TEXT NOT NULL,
287
+ squad_slug TEXT NOT NULL,
288
+ position_x REAL DEFAULT 0,
289
+ position_y REAL DEFAULT 0,
290
+ config_json TEXT,
291
+ FOREIGN KEY (pipeline_slug) REFERENCES squad_pipelines(slug),
292
+ FOREIGN KEY (squad_slug) REFERENCES squads(squad_slug),
293
+ UNIQUE(pipeline_slug, squad_slug)
294
+ );
295
+
296
+ CREATE TABLE IF NOT EXISTS pipeline_edges (
297
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
298
+ pipeline_slug TEXT NOT NULL,
299
+ source_squad TEXT NOT NULL,
300
+ source_port TEXT NOT NULL,
301
+ target_squad TEXT NOT NULL,
302
+ target_port TEXT NOT NULL,
303
+ transform_json TEXT,
304
+ FOREIGN KEY (pipeline_slug) REFERENCES squad_pipelines(slug)
305
+ );
306
+
307
+ CREATE TABLE IF NOT EXISTS squad_handoffs (
308
+ id TEXT PRIMARY KEY,
309
+ pipeline_slug TEXT,
310
+ from_squad TEXT NOT NULL,
311
+ from_port TEXT NOT NULL,
312
+ to_squad TEXT NOT NULL,
313
+ to_port TEXT NOT NULL,
314
+ payload_json TEXT,
315
+ status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'consumed', 'failed', 'expired')),
316
+ created_at TEXT NOT NULL,
317
+ consumed_at TEXT,
318
+ FOREIGN KEY (from_squad) REFERENCES squads(squad_slug),
319
+ FOREIGN KEY (to_squad) REFERENCES squads(squad_slug)
320
+ );
321
+
322
+ CREATE INDEX IF NOT EXISTS idx_squad_pipelines_status ON squad_pipelines(status, updated_at DESC);
323
+ CREATE INDEX IF NOT EXISTS idx_pipeline_nodes_pipeline ON pipeline_nodes(pipeline_slug);
324
+ CREATE INDEX IF NOT EXISTS idx_pipeline_edges_pipeline ON pipeline_edges(pipeline_slug);
325
+ CREATE INDEX IF NOT EXISTS idx_squad_handoffs_to ON squad_handoffs(to_squad, status, created_at DESC);
326
+
327
+ CREATE TABLE IF NOT EXISTS artisan_squads (
328
+ id TEXT PRIMARY KEY,
329
+ slug TEXT NOT NULL UNIQUE,
330
+ title TEXT NOT NULL,
331
+ status TEXT DEFAULT 'draft' CHECK(status IN ('draft', 'refining', 'ready', 'created', 'archived')),
332
+ domain TEXT,
333
+ goal TEXT,
334
+ mode TEXT DEFAULT 'content',
335
+ prd_markdown TEXT,
336
+ summary TEXT,
337
+ confidence REAL DEFAULT 0,
338
+ tags_json TEXT DEFAULT '[]',
339
+ created_at TEXT NOT NULL,
340
+ updated_at TEXT NOT NULL
341
+ );
342
+
343
+ CREATE TABLE IF NOT EXISTS artisan_messages (
344
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
345
+ artisan_id TEXT NOT NULL,
346
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
347
+ content TEXT NOT NULL,
348
+ created_at TEXT NOT NULL,
349
+ FOREIGN KEY (artisan_id) REFERENCES artisan_squads(id)
350
+ );
351
+
352
+ CREATE INDEX IF NOT EXISTS idx_artisan_squads_status ON artisan_squads(status, updated_at DESC);
353
+ CREATE INDEX IF NOT EXISTS idx_artisan_messages_artisan ON artisan_messages(artisan_id, created_at ASC);
354
+ `);
355
+
356
+ ensureLegacyColumns(db);
357
+
358
+ return { db, dbPath, runtimeDir };
359
+ }
360
+
361
+ function createRunKey(agentName) {
362
+ return `${slugify(agentName)}-${Date.now()}`;
363
+ }
364
+
365
+ function createTaskKey(title) {
366
+ return `task-${slugify(title)}-${Date.now()}`;
367
+ }
368
+
369
+ function normalizeStatus(value, fallback) {
370
+ const candidate = String(value || fallback || '')
371
+ .trim()
372
+ .toLowerCase();
373
+ return VALID_STATUSES.has(candidate) ? candidate : fallback;
374
+ }
375
+
376
+ function normalizeTaskStatus(value, fallback) {
377
+ const candidate = String(value || fallback || '')
378
+ .trim()
379
+ .toLowerCase();
380
+ return VALID_TASK_STATUSES.has(candidate) ? candidate : fallback;
381
+ }
382
+
383
+ function ensureLegacyColumns(db) {
384
+ const agentRunColumns = db.prepare('PRAGMA table_info(agent_runs)').all();
385
+ const agentRunColumnNames = new Set(agentRunColumns.map((column) => column.name));
386
+
387
+ if (!agentRunColumnNames.has('task_key')) {
388
+ db.exec('ALTER TABLE agent_runs ADD COLUMN task_key TEXT');
389
+ }
390
+
391
+ if (!agentRunColumnNames.has('used_skills_json')) {
392
+ db.exec('ALTER TABLE agent_runs ADD COLUMN used_skills_json TEXT');
393
+ }
394
+
395
+ if (!agentRunColumnNames.has('source')) {
396
+ db.exec("ALTER TABLE agent_runs ADD COLUMN source TEXT NOT NULL DEFAULT 'direct'");
397
+ }
398
+
399
+ if (!agentRunColumnNames.has('workflow_id')) {
400
+ db.exec('ALTER TABLE agent_runs ADD COLUMN workflow_id TEXT');
401
+ }
402
+
403
+ if (!agentRunColumnNames.has('workflow_stage')) {
404
+ db.exec('ALTER TABLE agent_runs ADD COLUMN workflow_stage TEXT');
405
+ }
406
+
407
+ if (!agentRunColumnNames.has('parent_run_key')) {
408
+ db.exec('ALTER TABLE agent_runs ADD COLUMN parent_run_key TEXT');
409
+ }
410
+
411
+ const squadColumns = db.prepare('PRAGMA table_info(squads)').all();
412
+ const squadColumnNames = new Set(squadColumns.map((column) => column.name));
413
+
414
+ if (!squadColumnNames.has('context_json')) {
415
+ db.exec('ALTER TABLE squads ADD COLUMN context_json TEXT');
416
+ }
417
+
418
+ if (!squadColumnNames.has('mode')) {
419
+ db.exec("ALTER TABLE squads ADD COLUMN mode TEXT NOT NULL DEFAULT 'content'");
420
+ }
421
+
422
+ if (!squadColumnNames.has('package_dir')) {
423
+ db.exec('ALTER TABLE squads ADD COLUMN package_dir TEXT');
424
+ }
425
+
426
+ const contentItemColumns = db.prepare('PRAGMA table_info(content_items)').all();
427
+ const contentItemColumnNames = new Set(contentItemColumns.map((column) => column.name));
428
+
429
+ if (!contentItemColumnNames.has('blueprint_slug')) {
430
+ db.exec('ALTER TABLE content_items ADD COLUMN blueprint_slug TEXT');
431
+ }
432
+
433
+ if (!contentItemColumnNames.has('used_skills_json')) {
434
+ db.exec('ALTER TABLE content_items ADD COLUMN used_skills_json TEXT');
435
+ }
436
+ }
437
+
438
+ function insertEvent(db, record) {
439
+ db.prepare(`
440
+ INSERT INTO agent_events (run_key, event_type, message, payload_json, created_at)
441
+ VALUES (@run_key, @event_type, @message, @payload_json, @created_at)
442
+ `).run(record);
443
+ }
444
+
445
+ function getRunContext(db, runKey) {
446
+ return db
447
+ .prepare(`
448
+ SELECT
449
+ run_key,
450
+ task_key,
451
+ agent_name,
452
+ agent_kind,
453
+ squad_slug,
454
+ session_key,
455
+ source,
456
+ workflow_id,
457
+ workflow_stage,
458
+ parent_run_key,
459
+ status,
460
+ summary,
461
+ title
462
+ FROM agent_runs
463
+ WHERE run_key = ?
464
+ `)
465
+ .get(runKey);
466
+ }
467
+
468
+ function nextExecutionSequence(db, runKey) {
469
+ if (!runKey) return 1;
470
+ const row = db
471
+ .prepare('SELECT COALESCE(MAX(sequence_no), 0) AS max_sequence FROM execution_events WHERE run_key = ?')
472
+ .get(runKey);
473
+ return Number(row?.max_sequence || 0) + 1;
474
+ }
475
+
476
+ function insertExecutionEvent(db, record) {
477
+ db.prepare(`
478
+ INSERT INTO execution_events (
479
+ task_key, run_key, agent_name, agent_kind, squad_slug, session_key,
480
+ source, workflow_id, workflow_stage, parent_run_key,
481
+ event_type, phase, status, tool_name, message, payload_json,
482
+ sequence_no, parent_event_id, created_at
483
+ ) VALUES (
484
+ @task_key, @run_key, @agent_name, @agent_kind, @squad_slug, @session_key,
485
+ @source, @workflow_id, @workflow_stage, @parent_run_key,
486
+ @event_type, @phase, @status, @tool_name, @message, @payload_json,
487
+ @sequence_no, @parent_event_id, @created_at
488
+ )
489
+ `).run(record);
490
+ }
491
+
492
+ function appendRunEvent(db, options) {
493
+ const run = getRunContext(db, options.runKey);
494
+ if (!run) {
495
+ throw new Error(`Run not found: ${options.runKey}`);
496
+ }
497
+
498
+ const now = options.createdAt || nowIso();
499
+ const payloadJson = options.payload ? JSON.stringify(options.payload) : null;
500
+
501
+ insertEvent(db, {
502
+ run_key: run.run_key,
503
+ event_type: String(options.eventType || 'update'),
504
+ message: String(options.message || ''),
505
+ payload_json: payloadJson,
506
+ created_at: now
507
+ });
508
+
509
+ insertExecutionEvent(db, {
510
+ task_key: run.task_key,
511
+ run_key: run.run_key,
512
+ agent_name: run.agent_name,
513
+ agent_kind: run.agent_kind,
514
+ squad_slug: run.squad_slug,
515
+ session_key: run.session_key,
516
+ source: run.source,
517
+ workflow_id: run.workflow_id,
518
+ workflow_stage: run.workflow_stage,
519
+ parent_run_key: run.parent_run_key,
520
+ event_type: String(options.eventType || 'update'),
521
+ phase: options.phase ? String(options.phase).trim() : null,
522
+ status: options.status ? String(options.status).trim() : run.status || null,
523
+ tool_name: options.toolName ? String(options.toolName).trim() : null,
524
+ message: String(options.message || ''),
525
+ payload_json: payloadJson,
526
+ sequence_no: nextExecutionSequence(db, run.run_key),
527
+ parent_event_id: options.parentEventId || null,
528
+ created_at: now
529
+ });
530
+ }
531
+
532
+ function startTask(db, options) {
533
+ const now = nowIso();
534
+ const taskKey = String(options.taskKey || createTaskKey(options.title));
535
+ const status = normalizeTaskStatus(options.status, 'running');
536
+
537
+ db.prepare(`
538
+ INSERT INTO tasks (
539
+ task_key, squad_slug, session_key, title, goal, status, created_by,
540
+ created_at, updated_at, finished_at
541
+ ) VALUES (
542
+ @task_key, @squad_slug, @session_key, @title, @goal, @status, @created_by,
543
+ @created_at, @updated_at, @finished_at
544
+ )
545
+ `).run({
546
+ task_key: taskKey,
547
+ squad_slug: options.squadSlug ? String(options.squadSlug).trim() : null,
548
+ session_key: options.sessionKey ? String(options.sessionKey).trim() : null,
549
+ title: String(options.title).trim(),
550
+ goal: options.goal ? String(options.goal).trim() : null,
551
+ status,
552
+ created_by: options.createdBy ? String(options.createdBy).trim() : null,
553
+ created_at: now,
554
+ updated_at: now,
555
+ finished_at: status === 'completed' || status === 'failed' ? now : null
556
+ });
557
+
558
+ return taskKey;
559
+ }
560
+
561
+ function updateTask(db, options) {
562
+ const existing = db.prepare('SELECT task_key, status FROM tasks WHERE task_key = ?').get(options.taskKey);
563
+ if (!existing) {
564
+ throw new Error(`Task not found: ${options.taskKey}`);
565
+ }
566
+
567
+ const now = nowIso();
568
+ const nextStatus = normalizeTaskStatus(options.status, existing.status || 'running');
569
+
570
+ db.prepare(`
571
+ UPDATE tasks
572
+ SET
573
+ status = @status,
574
+ goal = COALESCE(@goal, goal),
575
+ updated_at = @updated_at,
576
+ finished_at = CASE
577
+ WHEN @status IN ('completed', 'failed') THEN @updated_at
578
+ ELSE finished_at
579
+ END
580
+ WHERE task_key = @task_key
581
+ `).run({
582
+ task_key: String(options.taskKey),
583
+ status: nextStatus,
584
+ goal: options.goal ? String(options.goal).trim() : null,
585
+ updated_at: now
586
+ });
587
+
588
+ return nextStatus;
589
+ }
590
+
591
+ function attachArtifact(db, options) {
592
+ const now = nowIso();
593
+ db.prepare(`
594
+ INSERT INTO artifacts (
595
+ task_key, run_key, squad_slug, agent_name, kind, title, file_path, created_at
596
+ ) VALUES (
597
+ @task_key, @run_key, @squad_slug, @agent_name, @kind, @title, @file_path, @created_at
598
+ )
599
+ `).run({
600
+ task_key: options.taskKey ? String(options.taskKey) : null,
601
+ run_key: options.runKey ? String(options.runKey) : null,
602
+ squad_slug: options.squadSlug ? String(options.squadSlug) : null,
603
+ agent_name: options.agentName ? String(options.agentName) : null,
604
+ kind: String(options.kind || inferArtifactKind(options.filePath || '')).trim(),
605
+ title: options.title ? String(options.title).trim() : null,
606
+ file_path: String(options.filePath).trim(),
607
+ created_at: now
608
+ });
609
+ }
610
+
611
+ function upsertContentItem(db, options) {
612
+ const now = nowIso();
613
+ const contentKey = String(options.contentKey || createTaskKey(options.title || 'content')).trim();
614
+ const usedSkillsJson = normalizeStringArray(options.usedSkills).length > 0 ? JSON.stringify(normalizeStringArray(options.usedSkills)) : null;
615
+
616
+ db.prepare(`
617
+ INSERT INTO content_items (
618
+ content_key, task_key, run_key, squad_slug, session_key, title, content_type, layout_type,
619
+ status, summary, blueprint_slug, used_skills_json, payload_json, json_path, html_path, created_by_agent, created_at, updated_at
620
+ ) VALUES (
621
+ @content_key, @task_key, @run_key, @squad_slug, @session_key, @title, @content_type, @layout_type,
622
+ @status, @summary, @blueprint_slug, @used_skills_json, @payload_json, @json_path, @html_path, @created_by_agent, @created_at, @updated_at
623
+ )
624
+ ON CONFLICT(content_key) DO UPDATE SET
625
+ task_key = excluded.task_key,
626
+ run_key = excluded.run_key,
627
+ squad_slug = excluded.squad_slug,
628
+ session_key = excluded.session_key,
629
+ title = excluded.title,
630
+ content_type = excluded.content_type,
631
+ layout_type = excluded.layout_type,
632
+ status = excluded.status,
633
+ summary = excluded.summary,
634
+ blueprint_slug = excluded.blueprint_slug,
635
+ used_skills_json = excluded.used_skills_json,
636
+ payload_json = excluded.payload_json,
637
+ json_path = excluded.json_path,
638
+ html_path = excluded.html_path,
639
+ created_by_agent = excluded.created_by_agent,
640
+ updated_at = excluded.updated_at
641
+ `).run({
642
+ content_key: contentKey,
643
+ task_key: options.taskKey ? String(options.taskKey).trim() : null,
644
+ run_key: options.runKey ? String(options.runKey).trim() : null,
645
+ squad_slug: String(options.squadSlug).trim(),
646
+ session_key: options.sessionKey ? String(options.sessionKey).trim() : null,
647
+ title: String(options.title || contentKey).trim(),
648
+ content_type: String(options.contentType || 'content').trim(),
649
+ layout_type: String(options.layoutType || 'document').trim(),
650
+ status: String(options.status || 'completed').trim(),
651
+ summary: options.summary ? String(options.summary).trim() : null,
652
+ blueprint_slug: options.blueprintSlug ? String(options.blueprintSlug).trim() : null,
653
+ used_skills_json: usedSkillsJson,
654
+ payload_json:
655
+ options.payload && typeof options.payload === 'object'
656
+ ? JSON.stringify(options.payload)
657
+ : options.payloadJson
658
+ ? String(options.payloadJson)
659
+ : null,
660
+ json_path: options.jsonPath ? String(options.jsonPath).trim() : null,
661
+ html_path: options.htmlPath ? String(options.htmlPath).trim() : null,
662
+ created_by_agent: options.createdByAgent ? String(options.createdByAgent).trim() : null,
663
+ created_at: now,
664
+ updated_at: now
665
+ });
666
+
667
+ return contentKey;
668
+ }
669
+
670
+ function normalizeArray(value) {
671
+ return Array.isArray(value) ? value : [];
672
+ }
673
+
674
+ function normalizeStringArray(value) {
675
+ const values = Array.isArray(value)
676
+ ? value
677
+ : typeof value === 'string'
678
+ ? value.split(',')
679
+ : [];
680
+
681
+ return Array.from(
682
+ new Set(
683
+ values
684
+ .map((entry) => String(entry || '').trim())
685
+ .filter(Boolean)
686
+ )
687
+ );
688
+ }
689
+
690
+ function parseJsonArray(value) {
691
+ if (!value) return [];
692
+ try {
693
+ return normalizeStringArray(JSON.parse(value));
694
+ } catch {
695
+ return [];
696
+ }
697
+ }
698
+
699
+ function upsertSquadManifest(db, options) {
700
+ const now = nowIso();
701
+ const slug = String(options.slug).trim();
702
+ const manifest = options.manifest && typeof options.manifest === 'object' ? options.manifest : {};
703
+ const context =
704
+ options.context && typeof options.context === 'object'
705
+ ? options.context
706
+ : manifest.context && typeof manifest.context === 'object'
707
+ ? manifest.context
708
+ : null;
709
+ const skills = normalizeArray(manifest.skills);
710
+ const mcps = normalizeArray(manifest.mcps);
711
+ const executors = normalizeArray(manifest.executors);
712
+ const genomeBindings = mergeGenomeBindings({
713
+ blueprintBindings: manifest.genomeBindings,
714
+ manifestBindings: manifest.genomeBindings || manifest.genomes,
715
+ legacyExecutors: executors
716
+ });
717
+ const resolvedExecutors = attachBindingsToExecutors(executors, genomeBindings);
718
+ const genomes = flattenGenomeBindings(genomeBindings);
719
+
720
+ db.prepare(`
721
+ INSERT INTO squads (
722
+ squad_slug, name, mode, mission, goal, status, visibility, manifest_json,
723
+ context_json,
724
+ package_dir, agents_dir, output_dir, logs_dir, media_dir, latest_session_path,
725
+ created_at, updated_at
726
+ ) VALUES (
727
+ @squad_slug, @name, @mode, @mission, @goal, @status, @visibility, @manifest_json,
728
+ @context_json,
729
+ @package_dir, @agents_dir, @output_dir, @logs_dir, @media_dir, @latest_session_path,
730
+ @created_at, @updated_at
731
+ )
732
+ ON CONFLICT(squad_slug) DO UPDATE SET
733
+ name = excluded.name,
734
+ mode = excluded.mode,
735
+ mission = excluded.mission,
736
+ goal = excluded.goal,
737
+ status = excluded.status,
738
+ visibility = excluded.visibility,
739
+ manifest_json = excluded.manifest_json,
740
+ context_json = excluded.context_json,
741
+ package_dir = excluded.package_dir,
742
+ agents_dir = excluded.agents_dir,
743
+ output_dir = excluded.output_dir,
744
+ logs_dir = excluded.logs_dir,
745
+ media_dir = excluded.media_dir,
746
+ latest_session_path = excluded.latest_session_path,
747
+ updated_at = excluded.updated_at
748
+ `).run({
749
+ squad_slug: slug,
750
+ name: String(options.name || manifest.name || slug).trim(),
751
+ mode: String(options.mode || manifest.mode || 'content').trim(),
752
+ mission: options.mission ? String(options.mission).trim() : manifest.mission ? String(manifest.mission).trim() : null,
753
+ goal: options.goal ? String(options.goal).trim() : manifest.goal ? String(manifest.goal).trim() : null,
754
+ status: String(options.status || 'active').trim(),
755
+ visibility: String(options.visibility || manifest.visibility || 'private').trim(),
756
+ manifest_json: JSON.stringify(manifest),
757
+ context_json: context ? JSON.stringify(context) : null,
758
+ package_dir: options.packageDir ? String(options.packageDir).trim() : manifest?.package?.rootDir ? String(manifest.package.rootDir).trim() : null,
759
+ agents_dir: options.agentsDir ? String(options.agentsDir).trim() : null,
760
+ output_dir: options.outputDir ? String(options.outputDir).trim() : null,
761
+ logs_dir: options.logsDir ? String(options.logsDir).trim() : null,
762
+ media_dir: options.mediaDir ? String(options.mediaDir).trim() : null,
763
+ latest_session_path: options.latestSessionPath ? String(options.latestSessionPath).trim() : null,
764
+ created_at: now,
765
+ updated_at: now
766
+ });
767
+
768
+ db.prepare('DELETE FROM squad_executors WHERE squad_slug = ?').run(slug);
769
+ db.prepare('DELETE FROM squad_skills WHERE squad_slug = ?').run(slug);
770
+ db.prepare('DELETE FROM squad_mcps WHERE squad_slug = ?').run(slug);
771
+ db.prepare('DELETE FROM squad_genomes WHERE squad_slug = ?').run(slug);
772
+
773
+ const insertExecutor = db.prepare(`
774
+ INSERT INTO squad_executors (
775
+ squad_slug, executor_slug, title, role, file_path, skills_json, genomes_json,
776
+ created_at, updated_at
777
+ ) VALUES (
778
+ @squad_slug, @executor_slug, @title, @role, @file_path, @skills_json, @genomes_json,
779
+ @created_at, @updated_at
780
+ )
781
+ `);
782
+
783
+ for (const executor of resolvedExecutors) {
784
+ insertExecutor.run({
785
+ squad_slug: slug,
786
+ executor_slug: String(executor.slug || '').trim(),
787
+ title: executor.title ? String(executor.title).trim() : null,
788
+ role: executor.role ? String(executor.role).trim() : null,
789
+ file_path: executor.file ? String(executor.file).trim() : null,
790
+ skills_json: JSON.stringify(normalizeArray(executor.skills)),
791
+ genomes_json: JSON.stringify(normalizeArray(executor.genomes)),
792
+ created_at: now,
793
+ updated_at: now
794
+ });
795
+ }
796
+
797
+ const insertSkill = db.prepare(`
798
+ INSERT INTO squad_skills (squad_slug, skill_slug, title, description)
799
+ VALUES (@squad_slug, @skill_slug, @title, @description)
800
+ `);
801
+
802
+ for (const skill of skills) {
803
+ insertSkill.run({
804
+ squad_slug: slug,
805
+ skill_slug: String(skill.slug || '').trim(),
806
+ title: skill.title ? String(skill.title).trim() : null,
807
+ description: skill.description ? String(skill.description).trim() : null
808
+ });
809
+ }
810
+
811
+ const insertMcp = db.prepare(`
812
+ INSERT INTO squad_mcps (squad_slug, mcp_slug, required, purpose)
813
+ VALUES (@squad_slug, @mcp_slug, @required, @purpose)
814
+ `);
815
+
816
+ for (const mcp of mcps) {
817
+ insertMcp.run({
818
+ squad_slug: slug,
819
+ mcp_slug: String(mcp.slug || '').trim(),
820
+ required: mcp.required ? 1 : 0,
821
+ purpose: mcp.purpose ? String(mcp.purpose).trim() : null
822
+ });
823
+ }
824
+
825
+ const insertGenome = db.prepare(`
826
+ INSERT INTO squad_genomes (squad_slug, genome_slug, scope_type, agent_slug)
827
+ VALUES (@squad_slug, @genome_slug, @scope_type, @agent_slug)
828
+ `);
829
+
830
+ for (const genome of genomes) {
831
+ insertGenome.run({
832
+ squad_slug: slug,
833
+ genome_slug: String(genome.slug || '').trim(),
834
+ scope_type: String(genome.scope || 'squad').trim(),
835
+ agent_slug: genome.agentSlug ? String(genome.agentSlug).trim() : null
836
+ });
837
+ }
838
+
839
+ return slug;
840
+ }
841
+
842
+ // ─── Pipeline CRUD ────────────────────────────────────────────────────────────
843
+
844
+ function upsertPipeline(db, options) {
845
+ const now = nowIso();
846
+ const slug = String(options.slug).trim();
847
+ db.prepare(`
848
+ INSERT INTO squad_pipelines (slug, name, description, status, trigger_mode, created_at, updated_at)
849
+ VALUES (@slug, @name, @description, @status, @trigger_mode, @created_at, @updated_at)
850
+ ON CONFLICT(slug) DO UPDATE SET
851
+ name = excluded.name,
852
+ description = COALESCE(excluded.description, description),
853
+ status = excluded.status,
854
+ trigger_mode = excluded.trigger_mode,
855
+ updated_at = excluded.updated_at
856
+ `).run({
857
+ slug,
858
+ name: String(options.name || slug).trim(),
859
+ description: options.description ? String(options.description).trim() : null,
860
+ status: String(options.status || 'draft').trim(),
861
+ trigger_mode: String(options.triggerMode || 'manual').trim(),
862
+ created_at: now,
863
+ updated_at: now
864
+ });
865
+ return slug;
866
+ }
867
+
868
+ function addPipelineNode(db, options) {
869
+ db.prepare(`
870
+ INSERT INTO pipeline_nodes (pipeline_slug, squad_slug, position_x, position_y, config_json)
871
+ VALUES (@pipeline_slug, @squad_slug, @position_x, @position_y, @config_json)
872
+ ON CONFLICT(pipeline_slug, squad_slug) DO UPDATE SET
873
+ position_x = excluded.position_x,
874
+ position_y = excluded.position_y,
875
+ config_json = COALESCE(excluded.config_json, config_json)
876
+ `).run({
877
+ pipeline_slug: String(options.pipelineSlug).trim(),
878
+ squad_slug: String(options.squadSlug).trim(),
879
+ position_x: Number(options.positionX || 0),
880
+ position_y: Number(options.positionY || 0),
881
+ config_json: options.config ? JSON.stringify(options.config) : null
882
+ });
883
+ }
884
+
885
+ function updateNodePosition(db, options) {
886
+ db.prepare(`
887
+ UPDATE pipeline_nodes SET position_x = @position_x, position_y = @position_y
888
+ WHERE pipeline_slug = @pipeline_slug AND squad_slug = @squad_slug
889
+ `).run({
890
+ pipeline_slug: String(options.pipelineSlug).trim(),
891
+ squad_slug: String(options.squadSlug).trim(),
892
+ position_x: Number(options.positionX || 0),
893
+ position_y: Number(options.positionY || 0)
894
+ });
895
+ }
896
+
897
+ function addPipelineEdge(db, options) {
898
+ db.prepare(`
899
+ INSERT INTO pipeline_edges (pipeline_slug, source_squad, source_port, target_squad, target_port, transform_json)
900
+ VALUES (@pipeline_slug, @source_squad, @source_port, @target_squad, @target_port, @transform_json)
901
+ `).run({
902
+ pipeline_slug: String(options.pipelineSlug).trim(),
903
+ source_squad: String(options.sourceSquad).trim(),
904
+ source_port: String(options.sourcePort).trim(),
905
+ target_squad: String(options.targetSquad).trim(),
906
+ target_port: String(options.targetPort).trim(),
907
+ transform_json: options.transform ? JSON.stringify(options.transform) : null
908
+ });
909
+ }
910
+
911
+ function removePipelineEdge(db, id) {
912
+ db.prepare('DELETE FROM pipeline_edges WHERE id = ?').run(id);
913
+ }
914
+
915
+ function getPipelineDAG(db, pipelineSlug) {
916
+ const pipeline = db.prepare('SELECT * FROM squad_pipelines WHERE slug = ?').get(pipelineSlug);
917
+ if (!pipeline) return null;
918
+ const nodes = db.prepare('SELECT * FROM pipeline_nodes WHERE pipeline_slug = ?').all(pipelineSlug);
919
+ const edges = db.prepare('SELECT * FROM pipeline_edges WHERE pipeline_slug = ?').all(pipelineSlug);
920
+ return { pipeline, nodes, edges };
921
+ }
922
+
923
+ function listPipelines(db) {
924
+ return db.prepare('SELECT * FROM squad_pipelines ORDER BY updated_at DESC').all();
925
+ }
926
+
927
+ function upsertSquadPorts(db, squadSlug, ports) {
928
+ db.prepare('DELETE FROM squad_ports WHERE squad_slug = ?').run(squadSlug);
929
+ const insert = db.prepare(`
930
+ INSERT INTO squad_ports (squad_slug, port_type, port_key, data_type, description, required, content_blueprint_slug)
931
+ VALUES (@squad_slug, @port_type, @port_key, @data_type, @description, @required, @content_blueprint_slug)
932
+ `);
933
+ const all = [...(ports.inputs || []).map(p => ({ ...p, type: 'input' })), ...(ports.outputs || []).map(p => ({ ...p, type: 'output' }))];
934
+ for (const port of all) {
935
+ insert.run({
936
+ squad_slug: String(squadSlug).trim(),
937
+ port_type: port.type,
938
+ port_key: String(port.key).trim(),
939
+ data_type: String(port.dataType || 'any').trim(),
940
+ description: port.description ? String(port.description).trim() : null,
941
+ required: port.required ? 1 : 0,
942
+ content_blueprint_slug: port.contentBlueprintSlug ? String(port.contentBlueprintSlug).trim() : null
943
+ });
944
+ }
945
+ }
946
+
947
+ function getTopologicalOrder(db, pipelineSlug) {
948
+ const nodes = db.prepare('SELECT squad_slug FROM pipeline_nodes WHERE pipeline_slug = ?').all(pipelineSlug);
949
+ const edges = db.prepare('SELECT source_squad, target_squad FROM pipeline_edges WHERE pipeline_slug = ?').all(pipelineSlug);
950
+
951
+ const slugs = nodes.map(n => n.squad_slug);
952
+ const inDegree = Object.fromEntries(slugs.map(s => [s, 0]));
953
+ const adj = Object.fromEntries(slugs.map(s => [s, []]));
954
+
955
+ for (const { source_squad, target_squad } of edges) {
956
+ if (adj[source_squad] !== undefined && inDegree[target_squad] !== undefined) {
957
+ adj[source_squad].push(target_squad);
958
+ inDegree[target_squad]++;
959
+ }
960
+ }
961
+
962
+ const queue = slugs.filter(s => inDegree[s] === 0);
963
+ const order = [];
964
+
965
+ while (queue.length > 0) {
966
+ const node = queue.shift();
967
+ order.push(node);
968
+ for (const neighbor of (adj[node] || [])) {
969
+ inDegree[neighbor]--;
970
+ if (inDegree[neighbor] === 0) queue.push(neighbor);
971
+ }
972
+ }
973
+
974
+ return order.length === slugs.length ? order : null; // null = cycle detected
975
+ }
976
+
977
+ // ─── Artisan CRUD ─────────────────────────────────────────────────────────────
978
+
979
+ function createArtisanSquad(db, options) {
980
+ const now = nowIso();
981
+ const id = options.id || `artisan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
982
+ const slug = options.slug || id;
983
+ db.prepare(`
984
+ INSERT INTO artisan_squads (id, slug, title, status, domain, goal, mode, prd_markdown, summary, confidence, tags_json, created_at, updated_at)
985
+ VALUES (@id, @slug, @title, @status, @domain, @goal, @mode, @prd_markdown, @summary, @confidence, @tags_json, @created_at, @updated_at)
986
+ `).run({
987
+ id, slug,
988
+ title: options.title || 'Nova ideia',
989
+ status: 'draft',
990
+ domain: options.domain || null,
991
+ goal: options.goal || null,
992
+ mode: options.mode || 'content',
993
+ prd_markdown: options.prdMarkdown || null,
994
+ summary: options.summary || null,
995
+ confidence: options.confidence || 0,
996
+ tags_json: JSON.stringify(options.tags || []),
997
+ created_at: now, updated_at: now
998
+ });
999
+ return id;
1000
+ }
1001
+
1002
+ function updateArtisanSquad(db, id, updates) {
1003
+ const now = nowIso();
1004
+ const existing = db.prepare('SELECT id FROM artisan_squads WHERE id = ?').get(id);
1005
+ if (!existing) throw new Error(`Artisan squad not found: ${id}`);
1006
+ const fields = [];
1007
+ const values = { id, updated_at: now };
1008
+ const allowed = ['title', 'status', 'domain', 'goal', 'mode', 'prd_markdown', 'summary', 'confidence', 'tags_json'];
1009
+ for (const key of allowed) {
1010
+ if (Object.prototype.hasOwnProperty.call(updates, key)) {
1011
+ fields.push(`${key} = @${key}`);
1012
+ values[key] = updates[key];
1013
+ }
1014
+ }
1015
+ if (fields.length === 0) return;
1016
+ db.prepare(`UPDATE artisan_squads SET ${fields.join(', ')}, updated_at = @updated_at WHERE id = @id`).run(values);
1017
+ }
1018
+
1019
+ function getArtisanSquad(db, id) {
1020
+ return db.prepare('SELECT * FROM artisan_squads WHERE id = ?').get(id) || null;
1021
+ }
1022
+
1023
+ function listArtisanSquads(db) {
1024
+ return db.prepare('SELECT * FROM artisan_squads ORDER BY updated_at DESC').all();
1025
+ }
1026
+
1027
+ function deleteArtisanSquad(db, id) {
1028
+ db.prepare('DELETE FROM artisan_messages WHERE artisan_id = ?').run(id);
1029
+ db.prepare('DELETE FROM artisan_squads WHERE id = ?').run(id);
1030
+ }
1031
+
1032
+ function addArtisanMessage(db, artisanId, role, content) {
1033
+ const now = nowIso();
1034
+ db.prepare(`
1035
+ INSERT INTO artisan_messages (artisan_id, role, content, created_at)
1036
+ VALUES (@artisan_id, @role, @content, @created_at)
1037
+ `).run({ artisan_id: artisanId, role, content, created_at: now });
1038
+ }
1039
+
1040
+ function getArtisanMessages(db, artisanId) {
1041
+ return db.prepare('SELECT * FROM artisan_messages WHERE artisan_id = ? ORDER BY created_at ASC').all(artisanId);
1042
+ }
1043
+
1044
+ function insertSquadAnalysis(db, options) {
1045
+ const now = nowIso();
1046
+ db.prepare(`
1047
+ INSERT INTO squad_analyses (squad_slug, coverage_json, suggestions_json, metrics_json, created_at)
1048
+ VALUES (@squad_slug, @coverage_json, @suggestions_json, @metrics_json, @created_at)
1049
+ `).run({
1050
+ squad_slug: String(options.slug).trim(),
1051
+ coverage_json: JSON.stringify(options.coverage || {}),
1052
+ suggestions_json: JSON.stringify(options.suggestions || []),
1053
+ metrics_json: JSON.stringify(options.metrics || {}),
1054
+ created_at: now
1055
+ });
1056
+ }
1057
+
1058
+ function inferArtifactKind(filePath) {
1059
+ if (/\.html?$/i.test(filePath)) return 'html';
1060
+ if (/\.md$/i.test(filePath)) return 'markdown';
1061
+ return 'file';
1062
+ }
1063
+
1064
+ function startRun(db, options) {
1065
+ const now = nowIso();
1066
+ const runKey = String(options.runKey || createRunKey(options.agentName));
1067
+ const status = normalizeStatus(options.status, 'running');
1068
+ const agentKind = String(options.agentKind || (options.squadSlug ? 'squad' : 'official')).trim();
1069
+ const taskKey = options.taskKey ? String(options.taskKey).trim() : null;
1070
+ const source = String(options.source || 'direct').trim() || 'direct';
1071
+ const usedSkillsJson = normalizeStringArray(options.usedSkills).length > 0 ? JSON.stringify(normalizeStringArray(options.usedSkills)) : null;
1072
+
1073
+ if (taskKey) {
1074
+ const taskExists = db.prepare('SELECT task_key FROM tasks WHERE task_key = ?').get(taskKey);
1075
+ if (!taskExists) {
1076
+ throw new Error(`Task not found: ${taskKey}`);
1077
+ }
1078
+ }
1079
+
1080
+ db.prepare(`
1081
+ INSERT INTO agent_runs (
1082
+ run_key, task_key, agent_name, agent_kind, squad_slug, session_key, source,
1083
+ workflow_id, workflow_stage, parent_run_key, title,
1084
+ status, summary, used_skills_json, output_path, started_at, updated_at, finished_at
1085
+ ) VALUES (
1086
+ @run_key, @task_key, @agent_name, @agent_kind, @squad_slug, @session_key, @source,
1087
+ @workflow_id, @workflow_stage, @parent_run_key, @title,
1088
+ @status, @summary, @used_skills_json, @output_path, @started_at, @updated_at, @finished_at
1089
+ )
1090
+ `).run({
1091
+ run_key: runKey,
1092
+ task_key: taskKey,
1093
+ agent_name: String(options.agentName).trim(),
1094
+ agent_kind: agentKind,
1095
+ squad_slug: options.squadSlug ? String(options.squadSlug).trim() : null,
1096
+ session_key: options.sessionKey ? String(options.sessionKey).trim() : null,
1097
+ source,
1098
+ workflow_id: options.workflowId ? String(options.workflowId).trim() : null,
1099
+ workflow_stage: options.workflowStage ? String(options.workflowStage).trim() : null,
1100
+ parent_run_key: options.parentRunKey ? String(options.parentRunKey).trim() : null,
1101
+ title: options.title ? String(options.title).trim() : null,
1102
+ status,
1103
+ summary: options.summary ? String(options.summary).trim() : null,
1104
+ used_skills_json: usedSkillsJson,
1105
+ output_path: options.outputPath ? String(options.outputPath).trim() : null,
1106
+ started_at: now,
1107
+ updated_at: now,
1108
+ finished_at: status === 'completed' || status === 'failed' ? now : null
1109
+ });
1110
+
1111
+ appendRunEvent(db, {
1112
+ runKey,
1113
+ eventType: 'start',
1114
+ phase: options.phase || 'run',
1115
+ status,
1116
+ message: String(options.message || options.title || 'Agent started'),
1117
+ payload: options.payload,
1118
+ createdAt: now
1119
+ });
1120
+
1121
+ return runKey;
1122
+ }
1123
+
1124
+ function updateRun(db, options) {
1125
+ const now = nowIso();
1126
+ const existing = db
1127
+ .prepare('SELECT run_key, status, used_skills_json, source, workflow_id, workflow_stage, parent_run_key FROM agent_runs WHERE run_key = ?')
1128
+ .get(options.runKey);
1129
+ if (!existing) {
1130
+ throw new Error(`Run not found: ${options.runKey}`);
1131
+ }
1132
+ const taskKey = options.taskKey ? String(options.taskKey).trim() : null;
1133
+
1134
+ if (taskKey) {
1135
+ const taskExists = db.prepare('SELECT task_key FROM tasks WHERE task_key = ?').get(taskKey);
1136
+ if (!taskExists) {
1137
+ throw new Error(`Task not found: ${taskKey}`);
1138
+ }
1139
+ }
1140
+
1141
+ const nextStatus = normalizeStatus(options.status, existing.status || 'running');
1142
+ const existingUsedSkills = parseJsonArray(existing.used_skills_json);
1143
+ const nextUsedSkills = normalizeStringArray([...(existingUsedSkills || []), ...normalizeStringArray(options.usedSkills)]);
1144
+ db.prepare(`
1145
+ UPDATE agent_runs
1146
+ SET
1147
+ status = @status,
1148
+ summary = COALESCE(@summary, summary),
1149
+ used_skills_json = COALESCE(@used_skills_json, used_skills_json),
1150
+ output_path = COALESCE(@output_path, output_path),
1151
+ task_key = COALESCE(@task_key, task_key),
1152
+ source = COALESCE(@source, source),
1153
+ workflow_id = COALESCE(@workflow_id, workflow_id),
1154
+ workflow_stage = COALESCE(@workflow_stage, workflow_stage),
1155
+ parent_run_key = COALESCE(@parent_run_key, parent_run_key),
1156
+ updated_at = @updated_at,
1157
+ finished_at = CASE
1158
+ WHEN @status IN ('completed', 'failed') THEN @updated_at
1159
+ ELSE finished_at
1160
+ END
1161
+ WHERE run_key = @run_key
1162
+ `).run({
1163
+ run_key: String(options.runKey),
1164
+ status: nextStatus,
1165
+ summary: options.summary ? String(options.summary).trim() : null,
1166
+ used_skills_json: nextUsedSkills.length > 0 ? JSON.stringify(nextUsedSkills) : null,
1167
+ output_path: options.outputPath ? String(options.outputPath).trim() : null,
1168
+ task_key: taskKey,
1169
+ source: options.source ? String(options.source).trim() : null,
1170
+ workflow_id: options.workflowId ? String(options.workflowId).trim() : null,
1171
+ workflow_stage: options.workflowStage ? String(options.workflowStage).trim() : null,
1172
+ parent_run_key: options.parentRunKey ? String(options.parentRunKey).trim() : null,
1173
+ updated_at: now
1174
+ });
1175
+
1176
+ appendRunEvent(db, {
1177
+ runKey: String(options.runKey),
1178
+ eventType: String(options.eventType || 'update'),
1179
+ phase: options.phase || 'run',
1180
+ status: nextStatus,
1181
+ toolName: options.toolName,
1182
+ message: String(options.message || options.summary || 'Run updated'),
1183
+ payload: options.payload,
1184
+ createdAt: now
1185
+ });
1186
+
1187
+ return nextStatus;
1188
+ }
1189
+
1190
+ function getStatusSnapshot(db) {
1191
+ const taskSummaryRows = db.prepare(`
1192
+ SELECT status, COUNT(*) AS count
1193
+ FROM tasks
1194
+ GROUP BY status
1195
+ `).all();
1196
+
1197
+ const taskCounts = {
1198
+ queued: 0,
1199
+ running: 0,
1200
+ completed: 0,
1201
+ failed: 0
1202
+ };
1203
+
1204
+ for (const row of taskSummaryRows) {
1205
+ if (Object.prototype.hasOwnProperty.call(taskCounts, row.status)) {
1206
+ taskCounts[row.status] = Number(row.count || 0);
1207
+ }
1208
+ }
1209
+
1210
+ const summaryRows = db.prepare(`
1211
+ SELECT status, COUNT(*) AS count
1212
+ FROM agent_runs
1213
+ GROUP BY status
1214
+ `).all();
1215
+
1216
+ const counts = {
1217
+ queued: 0,
1218
+ running: 0,
1219
+ completed: 0,
1220
+ failed: 0
1221
+ };
1222
+
1223
+ for (const row of summaryRows) {
1224
+ if (Object.prototype.hasOwnProperty.call(counts, row.status)) {
1225
+ counts[row.status] = Number(row.count || 0);
1226
+ }
1227
+ }
1228
+
1229
+ const activeRuns = db.prepare(`
1230
+ SELECT run_key, task_key, agent_name, agent_kind, squad_slug, session_key, source, workflow_id, workflow_stage, parent_run_key, title, status, summary, used_skills_json, output_path, started_at, updated_at
1231
+ FROM agent_runs
1232
+ WHERE status IN ('queued', 'running')
1233
+ ORDER BY updated_at DESC, started_at DESC
1234
+ `).all();
1235
+
1236
+ const recentRuns = db.prepare(`
1237
+ SELECT run_key, task_key, agent_name, agent_kind, squad_slug, session_key, source, workflow_id, workflow_stage, parent_run_key, title, status, summary, used_skills_json, output_path, started_at, updated_at, finished_at
1238
+ FROM agent_runs
1239
+ ORDER BY updated_at DESC, started_at DESC
1240
+ LIMIT 20
1241
+ `).all();
1242
+
1243
+ const activeTasks = db.prepare(`
1244
+ SELECT
1245
+ task_key, squad_slug, session_key, title, goal, status, created_by, created_at, updated_at,
1246
+ (
1247
+ SELECT COUNT(*)
1248
+ FROM agent_runs
1249
+ WHERE agent_runs.task_key = tasks.task_key
1250
+ ) AS agent_count,
1251
+ (
1252
+ SELECT COUNT(*)
1253
+ FROM artifacts
1254
+ WHERE artifacts.task_key = tasks.task_key
1255
+ ) AS artifact_count
1256
+ FROM tasks
1257
+ WHERE status IN ('queued', 'running')
1258
+ ORDER BY updated_at DESC, created_at DESC
1259
+ `).all();
1260
+
1261
+ const recentTasks = db.prepare(`
1262
+ SELECT
1263
+ task_key, squad_slug, session_key, title, goal, status, created_by, created_at, updated_at, finished_at,
1264
+ (
1265
+ SELECT COUNT(*)
1266
+ FROM agent_runs
1267
+ WHERE agent_runs.task_key = tasks.task_key
1268
+ ) AS agent_count,
1269
+ (
1270
+ SELECT COUNT(*)
1271
+ FROM artifacts
1272
+ WHERE artifacts.task_key = tasks.task_key
1273
+ ) AS artifact_count
1274
+ FROM tasks
1275
+ ORDER BY updated_at DESC, created_at DESC
1276
+ LIMIT 20
1277
+ `).all();
1278
+
1279
+ const recentArtifacts = db.prepare(`
1280
+ SELECT id, task_key, run_key, squad_slug, agent_name, kind, title, file_path, created_at
1281
+ FROM artifacts
1282
+ ORDER BY created_at DESC
1283
+ LIMIT 20
1284
+ `).all();
1285
+
1286
+ const recentContentItems = db.prepare(`
1287
+ SELECT
1288
+ content_key, task_key, run_key, squad_slug, session_key, title, content_type, layout_type,
1289
+ status, summary, blueprint_slug, used_skills_json, json_path, html_path, created_by_agent, created_at, updated_at
1290
+ FROM content_items
1291
+ ORDER BY updated_at DESC, created_at DESC
1292
+ LIMIT 20
1293
+ `).all();
1294
+
1295
+ const recentExecutionEvents = db.prepare(`
1296
+ SELECT
1297
+ id, task_key, run_key, agent_name, agent_kind, squad_slug, session_key, source,
1298
+ workflow_id, workflow_stage, parent_run_key, event_type, phase, status,
1299
+ tool_name, message, payload_json, sequence_no, parent_event_id, created_at
1300
+ FROM execution_events
1301
+ ORDER BY created_at DESC, id DESC
1302
+ LIMIT 40
1303
+ `).all();
1304
+
1305
+ for (const row of activeRuns) {
1306
+ row.used_skills = parseJsonArray(row.used_skills_json);
1307
+ }
1308
+
1309
+ for (const row of recentRuns) {
1310
+ row.used_skills = parseJsonArray(row.used_skills_json);
1311
+ }
1312
+
1313
+ for (const row of recentContentItems) {
1314
+ row.used_skills = parseJsonArray(row.used_skills_json);
1315
+ }
1316
+
1317
+ return {
1318
+ taskCounts,
1319
+ counts,
1320
+ activeTasks,
1321
+ recentTasks,
1322
+ activeRuns,
1323
+ recentRuns,
1324
+ recentArtifacts,
1325
+ recentContentItems,
1326
+ recentExecutionEvents
1327
+ };
1328
+ }
1329
+
1330
+ // ─── Agent Log Session helpers ───────────────────────────────────────────────
1331
+
1332
+ function resolveSessionsDir(runtimeDir) {
1333
+ return path.join(runtimeDir, SESSIONS_DIR);
1334
+ }
1335
+
1336
+ function resolveSessionFile(runtimeDir, agentName) {
1337
+ return path.join(resolveSessionsDir(runtimeDir), `${agentName}.json`);
1338
+ }
1339
+
1340
+ async function readAgentSession(runtimeDir, agentName) {
1341
+ const filePath = resolveSessionFile(runtimeDir, agentName);
1342
+ try {
1343
+ const raw = await fs.readFile(filePath, 'utf8');
1344
+ return JSON.parse(raw);
1345
+ } catch {
1346
+ return null;
1347
+ }
1348
+ }
1349
+
1350
+ async function writeAgentSession(runtimeDir, agentName, data) {
1351
+ const sessionsDir = resolveSessionsDir(runtimeDir);
1352
+ await ensureDir(sessionsDir);
1353
+ const filePath = resolveSessionFile(runtimeDir, agentName);
1354
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
1355
+ }
1356
+
1357
+ async function clearAgentSession(runtimeDir, agentName) {
1358
+ const filePath = resolveSessionFile(runtimeDir, agentName);
1359
+ try { await fs.unlink(filePath); } catch { /* noop */ }
1360
+ }
1361
+
1362
+ /**
1363
+ * Core function for `aioson runtime-log`.
1364
+ *
1365
+ * Squad agents (--squad): state is stored in SQLite, not in session files.
1366
+ * This avoids race conditions when the orquestrador calls runtime-log in
1367
+ * parallel bash commands — SQLite serializes concurrent writes automatically.
1368
+ * Logic: find the most-recent running task for the squad → find or create a
1369
+ * run for this agent → add event. If no running task exists, create one.
1370
+ *
1371
+ * Official agents (no --squad): state is stored in .sessions/{agent}.json.
1372
+ * Single-process by design, no race condition.
1373
+ */
1374
+ async function logAgentEvent(db, runtimeDir, options) {
1375
+ const agentName = String(options.agentName || 'unknown').trim();
1376
+ const squadSlug = options.squadSlug ? String(options.squadSlug).trim() : null;
1377
+ const isFinish = Boolean(options.finish);
1378
+ const now = nowIso();
1379
+
1380
+ let runKey = null;
1381
+ let taskKey = null;
1382
+
1383
+ if (squadSlug) {
1384
+ // ── Squad agent: look up active task from SQLite ──────────────────────────
1385
+ const activeTask = db.prepare(
1386
+ `SELECT task_key FROM tasks WHERE squad_slug = ? AND status IN ('running', 'queued') ORDER BY created_at DESC LIMIT 1`
1387
+ ).get(squadSlug);
1388
+
1389
+ if (activeTask) {
1390
+ taskKey = activeTask.task_key;
1391
+ } else {
1392
+ // No active task — create one (only the first concurrent call wins; the
1393
+ // second will find the task on its next read because SQLite is serialized)
1394
+ const taskTitle = options.taskTitle || `${squadSlug} — sessão`;
1395
+ taskKey = startTask(db, {
1396
+ title: taskTitle,
1397
+ squadSlug,
1398
+ status: 'running',
1399
+ createdBy: agentName
1400
+ });
1401
+ }
1402
+
1403
+ // Find existing running run for this specific agent under the task
1404
+ const activeRun = db.prepare(
1405
+ `SELECT run_key FROM agent_runs WHERE task_key = ? AND agent_name = ? AND status = 'running' LIMIT 1`
1406
+ ).get(taskKey, agentName);
1407
+
1408
+ if (activeRun) {
1409
+ runKey = activeRun.run_key;
1410
+ if (!isFinish) {
1411
+ appendRunEvent(db, {
1412
+ runKey,
1413
+ eventType: options.type || 'status',
1414
+ phase: options.type || 'run',
1415
+ status: 'running',
1416
+ message: String(options.message || ''),
1417
+ payload: options.meta,
1418
+ createdAt: now
1419
+ });
1420
+ }
1421
+ } else {
1422
+ // First call for this agent — create a run (and emit start event via startRun)
1423
+ // Use taskTitle from --title only if provided, otherwise use agent name
1424
+ const runTitle = options.taskTitle || `@${agentName}`;
1425
+ runKey = startRun(db, {
1426
+ taskKey,
1427
+ agentName,
1428
+ agentKind: 'squad',
1429
+ squadSlug,
1430
+ title: runTitle,
1431
+ message: options.message || 'Iniciando'
1432
+ });
1433
+ }
1434
+ } else {
1435
+ // ── Official agent: session-file based ───────────────────────────────────
1436
+ const session = await readAgentSession(runtimeDir, agentName);
1437
+ runKey = session && !session.finished ? session.runKey : null;
1438
+ taskKey = session && !session.finished ? session.taskKey : null;
1439
+
1440
+ if (!runKey) {
1441
+ const taskTitle = options.taskTitle || `@${agentName}`;
1442
+ taskKey = startTask(db, {
1443
+ title: taskTitle,
1444
+ squadSlug: null,
1445
+ status: 'running',
1446
+ createdBy: agentName
1447
+ });
1448
+ runKey = startRun(db, {
1449
+ taskKey,
1450
+ agentName,
1451
+ agentKind: 'official',
1452
+ squadSlug: null,
1453
+ title: taskTitle,
1454
+ message: options.message || 'Iniciando'
1455
+ });
1456
+ await writeAgentSession(runtimeDir, agentName, { runKey, taskKey, startedAt: now, finished: false });
1457
+ } else {
1458
+ appendRunEvent(db, {
1459
+ runKey,
1460
+ eventType: options.type || 'status',
1461
+ phase: options.type || 'run',
1462
+ status: 'running',
1463
+ message: String(options.message || ''),
1464
+ payload: options.meta,
1465
+ createdAt: now
1466
+ });
1467
+ }
1468
+ }
1469
+
1470
+ if (isFinish) {
1471
+ const finalStatus = normalizeStatus(options.status, 'completed');
1472
+ updateRun(db, {
1473
+ runKey,
1474
+ status: finalStatus,
1475
+ summary: options.summary,
1476
+ eventType: finalStatus === 'completed' ? 'finished' : 'failed',
1477
+ message: options.message || (finalStatus === 'completed' ? 'Concluído' : 'Falhou')
1478
+ });
1479
+ // For squad: only finish the task when orquestrador calls --finish
1480
+ const isOrquestrador = agentName === 'orquestrador';
1481
+ if (taskKey && (!squadSlug || isOrquestrador)) {
1482
+ updateTask(db, { taskKey, status: finalStatus });
1483
+ if (!squadSlug) await clearAgentSession(runtimeDir, agentName);
1484
+ }
1485
+ }
1486
+
1487
+ return { runKey, taskKey };
1488
+ }
1489
+
1490
+ module.exports = {
1491
+ resolveRuntimePaths,
1492
+ runtimeStoreExists,
1493
+ openRuntimeDb,
1494
+ upsertSquadManifest,
1495
+ insertSquadAnalysis,
1496
+ startTask,
1497
+ updateTask,
1498
+ startRun,
1499
+ updateRun,
1500
+ attachArtifact,
1501
+ upsertContentItem,
1502
+ getStatusSnapshot,
1503
+ createRunKey,
1504
+ createTaskKey,
1505
+ appendRunEvent,
1506
+ logAgentEvent,
1507
+ readAgentSession,
1508
+ clearAgentSession,
1509
+ // Pipeline CRUD
1510
+ upsertPipeline,
1511
+ addPipelineNode,
1512
+ updateNodePosition,
1513
+ addPipelineEdge,
1514
+ removePipelineEdge,
1515
+ getPipelineDAG,
1516
+ listPipelines,
1517
+ upsertSquadPorts,
1518
+ getTopologicalOrder,
1519
+ // Artisan CRUD
1520
+ createArtisanSquad,
1521
+ updateArtisanSquad,
1522
+ getArtisanSquad,
1523
+ listArtisanSquads,
1524
+ deleteArtisanSquad,
1525
+ addArtisanMessage,
1526
+ getArtisanMessages
1527
+ };