@jaimevalasek/aioson 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -1
- package/LICENSE +661 -21
- package/README.md +3 -1
- package/docs/en/squad-dashboard.md +372 -0
- package/docs/openclaw-bridge.md +308 -0
- package/docs/pt/agentes.md +124 -10
- package/docs/pt/cenarios.md +46 -2
- package/docs/pt/comandos-cli.md +60 -1
- package/docs/pt/inicio-rapido.md +18 -2
- package/docs/pt/squad-dashboard.md +373 -0
- package/docs/testing/genome-2.0-matrix.md +5 -5
- package/docs/testing/genome-2.0-rollout.md +9 -9
- package/package.json +2 -2
- package/src/backup-local.js +74 -0
- package/src/cli.js +98 -0
- package/src/commands/backup-local-cmd.js +25 -0
- package/src/commands/runtime.js +242 -0
- package/src/commands/setup-context.js +7 -2
- package/src/commands/squad-daemon.js +209 -0
- package/src/commands/squad-dashboard.js +39 -0
- package/src/commands/squad-deploy.js +64 -0
- package/src/commands/squad-doctor.js +52 -0
- package/src/commands/squad-mcp.js +270 -0
- package/src/commands/squad-processes.js +56 -0
- package/src/commands/squad-recovery.js +42 -0
- package/src/commands/squad-roi.js +291 -0
- package/src/commands/squad-score.js +250 -0
- package/src/commands/squad-status.js +37 -1
- package/src/commands/squad-validate.js +62 -1
- package/src/commands/squad-webhook.js +160 -0
- package/src/commands/squad-worker.js +191 -0
- package/src/commands/squad-worktrees.js +75 -0
- package/src/commands/web-map.js +70 -0
- package/src/commands/web-scrape.js +71 -0
- package/src/constants.js +8 -0
- package/src/context-writer.js +45 -1
- package/src/i18n/messages/en.js +127 -1
- package/src/i18n/messages/es.js +117 -0
- package/src/i18n/messages/fr.js +117 -0
- package/src/i18n/messages/pt-BR.js +126 -1
- package/src/lib/webhook-server.js +328 -0
- package/src/mcp-connectors/registry.js +602 -0
- package/src/runtime-store.js +259 -2
- package/src/squad/external-session.js +180 -0
- package/src/squad/inter-squad.js +74 -0
- package/src/squad/recovery-context.js +201 -0
- package/src/squad/worktree-manager.js +114 -0
- package/src/squad-daemon.js +490 -0
- package/src/squad-dashboard/api.js +223 -0
- package/src/squad-dashboard/attachment-handler.js +93 -0
- package/src/squad-dashboard/context-monitor.js +157 -0
- package/src/squad-dashboard/execution-logs.js +115 -0
- package/src/squad-dashboard/hunk-review.js +209 -0
- package/src/squad-dashboard/metrics.js +133 -0
- package/src/squad-dashboard/process-monitor.js +125 -0
- package/src/squad-dashboard/renderer.js +858 -0
- package/src/squad-dashboard/server.js +232 -0
- package/src/squad-dashboard/styles.js +525 -0
- package/src/squad-dashboard/token-tracker.js +99 -0
- package/src/web.js +284 -0
- package/src/worker-runner.js +339 -0
- package/template/.aioson/agents/analyst.md +4 -0
- package/template/.aioson/agents/architect.md +4 -0
- package/template/.aioson/agents/dev.md +120 -11
- package/template/.aioson/agents/deyvin.md +8 -0
- package/template/.aioson/agents/neo.md +152 -0
- package/template/.aioson/agents/orache.md +17 -0
- package/template/.aioson/agents/orchestrator.md +26 -0
- package/template/.aioson/agents/product.md +60 -12
- package/template/.aioson/agents/qa.md +1 -0
- package/template/.aioson/agents/setup.md +63 -19
- package/template/.aioson/agents/sheldon.md +603 -0
- package/template/.aioson/agents/squad.md +191 -0
- package/template/.aioson/agents/tester.md +254 -0
- package/template/.aioson/agents/ux-ui.md +12 -0
- package/template/.aioson/config.md +6 -0
- package/template/.aioson/locales/en/agents/analyst.md +8 -0
- package/template/.aioson/locales/en/agents/architect.md +8 -0
- package/template/.aioson/locales/en/agents/dev.md +66 -7
- package/template/.aioson/locales/en/agents/deyvin.md +8 -0
- package/template/.aioson/locales/en/agents/neo.md +8 -0
- package/template/.aioson/locales/en/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/en/agents/qa.md +49 -0
- package/template/.aioson/locales/en/agents/setup.md +2 -1
- package/template/.aioson/locales/en/agents/sheldon.md +340 -0
- package/template/.aioson/locales/en/agents/ux-ui.md +8 -0
- package/template/.aioson/locales/es/agents/analyst.md +8 -0
- package/template/.aioson/locales/es/agents/architect.md +8 -0
- package/template/.aioson/locales/es/agents/dev.md +66 -7
- package/template/.aioson/locales/es/agents/deyvin.md +8 -0
- package/template/.aioson/locales/es/agents/neo.md +48 -0
- package/template/.aioson/locales/es/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/es/agents/qa.md +26 -0
- package/template/.aioson/locales/es/agents/setup.md +2 -1
- package/template/.aioson/locales/es/agents/sheldon.md +192 -0
- package/template/.aioson/locales/es/agents/squad.md +63 -0
- package/template/.aioson/locales/es/agents/ux-ui.md +8 -0
- package/template/.aioson/locales/fr/agents/analyst.md +8 -0
- package/template/.aioson/locales/fr/agents/architect.md +8 -0
- package/template/.aioson/locales/fr/agents/dev.md +66 -7
- package/template/.aioson/locales/fr/agents/deyvin.md +8 -0
- package/template/.aioson/locales/fr/agents/neo.md +48 -0
- package/template/.aioson/locales/fr/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/fr/agents/qa.md +26 -0
- package/template/.aioson/locales/fr/agents/setup.md +2 -1
- package/template/.aioson/locales/fr/agents/sheldon.md +192 -0
- package/template/.aioson/locales/fr/agents/squad.md +63 -0
- package/template/.aioson/locales/fr/agents/ux-ui.md +8 -0
- package/template/.aioson/locales/pt-BR/agents/analyst.md +19 -0
- package/template/.aioson/locales/pt-BR/agents/architect.md +19 -0
- package/template/.aioson/locales/pt-BR/agents/dev.md +75 -12
- package/template/.aioson/locales/pt-BR/agents/deyvin.md +8 -0
- package/template/.aioson/locales/pt-BR/agents/neo.md +147 -0
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/pt-BR/agents/product.md +8 -3
- package/template/.aioson/locales/pt-BR/agents/qa.md +60 -0
- package/template/.aioson/locales/pt-BR/agents/setup.md +2 -1
- package/template/.aioson/locales/pt-BR/agents/sheldon.md +192 -0
- package/template/.aioson/locales/pt-BR/agents/squad.md +105 -0
- package/template/.aioson/locales/pt-BR/agents/ux-ui.md +8 -0
- package/template/.aioson/schemas/squad-blueprint.schema.json +21 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +178 -1
- package/template/.aioson/skills/design/bold-editorial-ui/SKILL.md +205 -0
- package/template/.aioson/skills/design/bold-editorial-ui/references/art-direction.md +338 -0
- package/template/.aioson/skills/design/bold-editorial-ui/references/components.md +977 -0
- package/template/.aioson/skills/design/bold-editorial-ui/references/dashboards.md +218 -0
- package/template/.aioson/skills/design/bold-editorial-ui/references/design-tokens.md +326 -0
- package/template/.aioson/skills/design/bold-editorial-ui/references/motion.md +461 -0
- package/template/.aioson/skills/design/bold-editorial-ui/references/patterns.md +293 -0
- package/template/.aioson/skills/design/bold-editorial-ui/references/websites.md +352 -0
- package/template/.aioson/skills/design/clean-saas-ui/SKILL.md +210 -0
- package/template/.aioson/skills/design/clean-saas-ui/references/art-direction.md +319 -0
- package/template/.aioson/skills/design/clean-saas-ui/references/components.md +365 -0
- package/template/.aioson/skills/design/clean-saas-ui/references/dashboards.md +196 -0
- package/template/.aioson/skills/design/clean-saas-ui/references/design-tokens.md +244 -0
- package/template/.aioson/skills/design/clean-saas-ui/references/motion.md +235 -0
- package/template/.aioson/skills/design/clean-saas-ui/references/patterns.md +215 -0
- package/template/.aioson/skills/design/clean-saas-ui/references/websites.md +295 -0
- package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +55 -9
- package/template/.aioson/skills/design/cognitive-core-ui/references/art-direction.md +339 -0
- package/template/.aioson/skills/design/cognitive-core-ui/references/components.md +1 -1
- package/template/.aioson/skills/design/cognitive-core-ui/references/dashboards.md +100 -0
- package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +43 -9
- package/template/.aioson/skills/design/cognitive-core-ui/references/motion.md +40 -0
- package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +1 -1
- package/template/.aioson/skills/design/cognitive-core-ui/references/websites.md +99 -12
- package/template/.aioson/skills/design/warm-craft-ui/SKILL.md +209 -0
- package/template/.aioson/skills/design/warm-craft-ui/references/art-direction.md +324 -0
- package/template/.aioson/skills/design/warm-craft-ui/references/components.md +508 -0
- package/template/.aioson/skills/design/warm-craft-ui/references/dashboards.md +223 -0
- package/template/.aioson/skills/design/warm-craft-ui/references/design-tokens.md +374 -0
- package/template/.aioson/skills/design/warm-craft-ui/references/motion.md +356 -0
- package/template/.aioson/skills/design/warm-craft-ui/references/patterns.md +288 -0
- package/template/.aioson/skills/design/warm-craft-ui/references/websites.md +289 -0
- package/template/.aioson/skills/premium-visual-design/SKILL.md +83 -0
- package/template/.aioson/skills/premium-visual-design/components/agent-badge.md +92 -0
- package/template/.aioson/skills/premium-visual-design/components/dependency-node.md +102 -0
- package/template/.aioson/skills/premium-visual-design/components/mention-autocomplete.md +136 -0
- package/template/.aioson/skills/premium-visual-design/components/notification-center.md +136 -0
- package/template/.aioson/skills/premium-visual-design/components/review-action-bar.md +188 -0
- package/template/.aioson/skills/premium-visual-design/components/team-switcher.md +131 -0
- package/template/.aioson/skills/premium-visual-design/patterns/agent-message-thread.md +198 -0
- package/template/.aioson/skills/premium-visual-design/patterns/notification-panel.md +275 -0
- package/template/.aioson/skills/premium-visual-design/patterns/review-workflow-ui.md +234 -0
- package/template/.aioson/skills/premium-visual-design/patterns/task-dependency-graph.md +147 -0
- package/template/.aioson/skills/premium-visual-design/tokens/status-extended.md +142 -0
- package/template/.aioson/skills/squad/formats/catalog.json +15 -0
- package/template/.aioson/skills/squad/formats/content/blog-post.md +47 -0
- package/template/.aioson/skills/squad/formats/content/newsletter.md +47 -0
- package/template/.aioson/skills/squad/formats/creative/podcast-script.md +43 -0
- package/template/.aioson/skills/squad/formats/creative/video-script.md +41 -0
- package/template/.aioson/skills/squad/formats/social/instagram-feed.md +42 -0
- package/template/.aioson/skills/squad/formats/social/linkedin-post.md +42 -0
- package/template/.aioson/skills/squad/formats/social/tiktok.md +39 -0
- package/template/.aioson/skills/squad/formats/social/twitter-thread.md +39 -0
- package/template/.aioson/skills/squad/formats/social/youtube-long.md +47 -0
- package/template/.aioson/skills/squad/formats/social/youtube-shorts.md +39 -0
- package/template/.aioson/skills/squad/patterns/multi-platform-pattern.md +108 -0
- package/template/.aioson/skills/squad/patterns/persona-based-pattern.md +98 -0
- package/template/.aioson/skills/squad/patterns/pipeline-pattern.md +106 -0
- package/template/.aioson/skills/squad/patterns/review-loop-pattern.md +81 -0
- package/template/.aioson/skills/squad/references/checklist-templates.md +122 -0
- package/template/.aioson/skills/squad/references/executor-archetypes.md +123 -0
- package/template/.aioson/skills/squad/references/workflow-templates.md +169 -0
- package/template/.aioson/skills/static/debugging-protocol.md +42 -0
- package/template/.aioson/skills/static/git-worktrees.md +36 -0
- package/template/.aioson/tasks/implementation-plan.md +19 -0
- package/template/.aioson/tasks/squad-design.md +28 -0
- package/template/.aioson/tasks/squad-profile.md +48 -0
- package/template/.aioson/tasks/squad-review.md +61 -0
- package/template/.aioson/tasks/squad-task-decompose.md +66 -0
- package/template/.claude/commands/aioson/agent/neo.md +5 -0
- package/template/.claude/commands/aioson/agent/tester.md +5 -0
- package/template/.gemini/GEMINI.md +1 -0
- package/template/.gemini/commands/aios-neo.toml +4 -0
- package/template/.gemini/commands/aios-tester.toml +6 -0
- package/template/AGENTS.md +3 -0
- package/template/CLAUDE.md +5 -2
- package/template/OPENCODE.md +2 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const SQUADS_DIR = path.join('.aioson', 'squads');
|
|
7
|
+
|
|
8
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']);
|
|
9
|
+
|
|
10
|
+
const MIME_MAP = {
|
|
11
|
+
'.png': 'image/png',
|
|
12
|
+
'.jpg': 'image/jpeg',
|
|
13
|
+
'.jpeg': 'image/jpeg',
|
|
14
|
+
'.gif': 'image/gif',
|
|
15
|
+
'.webp': 'image/webp',
|
|
16
|
+
'.svg': 'image/svg+xml',
|
|
17
|
+
'.pdf': 'application/pdf',
|
|
18
|
+
'.txt': 'text/plain',
|
|
19
|
+
'.md': 'text/markdown',
|
|
20
|
+
'.json': 'application/json'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function attachmentsDir(projectDir, squadSlug) {
|
|
24
|
+
return path.join(projectDir, SQUADS_DIR, squadSlug, 'attachments');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeName(filename) {
|
|
28
|
+
return path.basename(filename).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Save a Buffer or string as an attachment.
|
|
33
|
+
* Returns { ok, filename, filePath, isImage }.
|
|
34
|
+
*/
|
|
35
|
+
async function saveAttachment(projectDir, squadSlug, filename, buffer) {
|
|
36
|
+
const dir = attachmentsDir(projectDir, squadSlug);
|
|
37
|
+
await fs.mkdir(dir, { recursive: true });
|
|
38
|
+
const safe = safeName(filename);
|
|
39
|
+
const dest = path.join(dir, safe);
|
|
40
|
+
await fs.writeFile(dest, buffer);
|
|
41
|
+
const ext = path.extname(safe).toLowerCase();
|
|
42
|
+
return { ok: true, filePath: dest, filename: safe, isImage: IMAGE_EXTS.has(ext) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List all attachments for a squad.
|
|
47
|
+
* Returns array of { filename, filePath, isImage, mime, size }.
|
|
48
|
+
*/
|
|
49
|
+
async function listAttachments(projectDir, squadSlug) {
|
|
50
|
+
const dir = attachmentsDir(projectDir, squadSlug);
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const result = [];
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry.isFile()) continue;
|
|
60
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
61
|
+
let size = 0;
|
|
62
|
+
try {
|
|
63
|
+
const stat = await fs.stat(path.join(dir, entry.name));
|
|
64
|
+
size = stat.size;
|
|
65
|
+
} catch { /* ignore */ }
|
|
66
|
+
result.push({
|
|
67
|
+
filename: entry.name,
|
|
68
|
+
filePath: path.join(dir, entry.name),
|
|
69
|
+
isImage: IMAGE_EXTS.has(ext),
|
|
70
|
+
mime: MIME_MAP[ext] || 'application/octet-stream',
|
|
71
|
+
size
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read an attachment file for serving (e.g. inline image preview).
|
|
79
|
+
* Returns { ok, buffer, mime, filename } or { ok: false }.
|
|
80
|
+
*/
|
|
81
|
+
async function readAttachment(projectDir, squadSlug, filename) {
|
|
82
|
+
const safe = safeName(filename);
|
|
83
|
+
const filePath = path.join(attachmentsDir(projectDir, squadSlug), safe);
|
|
84
|
+
try {
|
|
85
|
+
const buffer = await fs.readFile(filePath);
|
|
86
|
+
const ext = path.extname(safe).toLowerCase();
|
|
87
|
+
return { ok: true, buffer, mime: MIME_MAP[ext] || 'application/octet-stream', filename: safe };
|
|
88
|
+
} catch {
|
|
89
|
+
return { ok: false };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { saveAttachment, listAttachments, readAttachment, IMAGE_EXTS, MIME_MAP };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { generateRecovery, shouldRefreshOnEvent } = require('../squad/recovery-context');
|
|
6
|
+
|
|
7
|
+
const SQUADS_DIR = path.join('.aioson', 'squads');
|
|
8
|
+
|
|
9
|
+
// Minimum ratio drop between consecutive measurements to be considered a compact
|
|
10
|
+
const COMPACT_DROP_THRESHOLD = 0.30;
|
|
11
|
+
|
|
12
|
+
// Warning level thresholds (ratio of used/windowSize)
|
|
13
|
+
const THRESHOLDS = { warning: 0.85, critical: 0.95 };
|
|
14
|
+
|
|
15
|
+
// Notification event types (dispatched by caller when notification system is available)
|
|
16
|
+
const EVENTS = {
|
|
17
|
+
CONTEXT_WARNING: 'context_warning',
|
|
18
|
+
CONTEXT_CRITICAL: 'context_critical'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Context categories (6) — order determines donut segment order
|
|
22
|
+
const CATEGORIES = [
|
|
23
|
+
'system_prompt',
|
|
24
|
+
'conversation_history',
|
|
25
|
+
'tool_outputs',
|
|
26
|
+
'files_loaded',
|
|
27
|
+
'inline_data',
|
|
28
|
+
'other'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function computeWarningLevel(used, windowSize) {
|
|
32
|
+
if (!windowSize || windowSize <= 0) return 'unknown';
|
|
33
|
+
const ratio = used / windowSize;
|
|
34
|
+
if (ratio >= 1.0) return 'overflow';
|
|
35
|
+
if (ratio >= THRESHOLDS.critical) return 'critical';
|
|
36
|
+
if (ratio >= THRESHOLDS.warning) return 'warning';
|
|
37
|
+
return 'normal';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read context-monitor.json for a squad and compute warning levels.
|
|
42
|
+
* @param {string} projectDir
|
|
43
|
+
* @param {string} squadSlug
|
|
44
|
+
* @param {string|null} agentSlug — if set, return only that agent
|
|
45
|
+
* @returns {object|null}
|
|
46
|
+
*/
|
|
47
|
+
async function getContextUsage(projectDir, squadSlug, agentSlug) {
|
|
48
|
+
const filePath = path.join(projectDir, SQUADS_DIR, squadSlug, 'context-monitor.json');
|
|
49
|
+
let data;
|
|
50
|
+
try {
|
|
51
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
52
|
+
data = JSON.parse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const agents = data.agents || {};
|
|
58
|
+
|
|
59
|
+
if (agentSlug) {
|
|
60
|
+
const agent = agents[agentSlug];
|
|
61
|
+
if (!agent) return null;
|
|
62
|
+
const warningLevel = computeWarningLevel(agent.totalUsed || 0, agent.windowSize || 0);
|
|
63
|
+
return { squadSlug, agentSlug, ...agent, warningLevel };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const enrichedAgents = {};
|
|
67
|
+
for (const [slug, agent] of Object.entries(agents)) {
|
|
68
|
+
enrichedAgents[slug] = {
|
|
69
|
+
...agent,
|
|
70
|
+
warningLevel: computeWarningLevel(agent.totalUsed || 0, agent.windowSize || 0)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { squadSlug, agents: enrichedAgents, updatedAt: data.updatedAt };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Return pending notification events for a context snapshot.
|
|
78
|
+
* Caller dispatches these once the notification system exists.
|
|
79
|
+
*/
|
|
80
|
+
function checkNotificationEvents(squadSlug, contextData) {
|
|
81
|
+
if (!contextData || !contextData.agents) return [];
|
|
82
|
+
const events = [];
|
|
83
|
+
for (const [agentSlug, agent] of Object.entries(contextData.agents)) {
|
|
84
|
+
if (agent.warningLevel === 'critical' || agent.warningLevel === 'overflow') {
|
|
85
|
+
events.push({ type: EVENTS.CONTEXT_CRITICAL, squadSlug, agentSlug, warningLevel: agent.warningLevel });
|
|
86
|
+
} else if (agent.warningLevel === 'warning') {
|
|
87
|
+
events.push({ type: EVENTS.CONTEXT_WARNING, squadSlug, agentSlug, warningLevel: agent.warningLevel });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return events;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect if a context compact occurred between two consecutive measurements.
|
|
95
|
+
* A compact is inferred when totalUsed drops by > 30% from the previous snapshot.
|
|
96
|
+
* @param {number} prevUsed — previous totalUsed value
|
|
97
|
+
* @param {number} currUsed — current totalUsed value
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function isCompactDetected(prevUsed, currUsed) {
|
|
101
|
+
if (!prevUsed || prevUsed <= 0) return false;
|
|
102
|
+
const drop = (prevUsed - currUsed) / prevUsed;
|
|
103
|
+
return drop > COMPACT_DROP_THRESHOLD;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Compare two context-monitor snapshots and trigger recovery injection if a
|
|
108
|
+
* compact is detected for any agent.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} projectDir
|
|
111
|
+
* @param {string} squadSlug
|
|
112
|
+
* @param {object} prevData — previous result from getContextUsage (or null)
|
|
113
|
+
* @param {object} currData — current result from getContextUsage
|
|
114
|
+
* @returns {Array<{agentSlug, recovery}>} list of recovery results triggered
|
|
115
|
+
*/
|
|
116
|
+
async function checkAndInjectRecovery(projectDir, squadSlug, prevData, currData) {
|
|
117
|
+
if (!prevData || !currData) return [];
|
|
118
|
+
const prevAgents = prevData.agents || {};
|
|
119
|
+
const currAgents = currData.agents || {};
|
|
120
|
+
const triggered = [];
|
|
121
|
+
|
|
122
|
+
for (const [agentSlug, curr] of Object.entries(currAgents)) {
|
|
123
|
+
const prev = prevAgents[agentSlug];
|
|
124
|
+
if (!prev) continue;
|
|
125
|
+
if (isCompactDetected(prev.totalUsed || 0, curr.totalUsed || 0)) {
|
|
126
|
+
const recovery = await generateRecovery(projectDir, squadSlug, agentSlug);
|
|
127
|
+
triggered.push({ agentSlug, recovery });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return triggered;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Optionally trigger a recovery refresh when a specific runtime event fires.
|
|
136
|
+
* @param {string} projectDir
|
|
137
|
+
* @param {string} squadSlug
|
|
138
|
+
* @param {string} agentSlug
|
|
139
|
+
* @param {string} eventType
|
|
140
|
+
*/
|
|
141
|
+
async function onRuntimeEvent(projectDir, squadSlug, agentSlug, eventType) {
|
|
142
|
+
if (!shouldRefreshOnEvent(eventType)) return null;
|
|
143
|
+
return generateRecovery(projectDir, squadSlug, agentSlug);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
getContextUsage,
|
|
148
|
+
computeWarningLevel,
|
|
149
|
+
checkNotificationEvents,
|
|
150
|
+
isCompactDetected,
|
|
151
|
+
checkAndInjectRecovery,
|
|
152
|
+
onRuntimeEvent,
|
|
153
|
+
CATEGORIES,
|
|
154
|
+
EVENTS,
|
|
155
|
+
THRESHOLDS,
|
|
156
|
+
COMPACT_DROP_THRESHOLD
|
|
157
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const SQUADS_DIR = path.join('.aioson', 'squads');
|
|
7
|
+
|
|
8
|
+
const ENTRY_TYPES = {
|
|
9
|
+
TOOL_CALL: 'tool_call',
|
|
10
|
+
REASONING: 'reasoning',
|
|
11
|
+
MILESTONE: 'milestone',
|
|
12
|
+
ERROR: 'error'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Session log file schema:
|
|
17
|
+
* {
|
|
18
|
+
* agentSlug: string,
|
|
19
|
+
* taskId: string,
|
|
20
|
+
* startedAt: ISO string,
|
|
21
|
+
* summary: string | null,
|
|
22
|
+
* entries: Array<{
|
|
23
|
+
* type: 'tool_call' | 'reasoning' | 'milestone' | 'error',
|
|
24
|
+
* timestamp: ISO string,
|
|
25
|
+
* // tool_call
|
|
26
|
+
* toolName?: string,
|
|
27
|
+
* input?: any,
|
|
28
|
+
* output?: any,
|
|
29
|
+
* durationMs?: number,
|
|
30
|
+
* // reasoning
|
|
31
|
+
* text?: string,
|
|
32
|
+
* // milestone
|
|
33
|
+
* label?: string,
|
|
34
|
+
* // error
|
|
35
|
+
* message?: string,
|
|
36
|
+
* stack?: string
|
|
37
|
+
* }>
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* File path: .aioson/squads/{slug}/logs/{task-id}/session-{timestamp}.json
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
function logsDir(projectDir, squadSlug, taskId) {
|
|
44
|
+
return path.join(projectDir, SQUADS_DIR, squadSlug, 'logs', taskId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function listSessionFiles(projectDir, squadSlug, taskId) {
|
|
48
|
+
const dir = logsDir(projectDir, squadSlug, taskId);
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await fs.readdir(dir);
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return entries
|
|
56
|
+
.filter(f => f.startsWith('session-') && f.endsWith('.json'))
|
|
57
|
+
.map(f => {
|
|
58
|
+
const ts = f.replace(/^session-/, '').replace(/\.json$/, '');
|
|
59
|
+
return { sessionId: f.replace('.json', ''), filename: f, timestamp: ts, filePath: path.join(dir, f) };
|
|
60
|
+
})
|
|
61
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns all sessions (with entries) for a given task, sorted oldest-first.
|
|
66
|
+
*/
|
|
67
|
+
async function getLogsForTask(projectDir, squadSlug, taskId) {
|
|
68
|
+
const files = await listSessionFiles(projectDir, squadSlug, taskId);
|
|
69
|
+
const sessions = [];
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await fs.readFile(file.filePath, 'utf8');
|
|
73
|
+
const session = JSON.parse(raw);
|
|
74
|
+
sessions.push({
|
|
75
|
+
sessionId: file.sessionId,
|
|
76
|
+
timestamp: file.timestamp,
|
|
77
|
+
taskId,
|
|
78
|
+
squadSlug,
|
|
79
|
+
agentSlug: session.agentSlug || null,
|
|
80
|
+
startedAt: session.startedAt || file.timestamp,
|
|
81
|
+
summary: session.summary || null,
|
|
82
|
+
entries: session.entries || []
|
|
83
|
+
});
|
|
84
|
+
} catch {
|
|
85
|
+
sessions.push({
|
|
86
|
+
sessionId: file.sessionId,
|
|
87
|
+
timestamp: file.timestamp,
|
|
88
|
+
taskId,
|
|
89
|
+
squadSlug,
|
|
90
|
+
agentSlug: null,
|
|
91
|
+
startedAt: file.timestamp,
|
|
92
|
+
summary: null,
|
|
93
|
+
entries: [],
|
|
94
|
+
parseError: true
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return sessions;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns a single session log by sessionId (filename without .json).
|
|
103
|
+
*/
|
|
104
|
+
async function getSessionLog(projectDir, squadSlug, taskId, sessionId) {
|
|
105
|
+
const dir = logsDir(projectDir, squadSlug, taskId);
|
|
106
|
+
const filePath = path.join(dir, `${sessionId}.json`);
|
|
107
|
+
try {
|
|
108
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
109
|
+
return JSON.parse(raw);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { ENTRY_TYPES, getLogsForTask, getSessionLog };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const SQUADS_DIR = path.join('.aioson', 'squads');
|
|
7
|
+
|
|
8
|
+
// Valid hunk states
|
|
9
|
+
const HUNK_STATES = { PENDING: 'pending', APPROVED: 'approved', REJECTED: 'rejected', REVISED: 'revised' };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse a unified diff string into individual hunks.
|
|
13
|
+
* Each hunk includes its header, lines, and a stable id.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} diff — full unified diff text
|
|
16
|
+
* @returns {Array<{id, fileHeader, header, lines, additions, deletions}>}
|
|
17
|
+
*/
|
|
18
|
+
function parseDiffHunks(diff) {
|
|
19
|
+
if (!diff || typeof diff !== 'string') return [];
|
|
20
|
+
|
|
21
|
+
const hunks = [];
|
|
22
|
+
let currentFile = '';
|
|
23
|
+
let currentHunk = null;
|
|
24
|
+
let hunkIndex = 0;
|
|
25
|
+
|
|
26
|
+
for (const rawLine of diff.split('\n')) {
|
|
27
|
+
// File header lines
|
|
28
|
+
if (rawLine.startsWith('--- ') || rawLine.startsWith('+++ ')) {
|
|
29
|
+
if (rawLine.startsWith('+++ ')) {
|
|
30
|
+
// Strip b/ prefix from git diffs
|
|
31
|
+
currentFile = rawLine.slice(4).replace(/^b\//, '').trim();
|
|
32
|
+
}
|
|
33
|
+
if (currentHunk) {
|
|
34
|
+
hunks.push(finalizeHunk(currentHunk));
|
|
35
|
+
currentHunk = null;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Hunk header: @@ -a,b +c,d @@
|
|
41
|
+
if (rawLine.startsWith('@@ ')) {
|
|
42
|
+
if (currentHunk) {
|
|
43
|
+
hunks.push(finalizeHunk(currentHunk));
|
|
44
|
+
}
|
|
45
|
+
currentHunk = {
|
|
46
|
+
id: `hunk-${hunkIndex++}`,
|
|
47
|
+
fileHeader: currentFile,
|
|
48
|
+
header: rawLine,
|
|
49
|
+
lines: [],
|
|
50
|
+
additions: 0,
|
|
51
|
+
deletions: 0
|
|
52
|
+
};
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (currentHunk) {
|
|
57
|
+
currentHunk.lines.push(rawLine);
|
|
58
|
+
if (rawLine.startsWith('+') && !rawLine.startsWith('+++')) currentHunk.additions++;
|
|
59
|
+
if (rawLine.startsWith('-') && !rawLine.startsWith('---')) currentHunk.deletions++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (currentHunk) hunks.push(finalizeHunk(currentHunk));
|
|
64
|
+
return hunks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function finalizeHunk(hunk) {
|
|
68
|
+
return {
|
|
69
|
+
id: hunk.id,
|
|
70
|
+
fileHeader: hunk.fileHeader,
|
|
71
|
+
header: hunk.header,
|
|
72
|
+
lines: hunk.lines,
|
|
73
|
+
additions: hunk.additions,
|
|
74
|
+
deletions: hunk.deletions
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the hunk-review state file path for a task.
|
|
80
|
+
*/
|
|
81
|
+
function hunkStatePath(projectDir, squadSlug, taskId) {
|
|
82
|
+
return path.join(projectDir, SQUADS_DIR, squadSlug, 'tasks', taskId, 'hunk-review.json');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Load hunk review state for a task. Returns null if not found.
|
|
87
|
+
*/
|
|
88
|
+
async function loadHunkState(projectDir, squadSlug, taskId) {
|
|
89
|
+
try {
|
|
90
|
+
const raw = await fs.readFile(hunkStatePath(projectDir, squadSlug, taskId), 'utf8');
|
|
91
|
+
return JSON.parse(raw);
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Save hunk review state for a task.
|
|
99
|
+
*/
|
|
100
|
+
async function saveHunkState(projectDir, squadSlug, taskId, state) {
|
|
101
|
+
const p = hunkStatePath(projectDir, squadSlug, taskId);
|
|
102
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
103
|
+
await fs.writeFile(p, JSON.stringify(state, null, 2), 'utf8');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Initialize hunk review state from a diff string.
|
|
108
|
+
* Returns the initial state object (persisted to disk).
|
|
109
|
+
*/
|
|
110
|
+
async function initHunkReview(projectDir, squadSlug, taskId, diff) {
|
|
111
|
+
const hunks = parseDiffHunks(diff);
|
|
112
|
+
const state = {
|
|
113
|
+
taskId,
|
|
114
|
+
squadSlug,
|
|
115
|
+
diff,
|
|
116
|
+
hunks: hunks.map(h => ({
|
|
117
|
+
id: h.id,
|
|
118
|
+
fileHeader: h.fileHeader,
|
|
119
|
+
header: h.header,
|
|
120
|
+
lines: h.lines,
|
|
121
|
+
additions: h.additions,
|
|
122
|
+
deletions: h.deletions,
|
|
123
|
+
status: HUNK_STATES.PENDING,
|
|
124
|
+
comment: null,
|
|
125
|
+
reviewedAt: null
|
|
126
|
+
})),
|
|
127
|
+
createdAt: new Date().toISOString(),
|
|
128
|
+
updatedAt: new Date().toISOString()
|
|
129
|
+
};
|
|
130
|
+
await saveHunkState(projectDir, squadSlug, taskId, state);
|
|
131
|
+
return state;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get current hunk review state (or init from diff if missing).
|
|
136
|
+
*/
|
|
137
|
+
async function getHunks(projectDir, squadSlug, taskId, diff) {
|
|
138
|
+
let state = await loadHunkState(projectDir, squadSlug, taskId);
|
|
139
|
+
if (!state && diff) {
|
|
140
|
+
state = await initHunkReview(projectDir, squadSlug, taskId, diff);
|
|
141
|
+
}
|
|
142
|
+
return state;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Update the status of a single hunk.
|
|
147
|
+
* @param {string} newStatus — one of HUNK_STATES values
|
|
148
|
+
* @param {string|null} comment
|
|
149
|
+
* @returns {{ state, dispatch: string|null }}
|
|
150
|
+
* dispatch is 'task_done' | 'task_needs_revision' | null based on overall state after update
|
|
151
|
+
*/
|
|
152
|
+
async function updateHunk(projectDir, squadSlug, taskId, hunkId, newStatus, comment) {
|
|
153
|
+
const state = await loadHunkState(projectDir, squadSlug, taskId);
|
|
154
|
+
if (!state) return { ok: false, error: 'No hunk review state found' };
|
|
155
|
+
|
|
156
|
+
const hunk = state.hunks.find(h => h.id === hunkId);
|
|
157
|
+
if (!hunk) return { ok: false, error: `Hunk "${hunkId}" not found` };
|
|
158
|
+
|
|
159
|
+
hunk.status = newStatus;
|
|
160
|
+
if (comment !== undefined && comment !== null) hunk.comment = comment;
|
|
161
|
+
hunk.reviewedAt = new Date().toISOString();
|
|
162
|
+
state.updatedAt = new Date().toISOString();
|
|
163
|
+
|
|
164
|
+
await saveHunkState(projectDir, squadSlug, taskId, state);
|
|
165
|
+
|
|
166
|
+
const dispatch = computeDispatch(state.hunks);
|
|
167
|
+
return { ok: true, hunk, dispatch, state };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Compute the dispatch event based on all hunk statuses.
|
|
172
|
+
* Returns: 'task_done' if all approved, 'task_needs_revision' if any rejected,
|
|
173
|
+
* null if still pending.
|
|
174
|
+
*/
|
|
175
|
+
function computeDispatch(hunks) {
|
|
176
|
+
const allReviewed = hunks.every(h => h.status !== HUNK_STATES.PENDING);
|
|
177
|
+
if (!allReviewed) return null;
|
|
178
|
+
|
|
179
|
+
const rejectedHunks = hunks.filter(h => h.status === HUNK_STATES.REJECTED);
|
|
180
|
+
if (rejectedHunks.length > 0) {
|
|
181
|
+
return { event: 'task_needs_revision', rejectedHunks: rejectedHunks.map(h => h.id) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { event: 'task_done' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get a summary of review progress: total, approved, rejected, pending.
|
|
189
|
+
*/
|
|
190
|
+
function getReviewProgress(hunks) {
|
|
191
|
+
return {
|
|
192
|
+
total: hunks.length,
|
|
193
|
+
approved: hunks.filter(h => h.status === HUNK_STATES.APPROVED).length,
|
|
194
|
+
rejected: hunks.filter(h => h.status === HUNK_STATES.REJECTED).length,
|
|
195
|
+
revised: hunks.filter(h => h.status === HUNK_STATES.REVISED).length,
|
|
196
|
+
pending: hunks.filter(h => h.status === HUNK_STATES.PENDING).length
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
parseDiffHunks,
|
|
202
|
+
initHunkReview,
|
|
203
|
+
getHunks,
|
|
204
|
+
updateHunk,
|
|
205
|
+
loadHunkState,
|
|
206
|
+
computeDispatch,
|
|
207
|
+
getReviewProgress,
|
|
208
|
+
HUNK_STATES
|
|
209
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function countContentItems(db, squadSlug) {
|
|
4
|
+
const row = db.prepare('SELECT COUNT(*) AS cnt FROM content_items WHERE squad_slug = ?').get(squadSlug);
|
|
5
|
+
return row ? row.cnt : 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function countSessions(db, squadSlug) {
|
|
9
|
+
const row = db.prepare(
|
|
10
|
+
"SELECT COUNT(*) AS cnt FROM tasks WHERE meta_json LIKE ? AND task_kind = 'live_session'"
|
|
11
|
+
).get(`%${squadSlug}%`);
|
|
12
|
+
return row ? row.cnt : 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function countLearnings(db, squadSlug) {
|
|
16
|
+
const row = db.prepare('SELECT COUNT(*) AS cnt FROM squad_learnings WHERE squad_slug = ?').get(squadSlug);
|
|
17
|
+
return row ? row.cnt : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function calcDeliveryRate(db, squadSlug) {
|
|
21
|
+
const total = db.prepare('SELECT COUNT(*) AS cnt FROM delivery_log WHERE squad_slug = ?').get(squadSlug);
|
|
22
|
+
if (!total || total.cnt === 0) return null;
|
|
23
|
+
const success = db.prepare(
|
|
24
|
+
'SELECT COUNT(*) AS cnt FROM delivery_log WHERE squad_slug = ? AND status_code >= 200 AND status_code < 300'
|
|
25
|
+
).get(squadSlug);
|
|
26
|
+
return Math.round(((success ? success.cnt : 0) / total.cnt) * 100);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getRecentDeliveries(db, squadSlug, limit = 20) {
|
|
30
|
+
return db.prepare(
|
|
31
|
+
'SELECT * FROM delivery_log WHERE squad_slug = ? ORDER BY created_at DESC LIMIT ?'
|
|
32
|
+
).all(squadSlug, limit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRecentContent(db, squadSlug, limit = 20) {
|
|
36
|
+
return db.prepare(
|
|
37
|
+
'SELECT content_key, title, content_type, layout_type, status, created_at, updated_at FROM content_items WHERE squad_slug = ? ORDER BY updated_at DESC LIMIT ?'
|
|
38
|
+
).all(squadSlug, limit);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getLearnings(db, squadSlug, statusFilter = null) {
|
|
42
|
+
if (statusFilter) {
|
|
43
|
+
return db.prepare(
|
|
44
|
+
'SELECT * FROM squad_learnings WHERE squad_slug = ? AND status = ? ORDER BY updated_at DESC'
|
|
45
|
+
).all(squadSlug, statusFilter);
|
|
46
|
+
}
|
|
47
|
+
return db.prepare(
|
|
48
|
+
'SELECT * FROM squad_learnings WHERE squad_slug = ? ORDER BY updated_at DESC'
|
|
49
|
+
).all(squadSlug);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getLearningStats(db, squadSlug) {
|
|
53
|
+
const rows = db.prepare(
|
|
54
|
+
'SELECT status, COUNT(*) AS cnt FROM squad_learnings WHERE squad_slug = ? GROUP BY status'
|
|
55
|
+
).all(squadSlug);
|
|
56
|
+
const stats = { active: 0, stale: 0, archived: 0, promoted: 0 };
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(stats, row.status)) {
|
|
59
|
+
stats[row.status] = row.cnt;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return stats;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getExecutionPlan(db, squadSlug) {
|
|
66
|
+
const plan = db.prepare(
|
|
67
|
+
'SELECT * FROM squad_execution_plans WHERE squad_slug = ? ORDER BY updated_at DESC LIMIT 1'
|
|
68
|
+
).get(squadSlug);
|
|
69
|
+
if (!plan) return null;
|
|
70
|
+
const rounds = db.prepare(
|
|
71
|
+
'SELECT * FROM squad_plan_rounds WHERE plan_slug = ? ORDER BY round_number ASC'
|
|
72
|
+
).all(plan.plan_slug);
|
|
73
|
+
return { ...plan, rounds };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getPipelineInfo(db, squadSlug) {
|
|
77
|
+
const node = db.prepare(
|
|
78
|
+
'SELECT * FROM pipeline_nodes WHERE squad_slug = ?'
|
|
79
|
+
).get(squadSlug);
|
|
80
|
+
if (!node) return null;
|
|
81
|
+
const pipeline = db.prepare(
|
|
82
|
+
'SELECT * FROM squad_pipelines WHERE pipeline_slug = ?'
|
|
83
|
+
).get(node.pipeline_slug);
|
|
84
|
+
const handoffs = db.prepare(
|
|
85
|
+
'SELECT * FROM squad_handoffs WHERE (from_squad = ? OR to_squad = ?) ORDER BY created_at DESC LIMIT 20'
|
|
86
|
+
).all(squadSlug, squadSlug);
|
|
87
|
+
return { pipeline, node, handoffs };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getSquadMetrics(db, squadSlug) {
|
|
91
|
+
try {
|
|
92
|
+
return db.prepare(
|
|
93
|
+
'SELECT * FROM squad_metrics WHERE squad_slug = ? ORDER BY period DESC, metric_key ASC'
|
|
94
|
+
).all(squadSlug);
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getRecentEvents(db, squadSlug, limit = 30) {
|
|
101
|
+
return db.prepare(
|
|
102
|
+
"SELECT * FROM execution_events WHERE run_key LIKE ? ORDER BY created_at DESC LIMIT ?"
|
|
103
|
+
).all(`%${squadSlug}%`, limit);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getSquadOverview(db, squadSlug) {
|
|
107
|
+
return {
|
|
108
|
+
contentItems: countContentItems(db, squadSlug),
|
|
109
|
+
sessions: countSessions(db, squadSlug),
|
|
110
|
+
learnings: countLearnings(db, squadSlug),
|
|
111
|
+
deliveryRate: calcDeliveryRate(db, squadSlug),
|
|
112
|
+
learningStats: getLearningStats(db, squadSlug),
|
|
113
|
+
executionPlan: getExecutionPlan(db, squadSlug),
|
|
114
|
+
pipelineInfo: getPipelineInfo(db, squadSlug),
|
|
115
|
+
customMetrics: getSquadMetrics(db, squadSlug)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
countContentItems,
|
|
121
|
+
countSessions,
|
|
122
|
+
countLearnings,
|
|
123
|
+
calcDeliveryRate,
|
|
124
|
+
getRecentDeliveries,
|
|
125
|
+
getRecentContent,
|
|
126
|
+
getLearnings,
|
|
127
|
+
getLearningStats,
|
|
128
|
+
getExecutionPlan,
|
|
129
|
+
getPipelineInfo,
|
|
130
|
+
getSquadMetrics,
|
|
131
|
+
getRecentEvents,
|
|
132
|
+
getSquadOverview
|
|
133
|
+
};
|