stealthos-cli 0.1.0-alpha.3 → 0.1.0-alpha.4
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/ai/CONTRACT.md +110 -0
- package/ai/INDEX.md +203 -0
- package/ai/README.md +434 -0
- package/ai/ROUTER.md +288 -0
- package/ai/agents/README.md +103 -0
- package/ai/agents/architect.md +59 -0
- package/ai/agents/backend-engineer.md +62 -0
- package/ai/agents/founder.md +45 -0
- package/ai/agents/frontend-engineer.md +61 -0
- package/ai/agents/product-manager.md +56 -0
- package/ai/agents/qa-engineer.md +53 -0
- package/ai/agents/researcher.md +74 -0
- package/ai/agents/reviewer.md +73 -0
- package/ai/agents/security-engineer.md +59 -0
- package/ai/agents/sre-engineer.md +70 -0
- package/ai/agents/tech-lead.md +70 -0
- package/ai/architecture/README.md +35 -0
- package/ai/architecture/components.md +24 -0
- package/ai/architecture/containers.md +30 -0
- package/ai/architecture/event-flows.md +36 -0
- package/ai/architecture/sequence-diagrams.md +38 -0
- package/ai/architecture/system-context.md +46 -0
- package/ai/architecture/threat-modeling.md +40 -0
- package/ai/blueprints/README.md +67 -0
- package/ai/blueprints/_schema.json +40 -0
- package/ai/blueprints/ai-platform.json +28 -0
- package/ai/blueprints/crm.json +22 -0
- package/ai/blueprints/game.json +25 -0
- package/ai/blueprints/mobile.json +24 -0
- package/ai/blueprints/realtime.json +22 -0
- package/ai/blueprints/saas.json +25 -0
- package/ai/blueprints/telemetry.json +30 -0
- package/ai/blueprints/web.json +23 -0
- package/ai/bootstrap/discovery-questions.md +117 -0
- package/ai/bootstrap/dispatcher.md +85 -0
- package/ai/bootstrap/existing-project.md +191 -0
- package/ai/bootstrap/new-project.md +127 -0
- package/ai/bootstrap/tech-mapping.md +164 -0
- package/ai/clients/README.md +114 -0
- package/ai/clients/antigravity.md +125 -0
- package/ai/clients/claude-code.md +65 -0
- package/ai/clients/cline.md +69 -0
- package/ai/clients/codex-aider-cli.md +82 -0
- package/ai/clients/continue.md +67 -0
- package/ai/clients/copilot.md +49 -0
- package/ai/clients/cursor.md +81 -0
- package/ai/clients/snippets/mcp-absolute-paths.json +9 -0
- package/ai/clients/snippets/mcp-http.json +7 -0
- package/ai/clients/snippets/mcp-stdio.json +9 -0
- package/ai/clients/trae.md +69 -0
- package/ai/clients/windsurf.md +71 -0
- package/ai/core/pipeline/execution-engine.md +157 -0
- package/ai/engineering/README.md +32 -0
- package/ai/engineering/observability/incident-response.md +82 -0
- package/ai/evals/protocol-tests.md +150 -0
- package/ai/evolution/agent-evolution.md +161 -0
- package/ai/evolution/improvements.md +91 -0
- package/ai/evolution/learnings.md +49 -0
- package/ai/evolution/patterns-discovered.md +48 -0
- package/ai/execution/README.md +33 -0
- package/ai/execution/backlog.md +27 -0
- package/ai/execution/milestones.md +26 -0
- package/ai/execution/roadmap.md +30 -0
- package/ai/execution/sprint.md +42 -0
- package/ai/governance/README.md +34 -0
- package/ai/governance/architecture-principles.md +99 -0
- package/ai/governance/definition-of-done.md +88 -0
- package/ai/governance/definition-of-ready.md +69 -0
- package/ai/governance/engineering-principles.md +70 -0
- package/ai/governance/quality-gates.md +85 -0
- package/ai/governance/security-policies.md +84 -0
- package/ai/hooks/enforce-audit.ps1 +41 -0
- package/ai/hooks/enforce-audit.sh +39 -0
- package/ai/hooks/guard-edit.ps1 +182 -0
- package/ai/hooks/guard-edit.sh +161 -0
- package/ai/hooks/inject-os-reminder.ps1 +40 -0
- package/ai/hooks/inject-os-reminder.sh +16 -0
- package/ai/manifest.json +238 -0
- package/ai/memory/_detected-stack.json +33 -0
- package/ai/memory/_summary.md +49 -0
- package/ai/memory/archive/.gitkeep +3 -0
- package/ai/memory/completed-tasks.md +156 -0
- package/ai/memory/decisions.md +257 -0
- package/ai/memory/errors-and-solutions.md +41 -0
- package/ai/memory/known-issues.md +40 -0
- package/ai/memory/pending-tasks.md +37 -0
- package/ai/memory/project-context.md +67 -0
- package/ai/operating-system/architecture.md +54 -0
- package/ai/operating-system/coding-standards.md +84 -0
- package/ai/operating-system/folder-structure.md +126 -0
- package/ai/operating-system/performance-rules.md +86 -0
- package/ai/operating-system/quality-control.md +81 -0
- package/ai/operating-system/security-rules.md +91 -0
- package/ai/operating-system/workflow.md +86 -0
- package/ai/product/README.md +24 -0
- package/ai/product/business-rules.md +26 -0
- package/ai/product/personas.md +29 -0
- package/ai/product/user-journeys.md +30 -0
- package/ai/product/vision.md +35 -0
- package/ai/rules/behavior.md +45 -0
- package/ai/rules/do.md +47 -0
- package/ai/rules/dont.md +46 -0
- package/ai/rules/execution-flow.md +125 -0
- package/ai/rules/structural-constraints.md +59 -0
- package/ai/rules/structure-canon.md +116 -0
- package/ai/runtime.md +179 -0
- package/ai/scripts/detect-stack.ps1 +166 -0
- package/ai/scripts/detect-stack.sh +172 -0
- package/ai/scripts/init-ai-os.ps1 +170 -0
- package/ai/scripts/init-ai-os.sh +99 -0
- package/ai/scripts/lint-os.ps1 +99 -0
- package/ai/scripts/lint-os.sh +85 -0
- package/ai/scripts/start-os.ps1 +151 -0
- package/ai/scripts/start-os.sh +141 -0
- package/ai/server/README.md +105 -0
- package/ai/server/aios-server.mjs +2134 -0
- package/ai/server/package-lock.json +802 -0
- package/ai/server/package.json +31 -0
- package/ai/server/src/analyzer/graph-builder.ts +92 -0
- package/ai/server/src/analyzer/index.ts +191 -0
- package/ai/server/src/analyzer/module-mapper.ts +171 -0
- package/ai/server/src/analyzer/smell-detector.ts +54 -0
- package/ai/server/src/analyzer/stack-detector.ts +70 -0
- package/ai/server/src/index.ts +16 -0
- package/ai/server/src/packager/context-builder.ts +217 -0
- package/ai/server/src/packager/index.ts +3 -0
- package/ai/server/src/packager/memory-injector.ts +128 -0
- package/ai/server/src/packager/module-summarizer.ts +60 -0
- package/ai/server/src/packager/token-estimator.ts +26 -0
- package/ai/server/src/snapshot/index.ts +3 -0
- package/ai/server/src/snapshot/snapshot-creator.ts +206 -0
- package/ai/server/src/snapshot/snapshot-diff.ts +86 -0
- package/ai/server/src/snapshot/snapshot-restore.ts +14 -0
- package/ai/server/src/types.ts +94 -0
- package/ai/server/tsconfig.json +26 -0
- package/ai/skills/architecture-design.md +82 -0
- package/ai/skills/backend-engineering.md +57 -0
- package/ai/skills/database-design.md +76 -0
- package/ai/skills/frontend-engineering.md +63 -0
- package/ai/skills/performance.md +73 -0
- package/ai/skills/scalability.md +84 -0
- package/ai/skills/security.md +71 -0
- package/ai/skills/testing.md +77 -0
- package/ai/specs/ADR/ADR-0002-typescript-runtime.md +103 -0
- package/ai/specs/ADR/ADR-0004-runtime-orchestrator.md +94 -0
- package/ai/specs/ADR/ADR-0005-workflow-engine.md +105 -0
- package/ai/specs/ADR/ADR-0006-runtime-state.md +104 -0
- package/ai/specs/ADR/ADR-0007-state-compiler-drift-context-layers-artifact-index.md +82 -0
- package/ai/specs/ADR/ADR-0008-intent-runtime-discovery-branching.md +93 -0
- package/ai/specs/ADR/ADR-0009-confidence-system-maturity-tracking.md +113 -0
- package/ai/specs/ADR/ADR-0010-structural-architecture-standards.md +121 -0
- package/ai/specs/ADR/ADR-0011-mcp-prompts.md +86 -0
- package/ai/specs/ADR/ADR-0012-stealthos-hybrid-architecture.md +174 -0
- package/ai/specs/ADR/_TEMPLATE.md +60 -0
- package/ai/specs/BRD/_TEMPLATE.md +50 -0
- package/ai/specs/PRD/_TEMPLATE.md +72 -0
- package/ai/specs/README.md +43 -0
- package/ai/specs/RFC/RFC-0001-runtime-orchestrator.md +149 -0
- package/ai/specs/RFC/RFC-0002-runtime-orchestrator-extended.md +134 -0
- package/ai/specs/RFC/_TEMPLATE.md +61 -0
- package/ai/specs/RUNBOOKS/_TEMPLATE.md +68 -0
- package/ai/specs/SDD/_TEMPLATE.md +104 -0
- package/ai/specs/TASKS/_TEMPLATE.md +52 -0
- package/ai/tools/debugging.md +64 -0
- package/ai/tools/dependency-analysis.md +46 -0
- package/ai/tools/internet-research.md +42 -0
- package/ai/tools/mcp-discovery.md +44 -0
- package/ai/workflows/_schema.json +81 -0
- package/ai/workflows/init.json +148 -0
- package/ai/workflows/sync.json +71 -0
- package/ai/workflows/work.json +91 -0
- package/package.json +7 -1
- package/scripts/bundle-ai.mjs +58 -0
- package/src/cli.mjs +1 -1
- package/src/commands/install.mjs +35 -11
- package/src/lib/resolve-source.mjs +27 -10
|
@@ -0,0 +1,2134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AI OS — MCP Server (stdio, pure Node, zero deps)
|
|
3
|
+
// Speaks JSON-RPC 2.0 over stdin/stdout per the Model Context Protocol.
|
|
4
|
+
// Optional HTTP mode: --http [--port N]
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node server/aios-server.mjs # MCP stdio (default)
|
|
8
|
+
// node server/aios-server.mjs --http # HTTP server on port 47781 (auto-fallback)
|
|
9
|
+
// node server/aios-server.mjs --http --port 8080
|
|
10
|
+
|
|
11
|
+
import { readFile, readdir, stat, writeFile, mkdir, appendFile } from "node:fs/promises";
|
|
12
|
+
import { existsSync, statSync, readFileSync, readdirSync } from "node:fs";
|
|
13
|
+
import { join, dirname, resolve, relative, sep, normalize } from "node:path";
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { createServer } from "node:http";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import readline from "node:readline";
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// ---------- StealthOS multi-tenant resolution (ADR-0012) ----------
|
|
25
|
+
// MODE:
|
|
26
|
+
// 'embedded' (default) — knowledge base & project state both in <project>/.ai/
|
|
27
|
+
// 'hybrid' — knowledge base in ~/.stealthos/, project state in ~/.stealthos/projects/<hash>/
|
|
28
|
+
const STEALTHOS_MODE = (process.env.STEALTHOS_MODE || "embedded").toLowerCase();
|
|
29
|
+
const STEALTHOS_HOME = process.env.STEALTHOS_HOME || join(homedir(), ".stealthos");
|
|
30
|
+
|
|
31
|
+
// PROJECT_ROOT: where the user's code lives. In embedded mode, derived from server location.
|
|
32
|
+
// In hybrid mode, the server is in ~/.stealthos/ and the project is wherever the MCP client launched us.
|
|
33
|
+
const PROJECT_ROOT = STEALTHOS_MODE === "hybrid"
|
|
34
|
+
? (process.env.STEALTHOS_PROJECT_DIR || process.cwd())
|
|
35
|
+
: resolve(__dirname, "..", "..");
|
|
36
|
+
|
|
37
|
+
function computeProjectHash(absPath) {
|
|
38
|
+
return createHash("sha256").update(normalize(resolve(absPath)), "utf8").digest("hex").slice(0, 12);
|
|
39
|
+
}
|
|
40
|
+
const PROJECT_HASH = computeProjectHash(PROJECT_ROOT);
|
|
41
|
+
|
|
42
|
+
// KB_DIR — Knowledge base (canonical OS: rules, workflows, manifest, ROUTER, hooks, blueprints, templates)
|
|
43
|
+
const KB_DIR = STEALTHOS_MODE === "hybrid" ? STEALTHOS_HOME : join(PROJECT_ROOT, ".ai");
|
|
44
|
+
|
|
45
|
+
// PROJECT_STATE_DIR — Project-specific state (runtime, memory, snapshots, context, artifacts, specs, architecture)
|
|
46
|
+
const PROJECT_STATE_DIR = STEALTHOS_MODE === "hybrid"
|
|
47
|
+
? join(STEALTHOS_HOME, "projects", PROJECT_HASH)
|
|
48
|
+
: join(PROJECT_ROOT, ".ai");
|
|
49
|
+
|
|
50
|
+
// Legacy alias — AI_DIR is kept for code that pre-dates the split.
|
|
51
|
+
// In embedded mode it equals both KB_DIR and PROJECT_STATE_DIR.
|
|
52
|
+
// In hybrid mode it points to KB_DIR for backward-compat reads (knowledge base ops).
|
|
53
|
+
const AI_DIR = KB_DIR;
|
|
54
|
+
|
|
55
|
+
// ---------- Runtime v0.1 (optional, lazy) ----------
|
|
56
|
+
// Imports the compiled TS runtime from ./dist/index.mjs if available.
|
|
57
|
+
// If absent (build not run yet), daemon continues with the 12 base tools only.
|
|
58
|
+
let runtime = null;
|
|
59
|
+
let runtimeLoadError = null;
|
|
60
|
+
const RUNTIME_DIST_PATH = join(__dirname, "dist", "index.mjs");
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(RUNTIME_DIST_PATH)) {
|
|
63
|
+
runtime = await import(pathToFileURL(RUNTIME_DIST_PATH).href);
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
runtimeLoadError = String(e && e.message ? e.message : e);
|
|
67
|
+
process.stderr.write(`[aios] Runtime v0.1 dist failed to load: ${runtimeLoadError}\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------- Args ----------
|
|
71
|
+
const args = process.argv.slice(2);
|
|
72
|
+
const HTTP_MODE = args.includes("--http");
|
|
73
|
+
const portIdx = args.indexOf("--port");
|
|
74
|
+
const REQUESTED_PORT = portIdx > -1 ? parseInt(args[portIdx + 1], 10) : 47781;
|
|
75
|
+
|
|
76
|
+
// ---------- Utility: safe path ----------
|
|
77
|
+
// Resolves a relative path to its absolute form, restricted to the AI OS allowlist.
|
|
78
|
+
// Mode-aware (ADR-0012): in 'hybrid', knowledge-base paths resolve to KB_DIR (~/.stealthos/)
|
|
79
|
+
// and project-state paths to PROJECT_STATE_DIR (~/.stealthos/projects/<hash>/).
|
|
80
|
+
// In 'embedded', everything resolves under PROJECT_ROOT/.ai/ as before.
|
|
81
|
+
const PROJECT_STATE_PREFIXES = ["memory", "runtime", "snapshots", "context", "artifacts", "specs", "architecture", "product"];
|
|
82
|
+
|
|
83
|
+
function safeResolve(rel) {
|
|
84
|
+
const clean = rel.replace(/^[\/\\]+/, "");
|
|
85
|
+
|
|
86
|
+
// Project files (always under user's cwd, regardless of mode)
|
|
87
|
+
const projectFileMatches =
|
|
88
|
+
clean === "CLAUDE.md" || clean === "GEMINI.md" || clean === "GPT.md" || clean === "AGENTS.md" ||
|
|
89
|
+
clean === ".claude" || clean.startsWith(".claude/") || clean.startsWith(".claude\\") ||
|
|
90
|
+
clean.startsWith(".ai/scripts/") || clean.startsWith(".ai\\scripts\\");
|
|
91
|
+
if (projectFileMatches) {
|
|
92
|
+
const baseAbs = resolve(PROJECT_ROOT);
|
|
93
|
+
const abs = resolve(baseAbs, clean);
|
|
94
|
+
if (!abs.startsWith(baseAbs)) throw new Error("Path traversal blocked");
|
|
95
|
+
return abs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Determine base: project-state prefix → PROJECT_STATE_DIR; else → KB_DIR
|
|
99
|
+
const firstSeg = clean.split(/[\/\\]/)[0];
|
|
100
|
+
const baseRaw = PROJECT_STATE_PREFIXES.includes(firstSeg) ? PROJECT_STATE_DIR : KB_DIR;
|
|
101
|
+
// resolve(base) normalizes separators so startsWith() works on Windows even if env var used /
|
|
102
|
+
const base = resolve(baseRaw);
|
|
103
|
+
const abs = resolve(base, clean);
|
|
104
|
+
if (!abs.startsWith(base)) throw new Error("Path traversal blocked");
|
|
105
|
+
return abs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readDoc(rel) {
|
|
109
|
+
const abs = safeResolve(rel);
|
|
110
|
+
return await readFile(abs, "utf8");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function listDir(rel) {
|
|
114
|
+
const abs = safeResolve(rel);
|
|
115
|
+
const entries = await readdir(abs, { withFileTypes: true });
|
|
116
|
+
return entries.map((e) => ({ name: e.name, type: e.isDirectory() ? "dir" : "file" }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------- Router parser ----------
|
|
120
|
+
let routerCache = null;
|
|
121
|
+
async function parseRouter() {
|
|
122
|
+
if (routerCache) return routerCache;
|
|
123
|
+
const txt = await readFile(join(AI_DIR, "ROUTER.md"), "utf8");
|
|
124
|
+
const routes = [];
|
|
125
|
+
// Pattern: heading "### Title" followed by **Triggers**: list and **Load**: code block
|
|
126
|
+
const sections = txt.split(/\n###\s+/).slice(1);
|
|
127
|
+
for (const sec of sections) {
|
|
128
|
+
const lines = sec.split("\n");
|
|
129
|
+
const title = lines[0].trim();
|
|
130
|
+
const triggersMatch = sec.match(/\*\*Triggers\*\*:\s*(.+)/);
|
|
131
|
+
const loadMatch = sec.match(/\*\*Load\*\*:\s*\n```\n([\s\S]*?)\n```/);
|
|
132
|
+
if (!triggersMatch || !loadMatch) continue;
|
|
133
|
+
const triggers = triggersMatch[1]
|
|
134
|
+
.split(",")
|
|
135
|
+
.map((s) => s.trim().replace(/^`|`$/g, "").toLowerCase())
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
const files = loadMatch[1]
|
|
138
|
+
.split("\n")
|
|
139
|
+
.map((s) => s.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
routes.push({ title, triggers, files });
|
|
142
|
+
}
|
|
143
|
+
routerCache = routes;
|
|
144
|
+
return routes;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeQuery(q) {
|
|
148
|
+
return String(q || "").toLowerCase();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function routeQuery(query) {
|
|
152
|
+
const routes = await parseRouter();
|
|
153
|
+
const q = normalizeQuery(query);
|
|
154
|
+
const matched = [];
|
|
155
|
+
const files = new Set();
|
|
156
|
+
for (const r of routes) {
|
|
157
|
+
for (const t of r.triggers) {
|
|
158
|
+
if (t && q.includes(t)) {
|
|
159
|
+
matched.push({ route: r.title, trigger: t });
|
|
160
|
+
for (const f of r.files) files.add(f);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Always include CORE
|
|
166
|
+
const CORE = [
|
|
167
|
+
".ai/INDEX.md",
|
|
168
|
+
".ai/CONTRACT.md",
|
|
169
|
+
".ai/ROUTER.md",
|
|
170
|
+
".ai/rules/dont.md",
|
|
171
|
+
".ai/rules/behavior.md",
|
|
172
|
+
".ai/memory/project-context.md",
|
|
173
|
+
];
|
|
174
|
+
for (const f of CORE) files.add(f);
|
|
175
|
+
return { core: CORE, matched, files: [...files] };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------- Classifier ----------
|
|
179
|
+
function classifyTask(query) {
|
|
180
|
+
const q = normalizeQuery(query);
|
|
181
|
+
|
|
182
|
+
// Trivial signals: short, factual, no action verbs
|
|
183
|
+
const len = q.length;
|
|
184
|
+
const wordCount = q.split(/\s+/).length;
|
|
185
|
+
const isQuestion = /^(qual|quais|como|o que|onde|quando|por que|por quê|when|what|where|why|how)\b/.test(q) || q.endsWith("?");
|
|
186
|
+
const isLookup = /(lista|listar|mostra|mostrar|mostre|ler|show|read|describe|cite|cita)/.test(q);
|
|
187
|
+
const hasEditVerbs = /(adiciona|adicionar|cria|criar|modifica|modificar|altera|alterar|refator|implementa|implementar|build|criar?|adicionar?|escreva|fix|corrige|corrigir|add|create|update|change|implement)/.test(q);
|
|
188
|
+
const hasArchKeywords = /(arquitetura|architecture|adr|trade.?off|microservi|escala|sharding|schema migration|api contract|breaking change)/.test(q);
|
|
189
|
+
const hasMultiFile = /(em todos|all files|cross-?module|atravessa|toda a app|toda aplicação|toda a base)/.test(q);
|
|
190
|
+
|
|
191
|
+
let cls = "trivial";
|
|
192
|
+
const reasons = [];
|
|
193
|
+
|
|
194
|
+
if (hasArchKeywords || hasMultiFile) {
|
|
195
|
+
cls = "complex";
|
|
196
|
+
if (hasArchKeywords) reasons.push("contém palavra-chave arquitetural");
|
|
197
|
+
if (hasMultiFile) reasons.push("escopo cross-module aparente");
|
|
198
|
+
} else if (hasEditVerbs) {
|
|
199
|
+
cls = "simple";
|
|
200
|
+
reasons.push("verbo de edição presente");
|
|
201
|
+
} else if (isQuestion || isLookup) {
|
|
202
|
+
cls = "trivial";
|
|
203
|
+
reasons.push("pergunta/lookup");
|
|
204
|
+
} else if (len > 280 || wordCount > 50) {
|
|
205
|
+
cls = "simple";
|
|
206
|
+
reasons.push("tamanho do prompt sugere tarefa não-trivial");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { classification: cls, reasons, wordCount, length: len };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------- Lint runner ----------
|
|
213
|
+
async function runLint() {
|
|
214
|
+
return new Promise((resolveProm) => {
|
|
215
|
+
const isWin = process.platform === "win32";
|
|
216
|
+
const cmd = isWin ? "powershell" : "bash";
|
|
217
|
+
const scriptPs = join(PROJECT_ROOT, ".ai", "scripts", "lint-os.ps1");
|
|
218
|
+
const scriptSh = join(PROJECT_ROOT, ".ai", "scripts", "lint-os.sh");
|
|
219
|
+
const script = isWin ? scriptPs : (existsSync(scriptSh) ? scriptSh : scriptPs);
|
|
220
|
+
const argsRun = isWin
|
|
221
|
+
? ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script, "-TargetPath", PROJECT_ROOT]
|
|
222
|
+
: [script];
|
|
223
|
+
const child = spawn(cmd, argsRun, { cwd: PROJECT_ROOT });
|
|
224
|
+
let out = "";
|
|
225
|
+
let err = "";
|
|
226
|
+
child.stdout.on("data", (d) => (out += d));
|
|
227
|
+
child.stderr.on("data", (d) => (err += d));
|
|
228
|
+
child.on("close", (code) => {
|
|
229
|
+
resolveProm({ exitCode: code, stdout: out, stderr: err });
|
|
230
|
+
});
|
|
231
|
+
child.on("error", (e) => resolveProm({ exitCode: -1, stdout: "", stderr: String(e) }));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function runDetectStack(targetPath) {
|
|
236
|
+
return new Promise((resolveProm) => {
|
|
237
|
+
const isWin = process.platform === "win32";
|
|
238
|
+
const cmd = isWin ? "powershell" : "bash";
|
|
239
|
+
const scriptPs = join(PROJECT_ROOT, ".ai", "scripts", "detect-stack.ps1");
|
|
240
|
+
const scriptSh = join(PROJECT_ROOT, ".ai", "scripts", "detect-stack.sh");
|
|
241
|
+
const script = isWin ? scriptPs : (existsSync(scriptSh) ? scriptSh : scriptPs);
|
|
242
|
+
const argsRun = isWin
|
|
243
|
+
? ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script, "-TargetPath", targetPath]
|
|
244
|
+
: [script, "--target", targetPath];
|
|
245
|
+
const child = spawn(cmd, argsRun, { cwd: PROJECT_ROOT });
|
|
246
|
+
let out = "";
|
|
247
|
+
let err = "";
|
|
248
|
+
child.stdout.on("data", (d) => (out += d));
|
|
249
|
+
child.stderr.on("data", (d) => (err += d));
|
|
250
|
+
child.on("close", (code) => {
|
|
251
|
+
resolveProm({ exitCode: code, stdout: out, stderr: err });
|
|
252
|
+
});
|
|
253
|
+
child.on("error", (e) => resolveProm({ exitCode: -1, stdout: "", stderr: String(e) }));
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------- Memory search ----------
|
|
258
|
+
async function searchMemory(query, maxResults = 20) {
|
|
259
|
+
const memDir = join(PROJECT_STATE_DIR, "memory");
|
|
260
|
+
const files = await readdir(memDir);
|
|
261
|
+
const q = normalizeQuery(query);
|
|
262
|
+
const results = [];
|
|
263
|
+
for (const f of files) {
|
|
264
|
+
const abs = join(memDir, f);
|
|
265
|
+
const st = await stat(abs).catch(() => null);
|
|
266
|
+
if (!st || !st.isFile()) continue;
|
|
267
|
+
if (!f.endsWith(".md")) continue;
|
|
268
|
+
const content = await readFile(abs, "utf8");
|
|
269
|
+
const lines = content.split("\n");
|
|
270
|
+
lines.forEach((line, idx) => {
|
|
271
|
+
if (line.toLowerCase().includes(q)) {
|
|
272
|
+
results.push({ file: `.ai/memory/${f}`, line: idx + 1, text: line.trim() });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return results.slice(0, maxResults);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------- CORE bundle ----------
|
|
280
|
+
async function getCoreBundle() {
|
|
281
|
+
const CORE = [
|
|
282
|
+
".ai/INDEX.md",
|
|
283
|
+
".ai/CONTRACT.md",
|
|
284
|
+
".ai/ROUTER.md",
|
|
285
|
+
".ai/rules/dont.md",
|
|
286
|
+
".ai/rules/behavior.md",
|
|
287
|
+
".ai/memory/project-context.md",
|
|
288
|
+
];
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const f of CORE) {
|
|
291
|
+
try {
|
|
292
|
+
const content = await readDoc(f);
|
|
293
|
+
out.push(`<file path="${f}">\n${content}\n</file>`);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
out.push(`<file path="${f}" error="${e.message}"/>`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return out.join("\n\n");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function getManifest() {
|
|
302
|
+
const m = await readFile(join(AI_DIR, "manifest.json"), "utf8");
|
|
303
|
+
return JSON.parse(m);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function getSummary() {
|
|
307
|
+
try {
|
|
308
|
+
return await readFile(join(PROJECT_STATE_DIR, "memory", "_summary.md"), "utf8");
|
|
309
|
+
} catch {
|
|
310
|
+
return "(_summary.md ainda não foi gerado — rode /compact-memory)";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------- Audit validator ----------
|
|
315
|
+
function validateAudit(text) {
|
|
316
|
+
const required = ["Files touched:", "Memory updated:", "Validations:", "Decisions:", "Open items:"];
|
|
317
|
+
const missing = required.filter((k) => !text.includes(k));
|
|
318
|
+
return {
|
|
319
|
+
valid: missing.length === 0,
|
|
320
|
+
missing,
|
|
321
|
+
hasHeader: text.includes("[OS Audit]"),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------- Tools registry ----------
|
|
326
|
+
const TOOLS = [
|
|
327
|
+
{
|
|
328
|
+
name: "aios_get_core_bundle",
|
|
329
|
+
description: "Retorna o pacote CORE do AI OS (INDEX + CONTRACT + ROUTER + rules/dont + rules/behavior + memory/project-context). Use no início de cada sessão como atalho a 6 reads.",
|
|
330
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "aios_route_query",
|
|
334
|
+
description: "Dado o texto da tarefa do usuário, retorna a lista determinística de arquivos a carregar (CORE + conditional matched pelo ROUTER.md).",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: { query: { type: "string", description: "Texto/intent da tarefa." } },
|
|
338
|
+
required: ["query"],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "aios_classify_task",
|
|
343
|
+
description: "Classifica uma tarefa como trivial | simple | complex usando heurística baseada em verbos e escopo.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: { query: { type: "string" } },
|
|
347
|
+
required: ["query"],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "aios_read",
|
|
352
|
+
description: "Lê um arquivo do AI OS (.ai/, CLAUDE.md, GEMINI.md, GPT.md, .claude/, scripts/). Path traversal bloqueado.",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
type: "object",
|
|
355
|
+
properties: { path: { type: "string", description: "Caminho relativo ao project root." } },
|
|
356
|
+
required: ["path"],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "aios_list",
|
|
361
|
+
description: "Lista o conteúdo de um diretório do AI OS.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
type: "object",
|
|
364
|
+
properties: { path: { type: "string" } },
|
|
365
|
+
required: ["path"],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "aios_search_memory",
|
|
370
|
+
description: "Busca substring em todos os arquivos de .ai/memory/. Retorna trechos com arquivo e linha.",
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
query: { type: "string" },
|
|
375
|
+
max_results: { type: "number", default: 20 },
|
|
376
|
+
},
|
|
377
|
+
required: ["query"],
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "aios_get_summary",
|
|
382
|
+
description: "Retorna .ai/memory/_summary.md (resumo rotativo dos últimos 90 dias).",
|
|
383
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "aios_get_manifest",
|
|
387
|
+
description: "Retorna .ai/manifest.json — fonte da verdade estrutural.",
|
|
388
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "aios_lint",
|
|
392
|
+
description: "Roda o lint do OS (manifest + links + frontmatter). Retorna stdout/stderr/exit code.",
|
|
393
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: "aios_detect_stack",
|
|
397
|
+
description: "Inspeciona um diretório de projeto e detecta linguagens, frameworks, infra, CI, smells.",
|
|
398
|
+
inputSchema: {
|
|
399
|
+
type: "object",
|
|
400
|
+
properties: { target_path: { type: "string", description: "Caminho absoluto do projeto a inspecionar." } },
|
|
401
|
+
required: ["target_path"],
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "aios_validate_audit",
|
|
406
|
+
description: "Valida se um bloco [OS Audit] tem todos os campos obrigatórios.",
|
|
407
|
+
inputSchema: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: { text: { type: "string" } },
|
|
410
|
+
required: ["text"],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "aios_health",
|
|
415
|
+
description: "Health check do daemon. Retorna versão, uptime, project root, .ai/ status.",
|
|
416
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "aios_analyze_project",
|
|
420
|
+
description: "Roda o Project State Engine: detecta stack, mapeia módulos (AST via ts-morph), constrói grafo de dependências, detecta smells. Persiste .ai/context/project-state.{json,md}. Requer Runtime v0.1 compilado (dist/index.mjs).",
|
|
421
|
+
inputSchema: {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: {
|
|
424
|
+
scan_dirs: {
|
|
425
|
+
type: "array",
|
|
426
|
+
items: { type: "string" },
|
|
427
|
+
description: "Diretórios relativos ao project root para escanear. Padrão: ['.ai/server/src', 'src', 'lib', 'app', 'packages'].",
|
|
428
|
+
},
|
|
429
|
+
write: { type: "boolean", description: "Persistir arquivos em .ai/context/. Padrão: true." },
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "aios_build_context_package",
|
|
435
|
+
description: "Gera um Context Package otimizado: stack + módulos relevantes (YAML-blocks) + decisões/erros/issues do memory + smells. Substitui dump de arquivos brutos.",
|
|
436
|
+
inputSchema: {
|
|
437
|
+
type: "object",
|
|
438
|
+
properties: {
|
|
439
|
+
intent: { type: "string", description: "Descrição da tarefa para guiar a seleção de módulos relevantes." },
|
|
440
|
+
max_modules: { type: "number", description: "Limite de módulos no pacote. Padrão: 12." },
|
|
441
|
+
write: { type: "boolean", description: "Persistir em .ai/context/packages/<slug>.md. Padrão: true." },
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "aios_snapshot",
|
|
447
|
+
description: "Cria snapshot do estado atual: project-state + hashes SHA-256 dos arquivos rastreados (.ai, .claude, CLAUDE.md, GEMINI.md, GPT.md) + memory snapshot. Salva em .ai/snapshots/<id>/.",
|
|
448
|
+
inputSchema: {
|
|
449
|
+
type: "object",
|
|
450
|
+
properties: {
|
|
451
|
+
label: { type: "string", description: "Rótulo curto para o snapshot (ex.: 'pre-v01-refactor')." },
|
|
452
|
+
},
|
|
453
|
+
required: ["label"],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: "aios_snapshot_list",
|
|
458
|
+
description: "Lista todos os snapshots existentes em .ai/snapshots/ (mais novos primeiro).",
|
|
459
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "aios_snapshot_diff",
|
|
463
|
+
description: "Compara dois snapshots: arquivos adicionados/removidos/alterados, módulos, delta de smells.",
|
|
464
|
+
inputSchema: {
|
|
465
|
+
type: "object",
|
|
466
|
+
properties: {
|
|
467
|
+
a: { type: "string", description: "ID do snapshot base." },
|
|
468
|
+
b: { type: "string", description: "ID do snapshot comparado." },
|
|
469
|
+
},
|
|
470
|
+
required: ["a", "b"],
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "aios_run_workflow",
|
|
475
|
+
description: "Executa um workflow determinístico declarado em .ai/workflows/<name>.json. Workflow encadeia chamadas a tools primitivas com tratamento de erro por step. Use isso em vez de chamar tools individuais quando houver um workflow registrado (init, sync, work). Retorna relatório de execução com status por step.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {
|
|
479
|
+
name: {
|
|
480
|
+
type: "string",
|
|
481
|
+
description: "Nome do workflow (ex.: 'init', 'sync', 'work'). Mapeia para .ai/workflows/<name>.json.",
|
|
482
|
+
},
|
|
483
|
+
input: {
|
|
484
|
+
type: "object",
|
|
485
|
+
description: "Valores substituídos em ${input.X} dentro dos args dos steps. Ex.: { intent: 'refactor auth module' }.",
|
|
486
|
+
additionalProperties: true,
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
required: ["name"],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "aios_list_workflows",
|
|
494
|
+
description: "Lista os workflows disponíveis em .ai/workflows/ com id, versão, descrição e número de steps.",
|
|
495
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "aios_state_get",
|
|
499
|
+
description: "Lê estado operacional do AI OS de .ai/runtime/state.json. Sem path → retorna state completo. Com path → retorna sub-valor por dotted path (ex.: 'lifecycle.phase', 'identity.maturity').",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
path: { type: "string", description: "Dotted path opcional. Ex.: 'lifecycle.phase'." },
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: "aios_state_set",
|
|
509
|
+
description: "Atualiza um campo do state.json por dotted path. Atualiza updated_at automaticamente. Use para mudanças avulsas; para phase/mode use aios_state_advance.",
|
|
510
|
+
inputSchema: {
|
|
511
|
+
type: "object",
|
|
512
|
+
properties: {
|
|
513
|
+
path: { type: "string", description: "Dotted path (ex.: 'rotating.active_focus')." },
|
|
514
|
+
value: { description: "Valor a atribuir (qualquer tipo JSON)." },
|
|
515
|
+
},
|
|
516
|
+
required: ["path", "value"],
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "aios_state_advance",
|
|
521
|
+
description: "Atalho para avançar lifecycle: muda state.lifecycle.phase e/ou state.lifecycle.mode. Pelo menos um dos dois é obrigatório. Reporta diff.",
|
|
522
|
+
inputSchema: {
|
|
523
|
+
type: "object",
|
|
524
|
+
properties: {
|
|
525
|
+
phase: { enum: ["discovery", "planning", "implementation", "stabilization", "production"] },
|
|
526
|
+
mode: { enum: ["readonly", "analysis", "execution", "validation"] },
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "aios_state_reset",
|
|
532
|
+
description: "Recria state.json com baseline (lifecycle=discovery/analysis, identity vazia, listas vazias). DESTRUTIVO: workflow_history é perdido. Use só em reset deliberado.",
|
|
533
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "aios_compile_runtime_context",
|
|
537
|
+
description: "Project State Compiler (ADR-0007). Consolida state.json + project-state + últimas decisions + top-N módulos + memory rotating num único JSON otimizado (.ai/runtime/runtime-context.json, target ≤4000 tokens). Use no início do /work em vez de carregar tudo separado.",
|
|
538
|
+
inputSchema: {
|
|
539
|
+
type: "object",
|
|
540
|
+
properties: {
|
|
541
|
+
max_modules: { type: "number", description: "Limite de módulos a incluir. Default 10." },
|
|
542
|
+
max_decisions: { type: "number", description: "Limite de ADRs recentes. Default 5." },
|
|
543
|
+
write: { type: "boolean", description: "Persistir em runtime-context.json. Default true." },
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "aios_detect_drift",
|
|
549
|
+
description: "Documentation Drift Detection (ADR-0007). Compara estado atual com último snapshot: módulos sem spec referenciada, specs sem código, módulos órfãos. Retorna severity tiers (info|warning).",
|
|
550
|
+
inputSchema: {
|
|
551
|
+
type: "object",
|
|
552
|
+
properties: {
|
|
553
|
+
since_snapshot: { type: "string", description: "ID do snapshot base. Default: snapshot mais recente." },
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "aios_get_context",
|
|
559
|
+
description: "Atalho para Context Layers (ADR-0007). Devolve só o tier pedido: 'core' (INDEX+CONTRACT+ROUTER+rules+memory), 'working' (state+project-state resumo+rotating memory+top decisions), 'deep' (working+full project-state+full memory+all decisions).",
|
|
560
|
+
inputSchema: {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties: {
|
|
563
|
+
layer: { enum: ["core", "working", "deep"], description: "Tier de contexto." },
|
|
564
|
+
focus: { type: "string", description: "Tema/intent opcional para priorização." },
|
|
565
|
+
},
|
|
566
|
+
required: ["layer"],
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "aios_artifact_list",
|
|
571
|
+
description: "Lista artefatos do AI OS (specs, snapshots, packages, ADRs, RFCs) com metadata. Filtros opcionais por tipo/status.",
|
|
572
|
+
inputSchema: {
|
|
573
|
+
type: "object",
|
|
574
|
+
properties: {
|
|
575
|
+
type: { enum: ["ADR", "RFC", "PRD", "SDD", "RUNBOOK", "TASK", "BRD", "snapshot", "package", "workflow"] },
|
|
576
|
+
status: { type: "string", description: "Filtro de status (ex.: 'accepted' para ADRs)." },
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "aios_resume_workflow",
|
|
582
|
+
description: "Retoma um workflow pausado (status:'paused'). Use o execution_id devolvido por aios_run_workflow + forneça user_input para o pause point.",
|
|
583
|
+
inputSchema: {
|
|
584
|
+
type: "object",
|
|
585
|
+
properties: {
|
|
586
|
+
execution_id: { type: "string" },
|
|
587
|
+
user_input: { description: "Resposta do usuário ao prompt da pausa. Qualquer tipo JSON." },
|
|
588
|
+
},
|
|
589
|
+
required: ["execution_id"],
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
name: "aios_list_paused_executions",
|
|
594
|
+
description: "Lista execuções pausadas em .ai/runtime/paused-executions/ (TTL default 30 min).",
|
|
595
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "aios_cancel_paused_execution",
|
|
599
|
+
description: "Cancela e remove uma execução pausada.",
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: "object",
|
|
602
|
+
properties: { execution_id: { type: "string" } },
|
|
603
|
+
required: ["execution_id"],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "aios_confidence_get",
|
|
608
|
+
description: "Lê Confidence System (ADR-0009). Sem categoria → retorna toda a tabela; com categoria → só ela.",
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: "object",
|
|
611
|
+
properties: { category: { type: "string" } },
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "aios_confidence_set",
|
|
616
|
+
description: "Registra/atualiza confiança em uma categoria. level deve ser low|medium|high. reason recomendado (ajuda a entender em revisão).",
|
|
617
|
+
inputSchema: {
|
|
618
|
+
type: "object",
|
|
619
|
+
properties: {
|
|
620
|
+
category: { type: "string", description: "Ex.: 'architecture', 'modules', 'telemetry', 'decisions', 'domain'." },
|
|
621
|
+
level: { enum: ["low", "medium", "high"] },
|
|
622
|
+
reason: { type: "string" },
|
|
623
|
+
},
|
|
624
|
+
required: ["category", "level"],
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: "aios_confidence_assess",
|
|
629
|
+
description: "Heurística que olha state + project-state + drift e PROPÕE níveis de confidence + sugestão de maturity. NÃO aplica — usuário/LLM decide via aios_confidence_set / aios_state_set.",
|
|
630
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "aios_list_blueprints",
|
|
634
|
+
description: "Lista os structural blueprints disponíveis em .ai/blueprints/ (web, saas, crm, mobile, game, ai-platform, telemetry, realtime). Cada blueprint declara folders, modules, packages e integrations recomendados.",
|
|
635
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: "aios_get_blueprint",
|
|
639
|
+
description: "Retorna o blueprint completo (parseado) para o id informado. Use depois de aios_list_blueprints.",
|
|
640
|
+
inputSchema: {
|
|
641
|
+
type: "object",
|
|
642
|
+
properties: { name: { type: "string" } },
|
|
643
|
+
required: ["name"],
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "aios_compose_structure",
|
|
648
|
+
description: "Composition Engine (ADR-0010). Recebe lista de tipos (ex.: ['saas','telemetry','realtime']), resolve 'extends' transitivo, faz union semântica (dedup) de folders/modules/packages/integrations, merge de constraints (mais restritivo vence), e retorna estrutura final pronta para scaffolding.",
|
|
649
|
+
inputSchema: {
|
|
650
|
+
type: "object",
|
|
651
|
+
properties: {
|
|
652
|
+
types: {
|
|
653
|
+
type: "array",
|
|
654
|
+
items: { type: "string" },
|
|
655
|
+
description: "IDs de blueprints a compor (ex.: ['saas','telemetry']).",
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
required: ["types"],
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
];
|
|
662
|
+
|
|
663
|
+
const START_TIME = Date.now();
|
|
664
|
+
|
|
665
|
+
function requireRuntime(toolName) {
|
|
666
|
+
if (!runtime) {
|
|
667
|
+
const hint = runtimeLoadError
|
|
668
|
+
? `Reason: ${runtimeLoadError}`
|
|
669
|
+
: "Run: cd .ai/server && npm install && npm run build";
|
|
670
|
+
throw new Error(
|
|
671
|
+
`Tool ${toolName} requires Runtime v0.1 (dist/index.mjs). ${hint}`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
return runtime;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const HANDLERS = {
|
|
678
|
+
"aios_get_core_bundle": async () => await getCoreBundle(),
|
|
679
|
+
"aios_route_query": async ({ query }) => await routeQuery(query),
|
|
680
|
+
"aios_classify_task": async ({ query }) => classifyTask(query),
|
|
681
|
+
"aios_read": async ({ path }) => await readDoc(path),
|
|
682
|
+
"aios_list": async ({ path }) => await listDir(path),
|
|
683
|
+
"aios_search_memory": async ({ query, max_results }) => await searchMemory(query, max_results || 20),
|
|
684
|
+
"aios_get_summary": async () => await getSummary(),
|
|
685
|
+
"aios_get_manifest": async () => await getManifest(),
|
|
686
|
+
"aios_lint": async () => await runLint(),
|
|
687
|
+
"aios_detect_stack": async ({ target_path }) => await runDetectStack(target_path),
|
|
688
|
+
"aios_validate_audit": async ({ text }) => validateAudit(text),
|
|
689
|
+
"aios_health": async () => ({
|
|
690
|
+
status: "ok",
|
|
691
|
+
version: (await getManifest().catch(() => ({}))).version || "unknown",
|
|
692
|
+
uptime_seconds: Math.floor((Date.now() - START_TIME) / 1000),
|
|
693
|
+
mode: STEALTHOS_MODE,
|
|
694
|
+
stealthos_home: STEALTHOS_HOME,
|
|
695
|
+
project_root: PROJECT_ROOT,
|
|
696
|
+
project_hash: PROJECT_HASH,
|
|
697
|
+
kb_dir: KB_DIR,
|
|
698
|
+
kb_exists: existsSync(KB_DIR),
|
|
699
|
+
project_state_dir: PROJECT_STATE_DIR,
|
|
700
|
+
project_state_exists: existsSync(PROJECT_STATE_DIR),
|
|
701
|
+
pid: process.pid,
|
|
702
|
+
node: process.version,
|
|
703
|
+
platform: process.platform,
|
|
704
|
+
runtime_v0_1: !!runtime,
|
|
705
|
+
runtime_load_error: runtimeLoadError,
|
|
706
|
+
runtime_dist_path: RUNTIME_DIST_PATH,
|
|
707
|
+
}),
|
|
708
|
+
"aios_analyze_project": async ({ scan_dirs, write } = {}) => {
|
|
709
|
+
const rt = requireRuntime("aios_analyze_project");
|
|
710
|
+
return await rt.analyzer.analyzeProject(PROJECT_ROOT, {
|
|
711
|
+
scanDirs: scan_dirs,
|
|
712
|
+
write: write !== false,
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
"aios_build_context_package": async ({ intent, max_modules, write } = {}) => {
|
|
716
|
+
const rt = requireRuntime("aios_build_context_package");
|
|
717
|
+
return await rt.packager.buildContextPackage(PROJECT_ROOT, {
|
|
718
|
+
intent: intent || "",
|
|
719
|
+
maxModules: max_modules,
|
|
720
|
+
write: write !== false,
|
|
721
|
+
});
|
|
722
|
+
},
|
|
723
|
+
"aios_snapshot": async ({ label } = {}) => {
|
|
724
|
+
const rt = requireRuntime("aios_snapshot");
|
|
725
|
+
if (!label) throw new Error("aios_snapshot requires 'label' argument");
|
|
726
|
+
return await rt.snapshot.createSnapshot(PROJECT_ROOT, { label });
|
|
727
|
+
},
|
|
728
|
+
"aios_snapshot_list": async () => {
|
|
729
|
+
const rt = requireRuntime("aios_snapshot_list");
|
|
730
|
+
return await rt.snapshot.listSnapshots(PROJECT_ROOT);
|
|
731
|
+
},
|
|
732
|
+
"aios_snapshot_diff": async ({ a, b } = {}) => {
|
|
733
|
+
const rt = requireRuntime("aios_snapshot_diff");
|
|
734
|
+
if (!a || !b) throw new Error("aios_snapshot_diff requires 'a' and 'b' snapshot IDs");
|
|
735
|
+
return await rt.snapshot.diffSnapshots(PROJECT_ROOT, a, b);
|
|
736
|
+
},
|
|
737
|
+
"aios_run_workflow": async ({ name, input = {} } = {}) => {
|
|
738
|
+
if (!name) throw new Error("aios_run_workflow requires 'name'");
|
|
739
|
+
return await runWorkflow(name, input);
|
|
740
|
+
},
|
|
741
|
+
"aios_list_workflows": async () => await listWorkflows(),
|
|
742
|
+
"aios_state_get": async ({ path } = {}) => await stateGet(path),
|
|
743
|
+
"aios_state_set": async ({ path, value } = {}) => {
|
|
744
|
+
if (!path) throw new Error("aios_state_set requires 'path'");
|
|
745
|
+
return await stateSet(path, value);
|
|
746
|
+
},
|
|
747
|
+
"aios_state_advance": async ({ phase, mode } = {}) => {
|
|
748
|
+
if (!phase && !mode) throw new Error("aios_state_advance requires 'phase' or 'mode'");
|
|
749
|
+
return await stateAdvance(phase, mode);
|
|
750
|
+
},
|
|
751
|
+
"aios_state_reset": async () => await stateReset(),
|
|
752
|
+
"aios_compile_runtime_context": async ({ max_modules, max_decisions, write } = {}) =>
|
|
753
|
+
await compileRuntimeContext({
|
|
754
|
+
maxModules: max_modules || 10,
|
|
755
|
+
maxDecisions: max_decisions || 5,
|
|
756
|
+
write: write !== false,
|
|
757
|
+
}),
|
|
758
|
+
"aios_detect_drift": async ({ since_snapshot } = {}) => await detectDrift(since_snapshot),
|
|
759
|
+
"aios_get_context": async ({ layer, focus } = {}) => {
|
|
760
|
+
if (!layer) throw new Error("aios_get_context requires 'layer'");
|
|
761
|
+
return await getContextLayer(layer, focus);
|
|
762
|
+
},
|
|
763
|
+
"aios_artifact_list": async ({ type, status } = {}) => await artifactList(type, status),
|
|
764
|
+
"aios_resume_workflow": async ({ execution_id, user_input } = {}) => {
|
|
765
|
+
if (!execution_id) throw new Error("aios_resume_workflow requires 'execution_id'");
|
|
766
|
+
return await resumeWorkflow(execution_id, user_input);
|
|
767
|
+
},
|
|
768
|
+
"aios_list_paused_executions": async () => await listPausedExecutions(),
|
|
769
|
+
"aios_cancel_paused_execution": async ({ execution_id } = {}) => {
|
|
770
|
+
if (!execution_id) throw new Error("aios_cancel_paused_execution requires 'execution_id'");
|
|
771
|
+
return await cancelPausedExecution(execution_id);
|
|
772
|
+
},
|
|
773
|
+
"aios_confidence_get": async ({ category } = {}) => await confidenceGet(category),
|
|
774
|
+
"aios_confidence_set": async ({ category, level, reason } = {}) => {
|
|
775
|
+
if (!category || !level) throw new Error("aios_confidence_set requires 'category' and 'level'");
|
|
776
|
+
return await confidenceSet(category, level, reason);
|
|
777
|
+
},
|
|
778
|
+
"aios_confidence_assess": async () => await confidenceAssess(),
|
|
779
|
+
"aios_list_blueprints": async () => await listBlueprints(),
|
|
780
|
+
"aios_get_blueprint": async ({ name } = {}) => {
|
|
781
|
+
if (!name) throw new Error("aios_get_blueprint requires 'name'");
|
|
782
|
+
return await getBlueprint(name);
|
|
783
|
+
},
|
|
784
|
+
"aios_compose_structure": async ({ types } = {}) => {
|
|
785
|
+
if (!Array.isArray(types) || types.length === 0) {
|
|
786
|
+
throw new Error("aios_compose_structure requires 'types' as non-empty array");
|
|
787
|
+
}
|
|
788
|
+
return await composeStructure(types);
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// ---------- Workflow Engine (Fase 1 — ADR-0005, estendido em ADR-0006) ----------
|
|
793
|
+
const WORKFLOWS_DIR = join(KB_DIR, "workflows");
|
|
794
|
+
const RUNTIME_DIR = join(PROJECT_STATE_DIR, "runtime");
|
|
795
|
+
const STATE_PATH = join(RUNTIME_DIR, "state.json");
|
|
796
|
+
|
|
797
|
+
// Mapeamento guarantee → tools provedoras (ADR-0006 + ADR-0007)
|
|
798
|
+
const GUARANTEE_PROVIDERS = {
|
|
799
|
+
snapshot_created: ["aios_snapshot"],
|
|
800
|
+
state_updated: ["aios_state_set", "aios_state_advance", "aios_state_reset"],
|
|
801
|
+
context_compiled: ["aios_build_context_package"],
|
|
802
|
+
lint_clean: ["aios_lint"],
|
|
803
|
+
stack_detected: ["aios_detect_stack"],
|
|
804
|
+
project_analyzed: ["aios_analyze_project"],
|
|
805
|
+
core_loaded: ["aios_get_core_bundle"],
|
|
806
|
+
memory_searched: ["aios_search_memory"],
|
|
807
|
+
query_routed: ["aios_route_query"],
|
|
808
|
+
task_classified: ["aios_classify_task"],
|
|
809
|
+
runtime_context_compiled: ["aios_compile_runtime_context"],
|
|
810
|
+
drift_checked: ["aios_detect_drift"],
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
function resolveTemplateValue(str, ctx) {
|
|
814
|
+
if (typeof str !== "string" || !str.includes("${")) return str;
|
|
815
|
+
return str.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
816
|
+
// Support: input.X, timestamp, state.X, input.X || 'fallback'
|
|
817
|
+
const parts = expr.split("||").map((p) => p.trim());
|
|
818
|
+
for (const part of parts) {
|
|
819
|
+
const lit = part.match(/^['"](.*)['"]$/);
|
|
820
|
+
if (lit) return lit[1];
|
|
821
|
+
const segs = part.split(".");
|
|
822
|
+
let val = ctx;
|
|
823
|
+
let ok = true;
|
|
824
|
+
for (const s of segs) {
|
|
825
|
+
if (val == null || !(s in val)) { ok = false; break; }
|
|
826
|
+
val = val[s];
|
|
827
|
+
}
|
|
828
|
+
if (ok && val !== "" && val != null) return String(val);
|
|
829
|
+
}
|
|
830
|
+
return "";
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function resolveArgs(args, ctx) {
|
|
835
|
+
const out = {};
|
|
836
|
+
for (const [k, v] of Object.entries(args || {})) {
|
|
837
|
+
if (typeof v === "string") out[k] = resolveTemplateValue(v, ctx);
|
|
838
|
+
else if (Array.isArray(v)) out[k] = v.map((x) => (typeof x === "string" ? resolveTemplateValue(x, ctx) : x));
|
|
839
|
+
else if (v && typeof v === "object") out[k] = resolveArgs(v, ctx);
|
|
840
|
+
else out[k] = v;
|
|
841
|
+
}
|
|
842
|
+
return out;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function loadWorkflow(name) {
|
|
846
|
+
// Restrict name to safe chars
|
|
847
|
+
if (!/^[a-z][a-z0-9_-]{0,32}$/.test(name)) {
|
|
848
|
+
throw new Error(`Invalid workflow name: ${name}`);
|
|
849
|
+
}
|
|
850
|
+
const wfPath = join(WORKFLOWS_DIR, `${name}.json`);
|
|
851
|
+
if (!existsSync(wfPath)) {
|
|
852
|
+
throw new Error(`Workflow not found: ${name} (expected at ${wfPath})`);
|
|
853
|
+
}
|
|
854
|
+
const raw = await readFile(wfPath, "utf8");
|
|
855
|
+
const wf = JSON.parse(raw);
|
|
856
|
+
if (!wf.id || !Array.isArray(wf.steps)) {
|
|
857
|
+
throw new Error(`Invalid workflow ${name}: missing id or steps[]`);
|
|
858
|
+
}
|
|
859
|
+
return wf;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ADR-0008: paused executions
|
|
863
|
+
const PAUSED_DIR = join(RUNTIME_DIR, "paused-executions");
|
|
864
|
+
const PAUSE_TTL_MS = 30 * 60 * 1000; // 30 min
|
|
865
|
+
|
|
866
|
+
function expandSteps(steps, ctx) {
|
|
867
|
+
// Expand branches inline; recursive
|
|
868
|
+
const out = [];
|
|
869
|
+
for (const step of steps || []) {
|
|
870
|
+
if (Array.isArray(step.branches)) {
|
|
871
|
+
let chosen = null;
|
|
872
|
+
for (const br of step.branches) {
|
|
873
|
+
if (br.default) { if (!chosen) chosen = br; continue; }
|
|
874
|
+
if (evalBranchCondition(br.when, ctx)) { chosen = br; break; }
|
|
875
|
+
}
|
|
876
|
+
if (chosen && Array.isArray(chosen.steps)) {
|
|
877
|
+
out.push(...expandSteps(chosen.steps, ctx));
|
|
878
|
+
}
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
out.push(step);
|
|
882
|
+
}
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function evalBranchCondition(expr, ctx) {
|
|
887
|
+
if (!expr || typeof expr !== "string") return false;
|
|
888
|
+
// Supported: <path> == <literal> | <path> != <literal> | <literal> in <path>
|
|
889
|
+
const opMatch = expr.match(/^(.+?)\s+(==|!=|in)\s+(.+)$/);
|
|
890
|
+
if (!opMatch) return false;
|
|
891
|
+
const [, leftRaw, op, rightRaw] = opMatch;
|
|
892
|
+
const parseLiteralOrPath = (raw) => {
|
|
893
|
+
const s = raw.trim();
|
|
894
|
+
const litStr = s.match(/^['"](.*)['"]$/);
|
|
895
|
+
if (litStr) return litStr[1];
|
|
896
|
+
if (s === "true") return true;
|
|
897
|
+
if (s === "false") return false;
|
|
898
|
+
if (s === "null") return null;
|
|
899
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s);
|
|
900
|
+
// dotted path lookup
|
|
901
|
+
const segs = s.split(".");
|
|
902
|
+
let val = ctx;
|
|
903
|
+
for (const seg of segs) {
|
|
904
|
+
if (val == null || typeof val !== "object") return undefined;
|
|
905
|
+
val = val[seg];
|
|
906
|
+
}
|
|
907
|
+
return val;
|
|
908
|
+
};
|
|
909
|
+
const left = parseLiteralOrPath(leftRaw);
|
|
910
|
+
const right = parseLiteralOrPath(rightRaw);
|
|
911
|
+
if (op === "==") return left === right;
|
|
912
|
+
if (op === "!=") return left !== right;
|
|
913
|
+
if (op === "in") {
|
|
914
|
+
if (Array.isArray(right)) return right.includes(left);
|
|
915
|
+
if (typeof right === "string") return right.includes(String(left));
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async function ensurePausedDir() {
|
|
922
|
+
if (!existsSync(PAUSED_DIR)) await mkdir(PAUSED_DIR, { recursive: true });
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function savePausedExecution(execution) {
|
|
926
|
+
await ensurePausedDir();
|
|
927
|
+
const file = join(PAUSED_DIR, `${execution.execution_id}.json`);
|
|
928
|
+
await writeFile(file, JSON.stringify(execution, null, 2), "utf8");
|
|
929
|
+
return file;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function runWorkflow(name, input) {
|
|
933
|
+
const wf = await loadWorkflow(name);
|
|
934
|
+
const startIso = new Date().toISOString();
|
|
935
|
+
const report = {
|
|
936
|
+
workflow: wf.id,
|
|
937
|
+
version: wf.version || "unknown",
|
|
938
|
+
started_at: startIso,
|
|
939
|
+
input,
|
|
940
|
+
safety: wf.safety || null,
|
|
941
|
+
declared_guarantees: Array.isArray(wf.guarantees) ? wf.guarantees : [],
|
|
942
|
+
steps: [],
|
|
943
|
+
status: "running",
|
|
944
|
+
};
|
|
945
|
+
let stateSnapshot = null;
|
|
946
|
+
try { stateSnapshot = await loadState(); } catch (_) { stateSnapshot = null; }
|
|
947
|
+
const ctx = {
|
|
948
|
+
input: input || {},
|
|
949
|
+
timestamp: startIso.replace(/[:.]/g, "-"),
|
|
950
|
+
state: stateSnapshot,
|
|
951
|
+
resumed_input: null,
|
|
952
|
+
};
|
|
953
|
+
// Expand branches up front
|
|
954
|
+
const expandedSteps = expandSteps(wf.steps, ctx);
|
|
955
|
+
return await executeStepsFrom(wf, expandedSteps, 0, ctx, report);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function executeStepsFrom(wf, steps, startIdx, ctx, report) {
|
|
959
|
+
const toolsExecutedOk = new Set(report._toolsOk || []);
|
|
960
|
+
for (let i = startIdx; i < steps.length; i++) {
|
|
961
|
+
const step = steps[i];
|
|
962
|
+
// Pause-for-input (Fase 4 / ADR-0008)
|
|
963
|
+
if (step.pause_for && (step.pause_for.when || "before") === "before") {
|
|
964
|
+
return await pauseExecution(wf, steps, i, ctx, report, toolsExecutedOk, "before");
|
|
965
|
+
}
|
|
966
|
+
const stepStart = Date.now();
|
|
967
|
+
const stepReport = {
|
|
968
|
+
id: step.id,
|
|
969
|
+
tool: step.tool,
|
|
970
|
+
status: "pending",
|
|
971
|
+
duration_ms: 0,
|
|
972
|
+
};
|
|
973
|
+
try {
|
|
974
|
+
const handler = HANDLERS[step.tool];
|
|
975
|
+
if (!handler) throw new Error(`Unknown tool in workflow: ${step.tool}`);
|
|
976
|
+
const resolved = resolveArgs(step.args || {}, ctx);
|
|
977
|
+
const result = await handler(resolved);
|
|
978
|
+
stepReport.status = "ok";
|
|
979
|
+
stepReport.duration_ms = Date.now() - stepStart;
|
|
980
|
+
const preview = typeof result === "string" ? result : JSON.stringify(result);
|
|
981
|
+
stepReport.result_preview = preview.slice(0, 400);
|
|
982
|
+
stepReport.result_length = preview.length;
|
|
983
|
+
toolsExecutedOk.add(step.tool);
|
|
984
|
+
} catch (e) {
|
|
985
|
+
stepReport.status = "error";
|
|
986
|
+
stepReport.duration_ms = Date.now() - stepStart;
|
|
987
|
+
stepReport.error = String(e && e.message ? e.message : e);
|
|
988
|
+
report.steps.push(stepReport);
|
|
989
|
+
if (step.on_error !== "continue") {
|
|
990
|
+
report.status = "aborted";
|
|
991
|
+
report.finished_at = new Date().toISOString();
|
|
992
|
+
await recordWorkflowHistory(report);
|
|
993
|
+
return report;
|
|
994
|
+
}
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
report.steps.push(stepReport);
|
|
998
|
+
if (step.pause_for && step.pause_for.when === "after") {
|
|
999
|
+
return await pauseExecution(wf, steps, i + 1, ctx, report, toolsExecutedOk, "after");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
// Validate guarantees (ADR-0006)
|
|
1003
|
+
const metGuarantees = [];
|
|
1004
|
+
const unmetGuarantees = [];
|
|
1005
|
+
for (const g of report.declared_guarantees) {
|
|
1006
|
+
const providers = GUARANTEE_PROVIDERS[g] || [];
|
|
1007
|
+
const satisfied = providers.some((t) => toolsExecutedOk.has(t));
|
|
1008
|
+
(satisfied ? metGuarantees : unmetGuarantees).push(g);
|
|
1009
|
+
}
|
|
1010
|
+
report.guarantees_met = metGuarantees;
|
|
1011
|
+
report.unmet_guarantees = unmetGuarantees;
|
|
1012
|
+
report.status = unmetGuarantees.length > 0 ? "ok_with_warnings" : "ok";
|
|
1013
|
+
report.finished_at = new Date().toISOString();
|
|
1014
|
+
delete report._toolsOk;
|
|
1015
|
+
await recordWorkflowHistory(report);
|
|
1016
|
+
return report;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function pauseExecution(wf, steps, nextIndex, ctx, report, toolsExecutedOk, when) {
|
|
1020
|
+
const execution_id = `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1021
|
+
const pauseStep = steps[when === "before" ? nextIndex : nextIndex - 1];
|
|
1022
|
+
const persisted = {
|
|
1023
|
+
execution_id,
|
|
1024
|
+
workflow: wf.id,
|
|
1025
|
+
version: wf.version,
|
|
1026
|
+
paused_at: new Date().toISOString(),
|
|
1027
|
+
expires_at: new Date(Date.now() + PAUSE_TTL_MS).toISOString(),
|
|
1028
|
+
next_step_index: nextIndex,
|
|
1029
|
+
steps,
|
|
1030
|
+
ctx_minus_state: { input: ctx.input, timestamp: ctx.timestamp, resumed_input: ctx.resumed_input },
|
|
1031
|
+
report: { ...report, _toolsOk: [...toolsExecutedOk] },
|
|
1032
|
+
pause_info: {
|
|
1033
|
+
step_id: pauseStep.id,
|
|
1034
|
+
when,
|
|
1035
|
+
reason: pauseStep.pause_for.reason,
|
|
1036
|
+
prompt: pauseStep.pause_for.prompt,
|
|
1037
|
+
expected_input: pauseStep.pause_for.expected_input || null,
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
await savePausedExecution(persisted);
|
|
1041
|
+
return {
|
|
1042
|
+
status: "paused",
|
|
1043
|
+
execution_id,
|
|
1044
|
+
workflow: wf.id,
|
|
1045
|
+
paused_at: persisted.paused_at,
|
|
1046
|
+
expires_at: persisted.expires_at,
|
|
1047
|
+
pause_info: persisted.pause_info,
|
|
1048
|
+
steps_completed: report.steps.length,
|
|
1049
|
+
resume_instruction: `Call aios_resume_workflow({execution_id:"${execution_id}", user_input: <answer>}).`,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function resumeWorkflow(execution_id, user_input) {
|
|
1054
|
+
await ensurePausedDir();
|
|
1055
|
+
const file = join(PAUSED_DIR, `${execution_id}.json`);
|
|
1056
|
+
if (!existsSync(file)) throw new Error(`Paused execution not found: ${execution_id}`);
|
|
1057
|
+
const raw = await readFile(file, "utf8");
|
|
1058
|
+
const persisted = JSON.parse(raw);
|
|
1059
|
+
if (new Date(persisted.expires_at).getTime() < Date.now()) {
|
|
1060
|
+
return { status: "expired", execution_id, expired_at: persisted.expires_at };
|
|
1061
|
+
}
|
|
1062
|
+
// Reload state (may have changed)
|
|
1063
|
+
let stateSnapshot = null;
|
|
1064
|
+
try { stateSnapshot = await loadState(); } catch (_) { stateSnapshot = null; }
|
|
1065
|
+
const ctx = {
|
|
1066
|
+
...persisted.ctx_minus_state,
|
|
1067
|
+
state: stateSnapshot,
|
|
1068
|
+
resumed_input: user_input ?? null,
|
|
1069
|
+
};
|
|
1070
|
+
const report = persisted.report;
|
|
1071
|
+
// Remove paused file
|
|
1072
|
+
try { await readFile(file, "utf8"); } catch (_) { /* ignore */ }
|
|
1073
|
+
try { (await import("node:fs/promises")).unlink(file); } catch (_) { /* ignore */ }
|
|
1074
|
+
return await executeStepsFrom(
|
|
1075
|
+
{ id: persisted.workflow, version: persisted.version, guarantees: report.declared_guarantees },
|
|
1076
|
+
persisted.steps,
|
|
1077
|
+
persisted.next_step_index,
|
|
1078
|
+
ctx,
|
|
1079
|
+
report,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function listPausedExecutions() {
|
|
1084
|
+
await ensurePausedDir();
|
|
1085
|
+
const entries = await readdir(PAUSED_DIR).catch(() => []);
|
|
1086
|
+
const out = [];
|
|
1087
|
+
for (const f of entries) {
|
|
1088
|
+
if (!f.endsWith(".json")) continue;
|
|
1089
|
+
try {
|
|
1090
|
+
const raw = await readFile(join(PAUSED_DIR, f), "utf8");
|
|
1091
|
+
const e = JSON.parse(raw);
|
|
1092
|
+
out.push({
|
|
1093
|
+
execution_id: e.execution_id,
|
|
1094
|
+
workflow: e.workflow,
|
|
1095
|
+
paused_at: e.paused_at,
|
|
1096
|
+
expires_at: e.expires_at,
|
|
1097
|
+
expired: new Date(e.expires_at).getTime() < Date.now(),
|
|
1098
|
+
pause_info: e.pause_info,
|
|
1099
|
+
});
|
|
1100
|
+
} catch (_) { /* skip */ }
|
|
1101
|
+
}
|
|
1102
|
+
return { count: out.length, paused: out };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function cancelPausedExecution(execution_id) {
|
|
1106
|
+
const file = join(PAUSED_DIR, `${execution_id}.json`);
|
|
1107
|
+
if (!existsSync(file)) return { cancelled: false, reason: "not_found" };
|
|
1108
|
+
try {
|
|
1109
|
+
const fsp = await import("node:fs/promises");
|
|
1110
|
+
await fsp.unlink(file);
|
|
1111
|
+
return { cancelled: true, execution_id };
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
return { cancelled: false, error: String(e.message || e) };
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// ---------- Fase 5 (ADR-0009): Confidence System ----------
|
|
1118
|
+
async function ensureConfidenceBlock(state) {
|
|
1119
|
+
if (!state.confidence) state.confidence = { default_level: "medium", categories: {} };
|
|
1120
|
+
if (!state.confidence.categories) state.confidence.categories = {};
|
|
1121
|
+
if (!state.confidence.default_level) state.confidence.default_level = "medium";
|
|
1122
|
+
return state;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function confidenceGet(category) {
|
|
1126
|
+
const state = await loadState();
|
|
1127
|
+
await ensureConfidenceBlock(state);
|
|
1128
|
+
if (!category) {
|
|
1129
|
+
return {
|
|
1130
|
+
default_level: state.confidence.default_level,
|
|
1131
|
+
categories: state.confidence.categories,
|
|
1132
|
+
count: Object.keys(state.confidence.categories).length,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const entry = state.confidence.categories[category];
|
|
1136
|
+
if (!entry) {
|
|
1137
|
+
return {
|
|
1138
|
+
category,
|
|
1139
|
+
level: state.confidence.default_level,
|
|
1140
|
+
reason: "default (no explicit entry)",
|
|
1141
|
+
explicit: false,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
return { category, ...entry, explicit: true };
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async function confidenceSet(category, level, reason) {
|
|
1148
|
+
if (!["low", "medium", "high"].includes(level)) {
|
|
1149
|
+
throw new Error(`Invalid level: ${level} (must be low|medium|high)`);
|
|
1150
|
+
}
|
|
1151
|
+
const state = await loadState();
|
|
1152
|
+
await ensureConfidenceBlock(state);
|
|
1153
|
+
const prev = state.confidence.categories[category];
|
|
1154
|
+
state.confidence.categories[category] = {
|
|
1155
|
+
level,
|
|
1156
|
+
reason: reason || null,
|
|
1157
|
+
updated_at: new Date().toISOString(),
|
|
1158
|
+
};
|
|
1159
|
+
await saveState(state);
|
|
1160
|
+
return {
|
|
1161
|
+
category,
|
|
1162
|
+
previous: prev || null,
|
|
1163
|
+
current: state.confidence.categories[category],
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// ---------- Fase 6 (ADR-0010): Structural Blueprints + Composition Engine ----------
|
|
1168
|
+
const BLUEPRINTS_DIR = join(KB_DIR, "blueprints");
|
|
1169
|
+
|
|
1170
|
+
async function listBlueprints() {
|
|
1171
|
+
if (!existsSync(BLUEPRINTS_DIR)) return { count: 0, blueprints: [] };
|
|
1172
|
+
const entries = await readdir(BLUEPRINTS_DIR, { withFileTypes: true });
|
|
1173
|
+
const out = [];
|
|
1174
|
+
for (const e of entries) {
|
|
1175
|
+
if (!e.isFile() || !e.name.endsWith(".json") || e.name.startsWith("_")) continue;
|
|
1176
|
+
try {
|
|
1177
|
+
const raw = await readFile(join(BLUEPRINTS_DIR, e.name), "utf8");
|
|
1178
|
+
const bp = JSON.parse(raw);
|
|
1179
|
+
out.push({
|
|
1180
|
+
id: bp.id,
|
|
1181
|
+
description: bp.description,
|
|
1182
|
+
extends: bp.extends || [],
|
|
1183
|
+
modules_count: Array.isArray(bp.modules) ? bp.modules.length : 0,
|
|
1184
|
+
folders_count: Array.isArray(bp.folders) ? bp.folders.length : 0,
|
|
1185
|
+
file: e.name,
|
|
1186
|
+
});
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
out.push({ id: e.name.replace(/\.json$/, ""), error: String(err.message || err), file: e.name });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return { count: out.length, blueprints: out };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async function loadBlueprint(name) {
|
|
1195
|
+
if (!/^[a-z][a-z0-9_-]{0,32}$/.test(name)) {
|
|
1196
|
+
throw new Error(`Invalid blueprint name: ${name}`);
|
|
1197
|
+
}
|
|
1198
|
+
const file = join(BLUEPRINTS_DIR, `${name}.json`);
|
|
1199
|
+
if (!existsSync(file)) throw new Error(`Blueprint not found: ${name}`);
|
|
1200
|
+
const raw = await readFile(file, "utf8");
|
|
1201
|
+
const bp = JSON.parse(raw);
|
|
1202
|
+
if (!bp.id || !Array.isArray(bp.folders) || !Array.isArray(bp.modules)) {
|
|
1203
|
+
throw new Error(`Invalid blueprint ${name}: missing id/folders/modules`);
|
|
1204
|
+
}
|
|
1205
|
+
return bp;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
async function getBlueprint(name) {
|
|
1209
|
+
const bp = await loadBlueprint(name);
|
|
1210
|
+
return { blueprint: bp };
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async function resolveExtendsTransitive(types, visited = new Set()) {
|
|
1214
|
+
const ordered = [];
|
|
1215
|
+
for (const t of types) {
|
|
1216
|
+
if (visited.has(t)) continue;
|
|
1217
|
+
visited.add(t);
|
|
1218
|
+
let bp;
|
|
1219
|
+
try { bp = await loadBlueprint(t); } catch (e) {
|
|
1220
|
+
ordered.push({ id: t, error: String(e.message || e) });
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (Array.isArray(bp.extends) && bp.extends.length > 0) {
|
|
1224
|
+
const parents = await resolveExtendsTransitive(bp.extends, visited);
|
|
1225
|
+
ordered.push(...parents);
|
|
1226
|
+
}
|
|
1227
|
+
ordered.push(bp);
|
|
1228
|
+
}
|
|
1229
|
+
return ordered;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function mergeConstraints(base, override) {
|
|
1233
|
+
// Mais restritivo vence (menor número geralmente; para booleans true vence)
|
|
1234
|
+
const out = { ...base };
|
|
1235
|
+
for (const [k, v] of Object.entries(override || {})) {
|
|
1236
|
+
if (out[k] === undefined) { out[k] = v; continue; }
|
|
1237
|
+
if (typeof v === "number" && typeof out[k] === "number") {
|
|
1238
|
+
// Para limites: menor vence; para "max_*" é menor; aqui assumimos limits
|
|
1239
|
+
out[k] = Math.min(out[k], v);
|
|
1240
|
+
} else if (typeof v === "boolean") {
|
|
1241
|
+
out[k] = out[k] || v;
|
|
1242
|
+
} else {
|
|
1243
|
+
out[k] = v;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return out;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async function composeStructure(types) {
|
|
1250
|
+
const resolved = await resolveExtendsTransitive(types);
|
|
1251
|
+
const valid = resolved.filter((bp) => !bp.error);
|
|
1252
|
+
const errors = resolved.filter((bp) => bp.error);
|
|
1253
|
+
|
|
1254
|
+
const foldersSet = new Set();
|
|
1255
|
+
const modulesSet = new Set();
|
|
1256
|
+
const packagesSet = new Set();
|
|
1257
|
+
const integrationsSet = new Set();
|
|
1258
|
+
let constraints = {
|
|
1259
|
+
max_module_dependencies: 5,
|
|
1260
|
+
max_file_lines: 500,
|
|
1261
|
+
max_folder_depth: 4,
|
|
1262
|
+
max_modules_before_split: 20,
|
|
1263
|
+
require_module_readme: true,
|
|
1264
|
+
require_module_index: true,
|
|
1265
|
+
forbid_global_singletons: true,
|
|
1266
|
+
forbid_circular_module_deps: true,
|
|
1267
|
+
};
|
|
1268
|
+
const notes = [];
|
|
1269
|
+
|
|
1270
|
+
for (const bp of valid) {
|
|
1271
|
+
for (const f of bp.folders || []) foldersSet.add(f);
|
|
1272
|
+
for (const m of bp.modules || []) modulesSet.add(m);
|
|
1273
|
+
for (const p of bp.packages_recommended || []) packagesSet.add(p);
|
|
1274
|
+
for (const i of bp.integrations_recommended || []) integrationsSet.add(i);
|
|
1275
|
+
if (bp.constraints_overrides) {
|
|
1276
|
+
constraints = mergeConstraints(constraints, bp.constraints_overrides);
|
|
1277
|
+
}
|
|
1278
|
+
if (bp.notes) notes.push({ from: bp.id, note: bp.notes });
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return {
|
|
1282
|
+
requested_types: types,
|
|
1283
|
+
types_resolved: valid.map((bp) => bp.id),
|
|
1284
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
1285
|
+
folders: [...foldersSet].sort(),
|
|
1286
|
+
modules: [...modulesSet].sort(),
|
|
1287
|
+
packages_recommended: [...packagesSet].sort(),
|
|
1288
|
+
integrations_recommended: [...integrationsSet].sort(),
|
|
1289
|
+
constraints,
|
|
1290
|
+
notes,
|
|
1291
|
+
next_step_hint: "Estrutura sugerida — adapte ao seu contexto. Princípios: ver .ai/rules/structure-canon.md. Limites: .ai/rules/structural-constraints.md.",
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async function confidenceAssess() {
|
|
1296
|
+
const state = await loadState();
|
|
1297
|
+
await ensureConfidenceBlock(state);
|
|
1298
|
+
const projectState = safeReadJson(join(PROJECT_STATE_DIR, "context", "project-state.json"));
|
|
1299
|
+
const adrs = await listAdrs(50);
|
|
1300
|
+
const totalModules = projectState && Array.isArray(projectState.modules)
|
|
1301
|
+
? projectState.modules.length : 0;
|
|
1302
|
+
const smellsCount = projectState && Array.isArray(projectState.smells)
|
|
1303
|
+
? projectState.smells.length : 0;
|
|
1304
|
+
const adrCount = adrs.length;
|
|
1305
|
+
const syncCount = (state.workflow_history || []).filter((h) => h.workflow === "sync").length;
|
|
1306
|
+
|
|
1307
|
+
// Suggested maturity
|
|
1308
|
+
let suggestedMaturity = "empty";
|
|
1309
|
+
if (state.identity?.project_name && adrCount <= 2) suggestedMaturity = "minimal";
|
|
1310
|
+
if (adrCount >= 3 && adrCount <= 9 && totalModules > 0) suggestedMaturity = "partial";
|
|
1311
|
+
if (adrCount >= 10 && totalModules > 10 && syncCount >= 3) suggestedMaturity = "mature";
|
|
1312
|
+
|
|
1313
|
+
// Suggested confidence levels (heuristic)
|
|
1314
|
+
const suggestions = {};
|
|
1315
|
+
suggestions.decisions = {
|
|
1316
|
+
level: adrCount >= 10 ? "high" : adrCount >= 3 ? "medium" : "low",
|
|
1317
|
+
reason: `${adrCount} ADRs registrados`,
|
|
1318
|
+
};
|
|
1319
|
+
suggestions.architecture = {
|
|
1320
|
+
level: existsSync(join(PROJECT_STATE_DIR, "architecture", "containers.md")) ? "high" : "medium",
|
|
1321
|
+
reason: "presença de architecture/containers.md C4 L2",
|
|
1322
|
+
};
|
|
1323
|
+
suggestions.modules = {
|
|
1324
|
+
level: totalModules > 10 && smellsCount === 0 ? "high"
|
|
1325
|
+
: totalModules > 0 ? "medium" : "low",
|
|
1326
|
+
reason: `${totalModules} módulos, ${smellsCount} smells`,
|
|
1327
|
+
};
|
|
1328
|
+
suggestions.domain = {
|
|
1329
|
+
level: existsSync(join(PROJECT_STATE_DIR, "product", "vision.md")) ? "medium" : "low",
|
|
1330
|
+
reason: "presença de product/vision.md",
|
|
1331
|
+
};
|
|
1332
|
+
suggestions.telemetry = {
|
|
1333
|
+
level: "low",
|
|
1334
|
+
reason: "sem instrumentação detectada (heurística default)",
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
return {
|
|
1338
|
+
current_maturity: state.identity?.maturity || "unknown",
|
|
1339
|
+
suggested_maturity: suggestedMaturity,
|
|
1340
|
+
suggested_confidence: suggestions,
|
|
1341
|
+
apply_hint: "Use aios_confidence_set({category, level, reason}) e/ou aios_state_set({path:'identity.maturity', value}) para aplicar.",
|
|
1342
|
+
inputs: {
|
|
1343
|
+
adrs_count: adrCount,
|
|
1344
|
+
modules_count: totalModules,
|
|
1345
|
+
smells_count: smellsCount,
|
|
1346
|
+
sync_runs: syncCount,
|
|
1347
|
+
},
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// ---------- Runtime State helpers (ADR-0006) ----------
|
|
1352
|
+
const DEFAULT_STATE = {
|
|
1353
|
+
$schema: "./state.schema.json",
|
|
1354
|
+
version: "1.0.0",
|
|
1355
|
+
updated_at: new Date(0).toISOString(),
|
|
1356
|
+
identity: { project_name: null, maturity: "empty", stack: [] },
|
|
1357
|
+
lifecycle: { phase: "discovery", mode: "analysis" },
|
|
1358
|
+
persistent: { architecture_decisions_refs: [], rules_refs: [] },
|
|
1359
|
+
rotating: { ttl_days: 30, active_focus: null, recent_errors: [], open_questions: [] },
|
|
1360
|
+
workflow_history: [],
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
const MAX_WORKFLOW_HISTORY = 20;
|
|
1364
|
+
|
|
1365
|
+
async function ensureRuntimeDir() {
|
|
1366
|
+
if (!existsSync(RUNTIME_DIR)) await mkdir(RUNTIME_DIR, { recursive: true });
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function loadState() {
|
|
1370
|
+
await ensureRuntimeDir();
|
|
1371
|
+
if (!existsSync(STATE_PATH)) {
|
|
1372
|
+
await writeFile(STATE_PATH, JSON.stringify(DEFAULT_STATE, null, 2), "utf8");
|
|
1373
|
+
return JSON.parse(JSON.stringify(DEFAULT_STATE));
|
|
1374
|
+
}
|
|
1375
|
+
const raw = await readFile(STATE_PATH, "utf8");
|
|
1376
|
+
return JSON.parse(raw);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
async function saveState(state) {
|
|
1380
|
+
await ensureRuntimeDir();
|
|
1381
|
+
state.updated_at = new Date().toISOString();
|
|
1382
|
+
await writeFile(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
|
|
1383
|
+
return state;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function getByPath(obj, path) {
|
|
1387
|
+
if (!path) return obj;
|
|
1388
|
+
const segs = path.split(".");
|
|
1389
|
+
let val = obj;
|
|
1390
|
+
for (const s of segs) {
|
|
1391
|
+
if (val == null || typeof val !== "object") return undefined;
|
|
1392
|
+
val = val[s];
|
|
1393
|
+
}
|
|
1394
|
+
return val;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function setByPath(obj, path, value) {
|
|
1398
|
+
const segs = path.split(".");
|
|
1399
|
+
let parent = obj;
|
|
1400
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
1401
|
+
if (parent[segs[i]] == null || typeof parent[segs[i]] !== "object") {
|
|
1402
|
+
parent[segs[i]] = {};
|
|
1403
|
+
}
|
|
1404
|
+
parent = parent[segs[i]];
|
|
1405
|
+
}
|
|
1406
|
+
parent[segs[segs.length - 1]] = value;
|
|
1407
|
+
return obj;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async function stateGet(path) {
|
|
1411
|
+
const state = await loadState();
|
|
1412
|
+
if (!path) return state;
|
|
1413
|
+
const val = getByPath(state, path);
|
|
1414
|
+
if (val === undefined) {
|
|
1415
|
+
throw new Error(`State path not found: ${path}`);
|
|
1416
|
+
}
|
|
1417
|
+
return { path, value: val };
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
async function stateSet(path, value) {
|
|
1421
|
+
const state = await loadState();
|
|
1422
|
+
const prev = getByPath(state, path);
|
|
1423
|
+
setByPath(state, path, value);
|
|
1424
|
+
await saveState(state);
|
|
1425
|
+
return { path, previous: prev === undefined ? null : prev, current: value, updated_at: state.updated_at };
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async function stateAdvance(phase, mode) {
|
|
1429
|
+
const state = await loadState();
|
|
1430
|
+
const diff = {};
|
|
1431
|
+
if (phase) {
|
|
1432
|
+
diff.phase = { from: state.lifecycle?.phase, to: phase };
|
|
1433
|
+
if (!state.lifecycle) state.lifecycle = {};
|
|
1434
|
+
state.lifecycle.phase = phase;
|
|
1435
|
+
}
|
|
1436
|
+
if (mode) {
|
|
1437
|
+
diff.mode = { from: state.lifecycle?.mode, to: mode };
|
|
1438
|
+
if (!state.lifecycle) state.lifecycle = {};
|
|
1439
|
+
state.lifecycle.mode = mode;
|
|
1440
|
+
}
|
|
1441
|
+
await saveState(state);
|
|
1442
|
+
return { diff, lifecycle: state.lifecycle, updated_at: state.updated_at };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
async function stateReset() {
|
|
1446
|
+
const fresh = JSON.parse(JSON.stringify(DEFAULT_STATE));
|
|
1447
|
+
fresh.updated_at = new Date().toISOString();
|
|
1448
|
+
await ensureRuntimeDir();
|
|
1449
|
+
await writeFile(STATE_PATH, JSON.stringify(fresh, null, 2), "utf8");
|
|
1450
|
+
return { reset: true, state: fresh };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
async function recordWorkflowHistory(report) {
|
|
1454
|
+
try {
|
|
1455
|
+
const state = await loadState();
|
|
1456
|
+
if (!Array.isArray(state.workflow_history)) state.workflow_history = [];
|
|
1457
|
+
state.workflow_history.unshift({
|
|
1458
|
+
workflow: report.workflow,
|
|
1459
|
+
started_at: report.started_at,
|
|
1460
|
+
finished_at: report.finished_at,
|
|
1461
|
+
status: report.status,
|
|
1462
|
+
guarantees_met: report.guarantees_met || [],
|
|
1463
|
+
unmet_guarantees: report.unmet_guarantees || [],
|
|
1464
|
+
});
|
|
1465
|
+
state.workflow_history = state.workflow_history.slice(0, MAX_WORKFLOW_HISTORY);
|
|
1466
|
+
await saveState(state);
|
|
1467
|
+
} catch (_) {
|
|
1468
|
+
// history is best-effort; don't break workflow on history write failure
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ---------- Fase 3 (ADR-0007): State Compiler + Drift + Context Layers + Artifact Index ----------
|
|
1473
|
+
const RUNTIME_CONTEXT_PATH = join(RUNTIME_DIR, "runtime-context.json");
|
|
1474
|
+
const ARTIFACTS_INDEX_PATH = join(PROJECT_STATE_DIR, "artifacts", "index.json");
|
|
1475
|
+
|
|
1476
|
+
function estimateTokens(str) {
|
|
1477
|
+
// ~4 chars per token rough approximation
|
|
1478
|
+
return Math.ceil((str || "").length / 4);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function safeReadJson(absPath) {
|
|
1482
|
+
try {
|
|
1483
|
+
if (!existsSync(absPath)) return null;
|
|
1484
|
+
return JSON.parse(readFileSync(absPath, "utf8"));
|
|
1485
|
+
} catch (_) {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function safeReadText(absPath, maxChars = 4000) {
|
|
1491
|
+
try {
|
|
1492
|
+
if (!existsSync(absPath)) return null;
|
|
1493
|
+
const txt = readFileSync(absPath, "utf8");
|
|
1494
|
+
return txt.length > maxChars ? txt.slice(0, maxChars) + "\n…[truncated]" : txt;
|
|
1495
|
+
} catch (_) {
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
async function listAdrs(limit) {
|
|
1501
|
+
const dir = join(PROJECT_STATE_DIR, "specs", "ADR");
|
|
1502
|
+
if (!existsSync(dir)) return [];
|
|
1503
|
+
const entries = await readdir(dir);
|
|
1504
|
+
const adrs = entries
|
|
1505
|
+
.filter((n) => n.startsWith("ADR-") && n.endsWith(".md"))
|
|
1506
|
+
.sort()
|
|
1507
|
+
.reverse();
|
|
1508
|
+
const out = [];
|
|
1509
|
+
for (const name of adrs.slice(0, limit || 5)) {
|
|
1510
|
+
const txt = readFileSync(join(dir, name), "utf8");
|
|
1511
|
+
const idMatch = txt.match(/^id:\s*(\S+)/m);
|
|
1512
|
+
const slugMatch = txt.match(/^slug:\s*(\S+)/m);
|
|
1513
|
+
const statusMatch = txt.match(/^status:\s*(\S+)/m);
|
|
1514
|
+
const titleMatch = txt.match(/^#\s+(.+)$/m);
|
|
1515
|
+
out.push({
|
|
1516
|
+
file: name,
|
|
1517
|
+
id: idMatch?.[1] || name,
|
|
1518
|
+
slug: slugMatch?.[1] || null,
|
|
1519
|
+
status: statusMatch?.[1] || null,
|
|
1520
|
+
title: titleMatch?.[1] || null,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return out;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async function compileRuntimeContext({ maxModules, maxDecisions, write }) {
|
|
1527
|
+
const state = await loadState();
|
|
1528
|
+
const projectState = safeReadJson(join(PROJECT_STATE_DIR, "context", "project-state.json"));
|
|
1529
|
+
const manifest = safeReadJson(join(AI_DIR, "manifest.json"));
|
|
1530
|
+
const adrs = await listAdrs(maxDecisions);
|
|
1531
|
+
|
|
1532
|
+
// Top modules by edges count (most connected = most relevant)
|
|
1533
|
+
let topModules = [];
|
|
1534
|
+
if (projectState && Array.isArray(projectState.modules)) {
|
|
1535
|
+
topModules = [...projectState.modules]
|
|
1536
|
+
.map((m) => ({
|
|
1537
|
+
path: m.path || m.file || null,
|
|
1538
|
+
kind: m.kind || null,
|
|
1539
|
+
edges_in: m.edges_in || 0,
|
|
1540
|
+
edges_out: m.edges_out || 0,
|
|
1541
|
+
connectivity: (m.edges_in || 0) + (m.edges_out || 0),
|
|
1542
|
+
}))
|
|
1543
|
+
.sort((a, b) => b.connectivity - a.connectivity)
|
|
1544
|
+
.slice(0, maxModules);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Fase 5 (ADR-0009): low confidence flags
|
|
1548
|
+
const lowConfidenceFlags = [];
|
|
1549
|
+
const cats = state.confidence?.categories || {};
|
|
1550
|
+
for (const [name, entry] of Object.entries(cats)) {
|
|
1551
|
+
if (entry?.level === "low") {
|
|
1552
|
+
lowConfidenceFlags.push({ category: name, reason: entry.reason || null });
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
const compiled = {
|
|
1557
|
+
$schema: "runtime-context-v1",
|
|
1558
|
+
compiled_at: new Date().toISOString(),
|
|
1559
|
+
os_version: manifest?.version || "unknown",
|
|
1560
|
+
identity: state.identity || {},
|
|
1561
|
+
lifecycle: state.lifecycle || {},
|
|
1562
|
+
rotating: state.rotating || {},
|
|
1563
|
+
low_confidence_flags: lowConfidenceFlags.slice(0, 3),
|
|
1564
|
+
persistent: {
|
|
1565
|
+
architecture_decisions_refs: state.persistent?.architecture_decisions_refs || [],
|
|
1566
|
+
rules_refs: state.persistent?.rules_refs || [],
|
|
1567
|
+
recent_adrs: adrs,
|
|
1568
|
+
},
|
|
1569
|
+
project_state: projectState
|
|
1570
|
+
? {
|
|
1571
|
+
source_hash: projectState.source_hash || projectState.hash || null,
|
|
1572
|
+
stack: projectState.stack || null,
|
|
1573
|
+
total_files: projectState.total_files || null,
|
|
1574
|
+
total_modules: Array.isArray(projectState.modules) ? projectState.modules.length : null,
|
|
1575
|
+
total_edges: projectState.total_edges || null,
|
|
1576
|
+
smells_count: Array.isArray(projectState.smells) ? projectState.smells.length : 0,
|
|
1577
|
+
top_modules: topModules,
|
|
1578
|
+
}
|
|
1579
|
+
: null,
|
|
1580
|
+
recent_workflow_history: (state.workflow_history || []).slice(0, 5),
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
const serialized = JSON.stringify(compiled, null, 2);
|
|
1584
|
+
const estimated = estimateTokens(serialized);
|
|
1585
|
+
compiled.estimated_tokens = estimated;
|
|
1586
|
+
|
|
1587
|
+
if (write) {
|
|
1588
|
+
await ensureRuntimeDir();
|
|
1589
|
+
await writeFile(RUNTIME_CONTEXT_PATH, JSON.stringify(compiled, null, 2), "utf8");
|
|
1590
|
+
}
|
|
1591
|
+
return {
|
|
1592
|
+
path: write ? RUNTIME_CONTEXT_PATH : null,
|
|
1593
|
+
estimated_tokens: estimated,
|
|
1594
|
+
over_budget: estimated > 4000,
|
|
1595
|
+
summary: {
|
|
1596
|
+
identity: compiled.identity,
|
|
1597
|
+
lifecycle: compiled.lifecycle,
|
|
1598
|
+
modules: compiled.project_state?.total_modules,
|
|
1599
|
+
top_modules_count: topModules.length,
|
|
1600
|
+
adrs_count: adrs.length,
|
|
1601
|
+
},
|
|
1602
|
+
runtime_context: compiled,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
async function detectDrift(sinceSnapshot) {
|
|
1607
|
+
const projectState = safeReadJson(join(PROJECT_STATE_DIR, "context", "project-state.json"));
|
|
1608
|
+
if (!projectState) {
|
|
1609
|
+
return { error: "project-state.json not found. Run aios_analyze_project first.", drift: [] };
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Pick snapshot
|
|
1613
|
+
const snapshotsDir = join(PROJECT_STATE_DIR, "snapshots");
|
|
1614
|
+
let chosenSnapshotId = sinceSnapshot;
|
|
1615
|
+
if (!chosenSnapshotId && existsSync(snapshotsDir)) {
|
|
1616
|
+
const entries = await readdir(snapshotsDir, { withFileTypes: true });
|
|
1617
|
+
const snapshots = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
1618
|
+
chosenSnapshotId = snapshots[0] || null;
|
|
1619
|
+
}
|
|
1620
|
+
if (!chosenSnapshotId) {
|
|
1621
|
+
return { error: "no snapshot available. Run aios_snapshot first.", drift: [] };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Snapshot v0.1 grava como 'state.json'; aceitar ambos os nomes
|
|
1625
|
+
const snapStatePath = existsSync(join(snapshotsDir, chosenSnapshotId, "project-state.json"))
|
|
1626
|
+
? join(snapshotsDir, chosenSnapshotId, "project-state.json")
|
|
1627
|
+
: join(snapshotsDir, chosenSnapshotId, "state.json");
|
|
1628
|
+
const snapState = safeReadJson(snapStatePath);
|
|
1629
|
+
if (!snapState) {
|
|
1630
|
+
return {
|
|
1631
|
+
error: `snapshot ${chosenSnapshotId} has no state.json/project-state.json`,
|
|
1632
|
+
snapshot_id: chosenSnapshotId,
|
|
1633
|
+
drift: [],
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// Compare module paths
|
|
1638
|
+
const currentPaths = new Set((projectState.modules || []).map((m) => m.path || m.file).filter(Boolean));
|
|
1639
|
+
const prevPaths = new Set((snapState.modules || []).map((m) => m.path || m.file).filter(Boolean));
|
|
1640
|
+
const added = [...currentPaths].filter((p) => !prevPaths.has(p));
|
|
1641
|
+
const removed = [...prevPaths].filter((p) => !currentPaths.has(p));
|
|
1642
|
+
const persisted = [...currentPaths].filter((p) => prevPaths.has(p));
|
|
1643
|
+
|
|
1644
|
+
// Specs that mention modules
|
|
1645
|
+
const specsDir = join(PROJECT_STATE_DIR, "specs");
|
|
1646
|
+
const specMentions = new Set();
|
|
1647
|
+
if (existsSync(specsDir)) {
|
|
1648
|
+
const collect = (dir) => {
|
|
1649
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1650
|
+
for (const e of entries) {
|
|
1651
|
+
const p = join(dir, e.name);
|
|
1652
|
+
if (e.isDirectory()) collect(p);
|
|
1653
|
+
else if (e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_")) {
|
|
1654
|
+
const txt = safeReadText(p, 50000) || "";
|
|
1655
|
+
for (const mod of currentPaths) {
|
|
1656
|
+
if (txt.includes(mod)) specMentions.add(mod);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
try { collect(specsDir); } catch (_) { /* ignore */ }
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const orphanModules = [...currentPaths].filter((p) => !specMentions.has(p));
|
|
1665
|
+
|
|
1666
|
+
return {
|
|
1667
|
+
snapshot_id: chosenSnapshotId,
|
|
1668
|
+
severity: "info",
|
|
1669
|
+
summary: {
|
|
1670
|
+
modules_added_since_snapshot: added.length,
|
|
1671
|
+
modules_removed_since_snapshot: removed.length,
|
|
1672
|
+
modules_persisted: persisted.length,
|
|
1673
|
+
orphan_modules_no_spec_reference: orphanModules.length,
|
|
1674
|
+
},
|
|
1675
|
+
modules_added: added.slice(0, 30),
|
|
1676
|
+
modules_removed: removed.slice(0, 30),
|
|
1677
|
+
orphan_modules: orphanModules.slice(0, 30),
|
|
1678
|
+
note: "Heurística MVP: hash+path+spec text search. Severity 'info' por default; Fase 4+ pode aprofundar com AST diff.",
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
async function getContextLayer(layer, focus) {
|
|
1683
|
+
if (layer === "core") {
|
|
1684
|
+
return await getCoreBundle();
|
|
1685
|
+
}
|
|
1686
|
+
if (layer === "working") {
|
|
1687
|
+
const state = await loadState();
|
|
1688
|
+
const projectState = safeReadJson(join(PROJECT_STATE_DIR, "context", "project-state.json"));
|
|
1689
|
+
const adrs = await listAdrs(3);
|
|
1690
|
+
const summary = safeReadText(join(PROJECT_STATE_DIR, "memory", "_summary.md"), 2000);
|
|
1691
|
+
return {
|
|
1692
|
+
layer: "working",
|
|
1693
|
+
focus: focus || null,
|
|
1694
|
+
identity: state.identity,
|
|
1695
|
+
lifecycle: state.lifecycle,
|
|
1696
|
+
rotating: state.rotating,
|
|
1697
|
+
project_state_summary: projectState
|
|
1698
|
+
? {
|
|
1699
|
+
source_hash: projectState.source_hash || null,
|
|
1700
|
+
stack: projectState.stack || null,
|
|
1701
|
+
total_modules: Array.isArray(projectState.modules) ? projectState.modules.length : null,
|
|
1702
|
+
}
|
|
1703
|
+
: null,
|
|
1704
|
+
recent_adrs: adrs,
|
|
1705
|
+
memory_summary_preview: summary ? summary.slice(0, 1500) : null,
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
if (layer === "deep") {
|
|
1709
|
+
const state = await loadState();
|
|
1710
|
+
const projectState = safeReadJson(join(PROJECT_STATE_DIR, "context", "project-state.json"));
|
|
1711
|
+
const adrs = await listAdrs(20);
|
|
1712
|
+
const memoryDir = join(PROJECT_STATE_DIR, "memory");
|
|
1713
|
+
const memoryFiles = {};
|
|
1714
|
+
if (existsSync(memoryDir)) {
|
|
1715
|
+
const entries = await readdir(memoryDir, { withFileTypes: true });
|
|
1716
|
+
for (const e of entries) {
|
|
1717
|
+
if (e.isFile() && e.name.endsWith(".md")) {
|
|
1718
|
+
memoryFiles[e.name] = safeReadText(join(memoryDir, e.name), 8000);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return {
|
|
1723
|
+
layer: "deep",
|
|
1724
|
+
focus: focus || null,
|
|
1725
|
+
state,
|
|
1726
|
+
project_state: projectState,
|
|
1727
|
+
adrs,
|
|
1728
|
+
memory_files: memoryFiles,
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
throw new Error(`Unknown layer: ${layer} (use 'core', 'working' or 'deep')`);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
async function artifactList(typeFilter, statusFilter) {
|
|
1735
|
+
const items = [];
|
|
1736
|
+
|
|
1737
|
+
const specsBase = join(PROJECT_STATE_DIR, "specs");
|
|
1738
|
+
const specTypes = ["ADR", "RFC", "PRD", "SDD", "RUNBOOKS", "TASKS", "BRD"];
|
|
1739
|
+
for (const t of specTypes) {
|
|
1740
|
+
const dir = join(specsBase, t);
|
|
1741
|
+
if (!existsSync(dir)) continue;
|
|
1742
|
+
const entries = await readdir(dir);
|
|
1743
|
+
for (const f of entries) {
|
|
1744
|
+
if (!f.endsWith(".md") || f.startsWith("_")) continue;
|
|
1745
|
+
const txt = safeReadText(join(dir, f), 4000) || "";
|
|
1746
|
+
const idMatch = txt.match(/^id:\s*(\S+)/m);
|
|
1747
|
+
const statusMatch = txt.match(/^status:\s*(\S+)/m);
|
|
1748
|
+
const titleMatch = txt.match(/^#\s+(.+)$/m);
|
|
1749
|
+
const normalizedType = t.replace(/S$/, "");
|
|
1750
|
+
items.push({
|
|
1751
|
+
type: normalizedType,
|
|
1752
|
+
file: `${t}/${f}`,
|
|
1753
|
+
id: idMatch?.[1] || f,
|
|
1754
|
+
status: statusMatch?.[1] || null,
|
|
1755
|
+
title: titleMatch?.[1] || null,
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Snapshots
|
|
1761
|
+
const snapshotsDir = join(PROJECT_STATE_DIR, "snapshots");
|
|
1762
|
+
if (existsSync(snapshotsDir)) {
|
|
1763
|
+
const entries = await readdir(snapshotsDir, { withFileTypes: true });
|
|
1764
|
+
for (const e of entries) {
|
|
1765
|
+
if (!e.isDirectory()) continue;
|
|
1766
|
+
items.push({
|
|
1767
|
+
type: "snapshot",
|
|
1768
|
+
file: `snapshots/${e.name}`,
|
|
1769
|
+
id: e.name,
|
|
1770
|
+
status: null,
|
|
1771
|
+
title: e.name,
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// Packages
|
|
1777
|
+
const packagesDir = join(PROJECT_STATE_DIR, "context", "packages");
|
|
1778
|
+
if (existsSync(packagesDir)) {
|
|
1779
|
+
const entries = await readdir(packagesDir);
|
|
1780
|
+
for (const f of entries) {
|
|
1781
|
+
if (!f.endsWith(".md")) continue;
|
|
1782
|
+
items.push({
|
|
1783
|
+
type: "package",
|
|
1784
|
+
file: `context/packages/${f}`,
|
|
1785
|
+
id: f.replace(/\.md$/, ""),
|
|
1786
|
+
status: null,
|
|
1787
|
+
title: f,
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Workflows
|
|
1793
|
+
if (existsSync(WORKFLOWS_DIR)) {
|
|
1794
|
+
const entries = await readdir(WORKFLOWS_DIR);
|
|
1795
|
+
for (const f of entries) {
|
|
1796
|
+
if (!f.endsWith(".json") || f.startsWith("_")) continue;
|
|
1797
|
+
const wf = safeReadJson(join(WORKFLOWS_DIR, f)) || {};
|
|
1798
|
+
items.push({
|
|
1799
|
+
type: "workflow",
|
|
1800
|
+
file: `workflows/${f}`,
|
|
1801
|
+
id: wf.id || f,
|
|
1802
|
+
status: wf.version || null,
|
|
1803
|
+
title: wf.description || null,
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
let filtered = items;
|
|
1809
|
+
if (typeFilter) filtered = filtered.filter((i) => i.type === typeFilter);
|
|
1810
|
+
if (statusFilter) filtered = filtered.filter((i) => i.status === statusFilter);
|
|
1811
|
+
|
|
1812
|
+
// Write index if no filter (canonical full index)
|
|
1813
|
+
if (!typeFilter && !statusFilter) {
|
|
1814
|
+
try {
|
|
1815
|
+
const artifactsDir = join(PROJECT_STATE_DIR, "artifacts");
|
|
1816
|
+
if (!existsSync(artifactsDir)) await mkdir(artifactsDir, { recursive: true });
|
|
1817
|
+
const indexBlob = {
|
|
1818
|
+
compiled_at: new Date().toISOString(),
|
|
1819
|
+
count: items.length,
|
|
1820
|
+
items,
|
|
1821
|
+
};
|
|
1822
|
+
await writeFile(ARTIFACTS_INDEX_PATH, JSON.stringify(indexBlob, null, 2), "utf8");
|
|
1823
|
+
} catch (_) { /* best-effort */ }
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
return { count: filtered.length, total: items.length, items: filtered };
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async function listWorkflows() {
|
|
1830
|
+
if (!existsSync(WORKFLOWS_DIR)) return { workflows: [] };
|
|
1831
|
+
const entries = await readdir(WORKFLOWS_DIR, { withFileTypes: true });
|
|
1832
|
+
const out = [];
|
|
1833
|
+
for (const e of entries) {
|
|
1834
|
+
if (!e.isFile() || !e.name.endsWith(".json") || e.name.startsWith("_")) continue;
|
|
1835
|
+
try {
|
|
1836
|
+
const raw = await readFile(join(WORKFLOWS_DIR, e.name), "utf8");
|
|
1837
|
+
const wf = JSON.parse(raw);
|
|
1838
|
+
out.push({
|
|
1839
|
+
id: wf.id,
|
|
1840
|
+
version: wf.version,
|
|
1841
|
+
description: wf.description,
|
|
1842
|
+
steps: Array.isArray(wf.steps) ? wf.steps.length : 0,
|
|
1843
|
+
file: e.name,
|
|
1844
|
+
});
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
out.push({ id: e.name.replace(/\.json$/, ""), error: String(err.message || err), file: e.name });
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return { workflows: out };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// ---------- MCP Prompts (ADR-0010) ----------
|
|
1853
|
+
// Expõe os 3 workflows canônicos (init/sync/work) como MCP Prompts.
|
|
1854
|
+
// Em IDEs que suportam o primitivo Prompts (Claude Code, Cursor recente),
|
|
1855
|
+
// aparecem nativamente no autocomplete `/` — funcionam apenas conectando o MCP.
|
|
1856
|
+
// Híbrido: cada prompt retorna instrução para a LLM chamar `aios_run_workflow`
|
|
1857
|
+
// + contexto enriquecido (state.identity, ROUTER hint, suggested intent).
|
|
1858
|
+
|
|
1859
|
+
const PROMPT_WORKFLOWS = ["init", "sync", "work"];
|
|
1860
|
+
|
|
1861
|
+
async function listMcpPrompts() {
|
|
1862
|
+
const prompts = [];
|
|
1863
|
+
for (const name of PROMPT_WORKFLOWS) {
|
|
1864
|
+
const wfPath = join(WORKFLOWS_DIR, `${name}.json`);
|
|
1865
|
+
if (!existsSync(wfPath)) continue;
|
|
1866
|
+
try {
|
|
1867
|
+
const wf = JSON.parse(await readFile(wfPath, "utf8"));
|
|
1868
|
+
prompts.push({
|
|
1869
|
+
name,
|
|
1870
|
+
title: `/${name}`,
|
|
1871
|
+
description: wf.description || `Run the ${name} workflow.`,
|
|
1872
|
+
arguments: [
|
|
1873
|
+
{
|
|
1874
|
+
name: "intent",
|
|
1875
|
+
description:
|
|
1876
|
+
name === "work"
|
|
1877
|
+
? "Descrição da tarefa a executar (recomendado)."
|
|
1878
|
+
: "Intent opcional para contextualizar a execução.",
|
|
1879
|
+
required: name === "work",
|
|
1880
|
+
},
|
|
1881
|
+
],
|
|
1882
|
+
});
|
|
1883
|
+
} catch (_) {
|
|
1884
|
+
// skip malformed
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return prompts;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function buildRouterHint(intent) {
|
|
1891
|
+
if (!intent || typeof intent !== "string") return null;
|
|
1892
|
+
try {
|
|
1893
|
+
const routes = await parseRouter();
|
|
1894
|
+
const q = normalizeQuery(intent);
|
|
1895
|
+
const matched = [];
|
|
1896
|
+
const files = new Set();
|
|
1897
|
+
for (const r of routes) {
|
|
1898
|
+
for (const t of r.triggers) {
|
|
1899
|
+
if (t && q.includes(t)) {
|
|
1900
|
+
matched.push(r.title);
|
|
1901
|
+
for (const f of r.files) files.add(f);
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (matched.length === 0) return null;
|
|
1907
|
+
return { matched_routes: matched, conditional_files: [...files] };
|
|
1908
|
+
} catch (_) {
|
|
1909
|
+
return null;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
async function getMcpPrompt(name, args = {}) {
|
|
1914
|
+
if (!PROMPT_WORKFLOWS.includes(name)) {
|
|
1915
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
1916
|
+
}
|
|
1917
|
+
const wf = await loadWorkflow(name);
|
|
1918
|
+
const intent = typeof args.intent === "string" ? args.intent.trim() : "";
|
|
1919
|
+
|
|
1920
|
+
// Coleta contexto enriquecido (best-effort: nunca falha o prompt)
|
|
1921
|
+
let identity = null;
|
|
1922
|
+
let lifecycle = null;
|
|
1923
|
+
let maturity = null;
|
|
1924
|
+
try {
|
|
1925
|
+
const state = await loadState();
|
|
1926
|
+
identity = state.identity || null;
|
|
1927
|
+
lifecycle = state.lifecycle || null;
|
|
1928
|
+
maturity = state.identity?.maturity || null;
|
|
1929
|
+
} catch (_) { /* state ausente é ok */ }
|
|
1930
|
+
|
|
1931
|
+
const routerHint = await buildRouterHint(intent);
|
|
1932
|
+
|
|
1933
|
+
// Monta os argumentos da tool conforme convenção do workflow engine
|
|
1934
|
+
const toolArgs = { name };
|
|
1935
|
+
const input = {};
|
|
1936
|
+
if (intent) input.intent = intent;
|
|
1937
|
+
if (Object.keys(input).length > 0) toolArgs.input = input;
|
|
1938
|
+
|
|
1939
|
+
// Texto principal: instrução pra LLM + contexto compacto
|
|
1940
|
+
const lines = [];
|
|
1941
|
+
lines.push(`# /${name} — AI OS workflow (v${wf.version || "?"})`);
|
|
1942
|
+
lines.push("");
|
|
1943
|
+
lines.push(wf.description || "");
|
|
1944
|
+
lines.push("");
|
|
1945
|
+
lines.push("## Ação");
|
|
1946
|
+
lines.push("Chame a tool MCP `aios_run_workflow` com estes argumentos:");
|
|
1947
|
+
lines.push("");
|
|
1948
|
+
lines.push("```json");
|
|
1949
|
+
lines.push(JSON.stringify(toolArgs, null, 2));
|
|
1950
|
+
lines.push("```");
|
|
1951
|
+
|
|
1952
|
+
if (Array.isArray(wf.guarantees) && wf.guarantees.length > 0) {
|
|
1953
|
+
lines.push("");
|
|
1954
|
+
lines.push("## Garantias do workflow");
|
|
1955
|
+
for (const g of wf.guarantees) lines.push(`- ${g}`);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
if (identity || lifecycle || maturity) {
|
|
1959
|
+
lines.push("");
|
|
1960
|
+
lines.push("## Contexto do projeto (state.json)");
|
|
1961
|
+
const ctxObj = {};
|
|
1962
|
+
if (identity) ctxObj.identity = identity;
|
|
1963
|
+
if (lifecycle) ctxObj.lifecycle = lifecycle;
|
|
1964
|
+
if (maturity) ctxObj.maturity = maturity;
|
|
1965
|
+
lines.push("```json");
|
|
1966
|
+
lines.push(JSON.stringify(ctxObj, null, 2));
|
|
1967
|
+
lines.push("```");
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
if (routerHint) {
|
|
1971
|
+
lines.push("");
|
|
1972
|
+
lines.push("## ROUTER hint (intent → arquivos condicionais)");
|
|
1973
|
+
lines.push("```json");
|
|
1974
|
+
lines.push(JSON.stringify(routerHint, null, 2));
|
|
1975
|
+
lines.push("```");
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
lines.push("");
|
|
1979
|
+
lines.push(
|
|
1980
|
+
"> Após chamar `aios_run_workflow`, siga `rules/execution-flow.md`: emita `OS:loaded(...)` na primeira linha, classifique a tarefa, e termine com bloco `[OS Audit]`.",
|
|
1981
|
+
);
|
|
1982
|
+
|
|
1983
|
+
return {
|
|
1984
|
+
description: wf.description || `Run the ${name} workflow.`,
|
|
1985
|
+
messages: [
|
|
1986
|
+
{
|
|
1987
|
+
role: "user",
|
|
1988
|
+
content: { type: "text", text: lines.join("\n") },
|
|
1989
|
+
},
|
|
1990
|
+
],
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// ---------- MCP JSON-RPC handler ----------
|
|
1995
|
+
async function handleRpc(msg) {
|
|
1996
|
+
const { method, id, params } = msg;
|
|
1997
|
+
try {
|
|
1998
|
+
if (method === "initialize") {
|
|
1999
|
+
return {
|
|
2000
|
+
jsonrpc: "2.0",
|
|
2001
|
+
id,
|
|
2002
|
+
result: {
|
|
2003
|
+
protocolVersion: "2024-11-05",
|
|
2004
|
+
capabilities: { tools: {}, prompts: {} },
|
|
2005
|
+
serverInfo: { name: "ai-os", version: "1.2.0" },
|
|
2006
|
+
},
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
if (method === "tools/list") {
|
|
2010
|
+
return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
|
|
2011
|
+
}
|
|
2012
|
+
if (method === "tools/call") {
|
|
2013
|
+
const handler = HANDLERS[params.name];
|
|
2014
|
+
if (!handler) throw new Error(`Unknown tool: ${params.name}`);
|
|
2015
|
+
const result = await handler(params.arguments || {});
|
|
2016
|
+
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
2017
|
+
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text }] } };
|
|
2018
|
+
}
|
|
2019
|
+
if (method === "prompts/list") {
|
|
2020
|
+
const prompts = await listMcpPrompts();
|
|
2021
|
+
return { jsonrpc: "2.0", id, result: { prompts } };
|
|
2022
|
+
}
|
|
2023
|
+
if (method === "prompts/get") {
|
|
2024
|
+
const result = await getMcpPrompt(params?.name, params?.arguments || {});
|
|
2025
|
+
return { jsonrpc: "2.0", id, result };
|
|
2026
|
+
}
|
|
2027
|
+
if (method === "notifications/initialized") return null; // notif, no reply
|
|
2028
|
+
if (method === "ping") return { jsonrpc: "2.0", id, result: {} };
|
|
2029
|
+
throw new Error(`Method not found: ${method}`);
|
|
2030
|
+
} catch (e) {
|
|
2031
|
+
return { jsonrpc: "2.0", id, error: { code: -32000, message: e.message } };
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// ---------- Transports ----------
|
|
2036
|
+
function startStdio() {
|
|
2037
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
2038
|
+
rl.on("line", async (line) => {
|
|
2039
|
+
if (!line.trim()) return;
|
|
2040
|
+
let msg;
|
|
2041
|
+
try {
|
|
2042
|
+
msg = JSON.parse(line);
|
|
2043
|
+
} catch {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const resp = await handleRpc(msg);
|
|
2047
|
+
if (resp) process.stdout.write(JSON.stringify(resp) + "\n");
|
|
2048
|
+
});
|
|
2049
|
+
process.stderr.write(`[aios] MCP server up (stdio) — mode: ${STEALTHOS_MODE}, project: ${PROJECT_ROOT}\n`);
|
|
2050
|
+
if (STEALTHOS_MODE === "hybrid") {
|
|
2051
|
+
process.stderr.write(`[aios] KB: ${KB_DIR}, project state: ${PROJECT_STATE_DIR}\n`);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
async function tryListen(server, port, attempts = 5) {
|
|
2056
|
+
return new Promise((resolveProm, rejectProm) => {
|
|
2057
|
+
let tries = 0;
|
|
2058
|
+
function tryPort(p) {
|
|
2059
|
+
server.once("error", (err) => {
|
|
2060
|
+
if (err.code === "EADDRINUSE" && tries < attempts) {
|
|
2061
|
+
tries++;
|
|
2062
|
+
const next = p + 1;
|
|
2063
|
+
process.stderr.write(`[aios] Port ${p} busy, trying ${next}\n`);
|
|
2064
|
+
tryPort(next);
|
|
2065
|
+
} else {
|
|
2066
|
+
rejectProm(err);
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
server.listen(p, "127.0.0.1", () => resolveProm(p));
|
|
2070
|
+
}
|
|
2071
|
+
tryPort(port);
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
async function startHttp() {
|
|
2076
|
+
const server = createServer(async (req, res) => {
|
|
2077
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
2078
|
+
const h = await HANDLERS["aios_health"]({});
|
|
2079
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2080
|
+
res.end(JSON.stringify(h, null, 2));
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (req.method === "POST" && req.url === "/rpc") {
|
|
2084
|
+
let body = "";
|
|
2085
|
+
req.on("data", (d) => (body += d));
|
|
2086
|
+
req.on("end", async () => {
|
|
2087
|
+
let msg;
|
|
2088
|
+
try {
|
|
2089
|
+
msg = JSON.parse(body);
|
|
2090
|
+
} catch {
|
|
2091
|
+
res.writeHead(400);
|
|
2092
|
+
res.end('{"error":"invalid json"}');
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const resp = await handleRpc(msg);
|
|
2096
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2097
|
+
res.end(JSON.stringify(resp || {}));
|
|
2098
|
+
});
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (req.method === "GET" && req.url === "/tools") {
|
|
2102
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2103
|
+
res.end(JSON.stringify(TOOLS, null, 2));
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
res.writeHead(404);
|
|
2107
|
+
res.end("Not found");
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
try {
|
|
2111
|
+
const port = await tryListen(server, REQUESTED_PORT, 10);
|
|
2112
|
+
// Write port to runtime file
|
|
2113
|
+
const runtimeDir = join(PROJECT_STATE_DIR, ".runtime");
|
|
2114
|
+
await mkdir(runtimeDir, { recursive: true });
|
|
2115
|
+
await writeFile(join(runtimeDir, "port"), String(port));
|
|
2116
|
+
await writeFile(join(runtimeDir, "daemon.pid"), String(process.pid));
|
|
2117
|
+
process.stderr.write(`[aios] HTTP server listening on http://127.0.0.1:${port}\n`);
|
|
2118
|
+
process.stderr.write(`[aios] Endpoints: GET /health, GET /tools, POST /rpc\n`);
|
|
2119
|
+
} catch (e) {
|
|
2120
|
+
process.stderr.write(`[aios] HTTP start failed: ${e.message}\n`);
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// ---------- Boot ----------
|
|
2126
|
+
if (HTTP_MODE) {
|
|
2127
|
+
await startHttp();
|
|
2128
|
+
} else {
|
|
2129
|
+
startStdio();
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Graceful shutdown
|
|
2133
|
+
process.on("SIGINT", () => process.exit(0));
|
|
2134
|
+
process.on("SIGTERM", () => process.exit(0));
|