brito-cli 0.0.0-stage → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { listAgents } from "./agents.js";
4
+ import { buildSystemPrompt } from "./prompt.js";
5
+ import { skillsIndex } from "../skills/store.js";
6
+ import { MCP_NAME, MCP_PREFIX, APPROVE_TOOL } from "../mcp/server.js";
7
+ import { ui } from "../ui/ui.js";
8
+ import { MODE_ENV, perms } from "../permissions.js";
9
+ import { wireEffort } from "../model.js";
10
+ // Backend alternativo: delega ao `claude` oficial, que autentica com o /login do usuário.
11
+ // O brito nunca lê nem envia o token da assinatura. Grafo, memória e skills chegam ao claude via MCP.
12
+ export function hasClaudeCli() {
13
+ return spawnSync("claude", ["--version"], { stdio: "ignore" }).status === 0;
14
+ }
15
+ /** Nome das tools do Brito → nome que o claude enxerga. */
16
+ const CLAUDE_TOOL = { read_file: "Read", glob: "Glob", grep: "Grep", write_file: "Write", edit_file: "Edit", bash: "Bash" };
17
+ const claudeToolName = (n) => CLAUDE_TOOL[n] ?? MCP_PREFIX + n;
18
+ /** Agentes do Brito viram subagentes do claude (--agents), no lugar do `delegate`. */
19
+ function claudeAgents() {
20
+ const out = {};
21
+ for (const a of listAgents()) {
22
+ out[a.name] = {
23
+ description: a.description,
24
+ prompt: a.prompt,
25
+ ...(a.tools === "all" ? {} : { tools: a.tools.map(claudeToolName) }),
26
+ ...(a.model?.includes("haiku") ? { model: "haiku" } : {}),
27
+ };
28
+ }
29
+ return out;
30
+ }
31
+ /**
32
+ * Acumula um evento do stream-json no TurnRecord que alimenta a reflexão de skills.
33
+ * Só a thread principal entra: mensagens de subagentes (`parent_tool_use_id`) têm contexto próprio.
34
+ */
35
+ export function recordStreamEvent(turn, ev, loadedSkills) {
36
+ if (ev?.parent_tool_use_id)
37
+ return;
38
+ if (ev?.type === "assistant") {
39
+ const rec = [];
40
+ for (const b of ev.message?.content ?? []) {
41
+ if (b.type === "text" && b.text)
42
+ rec.push({ type: "text", text: b.text });
43
+ else if (b.type === "tool_use") {
44
+ const name = String(b.name).replace(MCP_PREFIX, "");
45
+ rec.push({ type: "tool_use", id: b.id, name, input: b.input });
46
+ if (name === "skill_load" && typeof b.input?.name === "string")
47
+ loadedSkills.add(b.input.name);
48
+ }
49
+ }
50
+ if (rec.length)
51
+ turn.messages.push({ role: "assistant", content: rec });
52
+ }
53
+ else if (ev?.type === "user") {
54
+ const results = (ev.message?.content ?? []).filter((b) => b.type === "tool_result")
55
+ .map((b) => ({ type: "tool_result", tool_use_id: b.tool_use_id, content: b.content, is_error: !!b.is_error }));
56
+ if (!results.length)
57
+ return;
58
+ turn.messages.push({ role: "user", content: results });
59
+ turn.toolCalls += results.length;
60
+ turn.errors += results.filter((r) => r.is_error).length;
61
+ }
62
+ }
63
+ /**
64
+ * Config do servidor MCP entregue ao claude. O servidor roda em outro processo: o env leva o socket de aprovação
65
+ * e o modo de permissão do REPL (vale por turno, pois cada turno sobe um claude novo com esta config).
66
+ */
67
+ export function mcpConfig(cmd, approvalSock, mode) {
68
+ // alwaysLoad: sem isso o claude adia as tools do Brito atrás do ToolSearch e o modelo raramente chega ao grafo.
69
+ return { mcpServers: { [MCP_NAME]: { type: "stdio", command: cmd.command, args: cmd.args, alwaysLoad: true, env: { BRITO_APPROVE_SOCK: approvalSock, [MODE_ENV]: mode } } } };
70
+ }
71
+ export class ClaudeSession {
72
+ o;
73
+ id = randomUUID();
74
+ started = false;
75
+ usage = { input: 0, output: 0, cacheRead: 0 };
76
+ model = "claude (assinatura)";
77
+ /** modelo pedido via `/model` (--model do claude); vazio = padrão da assinatura */
78
+ requestedModel = "";
79
+ /** esforço pedido via `/effort` (--effort do claude); vazio = padrão */
80
+ effort = "";
81
+ /** prompt da última chamada à API da thread principal = tamanho atual do contexto (como o Hermes) */
82
+ contextTokens = 0;
83
+ /** janela real do modelo, informada pelo claude no evento `result` */
84
+ contextWindow;
85
+ costUsd = 0;
86
+ /** transcript do último turno (thread principal), montado do stream-json para a reflexão de skills */
87
+ lastTurn = null;
88
+ recentTurns = [];
89
+ /** skills carregadas via skill_load (que roda no servidor MCP, outro processo) */
90
+ loadedSkills = new Set();
91
+ /** último turno em andamento, para o Ctrl+C */
92
+ child = null;
93
+ constructor(o) {
94
+ this.o = o;
95
+ }
96
+ reset() { this.id = randomUUID(); this.started = false; this.contextTokens = 0; this.recentTurns = []; }
97
+ abort() { this.child?.kill("SIGINT"); }
98
+ args(text) {
99
+ const { root, approvalSock, stats, mcpCommand } = this.o;
100
+ const mcp = mcpConfig(mcpCommand, approvalSock, perms.mode);
101
+ const readOnlyMcp = ["graph_search", "graph_callers", "graph_callees", "graph_impact", "graph_context", "graph_query", "graph_index", "remember", "recall", "skill_load"];
102
+ const system = buildSystemPrompt(root, stats, skillsIndex(root)) +
103
+ `\n\n# Nesta sessão\nAs tools do Brito chegam com o prefixo \`${MCP_PREFIX}\` (ex.: ${MCP_PREFIX}graph_context). Use-as como descrito acima. ` +
104
+ `read_file/grep/glob/edit_file/bash correspondem a Read/Grep/Glob/Edit/Bash, e \`delegate\` corresponde à tool Agent/Task com os subagentes configurados (explore, plan, general e os do projeto).`;
105
+ const args = [
106
+ "-p", text,
107
+ this.started ? "--resume" : "--session-id", this.id,
108
+ "--output-format", "stream-json", "--verbose",
109
+ "--append-system-prompt", system,
110
+ "--mcp-config", JSON.stringify(mcp),
111
+ "--agents", JSON.stringify(claudeAgents()),
112
+ ];
113
+ if (this.requestedModel)
114
+ args.push("--model", this.requestedModel);
115
+ const wire = wireEffort(this.effort);
116
+ if (wire.effort)
117
+ args.push("--effort", wire.effort);
118
+ // O modo vale por turno: Shift+Tab no REPL muda o próximo turno.
119
+ const mode = { manual: "default", auto: "acceptEdits", plan: "plan", bypass: "bypassPermissions" }[perms.mode];
120
+ args.push("--permission-mode", mode);
121
+ if (perms.mode !== "bypass") {
122
+ args.push("--allowedTools", ...readOnlyMcp.map((n) => MCP_PREFIX + n), "--permission-prompt-tool", `${MCP_PREFIX}${APPROVE_TOOL}`);
123
+ }
124
+ return args;
125
+ }
126
+ /** Roda um turno renderizando o stream-json com a UI do Brito. */
127
+ send(text) {
128
+ return new Promise((resolve) => {
129
+ ui.turnStart();
130
+ const child = (this.child = spawn("claude", this.args(text), {
131
+ cwd: this.o.root, stdio: ["ignore", "pipe", "pipe"],
132
+ // `/reasoning none`: o claude desliga o raciocínio com MAX_THINKING_TOKENS=0 (não há `--effort none`)
133
+ env: wireEffort(this.effort).thinking ? process.env : { ...process.env, MAX_THINKING_TOKENS: "0" },
134
+ }));
135
+ let buf = "", stderr = "", ok = true, interrupted = false;
136
+ const names = new Map();
137
+ const turn = { messages: [{ role: "user", content: text }], toolCalls: 0, errors: 0, finished: false };
138
+ this.lastTurn = turn;
139
+ this.recentTurns = [...this.recentTurns, turn].slice(-3);
140
+ const handle = (ev) => {
141
+ recordStreamEvent(turn, ev, this.loadedSkills);
142
+ if (ev.type === "assistant") {
143
+ for (const b of ev.message?.content ?? []) {
144
+ if (b.type === "thinking" && b.thinking && !ev.parent_tool_use_id)
145
+ ui.thinking(b.thinking);
146
+ else if (b.type === "text" && b.text) {
147
+ ui.text(b.text);
148
+ ui.textEnd();
149
+ }
150
+ else if (b.type === "tool_use") {
151
+ const name = String(b.name).replace(MCP_PREFIX, "");
152
+ names.set(b.id, { name, input: b.input });
153
+ ui.toolStart(b.id, name, b.input);
154
+ }
155
+ }
156
+ if (ev.message?.model)
157
+ this.model = ev.message.model;
158
+ const u = ev.message?.usage;
159
+ // Mensagens de subagentes (sidechain) têm contexto próprio: não contam para a barra.
160
+ if (u && !ev.parent_tool_use_id)
161
+ this.contextTokens = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
162
+ }
163
+ else if (ev.type === "user") {
164
+ for (const b of ev.message?.content ?? [])
165
+ if (b.type === "tool_result")
166
+ ui.toolEnd(b.tool_use_id, !!b.is_error);
167
+ }
168
+ else if (ev.type === "result") {
169
+ this.usage.input += (ev.usage?.input_tokens ?? 0) + (ev.usage?.cache_read_input_tokens ?? 0) + (ev.usage?.cache_creation_input_tokens ?? 0);
170
+ this.usage.output += ev.usage?.output_tokens ?? 0;
171
+ this.usage.cacheRead += ev.usage?.cache_read_input_tokens ?? 0;
172
+ this.costUsd += ev.total_cost_usd ?? 0;
173
+ const win = Object.values(ev.modelUsage ?? {}).map((m) => m?.contextWindow).filter((w) => w > 0);
174
+ if (win.length)
175
+ this.contextWindow = Math.min(...win);
176
+ if (ev.is_error) {
177
+ ok = false;
178
+ ui.error(String(ev.result ?? "erro do claude"));
179
+ }
180
+ }
181
+ };
182
+ child.stdout.on("data", (d) => {
183
+ buf += d.toString();
184
+ let i;
185
+ while ((i = buf.indexOf("\n")) >= 0) {
186
+ const line = buf.slice(0, i).trim();
187
+ buf = buf.slice(i + 1);
188
+ if (!line)
189
+ continue;
190
+ try {
191
+ handle(JSON.parse(line));
192
+ }
193
+ catch { /* linha que não é JSON: ignora */ }
194
+ }
195
+ });
196
+ child.stderr.on("data", (d) => { stderr += d.toString(); });
197
+ child.on("error", (e) => { ok = false; ui.error(`não consegui executar o claude: ${e.message}`); });
198
+ child.on("close", (code, signal) => {
199
+ this.child = null;
200
+ interrupted = signal === "SIGINT" || code === 130;
201
+ if (code !== 0 && !interrupted) {
202
+ ok = false;
203
+ if (stderr.trim())
204
+ ui.error(stderr.trim().slice(0, 600));
205
+ }
206
+ else if (code === 0)
207
+ this.started = true;
208
+ turn.finished = ok && !interrupted && code === 0;
209
+ if (interrupted)
210
+ ui.warn("⏹ interrompido");
211
+ ui.turnEnd(ok && !interrupted);
212
+ resolve(ok);
213
+ });
214
+ });
215
+ }
216
+ }
@@ -0,0 +1,40 @@
1
+ export const estimateTokens = (msgs) => Math.ceil(JSON.stringify(msgs).length / 4);
2
+ const MAX_OLD_RESULT = 1500;
3
+ /** Fase 1 (barata): encurta tool_results antigos, preservando as últimas `keep` mensagens. */
4
+ export function trimOldToolResults(msgs, keep = 8) {
5
+ return msgs.map((m, i) => {
6
+ if (i >= msgs.length - keep || typeof m.content === "string")
7
+ return m;
8
+ return {
9
+ ...m,
10
+ content: m.content.map((b) => {
11
+ if (b.type === "tool_result" && typeof b.content === "string" && b.content.length > MAX_OLD_RESULT) {
12
+ return { ...b, content: b.content.slice(0, 700) + `\n…[${b.content.length - 1400} chars omitidos]…\n` + b.content.slice(-700) };
13
+ }
14
+ return b;
15
+ }),
16
+ };
17
+ });
18
+ }
19
+ /** Fase 2: resume o meio da conversa via LLM, mantendo o início e os últimos turnos. */
20
+ export async function summarizeMiddle(client, model, msgs, keepTail = 6) {
21
+ if (msgs.length <= keepTail + 2)
22
+ return msgs;
23
+ let cut = msgs.length - keepTail;
24
+ // O trecho final precisa começar numa mensagem de usuário "pura" (sem tool_result órfão).
25
+ while (cut < msgs.length && !(msgs[cut].role === "user" && typeof msgs[cut].content === "string"))
26
+ cut++;
27
+ if (cut >= msgs.length)
28
+ return msgs;
29
+ const head = msgs.slice(0, 1);
30
+ const middle = msgs.slice(1, cut);
31
+ const tail = msgs.slice(cut);
32
+ const res = await client.messages.create({
33
+ model,
34
+ max_tokens: 2000,
35
+ system: "Resuma a conversa de um agente de programação. Preserve: objetivo, decisões, arquivos/símbolos tocados, erros encontrados, estado atual e próximos passos. Seja denso.",
36
+ messages: [{ role: "user", content: JSON.stringify(middle).slice(0, 150_000) }],
37
+ });
38
+ const summary = res.content.map((b) => (b.type === "text" ? b.text : "")).join("");
39
+ return [...head, { role: "user", content: `[Resumo da conversa anterior]\n${summary}` }, { role: "assistant", content: "Entendido, continuando a partir do resumo." }, ...tail];
40
+ }
@@ -0,0 +1,125 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { toApiTools, execTool, isConcurrent } from "../tools/registry.js";
3
+ import { estimateTokens, summarizeMiddle, trimOldToolResults } from "./context.js";
4
+ import { ui } from "../ui/ui.js";
5
+ import { initialModel } from "../model.js";
6
+ export const MODEL = initialModel();
7
+ const COMPACT_AT = Number(process.env.BRITO_COMPACT_TOKENS ?? 120_000);
8
+ export class Agent {
9
+ system;
10
+ ctx;
11
+ opts;
12
+ client = new Anthropic();
13
+ messages = [];
14
+ usage = { input: 0, output: 0, cacheRead: 0, steps: 0 };
15
+ /** tamanho do contexto na última requisição (alimenta a barra de status) */
16
+ contextTokens = 0;
17
+ /** troca em runtime via `/model`; vale a partir da próxima requisição */
18
+ model;
19
+ lastTurn = null;
20
+ /** últimos turnos (o mais novo por último): o anterior dá contexto a correções do usuário */
21
+ recentTurns = [];
22
+ maxSteps;
23
+ log;
24
+ constructor(system, ctx, opts = {}) {
25
+ this.system = system;
26
+ this.ctx = ctx;
27
+ this.opts = opts;
28
+ this.model = opts.model ?? MODEL;
29
+ this.maxSteps = opts.maxSteps ?? 40;
30
+ this.log = opts.log ?? ((s) => ui.info(s));
31
+ }
32
+ /** Roda um turno completo (até não haver mais tool_use) e devolve o texto final. */
33
+ async send(userText, signal) {
34
+ try {
35
+ return await this.run(userText, signal);
36
+ }
37
+ catch (e) {
38
+ if (signal?.aborted) {
39
+ this.repairAfterAbort();
40
+ throw new Error("interrompido");
41
+ }
42
+ throw e;
43
+ }
44
+ }
45
+ /** Após Ctrl+C, evita deixar um tool_use sem tool_result no histórico (a API recusaria o próximo turno). */
46
+ repairAfterAbort() {
47
+ const last = this.messages[this.messages.length - 1];
48
+ if (last?.role !== "assistant" || typeof last.content === "string")
49
+ return;
50
+ const pending = last.content.filter((b) => b.type === "tool_use");
51
+ if (pending.length)
52
+ this.messages.push({ role: "user", content: pending.map((u) => ({ type: "tool_result", tool_use_id: u.id, content: "Interrompido pelo usuário.", is_error: true })) });
53
+ }
54
+ async run(userText, signal) {
55
+ const turn = { messages: [], toolCalls: 0, errors: 0, finished: false };
56
+ this.lastTurn = turn;
57
+ this.recentTurns = [...this.recentTurns, turn].slice(-3);
58
+ const push = (m) => { this.messages.push(m); turn.messages.push(m); };
59
+ push({ role: "user", content: userText });
60
+ await this.compactIfNeeded();
61
+ let finalText = "";
62
+ for (let step = 0; step < this.maxSteps; step++) {
63
+ if (signal?.aborted)
64
+ throw new Error("abort");
65
+ const stream = this.client.messages.stream({
66
+ model: this.model,
67
+ max_tokens: 8192,
68
+ system: [{ type: "text", text: typeof this.system === "function" ? this.system() : this.system, cache_control: { type: "ephemeral" } }],
69
+ tools: toApiTools(this.opts.tools),
70
+ messages: this.messages,
71
+ }, { signal });
72
+ if (!this.opts.quiet)
73
+ stream.on("text", (t) => ui.text(t));
74
+ const msg = await stream.finalMessage();
75
+ this.contextTokens = msg.usage.input_tokens + (msg.usage.cache_read_input_tokens ?? 0) + (msg.usage.cache_creation_input_tokens ?? 0);
76
+ this.usage.input += msg.usage.input_tokens + (msg.usage.cache_read_input_tokens ?? 0) + (msg.usage.cache_creation_input_tokens ?? 0);
77
+ this.usage.output += msg.usage.output_tokens;
78
+ this.usage.cacheRead += msg.usage.cache_read_input_tokens ?? 0;
79
+ this.usage.steps++;
80
+ push({ role: "assistant", content: msg.content });
81
+ const text = msg.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
82
+ if (text)
83
+ finalText = text;
84
+ if (!this.opts.quiet)
85
+ ui.textEnd();
86
+ const uses = msg.content.filter((b) => b.type === "tool_use");
87
+ if (msg.stop_reason !== "tool_use" || uses.length === 0) {
88
+ turn.finished = true;
89
+ return finalText;
90
+ }
91
+ const runOne = async (u) => {
92
+ const t0 = Date.now();
93
+ if (!this.opts.quiet)
94
+ ui.toolStart(u.id, u.name, u.input);
95
+ const denied = this.opts.tools && !this.opts.tools.has(u.name);
96
+ const r = denied ? { text: `Tool não permitida para este agente: ${u.name}`, isError: true } : await execTool(u.name, u.input, this.ctx);
97
+ if (this.opts.quiet)
98
+ this.log(ui.toolLine(u.name, u.input, Date.now() - t0, r.isError));
99
+ else
100
+ ui.toolEnd(u.id, r.isError);
101
+ return { type: "tool_result", tool_use_id: u.id, content: r.text.slice(0, 40_000), is_error: r.isError };
102
+ };
103
+ // Leituras puras rodam em paralelo; qualquer escrita/aprovação força execução em série.
104
+ const results = uses.every((u) => isConcurrent(u.name, u.input))
105
+ ? await Promise.all(uses.map(runOne))
106
+ : await uses.reduce(async (acc, u) => [...(await acc), await runOne(u)], Promise.resolve([]));
107
+ turn.toolCalls += results.length;
108
+ turn.errors += results.filter((r) => r.is_error).length;
109
+ push({ role: "user", content: results });
110
+ await this.compactIfNeeded();
111
+ }
112
+ ui.warn("Limite de passos atingido.");
113
+ return finalText || "(limite de passos atingido sem resposta final)";
114
+ }
115
+ async compactIfNeeded() {
116
+ if (estimateTokens(this.messages) < COMPACT_AT)
117
+ return;
118
+ this.messages = trimOldToolResults(this.messages);
119
+ if (estimateTokens(this.messages) < COMPACT_AT)
120
+ return;
121
+ this.log("compactando contexto…");
122
+ this.messages = await summarizeMiddle(this.client, this.model, this.messages);
123
+ this.contextTokens = estimateTokens(this.messages); // até a próxima chamada real, a barra não fica com o valor antigo
124
+ }
125
+ }
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Frase sobre o grafo para o system prompt. "0 símbolos" sem contexto faz o modelo concluir que o grafo é inútil, mas o
5
+ * projeto pode só estar vazio no início (e o grafo se atualiza sozinho a cada edição). Por isso: rótulo do momento da
6
+ * contagem e, quando vazio, a orientação de consultar mesmo assim.
7
+ * `start` = foto tirada na abertura da sessão (backend do claude: o servidor MCP atualiza o índice em outro processo);
8
+ * `now` = contagem atualizada após cada sincronização.
9
+ */
10
+ export function graphStatsLine(syms, notes, when = "now") {
11
+ if (syms === 0 && notes === 0) {
12
+ return `O grafo estava vazio ${when === "start" ? "na abertura da sessão" : "na última sincronização"} (projeto novo ou sem código suportado). Ele se atualiza sozinho conforme arquivos são criados e editados: consulte-o com graph_search em vez de supor que está vazio.`;
13
+ }
14
+ return `${when === "start" ? "Na abertura da sessão" : "Agora"}: ${syms} símbolos e ${notes} notas indexados. O grafo se atualiza sozinho após edições.`;
15
+ }
16
+ export function buildSystemPrompt(root, graphStats, skills = "") {
17
+ let projectDoc = "";
18
+ for (const f of ["BRITO.md", "CLAUDE.md", "AGENTS.md"]) {
19
+ const p = path.join(root, f);
20
+ if (fs.existsSync(p)) {
21
+ projectDoc = `\n\n# Instruções do projeto (${f})\n${fs.readFileSync(p, "utf8")}`;
22
+ break;
23
+ }
24
+ }
25
+ return `Você é Brito, um agente de engenharia de software que trabalha em CLI. Responda em português do Brasil, de forma direta.
26
+
27
+ # Como trabalhar
28
+ - Você tem um GRAFO DE CONHECIMENTO do projeto (símbolos, chamadas, imports) e uma MEMÓRIA em grafo (notas). ${graphStats}
29
+ - Para entender código: comece por graph_search / graph_context / graph_callers / graph_callees / graph_impact. Só leia arquivos (read_file) nas linhas que o grafo apontou. Use grep apenas para texto que não é símbolo.
30
+ - Antes de alterar uma função ou arquivo, rode graph_callers / graph_impact e considere o que pode quebrar.
31
+ - Após editar muitos arquivos, rode graph_index para atualizar o grafo.
32
+ - Ao tomar uma decisão não óbvia, descobrir um bug ou uma convenção, grave com remember (com refs para arquivos/símbolos). Consulte recall no início de tarefas relevantes.
33
+ - Edições e comandos shell pedem aprovação do usuário. Prefira edit_file a write_file. Verifique seu trabalho (typecheck/testes) via bash quando existir.
34
+ - Para investigações amplas ou sub-tarefas grandes, use delegate: o subagente trabalha em contexto isolado e devolve só o resultado. Vários delegate somente-leitura (explore/plan) no mesmo turno rodam em paralelo. Passe \`focus\` com os símbolos relevantes. Não delegue o que resolve com 1–2 chamadas de graph_*.
35
+ - SKILLS: procedimentos reutilizáveis aprendidos neste projeto. Se alguma se aplica à tarefa, chame skill_load ANTES de começar e siga-a. Se ela estiver errada ou incompleta, corrija com skill_save. Ao terminar tarefas complexas, o sistema pode propor novas skills; não crie skill para fatos de uma tarefa única.
36
+ - Seja conciso. Não repita conteúdo de arquivos na resposta.
37
+
38
+ # Ambiente
39
+ Diretório do projeto: ${root}
40
+ Data: ${new Date().toISOString().slice(0, 10)}${skills ? `\n\n# Skills disponíveis (use skill_load)\n${skills}` : ""}${projectDoc}`;
41
+ }
42
+ import { skillsIndex } from "../skills/store.js";
43
+ export function buildSubagentPrompt(root, def) {
44
+ return `${def.prompt}
45
+
46
+ # Regras de subagente
47
+ - Você foi chamado por outro agente e NÃO vê a conversa dele: tudo que precisa está na tarefa.
48
+ - Use o grafo antes de ler arquivos: graph_search, graph_context, graph_callers, graph_callees, graph_impact. Leia só as linhas que ele apontar.
49
+ - Sua última mensagem é a única coisa que o chamador recebe. Seja objetivo e use caminhos \`arquivo:linha\`; sem preâmbulo.
50
+
51
+ # Ambiente
52
+ Diretório do projeto: ${root}
53
+ Data: ${new Date().toISOString().slice(0, 10)}${skillsIndex(root) ? `\n\n# Skills disponíveis (use skill_load)\n${skillsIndex(root)}` : ""}`;
54
+ }