@seedgrid/ai-toolkit 0.4.0 → 0.4.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.
@@ -132,6 +132,7 @@ export function agentsMdBlock(bundle, targets, mode) {
132
132
  '',
133
133
  `Bloco gerenciado pelo \`seedgrid-ai\` (conteúdo ${bundle.contentVersion}, modo \`${mode}\`). Não edite entre os marcadores; personalize em \`.ai/local/\`.`,
134
134
  '',
135
+ ...(bundle.agentsNotes.length > 0 ? [...bundle.agentsNotes.map((n) => `> ${n}`), ''] : []),
135
136
  'Cada pasta de `.ai/` tem um `index.md` com o que há nela: comece por ele e leia só o que a tarefa pedir.',
136
137
  '',
137
138
  '- Regras: `.ai/rules/`. Antes de tarefas não triviais, leia `coordenacao.md`.',
@@ -510,6 +510,10 @@ export async function loadContent(root) {
510
510
  checkBrain(ctx, brain.notes, brain.entrypoints, brain.base);
511
511
  shared.push(...brain.notes);
512
512
  }
513
+ const rawNotes = manifest.agentsNotes ?? [];
514
+ if (!Array.isArray(rawNotes) || rawNotes.length > 10 || rawNotes.some((n) => typeof n !== 'string' || n.trim() === '' || n.length > 600 || n.includes('\n'))) {
515
+ ctx.errors.push('manifest: agentsNotes deve ser uma lista de até 10 linhas de texto (até 600 caracteres, sem quebra de linha)');
516
+ }
513
517
  if (ctx.errors.length > 0) {
514
518
  throw new ToolkitError(`Conteúdo inválido em ${absRoot}`, ctx.errors);
515
519
  }
@@ -523,6 +527,7 @@ export async function loadContent(root) {
523
527
  brain: brain
524
528
  ? { notes: brain.notes.length, entrypoints: brain.entrypoints.map((e) => `.ai/brain/${brain.base}/${e}`) }
525
529
  : null,
530
+ agentsNotes: rawNotes,
526
531
  sourceFiles: [...ctx.sources].sort(),
527
532
  };
528
533
  }
package/docs/formato.md CHANGED
@@ -87,6 +87,7 @@ Arquivos e pastas que começam com ponto são ignorados.
87
87
  | --- | --- |
88
88
  | `schemaVersion` | `2` (versão do formato do manifesto) |
89
89
  | `contentVersion` | semver; deve ser **igual** à versão do pacote (`package.json`); aumente a cada mudança de conteúdo |
90
+ | `agentsNotes` | (opcional) até 10 linhas curtas que o bloco do `AGENTS.md` mostra no topo, como aviso (ex.: o modo de trabalho que uma camada adicional exige) |
90
91
  | `toolkitBase` | pasta do vault onde ficam `agents/`, `rules/`, `templates/`, `references/` e `skills/` (aqui, `SeedGrid/Toolkit`) |
91
92
  | `components[].id` | único; para skills, `skill:<name>` |
92
93
  | `components[].type` | `agents`, `rules`, `templates`, `references`, `skill` ou `brain` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedgrid/ai-toolkit",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Instala agentes, skills, regras, templates e referências versionados em projetos locais para Claude Code e Codex.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,10 +17,11 @@ Crie uma ADR quando a decisão: muda um comportamento observável; cria, remove
17
17
 
18
18
  1. **Listar as decisões.** Escreva cada uma em uma frase e **mostre ao usuário** para confirmar: você propõe, quem decide é o usuário. Ao registrar, repita a decisão em uma frase (decisão registrada errada só aparece quando alguém relê).
19
19
  2. **Domínio.** Escolha na lista `docs/ai/decisions/dominios.txt` (um domínio por linha). Se o arquivo não existir, proponha 5 a 10 domínios do projeto (por exemplo `seguranca`, `dados`, `api`, `configuracao`, `processo`), confirme e crie. Domínio novo só com confirmação do usuário.
20
- 3. **Criar o arquivo** `docs/ai/decisions/AAAA-MM-DD-slug.md` a partir do modelo `.ai/templates/decisao-arquitetura.md`: cabeçalho completo (`titulo`, `data`, `status`, `decidido-por`, `consultados`, `dominio`, `modulos`, `origem`) e as seções Contexto, Decisão, Alternativas e Consequências. Comece com `status: proposta` e mude para `aceita` quando o usuário confirmar.
21
- 4. **Substituição.** Se a decisão troca outra, não edite a antiga: crie a nova com `substitui: [arquivo-antigo.md]` e apenas marque a antiga como `status: substituida` e `substituida-por`.
22
- 5. **Índice.** Rode `node .ai/skills/sg-adr/scripts/adr-index.mjs`. Ele valida o cabeçalho e gera `docs/ai/decisions/INDEX.md` em ordem decrescente de data. Nunca edite o `INDEX.md` à mão; use `--check` para conferir se está em dia.
23
- 6. **Entregar** listando as ADRs criadas e o que ficou como `proposta`.
20
+ 3. **Número.** `node .ai/skills/sg-adr/scripts/adr-index.mjs --proximo` dá o próximo número livre; ele é sequencial, vai em `numero:` e **nunca muda** (dá para conversar sobre "a ADR #7"). Para ADRs antigas sem número, `--numerar` numera só as que não têm.
21
+ 4. **Criar o arquivo** `docs/ai/decisions/AAAA-MM-DD-slug.md` a partir do modelo `.ai/templates/decisao-arquitetura.md`: cabeçalho completo (`titulo`, `numero`, `data`, `status`, `decidido-por`, `consultados`, `dominio`, `modulos`, `origem`) e as seções Contexto, Decisão, Alternativas e Consequências. Comece com `status: proposta` e mude para `aceita` quando o usuário confirmar.
22
+ 5. **Substituição.** Se a decisão troca outra, não edite a antiga: crie a nova com `substitui: [arquivo-antigo.md]` e apenas marque a antiga como `status: substituida` e `substituida-por`.
23
+ 6. **Índice.** Rode `node .ai/skills/sg-adr/scripts/adr-index.mjs`. Ele valida o cabeçalho e gera `docs/ai/decisions/INDEX.md` em ordem decrescente de data. Nunca edite o `INDEX.md` à mão; use `--check` para conferir se está em dia.
24
+ 7. **Entregar** listando as ADRs criadas e o que ficou como `proposta` (o que o dono confirmou e já está no código é `implementada`).
24
25
 
25
26
  ## Consultar
26
27
 
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // Gera docs/ai/decisions/INDEX.md a partir do cabeçalho das ADRs, da mais nova para a mais antiga.
3
3
  // Uso: node .ai/skills/sg-adr/scripts/adr-index.mjs [--dir docs/ai/decisions] [--check]
4
- // --check só valida e falha se o INDEX.md estiver desatualizado (sem escrever)
4
+ // --check só valida e falha se o INDEX.md estiver desatualizado (sem escrever)
5
+ // --proximo imprime o próximo número livre para uma ADR nova
6
+ // --numerar dá número, em ordem de data e nome, SÓ às ADRs sem "numero"; nunca renumera
5
7
  // Domínios: se existir <dir>/dominios.txt (um por linha), só esses são aceitos; senão, qualquer slug em minúsculas com hífen.
6
8
  import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
7
9
  import path from 'node:path';
@@ -10,7 +12,7 @@ const args = process.argv.slice(2);
10
12
  const di = args.indexOf('--dir');
11
13
  const dir = path.resolve(di >= 0 && args[di + 1] ? args[di + 1] : 'docs/ai/decisions');
12
14
  const check = args.includes('--check');
13
- const STATUS = ['proposta', 'aceita', 'substituida', 'rejeitada'];
15
+ const STATUS = ['proposta', 'aceita', 'implementada', 'substituida', 'rejeitada'];
14
16
 
15
17
  if (!existsSync(dir)) {
16
18
  console.error(`Pasta não encontrada: ${dir}`);
@@ -35,9 +37,34 @@ function parse(text, file) {
35
37
  return out;
36
38
  }
37
39
 
40
+ const files = readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'INDEX.md').sort();
41
+ const numeroDe = (f) => {
42
+ try {
43
+ const n = parse(readFileSync(path.join(dir, f), 'utf8'), f).numero;
44
+ return /^\d+$/.test(String(n ?? '')) ? Number(n) : null;
45
+ } catch { return null; }
46
+ };
47
+ const maior = () => Math.max(0, ...files.map(numeroDe).filter((n) => n !== null));
48
+ if (args.includes('--numerar')) {
49
+ let max = maior();
50
+ for (const f of files.filter((x) => numeroDe(x) === null)) {
51
+ const file = path.join(dir, f);
52
+ const raw = readFileSync(file, 'utf8');
53
+ const eol = raw.includes('\r\n') ? '\r\n' : '\n';
54
+ const out = raw.replace(/^(titulo:[^\r\n]*)/m, `$1${eol}numero: ${++max}`);
55
+ if (out === raw) { console.error(`${f}: sem linha "titulo:" para numerar`); process.exit(1); }
56
+ writeFileSync(file, out, 'utf8');
57
+ console.log(`${f} -> #${max}`);
58
+ }
59
+ }
60
+ if (args.includes('--proximo')) {
61
+ console.log(maior() + 1);
62
+ process.exit(0);
63
+ }
64
+
38
65
  const errors = [];
39
66
  const adrs = [];
40
- for (const f of readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'INDEX.md').sort()) {
67
+ for (const f of files) {
41
68
  let h;
42
69
  try {
43
70
  h = parse(readFileSync(path.join(dir, f), 'utf8'), f);
@@ -46,7 +73,8 @@ for (const f of readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'INDEX
46
73
  continue;
47
74
  }
48
75
  if (!/^\d{4}-\d{2}-\d{2}-/.test(f)) errors.push(`${f}: o nome deve começar com AAAA-MM-DD-`);
49
- for (const k of ['titulo', 'data', 'status', 'decidido-por', 'dominio']) if (!h[k] || h[k].length === 0) errors.push(`${f}: falta "${k}"`);
76
+ for (const k of ['titulo', 'numero', 'data', 'status', 'decidido-por', 'dominio']) if (!h[k] || h[k].length === 0) errors.push(`${f}: falta "${k}"`);
77
+ if (h.numero && !/^[1-9]\d*$/.test(h.numero)) errors.push(`${f}: numero inválido (${h.numero}); use inteiro a partir de 1`);
50
78
  if (h.data && !/^\d{4}-\d{2}-\d{2}$/.test(h.data)) errors.push(`${f}: data inválida (${h.data})`);
51
79
  if (h.status && !STATUS.includes(h.status)) errors.push(`${f}: status inválido (${h.status}); use ${STATUS.join(' | ')}`);
52
80
  const doms = Array.isArray(h.dominio) ? h.dominio : [h.dominio].filter(Boolean);
@@ -57,22 +85,27 @@ for (const f of readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'INDEX
57
85
  }
58
86
  adrs.push({ file: f, ...h, doms });
59
87
  }
88
+ const nums = adrs.map((a) => Number(a.numero)).filter(Number.isInteger).sort((a, b) => a - b);
89
+ for (let i = 0; i < nums.length; i++) {
90
+ if (i > 0 && nums[i] === nums[i - 1]) errors.push(`numero #${nums[i]} repetido`);
91
+ else if (nums[i] !== i + 1) { errors.push(`numeração com lacuna: esperado #${i + 1}, achei #${nums[i]}`); break; }
92
+ }
60
93
  if (errors.length) {
61
94
  console.error(errors.join('\n'));
62
95
  process.exit(1);
63
96
  }
64
- adrs.sort((a, b) => b.data.localeCompare(a.data) || b.file.localeCompare(a.file));
97
+ adrs.sort((a, b) => b.data.localeCompare(a.data) || Number(b.numero) - Number(a.numero));
65
98
 
66
99
  const list = (v) => (Array.isArray(v) ? v.join(', ') : v || '');
67
- const rows = adrs.map((a) => `| ${a.data} | [${a.titulo}](${a.file}) | ${a.doms.join(', ')} | ${a.status} | ${list(a['decidido-por'])} | ${list(a.consultados)} |`);
100
+ const rows = adrs.map((a) => `| ${a.numero} | ${a.data} | [${a.titulo}](${a.file}) | ${a.doms.join(', ')} | ${a.status} | ${list(a['decidido-por'])} | ${list(a.consultados)} |`);
68
101
  const body = `# Decisões (ADR)
69
102
 
70
- > Gerado por \`node .ai/skills/sg-adr/scripts/adr-index.mjs\` a partir do cabeçalho de cada ADR. Não edite à mão. Ordem: da mais nova para a mais antiga.
103
+ > Gerado por \`node .ai/skills/sg-adr/scripts/adr-index.mjs\` a partir do cabeçalho de cada ADR. Não edite à mão. Ordem: da mais nova para a mais antiga. O número \`#\` é sequencial, dado na criação e **nunca muda**: use-o para falar de uma ADR ("ADR #7").
71
104
 
72
105
  Para saber **por que** algo foi feito, procure pelo domínio (coluna 3) ou pelo módulo no cabeçalho de cada ADR.${DOMINIOS ? ` Domínios: ${DOMINIOS.join(', ')}.` : ''}
73
106
 
74
- | Data | Decisão | Domínio | Status | Decidido por | Consultados |
75
- |---|---|---|---|---|---|
107
+ | # | Data | Decisão | Domínio | Status | Decidido por | Consultados |
108
+ |---|---|---|---|---|---|---|
76
109
  ${rows.join('\n')}
77
110
  `;
78
111
  const target = path.join(dir, 'INDEX.md');
@@ -11,8 +11,9 @@ Cabeçalho (obrigatório, no topo do arquivo, entre `---`):
11
11
  ---
12
12
  description: Uma linha com a decisão.
13
13
  titulo: Título curto da decisão
14
+ numero: 1 # sequencial, dado na criação (--proximo); nunca muda
14
15
  data: AAAA-MM-DD
15
- status: proposta # proposta | aceita | substituida | rejeitada
16
+ status: proposta # proposta | aceita | implementada | substituida | rejeitada
16
17
  decidido-por: nome ou papel
17
18
  consultados: [nomes, quando houver]
18
19
  dominio: [seguranca] # lista do projeto (docs/ai/decisions/dominios.txt)
@@ -23,6 +24,8 @@ origem: plano, commit ou tarefa que gerou a decisão
23
24
  ---
24
25
  ```
25
26
 
27
+ Status: `proposta` (a IA propôs; o dono não confirmou); `aceita` (confirmada, ainda não está no código); `implementada` (confirmada e já no código; o que já foi implementado não fica `proposta`).
28
+
26
29
  ## Contexto
27
30
  Problema, forças em jogo e restrições.
28
31
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "contentVersion": "0.4.0",
3
+ "contentVersion": "0.4.2",
4
4
  "toolkitBase": "SeedGrid/Toolkit",
5
5
  "components": [
6
6
  {