@jaimevalasek/aioson 1.3.0 → 1.4.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 (213) hide show
  1. package/README.md +19 -2
  2. package/docs/pt/README.md +62 -2
  3. package/docs/pt/advisor-spec.md +5 -5
  4. package/docs/pt/agentes-customizados.md +670 -0
  5. package/docs/pt/agentes.md +111 -13
  6. package/docs/pt/automacao-squads.md +407 -0
  7. package/docs/pt/cenarios.md +3 -3
  8. package/docs/pt/clientes-ai.md +62 -0
  9. package/docs/pt/comandos-cli.md +167 -17
  10. package/docs/pt/deyvin.md +115 -0
  11. package/docs/pt/genome-3.0-spec.md +11 -11
  12. package/docs/pt/inicio-rapido.md +45 -0
  13. package/docs/pt/memoria-contexto.md +255 -0
  14. package/docs/pt/output-strategy-delivery.md +655 -0
  15. package/docs/pt/profiler-system.md +17 -17
  16. package/docs/pt/runtime-observability.md +5 -1
  17. package/docs/pt/skills.md +175 -0
  18. package/docs/pt/{squad-genoma.md → squad-genome.md} +81 -75
  19. package/docs/testing/genome-2.0-rollout.md +1 -1
  20. package/package.json +3 -3
  21. package/src/agents.js +21 -5
  22. package/src/backup-provider.js +303 -0
  23. package/src/cli.js +178 -2
  24. package/src/commands/agents.js +22 -4
  25. package/src/commands/backup.js +533 -0
  26. package/src/commands/cloud.js +17 -17
  27. package/src/commands/context-pack.js +45 -0
  28. package/src/commands/implementation-plan.js +340 -0
  29. package/src/commands/learning.js +134 -0
  30. package/src/commands/live.js +1583 -0
  31. package/src/commands/runtime.js +833 -2
  32. package/src/commands/scan-project.js +288 -24
  33. package/src/commands/setup-context.js +23 -0
  34. package/src/commands/skill.js +558 -0
  35. package/src/commands/squad-agent-create.js +788 -0
  36. package/src/commands/squad-doctor.js +51 -1
  37. package/src/commands/squad-investigate.js +261 -0
  38. package/src/commands/squad-learning.js +209 -0
  39. package/src/commands/squad-pipeline.js +247 -1
  40. package/src/commands/squad-plan.js +329 -0
  41. package/src/commands/squad-status.js +1 -1
  42. package/src/commands/squad-validate.js +57 -1
  43. package/src/commands/test-agents.js +6 -1
  44. package/src/commands/workflow-next.js +8 -1
  45. package/src/commands/workflow-status.js +250 -0
  46. package/src/constants.js +80 -16
  47. package/src/context-memory.js +837 -0
  48. package/src/context-writer.js +2 -0
  49. package/src/delivery-runner.js +319 -0
  50. package/src/genome-files.js +1 -1
  51. package/src/genome-format.js +1 -1
  52. package/src/i18n/messages/en.js +206 -7
  53. package/src/i18n/messages/es.js +123 -6
  54. package/src/i18n/messages/fr.js +122 -5
  55. package/src/i18n/messages/pt-BR.js +205 -12
  56. package/src/installer.js +30 -2
  57. package/src/lib/genomes/compat.js +1 -1
  58. package/src/runtime-store.js +780 -42
  59. package/src/session-handoff.js +77 -0
  60. package/template/.aioson/agents/analyst.md +36 -9
  61. package/template/.aioson/agents/architect.md +20 -5
  62. package/template/.aioson/agents/dev.md +135 -15
  63. package/template/.aioson/agents/deyvin.md +166 -0
  64. package/template/.aioson/agents/discovery-design-doc.md +25 -1
  65. package/template/.aioson/agents/{genoma.md → genome.md} +20 -20
  66. package/template/.aioson/agents/orache.md +371 -0
  67. package/template/.aioson/agents/orchestrator.md +37 -2
  68. package/template/.aioson/agents/pair.md +5 -0
  69. package/template/.aioson/agents/pm.md +17 -5
  70. package/template/.aioson/agents/product.md +58 -22
  71. package/template/.aioson/agents/profiler-enricher.md +1 -1
  72. package/template/.aioson/agents/profiler-forge.md +9 -9
  73. package/template/.aioson/agents/profiler-researcher.md +1 -1
  74. package/template/.aioson/agents/qa.md +17 -5
  75. package/template/.aioson/agents/setup.md +81 -5
  76. package/template/.aioson/agents/squad.md +675 -28
  77. package/template/.aioson/agents/ux-ui.md +277 -34
  78. package/template/.aioson/config.md +175 -0
  79. package/template/.aioson/context/spec.md.template +17 -0
  80. package/template/.aioson/genomes/.gitkeep +0 -0
  81. package/template/.aioson/installed-skills/.gitkeep +0 -0
  82. package/template/.aioson/locales/en/agents/analyst.md +26 -4
  83. package/template/.aioson/locales/en/agents/architect.md +10 -0
  84. package/template/.aioson/locales/en/agents/dev.md +89 -4
  85. package/template/.aioson/locales/en/agents/deyvin.md +129 -0
  86. package/template/.aioson/locales/en/agents/{genoma.md → genome.md} +14 -14
  87. package/template/.aioson/locales/en/agents/orchestrator.md +36 -2
  88. package/template/.aioson/locales/en/agents/pair.md +5 -0
  89. package/template/.aioson/locales/en/agents/pm.md +7 -0
  90. package/template/.aioson/locales/en/agents/product.md +35 -17
  91. package/template/.aioson/locales/en/agents/qa.md +7 -0
  92. package/template/.aioson/locales/en/agents/setup.md +51 -5
  93. package/template/.aioson/locales/en/agents/squad.md +203 -15
  94. package/template/.aioson/locales/en/agents/ux-ui.md +375 -35
  95. package/template/.aioson/locales/es/agents/analyst.md +16 -4
  96. package/template/.aioson/locales/es/agents/architect.md +10 -0
  97. package/template/.aioson/locales/es/agents/dev.md +70 -2
  98. package/template/.aioson/locales/es/agents/deyvin.md +89 -0
  99. package/template/.aioson/locales/es/agents/{genoma.md → genome.md} +13 -13
  100. package/template/.aioson/locales/es/agents/orache.md +103 -0
  101. package/template/.aioson/locales/es/agents/orchestrator.md +36 -2
  102. package/template/.aioson/locales/es/agents/pair.md +5 -0
  103. package/template/.aioson/locales/es/agents/pm.md +7 -0
  104. package/template/.aioson/locales/es/agents/product.md +13 -3
  105. package/template/.aioson/locales/es/agents/qa.md +7 -0
  106. package/template/.aioson/locales/es/agents/setup.md +28 -5
  107. package/template/.aioson/locales/es/agents/squad.md +221 -15
  108. package/template/.aioson/locales/es/agents/ux-ui.md +26 -25
  109. package/template/.aioson/locales/fr/agents/analyst.md +16 -4
  110. package/template/.aioson/locales/fr/agents/architect.md +10 -0
  111. package/template/.aioson/locales/fr/agents/dev.md +70 -2
  112. package/template/.aioson/locales/fr/agents/deyvin.md +89 -0
  113. package/template/.aioson/locales/fr/agents/{genoma.md → genome.md} +7 -7
  114. package/template/.aioson/locales/fr/agents/orache.md +104 -0
  115. package/template/.aioson/locales/fr/agents/orchestrator.md +36 -2
  116. package/template/.aioson/locales/fr/agents/pair.md +5 -0
  117. package/template/.aioson/locales/fr/agents/pm.md +7 -0
  118. package/template/.aioson/locales/fr/agents/product.md +13 -3
  119. package/template/.aioson/locales/fr/agents/qa.md +7 -0
  120. package/template/.aioson/locales/fr/agents/setup.md +28 -5
  121. package/template/.aioson/locales/fr/agents/squad.md +216 -10
  122. package/template/.aioson/locales/fr/agents/ux-ui.md +26 -25
  123. package/template/.aioson/locales/pt-BR/agents/analyst.md +26 -4
  124. package/template/.aioson/locales/pt-BR/agents/architect.md +10 -0
  125. package/template/.aioson/locales/pt-BR/agents/dev.md +93 -4
  126. package/template/.aioson/locales/pt-BR/agents/deyvin.md +129 -0
  127. package/template/.aioson/locales/pt-BR/agents/{genoma.md → genome.md} +49 -49
  128. package/template/.aioson/locales/pt-BR/agents/orache.md +137 -0
  129. package/template/.aioson/locales/pt-BR/agents/orchestrator.md +36 -2
  130. package/template/.aioson/locales/pt-BR/agents/pair.md +5 -0
  131. package/template/.aioson/locales/pt-BR/agents/pm.md +7 -0
  132. package/template/.aioson/locales/pt-BR/agents/product.md +35 -17
  133. package/template/.aioson/locales/pt-BR/agents/qa.md +7 -0
  134. package/template/.aioson/locales/pt-BR/agents/setup.md +51 -5
  135. package/template/.aioson/locales/pt-BR/agents/squad.md +486 -47
  136. package/template/.aioson/locales/pt-BR/agents/ux-ui.md +361 -22
  137. package/template/.aioson/my-agents/.gitkeep +0 -0
  138. package/template/.aioson/rules/.gitkeep +0 -0
  139. package/template/.aioson/rules/squad/.gitkeep +0 -0
  140. package/template/.aioson/rules/squad/README.md +50 -0
  141. package/template/.aioson/schemas/genome-meta.schema.json +1 -1
  142. package/template/.aioson/schemas/genome.schema.json +1 -1
  143. package/template/.aioson/schemas/squad-blueprint.schema.json +11 -0
  144. package/template/.aioson/schemas/squad-manifest.schema.json +257 -1
  145. package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +157 -0
  146. package/template/.aioson/skills/design/cognitive-core-ui/references/components.md +407 -0
  147. package/template/.aioson/skills/design/cognitive-core-ui/references/dashboards.md +172 -0
  148. package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +490 -0
  149. package/template/.aioson/skills/design/cognitive-core-ui/references/motion.md +237 -0
  150. package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +289 -0
  151. package/template/.aioson/skills/design/cognitive-core-ui/references/websites.md +350 -0
  152. package/template/.aioson/skills/design/interface-design/SKILL.md +47 -0
  153. package/template/.aioson/skills/design/interface-design/references/components-and-states.md +105 -0
  154. package/template/.aioson/skills/design/interface-design/references/design-directions.md +101 -0
  155. package/template/.aioson/skills/design/interface-design/references/handoff-and-quality.md +71 -0
  156. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +74 -0
  157. package/template/.aioson/skills/design/interface-design/references/tokens-and-depth.md +173 -0
  158. package/template/.aioson/skills/design/premium-command-center-ui/SKILL.md +62 -0
  159. package/template/.aioson/skills/design/premium-command-center-ui/references/operations.md +74 -0
  160. package/template/.aioson/skills/design/premium-command-center-ui/references/patterns.md +116 -0
  161. package/template/.aioson/skills/design/premium-command-center-ui/references/validation.md +47 -0
  162. package/template/.aioson/skills/design/premium-command-center-ui/references/visual-system.md +215 -0
  163. package/template/.aioson/skills/design-system/SKILL.md +92 -0
  164. package/template/.aioson/skills/design-system/cognitive-core-ui.skill +0 -0
  165. package/template/.aioson/skills/design-system/components/SKILL.md +274 -0
  166. package/template/.aioson/skills/design-system/components/SKILL.md:Zone.Identifier +0 -0
  167. package/template/.aioson/skills/design-system/dashboards/SKILL.md +184 -0
  168. package/template/.aioson/skills/design-system/dashboards/SKILL.md:Zone.Identifier +0 -0
  169. package/template/.aioson/skills/design-system/foundations/SKILL.md +250 -0
  170. package/template/.aioson/skills/design-system/foundations/SKILL.md:Zone.Identifier +0 -0
  171. package/template/.aioson/skills/design-system/motion/SKILL.md +197 -0
  172. package/template/.aioson/skills/design-system/motion/SKILL.md:Zone.Identifier +0 -0
  173. package/template/.aioson/skills/design-system/patterns/SKILL.md +231 -0
  174. package/template/.aioson/skills/design-system/patterns/SKILL.md:Zone.Identifier +0 -0
  175. package/template/.aioson/skills/squad/SKILL.md +58 -0
  176. package/template/.aioson/skills/squad/domains/.gitkeep +0 -0
  177. package/template/.aioson/skills/squad/formats/.gitkeep +0 -0
  178. package/template/.aioson/skills/squad/patterns/.gitkeep +0 -0
  179. package/template/.aioson/skills/squad/references/.gitkeep +0 -0
  180. package/template/.aioson/tasks/implementation-plan.md +288 -0
  181. package/template/.aioson/tasks/squad-create.md +1 -1
  182. package/template/.aioson/tasks/squad-execution-plan.md +279 -0
  183. package/template/.aioson/tasks/squad-export.md +1 -1
  184. package/template/.aioson/tasks/squad-investigate.md +44 -0
  185. package/template/.aioson/tasks/squad-learning-review.md +44 -0
  186. package/template/.aioson/tasks/squad-output-config.md +177 -0
  187. package/template/.aioson/tasks/squad-validate.md +1 -1
  188. package/template/.claude/commands/aioson/agent/deyvin.md +5 -0
  189. package/template/.claude/commands/aioson/agent/discovery-design-doc.md +5 -0
  190. package/template/.claude/commands/aioson/agent/genome.md +5 -0
  191. package/template/.claude/commands/aioson/agent/product.md +5 -0
  192. package/template/.claude/commands/aioson/agent/profiler-enricher.md +5 -0
  193. package/template/.claude/commands/aioson/agent/profiler-forge.md +5 -0
  194. package/template/.claude/commands/aioson/agent/profiler-researcher.md +5 -0
  195. package/template/.claude/commands/aioson/agent/squad.md +5 -0
  196. package/template/.gemini/GEMINI.md +2 -0
  197. package/template/.gemini/commands/aios-deyvin.toml +6 -0
  198. package/template/.gemini/commands/aios-pair.toml +6 -0
  199. package/template/AGENTS.md +34 -6
  200. package/template/CLAUDE.md +31 -4
  201. package/template/OPENCODE.md +6 -2
  202. package/template/squad-searches/.gitkeep +0 -0
  203. package/template/.aioson/skills/static/interface-design.md +0 -372
  204. package/template/.aioson/skills/static/premium-command-center-ui.md +0 -190
  205. /package/template/.aioson/{genomas → docs}/.gitkeep +0 -0
  206. /package/template/.claude/commands/aioson/{analyst.md → agent/analyst.md} +0 -0
  207. /package/template/.claude/commands/aioson/{architect.md → agent/architect.md} +0 -0
  208. /package/template/.claude/commands/aioson/{dev.md → agent/dev.md} +0 -0
  209. /package/template/.claude/commands/aioson/{orchestrator.md → agent/orchestrator.md} +0 -0
  210. /package/template/.claude/commands/aioson/{pm.md → agent/pm.md} +0 -0
  211. /package/template/.claude/commands/aioson/{qa.md → agent/qa.md} +0 -0
  212. /package/template/.claude/commands/aioson/{setup.md → agent/setup.md} +0 -0
  213. /package/template/.claude/commands/aioson/{ux-ui.md → agent/ux-ui.md} +0 -0
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('node:fs/promises');
3
4
  const path = require('node:path');
4
5
  const {
5
6
  openRuntimeDb,
@@ -9,6 +10,107 @@ const {
9
10
  getTopologicalOrder
10
11
  } = require('../runtime-store');
11
12
 
13
+ /**
14
+ * Determine node completion status by checking handoffs.
15
+ * A node is "complete" if all its outgoing edges have consumed handoffs,
16
+ * or if it has no outgoing edges and has at least one incoming consumed handoff
17
+ * (or is the first node with no incoming edges and has produced output handoffs).
18
+ */
19
+ function classifyNodes(db, pipelineSlug, order, edges) {
20
+ const handoffs = db
21
+ .prepare('SELECT * FROM squad_handoffs WHERE pipeline_slug = ? ORDER BY created_at DESC')
22
+ .all(pipelineSlug);
23
+
24
+ const outgoing = {};
25
+ const incoming = {};
26
+ for (const slug of order) {
27
+ outgoing[slug] = [];
28
+ incoming[slug] = [];
29
+ }
30
+ for (const edge of edges) {
31
+ if (outgoing[edge.source_squad]) outgoing[edge.source_squad].push(edge);
32
+ if (incoming[edge.target_squad]) incoming[edge.target_squad].push(edge);
33
+ }
34
+
35
+ const nodeStatus = {};
36
+
37
+ for (const slug of order) {
38
+ const outEdges = outgoing[slug];
39
+ const inEdges = incoming[slug];
40
+
41
+ // Check if all outgoing handoffs are consumed
42
+ if (outEdges.length > 0) {
43
+ const allProduced = outEdges.every(edge =>
44
+ handoffs.some(h =>
45
+ h.from_squad === edge.source_squad &&
46
+ h.from_port === edge.source_port &&
47
+ (h.status === 'consumed' || h.status === 'pending')
48
+ )
49
+ );
50
+ const allConsumed = outEdges.every(edge =>
51
+ handoffs.some(h =>
52
+ h.from_squad === edge.source_squad &&
53
+ h.from_port === edge.source_port &&
54
+ h.status === 'consumed'
55
+ )
56
+ );
57
+
58
+ if (allConsumed) {
59
+ nodeStatus[slug] = 'completed';
60
+ } else if (allProduced) {
61
+ nodeStatus[slug] = 'produced';
62
+ } else {
63
+ nodeStatus[slug] = 'pending';
64
+ }
65
+ } else {
66
+ // Terminal node — check if all incoming handoffs are consumed
67
+ if (inEdges.length === 0) {
68
+ // Root node with no edges — mark pending
69
+ nodeStatus[slug] = 'pending';
70
+ } else {
71
+ const allInConsumed = inEdges.every(edge =>
72
+ handoffs.some(h =>
73
+ h.to_squad === edge.target_squad &&
74
+ h.to_port === edge.target_port &&
75
+ h.status === 'consumed'
76
+ )
77
+ );
78
+ nodeStatus[slug] = allInConsumed ? 'completed' : 'pending';
79
+ }
80
+ }
81
+ }
82
+
83
+ // First node with no incoming edges: if it has produced outputs, mark completed
84
+ for (const slug of order) {
85
+ if (incoming[slug].length === 0 && outgoing[slug].length > 0) {
86
+ if (nodeStatus[slug] === 'produced' || nodeStatus[slug] === 'completed') {
87
+ nodeStatus[slug] = 'completed';
88
+ }
89
+ }
90
+ }
91
+
92
+ return { nodeStatus, handoffs };
93
+ }
94
+
95
+ function findNextPendingNode(order, nodeStatus) {
96
+ // Find first node that is pending and whose dependencies are all completed
97
+ return order.find(slug => nodeStatus[slug] === 'pending') || null;
98
+ }
99
+
100
+ function findSquadOrchestratorAgent(projectDir, squadSlug) {
101
+ // Convention: squad orchestrator is at .aioson/squads/{slug}/agents/{slug}-orchestrator.md
102
+ // or the first agent in the squad
103
+ return `@${squadSlug}-orchestrator`;
104
+ }
105
+
106
+ async function requirePipelineSlug(slugArg, logger) {
107
+ if (!slugArg) {
108
+ logger.error('Usage: aioson squad:pipeline [path] --sub=run --pipeline=<slug>');
109
+ return null;
110
+ }
111
+ return slugArg;
112
+ }
113
+
12
114
  async function runSquadPipeline({ args = [], options = {}, logger = console } = {}) {
13
115
  const projectDir = path.resolve(process.cwd(), args[0] || '.');
14
116
  const subcommand = options.sub || args[1] || 'list';
@@ -87,7 +189,151 @@ async function runSquadPipeline({ args = [], options = {}, logger = console } =
87
189
  return { ok: true, pipeline: dag.pipeline, handoffs: { pending, consumed, failed } };
88
190
  }
89
191
 
90
- logger.error(`Unknown subcommand: ${subcommand}. Available: list, show, status`);
192
+ // --- NEW: run (guided mode) ---
193
+ if (subcommand === 'run' || subcommand === 'continue') {
194
+ const slug = await requirePipelineSlug(slugArg, logger);
195
+ if (!slug) return { ok: false, error: 'missing_slug' };
196
+
197
+ const dag = getPipelineDAG(db, slug);
198
+ if (!dag) {
199
+ logger.error(`Pipeline not found: ${slug}`);
200
+ return { ok: false, error: 'not_found' };
201
+ }
202
+
203
+ const order = getTopologicalOrder(db, slug);
204
+ if (!order) {
205
+ logger.error('Cycle detected in pipeline — cannot run.');
206
+ return { ok: false, error: 'cycle_detected' };
207
+ }
208
+
209
+ const { nodeStatus, handoffs } = classifyNodes(db, slug, order, dag.edges);
210
+
211
+ // Show pipeline progress
212
+ logger.log('');
213
+ logger.log(`Pipeline: ${dag.pipeline.name || dag.pipeline.slug}`);
214
+ logger.log(`Mode: guided (the system suggests, you execute)`);
215
+ logger.log('');
216
+
217
+ logger.log('Progress:');
218
+ for (const nodeSlugg of order) {
219
+ const status = nodeStatus[nodeSlugg];
220
+ const icon = status === 'completed' ? '[v]'
221
+ : status === 'produced' ? '[~]'
222
+ : '[>]';
223
+ // Only mark the first pending as [>], rest as [ ]
224
+ const displayIcon = status === 'pending'
225
+ ? (nodeSlugg === findNextPendingNode(order, nodeStatus) ? '[>]' : '[ ]')
226
+ : icon;
227
+ logger.log(` ${displayIcon} ${nodeSlugg} (${status})`);
228
+ }
229
+ logger.log('');
230
+
231
+ // Find next node to activate
232
+ const nextNode = findNextPendingNode(order, nodeStatus);
233
+
234
+ if (!nextNode) {
235
+ // All nodes completed
236
+ const allCompleted = order.every(s => nodeStatus[s] === 'completed');
237
+ if (allCompleted) {
238
+ logger.log('Pipeline completed! All nodes have been executed.');
239
+ return { ok: true, pipeline: slug, status: 'completed', nextNode: null };
240
+ }
241
+ logger.log('No actionable node found. Check pending handoffs.');
242
+ return { ok: true, pipeline: slug, status: 'blocked', nextNode: null };
243
+ }
244
+
245
+ // Check if next node has pending incoming handoffs to consume
246
+ const pendingIncoming = handoffs.filter(h =>
247
+ h.to_squad === nextNode && h.status === 'pending'
248
+ );
249
+
250
+ // Consume pending incoming handoffs for this node
251
+ if (pendingIncoming.length > 0) {
252
+ const updateStmt = db.prepare(
253
+ 'UPDATE squad_handoffs SET status = ?, consumed_at = ? WHERE id = ?'
254
+ );
255
+ for (const h of pendingIncoming) {
256
+ updateStmt.run('consumed', new Date().toISOString(), h.id);
257
+ }
258
+ logger.log(`Consumed ${pendingIncoming.length} incoming handoff(s) for ${nextNode}.`);
259
+ }
260
+
261
+ const orchestratorAgent = findSquadOrchestratorAgent(projectDir, nextNode);
262
+ logger.log(`Next: activate squad "${nextNode}"`);
263
+ logger.log(` Agent: ${orchestratorAgent}`);
264
+ logger.log(` Command: aioson agent ${orchestratorAgent} --tool=claude`);
265
+ logger.log('');
266
+ logger.log('After the squad completes its work, run:');
267
+ logger.log(` aioson squad:pipeline . --sub=run --pipeline=${slug}`);
268
+
269
+ return {
270
+ ok: true,
271
+ pipeline: slug,
272
+ status: 'running',
273
+ nextNode,
274
+ orchestratorAgent,
275
+ nodeStatus
276
+ };
277
+ }
278
+
279
+ // --- NEW: skip ---
280
+ if (subcommand === 'skip') {
281
+ const slug = await requirePipelineSlug(slugArg, logger);
282
+ if (!slug) return { ok: false, error: 'missing_slug' };
283
+
284
+ const dag = getPipelineDAG(db, slug);
285
+ if (!dag) {
286
+ logger.error(`Pipeline not found: ${slug}`);
287
+ return { ok: false, error: 'not_found' };
288
+ }
289
+
290
+ const order = getTopologicalOrder(db, slug);
291
+ if (!order) {
292
+ logger.error('Cycle detected — cannot skip.');
293
+ return { ok: false, error: 'cycle_detected' };
294
+ }
295
+
296
+ const { nodeStatus } = classifyNodes(db, slug, order, dag.edges);
297
+ const nextNode = findNextPendingNode(order, nodeStatus);
298
+
299
+ if (!nextNode) {
300
+ logger.log('No pending node to skip.');
301
+ return { ok: true, pipeline: slug, skipped: null };
302
+ }
303
+
304
+ // Create synthetic handoffs for all outgoing edges of the skipped node
305
+ const outEdges = dag.edges.filter(e => e.source_squad === nextNode);
306
+ const insertHandoff = db.prepare(`
307
+ INSERT INTO squad_handoffs (id, pipeline_slug, from_squad, from_port, to_squad, to_port, payload_json, status, created_at)
308
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?)
309
+ `);
310
+
311
+ for (const edge of outEdges) {
312
+ const id = `skip-${nextNode}-${edge.target_squad}-${Date.now()}`;
313
+ insertHandoff.run(
314
+ id,
315
+ slug,
316
+ edge.source_squad,
317
+ edge.source_port,
318
+ edge.target_squad,
319
+ edge.target_port,
320
+ JSON.stringify({ skipped: true, reason: 'Node skipped by user' }),
321
+ new Date().toISOString()
322
+ );
323
+ }
324
+
325
+ logger.log(`Skipped node: ${nextNode}`);
326
+ if (outEdges.length > 0) {
327
+ logger.log(`Created ${outEdges.length} synthetic handoff(s) for downstream nodes.`);
328
+ }
329
+ logger.log('');
330
+ logger.log('Run again to see the next node:');
331
+ logger.log(` aioson squad:pipeline . --sub=run --pipeline=${slug}`);
332
+
333
+ return { ok: true, pipeline: slug, skipped: nextNode };
334
+ }
335
+
336
+ logger.error(`Unknown subcommand: ${subcommand}. Available: list, show, status, run, continue, skip`);
91
337
  return { ok: false, error: 'unknown_subcommand' };
92
338
  } finally {
93
339
  db.close();
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const {
7
+ openRuntimeDb,
8
+ upsertSquadExecutionPlan,
9
+ getSquadExecutionPlan,
10
+ getSquadExecutionPlanBySquad,
11
+ listSquadExecutionPlans,
12
+ updateSquadExecutionPlanStatus,
13
+ upsertSquadPlanRound,
14
+ updateSquadPlanRoundStatus,
15
+ getSquadPlanRounds
16
+ } = require('../runtime-store');
17
+
18
+ const SQUADS_DIR = path.join('.aioson', 'squads');
19
+
20
+ async function pathExists(targetPath) {
21
+ try {
22
+ await fs.access(targetPath);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Compute hash of squad manifest for staleness detection.
31
+ */
32
+ async function computeManifestHash(projectDir, squadSlug) {
33
+ const manifestPath = path.resolve(projectDir, SQUADS_DIR, squadSlug, 'squad.manifest.json');
34
+ const hash = crypto.createHash('sha256');
35
+ try {
36
+ const stat = await fs.stat(manifestPath);
37
+ hash.update(`manifest:${stat.mtimeMs}`);
38
+ } catch {
39
+ hash.update('manifest:missing');
40
+ }
41
+ return hash.digest('hex').slice(0, 16);
42
+ }
43
+
44
+ /**
45
+ * Parse execution plan frontmatter.
46
+ */
47
+ function parsePlanFrontmatter(content) {
48
+ const text = String(content || '');
49
+ const match = text.match(/^---\n([\s\S]*?)\n---/);
50
+ if (!match) return {};
51
+ const meta = {};
52
+ for (const line of match[1].split('\n')) {
53
+ const [key, ...rest] = line.split(':');
54
+ if (key && rest.length) {
55
+ meta[key.trim()] = rest.join(':').trim().replace(/^"(.*)"$/, '$1');
56
+ }
57
+ }
58
+ return meta;
59
+ }
60
+
61
+ /**
62
+ * Count rounds in an execution plan markdown.
63
+ */
64
+ function countRounds(content) {
65
+ const text = String(content || '');
66
+ const matches = text.match(/^### Round \d+/gm);
67
+ return matches ? matches.length : 0;
68
+ }
69
+
70
+ /**
71
+ * Resolve the execution plan path for a squad.
72
+ */
73
+ function planPath(projectDir, squadSlug) {
74
+ return path.resolve(projectDir, SQUADS_DIR, squadSlug, 'docs', 'execution-plan.md');
75
+ }
76
+
77
+ /**
78
+ * Subcommand: show <slug>
79
+ * Shows the execution plan for a squad.
80
+ */
81
+ async function handleShow(projectDir, squadSlug, { logger, t }) {
82
+ if (!squadSlug) {
83
+ logger.error(t('squad_plan.slug_required'));
84
+ return { found: false };
85
+ }
86
+
87
+ const pp = planPath(projectDir, squadSlug);
88
+ if (!(await pathExists(pp))) {
89
+ logger.error(t('squad_plan.not_found', { slug: squadSlug }));
90
+ return { found: false };
91
+ }
92
+
93
+ const content = await fs.readFile(pp, 'utf8');
94
+ const meta = parsePlanFrontmatter(content);
95
+ const rounds = countRounds(content);
96
+
97
+ logger.log(`Execution Plan: ${squadSlug}`);
98
+ logger.log(`Status: ${meta.status || 'unknown'}`);
99
+ logger.log(`Rounds: ${rounds}`);
100
+ logger.log('');
101
+ logger.log(content);
102
+
103
+ return { found: true, meta, rounds };
104
+ }
105
+
106
+ /**
107
+ * Subcommand: status <slug>
108
+ * Shows progress of the execution plan from SQLite.
109
+ */
110
+ async function handleStatus(projectDir, squadSlug, { logger, t }) {
111
+ if (!squadSlug) {
112
+ logger.error(t('squad_plan.slug_required'));
113
+ return { found: false };
114
+ }
115
+
116
+ const handle = await openRuntimeDb(projectDir, { mustExist: true });
117
+ if (!handle) {
118
+ logger.error(t('squad_plan.no_runtime'));
119
+ return { found: false };
120
+ }
121
+ const { db } = handle;
122
+ try {
123
+ const plan = getSquadExecutionPlanBySquad(db, squadSlug);
124
+ if (!plan) {
125
+ logger.error(t('squad_plan.no_plan', { slug: squadSlug }));
126
+ return { found: false };
127
+ }
128
+
129
+ const rounds = getSquadPlanRounds(db, plan.plan_slug);
130
+ logger.log(`Plan: ${plan.plan_slug}`);
131
+ logger.log(`Squad: ${plan.squad_slug}`);
132
+ logger.log(`Status: ${plan.status}`);
133
+ logger.log(`Progress: ${plan.rounds_completed}/${plan.rounds_total}`);
134
+ logger.log('');
135
+ for (const rd of rounds) {
136
+ const icon = rd.status === 'completed' ? '✓' : rd.status === 'in_progress' ? '▸' : '○';
137
+ logger.log(` ${icon} Round ${rd.round_number}: ${rd.title} (@${rd.executor_slug}) [${rd.status}]`);
138
+ }
139
+ return { found: true, plan, rounds };
140
+ } finally {
141
+ db.close();
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Subcommand: checkpoint <slug> <round-number>
147
+ * Marks a round as completed.
148
+ */
149
+ async function handleCheckpoint(projectDir, squadSlug, roundNumber, { logger, t }) {
150
+ if (!squadSlug || !roundNumber || isNaN(Number(roundNumber))) {
151
+ logger.error(t('squad_plan.checkpoint_usage'));
152
+ return { updated: false };
153
+ }
154
+
155
+ const handle = await openRuntimeDb(projectDir, { mustExist: true });
156
+ if (!handle) {
157
+ logger.error(t('squad_plan.no_runtime'));
158
+ return { updated: false };
159
+ }
160
+ const { db } = handle;
161
+ try {
162
+ const plan = getSquadExecutionPlanBySquad(db, squadSlug);
163
+ if (!plan) {
164
+ logger.error(t('squad_plan.no_plan', { slug: squadSlug }));
165
+ return { updated: false };
166
+ }
167
+
168
+ const updated = updateSquadPlanRoundStatus(db, plan.plan_slug, Number(roundNumber), 'completed');
169
+ if (updated) {
170
+ logger.log(t('squad_plan.round_completed', { round: roundNumber }));
171
+ } else {
172
+ logger.error(t('squad_plan.round_not_found', { round: roundNumber }));
173
+ }
174
+ return { updated };
175
+ } finally {
176
+ db.close();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Subcommand: stale <slug>
182
+ * Checks if the squad manifest changed after the plan was created.
183
+ */
184
+ async function handleStale(projectDir, squadSlug, { logger, t }) {
185
+ if (!squadSlug) {
186
+ logger.error(t('squad_plan.slug_required'));
187
+ return { found: false, stale: false };
188
+ }
189
+
190
+ const pp = planPath(projectDir, squadSlug);
191
+ if (!(await pathExists(pp))) {
192
+ logger.error(t('squad_plan.not_found', { slug: squadSlug }));
193
+ return { found: false, stale: false };
194
+ }
195
+
196
+ const content = await fs.readFile(pp, 'utf8');
197
+ const meta = parsePlanFrontmatter(content);
198
+ if (!meta.created) {
199
+ logger.log(t('squad_plan.no_created_date'));
200
+ return { found: true, stale: false };
201
+ }
202
+
203
+ const planDate = new Date(meta.created);
204
+ const manifestFile = path.resolve(projectDir, SQUADS_DIR, squadSlug, 'squad.manifest.json');
205
+ let stale = false;
206
+
207
+ try {
208
+ const stat = await fs.stat(manifestFile);
209
+ if (stat.mtime > planDate) {
210
+ logger.log(' ⚠ squad.manifest.json modified after plan was created');
211
+ stale = true;
212
+ }
213
+ } catch {
214
+ // manifest missing — not stale, just broken
215
+ }
216
+
217
+ // Also check blueprint
218
+ const designsDir = path.resolve(projectDir, SQUADS_DIR, '.designs');
219
+ try {
220
+ const files = await fs.readdir(designsDir);
221
+ const bpFile = files.find(f => f.startsWith(squadSlug) && f.endsWith('.blueprint.json'));
222
+ if (bpFile) {
223
+ const bpPath = path.join(designsDir, bpFile);
224
+ const stat = await fs.stat(bpPath);
225
+ if (stat.mtime > planDate) {
226
+ logger.log(' ⚠ blueprint modified after plan was created');
227
+ stale = true;
228
+ }
229
+ }
230
+ } catch {
231
+ // designs dir may not exist
232
+ }
233
+
234
+ if (stale) {
235
+ logger.log(t('squad_plan.is_stale'));
236
+ } else {
237
+ logger.log(t('squad_plan.is_fresh'));
238
+ }
239
+
240
+ return { found: true, stale };
241
+ }
242
+
243
+ /**
244
+ * Subcommand: register <slug>
245
+ * Registers an existing execution plan file into the runtime SQLite.
246
+ */
247
+ async function handleRegister(projectDir, squadSlug, { logger, t }) {
248
+ if (!squadSlug) {
249
+ logger.error(t('squad_plan.slug_required'));
250
+ return { registered: false };
251
+ }
252
+
253
+ const pp = planPath(projectDir, squadSlug);
254
+ if (!(await pathExists(pp))) {
255
+ logger.error(t('squad_plan.not_found', { slug: squadSlug }));
256
+ return { registered: false };
257
+ }
258
+
259
+ const content = await fs.readFile(pp, 'utf8');
260
+ const meta = parsePlanFrontmatter(content);
261
+ const rounds = countRounds(content);
262
+ const hash = await computeManifestHash(projectDir, squadSlug);
263
+
264
+ const handle = await openRuntimeDb(projectDir);
265
+ const { db } = handle;
266
+ try {
267
+ const planSlug = upsertSquadExecutionPlan(db, {
268
+ squadSlug,
269
+ status: meta.status || 'draft',
270
+ roundsTotal: rounds,
271
+ roundsCompleted: 0,
272
+ basedOnBlueprint: meta.based_on_blueprint || null,
273
+ basedOnInvestigation: meta.based_on_investigation || null,
274
+ sourceHash: hash
275
+ });
276
+ logger.log(t('squad_plan.registered', { planSlug, rounds }));
277
+ return { registered: true, planSlug };
278
+ } finally {
279
+ db.close();
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Main router for squad-plan subcommands.
285
+ */
286
+ async function run(projectDir, args, context) {
287
+ const sub = args[0] || 'show';
288
+ const rest = args.slice(1);
289
+
290
+ switch (sub) {
291
+ case 'show':
292
+ return handleShow(projectDir, rest[0], context);
293
+ case 'status':
294
+ return handleStatus(projectDir, rest[0], context);
295
+ case 'checkpoint':
296
+ return handleCheckpoint(projectDir, rest[0], rest[1], context);
297
+ case 'stale':
298
+ return handleStale(projectDir, rest[0], context);
299
+ case 'register':
300
+ return handleRegister(projectDir, rest[0], context);
301
+ default:
302
+ context.logger.error(`Unknown subcommand: ${sub}. Available: show, status, checkpoint, stale, register`);
303
+ return { error: true };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Entry point for CLI integration (same signature as other commands).
309
+ */
310
+ async function runSquadPlan({ args = [], options = {}, logger = console, t = (k) => k } = {}) {
311
+ const projectDir = path.resolve(process.cwd(), args[0] || '.');
312
+ const sub = options.sub || args[1] || 'show';
313
+ const slug = options.squad || args[2] || null;
314
+ const context = { logger, t };
315
+
316
+ if (sub === 'show') return handleShow(projectDir, slug, context);
317
+ if (sub === 'status') return handleStatus(projectDir, slug, context);
318
+ if (sub === 'checkpoint') {
319
+ const round = args[3] || options.round;
320
+ return handleCheckpoint(projectDir, slug, round, context);
321
+ }
322
+ if (sub === 'stale') return handleStale(projectDir, slug, context);
323
+ if (sub === 'register') return handleRegister(projectDir, slug, context);
324
+
325
+ logger.error(`Unknown subcommand: ${sub}. Available: show, status, checkpoint, stale, register`);
326
+ return { error: true };
327
+ }
328
+
329
+ module.exports = { run, runSquadPlan, handleShow, handleStatus, handleCheckpoint, handleStale, handleRegister };
@@ -7,7 +7,7 @@ const { flattenGenomeBindings, mergeGenomeBindings } = require('../genomes/bindi
7
7
  const SQUADS_DIR = '.aioson/squads';
8
8
  const AGENTS_ROOT = 'agents';
9
9
  const OUTPUT_ROOT = 'output';
10
- const LOGS_ROOT = 'aios-logs';
10
+ const LOGS_ROOT = 'aioson-logs';
11
11
  const SKIP_FILES = new Set(['memory.md', '.gitkeep']);
12
12
  const SESSION_HTML_RE = /\.html?$/i;
13
13
 
@@ -142,7 +142,62 @@ async function validateSemanticDeep(projectDir, slug, manifest) {
142
142
  }
143
143
  } catch { warnings.push('AGENTS.md not found'); }
144
144
 
145
- // 5. Readiness não contradiz blockers
145
+ // 5. Output strategy validation
146
+ const outputStrategy = manifest.outputStrategy && typeof manifest.outputStrategy === 'object'
147
+ ? manifest.outputStrategy
148
+ : null;
149
+
150
+ if (outputStrategy) {
151
+ const validModes = ['files', 'sqlite', 'hybrid'];
152
+ if (outputStrategy.mode && !validModes.includes(outputStrategy.mode)) {
153
+ errors.push(`Invalid outputStrategy.mode: "${outputStrategy.mode}" (expected: ${validModes.join(', ')})`);
154
+ }
155
+
156
+ const delivery = outputStrategy.delivery && typeof outputStrategy.delivery === 'object'
157
+ ? outputStrategy.delivery
158
+ : null;
159
+
160
+ if (delivery) {
161
+ const webhooks = Array.isArray(delivery.webhooks) ? delivery.webhooks : [];
162
+ for (const wh of webhooks) {
163
+ if (!wh.slug) {
164
+ errors.push('Webhook missing required "slug" field');
165
+ }
166
+ if (!wh.trigger) {
167
+ errors.push(`Webhook "${wh.slug || '?'}" missing required "trigger" field`);
168
+ }
169
+ const validTriggers = ['on-publish', 'on-create', 'manual'];
170
+ if (wh.trigger && !validTriggers.includes(wh.trigger)) {
171
+ warnings.push(`Webhook "${wh.slug}" has unknown trigger: "${wh.trigger}"`);
172
+ }
173
+ if (wh.url && wh.url.includes('{{ENV:')) {
174
+ const envMatch = wh.url.match(/\{\{ENV:(\w+)\}\}/);
175
+ if (envMatch && !process.env[envMatch[1]]) {
176
+ warnings.push(`Webhook "${wh.slug}" references unset env var: ${envMatch[1]}`);
177
+ }
178
+ }
179
+ if (wh.worker) {
180
+ const workerPath = path.join(projectDir, wh.worker);
181
+ if (!(await pathExists(workerPath))) {
182
+ warnings.push(`Webhook "${wh.slug}" worker not found: ${wh.worker}`);
183
+ }
184
+ }
185
+ }
186
+
187
+ if (delivery.autoPublish && webhooks.length === 0 && !delivery.cloudPublish) {
188
+ warnings.push('autoPublish is enabled but no webhooks or cloudPublish configured');
189
+ }
190
+ }
191
+
192
+ if (outputStrategy.mode === 'files' && outputStrategy.dataOutput && outputStrategy.dataOutput.enabled) {
193
+ warnings.push('outputStrategy.mode is "files" but dataOutput.enabled is true — consider "hybrid"');
194
+ }
195
+ if (outputStrategy.mode === 'sqlite' && outputStrategy.fileOutput && outputStrategy.fileOutput.enabled) {
196
+ warnings.push('outputStrategy.mode is "sqlite" but fileOutput.enabled is true — consider "hybrid"');
197
+ }
198
+ }
199
+
200
+ // 6. Readiness não contradiz blockers
146
201
  if (manifest.readiness) {
147
202
  for (const [dim, val] of Object.entries(manifest.readiness)) {
148
203
  if (val && val.status === 'ready' && val.blocker) {
@@ -207,6 +262,7 @@ async function runSquadValidate({ args = [], options = {}, logger = console } =
207
262
  logger.log(` Structure: ${structure.errors.length === 0 ? '\u2705 PASS' : '\u274c FAIL'}`);
208
263
  logger.log(` Semantics: ${semantics.errors.length === 0 ? (semantics.warnings.length > 0 ? '\u26a0\ufe0f WARNINGS' : '\u2705 PASS') : '\u274c FAIL'}`);
209
264
  logger.log(` Semantic deep: ${semanticDeep.errors.length === 0 ? (semanticDeep.warnings.length > 0 ? '\u26a0\ufe0f WARNINGS' : '\u2705 PASS') : '\u274c FAIL'}`);
265
+ logger.log(` Output strategy: ${manifest.outputStrategy ? `${manifest.outputStrategy.mode || 'unknown'} mode` : 'not configured'}`);
210
266
 
211
267
  if (allErrors.length > 0) {
212
268
  logger.log('');
@@ -14,7 +14,7 @@ const AGENTS = [
14
14
  'qa',
15
15
  'orchestrator',
16
16
  'squad',
17
- 'genoma',
17
+ 'genome',
18
18
  'profiler-researcher',
19
19
  'profiler-enricher',
20
20
  'profiler-forge'
@@ -167,6 +167,11 @@ async function runTestAgents({ options, logger }) {
167
167
  addCheck(`skill: ${skill}`, content !== null && content.length > 100);
168
168
  }
169
169
 
170
+ const packagedDesignSkill = await readFile(
171
+ path.join(TEMPLATE_DIR, '.aioson', 'skills', 'design', 'cognitive-ui', 'SKILL.md')
172
+ );
173
+ addCheck('skill package: cognitive-ui', packagedDesignSkill !== null && packagedDesignSkill.length > 100);
174
+
170
175
  // ── Summary ──────────────────────────────────────────────────────────────
171
176
  log('');
172
177
  const total = passed + failed;