sabia 2.0.0-alpha.1 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compilar.mjs +242 -36
- package/package.json +1 -1
- package/sabia.mjs +147 -59
package/compilar.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Compilador as-built do Sabiá: fluxo/ + padrao/ + skills/ (a fonte, no framework) → skills comuns (SKILL.md +
|
|
2
|
-
// references/) numa pasta de staging, uma por alvo.
|
|
2
|
+
// references/) numa pasta de staging, uma por alvo.
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
@@ -22,11 +22,28 @@ export function partes(texto) {
|
|
|
22
22
|
export const slug = (titulo) => titulo.trim().toLowerCase().replace(/`/g, "").replace(/[^\p{L}\p{N} _-]/gu, "").replace(/ /g, "-");
|
|
23
23
|
const ancoras = (texto) => new Set([...texto.matchAll(/^#+ (.+)$/gm)].map((m) => slug(m[1])));
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
export const VIZINHOS = "## Entrada: vizinhos";
|
|
26
|
+
export const SPRINT = ["exige", "nao"];
|
|
27
|
+
|
|
28
|
+
/** As linhas "- <estado> → <destino>" da seção Entrada: vizinhos do corpo do mestre (null sem a seção). */
|
|
29
|
+
function vizinhos(corpo, pulo) {
|
|
30
|
+
const ls = corpo.split("\n"), i = ls.findIndex((l) => l.trim() === VIZINHOS);
|
|
31
|
+
if (i < 0) return null;
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let j = i + 1; j < ls.length && !/^#{1,6} /.test(ls[j]); j++) {
|
|
34
|
+
if (!ls[j].trim()) continue;
|
|
35
|
+
const t = ls[j].trim(), k = t.lastIndexOf(" → ");
|
|
36
|
+
out.push({ n: pulo + j + 1, ...(t.startsWith("- ") && k > 2 ? { estado: t.slice(2, k), destino: t.slice(k + 3) } : {}) });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Lê a fonte inteira: fluxos, passos, regras, padrões, travas, tipos e papéis. */
|
|
26
42
|
export function carregar(fw) {
|
|
27
|
-
const m = { fw, fluxos: {}, regras: {}, padroes: {}, travas: {}, subag: {}, skillsAgente: {} };
|
|
43
|
+
const m = { fw, fluxos: {}, regras: {}, padroes: {}, travas: {}, tipos: {}, subag: {}, skillsAgente: {} };
|
|
28
44
|
const dirFluxo = path.join(fw, "fluxo");
|
|
29
45
|
if (fs.existsSync(path.join(dirFluxo, "travas.json"))) m.travas = json(path.join(dirFluxo, "travas.json")).travas;
|
|
46
|
+
if (fs.existsSync(path.join(dirFluxo, "tipos.json"))) m.tipos = json(path.join(dirFluxo, "tipos.json")).tipos || {};
|
|
30
47
|
const papeis = path.join(fw, "skills/sa-setup/assets/agents/papeis.json");
|
|
31
48
|
if (fs.existsSync(papeis))
|
|
32
49
|
for (const [k, v] of Object.entries(json(papeis).papeis)) {
|
|
@@ -42,18 +59,83 @@ export function carregar(fw) {
|
|
|
42
59
|
if (!fs.existsSync(mestre)) continue;
|
|
43
60
|
const p = partes(ler(mestre));
|
|
44
61
|
const passos = (p.dados?.passos || []).map((x) => {
|
|
45
|
-
|
|
62
|
+
// passo compartilhado ({"de": "entrada"}): o corpo vem de fluxo/<de>.md, não de passos/
|
|
63
|
+
const arq = x.de ? path.join(dirFluxo, `${x.de}.md`) : path.join(ds, f, "passos", `${x.id}.md`);
|
|
46
64
|
return { ...x, arq, ...(fs.existsSync(arq) ? partes(ler(arq)) : { dados: null, corpo: "", pulo: 0 }) };
|
|
47
65
|
});
|
|
48
|
-
m.fluxos[`${s}.${f}`] = { skill: s, fluxo: f, arq: mestre, d: p.dados || {}, corpo: p.corpo, pulo: p.pulo, passos };
|
|
66
|
+
m.fluxos[`${s}.${f}`] = { skill: s, fluxo: f, arq: mestre, d: p.dados || {}, corpo: p.corpo, pulo: p.pulo, passos, vizinhos: vizinhos(p.corpo, p.pulo) };
|
|
49
67
|
}
|
|
50
68
|
}
|
|
51
69
|
const dirPadrao = path.join(fw, "padrao");
|
|
52
70
|
for (const s of fs.existsSync(dirPadrao) ? fs.readdirSync(dirPadrao).sort() : [])
|
|
53
71
|
m.padroes[s] = mds(path.join(dirPadrao, s)).map((n) => ({ nome: n, arq: path.join(dirPadrao, s, n), ...partes(ler(path.join(dirPadrao, s, n))) }));
|
|
72
|
+
// os passos de "desviavel" são o ponto de desvio por tipo (⇢): a skill de um toolkit escreve esse artefato
|
|
73
|
+
for (const [sg, t] of Object.entries(m.tipos))
|
|
74
|
+
if (!t.pendente && Array.isArray(t.desviavel)) for (const c of t.desviavel) { const r = resolverId(m, c); if (r?.passo) (r.passo.tipos ||= []).push(sg); }
|
|
54
75
|
return m;
|
|
55
76
|
}
|
|
56
77
|
|
|
78
|
+
/** a < b em versão semântica (os três números). */
|
|
79
|
+
function versaoMenor(a, b) {
|
|
80
|
+
const [x, y] = [a, b].map((v) => String(v).split(/[.-]/).slice(0, 3).map(Number));
|
|
81
|
+
const i = x.findIndex((n, k) => n !== y[k]);
|
|
82
|
+
return i >= 0 && x[i] < y[i];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** O arquivo `f` está dentro da pasta `raiz` (caminhos relativos ou absolutos)? */
|
|
86
|
+
// como o dentro() do gerador: relativo, com /, sem ~, \\, unidade nem ..
|
|
87
|
+
const relativo = (c) => typeof c === "string" && !/^[/~]|\\|^[A-Za-z]:/.test(c) && !c.split("/").includes("..");
|
|
88
|
+
const sob = (raiz, f) => path.resolve(f).startsWith(path.resolve(raiz) + path.sep);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Erros do toolkit na pasta `tk` contra o Sabiá `fw`: requer, tipos (o tipo vai para uma skill do toolkit, com o fluxo e
|
|
92
|
+
* o padrão do tipo, arquivos da pasta lidos pelo caminho) e rubricas. O Sabiá não compila conteúdo de toolkit.
|
|
93
|
+
*/
|
|
94
|
+
export function validarToolkitFonte(fw, tk) {
|
|
95
|
+
const m = carregar(fw), man = json(path.join(tk, "toolkit.json")), erros = [];
|
|
96
|
+
const versaoSabia = fs.existsSync(path.join(fw, "plugin.json")) ? json(path.join(fw, "plugin.json")).version : null;
|
|
97
|
+
if (man.requer?.sabia && versaoSabia && versaoMenor(versaoSabia, man.requer.sabia))
|
|
98
|
+
erros.push(`requer o Sabiá ${man.requer.sabia}; o instalado é o ${versaoSabia}`);
|
|
99
|
+
for (const [sg, q] of Object.entries(man.tipos || {})) {
|
|
100
|
+
const t = m.tipos[sg];
|
|
101
|
+
if (!t) erros.push(`tipo ${sg}: fora do catálogo do Sabiá (o toolkit não cria tipo)`);
|
|
102
|
+
else if (!t.desviavel?.length) erros.push(`tipo ${sg}: não é desviável no catálogo`);
|
|
103
|
+
else if (t.pendente) erros.push(`tipo ${sg}: pendente no Sabiá instalado (ainda sem o passo que o cria)`);
|
|
104
|
+
if (!q || typeof q !== "object") { erros.push(`tipo ${sg}: use {"skill", "fluxo", "padrao"} (a skill do toolkit; o fluxo e o padrão do tipo, opcionais)`); continue; }
|
|
105
|
+
const sk = (man.skills || []).find((s) => s.caminho === q.skill);
|
|
106
|
+
if (!sk) erros.push(`tipo ${sg}: a skill ${q.skill} não está em skills`);
|
|
107
|
+
// o gerador liga a skill ao dono de cada passo de desvio: sem o papel em "papeis", o toolkit não declara esse uso
|
|
108
|
+
else if (t?.desviavel) for (const c of t.desviavel) {
|
|
109
|
+
const papel = resolverId(m, c)?.fl.d.dono?.replace(/^sa-/, "");
|
|
110
|
+
if (papel && !(sk.papeis || []).includes(papel)) erros.push(`tipo ${sg}: a skill ${q.skill} não traz o papel ${papel} em papeis (dono do passo de desvio ${c})`);
|
|
111
|
+
}
|
|
112
|
+
for (const k of ["fluxo", "padrao"].filter((k) => k in q))
|
|
113
|
+
if (!relativo(q[k]) || !sob(tk, path.join(tk, q[k])) || !fs.statSync(path.join(tk, q[k]), { throwIfNoEntry: false })?.isFile()) erros.push(`tipo ${sg}: falta o ${k} ${q[k]} na pasta do toolkit`);
|
|
114
|
+
}
|
|
115
|
+
const doSabia = new Set([...(m.padroes["sa-revisar"]?.find((p) => p.nome === "rubricas.md")?.corpo || "").matchAll(/^\| ([A-Z]\d+) \|/gm)].map((r) => r[1]));
|
|
116
|
+
const ids = (man.rubricas || []).map((r) => r.id);
|
|
117
|
+
for (const r of man.rubricas || []) {
|
|
118
|
+
if (!/^[A-Z]+\d+$/.test(r.id ?? "")) erros.push(`rubrica ${r.id ?? "?"}: id como EX1 (maiúsculas e número)`);
|
|
119
|
+
else if (doSabia.has(r.id) || ids.filter((x) => x === r.id).length > 1) erros.push(`rubrica ${r.id}: repetida ou já do Sabiá (o toolkit só acrescenta itens)`);
|
|
120
|
+
if (!String(r.verificacao ?? "").trim()) erros.push(`rubrica ${r.id ?? "?"}: sem verificacao`);
|
|
121
|
+
if (!["BLOQUEANTE", "IMPORTANTE", "SUGESTÃO"].includes(r.severidade)) erros.push(`rubrica ${r.id ?? "?"}: severidade "${r.severidade ?? ""}" (use BLOQUEANTE, IMPORTANTE ou SUGESTÃO)`);
|
|
122
|
+
}
|
|
123
|
+
return erros;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** As siglas na ordem de publicação: cada uma depois das do seu "depois" (a ordem do catálogo desempata); null com ciclo. */
|
|
127
|
+
export function ordemTipos(tipos) {
|
|
128
|
+
const feitos = [], resto = Object.keys(tipos);
|
|
129
|
+
while (resto.length) {
|
|
130
|
+
const i = resto.findIndex((s) => (tipos[s].depois || []).every((d) => feitos.includes(d) || !tipos[d]));
|
|
131
|
+
if (i < 0) return null;
|
|
132
|
+
feitos.push(...resto.splice(i, 1));
|
|
133
|
+
}
|
|
134
|
+
return feitos;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const padraoExiste = (m, ref) => { const [s, n] = ref.split("/"); return !!m.padroes[s]?.some((p) => p.nome === n); };
|
|
138
|
+
|
|
57
139
|
const fluxosDa = (m, s) => Object.values(m.fluxos).filter((f) => f.skill === s);
|
|
58
140
|
|
|
59
141
|
/** "sa-x.f" ou "sa-x.f.p" existe? Devolve o fluxo e o passo (ou null). */
|
|
@@ -75,7 +157,7 @@ function modoChamada(m, dono, alvo) {
|
|
|
75
157
|
|
|
76
158
|
// ---------------------------------------------------------------- validação
|
|
77
159
|
|
|
78
|
-
/** Erros "arquivo:linha: mensagem" da fonte
|
|
160
|
+
/** Erros "arquivo:linha: mensagem" da fonte. Lista vazia = pronta para compilar. */
|
|
79
161
|
export function validar(m) {
|
|
80
162
|
const erros = [];
|
|
81
163
|
const rel = (f) => path.relative(m.fw, f);
|
|
@@ -93,7 +175,28 @@ export function validar(m) {
|
|
|
93
175
|
? mds(path.join(path.dirname(fl.arq), "passos")).map((f) => f.slice(0, -3)).filter((x) => !ids.includes(x)) : [];
|
|
94
176
|
for (const o of orfaos) e(fl.arq, 2, `passo ${o} órfão (tem arquivo, não está no mestre)`);
|
|
95
177
|
for (const t of d.travas_gerais || []) if (!m.travas[t]) e(fl.arq, 2, `trava ${t} fora do catálogo`);
|
|
178
|
+
if (fl.passos.some((x) => x.de) && !fl.vizinhos?.length)
|
|
179
|
+
e(fl.arq, 2, `passo entrada sem a seção "${VIZINHOS}" no corpo do mestre (uma linha "- <estado> → <dona>" por vizinho)`);
|
|
180
|
+
for (const v of fl.vizinhos || []) {
|
|
181
|
+
const alvos = v.destino ? [...v.destino.matchAll(ID)].map((r) => r[1]) : [];
|
|
182
|
+
if (!alvos.length && !v.destino?.endsWith("■")) { e(fl.arq, v.n, 'vizinho fora do formato "- <estado> → `sa-x.f[.p]`" ou "- <estado> → <frase> ■"'); continue; }
|
|
183
|
+
for (const q2 of alvos) {
|
|
184
|
+
const r = resolverId(m, q2);
|
|
185
|
+
if (!r) continue; // o id inexistente já sai na varredura do texto
|
|
186
|
+
if (r.fl === fl) e(fl.arq, v.n, `vizinho → ${q2}: é deste fluxo → declare como saída do passo entrada ("saidas" no mestre)`);
|
|
187
|
+
else if (!m.subag[r.fl.d.dono]) e(fl.arq, v.n, `vizinho → ${q2}: o dono ${r.fl.d.dono} não é agente do papeis.json`);
|
|
188
|
+
else if (r.passo && r.fl.d.dono !== d.dono) e(fl.arq, v.n, `vizinho → ${q2}: passo de outro agente (${r.fl.d.dono}) → use o fluxo (${r.fl.skill}.${r.fl.fluxo})`);
|
|
189
|
+
else if (r.fl.d.dono !== d.dono && !(v.estado + " → " + v.destino).includes("agente `" + r.fl.d.dono + "`")) e(fl.arq, v.n, `vizinho → ${q2}: dono ${r.fl.d.dono} é outro agente → acrescente "(agente \`${r.fl.d.dono}\`)" na linha`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
96
192
|
for (const x of fl.passos) {
|
|
193
|
+
if (x.id === "grafo") e(fl.arq, 2, "passo grafo: id reservado (o compilador gera references/<fluxo>/grafo.md)");
|
|
194
|
+
if (x.id === "entrada" && !x.de) e(fl.arq, 2, 'passo entrada: id reservado ao passo compartilhado → {"id": "entrada", "de": "entrada", "sprint": "exige|nao"}');
|
|
195
|
+
if (x.de) {
|
|
196
|
+
if (!SPRINT.includes(x.sprint)) e(fl.arq, 2, `passo ${x.id}: sprint "${x.sprint ?? ""}" (use ${SPRINT.join(" ou ")})`);
|
|
197
|
+
if (fs.existsSync(path.join(path.dirname(fl.arq), "passos", `${x.id}.md`))) e(fl.arq, 2, `passo ${x.id}: vem de fluxo/${x.de}.md; apague passos/${x.id}.md`);
|
|
198
|
+
if (!fs.existsSync(x.arq)) { e(fl.arq, 2, `passo ${x.id}: de "${x.de}" sem fluxo/${x.de}.md`); continue; }
|
|
199
|
+
}
|
|
97
200
|
if (!x.dados) { e(x.arq, 1, `passo ${x.id} sem arquivo ou sem front-matter`); continue; }
|
|
98
201
|
if (!x.dados.resumo) e(x.arq, 2, "passo sem resumo");
|
|
99
202
|
for (const sec of ["## Entrada", "## Faça", "## Saída", "## Pare se"]) if (!x.corpo.includes(sec)) e(x.arq, 1, `falta ${sec}`);
|
|
@@ -104,12 +207,38 @@ export function validar(m) {
|
|
|
104
207
|
else if (!modoChamada(m, d.dono, r.fl)) e(fl.arq, 2, `passo ${x.id}: chama ${c} (dono ${r.fl.d.dono}), mas ${d.dono} não tem a skill nem esse subagente → use passagem (→)`);
|
|
105
208
|
}
|
|
106
209
|
for (const [cond, dest] of Object.entries(x.saidas || {})) {
|
|
107
|
-
|
|
210
|
+
const r = dest.startsWith("→ ") ? resolverId(m, dest.slice(2).trim()) : null;
|
|
211
|
+
if (r?.passo && r.fl.d.dono !== d.dono)
|
|
212
|
+
e(fl.arq, 2, `passo ${x.id}: saída "${cond}" passa ao passo ${dest.slice(2).trim()}, de outro agente (${r.fl.d.dono}) → use o fluxo (→ ${r.fl.skill}.${r.fl.fluxo})`);
|
|
213
|
+
if (dest === "fim" || ids.includes(dest) || r) continue;
|
|
108
214
|
e(fl.arq, 2, `passo ${x.id}: saída "${cond}" → "${dest}" (use id do fluxo, fim ou "→ skill.fluxo[.passo]" existente)`);
|
|
109
215
|
}
|
|
110
216
|
}
|
|
111
217
|
}
|
|
112
218
|
|
|
219
|
+
const arqTipos = path.join(m.fw, "fluxo", "tipos.json");
|
|
220
|
+
const rubrica = new Set([...(m.padroes["sa-revisar"]?.find((p) => p.nome === "rubricas.md")?.corpo || "").matchAll(/^\| ([A-Z]\d+) \|/gm)].map((r) => r[1]));
|
|
221
|
+
let forma = true;
|
|
222
|
+
for (const [sg, t] of Object.entries(m.tipos)) {
|
|
223
|
+
const naoLista = ["cria", "desviavel", "depois", "embute", "rubrica"].filter((k) => t[k] != null && !Array.isArray(t[k]));
|
|
224
|
+
for (const k of naoLista) e(arqTipos, 1, `tipo ${sg}: ${k} tem de ser lista`);
|
|
225
|
+
if (naoLista.length) { forma = false; continue; }
|
|
226
|
+
if (!/^[A-Z]{2,4}$/.test(sg) || !t.nome) e(arqTipos, 1, `tipo ${sg}: sigla de 2 a 4 maiúsculas e nome`);
|
|
227
|
+
// sigla única: o JSON.parse junta chaves repetidas; a conferência é no texto
|
|
228
|
+
else if ((ler(arqTipos).match(new RegExp(`"${sg}"\\s*:\\s*\\{`, "g")) || []).length > 1) e(arqTipos, 1, `tipo ${sg}: sigla repetida`);
|
|
229
|
+
if (!["DOORS", "ETM"].includes(t.elm)) e(arqTipos, 1, `tipo ${sg}: elm "${t.elm ?? ""}" (use DOORS ou ETM)`);
|
|
230
|
+
if (!t.cria?.length) e(arqTipos, 1, `tipo ${sg}: sem cria`);
|
|
231
|
+
for (const c of t.desviavel || []) if (!t.cria?.includes(c)) e(arqTipos, 1, `tipo ${sg}: desviavel ${c} fora de cria`);
|
|
232
|
+
// tipo pendente: declarado antes do padrão e do fluxo que o escrevem
|
|
233
|
+
for (const c of t.pendente ? [] : t.cria || []) if (!resolverId(m, c)?.passo) e(arqTipos, 1, `tipo ${sg}: cria ${c} não é passo de fluxo`);
|
|
234
|
+
if (t.padrao && !t.pendente && !padraoExiste(m, t.padrao)) e(arqTipos, 1, `tipo ${sg}: padrão ${t.padrao} inexistente`);
|
|
235
|
+
for (const d of [...(t.depois || []), ...(t.embute || [])]) if (!m.tipos[d]) e(arqTipos, 1, `tipo ${sg}: ${d} fora do catálogo`);
|
|
236
|
+
for (const r of t.pendente ? [] : t.rubrica || []) if (!rubrica.has(r)) e(arqTipos, 1, `tipo ${sg}: rubrica ${r} fora de rubricas.md`);
|
|
237
|
+
if (t.pendente && t.cria?.length && t.cria.every((c) => resolverId(m, c)?.passo) && (!t.padrao || padraoExiste(m, t.padrao)))
|
|
238
|
+
e(arqTipos, 1, `tipo ${sg}: cria e padrão já existem; tire o "pendente"`);
|
|
239
|
+
}
|
|
240
|
+
if (forma && !ordemTipos(m.tipos)) e(arqTipos, 1, `ciclo no "depois" dos tipos`);
|
|
241
|
+
|
|
113
242
|
const textos = [];
|
|
114
243
|
for (const fl of Object.values(m.fluxos)) {
|
|
115
244
|
textos.push({ arq: fl.arq, corpo: fl.corpo, pulo: fl.pulo, skill: fl.skill, fl });
|
|
@@ -161,7 +290,7 @@ export function validar(m) {
|
|
|
161
290
|
if (/`sa-[a-z]+\/[a-z-]+\/passos\/|`sa-[a-z]+\/regras\.md`/.test(linha)) e(t.arq, n, "caminho de fluxo/ em code-span → id do passo");
|
|
162
291
|
});
|
|
163
292
|
}
|
|
164
|
-
return erros;
|
|
293
|
+
return [...new Set(erros)]; // o passo compartilhado repete os mesmos erros em cada fluxo
|
|
165
294
|
}
|
|
166
295
|
|
|
167
296
|
// ---------------------------------------------------------------- reescrita
|
|
@@ -178,7 +307,8 @@ export function mapear(fw, src) {
|
|
|
178
307
|
}
|
|
179
308
|
|
|
180
309
|
/** Reescreve links e caminhos do framework para o caminho absoluto do alvo (`base`) e liga ids e padrões. */
|
|
181
|
-
export function reescrever(texto, origem,
|
|
310
|
+
export function reescrever(texto, origem, ctx) {
|
|
311
|
+
const { fw, base, m } = ctx;
|
|
182
312
|
const destino = (alvo) => {
|
|
183
313
|
const [cam, anc = ""] = alvo.split("#");
|
|
184
314
|
let src;
|
|
@@ -232,13 +362,57 @@ const destinoId = (base, r) => (r.passo ? `${base}/${r.fl.skill}/references/${r.
|
|
|
232
362
|
const cel = (t) => String(t).replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
233
363
|
const italico = (resumo) => (resumo ? `_${resumo}_\n\n` : "");
|
|
234
364
|
|
|
365
|
+
export const LEGENDA = "↓ segue · ↑ volta · ↺ repete · ⇄ chama · ⇒ passa a (o fluxo termina) · ↪ vizinhos da entrada (diga a dona, ou a frase da linha ■, e pare) · ■ fim · 🔒 trava · ⇢ passo com desvio de toolkit (pelo para e arquivos, ou pelo tipo que o passo cria)";
|
|
366
|
+
|
|
367
|
+
/** O passo aceita desvio de toolkit: pelo para e arquivos ("desvio": true) ou pelo tipo do catálogo que ele cria. */
|
|
368
|
+
export const temDesvio = (x) => !!(x.desvio || x.tipos?.length);
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* O grafo ASCII do fluxo `fl` (de carregar): uma caixa por passo de `passos` (o rota passa os resolvidos no projeto) e, à
|
|
372
|
+
* direita, chama, travas e saídas. `notas(x)` acrescenta linhas no topo das anotações (origem, desvio ligado).
|
|
373
|
+
*/
|
|
374
|
+
export function grafoAscii(fl, passos = fl.passos, { notas = () => [] } = {}) {
|
|
375
|
+
const ids = passos.map((x) => x.id), out = [];
|
|
376
|
+
const rot = (x, i) => `${i + 1} ${x.id}${x.sprint ? ` [${x.sprint}]` : ""}${temDesvio(x) ? " ⇢" : ""}`;
|
|
377
|
+
// 26 colunas cabem os ids do Sabiá; um id longo inserido pelo projeto alarga a caixa (par, por causa do ┬ central)
|
|
378
|
+
const W = 2 * Math.ceil(Math.max(26, ...passos.map((x, i) => rot(x, i).length + 4)) / 2);
|
|
379
|
+
const cond = (c) => (c.length > 60 ? c.slice(0, 57) + "…" : c);
|
|
380
|
+
out.push(`${fl.skill}.${fl.fluxo} dono ${fl.d.dono} travas ${(fl.d.travas_gerais || []).join(", ") || "—"}`, "");
|
|
381
|
+
passos.forEach((x, i) => {
|
|
382
|
+
const lado = [...notas(x)];
|
|
383
|
+
// os vizinhos numa linha só: a entrada diz a dona e para, não é passagem de trabalho (⇒)
|
|
384
|
+
const viz = x.de ? (fl.vizinhos || []).filter((v) => v.destino).map((v) => [...v.destino.matchAll(ID)][0]?.[1] || `■ ${cond(v.estado.replace(/`/g, ""))}`) : [];
|
|
385
|
+
if (viz.length) lado.push(`↪ vizinhos (diga e pare): ${[...new Set(viz)].join(" · ")}`);
|
|
386
|
+
if (x.tipos?.length) lado.push(`⇢ por tipo: ${x.tipos.join(", ")}`);
|
|
387
|
+
if (x.chama) lado.push(`⇄ chama ${[].concat(x.chama).join(", ")}`);
|
|
388
|
+
if (x.travas?.length) lado.push(`🔒 ${x.travas.join(", ")}`);
|
|
389
|
+
for (const [c, dest] of Object.entries(x.saidas || {})) {
|
|
390
|
+
const j = ids.indexOf(dest);
|
|
391
|
+
const seta = dest.startsWith("→ ") ? `⇒ passa a ${dest.slice(2).trim()}` : dest === "fim" ? "■ fim" : dest === x.id ? `↺ ${dest}` : j >= 0 && j < i ? `↑ volta a ${dest}` : `↓ ${dest}`;
|
|
392
|
+
lado.push(`${seta} (${cond(c)})`);
|
|
393
|
+
}
|
|
394
|
+
const ultimo = i === passos.length - 1;
|
|
395
|
+
out.push(` ┌${"─".repeat(W - 2)}┐`);
|
|
396
|
+
out.push(` │ ${rot(x, i).padEnd(W - 4)} │${lado[0] ? `──▶ ${lado[0]}` : ""}`);
|
|
397
|
+
out.push((ultimo ? ` └${"─".repeat(W - 2)}┘` : ` └${"─".repeat(W / 2 - 2)}┬${"─".repeat(W / 2 - 1)}┘`) + (lado[1] ? ` ${lado[1]}` : ""));
|
|
398
|
+
for (const l of lado.slice(2)) out.push(`${" ".repeat(W + 6)}${l}`);
|
|
399
|
+
if (!ultimo) out.push(` ${" ".repeat(W / 2 - 1)}▼`);
|
|
400
|
+
});
|
|
401
|
+
return out.join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const grafoMd = (fl) => [`# Grafo de ${fl.skill}.${fl.fluxo}`, "", "```", grafoAscii(fl), "```", "", LEGENDA, ""].join("\n");
|
|
405
|
+
|
|
235
406
|
const NO_PROJETO = `## No projeto
|
|
236
407
|
|
|
237
408
|
Antes de ler o arquivo de um passo (\`…/skills/<skill>/references/<fluxo>/<passo>.md\`, de qualquer skill), tente abrir
|
|
238
409
|
direto \`.sabia/fluxo/<skill>/<fluxo>/passos/<passo>.md\` na raiz do projeto: se existir, vale ele; senão, o da skill.
|
|
239
410
|
O mesmo para um padrão: \`.sabia/padrao/<skill>/<nome>.md\` no lugar de \`…/references/padrao/<nome>.md\`. Abra pelo
|
|
240
411
|
caminho; não liste a pasta (a busca não vê pastas ocultas). Com agente do Sabiá, o prompt lista também os passos inseridos
|
|
241
|
-
e removidos nos fluxos de que ele é dono.
|
|
412
|
+
e removidos nos fluxos de que ele é dono. Num passo ⇢, o desvio de toolkit sai pela skill dele: o agente dono a tem no
|
|
413
|
+
prompt; sem agente, valem a seção **Desvios de toolkit** (desvio por passo) e a **Tipos com skill de toolkit** (desvio por
|
|
414
|
+
tipo, com a skill, o fluxo e o padrão do tipo, arquivos do toolkit lidos pelo caminho, e os itens de rubrica que ele
|
|
415
|
+
acrescenta à revisão) do \`.sabia/sabia.md\`.
|
|
242
416
|
`;
|
|
243
417
|
|
|
244
418
|
/** O SKILL.md compilado de uma skill de fluxo. */
|
|
@@ -257,8 +431,9 @@ function skillMd(m, s, fmYaml, h1, ctx) {
|
|
|
257
431
|
const { titulo, resto } = semH1(fl.corpo);
|
|
258
432
|
out.push(`## Fluxo ${fl.fluxo}`, "");
|
|
259
433
|
if (titulo) out.push(`**${titulo}**`, "");
|
|
260
|
-
out.push(`Agente: \`${fl.d.dono}\`. Sem ele, sugira à pessoa usá-lo; seguindo aqui, os passos inseridos
|
|
261
|
-
"desvios de toolkit (⇢)
|
|
434
|
+
out.push(`Agente: \`${fl.d.dono}\`. Sem ele, sugira à pessoa usá-lo; seguindo aqui, os passos inseridos e removidos pelo`,
|
|
435
|
+
"projeto não entram, e os desvios de toolkit (⇢) valem pelas seções **Desvios de toolkit** (desvio por passo) e",
|
|
436
|
+
"**Tipos com skill de toolkit** (desvio por tipo) do `.sabia/sabia.md`.", "");
|
|
262
437
|
if (resto.trim()) out.push(rebaixar(reescrever(resto, fl.arq, ctx), 1).trimEnd(), "");
|
|
263
438
|
out.push("| # | Passo | Arquivo | Chama | Travas | Saídas |", "|---|---|---|---|---|---|");
|
|
264
439
|
(fl.d.travas_gerais || []).forEach((t) => usadas.add(t));
|
|
@@ -279,10 +454,11 @@ function skillMd(m, s, fmYaml, h1, ctx) {
|
|
|
279
454
|
const alvo = destinoId(base, r);
|
|
280
455
|
return `${cond} → passa a [\`${q}\`](${alvo})`;
|
|
281
456
|
}).join("<br>") || "—";
|
|
282
|
-
out.push(`| ${i + 1} | \`${x.id}\`${x.
|
|
457
|
+
out.push(`| ${i + 1} | \`${x.id}\`${temDesvio(x) ? ` ⇢${x.tipos?.length ? ` (${x.tipos.join(", ")})` : ""}` : ""} | [${x.id}.md](${arq}) | ${cel(chama)} | ${cel((x.travas || []).join(", ") || "—")} | ${cel(saidas)} |`);
|
|
283
458
|
});
|
|
284
|
-
out.push("",
|
|
285
|
-
|
|
459
|
+
out.push("", `Grafo: [grafo.md](${base}/${s}/references/${fl.fluxo}/grafo.md)`);
|
|
460
|
+
out.push("", "Ao começar um passo, leia o arquivo dele (não os dos outros passos) e o que ele mandar ler. Nenhuma Saída casou (ou o passo não tem Saídas) → o",
|
|
461
|
+
`próximo da lista; depois do último passo, o fluxo termina. Travas do fluxo: ${(fl.d.travas_gerais || []).map((t) => `\`${t}\``).join(", ") || "—"}.`, "");
|
|
286
462
|
}
|
|
287
463
|
const ordem = Object.keys(m.travas).filter((t) => usadas.has(t));
|
|
288
464
|
if (ordem.length) out.push("## Travas", "", "Valem sobre o corpo de qualquer passo.", "", ...ordem.map((t) => `- **${t}** — ${m.travas[t].texto}`), "");
|
|
@@ -298,6 +474,28 @@ function skillMd(m, s, fmYaml, h1, ctx) {
|
|
|
298
474
|
return out.join("\n");
|
|
299
475
|
}
|
|
300
476
|
|
|
477
|
+
/** references/tipos.md da skill sabia: o catálogo de fluxo/tipos.json em tabela, com a ordem de publicação. */
|
|
478
|
+
function tiposMd(m, ctx) {
|
|
479
|
+
const { base } = ctx;
|
|
480
|
+
const ord = ordemTipos(m.tipos), sim = (b) => (b ? "sim" : "não"), lista = (xs) => (xs?.length ? xs.join(", ") : "—");
|
|
481
|
+
const padrao = (t) => !t.padrao ? "— (corpo no passo que cria)" : t.pendente ? `\`${t.padrao}\`` : `[\`${t.padrao}\`](${base}/${t.padrao.replace("/", "/references/padrao/")})`;
|
|
482
|
+
const out = ["# Tipos de artefato", "",
|
|
483
|
+
"Catálogo fechado: todo artefato da especificação e do ELM é de um destes tipos, e um toolkit não cria tipo. A coluna",
|
|
484
|
+
"Desviável diz os passos de Cria que são o ponto de desvio (⇢): neles, o toolkit que traz uma skill com o fluxo e o",
|
|
485
|
+
"padrão do tipo (`tipos` no `toolkit.json`) escreve esse artefato; nos outros passos de Cria, o tipo é sempre do Sabiá. O envelope OKF de todos",
|
|
486
|
+
`é o do [\`okf-elm.md\`](${base}/sa-prd/references/okf-elm.md) da \`sa-prd\`; o corpo é o do padrão do tipo. Tipo **pendente** está declarado, mas ainda não tem padrão nem fluxo: não é criado nem publicado.`, "",
|
|
487
|
+
"| Sigla | Nome | Arquivo | Cria | Padrão | ELM | Requisito | Desviável | Publica depois de | Embute | Rubrica |",
|
|
488
|
+
"|---|---|---|---|---|---|---|---|---|---|---|"];
|
|
489
|
+
for (const sg of Object.keys(m.tipos)) {
|
|
490
|
+
const t = m.tipos[sg];
|
|
491
|
+
out.push(`| \`${sg}\`${t.pendente ? " (pendente)" : ""} | ${cel(t.nome)} | \`${cel(t.arquivo || "—")}\` | ${t.cria.map((c) => `\`${c}\``).join("<br>")} | ${padrao(t)} | ${t.elm} | ${sim(t.requisito)} | ${t.desviavel?.length ? t.desviavel.map((c) => `\`${c}\``).join("<br>") : "não"} | ${lista(t.depois)} | ${lista(t.embute)} | ${lista(t.rubrica)} |`);
|
|
492
|
+
}
|
|
493
|
+
const pub = ord.filter((sg) => m.tipos[sg].requisito && !m.tipos[sg].pendente);
|
|
494
|
+
out.push("", `**Ordem de publicação dos requisitos** (coluna Requisito, de baixo para cima, para cada um nascer com os ids que cita): ${pub.map((sg) => `\`${sg}\``).join(" → ")}.`,
|
|
495
|
+
"Os tipos de Embute entram em embed (`embutir`) no texto de quem os cita; os demais links ficam hyperlink.", "");
|
|
496
|
+
return reescrever(out.join("\n"), path.join(m.fw, "skills/sabia/references/tipos.md"), ctx);
|
|
497
|
+
}
|
|
498
|
+
|
|
301
499
|
const comVersao = (fmYaml, versao) => (versao ? fmYaml.replace(/^(\s+version:\s*)"[^"]*"/m, `$1"${versao}"`) : fmYaml);
|
|
302
500
|
|
|
303
501
|
function copiarArvore(origem, destino, transformar) {
|
|
@@ -334,6 +532,31 @@ function conferirSaida(staging, base) {
|
|
|
334
532
|
return erros;
|
|
335
533
|
}
|
|
336
534
|
|
|
535
|
+
/** Uma skill compilada em `out`: a árvore com os links reescritos e, se tem fluxo, o SKILL.md, os passos e o grafo. */
|
|
536
|
+
function escreverSkill(m, s, out, ctx, versao = null) {
|
|
537
|
+
const { fw, base } = ctx, dir = path.join(fw, "skills", s);
|
|
538
|
+
copiarArvore(dir, out, (t, o) => reescrever(t, o, ctx));
|
|
539
|
+
const fonte = ler(path.join(dir, "SKILL.md"));
|
|
540
|
+
const fmYaml = (fonte.match(FM) || [""])[0];
|
|
541
|
+
if (fluxosDa(m, s).length) {
|
|
542
|
+
const h1 = (fonte.slice(fmYaml.length).match(/^# .+$/m) || [`# ${s}`])[0];
|
|
543
|
+
fs.writeFileSync(path.join(out, "SKILL.md"), skillMd(m, s, comVersao(fmYaml, versao), h1, ctx));
|
|
544
|
+
for (const fl of fluxosDa(m, s)) {
|
|
545
|
+
fs.mkdirSync(path.join(out, "references", fl.fluxo), { recursive: true });
|
|
546
|
+
fs.writeFileSync(path.join(out, "references", fl.fluxo, "grafo.md"), grafoMd(fl));
|
|
547
|
+
const linhaSprint = (x) => (x.de ? `Sprint deste fluxo: \`${x.sprint}\`. Vizinhos: a seção Entrada: vizinhos do [Fluxo ${fl.fluxo}](${base}/${s}/SKILL.md#${slug(`Fluxo ${fl.fluxo}`)}).\n\n` : "");
|
|
548
|
+
for (const x of fl.passos)
|
|
549
|
+
fs.writeFileSync(path.join(out, "references", fl.fluxo, `${x.id}.md`), italico(x.dados.resumo) + linhaSprint(x) + reescrever(x.corpo, x.arq, ctx).replace(/^\n+/, ""));
|
|
550
|
+
}
|
|
551
|
+
} else if (fmYaml) {
|
|
552
|
+
fs.writeFileSync(path.join(out, "SKILL.md"), comVersao(fmYaml, versao) + reescrever(fonte.slice(fmYaml.length), path.join(dir, "SKILL.md"), ctx));
|
|
553
|
+
}
|
|
554
|
+
for (const p of m.padroes[s] || []) {
|
|
555
|
+
fs.mkdirSync(path.join(out, "references", "padrao"), { recursive: true });
|
|
556
|
+
fs.writeFileSync(path.join(out, "references", "padrao", p.nome), italico(p.dados?.resumo) + reescrever(p.corpo, p.arq, ctx).replace(/^\n+/, ""));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
337
560
|
/**
|
|
338
561
|
* Compila o framework `fw` no `staging` (recriado do zero): staging/skills/<s>/… e, com `raiz`, plugin.json, mcp.json,
|
|
339
562
|
* README.md e LICENSE (o power do Kiro). `base` é o caminho absoluto com ~ onde as skills ficam no alvo.
|
|
@@ -344,27 +567,10 @@ export function compilar(fw, staging, { base, raiz = false, versao = null } = {}
|
|
|
344
567
|
if (erros.length) throw new Error(`a fonte do Sabiá tem ${erros.length} erro(s):\n${erros.slice(0, 40).join("\n")}`);
|
|
345
568
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
346
569
|
const ctx = { fw, base, m };
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const fonte = ler(path.join(dir, "SKILL.md"));
|
|
352
|
-
const fmYaml = (fonte.match(FM) || [""])[0];
|
|
353
|
-
if (comFluxo.has(s)) {
|
|
354
|
-
const h1 = (fonte.slice(fmYaml.length).match(/^# .+$/m) || [`# ${s}`])[0];
|
|
355
|
-
fs.writeFileSync(path.join(out, "SKILL.md"), skillMd(m, s, comVersao(fmYaml, versao), h1, ctx));
|
|
356
|
-
for (const fl of fluxosDa(m, s))
|
|
357
|
-
for (const x of fl.passos) {
|
|
358
|
-
fs.mkdirSync(path.join(out, "references", fl.fluxo), { recursive: true });
|
|
359
|
-
fs.writeFileSync(path.join(out, "references", fl.fluxo, `${x.id}.md`), italico(x.dados.resumo) + reescrever(x.corpo, x.arq, ctx).replace(/^\n+/, ""));
|
|
360
|
-
}
|
|
361
|
-
} else if (fmYaml) {
|
|
362
|
-
fs.writeFileSync(path.join(out, "SKILL.md"), comVersao(fmYaml, versao) + reescrever(fonte.slice(fmYaml.length), path.join(dir, "SKILL.md"), ctx));
|
|
363
|
-
}
|
|
364
|
-
for (const p of m.padroes[s] || []) {
|
|
365
|
-
fs.mkdirSync(path.join(out, "references", "padrao"), { recursive: true });
|
|
366
|
-
fs.writeFileSync(path.join(out, "references", "padrao", p.nome), italico(p.dados?.resumo) + reescrever(p.corpo, p.arq, ctx).replace(/^\n+/, ""));
|
|
367
|
-
}
|
|
570
|
+
for (const s of skillsDe(fw)) escreverSkill(m, s, path.join(staging, "skills", s), ctx, versao);
|
|
571
|
+
if (Object.keys(m.tipos).length && fs.existsSync(path.join(staging, "skills/sabia"))) {
|
|
572
|
+
fs.mkdirSync(path.join(staging, "skills/sabia/references"), { recursive: true });
|
|
573
|
+
fs.writeFileSync(path.join(staging, "skills/sabia/references/tipos.md"), tiposMd(m, ctx));
|
|
368
574
|
}
|
|
369
575
|
if (raiz) for (const f of ["plugin.json", "mcp.json", "README.md", "LICENSE"]) if (fs.existsSync(path.join(fw, f))) fs.copyFileSync(path.join(fw, f), path.join(staging, f));
|
|
370
576
|
const ruins = conferirSaida(staging, base);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sabia",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
4
|
"description": "Instala, atualiza e desinstala o Sabiá e os toolkits dele no Kiro e no opencode, com um inventário em ~/.sabia/, e explica a rota dos fluxos (sabia rota).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/sabia.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import readline from "node:readline/promises";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { carregar, compilar, partes } from "./compilar.mjs";
|
|
13
|
+
import { carregar, compilar, grafoAscii, LEGENDA, partes, temDesvio, validarToolkitFonte } from "./compilar.mjs";
|
|
14
14
|
|
|
15
15
|
const VERSAO = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
16
16
|
const REPO = "https://github.com/dxaraujo/sabia.git";
|
|
@@ -193,13 +193,31 @@ export function separarRef(fonte) {
|
|
|
193
193
|
* Baixa `url` (ou copia a pasta local) na `ref`, confere o `sha` e devolve { pasta, sha }. Clona com o git da
|
|
194
194
|
* pessoa (credencial, SSH, CA), sem submódulos; nunca guarda token nem executa nada do que baixou.
|
|
195
195
|
*/
|
|
196
|
+
// as cópias baixadas são lidas e copiadas para ~/.sabia durante o comando; somem quando o processo termina
|
|
197
|
+
const temporarios = new Set();
|
|
198
|
+
process.on("exit", () => { for (const t of temporarios) fs.rmSync(t, { recursive: true, force: true }); });
|
|
199
|
+
|
|
196
200
|
export function baixar(url, { ref = null, sha = null, subpasta = null } = {}) {
|
|
197
201
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "sabia-"));
|
|
202
|
+
temporarios.add(tmp);
|
|
198
203
|
const repo = path.join(tmp, "repo");
|
|
199
204
|
let atual = null;
|
|
200
205
|
if (fs.existsSync(url) && fs.statSync(url).isDirectory()) {
|
|
201
|
-
|
|
202
|
-
if (
|
|
206
|
+
const topo = ctx.exec("git", ["rev-parse", "--show-toplevel"], { cwd: url });
|
|
207
|
+
if (ref) {
|
|
208
|
+
// com @ref, a pasta vem do commit (git archive), não da árvore de trabalho; ref inexistente é recusada
|
|
209
|
+
if (topo.status !== 0) throw new Error(`${url}: @${ref} pede uma pasta dentro de um repositório git`);
|
|
210
|
+
atual = git(["rev-parse", "--verify", `${ref}^{commit}`], url);
|
|
211
|
+
const rel = path.relative(fs.realpathSync(topo.stdout.trim()), fs.realpathSync(url)).split(path.sep).join("/");
|
|
212
|
+
const tar = path.join(tmp, "a.tar");
|
|
213
|
+
git(["archive", "--format=tar", "-o", tar, rel ? `${atual}:${rel}` : atual], url);
|
|
214
|
+
fs.mkdirSync(repo);
|
|
215
|
+
const r = ctx.exec("tar", ["-xf", tar, "-C", repo]);
|
|
216
|
+
if (r.status !== 0) throw new Error(`tar: ${(r.stderr || "").trim()}`);
|
|
217
|
+
} else {
|
|
218
|
+
fs.cpSync(url, repo, { recursive: true, filter: (f) => path.basename(f) !== ".git" });
|
|
219
|
+
if (topo.status === 0) atual = git(["rev-parse", "HEAD"], url);
|
|
220
|
+
}
|
|
203
221
|
} else {
|
|
204
222
|
git(["clone", "--quiet", "--depth", "1", "--no-recurse-submodules", ...(ref ? ["--branch", ref] : []), url, repo]);
|
|
205
223
|
atual = git(["rev-parse", "HEAD"], repo);
|
|
@@ -290,11 +308,13 @@ async function registrarMcps(fw, reg) {
|
|
|
290
308
|
}
|
|
291
309
|
}
|
|
292
310
|
|
|
293
|
-
/**
|
|
311
|
+
/** Onde as skills do Sabiá ficam no alvo (caminho com ~). */
|
|
312
|
+
const baseSabia = (alvo) => (alvo === "kiro" ? "~/.kiro/powers/installed/sabia/skills" : curto(PASTA_SKILLS[alvo]()));
|
|
313
|
+
|
|
314
|
+
/** Compila o framework para o alvo em ~/.sabia/compilado/<alvo>/. */
|
|
294
315
|
function compilarPara(alvo, fw) {
|
|
295
316
|
const versao = JSON.parse(fs.readFileSync(path.join(fw, "plugin.json"), "utf8")).version;
|
|
296
|
-
|
|
297
|
-
return compilar(fw, sabia("compilado", alvo), { base, raiz: alvo === "kiro", versao });
|
|
317
|
+
return compilar(fw, sabia("compilado", alvo), { base: baseSabia(alvo), raiz: alvo === "kiro", versao });
|
|
298
318
|
}
|
|
299
319
|
|
|
300
320
|
async function instalarAlvo(alvo, inv, opcoes) {
|
|
@@ -309,15 +329,11 @@ async function instalarAlvo(alvo, inv, opcoes) {
|
|
|
309
329
|
if (r.status !== 0) throw new Error(`kiro-cli powers install: ${(r.stderr || r.error?.message || "").trim()}`);
|
|
310
330
|
inv.alvos.kiro = { power: "sabia" };
|
|
311
331
|
ctx.log("power sabia instalado no Kiro");
|
|
312
|
-
if (fs.existsSync(path.join(home(), ".kiro", "powers", "installed", "sdd")))
|
|
313
|
-
ctx.log("aviso: o power antigo sdd ainda está no Kiro e concorre com o sabia (desinstale pelo painel Powers se não usa mais)");
|
|
314
332
|
return;
|
|
315
333
|
}
|
|
316
334
|
const reg = (inv.alvos[alvo] ||= { copias: {}, mcp: [] });
|
|
317
335
|
for (const s of skillsDoFluxo(fw)) await copiar(path.join(fw, "skills", s), path.join(PASTA_SKILLS[alvo](), s), reg.copias, opcoes);
|
|
318
336
|
if (!opcoes.semMcp) await registrarMcps(sabia("framework"), reg);
|
|
319
|
-
const velhas = (fs.existsSync(PASTA_SKILLS.opencode()) ? fs.readdirSync(PASTA_SKILLS.opencode()) : []).filter((d) => /^sdd/.test(d));
|
|
320
|
-
if (velhas.length) ctx.log(`aviso: skills antigas no opencode concorrem com as sa-*: ${velhas.join(", ")} (remova se não usa mais)`);
|
|
321
337
|
}
|
|
322
338
|
|
|
323
339
|
/** Baixa o framework para uma pasta temporária e diz o que muda em relação a ~/.sabia/framework/. */
|
|
@@ -359,7 +375,7 @@ export async function instalar(opcoes = {}) {
|
|
|
359
375
|
} finally {
|
|
360
376
|
gravarInventario(inv);
|
|
361
377
|
}
|
|
362
|
-
ctx.log('Pronto: skills e MCPs instalados. Os agentes são de cada projeto: abra a ferramenta na raiz dele, na sessão\n' +
|
|
378
|
+
ctx.log((opcoes.semMcp ? 'Pronto: skills instaladas (MCPs não registrados: --sem-mcp).' : 'Pronto: skills e MCPs instalados.') + ' Os agentes são de cada projeto: abra a ferramenta na raiz dele, na sessão\n' +
|
|
363
379
|
'padrão, e peça "configurar o Sabiá" (ou, num projeto já configurado, "atualizar os agentes do Sabiá").');
|
|
364
380
|
}
|
|
365
381
|
|
|
@@ -378,8 +394,6 @@ export async function atualizar(opcoes = {}) {
|
|
|
378
394
|
inv.framework = { ...inv.framework, versao: novo.versao, sha: novo.sha, compilador: VERSAO, instalado_em: new Date().toISOString() };
|
|
379
395
|
try {
|
|
380
396
|
for (const alvo of Object.keys(inv.alvos)) {
|
|
381
|
-
// alvo que saiu do Sabiá (o claude, na 2.0): fica como está; `sabia desinstalar --alvo <ele>` apaga as cópias
|
|
382
|
-
if (!ALVOS.includes(alvo)) { ctx.log(`${alvo}: não é mais alvo do Sabiá (${ALVOS.join(", ")}) — ignorado; tire com sabia desinstalar --alvo ${alvo}`); continue; }
|
|
383
397
|
if (alvo !== "kiro") { // skill que saiu do framework sai da ferramenta
|
|
384
398
|
const copias = inv.alvos[alvo].copias;
|
|
385
399
|
const fica = new Set(skillsDoFluxo(sabia("framework")).map((s) => curto(path.join(PASTA_SKILLS[alvo](), s))));
|
|
@@ -403,10 +417,11 @@ export async function desinstalar(opcoes = {}) {
|
|
|
403
417
|
const reg = inv.alvos[alvo];
|
|
404
418
|
if (!reg) { ctx.log(`${alvo}: nada instalado pelo Sabiá`); continue; }
|
|
405
419
|
for (const tk of Object.values(inv.toolkits)) if (tk.alvos?.[alvo]) { await apagar(tk.alvos[alvo].copias, opcoes); delete tk.alvos[alvo]; }
|
|
406
|
-
if (alvo === "kiro")
|
|
407
|
-
|
|
420
|
+
if (alvo === "kiro") {
|
|
421
|
+
const r = ctx.exec("kiro-cli", ["powers", "uninstall", reg.power]);
|
|
422
|
+
if (r.status !== 0) { ctx.log(`kiro: kiro-cli powers uninstall falhou: ${(r.stderr || r.error?.message || "").trim()}`); continue; }
|
|
423
|
+
} else await apagar(reg.copias, opcoes);
|
|
408
424
|
if (alvo === "opencode") for (const nome of reg.mcp || []) tirarMcpDoOpencode(nome);
|
|
409
|
-
else if (reg.mcp?.length) ctx.log(`${alvo}: os MCPs ${reg.mcp.join(", ")} ficam (o Sabiá não mexe mais no ${alvo}); tire à mão se não usa`);
|
|
410
425
|
if (alvo !== "kiro" && Object.keys(reg.copias).length) continue; // ficou cópia alterada à mão
|
|
411
426
|
delete inv.alvos[alvo];
|
|
412
427
|
ctx.log(`${alvo}: Sabiá desinstalado`);
|
|
@@ -423,6 +438,11 @@ export async function desinstalar(opcoes = {}) {
|
|
|
423
438
|
|
|
424
439
|
// ---------- toolkits ----------
|
|
425
440
|
|
|
441
|
+
/** O passo do framework (`m` do carregar) que o `passo` de um toolkit assume, pelo id — a mesma regra do gancho() do
|
|
442
|
+
* gerar_agentes.py. */
|
|
443
|
+
const ganchoDe = (m, ext) => Object.entries(m.fluxos).flatMap(([q, fl]) => fl.passos.map((x) => ({ id: `${q}.${x.id}`, x })))
|
|
444
|
+
.find(({ id }) => id === ext.passo);
|
|
445
|
+
|
|
426
446
|
/** Erros do toolkit.json: campos, nome, caminhos presos à pasta e arquivos que existem (o schema completo é o da setup). */
|
|
427
447
|
export function validarToolkit(pasta, nomeEsperado = null, fw = null) {
|
|
428
448
|
const f = path.join(pasta, "toolkit.json");
|
|
@@ -447,14 +467,20 @@ export function validarToolkit(pasta, nomeEsperado = null, fw = null) {
|
|
|
447
467
|
const ganchos = fw && fs.existsSync(path.join(fw, "fluxo")) ? carregar(fw) : null;
|
|
448
468
|
for (const s of m.skills || []) {
|
|
449
469
|
existe(s.caminho, "skills");
|
|
470
|
+
// a skill do toolkit não ocupa um nome do Sabiá: a cópia dela trocaria a do Sabiá sem aviso
|
|
471
|
+
const sk = path.basename(path.dirname(String(s.caminho)));
|
|
472
|
+
if (sk.startsWith("sa-") || sk === "sabia") erros.push(`skill ${s.caminho}: nome do Sabiá (sa-* ou sabia); use outro nome de pasta`);
|
|
450
473
|
if (s.passo?.agente && !agentes.has(s.passo.agente)) erros.push(`passo de ${s.caminho}: agente ${s.passo.agente} não está em agentes`);
|
|
451
|
-
if (s.passo
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
if (!
|
|
455
|
-
else if (!x.desvio) erros.push(`passo de ${s.caminho}: ${s.passo.passo} não aceita desvio de toolkit`);
|
|
474
|
+
if (s.passo && !s.passo.passo) erros.push(`passo de ${s.caminho}: use o id do passo (passo.passo)`);
|
|
475
|
+
else if (s.passo && ganchos) {
|
|
476
|
+
const g = ganchoDe(ganchos, s.passo);
|
|
477
|
+
if (!g) erros.push(`passo de ${s.caminho}: ${s.passo.passo} não existe no Sabiá instalado`);
|
|
478
|
+
else if (!g.x.desvio) erros.push(`passo de ${s.caminho}: ${s.passo.passo} não aceita desvio de toolkit${g.x.tipos ? ` (o tipo ${g.x.tipos.join(", ")} vai por \`tipos\`)` : ""}`);
|
|
456
479
|
}
|
|
457
480
|
}
|
|
481
|
+
if (ganchos && (m.tipos || m.rubricas || m.requer)) {
|
|
482
|
+
try { erros.push(...validarToolkitFonte(fw, pasta)); } catch (e) { erros.push(e.message); }
|
|
483
|
+
}
|
|
458
484
|
for (const s of m.steering || []) existe(s.caminho, "steering");
|
|
459
485
|
for (const a of m.agentes || []) {
|
|
460
486
|
if (!nomeOk(a.nome)) erros.push(`agente com nome inválido (kebab-case, sem sa-): ${a.nome}`);
|
|
@@ -496,28 +522,36 @@ function acharNoIndice(nome, versao, opcoes) {
|
|
|
496
522
|
|
|
497
523
|
export async function baixarToolkit(alvo, opcoes = {}) {
|
|
498
524
|
const ehUrl = /[:/]/.test(alvo) && !/^[a-z0-9-]+(@[\w.-]+)?$/.test(alvo);
|
|
499
|
-
|
|
500
|
-
const
|
|
525
|
+
// `<url>[@ref]#<subpasta>`: o toolkit numa subpasta de um repositório (monorepo de toolkits)
|
|
526
|
+
const [semSub, subpasta = null] = ehUrl ? alvo.split("#") : [alvo];
|
|
527
|
+
const [nomeOuUrl, versaoOuRef] = separarRef(semSub);
|
|
528
|
+
const e = ehUrl ? { nome: null, url: nomeOuUrl, ref: versaoOuRef, subpasta } : acharNoIndice(nomeOuUrl, versaoOuRef, opcoes);
|
|
501
529
|
const { pasta, sha } = baixar(e.url, e);
|
|
502
530
|
const erros = validarToolkit(pasta, e.nome, sabia("framework"));
|
|
503
531
|
if (erros.length) throw new Error(`toolkit recusado:\n- ${erros.join("\n- ")}`);
|
|
504
532
|
const m = JSON.parse(fs.readFileSync(path.join(pasta, "toolkit.json"), "utf8"));
|
|
505
533
|
const destino = sabia("toolkits", m.nome);
|
|
506
534
|
const dif = diferencas(destino, pasta);
|
|
535
|
+
dif.removidos = dif.removidos.filter((f) => f !== ".origem.json"); // é nossa, regravada abaixo
|
|
536
|
+
let mudou = true;
|
|
507
537
|
if (fs.existsSync(destino)) {
|
|
508
538
|
ctx.log(`toolkit ${m.nome}: ${JSON.parse(fs.readFileSync(path.join(destino, "toolkit.json"), "utf8")).versao} → ${m.versao}`);
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
|
|
539
|
+
mudou = mostrarDiferencas(dif);
|
|
540
|
+
if (!mudou) ctx.log("nada mudou");
|
|
541
|
+
else if (!/^s/i.test(await ctx.perguntar("Substituir? [s/N]", "n"))) { ctx.log("nada foi alterado"); return null; }
|
|
542
|
+
else fs.rmSync(destino, { recursive: true, force: true });
|
|
512
543
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
544
|
+
if (mudou) {
|
|
545
|
+
fs.mkdirSync(path.dirname(destino), { recursive: true });
|
|
546
|
+
fs.cpSync(pasta, destino, { recursive: true });
|
|
547
|
+
}
|
|
548
|
+
// mesmo sem mudar o conteúdo, a origem pode ter mudado (da pasta local para o índice, por exemplo)
|
|
549
|
+
const origem = { indice: e.indice || null, url: e.url, ref: e.ref || null, subpasta: e.indice ? null : e.subpasta || null, sha, versao: m.versao };
|
|
516
550
|
fs.writeFileSync(path.join(destino, ".origem.json"), JSON.stringify(origem, null, 2) + "\n");
|
|
517
551
|
const inv = lerInventario();
|
|
518
552
|
inv.toolkits[m.nome] = { ...(inv.toolkits[m.nome] || { alvos: {} }), versao: m.versao, sha, fonte: e.url };
|
|
519
553
|
gravarInventario(inv);
|
|
520
|
-
ctx.log(`toolkit ${m.nome} ${m.versao} em ~/.sabia/toolkits/${m.nome}`);
|
|
554
|
+
if (mudou) ctx.log(`toolkit ${m.nome} ${m.versao} em ~/.sabia/toolkits/${m.nome}`);
|
|
521
555
|
return m.nome;
|
|
522
556
|
}
|
|
523
557
|
|
|
@@ -535,7 +569,7 @@ export async function instalarToolkit(alvo, opcoes = {}) {
|
|
|
535
569
|
// mesmos nomes de skill e de agente: as duas no usuário se sobrescreveriam
|
|
536
570
|
if (rival) throw new Error(`${baixado} e ${rival} são versões do mesmo toolkit (base ${familia}); instale um só: sabia toolkits remover ${rival}`);
|
|
537
571
|
const tk = (inv.toolkits[baixado] ||= { versao: m.versao, alvos: {} });
|
|
538
|
-
const doInv = Object.keys(inv.alvos)
|
|
572
|
+
const doInv = Object.keys(inv.alvos);
|
|
539
573
|
const alvos = opcoes.alvo || !doInv.length ? await escolherAlvos(opcoes) : doInv;
|
|
540
574
|
try {
|
|
541
575
|
for (const a of alvos.filter((x) => x !== "kiro")) {
|
|
@@ -574,15 +608,20 @@ export async function removerToolkit(nome, opcoes = {}) {
|
|
|
574
608
|
gravarInventario(inv);
|
|
575
609
|
}
|
|
576
610
|
ctx.log(`toolkit ${nome} removido`);
|
|
611
|
+
const f = path.join(process.cwd(), ".sabia", "config", "agentes.json");
|
|
612
|
+
if (fs.existsSync(f) && JSON.parse(fs.readFileSync(f, "utf8")).toolkits?.[nome])
|
|
613
|
+
ctx.log(`aviso: este projeto ainda usa o ${nome}; peça "atualizar os agentes do Sabiá" para desligá-lo`);
|
|
577
614
|
}
|
|
578
615
|
|
|
579
616
|
export async function atualizarToolkits(nome, opcoes = {}) {
|
|
580
617
|
const inv = lerInventario();
|
|
581
618
|
for (const n of nome ? [nome] : Object.keys(inv.toolkits)) {
|
|
582
|
-
const
|
|
583
|
-
|
|
619
|
+
const fo = sabia("toolkits", n, ".origem.json");
|
|
620
|
+
if (!fs.existsSync(fo)) throw new Error(`toolkit ${n}: sem ~/.sabia/toolkits/${n}/.origem.json; baixe de novo: sabia toolkits baixar <nome|url>`);
|
|
621
|
+
const origem = JSON.parse(fs.readFileSync(fo, "utf8"));
|
|
622
|
+
const alvo = origem.indice ? n : `${origem.url}${origem.ref ? "@" + origem.ref : ""}${origem.subpasta ? "#" + origem.subpasta : ""}`;
|
|
584
623
|
await instalarToolkit(alvo, { ...opcoes, atualizar: true, indice: origem.indice || opcoes.indice,
|
|
585
|
-
alvo: Object.keys(inv.toolkits[n]?.alvos || {}).
|
|
624
|
+
alvo: Object.keys(inv.toolkits[n]?.alvos || {}).join(",") || "kiro" });
|
|
586
625
|
}
|
|
587
626
|
}
|
|
588
627
|
|
|
@@ -632,8 +671,6 @@ export function listarToolkits(opcoes = {}) {
|
|
|
632
671
|
|
|
633
672
|
// ---------- rota (diagnóstico, para pessoas) ----------
|
|
634
673
|
|
|
635
|
-
// toolkit 1.x (passo.papel, e "codificador" antes do renome): o mesmo mapa do gerar_agentes.py
|
|
636
|
-
const GANCHO_1X = { desenvolvedor: "sa-codificar.codificar.tarefa", codificador: "sa-codificar.codificar.tarefa", especificador: "sa-prd.prd.escrever" };
|
|
637
674
|
const lerJson = (f, padrao) => (fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")) : padrao);
|
|
638
675
|
const lerFm = (f) => (fs.existsSync(f) ? partes(fs.readFileSync(f, "utf8")).dados || {} : null);
|
|
639
676
|
|
|
@@ -657,16 +694,32 @@ function contextoRota(opcoes) {
|
|
|
657
694
|
const projeto = path.resolve(opcoes.projeto || process.cwd());
|
|
658
695
|
const cfg = lerJson(path.join(projeto, ".sabia", "config", "agentes.json"), {});
|
|
659
696
|
const m = carregar(fw);
|
|
697
|
+
const dirTk = (nome) => { const pasta = cfg.toolkits?.[nome]?.pasta; return !pasta ? sabia("toolkits", nome) : pasta.startsWith("~/") ? longo(pasta) : path.resolve(projeto, pasta); };
|
|
698
|
+
// os tipos dos toolkits do projeto viram o desvio por tipo: a skill do toolkit, com o fluxo do tipo, se o traz
|
|
699
|
+
const tipos = [];
|
|
700
|
+
for (const nome of Object.keys(cfg.toolkits || {})) {
|
|
701
|
+
const man = lerJson(path.join(dirTk(nome), "toolkit.json"), null);
|
|
702
|
+
if (!man) continue;
|
|
703
|
+
for (const [sigla, q] of Object.entries(man.tipos || {})) {
|
|
704
|
+
if (typeof q?.skill !== "string") { tipos.push({ sigla, fluxo: "(inválido: o tipo no toolkit.json precisa de {\"skill\"}; o gerador recusa)", toolkit: nome, versao: man.versao }); continue; }
|
|
705
|
+
const sk = path.basename(path.dirname(q.skill));
|
|
706
|
+
tipos.push({ sigla, fluxo: q.fluxo ? `skill ${sk} (${q.fluxo})` : `skill ${sk}`, toolkit: nome, versao: man.versao });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
660
709
|
const desvios = (cfg.passos || []).flatMap((e) => {
|
|
661
|
-
const
|
|
662
|
-
const dir = !pasta ? sabia("toolkits", e.toolkit) : pasta.startsWith("~/") ? longo(pasta) : path.resolve(projeto, pasta);
|
|
710
|
+
const dir = dirTk(e.toolkit);
|
|
663
711
|
const man = lerJson(path.join(dir, "toolkit.json"), null);
|
|
664
712
|
const ext = man?.skills?.find((x) => x.caminho === e.skill && x.passo)?.passo;
|
|
665
713
|
if (!ext) { ctx.log(`aviso: toolkit ${e.toolkit} sem passo em ${e.skill} (${curto(dir)})`); return []; }
|
|
714
|
+
if (!ext.passo) {
|
|
715
|
+
ctx.log(`aviso: toolkit ${e.toolkit}: o passo de ${e.skill} não tem o id: use o id do passo (passo.passo)`
|
|
716
|
+
+ ` e reinstale o toolkit (\`sabia toolkits atualizar ${e.toolkit}\`) — ignorado`);
|
|
717
|
+
return [];
|
|
718
|
+
}
|
|
666
719
|
// como o gancho() do gerador: só passo do framework com "desvio": true (senão o gerador recusa e o prompt não o tem)
|
|
667
|
-
const
|
|
668
|
-
if (!
|
|
669
|
-
ctx.log(`aviso: toolkit ${e.toolkit}:
|
|
720
|
+
const g = ganchoDe(m, ext), passo = g?.x.desvio ? g.id : null;
|
|
721
|
+
if (!passo) {
|
|
722
|
+
ctx.log(`aviso: toolkit ${e.toolkit}: o passo ${ext.passo} não aceita desvio de toolkit (o gerador recusa) — ignorado`);
|
|
670
723
|
return [];
|
|
671
724
|
}
|
|
672
725
|
const agente = e.agente && ext.agente ? ext.agente : null;
|
|
@@ -674,7 +727,7 @@ function contextoRota(opcoes) {
|
|
|
674
727
|
return [{ toolkit: e.toolkit, versao: man.versao, passo, para: ext.para, arquivos: ext.arquivos || [],
|
|
675
728
|
agente, skill: path.basename(path.dirname(e.skill)), skills: skills.map((x) => path.basename(path.dirname(x))) }];
|
|
676
729
|
});
|
|
677
|
-
return { fw, projeto, m, versao: lerJson(path.join(fw, "plugin.json"), {}).version || "?", desvios };
|
|
730
|
+
return { fw, projeto, m, versao: lerJson(path.join(fw, "plugin.json"), {}).version || "?", desvios, tipos };
|
|
678
731
|
}
|
|
679
732
|
|
|
680
733
|
/** Os passos do fluxo `q` na ordem, com o que o projeto mudou: origem sabia | projeto (corpo trocado) | inserido | removido. */
|
|
@@ -705,15 +758,41 @@ function resolverFluxo(c, q) {
|
|
|
705
758
|
|
|
706
759
|
const travasDe = (fl, x) => [...new Set([...(fl.d.travas_gerais || []), ...(x?.travas || [])])].join(", ") || "—";
|
|
707
760
|
|
|
708
|
-
/**
|
|
761
|
+
/** A tarefa `id` do tasks.md (`1`, `2.1`; `T-2` também): o texto do bloco dela (a linha e as de baixo, mais
|
|
762
|
+
* recuadas) e o que ela cita entre crases, sem espaço (os arquivos, classes e métodos). */
|
|
763
|
+
export function lerTarefa(arquivo, id) {
|
|
764
|
+
if (!fs.existsSync(arquivo)) throw new Error(`${arquivo} não existe`);
|
|
765
|
+
const norm = (t) => t.replace(/^T-?/i, "").split(".").map(Number).join(".");
|
|
766
|
+
const linhas = fs.readFileSync(arquivo, "utf8").split("\n");
|
|
767
|
+
let achado;
|
|
768
|
+
const i = linhas.findIndex((l) => (achado = l.match(/^\s*- \[.\]\*? (\d+(?:\.\d+)*)\.? /)) && norm(achado[1]) === norm(id));
|
|
769
|
+
if (i < 0) throw new Error(`${arquivo}: não há a tarefa ${id} (use o número dela, ex.: #2 ou #2.1)`);
|
|
770
|
+
let fim = i + 1;
|
|
771
|
+
// linha em branco entre as subtarefas não fecha o bloco; fecha a primeira linha com recuo igual ou menor
|
|
772
|
+
for (let k = i + 1; k < linhas.length; k++) {
|
|
773
|
+
const r = linhas[k].search(/\S/);
|
|
774
|
+
if (r < 0) continue;
|
|
775
|
+
if (r <= linhas[i].search(/\S/)) break;
|
|
776
|
+
fim = k + 1;
|
|
777
|
+
}
|
|
778
|
+
const texto = linhas.slice(i, fim).filter((l) => l.trim()).map((l) => l.trim().replace(/^- (?:\[.\]\*? )?/, "")).join(" · ");
|
|
779
|
+
return { id: achado[1], texto, arquivos: [...new Set([...texto.matchAll(/`([^`\s]+)`/g)].map((m) => m[1]))] };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** `sabia rota <arquivo>` (ou `<tasks.md>#<tarefa>`, pelos arquivos que ela cita): o passo com gancho cujo desvio
|
|
783
|
+
* casa, o dono, o toolkit, o motivo e as travas. */
|
|
709
784
|
export function rota(arquivo, opcoes = {}) {
|
|
710
785
|
const c = contextoRota(opcoes);
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
786
|
+
const t = arquivo.match(/^(.+\.md)#(.+)$/), tarefa = t && lerTarefa(t[1], t[2]);
|
|
787
|
+
const relativo = (a, base) => {
|
|
788
|
+
const r = path.relative(c.projeto, path.resolve(base, a));
|
|
789
|
+
return (r.startsWith("..") ? a : r).split(path.sep).join("/"); // fora do projeto: o caminho como veio
|
|
790
|
+
};
|
|
791
|
+
// a tarefa cita os arquivos como no repositório: relativos à raiz do projeto
|
|
792
|
+
const alvos = tarefa ? tarefa.arquivos.map((a) => relativo(a, c.projeto)) : [relativo(arquivo, process.cwd())];
|
|
714
793
|
const ganchos = Object.entries(c.m.fluxos).flatMap(([q, fl]) => fl.passos.filter((x) => x.desvio).map((x) => `${q}.${x.id}`));
|
|
715
794
|
let casou = null;
|
|
716
|
-
for (const d of c.desvios) { const g = d.arquivos.find((
|
|
795
|
+
achar: for (const d of c.desvios) for (const a of alvos) { const g = d.arquivos.find((x) => casaGlob(x, a)); if (g) { casou = { d, g, a }; break achar; } }
|
|
717
796
|
// sem casar: o passo dos desvios por arquivo do projeto (o de código), senão o primeiro gancho do framework
|
|
718
797
|
const pid = casou?.d.passo || c.desvios.find((d) => d.arquivos.length)?.passo || ganchos[0];
|
|
719
798
|
if (!pid) throw new Error(`${curto(c.fw)} não tem passo com desvio de toolkit`);
|
|
@@ -722,11 +801,12 @@ export function rota(arquivo, opcoes = {}) {
|
|
|
722
801
|
if (!x) throw new Error(`o desvio aponta para ${pid}, que não é passo do Sabiá instalado`);
|
|
723
802
|
const agentes = [`.opencode/agents/${fl.d.dono}.md`, `.kiro/agents/${fl.d.dono}.json`].filter((f) => fs.existsSync(path.join(c.projeto, f)));
|
|
724
803
|
const linhas = [
|
|
804
|
+
...(tarefa ? [["tarefa", tarefa.id, tarefa.texto]] : []),
|
|
725
805
|
["passo", pid, x.origem === "projeto" ? `${path.relative(c.projeto, x.arq)} (projeto)` : `${curto(x.arq)} (Sabiá ${c.versao})`],
|
|
726
806
|
["dono", fl.d.dono, agentes.join(", ") || "agente não gerado neste projeto"],
|
|
727
807
|
];
|
|
728
808
|
if (casou) linhas.push(["desvio", casou.d.agente || `skill ${casou.d.skill}`, `toolkit ${casou.d.toolkit} ${casou.d.versao}`],
|
|
729
|
-
["motivo", `casou ${casou.g}`], ["skills", [fl.skill, ...casou.d.skills].join(", ")]);
|
|
809
|
+
["motivo", `casou ${casou.g}${tarefa ? ` em ${casou.a}` : ""}`], ["skills", [fl.skill, ...casou.d.skills].join(", ")]);
|
|
730
810
|
else {
|
|
731
811
|
linhas.push(["desvio", "— fluxo padrão", "nenhum padrão casou"]);
|
|
732
812
|
for (const d of c.desvios.filter((y) => y.passo === pid)) linhas.push(["pelo texto", `"${d.para}" → ${d.agente || `skill ${d.skill}`}`, "o modelo decide pela tarefa"]);
|
|
@@ -738,19 +818,22 @@ export function rota(arquivo, opcoes = {}) {
|
|
|
738
818
|
const sid = (t) => t.replace(/[^A-Za-z0-9_]/g, "_");
|
|
739
819
|
const rotulo = (t) => t.replace(/[;:]/g, ",").replace(/[`"#]/g, "");
|
|
740
820
|
|
|
741
|
-
/** `sabia rota --fluxo <skill.fluxo>`: a tabela resolvida (com o que o projeto mudou) e o diagrama de estados. */
|
|
821
|
+
/** `sabia rota --fluxo <skill.fluxo>`: a tabela resolvida (com o que o projeto mudou), o grafo ASCII e o diagrama de estados. */
|
|
742
822
|
export function rotaFluxo(q, opcoes = {}) {
|
|
743
823
|
const c = contextoRota(opcoes);
|
|
744
824
|
const r = resolverFluxo(c, q), { fl } = r;
|
|
745
825
|
const desvios = c.desvios.filter((d) => d.passo?.startsWith(`${r.q}.`));
|
|
826
|
+
// desvio por tipo: o tipo que o passo cria tem skill de toolkit no projeto
|
|
827
|
+
const porTipo = (x) => c.tipos.filter((t) => x.tipos?.includes(t.sigla));
|
|
746
828
|
const origem = (x) => ({ sabia: "Sabiá", projeto: `projeto${x.base ? ` (base ${x.base})` : ""}`,
|
|
747
829
|
inserido: `projeto, inserido depois de ${x.depois}`, removido: "removido no projeto" })[x.origem];
|
|
748
830
|
let n = 0;
|
|
749
831
|
const tabela = [["#", "passo", "origem", "chama", "travas", "saídas"], ...r.passos.map((x) => [
|
|
750
|
-
x.origem === "removido" ? "-" : String(++n), x.id + (x
|
|
832
|
+
x.origem === "removido" ? "-" : String(++n), x.id + (temDesvio(x) ? " ⇢" : ""), origem(x), [].concat(x.chama || []).join(", ") || "—",
|
|
751
833
|
(x.travas || []).join(", ") || "—", Object.entries(x.saidas || {}).map(([k, v]) => `${k} → ${v.replace(/^→ /, "passa a ")}`).join("; ") || "—"])];
|
|
752
834
|
const cab = [["fluxo", r.q, `dono ${fl.d.dono}`, `Sabiá ${c.versao}`], ["travas", (fl.d.travas_gerais || []).join(", ") || "—"],
|
|
753
|
-
...desvios.map((d) => ["desvio ⇢", `${d.passo.split(".")[2]}: ${d.arquivos.join(", ") || `"${d.para}"`}`, `→ ${d.agente || `skill ${d.skill}`}`, `toolkit ${d.toolkit} ${d.versao}`])
|
|
835
|
+
...desvios.map((d) => ["desvio ⇢", `${d.passo.split(".")[2]}: ${d.arquivos.join(", ") || `"${d.para}"`}`, `→ ${d.agente || `skill ${d.skill}`}`, `toolkit ${d.toolkit} ${d.versao}`]),
|
|
836
|
+
...r.passos.flatMap((x) => porTipo(x).map((t) => ["tipo ⇢", `${x.id}: ${t.sigla}`, `→ ${t.fluxo}`, `toolkit ${t.toolkit} ${t.versao}`]))];
|
|
754
837
|
|
|
755
838
|
const out = ["stateDiagram-v2"], declarados = new Set();
|
|
756
839
|
const estado = (id, nome = id) => {
|
|
@@ -769,10 +852,14 @@ export function rotaFluxo(q, opcoes = {}) {
|
|
|
769
852
|
}
|
|
770
853
|
out.push(prox ? ` ${a} --> ${estado(prox.id)}${prox.origem === "inserido" ? ": projeto (inserido)" : ""}` : ` ${a} --> [*]`);
|
|
771
854
|
const nota = [x.chama && `chama ${[].concat(x.chama).join(", ")}`, x.origem === "projeto" && `corpo do projeto${x.base ? ` (base ${x.base})` : ""}`,
|
|
772
|
-
x.desvio && desvios.length && `desvio ⇢ ${desvios.filter((d) => d.passo === `${r.q}.${x.id}`).map((d) => d.agente || d.skill).join(", ")}
|
|
855
|
+
x.desvio && desvios.length && `desvio ⇢ ${desvios.filter((d) => d.passo === `${r.q}.${x.id}`).map((d) => d.agente || d.skill).join(", ")}`,
|
|
856
|
+
...porTipo(x).map((t) => `tipo ${t.sigla} ⇢ ${t.fluxo}`)].filter(Boolean);
|
|
773
857
|
if (nota.length) out.push(` note right of ${a}: ${rotulo(nota.join(" · "))}`);
|
|
774
858
|
});
|
|
775
|
-
|
|
859
|
+
const notas = (x) => [x.origem !== "sabia" && `origem: ${origem(x)}`,
|
|
860
|
+
...desvios.filter((d) => d.passo === `${r.q}.${x.id}`).map((d) => `⇢ desvio → ${d.agente || `skill ${d.skill}`} (toolkit ${d.toolkit} ${d.versao})`),
|
|
861
|
+
...porTipo(x).map((t) => `⇢ tipo ${t.sigla} → ${t.fluxo} (toolkit ${t.toolkit} ${t.versao})`)].filter(Boolean);
|
|
862
|
+
return [colunas(cab), "", colunas(tabela), "", grafoAscii(fl, ativos, { notas }), "", LEGENDA, "", "```mermaid", ...out, "```"].join("\n");
|
|
776
863
|
}
|
|
777
864
|
|
|
778
865
|
/** `sabia rota --mapa`: as arestas entre fluxos de todo o framework (chama e passagens), em Mermaid. */
|
|
@@ -802,15 +889,16 @@ const AJUDA = `${BANNER}
|
|
|
802
889
|
sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro e no opencode
|
|
803
890
|
|
|
804
891
|
sabia instalar [--alvo kiro,opencode] [--versao X] [--fonte <pasta|url-git>[@ref]]
|
|
805
|
-
sabia atualizar [--versao X]
|
|
892
|
+
sabia atualizar [--versao X] [--fonte <pasta|url-git>[@ref]]
|
|
806
893
|
sabia desinstalar [--alvo …]
|
|
807
894
|
sabia toolkits listar [--indice <url-git|pasta>]
|
|
808
|
-
sabia toolkits baixar <nome>[@versao] | <url-git>[@ref]
|
|
809
|
-
sabia toolkits instalar <nome>[@versao] | <url-git>[@ref] [--alvo …] (sem argumento, no projeto: as versões dele)
|
|
810
|
-
sabia toolkits atualizar [<nome>]
|
|
895
|
+
sabia toolkits baixar <nome>[@versao] | <url-git|pasta>[@ref][#subpasta] [--indice <url-git|pasta>]
|
|
896
|
+
sabia toolkits instalar <nome>[@versao] | <url-git|pasta>[@ref][#subpasta] [--alvo …] [--indice <url-git|pasta>] (sem argumento, no projeto: as versões dele)
|
|
897
|
+
sabia toolkits atualizar [<nome>] [--indice <url-git|pasta>]
|
|
811
898
|
sabia toolkits remover <nome>
|
|
812
899
|
sabia rota <arquivo> o passo com desvio de toolkit, o dono, o toolkit, o motivo e as travas
|
|
813
|
-
sabia rota
|
|
900
|
+
sabia rota <tasks.md>#<tarefa> o mesmo pelos arquivos que a tarefa (#2, #2.1) cita; sem casar, o texto dela
|
|
901
|
+
sabia rota --fluxo <skill.fluxo> o fluxo resolvido no projeto (tabela, grafo ASCII e Mermaid)
|
|
814
902
|
sabia rota --mapa as arestas entre fluxos (chama e passagens), em Mermaid
|
|
815
903
|
[--projeto <pasta>] (padrão: a atual) [--framework <pasta>] (padrão: ~/.sabia/framework)
|
|
816
904
|
|
|
@@ -818,7 +906,7 @@ sabia ${VERSAO} — instala o Sabiá e os toolkits no Kiro e no opencode
|
|
|
818
906
|
--forcar sobrescreve o que não foi instalado pelo Sabiá ou foi alterado à mão
|
|
819
907
|
--sem-mcp não registra os MCPs
|
|
820
908
|
|
|
821
|
-
Tudo fica em ~/.sabia/ (framework/, toolkits/, inventario.json).`;
|
|
909
|
+
Tudo fica em ~/.sabia/ (framework/, compilado/, toolkits/, inventario.json e indices.json).`;
|
|
822
910
|
|
|
823
911
|
export function lerArgs(argv) {
|
|
824
912
|
const posicionais = [];
|