@jaimevalasek/aioson 1.6.0 → 1.7.2
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 +74 -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 +22 -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/copywriter.md +463 -0
- package/template/.aioson/agents/design-hybrid-forge.md +14 -0
- package/template/.aioson/agents/dev.md +271 -25
- package/template/.aioson/agents/deyvin.md +67 -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 +83 -2
- 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 +273 -21
- package/template/.aioson/agents/setup.md +96 -10
- package/template/.aioson/agents/sheldon.md +131 -6
- package/template/.aioson/agents/site-forge.md +1753 -0
- package/template/.aioson/agents/squad.md +352 -0
- package/template/.aioson/agents/tester.md +53 -0
- package/template/.aioson/agents/ux-ui.md +203 -4
- 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/genomes/copywriting.md +204 -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/cognitive-core-ui/references/motion.md +2 -0
- 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/marketing/references/anti-patterns.md +254 -0
- package/template/.aioson/skills/marketing/references/fascinations.md +192 -0
- package/template/.aioson/skills/marketing/references/five-acts.md +248 -0
- package/template/.aioson/skills/marketing/references/market-intelligence.md +198 -0
- package/template/.aioson/skills/marketing/references/offer-structure.md +203 -0
- package/template/.aioson/skills/marketing/references/one-belief.md +149 -0
- package/template/.aioson/skills/marketing/references/patterns.md +218 -0
- package/template/.aioson/skills/marketing/references/pms-research.md +193 -0
- package/template/.aioson/skills/marketing/vsl-craft.md +385 -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/landing-page-deploy.md +192 -0
- package/template/.aioson/skills/static/landing-page-forge.md +730 -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/ui-ux-modern.md +1 -0
- package/template/.aioson/skills/static/web-research-cache.md +112 -0
- package/template/.aioson/tasks/implementation-plan.md +21 -1
- package/template/.aioson/tasks/squad-create.md +22 -0
- package/template/.aioson/tasks/squad-design.md +30 -0
- package/template/.aioson/templates/squads/digital-marketing-agency/template.json +96 -0
- 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 +31 -0
- package/template/OPENCODE.md +4 -0
- package/template/researchs/.gitkeep +0 -0
- package/template/.aioson/skills/design-system/components/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/dashboards/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/foundations/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/motion/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/patterns/SKILL.md:Zone.Identifier +0 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* preflight-engine — shared deterministic utilities for preflight, gate:check, artifact:validate.
|
|
5
|
+
* No LLM calls. Pure file parsing + logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs/promises');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
|
|
11
|
+
// ─── Frontmatter parser ───────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function parseFrontmatter(content) {
|
|
14
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
15
|
+
if (!match) return {};
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
18
|
+
const colonIdx = line.indexOf(':');
|
|
19
|
+
if (colonIdx === -1) continue;
|
|
20
|
+
const key = line.slice(0, colonIdx).trim();
|
|
21
|
+
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
22
|
+
if (key) result[key] = value;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readFileSafe(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
return await fs.readFile(filePath, 'utf8');
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fileExists(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
await fs.access(filePath);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fileStat(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
return await fs.stat(filePath);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Framework detection ──────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const FRAMEWORK_INDICATORS = [
|
|
55
|
+
{ file: 'composer.json', key: 'laravel/framework', name: 'Laravel' },
|
|
56
|
+
{ file: 'composer.json', key: 'symfony/framework-bundle', name: 'Symfony' },
|
|
57
|
+
{ file: 'package.json', key: '"next"', name: 'Next.js' },
|
|
58
|
+
{ file: 'package.json', key: '"nuxt"', name: 'Nuxt.js' },
|
|
59
|
+
{ file: 'package.json', key: '"react"', name: 'React' },
|
|
60
|
+
{ file: 'package.json', key: '"vue"', name: 'Vue' },
|
|
61
|
+
{ file: 'package.json', key: '"svelte"', name: 'Svelte' },
|
|
62
|
+
{ file: 'package.json', key: '"express"', name: 'Express' },
|
|
63
|
+
{ file: 'Gemfile', key: 'rails', name: 'Rails' },
|
|
64
|
+
{ file: 'requirements.txt', key: 'django', name: 'Django' },
|
|
65
|
+
{ file: 'requirements.txt', key: 'fastapi', name: 'FastAPI' },
|
|
66
|
+
{ file: 'requirements.txt', key: 'flask', name: 'Flask' },
|
|
67
|
+
{ file: 'go.mod', key: 'gin-gonic', name: 'Gin' },
|
|
68
|
+
{ file: 'go.mod', key: 'echo', name: 'Echo' },
|
|
69
|
+
{ file: 'Cargo.toml', key: 'actix-web', name: 'Actix' },
|
|
70
|
+
{ file: 'foundry.toml', key: null, name: 'Foundry (Solidity)' }
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
async function detectFramework(targetDir) {
|
|
74
|
+
for (const { file, key, name } of FRAMEWORK_INDICATORS) {
|
|
75
|
+
const filePath = path.join(targetDir, file);
|
|
76
|
+
const content = await readFileSafe(filePath);
|
|
77
|
+
if (!content) continue;
|
|
78
|
+
if (!key || content.toLowerCase().includes(key.toLowerCase())) {
|
|
79
|
+
return name;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Test runner detection ────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const TEST_RUNNER_INDICATORS = [
|
|
88
|
+
{ file: 'phpunit.xml', name: 'Pest/PHPUnit', command: 'php artisan test' },
|
|
89
|
+
{ file: 'phpunit.xml.dist', name: 'PHPUnit', command: './vendor/bin/phpunit' },
|
|
90
|
+
{ file: 'jest.config.js', name: 'Jest', command: 'npx jest' },
|
|
91
|
+
{ file: 'jest.config.ts', name: 'Jest', command: 'npx jest' },
|
|
92
|
+
{ file: 'jest.config.mjs', name: 'Jest', command: 'npx jest' },
|
|
93
|
+
{ file: 'vitest.config.js', name: 'Vitest', command: 'npx vitest' },
|
|
94
|
+
{ file: 'vitest.config.ts', name: 'Vitest', command: 'npx vitest' },
|
|
95
|
+
{ file: 'vitest.config.mjs', name: 'Vitest', command: 'npx vitest' },
|
|
96
|
+
{ file: 'pytest.ini', name: 'Pytest', command: 'pytest' },
|
|
97
|
+
{ file: 'setup.cfg', name: 'Pytest', command: 'pytest', key: '[tool:pytest]' },
|
|
98
|
+
{ file: 'pyproject.toml', name: 'Pytest', command: 'pytest', key: '[tool.pytest' },
|
|
99
|
+
{ file: '.rspec', name: 'RSpec', command: 'bundle exec rspec' },
|
|
100
|
+
{ file: 'foundry.toml', name: 'Forge', command: 'forge test' },
|
|
101
|
+
{ file: 'Makefile', name: 'Make', command: 'make test', key: 'test:' }
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
async function detectTestRunner(targetDir) {
|
|
105
|
+
for (const { file, name, command, key } of TEST_RUNNER_INDICATORS) {
|
|
106
|
+
const filePath = path.join(targetDir, file);
|
|
107
|
+
const content = await readFileSafe(filePath);
|
|
108
|
+
if (!content) continue;
|
|
109
|
+
if (key && !content.includes(key)) continue;
|
|
110
|
+
return { name, command, configFile: file };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check package.json scripts (test, test:unit, test:e2e, etc.)
|
|
114
|
+
const pkgContent = await readFileSafe(path.join(targetDir, 'package.json'));
|
|
115
|
+
if (pkgContent) {
|
|
116
|
+
try {
|
|
117
|
+
const pkg = JSON.parse(pkgContent);
|
|
118
|
+
if (pkg.scripts) {
|
|
119
|
+
// Check all test-related script keys, prioritize "test" then "test:*"
|
|
120
|
+
const testKeys = Object.keys(pkg.scripts).filter((k) => k === 'test' || k.startsWith('test:'));
|
|
121
|
+
for (const key of testKeys) {
|
|
122
|
+
const script = pkg.scripts[key];
|
|
123
|
+
if (script.includes('jest')) return { name: 'Jest', command: `npm run ${key}`, configFile: 'package.json' };
|
|
124
|
+
if (script.includes('vitest')) return { name: 'Vitest', command: `npm run ${key}`, configFile: 'package.json' };
|
|
125
|
+
if (script.includes('mocha')) return { name: 'Mocha', command: `npm run ${key}`, configFile: 'package.json' };
|
|
126
|
+
if (script.includes('node --test') || script.includes('node:test')) return { name: 'node:test', command: `npm run ${key}`, configFile: 'package.json' };
|
|
127
|
+
}
|
|
128
|
+
if (pkg.scripts.test) {
|
|
129
|
+
return { name: 'npm test', command: 'npm test', configFile: 'package.json' };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch { /* ignore malformed package.json */ }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Context file paths ───────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function contextDir(targetDir) {
|
|
141
|
+
return path.join(targetDir, '.aioson', 'context');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function rulesDir(targetDir) {
|
|
145
|
+
return path.join(targetDir, '.aioson', 'rules');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function artifactPath(targetDir, name, slug) {
|
|
149
|
+
const dir = contextDir(targetDir);
|
|
150
|
+
if (slug) return path.join(dir, `${name}-${slug}.md`);
|
|
151
|
+
return path.join(dir, `${name}.md`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Project context reader ───────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
async function loadProjectContext(targetDir) {
|
|
157
|
+
const filePath = path.join(contextDir(targetDir), 'project.context.md');
|
|
158
|
+
const content = await readFileSafe(filePath);
|
|
159
|
+
if (!content) return { exists: false, data: {} };
|
|
160
|
+
const data = parseFrontmatter(content);
|
|
161
|
+
return { exists: true, data, content };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Artifact scanner ─────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async function scanArtifacts(targetDir, slug) {
|
|
167
|
+
const dir = contextDir(targetDir);
|
|
168
|
+
|
|
169
|
+
async function check(name, filePath) {
|
|
170
|
+
const stat = await fileStat(filePath);
|
|
171
|
+
if (!stat) return { exists: false };
|
|
172
|
+
|
|
173
|
+
const content = await readFileSafe(filePath);
|
|
174
|
+
const fm = content ? parseFrontmatter(content) : {};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
exists: true,
|
|
178
|
+
path: path.relative(targetDir, filePath),
|
|
179
|
+
size: stat.size,
|
|
180
|
+
frontmatter: fm,
|
|
181
|
+
content
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const results = {
|
|
186
|
+
project_context: await check('project.context', path.join(dir, 'project.context.md')),
|
|
187
|
+
prd: slug ? await check('prd', path.join(dir, `prd-${slug}.md`)) : { exists: false },
|
|
188
|
+
sheldon_enrichment: slug ? await check('sheldon', path.join(dir, `sheldon-enrichment-${slug}.md`)) : { exists: false },
|
|
189
|
+
requirements: slug ? await check('requirements', path.join(dir, `requirements-${slug}.md`)) : { exists: false },
|
|
190
|
+
spec: slug ? await check('spec', path.join(dir, `spec-${slug}.md`)) : await check('spec', path.join(dir, 'spec.md')),
|
|
191
|
+
architecture: await check('architecture', path.join(dir, 'architecture.md')),
|
|
192
|
+
implementation_plan: slug ? await check('impl-plan', path.join(dir, `implementation-plan-${slug}.md`)) : { exists: false },
|
|
193
|
+
conformance: slug ? await check('conformance', path.join(dir, `conformance-${slug}.yaml`)) : { exists: false },
|
|
194
|
+
dev_state: await check('dev-state', path.join(dir, 'dev-state.md')),
|
|
195
|
+
features: await check('features', path.join(dir, 'features.md'))
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return results;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Gate reader ─────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
const GATE_NAMES = {
|
|
204
|
+
A: 'requirements',
|
|
205
|
+
B: 'design',
|
|
206
|
+
C: 'plan',
|
|
207
|
+
D: 'execution'
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const GATE_ALIASES = {
|
|
211
|
+
requirements: 'A',
|
|
212
|
+
design: 'B',
|
|
213
|
+
plan: 'C',
|
|
214
|
+
execution: 'D'
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function parseGatesFromSpec(content) {
|
|
218
|
+
if (!content) return {};
|
|
219
|
+
const fm = parseFrontmatter(content);
|
|
220
|
+
const gates = {};
|
|
221
|
+
|
|
222
|
+
// Try explicit gate fields: gate_requirements, gate_design, gate_plan, gate_execution
|
|
223
|
+
for (const [letter, name] of Object.entries(GATE_NAMES)) {
|
|
224
|
+
const val = fm[`gate_${name}`] || fm[`gate${letter}`] || fm[`gate_${letter}`];
|
|
225
|
+
if (val) gates[name] = val.toLowerCase();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Try phase_gates JSON field
|
|
229
|
+
if (fm.phase_gates) {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(fm.phase_gates.replace(/'/g, '"'));
|
|
232
|
+
Object.assign(gates, parsed);
|
|
233
|
+
} catch {
|
|
234
|
+
// phase_gates field exists but is not valid JSON — gate data from this field is lost
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Try scanning content for gate approval lines
|
|
239
|
+
const gateLineRe = /gate\s+([A-D])[^:]*:\s*(approved|pending|rejected)/gi;
|
|
240
|
+
let m;
|
|
241
|
+
while ((m = gateLineRe.exec(content)) !== null) {
|
|
242
|
+
const letter = m[1].toUpperCase();
|
|
243
|
+
const name = GATE_NAMES[letter];
|
|
244
|
+
if (name && !gates[name]) gates[name] = m[2].toLowerCase();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return gates;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function readPhaseGates(targetDir, slug) {
|
|
251
|
+
const specFile = slug
|
|
252
|
+
? path.join(contextDir(targetDir), `spec-${slug}.md`)
|
|
253
|
+
: path.join(contextDir(targetDir), 'spec.md');
|
|
254
|
+
|
|
255
|
+
const content = await readFileSafe(specFile);
|
|
256
|
+
if (!content) return {};
|
|
257
|
+
return parseGatesFromSpec(content);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Dev state reader ─────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async function readDevState(targetDir) {
|
|
263
|
+
const filePath = path.join(contextDir(targetDir), 'dev-state.md');
|
|
264
|
+
const content = await readFileSafe(filePath);
|
|
265
|
+
if (!content) return { exists: false };
|
|
266
|
+
const fm = parseFrontmatter(content);
|
|
267
|
+
return { exists: true, ...fm, content };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Project pulse reader ────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
async function readProjectPulse(targetDir) {
|
|
273
|
+
const filePath = path.join(contextDir(targetDir), 'project-pulse.md');
|
|
274
|
+
const content = await readFileSafe(filePath);
|
|
275
|
+
if (!content) return { exists: false };
|
|
276
|
+
const fm = parseFrontmatter(content);
|
|
277
|
+
return { exists: true, ...fm, content };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Classification reader ────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
async function detectClassification(targetDir, slug) {
|
|
283
|
+
// 1. Try project context
|
|
284
|
+
const ctx = await loadProjectContext(targetDir);
|
|
285
|
+
if (ctx.data.classification) return ctx.data.classification.toUpperCase();
|
|
286
|
+
|
|
287
|
+
// 2. Try spec frontmatter
|
|
288
|
+
if (slug) {
|
|
289
|
+
const specContent = await readFileSafe(path.join(contextDir(targetDir), `spec-${slug}.md`));
|
|
290
|
+
if (specContent) {
|
|
291
|
+
const fm = parseFrontmatter(specContent);
|
|
292
|
+
if (fm.classification) return fm.classification.toUpperCase();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 3. Try PRD frontmatter
|
|
296
|
+
const prdContent = await readFileSafe(path.join(contextDir(targetDir), `prd-${slug}.md`));
|
|
297
|
+
if (prdContent) {
|
|
298
|
+
const fm = parseFrontmatter(prdContent);
|
|
299
|
+
if (fm.classification) return fm.classification.toUpperCase();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Rules discovery ──────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
async function discoverRules(targetDir, agent) {
|
|
309
|
+
const dir = rulesDir(targetDir);
|
|
310
|
+
const rules = [];
|
|
311
|
+
|
|
312
|
+
let entries;
|
|
313
|
+
try {
|
|
314
|
+
entries = await fs.readdir(dir);
|
|
315
|
+
} catch {
|
|
316
|
+
return rules;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const entry of entries) {
|
|
320
|
+
if (!entry.endsWith('.md')) continue;
|
|
321
|
+
const content = await readFileSafe(path.join(dir, entry));
|
|
322
|
+
if (!content) continue;
|
|
323
|
+
|
|
324
|
+
// Check applicability: universal rules or agent-specific
|
|
325
|
+
const fm = parseFrontmatter(content);
|
|
326
|
+
const applies = !fm.agents || fm.agents.includes('all') || fm.agents.includes(agent);
|
|
327
|
+
if (applies) rules.push(entry);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return rules;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ─── Context package builder ──────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
function buildContextPackage(agent, slug, classification, artifacts, devState) {
|
|
336
|
+
const pkg = [];
|
|
337
|
+
|
|
338
|
+
if (artifacts.project_context.exists) pkg.push(artifacts.project_context.path);
|
|
339
|
+
|
|
340
|
+
if (slug) {
|
|
341
|
+
// Feature-specific context
|
|
342
|
+
if (artifacts.spec.exists) pkg.push(artifacts.spec.path);
|
|
343
|
+
if (artifacts.implementation_plan.exists) pkg.push(artifacts.implementation_plan.path);
|
|
344
|
+
if (artifacts.requirements.exists && ['analyst', 'architect', 'dev'].includes(agent)) {
|
|
345
|
+
pkg.push(artifacts.requirements.path);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Agent-specific additions
|
|
350
|
+
if (agent === 'dev' && artifacts.dev_state.exists) pkg.push('dev-state.md (check for active state)');
|
|
351
|
+
if (agent === 'qa' && artifacts.spec.exists) pkg.push(artifacts.spec.path);
|
|
352
|
+
if (agent === 'architect' && artifacts.architecture.exists) pkg.push(artifacts.architecture.path);
|
|
353
|
+
|
|
354
|
+
return [...new Set(pkg)];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─── Readiness evaluator ─────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
function evaluateReadiness(artifacts, phaseGates, classification, agent) {
|
|
360
|
+
const blockers = [];
|
|
361
|
+
|
|
362
|
+
if (!artifacts.project_context.exists) blockers.push('project.context.md missing');
|
|
363
|
+
|
|
364
|
+
if (agent === 'dev') {
|
|
365
|
+
if (!artifacts.spec.exists) blockers.push('spec file missing');
|
|
366
|
+
if (phaseGates.plan && phaseGates.plan !== 'approved') {
|
|
367
|
+
blockers.push(`Gate C (plan) not approved: ${phaseGates.plan || 'pending'}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (agent === 'qa') {
|
|
372
|
+
if (!artifacts.spec.exists) blockers.push('spec file missing');
|
|
373
|
+
if (classification && classification !== 'MICRO') {
|
|
374
|
+
if (phaseGates.plan && phaseGates.plan !== 'approved') {
|
|
375
|
+
blockers.push(`Gate C (plan) not approved: ${phaseGates.plan || 'pending'}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (agent === 'analyst') {
|
|
381
|
+
if (!artifacts.prd.exists) blockers.push('prd file missing');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (agent === 'architect') {
|
|
385
|
+
if (!artifacts.requirements.exists) blockers.push('requirements file missing');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return blockers.length === 0
|
|
389
|
+
? { status: 'READY', blockers: [] }
|
|
390
|
+
: { status: 'BLOCKED', blockers };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ─── Spec version extractor ───────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
function extractSpecVersion(artifact) {
|
|
396
|
+
if (!artifact.exists) return null;
|
|
397
|
+
return artifact.frontmatter.version || null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function extractLastCheckpoint(artifact) {
|
|
401
|
+
if (!artifact.exists) return null;
|
|
402
|
+
const fm = artifact.frontmatter;
|
|
403
|
+
if (fm.last_checkpoint) return fm.last_checkpoint;
|
|
404
|
+
|
|
405
|
+
// Scan content for checkpoint patterns — use last occurrence (most recent)
|
|
406
|
+
if (artifact.content) {
|
|
407
|
+
const matches = artifact.content.match(/last_checkpoint:\s*(.+)/g);
|
|
408
|
+
if (matches && matches.length > 0) {
|
|
409
|
+
const last = matches[matches.length - 1];
|
|
410
|
+
const val = last.replace(/^last_checkpoint:\s*/, '').trim().replace(/^["']|["']$/g, '');
|
|
411
|
+
return val;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
parseFrontmatter,
|
|
421
|
+
readFileSafe,
|
|
422
|
+
fileExists,
|
|
423
|
+
fileStat,
|
|
424
|
+
detectFramework,
|
|
425
|
+
detectTestRunner,
|
|
426
|
+
contextDir,
|
|
427
|
+
rulesDir,
|
|
428
|
+
artifactPath,
|
|
429
|
+
loadProjectContext,
|
|
430
|
+
scanArtifacts,
|
|
431
|
+
parseGatesFromSpec,
|
|
432
|
+
readPhaseGates,
|
|
433
|
+
readDevState,
|
|
434
|
+
readProjectPulse,
|
|
435
|
+
detectClassification,
|
|
436
|
+
discoverRules,
|
|
437
|
+
buildContextPackage,
|
|
438
|
+
evaluateReadiness,
|
|
439
|
+
extractSpecVersion,
|
|
440
|
+
extractLastCheckpoint,
|
|
441
|
+
GATE_NAMES,
|
|
442
|
+
GATE_ALIASES
|
|
443
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { launchCLI } = require('./cli-launcher');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_ATTEMPTS = { haiku: 3, sonnet: 2, opus: 1 };
|
|
6
|
+
|
|
7
|
+
// Model IDs por alias. Injetados via ANTHROPIC_MODEL env var para Claude Code.
|
|
8
|
+
const MODEL_MAP = {
|
|
9
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
10
|
+
sonnet: 'claude-sonnet-4-6',
|
|
11
|
+
opus: 'claude-opus-4-6'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Executa uma task com cascade de modelos.
|
|
16
|
+
*
|
|
17
|
+
* Para cada modelo na cadeia:
|
|
18
|
+
* 1. Tenta até N vezes (DEFAULT_ATTEMPTS ou customizado)
|
|
19
|
+
* 2. Se result.ok: verifica via gateConfig (opcional)
|
|
20
|
+
* 3. Se aprovado: retorna resultado imediatamente
|
|
21
|
+
* 4. Se reprovado ou falha: tenta próximo modelo
|
|
22
|
+
*
|
|
23
|
+
* @param {string} projectDir
|
|
24
|
+
* @param {string} prompt
|
|
25
|
+
* @param {string[]} modelChain ex: ['haiku', 'sonnet', 'opus']
|
|
26
|
+
* @param {object} options
|
|
27
|
+
* @param {string} [options.tool] CLI a usar (padrão: auto-detectado)
|
|
28
|
+
* @param {Function} [options.gateCheck] fn(output: string) → {passed: boolean, reason?: string}
|
|
29
|
+
* @param {Function} [options.onProgress] fn({model, attempt, maxAttempts, status, reason?})
|
|
30
|
+
* @param {number} [options.timeout] Timeout em ms por tentativa
|
|
31
|
+
* @returns {Promise<{ok: boolean, result?, modelUsed?: string, attempts?: number, error?: string}>}
|
|
32
|
+
*/
|
|
33
|
+
async function runWithCascade(projectDir, prompt, modelChain, options = {}) {
|
|
34
|
+
const { tool, gateCheck, onProgress, timeout } = options;
|
|
35
|
+
|
|
36
|
+
for (const modelAlias of modelChain) {
|
|
37
|
+
const maxAttempts = DEFAULT_ATTEMPTS[modelAlias] ?? 1;
|
|
38
|
+
const modelId = MODEL_MAP[modelAlias];
|
|
39
|
+
|
|
40
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
41
|
+
if (onProgress) {
|
|
42
|
+
onProgress({ model: modelAlias, attempt, maxAttempts, status: 'running' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Injeta o modelo via ANTHROPIC_MODEL (compatível com Claude Code)
|
|
46
|
+
const extraEnv = modelId ? { ANTHROPIC_MODEL: modelId } : {};
|
|
47
|
+
|
|
48
|
+
const result = await launchCLI(projectDir, prompt, {
|
|
49
|
+
tool,
|
|
50
|
+
timeout,
|
|
51
|
+
env: extraEnv
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
if (onProgress) {
|
|
56
|
+
onProgress({ model: modelAlias, attempt, maxAttempts, status: 'cli_failed' });
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Verifica qualidade se gate configurado
|
|
62
|
+
if (gateCheck) {
|
|
63
|
+
const gateResult = gateCheck(result.output);
|
|
64
|
+
if (gateResult.passed) {
|
|
65
|
+
return { ok: true, result, modelUsed: modelAlias, attempts: attempt };
|
|
66
|
+
}
|
|
67
|
+
if (onProgress) {
|
|
68
|
+
onProgress({
|
|
69
|
+
model: modelAlias,
|
|
70
|
+
attempt,
|
|
71
|
+
maxAttempts,
|
|
72
|
+
status: 'gate_failed',
|
|
73
|
+
reason: gateResult.reason || 'quality gate failed'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Não retorna — tenta próxima tentativa ou próximo modelo
|
|
77
|
+
} else {
|
|
78
|
+
// Sem gate: aceita o resultado
|
|
79
|
+
return { ok: true, result, modelUsed: modelAlias, attempts: attempt };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { ok: false, error: 'All cascade models exhausted without passing quality gate' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parseia uma string de cascade como "haiku,sonnet,opus" para um array.
|
|
89
|
+
* @param {string} cascadeStr
|
|
90
|
+
* @returns {string[]}
|
|
91
|
+
*/
|
|
92
|
+
function parseCascadeChain(cascadeStr) {
|
|
93
|
+
if (!cascadeStr) return [];
|
|
94
|
+
return cascadeStr.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { runWithCascade, parseCascadeChain, MODEL_MAP, DEFAULT_ATTEMPTS };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detecta qual CLI de AI está disponível no sistema.
|
|
7
|
+
* Usa AIOSON_RUNNER_TOOL env var se definida, depois tenta
|
|
8
|
+
* claude, codex, gemini, opencode em sequência.
|
|
9
|
+
*/
|
|
10
|
+
async function detectCLI() {
|
|
11
|
+
const envTool = process.env.AIOSON_RUNNER_TOOL;
|
|
12
|
+
if (envTool) return envTool;
|
|
13
|
+
|
|
14
|
+
for (const cli of ['claude', 'codex', 'gemini', 'opencode']) {
|
|
15
|
+
const found = await new Promise((resolve) => {
|
|
16
|
+
const child = spawn('which', [cli], { stdio: 'pipe' });
|
|
17
|
+
child.on('close', (code) => resolve(code === 0));
|
|
18
|
+
child.on('error', () => resolve(false));
|
|
19
|
+
});
|
|
20
|
+
if (found) return cli;
|
|
21
|
+
}
|
|
22
|
+
throw new Error('No AI CLI found. Install claude, codex, gemini, or opencode.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Monta os argumentos de headless para cada CLI.
|
|
27
|
+
*/
|
|
28
|
+
function buildArgs(cli, prompt, options = {}) {
|
|
29
|
+
const { allowedTools, outputFormat } = options;
|
|
30
|
+
|
|
31
|
+
switch (cli) {
|
|
32
|
+
case 'claude':
|
|
33
|
+
return [
|
|
34
|
+
'-p', prompt,
|
|
35
|
+
'--output-format', outputFormat || 'stream-json',
|
|
36
|
+
'--dangerously-skip-permissions',
|
|
37
|
+
...(allowedTools ? ['--allowedTools', allowedTools] : [])
|
|
38
|
+
];
|
|
39
|
+
case 'codex':
|
|
40
|
+
return ['-p', prompt, '--quiet', '--no-interactive'];
|
|
41
|
+
case 'gemini':
|
|
42
|
+
return ['-p', prompt, '--quiet'];
|
|
43
|
+
default:
|
|
44
|
+
return ['-p', prompt];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Spawna o CLI de AI com o prompt headless e retorna o output.
|
|
50
|
+
* Detecta TASK_COMPLETE no stream para sinalizar conclusão.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} projectDir
|
|
53
|
+
* @param {string} prompt
|
|
54
|
+
* @param {object} options
|
|
55
|
+
* @returns {Promise<{ok: boolean, output: string, stderr: string, completionMarker: boolean, exitCode: number}>}
|
|
56
|
+
*/
|
|
57
|
+
async function launchCLI(projectDir, prompt, options = {}) {
|
|
58
|
+
const { tool, timeout = 120000, onData, env: extraEnv } = options;
|
|
59
|
+
const cli = tool || await detectCLI();
|
|
60
|
+
const args = buildArgs(cli, prompt, options);
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const child = spawn(cli, args, {
|
|
64
|
+
cwd: projectDir,
|
|
65
|
+
env: { ...process.env, ...(extraEnv || {}) },
|
|
66
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let stdout = '';
|
|
70
|
+
let stderr = '';
|
|
71
|
+
let completionMarker = false;
|
|
72
|
+
let settled = false;
|
|
73
|
+
|
|
74
|
+
const timer = timeout > 0
|
|
75
|
+
? setTimeout(() => {
|
|
76
|
+
if (!settled) child.kill('SIGTERM');
|
|
77
|
+
}, timeout)
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
child.stdout.on('data', (chunk) => {
|
|
81
|
+
const text = chunk.toString();
|
|
82
|
+
stdout += text;
|
|
83
|
+
if (onData) onData(text);
|
|
84
|
+
if (text.includes('TASK_COMPLETE')) completionMarker = true;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
88
|
+
|
|
89
|
+
child.on('close', (code) => {
|
|
90
|
+
settled = true;
|
|
91
|
+
if (timer) clearTimeout(timer);
|
|
92
|
+
resolve({
|
|
93
|
+
ok: code === 0,
|
|
94
|
+
output: stdout.trim(),
|
|
95
|
+
stderr: stderr.trim(),
|
|
96
|
+
completionMarker,
|
|
97
|
+
exitCode: code ?? -1
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
child.on('error', (err) => {
|
|
102
|
+
settled = true;
|
|
103
|
+
if (timer) clearTimeout(timer);
|
|
104
|
+
resolve({ ok: false, output: '', stderr: err.message, completionMarker: false, exitCode: -1 });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { launchCLI, detectCLI, buildArgs };
|