@jaimevalasek/aioson 1.6.0 → 1.7.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.
- package/CHANGELOG.md +49 -0
- package/README.md +729 -232
- package/docs/design-previews/pt.squarespace.com-homepage.html +889 -0
- package/docs/integrations/sdlc-genius-boundary.md +76 -0
- package/docs/integrations/sdlc-genius-eval-matrix.md +75 -0
- package/docs/integrations/sdlc-genius-install-checklist.md +93 -0
- package/docs/integrations/sdlc-genius-review-samples.md +86 -0
- package/docs/pt/README.md +3 -0
- package/docs/pt/agentes.md +1 -0
- package/docs/pt/comandos-cli.md +888 -2
- package/docs/pt/design-hybrid-forge.md +255 -6
- package/docs/pt/devlog-pipeline.md +270 -0
- package/docs/pt/fluxo-artefatos.md +178 -0
- package/docs/pt/hooks-session-guard.md +454 -0
- package/docs/pt/monitor-de-contexto.md +59 -5
- package/docs/pt/sdd-automation-scripts.md +557 -0
- package/docs/pt/site-forge.md +309 -0
- package/docs/pt/spec-learnings-pipeline.md +265 -0
- package/package.json +1 -1
- package/src/a2a/client.js +165 -0
- package/src/a2a/server.js +223 -0
- package/src/cli.js +235 -1
- package/src/commands/agent-audit.js +397 -0
- package/src/commands/agent-export-skill.js +229 -0
- package/src/commands/artifact-validate.js +189 -0
- package/src/commands/brief-gen.js +405 -0
- package/src/commands/brief-validate.js +65 -0
- package/src/commands/classify.js +256 -0
- package/src/commands/context-compact.js +49 -0
- package/src/commands/context-health.js +175 -0
- package/src/commands/context-monitor.js +71 -0
- package/src/commands/context-trim.js +177 -0
- package/src/commands/detect-test-runner.js +55 -0
- package/src/commands/devlog-export-brains.js +27 -0
- package/src/commands/devlog-process.js +292 -0
- package/src/commands/devlog-watch.js +131 -0
- package/src/commands/feature-close.js +165 -0
- package/src/commands/gate-check.js +228 -0
- package/src/commands/hooks-emit.js +253 -0
- package/src/commands/hooks-install.js +347 -0
- package/src/commands/learning-auto-promote.js +195 -0
- package/src/commands/learning-evolve.js +18 -9
- package/src/commands/learning-export.js +103 -0
- package/src/commands/learning-rollback.js +164 -0
- package/src/commands/live.js +25 -1
- package/src/commands/pattern-detect.js +33 -0
- package/src/commands/preflight-context.js +30 -0
- package/src/commands/preflight.js +208 -0
- package/src/commands/pulse-update.js +130 -0
- package/src/commands/runner-daemon.js +274 -0
- package/src/commands/runner-plan.js +70 -0
- package/src/commands/runner-queue-from-plan.js +166 -0
- package/src/commands/runner-queue.js +189 -0
- package/src/commands/runner-run.js +129 -0
- package/src/commands/runtime.js +47 -1
- package/src/commands/self-implement-loop.js +256 -0
- package/src/commands/session-guard.js +218 -0
- package/src/commands/sizing.js +165 -0
- package/src/commands/skill.js +65 -0
- package/src/commands/spec-checkpoint.js +177 -0
- package/src/commands/spec-status.js +79 -0
- package/src/commands/spec-sync.js +190 -0
- package/src/commands/spec-tasks.js +288 -0
- package/src/commands/squad-autorun.js +1220 -0
- package/src/commands/squad-bus.js +217 -0
- package/src/commands/squad-card.js +149 -0
- package/src/commands/squad-daemon.js +134 -0
- package/src/commands/squad-dependency-graph.js +164 -0
- package/src/commands/squad-review.js +106 -0
- package/src/commands/squad-scaffold.js +55 -0
- package/src/commands/squad-tool-register.js +157 -0
- package/src/commands/state-save.js +122 -0
- package/src/commands/update.js +2 -0
- package/src/commands/verify-gate.js +572 -0
- package/src/commands/workflow-execute.js +241 -0
- package/src/constants.js +9 -0
- package/src/install-profile.js +2 -2
- package/src/install-wizard.js +3 -2
- package/src/installer.js +6 -0
- package/src/lib/health-check.js +158 -0
- package/src/lib/hook-protocol.js +76 -0
- package/src/mcp/apps/squad-dashboard/app.js +163 -0
- package/src/mcp/apps/squad-dashboard/index.html +261 -0
- package/src/mcp/apps/squad-dashboard/mcp-manifest.json +23 -0
- package/src/mcp/resources/squad-state.js +130 -0
- package/src/preflight-engine.js +443 -0
- package/src/runner/cascade.js +97 -0
- package/src/runner/cli-launcher.js +109 -0
- package/src/runner/plan-importer.js +63 -0
- package/src/runner/queue-store.js +159 -0
- package/src/runtime-store.js +61 -3
- package/src/squad/agent-teams-adapter.js +264 -0
- package/src/squad/brief-validator.js +350 -0
- package/src/squad/bus-bridge.js +140 -0
- package/src/squad/context-compactor.js +265 -0
- package/src/squad/cross-ai-synthesizer.js +250 -0
- package/src/squad/hooks-generator.js +196 -0
- package/src/squad/inter-squad-events.js +175 -0
- package/src/squad/intra-bus.js +345 -0
- package/src/squad/learning-extractor.js +213 -0
- package/src/squad/pattern-detector.js +365 -0
- package/src/squad/preflight-context.js +296 -0
- package/src/squad/recovery-context.js +242 -71
- package/src/squad/reflection.js +365 -0
- package/src/squad/squad-scaffold.js +177 -0
- package/src/squad/state-manager.js +310 -0
- package/src/squad/task-decomposer.js +652 -0
- package/src/squad/verify-gate.js +303 -0
- package/src/updater.js +4 -5
- package/src/worker-runner.js +186 -1
- package/template/.aioson/agents/analyst.md +62 -1
- package/template/.aioson/agents/architect.md +61 -1
- package/template/.aioson/agents/design-hybrid-forge.md +14 -0
- package/template/.aioson/agents/dev.md +242 -24
- package/template/.aioson/agents/deyvin.md +66 -8
- package/template/.aioson/agents/discovery-design-doc.md +44 -0
- package/template/.aioson/agents/genome.md +14 -0
- package/template/.aioson/agents/neo.md +78 -1
- package/template/.aioson/agents/orache.md +50 -4
- package/template/.aioson/agents/orchestrator.md +197 -1
- package/template/.aioson/agents/pm.md +35 -0
- package/template/.aioson/agents/product.md +50 -5
- package/template/.aioson/agents/profiler-enricher.md +14 -0
- package/template/.aioson/agents/profiler-forge.md +14 -0
- package/template/.aioson/agents/profiler-researcher.md +14 -0
- package/template/.aioson/agents/qa.md +172 -21
- package/template/.aioson/agents/setup.md +79 -9
- package/template/.aioson/agents/sheldon.md +131 -6
- package/template/.aioson/agents/site-forge.md +1753 -0
- package/template/.aioson/agents/squad.md +162 -0
- package/template/.aioson/agents/tester.md +53 -0
- package/template/.aioson/agents/ux-ui.md +34 -1
- package/template/.aioson/brains/README.md +128 -0
- package/template/.aioson/brains/_index.json +16 -0
- package/template/.aioson/brains/scripts/query.js +103 -0
- package/template/.aioson/brains/site-forge/visual-patterns.brain.json +205 -0
- package/template/.aioson/config.md +143 -13
- package/template/.aioson/constitution.md +33 -0
- package/template/.aioson/context/project-pulse.md +34 -0
- package/template/.aioson/docs/LAYERS.md +79 -0
- package/template/.aioson/docs/README.md +76 -0
- package/template/.aioson/docs/example-external-api-context.md +72 -0
- package/template/.aioson/locales/en/agents/architect.md +17 -0
- package/template/.aioson/locales/en/agents/dev.md +79 -13
- package/template/.aioson/locales/en/agents/orache.md +6 -0
- package/template/.aioson/locales/en/agents/orchestrator.md +24 -0
- package/template/.aioson/locales/en/agents/product.md +50 -0
- package/template/.aioson/locales/en/agents/sheldon.md +115 -0
- package/template/.aioson/locales/en/agents/squad.md +14 -0
- package/template/.aioson/locales/en/agents/tester.md +6 -0
- package/template/.aioson/locales/es/agents/analyst.md +2 -0
- package/template/.aioson/locales/es/agents/architect.md +19 -0
- package/template/.aioson/locales/es/agents/dev.md +64 -4
- package/template/.aioson/locales/es/agents/deyvin.md +2 -0
- package/template/.aioson/locales/es/agents/discovery-design-doc.md +2 -0
- package/template/.aioson/locales/es/agents/genome.md +2 -0
- package/template/.aioson/locales/es/agents/neo.md +2 -0
- package/template/.aioson/locales/es/agents/orache.md +2 -0
- package/template/.aioson/locales/es/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/es/agents/pair.md +2 -0
- package/template/.aioson/locales/es/agents/pm.md +2 -0
- package/template/.aioson/locales/es/agents/product.md +52 -0
- package/template/.aioson/locales/es/agents/profiler-enricher.md +2 -0
- package/template/.aioson/locales/es/agents/profiler-forge.md +2 -0
- package/template/.aioson/locales/es/agents/profiler-researcher.md +2 -0
- package/template/.aioson/locales/es/agents/qa.md +2 -0
- package/template/.aioson/locales/es/agents/setup.md +2 -0
- package/template/.aioson/locales/es/agents/sheldon.md +117 -0
- package/template/.aioson/locales/es/agents/squad.md +16 -0
- package/template/.aioson/locales/es/agents/tester.md +9 -0
- package/template/.aioson/locales/es/agents/ux-ui.md +2 -0
- package/template/.aioson/locales/fr/agents/analyst.md +2 -0
- package/template/.aioson/locales/fr/agents/architect.md +19 -0
- package/template/.aioson/locales/fr/agents/dev.md +64 -4
- package/template/.aioson/locales/fr/agents/deyvin.md +2 -0
- package/template/.aioson/locales/fr/agents/discovery-design-doc.md +2 -0
- package/template/.aioson/locales/fr/agents/genome.md +2 -0
- package/template/.aioson/locales/fr/agents/neo.md +2 -0
- package/template/.aioson/locales/fr/agents/orache.md +2 -0
- package/template/.aioson/locales/fr/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/fr/agents/pair.md +2 -0
- package/template/.aioson/locales/fr/agents/pm.md +2 -0
- package/template/.aioson/locales/fr/agents/product.md +52 -0
- package/template/.aioson/locales/fr/agents/profiler-enricher.md +2 -0
- package/template/.aioson/locales/fr/agents/profiler-forge.md +2 -0
- package/template/.aioson/locales/fr/agents/profiler-researcher.md +2 -0
- package/template/.aioson/locales/fr/agents/qa.md +2 -0
- package/template/.aioson/locales/fr/agents/setup.md +2 -0
- package/template/.aioson/locales/fr/agents/sheldon.md +117 -0
- package/template/.aioson/locales/fr/agents/squad.md +16 -0
- package/template/.aioson/locales/fr/agents/tester.md +9 -0
- package/template/.aioson/locales/fr/agents/ux-ui.md +2 -0
- package/template/.aioson/locales/pt-BR/agents/analyst.md +64 -3
- package/template/.aioson/locales/pt-BR/agents/architect.md +42 -0
- package/template/.aioson/locales/pt-BR/agents/dev.md +147 -14
- package/template/.aioson/locales/pt-BR/agents/deyvin.md +47 -0
- package/template/.aioson/locales/pt-BR/agents/neo.md +62 -1
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +158 -2
- package/template/.aioson/locales/pt-BR/agents/pm.md +95 -1
- package/template/.aioson/locales/pt-BR/agents/product.md +145 -18
- package/template/.aioson/locales/pt-BR/agents/qa.md +16 -0
- package/template/.aioson/locales/pt-BR/agents/setup.md +101 -18
- package/template/.aioson/locales/pt-BR/agents/sheldon.md +132 -1
- package/template/.aioson/locales/pt-BR/agents/squad.md +14 -0
- package/template/.aioson/locales/pt-BR/agents/tester.md +449 -0
- package/template/.aioson/rules/README.md +69 -0
- package/template/.aioson/rules/data-format-convention.md +136 -0
- package/template/.aioson/rules/example-monetary-values.md +30 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +124 -3
- package/template/.aioson/skills/design/pt.squarespace.com/.skill-meta.json +31 -0
- package/template/.aioson/skills/design/pt.squarespace.com/SKILL.md +66 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +368 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/design-tokens.md +150 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/motion.md +270 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/patterns.md +189 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +165 -0
- package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +1 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/analyst.md +30 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +23 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +47 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/deyvin.md +27 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/maintenance-and-state.md +35 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/product.md +25 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +30 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/sheldon.md +25 -0
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +4 -1
- package/template/.aioson/skills/process/design-hybrid-forge/references/output-contract.md +15 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/pair-compatibility.md +32 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/quality-gates.md +20 -0
- package/template/.aioson/skills/process/simplify/SKILL.md +173 -0
- package/template/.aioson/skills/static/context-budget-guide.md +46 -0
- package/template/.aioson/skills/static/harness-sensors.md +74 -0
- package/template/.aioson/skills/static/multi-agent-patterns.md +43 -0
- package/template/.aioson/skills/static/react-motion-patterns.md +22 -0
- package/template/.aioson/skills/static/static-html-patterns/checklists.md +43 -0
- package/template/.aioson/skills/static/static-html-patterns/css-tokens.md +609 -0
- package/template/.aioson/skills/static/static-html-patterns/motion.md +193 -0
- package/template/.aioson/skills/static/static-html-patterns/premium.md +711 -0
- package/template/.aioson/skills/static/static-html-patterns/structure.md +209 -0
- package/template/.aioson/skills/static/static-html-patterns/utilities.md +190 -0
- package/template/.aioson/skills/static/static-html-patterns.md +58 -1913
- package/template/.aioson/skills/static/threejs-patterns.md +929 -0
- package/template/.aioson/skills/static/web-research-cache.md +112 -0
- package/template/.aioson/tasks/implementation-plan.md +21 -1
- package/template/.claude/commands/aioson/agent/design-hybrid-forge.md +5 -0
- package/template/.claude/commands/aioson/agent/orache.md +5 -0
- package/template/.claude/commands/aioson/agent/sheldon.md +5 -0
- package/template/.claude/commands/aioson/agent/site-forge.md +5 -0
- package/template/AGENTS.md +55 -3
- package/template/CLAUDE.md +30 -0
- package/template/OPENCODE.md +4 -0
- package/template/researchs/.gitkeep +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inter-Squad Event Streaming — publish/subscribe over SQLite
|
|
5
|
+
*
|
|
6
|
+
* Squads publish typed events (e.g. 'episode.created') and subscribe
|
|
7
|
+
* to patterns (e.g. 'episode.*'). Consumers receive events at the
|
|
8
|
+
* start of each squad:autorun run.
|
|
9
|
+
*
|
|
10
|
+
* Usage in squad manifests:
|
|
11
|
+
* "subscriptions": ["episode.*", "review.completed"]
|
|
12
|
+
* "publishes": ["episode.created"]
|
|
13
|
+
*
|
|
14
|
+
* Table: inter_squad_events (created in runtime-store.js)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { randomUUID } = require('node:crypto');
|
|
18
|
+
const { openRuntimeDb } = require('../runtime-store');
|
|
19
|
+
|
|
20
|
+
function nowIso() { return new Date().toISOString(); }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Publish an event from a squad.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} projectDir
|
|
26
|
+
* @param {{ fromSquad: string, event: string, payload?: object }} opts
|
|
27
|
+
* @returns {Promise<string|null>} event id, or null if db unavailable
|
|
28
|
+
*/
|
|
29
|
+
async function publish(projectDir, { fromSquad, event, payload = null }) {
|
|
30
|
+
const handle = await openRuntimeDb(projectDir);
|
|
31
|
+
if (!handle) return null;
|
|
32
|
+
const { db } = handle;
|
|
33
|
+
try {
|
|
34
|
+
const id = randomUUID();
|
|
35
|
+
db.prepare(`
|
|
36
|
+
INSERT INTO inter_squad_events (id, from_squad, event, payload, created_at, consumed_by, ttl_hours)
|
|
37
|
+
VALUES (?, ?, ?, ?, ?, '[]', 48)
|
|
38
|
+
`).run(id, fromSquad, event, payload ? JSON.stringify(payload) : null, nowIso());
|
|
39
|
+
return id;
|
|
40
|
+
} finally {
|
|
41
|
+
db.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Consume pending events for a squad.
|
|
47
|
+
* Marks consumed events so they are not returned again for this squad.
|
|
48
|
+
*
|
|
49
|
+
* Pattern matching:
|
|
50
|
+
* 'episode.*' matches 'episode.created', 'episode.updated' (one segment after prefix)
|
|
51
|
+
* '*' matches any event
|
|
52
|
+
* 'exact.name' matches that exact event name only
|
|
53
|
+
*
|
|
54
|
+
* @param {string} projectDir
|
|
55
|
+
* @param {{ toSquad: string, subscriptions: string[] }} opts
|
|
56
|
+
* @returns {Promise<Array<{ id, fromSquad, event, payload, createdAt }>>}
|
|
57
|
+
*/
|
|
58
|
+
async function consume(projectDir, { toSquad, subscriptions = [] }) {
|
|
59
|
+
if (subscriptions.length === 0) return [];
|
|
60
|
+
|
|
61
|
+
const handle = await openRuntimeDb(projectDir, { mustExist: true });
|
|
62
|
+
if (!handle) return [];
|
|
63
|
+
const { db } = handle;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// TTL cleanup: remove events older than their ttl_hours
|
|
67
|
+
db.prepare(`
|
|
68
|
+
DELETE FROM inter_squad_events
|
|
69
|
+
WHERE datetime(created_at, '+' || ttl_hours || ' hours') < datetime('now')
|
|
70
|
+
`).run();
|
|
71
|
+
|
|
72
|
+
const rows = db.prepare(`
|
|
73
|
+
SELECT * FROM inter_squad_events ORDER BY created_at ASC
|
|
74
|
+
`).all();
|
|
75
|
+
|
|
76
|
+
const matching = [];
|
|
77
|
+
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const consumed = JSON.parse(row.consumed_by || '[]');
|
|
80
|
+
if (consumed.includes(toSquad)) continue;
|
|
81
|
+
|
|
82
|
+
const matched = subscriptions.some((pattern) => matchesPattern(row.event, pattern));
|
|
83
|
+
if (!matched) continue;
|
|
84
|
+
|
|
85
|
+
consumed.push(toSquad);
|
|
86
|
+
db.prepare(`UPDATE inter_squad_events SET consumed_by = ? WHERE id = ?`)
|
|
87
|
+
.run(JSON.stringify(consumed), row.id);
|
|
88
|
+
|
|
89
|
+
matching.push({
|
|
90
|
+
id: row.id,
|
|
91
|
+
fromSquad: row.from_squad,
|
|
92
|
+
event: row.event,
|
|
93
|
+
payload: row.payload ? JSON.parse(row.payload) : null,
|
|
94
|
+
createdAt: row.created_at
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return matching;
|
|
99
|
+
} finally {
|
|
100
|
+
db.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pattern matching for inter-squad event subscriptions.
|
|
106
|
+
* '*' → any event
|
|
107
|
+
* 'episode.*' → 'episode.created', 'episode.updated' (exactly one segment after prefix, no deeper nesting)
|
|
108
|
+
* 'exact.name' → only that exact event name
|
|
109
|
+
*/
|
|
110
|
+
function matchesPattern(event, pattern) {
|
|
111
|
+
if (pattern === '*') return true;
|
|
112
|
+
if (!pattern.includes('*')) return pattern === event;
|
|
113
|
+
// Trailing '.*': match exactly one additional dot-separated segment
|
|
114
|
+
if (pattern.endsWith('.*')) {
|
|
115
|
+
const prefix = pattern.slice(0, -2); // e.g. 'episode'
|
|
116
|
+
if (!event.startsWith(prefix + '.')) return false;
|
|
117
|
+
const remainder = event.slice(prefix.length + 1); // e.g. 'created'
|
|
118
|
+
return remainder.length > 0 && !remainder.includes('.');
|
|
119
|
+
}
|
|
120
|
+
// Other glob patterns not supported → exact match only
|
|
121
|
+
return pattern === event;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── A2A Remote Backend (Plan 81 §3.2) ───────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Publish an event to remote A2A peers (if configured in manifest).
|
|
128
|
+
*
|
|
129
|
+
* @param {string} projectDir
|
|
130
|
+
* @param {{ fromSquad: string, event: string, payload?: object }} eventData
|
|
131
|
+
* @param {{ peers: Array<{ name: string, url: string }> }} a2aConfig
|
|
132
|
+
* @returns {Promise<object[]>} — results per peer
|
|
133
|
+
*/
|
|
134
|
+
async function publishRemote(projectDir, eventData, a2aConfig) {
|
|
135
|
+
if (!a2aConfig || !a2aConfig.peers || a2aConfig.peers.length === 0) return [];
|
|
136
|
+
|
|
137
|
+
let publishEvent;
|
|
138
|
+
try {
|
|
139
|
+
({ publishEvent } = require('../a2a/client'));
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const results = [];
|
|
145
|
+
for (const peer of a2aConfig.peers) {
|
|
146
|
+
const result = await publishEvent(peer.url, eventData).catch((err) => ({
|
|
147
|
+
ok: false, error: err.message
|
|
148
|
+
}));
|
|
149
|
+
results.push({ peer: peer.name, ...result });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Enhanced publish: local + optional remote A2A.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} projectDir
|
|
159
|
+
* @param {{ fromSquad: string, event: string, payload?: object }} eventData
|
|
160
|
+
* @param {{ remote?: boolean, a2a?: object }} options
|
|
161
|
+
*/
|
|
162
|
+
async function publishWithA2A(projectDir, eventData, options = {}) {
|
|
163
|
+
// Always publish locally
|
|
164
|
+
const localId = await publish(projectDir, eventData);
|
|
165
|
+
|
|
166
|
+
// Optionally publish to A2A peers
|
|
167
|
+
let remoteResults = [];
|
|
168
|
+
if (options.remote && options.a2a) {
|
|
169
|
+
remoteResults = await publishRemote(projectDir, eventData, options.a2a);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { localId, remoteResults };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { publish, consume, matchesPattern, publishWithA2A, publishRemote };
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Intra-squad message bus
|
|
5
|
+
*
|
|
6
|
+
* Executors within the same squad session write and read from a shared
|
|
7
|
+
* append-only bus file. This enables real-time communication between
|
|
8
|
+
* agents without going through the coordinator.
|
|
9
|
+
*
|
|
10
|
+
* Bus file location:
|
|
11
|
+
* .aioson/squads/{squadSlug}/sessions/{sessionId}/bus.jsonl
|
|
12
|
+
*
|
|
13
|
+
* Message schema:
|
|
14
|
+
* { id, session_id, from, to, type, content, ts, metadata }
|
|
15
|
+
*
|
|
16
|
+
* Message types:
|
|
17
|
+
* finding — executor discovered something relevant to others
|
|
18
|
+
* feedback — critique or review of another executor's output
|
|
19
|
+
* question — executor needs input from a peer
|
|
20
|
+
* result — executor completed a task, sharing output summary
|
|
21
|
+
* status — executor announcing current activity (mirrors task list)
|
|
22
|
+
* block — executor is blocked, needs coordinator or peer help
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs/promises');
|
|
26
|
+
const fsSync = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const { randomUUID } = require('node:crypto');
|
|
29
|
+
|
|
30
|
+
const BUS_FILE = 'bus.jsonl';
|
|
31
|
+
const VALID_TYPES = new Set([
|
|
32
|
+
'finding', // executor discovered something relevant to others
|
|
33
|
+
'feedback', // critique or review of another executor's output
|
|
34
|
+
'question', // executor needs input from a peer
|
|
35
|
+
'result', // executor completed a task, sharing output summary
|
|
36
|
+
'status', // executor announcing current activity
|
|
37
|
+
'block', // executor is blocked, needs coordinator or peer help
|
|
38
|
+
'resolution', // coordinator resolved a block message
|
|
39
|
+
'gap_closure_attempt', // coordinator retrying a failed task with context
|
|
40
|
+
'heartbeat', // coordinator alive-check during long operations
|
|
41
|
+
'tool_request' // executor requesting dynamic tool registration (4.2)
|
|
42
|
+
]);
|
|
43
|
+
const DEFAULT_POLL_MS = 1500;
|
|
44
|
+
const MAX_WATCH_TIMEOUT_MS = 60 * 60 * 1000; // 1h hard cap
|
|
45
|
+
|
|
46
|
+
function busDir(projectDir, squadSlug, sessionId) {
|
|
47
|
+
return path.join(projectDir, '.aioson', 'squads', squadSlug, 'sessions', sessionId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function busPath(projectDir, squadSlug, sessionId) {
|
|
51
|
+
return path.join(busDir(projectDir, squadSlug, sessionId), BUS_FILE);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nowIso() {
|
|
55
|
+
return new Date().toISOString();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Post a message to the bus.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} projectDir
|
|
62
|
+
* @param {string} squadSlug
|
|
63
|
+
* @param {string} sessionId
|
|
64
|
+
* @param {object} msg — { from, to?, type, content, metadata? }
|
|
65
|
+
* @returns {Promise<object>} Full message with id and ts
|
|
66
|
+
*/
|
|
67
|
+
async function post(projectDir, squadSlug, sessionId, msg) {
|
|
68
|
+
const { from, to = '*', type, content, metadata = {} } = msg;
|
|
69
|
+
|
|
70
|
+
if (!from) throw new Error('bus.post: msg.from is required');
|
|
71
|
+
if (!content) throw new Error('bus.post: msg.content is required');
|
|
72
|
+
if (!VALID_TYPES.has(type)) {
|
|
73
|
+
throw new Error(`bus.post: invalid type "${type}". Valid: ${[...VALID_TYPES].join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const entry = {
|
|
77
|
+
id: randomUUID(),
|
|
78
|
+
session_id: sessionId,
|
|
79
|
+
from,
|
|
80
|
+
to,
|
|
81
|
+
type,
|
|
82
|
+
content: String(content),
|
|
83
|
+
ts: nowIso(),
|
|
84
|
+
metadata
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const dir = busDir(projectDir, squadSlug, sessionId);
|
|
88
|
+
await fs.mkdir(dir, { recursive: true });
|
|
89
|
+
await fs.appendFile(busPath(projectDir, squadSlug, sessionId), JSON.stringify(entry) + '\n', 'utf8');
|
|
90
|
+
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read messages from the bus.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} projectDir
|
|
98
|
+
* @param {string} squadSlug
|
|
99
|
+
* @param {string} sessionId
|
|
100
|
+
* @param {object} [filters] — { from?, to?, type?, since? (ISO string), last? (N messages) }
|
|
101
|
+
* @returns {Promise<object[]>}
|
|
102
|
+
*/
|
|
103
|
+
async function read(projectDir, squadSlug, sessionId, filters = {}) {
|
|
104
|
+
const filePath = busPath(projectDir, squadSlug, sessionId);
|
|
105
|
+
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = await fs.readFile(filePath, 'utf8');
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
114
|
+
let messages = [];
|
|
115
|
+
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
try {
|
|
118
|
+
messages.push(JSON.parse(line));
|
|
119
|
+
} catch {
|
|
120
|
+
// skip malformed lines
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { from, to, type, since, last } = filters;
|
|
125
|
+
|
|
126
|
+
if (from) messages = messages.filter((m) => m.from === from);
|
|
127
|
+
if (to) messages = messages.filter((m) => m.to === to || m.to === '*');
|
|
128
|
+
if (type) {
|
|
129
|
+
const types = Array.isArray(type) ? type : [type];
|
|
130
|
+
messages = messages.filter((m) => types.includes(m.type));
|
|
131
|
+
}
|
|
132
|
+
if (since) {
|
|
133
|
+
const sinceMs = new Date(since).getTime();
|
|
134
|
+
messages = messages.filter((m) => new Date(m.ts).getTime() > sinceMs);
|
|
135
|
+
}
|
|
136
|
+
if (last && last > 0) {
|
|
137
|
+
messages = messages.slice(-last);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return messages;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Watch the bus for new messages.
|
|
145
|
+
* Polls the file at a configurable interval.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} projectDir
|
|
148
|
+
* @param {string} squadSlug
|
|
149
|
+
* @param {string} sessionId
|
|
150
|
+
* @param {function} onMessage — called with each new message
|
|
151
|
+
* @param {object} [options] — { pollMs?, timeoutMs?, to?, type? }
|
|
152
|
+
* @returns {function} stop — call to stop watching
|
|
153
|
+
*/
|
|
154
|
+
function watch(projectDir, squadSlug, sessionId, onMessage, options = {}) {
|
|
155
|
+
const {
|
|
156
|
+
pollMs = DEFAULT_POLL_MS,
|
|
157
|
+
timeoutMs = MAX_WATCH_TIMEOUT_MS,
|
|
158
|
+
to,
|
|
159
|
+
type
|
|
160
|
+
} = options;
|
|
161
|
+
|
|
162
|
+
let lastTs = new Date().toISOString();
|
|
163
|
+
let stopped = false;
|
|
164
|
+
|
|
165
|
+
const hardStop = setTimeout(() => {
|
|
166
|
+
stopped = true;
|
|
167
|
+
}, timeoutMs);
|
|
168
|
+
|
|
169
|
+
async function poll() {
|
|
170
|
+
if (stopped) return;
|
|
171
|
+
|
|
172
|
+
const newMessages = await read(projectDir, squadSlug, sessionId, { since: lastTs, to, type });
|
|
173
|
+
|
|
174
|
+
for (const msg of newMessages) {
|
|
175
|
+
if (msg.ts > lastTs) {
|
|
176
|
+
lastTs = msg.ts;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await onMessage(msg);
|
|
180
|
+
} catch {
|
|
181
|
+
// handler errors do not stop the watch
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!stopped) {
|
|
186
|
+
setTimeout(poll, pollMs);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Start polling after one tick so caller can capture stop fn first
|
|
191
|
+
setTimeout(poll, pollMs);
|
|
192
|
+
|
|
193
|
+
return function stop() {
|
|
194
|
+
stopped = true;
|
|
195
|
+
clearTimeout(hardStop);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Read messages posted since a given timestamp, then return the last timestamp seen.
|
|
201
|
+
* Useful for coordinator polling without keeping a watcher alive.
|
|
202
|
+
*
|
|
203
|
+
* @returns {Promise<{ messages: object[], lastTs: string }>}
|
|
204
|
+
*/
|
|
205
|
+
async function poll(projectDir, squadSlug, sessionId, since, filters = {}) {
|
|
206
|
+
const messages = await read(projectDir, squadSlug, sessionId, { ...filters, since });
|
|
207
|
+
let lastTs = since;
|
|
208
|
+
for (const m of messages) {
|
|
209
|
+
if (!lastTs || m.ts > lastTs) lastTs = m.ts;
|
|
210
|
+
}
|
|
211
|
+
return { messages, lastTs: lastTs || nowIso() };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get a summary of bus activity for a session.
|
|
216
|
+
* Useful for coordinator to get a quick status overview.
|
|
217
|
+
*
|
|
218
|
+
* @returns {Promise<object>} summary
|
|
219
|
+
*/
|
|
220
|
+
async function summary(projectDir, squadSlug, sessionId) {
|
|
221
|
+
const messages = await read(projectDir, squadSlug, sessionId);
|
|
222
|
+
|
|
223
|
+
if (messages.length === 0) {
|
|
224
|
+
return { total: 0, by_type: {}, by_executor: {}, latest: null, blocks: [] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const by_type = {};
|
|
228
|
+
const by_executor = {};
|
|
229
|
+
const blocks = [];
|
|
230
|
+
|
|
231
|
+
for (const m of messages) {
|
|
232
|
+
by_type[m.type] = (by_type[m.type] || 0) + 1;
|
|
233
|
+
by_executor[m.from] = (by_executor[m.from] || 0) + 1;
|
|
234
|
+
if (m.type === 'block') blocks.push({ from: m.from, content: m.content, ts: m.ts });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
total: messages.length,
|
|
239
|
+
by_type,
|
|
240
|
+
by_executor,
|
|
241
|
+
latest: messages[messages.length - 1],
|
|
242
|
+
blocks,
|
|
243
|
+
first_ts: messages[0].ts,
|
|
244
|
+
last_ts: messages[messages.length - 1].ts
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Clear the bus for a session (delete the file).
|
|
250
|
+
* Use with caution — irreversible.
|
|
251
|
+
*/
|
|
252
|
+
async function clear(projectDir, squadSlug, sessionId) {
|
|
253
|
+
const filePath = busPath(projectDir, squadSlug, sessionId);
|
|
254
|
+
try {
|
|
255
|
+
await fs.unlink(filePath);
|
|
256
|
+
return { ok: true };
|
|
257
|
+
} catch {
|
|
258
|
+
return { ok: false, reason: 'file_not_found' };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* List all sessions that have a bus file for a given squad.
|
|
264
|
+
*/
|
|
265
|
+
async function listSessions(projectDir, squadSlug) {
|
|
266
|
+
const sessionsDir = path.join(projectDir, '.aioson', 'squads', squadSlug, 'sessions');
|
|
267
|
+
let entries;
|
|
268
|
+
try {
|
|
269
|
+
entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
270
|
+
} catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = [];
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (!entry.isDirectory()) continue;
|
|
277
|
+
const busFile = path.join(sessionsDir, entry.name, BUS_FILE);
|
|
278
|
+
try {
|
|
279
|
+
const stat = await fs.stat(busFile);
|
|
280
|
+
result.push({
|
|
281
|
+
session_id: entry.name,
|
|
282
|
+
bus_path: path.relative(projectDir, busFile),
|
|
283
|
+
size_bytes: stat.size,
|
|
284
|
+
modified_at: stat.mtime.toISOString()
|
|
285
|
+
});
|
|
286
|
+
} catch {
|
|
287
|
+
// no bus file in this session — skip
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return result.sort((a, b) => b.modified_at.localeCompare(a.modified_at));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Detect if an executor is in an analysis loop.
|
|
296
|
+
*
|
|
297
|
+
* Returns true if the executor has posted `threshold` or more consecutive
|
|
298
|
+
* `status` messages without a `result` in between.
|
|
299
|
+
*
|
|
300
|
+
* @param {object[]} messages — All bus messages for the session
|
|
301
|
+
* @param {string} executorSlug
|
|
302
|
+
* @param {number} [threshold=8]
|
|
303
|
+
* @returns {boolean}
|
|
304
|
+
*/
|
|
305
|
+
function isAnalysisLoop(messages, executorSlug, threshold = 8) {
|
|
306
|
+
let count = 0;
|
|
307
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
308
|
+
const m = messages[i];
|
|
309
|
+
if (m.from !== executorSlug) continue;
|
|
310
|
+
if (m.type === 'result') break;
|
|
311
|
+
if (m.type === 'status') count++;
|
|
312
|
+
}
|
|
313
|
+
return count >= threshold;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Count unresolved block messages for a session.
|
|
318
|
+
* A block is resolved when a `resolution` message exists with
|
|
319
|
+
* metadata.block_id matching the block's id.
|
|
320
|
+
*
|
|
321
|
+
* @returns {object[]} unresolved block messages
|
|
322
|
+
*/
|
|
323
|
+
function getUnresolvedBlocks(messages) {
|
|
324
|
+
const resolvedIds = new Set(
|
|
325
|
+
messages
|
|
326
|
+
.filter((m) => m.type === 'resolution' && m.metadata && m.metadata.block_id)
|
|
327
|
+
.map((m) => m.metadata.block_id)
|
|
328
|
+
);
|
|
329
|
+
return messages.filter((m) => m.type === 'block' && !resolvedIds.has(m.id));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
post,
|
|
334
|
+
read,
|
|
335
|
+
watch,
|
|
336
|
+
poll,
|
|
337
|
+
summary,
|
|
338
|
+
clear,
|
|
339
|
+
listSessions,
|
|
340
|
+
busPath,
|
|
341
|
+
busDir,
|
|
342
|
+
VALID_TYPES,
|
|
343
|
+
isAnalysisLoop,
|
|
344
|
+
getUnresolvedBlocks
|
|
345
|
+
};
|