synthesisui 0.16.414 → 0.16.415

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.
@@ -150,14 +150,16 @@ async function wireHook(root, command) {
150
150
  /**
151
151
  * O MESMO SERVIDOR MCP, NOS CAMINHOS QUE CADA AGENTE LÊ.
152
152
  *
153
- * `.mcp.json` é o escopo-de-projeto do Claude Code; o Cursor `.cursor/mcp.json` com o MESMO shape
153
+ * `.mcp.json` é o escopo-de-projeto do Claude Code. O Cursor lia `.cursor/mcp.json` com o mesmo shape
154
154
  * (`mcpServers`). Então cobrir os dois é o mesmo JSON num segundo caminho, e as ferramentas passam
155
155
  * a existir nos dois editores sem uma linha de motor novo - era a lacuna mais barata da análise de DX
156
156
  * (05/08).
157
157
  *
158
- * O SEGUNDO SE A PASTA EXISTIR. Criar `.cursor/` num repo de quem usa Claude Code é escrever
159
- * arquivo de um editor que a pessoa não usa, e é a primeira coisa que alguém apaga com raiva - a
160
- * mesma regra que vale para o `AGENTS.md`.
158
+ * O CURSOR SAIU EM 10/09, por decisão dele: a lista do produto é Claude Code e Codex (ver
159
+ * `agents-chosen.ts` e `cursor-is-retired.spec.ts`). O que sobreviveu daquela linha é a regra que
160
+ * ela ensinou - escrever arquivo de um editor que a pessoa não usa é a primeira coisa que alguém
161
+ * apaga com raiva -, e ela agora vale para TODOS, inclusive o Claude Code: quem decide é a escolha
162
+ * dele, e a pasta apenas pré-marca.
161
163
  */
162
164
  async function wireMcpAt(root, rel) {
163
165
  const path = join(root, rel);
@@ -209,24 +211,24 @@ async function wireMcpAt(root, rel) {
209
211
  return replacing ? "updated" : "added";
210
212
  }
211
213
  /**
212
- * Os caminhos de MCP que este repo merece: sempre o do Claude Code, e o do Cursor quando `.cursor/`
213
- * já existe. Devolve o que foi escrito, por caminho, para o `connect` poder dizer os nomes.
214
+ * Os caminhos de MCP que ELE ESCOLHEU - um por agente marcado, e nada dos outros. Devolve o que foi
215
+ * escrito, por caminho, para o `connect` poder dizer os nomes.
214
216
  */
215
217
  /**
216
218
  * E NENHUM DOS TRÊS RECEBE VERSÃO, desde 10/09 - o parâmetro morreu quando o Codex passou a seguir
217
219
  * a régua de `@latest` (ver `codex-mcp.ts`). Os escritores de JSON nunca a usaram: a decisão de
218
220
  * 06/08 é que o MCP flutua. Um argumento que ninguém lê é o próximo a ser lido por engano.
219
221
  */
220
- async function wireMcp(root) {
222
+ async function wireMcp(root, agents) {
221
223
  const wrote = [];
222
- wrote.push({
223
- path: ".mcp.json",
224
- status: await wireMcpAt(root, ".mcp.json"),
225
- });
226
- if (await exists(join(root, ".cursor")))
224
+ /**
225
+ * O `.mcp.json` É DO CLAUDE CODE, e por isso ele entrou na régua (A2 da etapa 12). Era o único
226
+ * escritor sem condição nenhuma: num repositório que só usa Codex, ele nascia de todo jeito.
227
+ */
228
+ if (agents.includes("claude"))
227
229
  wrote.push({
228
- path: ".cursor/mcp.json",
229
- status: await wireMcpAt(root, ".cursor/mcp.json"),
230
+ path: ".mcp.json",
231
+ status: await wireMcpAt(root, ".mcp.json"),
230
232
  });
231
233
  /**
232
234
  * E O CODEX, quando `.codex/` já existe - a mesma regra de evidência que decide o Cursor e a casa
@@ -236,7 +238,7 @@ async function wireMcp(root) {
236
238
  * região delimitada num TOML que a pessoa também edita. Ver `codex-mcp.ts` para o porquê de não
237
239
  * haver parser.
238
240
  */
239
- if (await exists(join(root, ".codex")))
241
+ if (agents.includes("codex"))
240
242
  wrote.push({
241
243
  path: ".codex/config.toml",
242
244
  status: await wireCodexMcp(root),
@@ -281,8 +283,17 @@ async function wireCodexMcp(root) {
281
283
  return had ? "updated" : "added";
282
284
  }
283
285
  export async function wireAgent(root, version, want) {
286
+ /**
287
+ * A ESCOLHA DELE DECIDE QUEM É FIADO (A2 da etapa 12) - ver `agents-chosen.ts`.
288
+ *
289
+ * O hook e a verificação de abertura moram no `.claude/settings.json`, então eles são do Claude
290
+ * Code: num repositório onde ele não escolheu o Claude Code, escrever aquele arquivo é plantar a
291
+ * fiação de uma ferramenta que ninguém pediu. Medido em 10/09: eram 8 arquivos dele num repo que
292
+ * só provava o Codex.
293
+ */
294
+ const claude = want.agents.includes("claude");
284
295
  const proposed = await hookCommand(root, version);
285
- const hook = want.hook
296
+ const hook = want.hook && claude
286
297
  ? await wireHook(root, proposed)
287
298
  : { status: "skipped", command: proposed, was: undefined };
288
299
  return {
@@ -296,10 +307,10 @@ export async function wireAgent(root, version, want) {
296
307
  * único hook instalado rodava DEPOIS de uma escrita - tarde demais para dizer "você não está
297
308
  * logado" ou "este sistema não sabe de onde foi medido".
298
309
  */
299
- session: want.hook
310
+ session: want.hook && claude
300
311
  ? await wireSessionStart(root, await alignCommand(root, version))
301
312
  : "skipped",
302
- mcp: want.mcp ? await wireMcp(root) : "skipped",
313
+ mcp: want.mcp ? await wireMcp(root, want.agents) : "skipped",
303
314
  };
304
315
  }
305
316
  /**
@@ -0,0 +1,246 @@
1
+ /**
2
+ * QUAIS AGENTES ENTRAM NO REPOSITÓRIO DELE - e a resposta é a ESCOLHA dele, não a nossa dedução.
3
+ *
4
+ * O QUE ELE GANHA: nenhum arquivo de uma ferramenta que ele não usa. MEDIDO em 10/09, com o
5
+ * `connect` de verdade num repositório que só tem `.codex/`: **10 arquivos escritos, e 8 do Claude
6
+ * Code** - `.claude/settings.json`, cinco skills, `.mcp.json` e `CLAUDE.md`. A régua da evidência
7
+ * já existia e protegia o Codex e o Cursor; o Claude Code era escrito sempre, sem prova nenhuma.
8
+ *
9
+ * A DECISÃO DELE, 10/09: *"seria legal o próprio connect eu selecionar quais agentes eu quero
10
+ * colocar"*. Então a evidência deixa de DECIDIR e passa a PRÉ-MARCAR - ela continua sendo a melhor
11
+ * informação que a gente tem sobre o repositório dele, e deixa de ser a palavra final.
12
+ *
13
+ * ─────────────────────────────────────────────────────────────────────────
14
+ * DE ONDE A ESCOLHA VEM, e a saída SEMPRE diz qual dos três foi:
15
+ *
16
+ * flag ele mandou (`--agents claude,codex`, ou `--agents none`)
17
+ * asked ele confirmou ou mudou a lista pré-marcada, num terminal
18
+ * evidence não havia ninguém para perguntar - CI, pipe, agente -, então a pasta que
19
+ * existe decide, e a linha DIZ que foi ela
20
+ *
21
+ * O silêncio nunca é escolha: um comando que não pode perguntar e escreve tudo é o defeito que
22
+ * este módulo existe para acabar, e um que não pode perguntar e não escreve nada quebraria o CI de
23
+ * quem já usa. A evidência é o meio-termo que já era o comportamento de duas das ferramentas.
24
+ *
25
+ * A LISTA É DO PRODUTO, e é curta de propósito: o nome de uma ferramenta não decide nada sozinho -
26
+ * o que decide é o que ELE escolheu. O Cursor saiu em 10/09 por decisão dele (ver
27
+ * `cursor-is-retired.spec.ts`), e uma quarta ferramenta é ESCOPO DE CAPACIDADE: ela entra quando o
28
+ * produto passar a suportá-la, e essa pergunta é dele.
29
+ */
30
+ import { access } from "node:fs/promises";
31
+ import { join } from "node:path";
32
+ export const AGENTS = [
33
+ {
34
+ id: "claude",
35
+ label: "Claude Code",
36
+ evidence: ".claude",
37
+ writes: [
38
+ /** O hook e a verificação de abertura moram aqui, junto dos hooks DELE. */
39
+ { path: ".claude/settings.json", ours: "entry" },
40
+ /** Junto dos outros servidores MCP dele. */
41
+ { path: ".mcp.json", ours: "entry" },
42
+ { path: "CLAUDE.md", ours: "block" },
43
+ /** A pasta é nossa inteira: o conteúdo é o nosso texto, e ninguém edita skill gerada. */
44
+ { path: ".claude/skills/", ours: "file" },
45
+ ],
46
+ },
47
+ {
48
+ id: "codex",
49
+ label: "Codex",
50
+ evidence: ".codex",
51
+ writes: [
52
+ /** O `[tui]` dele mora neste arquivo - ver `Ours`. */
53
+ { path: ".codex/config.toml", ours: "block" },
54
+ { path: "AGENTS.md", ours: "block" },
55
+ ],
56
+ },
57
+ ];
58
+ const exists = (p) => access(p).then(() => true, () => false);
59
+ /** O que o repositório PROVA - a pré-marcação, nunca a decisão. */
60
+ export async function detected(root) {
61
+ const out = [];
62
+ for (const agent of AGENTS)
63
+ if (await exists(join(root, agent.evidence)))
64
+ out.push(agent.id);
65
+ return out;
66
+ }
67
+ /**
68
+ * O QUE ELE DIGITOU, lido do jeito mais largo possível - `claude,codex`, `claude codex`, `CLAUDE`.
69
+ *
70
+ * Um nome que a lista não conhece é DEVOLVIDO em `unknown` em vez de ignorado: ignorar em silêncio
71
+ * é o que faz alguém escrever `--agents cursor` e concluir que o comando obedeceu.
72
+ */
73
+ export function parseAgents(raw) {
74
+ const words = raw
75
+ .split(/[\s,]+/)
76
+ .map((w) => w.trim().toLowerCase())
77
+ .filter(Boolean);
78
+ if (words.length === 1 && words[0] === "none")
79
+ return { agents: [], unknown: [], none: true };
80
+ const agents = [];
81
+ const unknown = [];
82
+ for (const word of words) {
83
+ const hit = AGENTS.find((a) => a.id === word);
84
+ if (hit) {
85
+ if (!agents.includes(hit.id))
86
+ agents.push(hit.id);
87
+ }
88
+ else
89
+ unknown.push(word);
90
+ }
91
+ return { agents, unknown, none: false };
92
+ }
93
+ /** Os arquivos que a fiação de um agente escreve - `[]` para um id que a lista não conhece. */
94
+ export const filesOf = (id) => AGENTS.find((a) => a.id === id)?.writes ?? [];
95
+ /**
96
+ * QUAIS AGENTES JÁ ESTÃO FIADOS NESTE REPOSITÓRIO - a memória da escolha, e ela é o próprio disco.
97
+ *
98
+ * A PREMISSA, declarada na abertura da etapa 12: a escolha é lembrada pelo que EXISTE no
99
+ * repositório, sem arquivo de preferência nosso. Um arquivo desses no repositório dele
100
+ * contraria a seção 0 da jornada, e `~/.synthesisui/` é preferência de MÁQUINA - a escolha é do
101
+ * projeto, e vai para outra máquina com o `git clone`.
102
+ *
103
+ * O TESTE É A NOSSA MARCA, nunca a existência do arquivo. Um `CLAUDE.md` existe em muitos
104
+ * repositórios sem ter uma linha nossa dentro, e `.codex/config.toml` é arquivo DELE com a nossa
105
+ * região no meio - concluir "está fiado" de um arquivo que a gente não escreveu faria os comandos de
106
+ * manutenção reescreverem a fiação de um agente que ele nunca escolheu.
107
+ *
108
+ * QUEM USA ISTO: os comandos que MANTÊM o que já existe (`add`, `adopt`, `upgrade`) - eles não
109
+ * perguntam nada, e a resposta certa para eles é *"continue com quem já está aqui"* - e a
110
+ * PRÉ-MARCAÇÃO da lista do `connect`, porque desmarcar não apaga a pasta: sem isto, a rodada
111
+ * seguinte re-marcava o que ele tinha acabado de tirar (revisão de DX, 10/09).
112
+ */
113
+ export async function wiredAgents(root) {
114
+ const { readFile } = await import("node:fs/promises");
115
+ const has = async (rel, mark) => {
116
+ const raw = await readFile(join(root, rel), "utf8").catch(() => null);
117
+ return raw?.includes(mark) === true;
118
+ };
119
+ const out = [];
120
+ if ((await has(".mcp.json", "synthesisui")) ||
121
+ (await has("CLAUDE.md", MARK)) ||
122
+ (await has(".claude/settings.json", "synthesisui")))
123
+ out.push("claude");
124
+ if ((await has(".codex/config.toml", "synthesisui")) ||
125
+ (await has("AGENTS.md", MARK)))
126
+ out.push("codex");
127
+ return out;
128
+ }
129
+ /** A marca do bloco gerido - a mesma de `claude-md.ts`, e a única prova de que o texto é nosso. */
130
+ const MARK = "<!-- synthesisui:start -->";
131
+ /**
132
+ * A ESCOLHA, RESOLVIDA - e a saída sempre diz de onde ela veio.
133
+ *
134
+ * Três caminhos, e a ordem é a da autoridade: o que ele MANDOU vence o que ele CONFIRMA, que vence
135
+ * o que a pasta prova. `ask` é injetado para o `connect` decidir se há alguém para perguntar - um
136
+ * módulo que consulta `process.stdin` por conta própria não tem como ser provado sem um terminal.
137
+ */
138
+ export async function chooseAgents(input) {
139
+ if (input.flag != null && input.flag.trim() !== "") {
140
+ const read = parseAgents(input.flag);
141
+ return { agents: read.agents, from: "flag", unknown: read.unknown };
142
+ }
143
+ /**
144
+ * A PRÉ-MARCAÇÃO É O QUE ESTÁ FIADO, quando há algo fiado - e só então a pasta.
145
+ *
146
+ * O DEFEITO, achado pela revisão de DX no fecho: a pré-marcação usava só a pasta, e desmarcar não
147
+ * apaga a pasta (é a decisão dele). Então a rodada seguinte re-marcava o que ele tinha acabado de
148
+ * tirar, e `Enter` - que a tela chama de *"keeps this"* - revertia a escolha dele sem ele digitar
149
+ * nada. A premissa desta etapa já dizia o certo: *"a escolha é derivada do que EXISTE no
150
+ * repositório (os arquivos fiados são a memória)"*.
151
+ *
152
+ * A ordem é a da autoridade, como em todo este módulo: o que ele REGISTROU vence o que sobrou no
153
+ * disco, que vence o que a pasta sugere - ver `rememberedChoice`, que é o que distingue "ele
154
+ * desmarcou" de "sobrou arquivo". Num repositório sem fiação nenhuma - o primeiro contato - não
155
+ * há escolha anterior, e aí a pasta é a melhor informação que existe.
156
+ */
157
+ const remembered = await rememberedChoice(input.root);
158
+ const wired = await wiredAgents(input.root);
159
+ const pre = remembered ?? (wired.length > 0 ? wired : await detected(input.root));
160
+ if (!input.ask)
161
+ return { agents: pre, from: "evidence", unknown: [] };
162
+ const typed = await input.ask(pre);
163
+ /** Enter mantém a pré-marcação - a confirmação é uma tecla, e continua sendo escolha dele. */
164
+ if (typed.trim() === "")
165
+ return { agents: pre, from: "asked", unknown: [] };
166
+ const read = parseAgents(typed);
167
+ return { agents: read.agents, from: "asked", unknown: read.unknown };
168
+ }
169
+ /**
170
+ * A ESCOLHA REGISTRADA - e é ela que distingue "ele desmarcou" de "sobrou arquivo no disco".
171
+ *
172
+ * O DEFEITO QUE ISTO CONSERTA, medido pela revisão de QA no fecho da etapa 12, e ele é o caso que
173
+ * originou a jornada: quem JÁ TEM os oito arquivos do Claude Code e os desmarca. `wiredAgents` lê a
174
+ * nossa marca nos arquivos - e A5 promete que desmarcar NÃO apaga arquivo nenhum. Então a memória
175
+ * respondia "fiado" para exatamente quem tinha acabado de dizer "não quero":
176
+ *
177
+ * ```
178
+ * 1) escolhe claude wiredAgents -> [claude]
179
+ * 2) roda de novo, desmarca claude wiredAgents -> [claude, codex] <- a sobra fala por ele
180
+ * 3) a rodada seguinte pre-marca -> [claude, codex]
181
+ * 4) `Enter` ("keeps this") devolve o que ele tirou
182
+ * ```
183
+ *
184
+ * E o `upgrade` andava pela mesma porta: ele mantém `wiredAgents`, então re-pinava o hook que ele
185
+ * tinha recusado - a gente não só não parava, a gente ATUALIZAVA o que ele recusou.
186
+ *
187
+ * A PREMISSA DA ABERTURA JÁ TINHA ESCRITO A SAÍDA: *"a escolha é derivada do que EXISTE no
188
+ * repositório... se estiver errado: nasce um campo em `_synthesisui/config.json`, que já é nosso e
189
+ * já está no repo dele"*. A medição provou que estava errada, e este é o campo.
190
+ *
191
+ * ELE VAI NO REPOSITÓRIO, e não na máquina: a escolha é do PROJETO - vai junto no `git clone`, e o
192
+ * time inteiro herda. `~/.synthesisui/` é preferência de máquina, e seria a resposta errada aqui.
193
+ *
194
+ * A leitura e a escrita preservam o resto do arquivo: `config.json` é nosso, mas carrega o alvo, as
195
+ * pastas e o modo de estilo dele, e reserializar o objeto inteiro a partir de um tipo parcial
196
+ * apagaria campo que um CLI mais novo grave.
197
+ */
198
+ const CONFIG = ["_synthesisui", "config.json"];
199
+ export async function rememberedChoice(root) {
200
+ const { readFile } = await import("node:fs/promises");
201
+ const raw = await readFile(join(root, ...CONFIG), "utf8").catch(() => null);
202
+ if (!raw)
203
+ return null;
204
+ try {
205
+ const parsed = JSON.parse(raw);
206
+ if (!Array.isArray(parsed.agents))
207
+ return null;
208
+ /** Um nome que a lista não conhece mais - um Cursor gravado antes da retirada - some aqui. */
209
+ return parsed.agents.filter((a) => AGENTS.some((known) => known.id === a));
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
215
+ export async function rememberChoice(root, agents) {
216
+ const { mkdir, readFile, writeFile } = await import("node:fs/promises");
217
+ const path = join(root, ...CONFIG);
218
+ const raw = await readFile(path, "utf8").catch(() => null);
219
+ let current = {};
220
+ if (raw) {
221
+ try {
222
+ current = JSON.parse(raw);
223
+ }
224
+ catch {
225
+ /** Um config ilegível não é apagado por nós - a escolha simplesmente não é lembrada. */
226
+ return;
227
+ }
228
+ }
229
+ await mkdir(join(root, CONFIG[0]), { recursive: true }).catch(() => { });
230
+ await writeFile(path, `${JSON.stringify({ ...current, agents: [...agents] }, null, 2)}\n`, "utf8");
231
+ }
232
+ /**
233
+ * COM QUEM OS COMANDOS DE MANUTENÇÃO CONTINUAM - `add`, `adopt`, `upgrade`.
234
+ *
235
+ * A ordem é a da autoridade: o que ele REGISTROU vence o que sobrou no disco, que vence a pasta.
236
+ * O último degrau existe para o primeiro contato - um `adopt` num repositório que nunca viu o
237
+ * `connect` não pode ficar sem casa nenhuma, senão o comando escreve o sistema e diz *"seu agente
238
+ * agora conhece o seu sistema"* sobre um repositório onde nenhum agente lê nada (medido em 10/09).
239
+ */
240
+ export async function agentsToMaintain(root) {
241
+ const remembered = await rememberedChoice(root);
242
+ if (remembered)
243
+ return remembered;
244
+ const wired = await wiredAgents(root);
245
+ return wired.length > 0 ? wired : await detected(root);
246
+ }
package/dist/claude-md.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { exists, hasHook } from "./agent-wiring.js";
3
+ import { hasHook } from "./agent-wiring.js";
4
4
  import { declaredReference } from "./group-role.js";
5
5
  import { recallAvailable } from "./memory/availability.js";
6
6
  const START = "<!-- synthesisui:start -->";
@@ -564,28 +564,39 @@ _Block managed by the CLI - do not edit by hand; run \`synthesisui ${onlyAdopted
564
564
  * ONDE O BLOCO GERENCIADO MORA, POR AGENTE.
565
565
  *
566
566
  * O bloco é o mesmo texto - as regras que um agente lê ANTES de escrever UI - e cada ferramenta
567
- * escolheu um arquivo diferente para lê-lo. Escrever nos três custa dois `writeFile` a mais e
567
+ * escolheu um arquivo diferente para lê-lo. Escrever nos dois custa um `writeFile` a mais e
568
568
  * multiplica os editores cobertos sem escrever motor novo, que era a lacuna mais barata da análise de
569
569
  * DX (05/08): a governança só chegava a quem usa Claude Code.
570
570
  *
571
- * `AGENTS.md` é o nome que Codex e vários outros leem; `.cursor/rules/*.mdc` é o do Cursor, e ele
572
- * exige frontmatter com `alwaysApply` senão a regra fica dormindo até alguém citá-la.
571
+ * `CLAUDE.md` é o do Claude Code; `AGENTS.md` é o nome que Codex e vários outros leem. O
572
+ * `.cursor/rules/*.mdc` do Cursor saiu em 10/09, por decisão dele - ver `agents-chosen.ts`.
573
+ *
574
+ * E QUEM DECIDE QUAIS DESTAS CASAS SÃO ESCRITAS É A ESCOLHA DELE (A2 da etapa 12), não a pasta:
575
+ * `syncClaudeMd` recebe os agentes marcados. A pasta continua sendo a melhor pré-marcação que a
576
+ * gente tem, e deixou de ser a palavra final.
573
577
  */
574
578
  const HOMES = [
575
- { path: "CLAUDE.md" },
576
- { path: "AGENTS.md", bornWhen: ".codex" },
577
- {
578
- path: ".cursor/rules/synthesisui.mdc",
579
- frontmatter: "---\ndescription: Design system rules - read before writing UI\nalwaysApply: true\n---\n\n",
580
- bornWhen: ".cursor",
581
- },
579
+ { path: "CLAUDE.md", agent: "claude" },
580
+ { path: "AGENTS.md", agent: "codex" },
582
581
  ];
583
- export async function syncClaudeMd(projectRoot) {
582
+ export async function syncClaudeMd(projectRoot,
583
+ /**
584
+ * OS AGENTES QUE ELE ESCOLHEU - e é a escolha que autoriza cada casa (A2 da etapa 12).
585
+ *
586
+ * Argumento OBRIGATÓRIO, e não opcional com default: um default aqui seria a régua velha
587
+ * sobrevivendo escondida no dia em que um chamador novo esquecer de passar a escolha - e a régua
588
+ * velha é exatamente o que fazia 8 arquivos do Claude Code entrarem num repositório de quem usa
589
+ * Codex. O `tsc` recusa a meia-chamada (lei 6: fechar a porta em vez de vigiá-la).
590
+ */
591
+ agents) {
584
592
  const installed = await readInstalled(projectRoot);
585
593
  const region = await renderRegion(projectRoot, installed);
586
594
  let createdAny = false;
587
595
  const changed = [];
588
596
  for (const home of HOMES) {
597
+ /** A casa de um agente que ele não escolheu não é escrita nem reescrita. */
598
+ if (!agents.includes(home.agent))
599
+ continue;
589
600
  const path = join(projectRoot, home.path);
590
601
  let existing = null;
591
602
  try {
@@ -594,20 +605,7 @@ export async function syncClaudeMd(projectRoot) {
594
605
  catch {
595
606
  existing = null;
596
607
  }
597
- /**
598
- * O ARQUIVO NOVO NASCE QUANDO O REPOSITÓRIO JÁ MOSTROU QUEM ELE USA - ver `bornWhen`.
599
- *
600
- * `CLAUDE.md` sem condição, porque é a casa que a gente cria num repo que não tem nenhuma. Os
601
- * outros dois só quando a pasta da ferramenta está lá: criar `AGENTS.md` num repo que só usa
602
- * Claude Code continua sendo decidir por outra pessoa qual agente ela usa, e um arquivo que
603
- * apareceu sozinho na raiz é a primeira coisa que alguém apaga com raiva.
604
- */
605
608
  if (existing === null) {
606
- const authorised = home.path === "CLAUDE.md" ||
607
- (home.bornWhen != null &&
608
- (await exists(join(projectRoot, home.bornWhen))));
609
- if (!authorised)
610
- continue;
611
609
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
612
610
  await writeFile(path, `${home.frontmatter ?? ""}${region}\n`, "utf8");
613
611
  createdAny = true;
@@ -666,9 +664,16 @@ const OPENS = {
666
664
  /**
667
665
  * Os comandos que abrem os agentes que ESTE repositório mostra ter - vazio quando nenhum deles
668
666
  * carrega o bloco, e aí a saída não fala de abrir nada.
667
+ *
668
+ * E FILTRADO PELA ESCOLHA quando quem chama a conhece (etapa 12): o bloco de um agente desmarcado
669
+ * CONTINUA no arquivo, porque desmarcar não apaga nada. Sem o filtro, a tela recomendava abrir a
670
+ * ferramenta que ele tinha acabado de tirar - achado da revisão de QA, 10/09.
669
671
  */
670
- export async function agentOpenCommands(projectRoot) {
671
- const homes = await blockHomes(projectRoot);
672
+ export async function agentOpenCommands(projectRoot, agents) {
673
+ const allowed = agents
674
+ ? new Set(HOMES.filter((h) => agents.includes(h.agent)).map((h) => h.path))
675
+ : null;
676
+ const homes = (await blockHomes(projectRoot)).filter((h) => allowed === null || allowed.has(h));
672
677
  return homes.map((h) => OPENS[h]).filter((c) => Boolean(c));
673
678
  }
674
679
  export async function blockHomes(projectRoot) {
@@ -1,5 +1,6 @@
1
1
  import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { agentsToMaintain } from "../agents-chosen.js";
3
4
  import { syncClaudeMd } from "../claude-md.js";
4
5
  import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
5
6
  import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
@@ -432,7 +433,7 @@ export async function add(slug, opts) {
432
433
  * (`doctrine.json`): o doctor e o hook precisam delas offline.
433
434
  */
434
435
  // 6. discovery by the agent
435
- const claudeMd = await syncClaudeMd(projectRoot);
436
+ const claudeMd = await syncClaudeMd(projectRoot, await agentsToMaintain(projectRoot));
436
437
  // outcome line
437
438
  const v = payload.version;
438
439
  if (!prev) {
@@ -1,8 +1,9 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
+ import { agentsToMaintain } from "../agents-chosen.js";
3
4
  import { syncClaudeMd } from "../claude-md.js";
4
5
  import { parseRootTokens } from "../doctor/tokens.js";
5
- import { body, section, snippet } from "../output.js";
6
+ import { body, paint, section, snippet } from "../output.js";
6
7
  import { walk } from "./doctor.js";
7
8
  /**
8
9
  * `synthesisui adopt` - the system somebody ALREADY has, made legible.
@@ -239,12 +240,27 @@ export async function adopt(opts) {
239
240
  // `version: 0` marks a system nobody published - the doctor and CLAUDE.md
240
241
  // read this to know it is adopted rather than installed.
241
242
  await writeFile(join(dir, ".lock"), `${JSON.stringify({ slug, name, version: 0, adopted: true }, null, 2)}\n`, "utf8");
242
- await syncClaudeMd(root);
243
+ /**
244
+ * A LINHA SAI DO QUE FOI ESCRITO, e não de uma promessa fixa - achado da revisão de QA no fecho
245
+ * da etapa 12, e é uma regressão que ela mesma criou.
246
+ *
247
+ * Desde que as casas do bloco passaram a obedecer à escolha dele, um repositório de PRIMEIRO
248
+ * CONTATO - nenhuma escolha registrada, nenhuma pasta de agente - não recebe casa nenhuma. E
249
+ * estas três linhas eram fixas: o comando dizia `✓ CLAUDE.md managed block added` e *"seu agente
250
+ * agora conhece o seu sistema"* sobre um repositório onde nenhum agente lê nada. O `adopt` é a
251
+ * porta que a home anuncia para quem quer o contrato sem conta.
252
+ */
253
+ const claudeMd = await syncClaudeMd(root, await agentsToMaintain(root));
243
254
  console.log("");
244
255
  console.log(body(`✓ _synthesisui/ds/${slug}/ written`));
245
- console.log(body("✓ CLAUDE.md managed block added"));
256
+ for (const home of claudeMd.changed)
257
+ console.log(body(`✓ ${home.padEnd(22)} managed block added`));
246
258
  console.log("");
247
- console.log(body("Your agent now knows your system. Nothing of yours changed."));
259
+ console.log(body(claudeMd.changed.length > 0
260
+ ? "Your agent now knows your system. Nothing of yours changed."
261
+ : "Nothing of yours changed - and no agent home was written, because this repo does not say which agent you use yet:"));
262
+ if (claudeMd.changed.length === 0)
263
+ console.log(body(paint.blue(" npx synthesisui@latest connect")));
248
264
  console.log("");
249
265
  console.log(snippet(["npx synthesisui@latest doctor"]));
250
266
  console.log(body("see what it makes measurable"));
@@ -712,7 +712,13 @@ opts = {}) {
712
712
  * repositório inteiro, e isto roda em TODA abertura de sessão. Um aviso que custa um walk completo
713
713
  * a cada sessão é um aviso que alguém desliga.
714
714
  */
715
- export async function nextStepFor(root) {
715
+ export async function nextStepFor(root,
716
+ /**
717
+ * OS AGENTES QUE ELE ESCOLHEU, quando quem chama sabe (etapa 12) - o bloco de um agente
718
+ * desmarcado continua no arquivo, porque desmarcar não apaga nada, e sem este filtro a tela
719
+ * recomendava abrir a ferramenta que ele tinha acabado de tirar.
720
+ */
721
+ agents) {
716
722
  const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
717
723
  if (locks.length > 0)
718
724
  return null;
@@ -742,7 +748,7 @@ export async function nextStepFor(root) {
742
748
  : {
743
749
  sentence: 'This repo has no design system contract yet. Ask me: "import my design system."',
744
750
  headline: "Open a new agent session in this repo",
745
- open: await agentOpenCommands(root).catch(() => []),
751
+ open: await agentOpenCommands(root, agents).catch(() => []),
746
752
  say: "import my design system",
747
753
  why: "I read what is already in your code - the colours, the type, the shapes, the components. Nothing is invented.",
748
754
  };
@@ -830,7 +836,7 @@ export function renderNextStep(next, freshSession) {
830
836
  */
831
837
  export async function reportWhatIsLeft(root, opts = {}) {
832
838
  const items = await misalignments(root, opts).catch(() => []);
833
- const next = await nextStepFor(root).catch(() => null);
839
+ const next = await nextStepFor(root, opts.agents).catch(() => null);
834
840
  /**
835
841
  * A AÇÃO RECOMENDADA GANHA DA LISTA - e a falta desta regra foi a regressão que o dono leu na
836
842
  * 0.16.365.
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { codexPinBefore, wireAgent } from "../agent-wiring.js";
3
+ import { codexPinBefore, exists, wireAgent } from "../agent-wiring.js";
4
+ import { AGENTS, chooseAgents, filesOf, rememberChoice, } from "../agents-chosen.js";
4
5
  import { blockHomes, syncClaudeMd } from "../claude-md.js";
5
6
  import { resolveRegistry } from "../config.js";
6
7
  import { notAProject, projectRootFrom } from "../is-a-project.js";
@@ -211,6 +212,53 @@ version) {
211
212
  await writeFile(rc, withHook(current ?? "", shellSnippet(shell, version)), "utf8");
212
213
  console.log(body(`Written. Open a new terminal, or run: source ${rc}`));
213
214
  }
215
+ /**
216
+ * A LISTA, E ELE DECIDE (A1 da etapa 12) - com o que o repositório PROVA já marcado.
217
+ *
218
+ * A pré-marcação é a evidência: ela continua sendo a melhor informação que a gente tem sobre o
219
+ * repositório dele, e deixou de ser a palavra final. Enter confirma - a confirmação é uma tecla, e
220
+ * continua sendo escolha dele -, e digitar os nomes substitui a lista.
221
+ *
222
+ * SEM BIBLIOTECA DE CHECKBOX, de propósito: um seletor com setas exige controlar o terminal em raw
223
+ * mode, e este comando roda em terminal de CI, em terminal dentro de editor e em terminal de quem
224
+ * usa Windows. Uma linha por agente e uma pergunta é o que funciona nos três.
225
+ */
226
+ async function askWhichAgents(pre) {
227
+ console.log("");
228
+ console.log(section(say("Which agents")));
229
+ for (const agent of AGENTS) {
230
+ const on = pre.includes(agent.id);
231
+ console.log(body(`${on ? "[x]" : "[ ]"} ${agent.label.padEnd(13)} ${on
232
+ ? fmt(say("{dir} is here"), { dir: agent.evidence })
233
+ : fmt(say("no {dir} in this repo"), { dir: agent.evidence })}`));
234
+ }
235
+ console.log("");
236
+ console.log(body(say("Enter keeps this. To change it, type the ones you want - or `none`:")));
237
+ console.log(body(paint.faint(` ${AGENTS.map((a) => a.id).join(" ")}`)));
238
+ const { createInterface } = await import("node:readline/promises");
239
+ const rl = createInterface({
240
+ input: process.stdin,
241
+ output: process.stdout,
242
+ });
243
+ const typed = await rl.question(" > ").catch(() => "");
244
+ rl.close();
245
+ return typed;
246
+ }
247
+ /**
248
+ * O QUE FICOU ESCOLHIDO, ecoado antes de agir - achado da revisão de DX no fecho.
249
+ *
250
+ * A frase da lista já diz que digitar SUBSTITUI o conjunto, e o risco não é de vocabulário: é de
251
+ * hábito. Quem está acostumado com seletor incremental vê `[x] Codex` marcado, digita `claude`
252
+ * querendo acrescentar, e perde o Codex. A consequência aparecia embaixo, misturada com as outras
253
+ * linhas; uma linha só, antes de qualquer escrita, torna o engano visível no lugar onde ele
254
+ * aconteceu.
255
+ */
256
+ function echoChoice(agents) {
257
+ console.log("");
258
+ console.log(body(fmt(say("Selected: {agents}"), {
259
+ agents: agents.length > 0 ? agents.join(" ") : "none",
260
+ })));
261
+ }
214
262
  export async function connect(opts) {
215
263
  const root = opts.dir ?? process.cwd();
216
264
  /**
@@ -234,7 +282,36 @@ export async function connect(opts) {
234
282
  }
235
283
  // Both unless one is explicitly turned off - somebody who says `--no-hook`
236
284
  // means it, and somebody who says nothing wants the thing to work.
237
- const want = { hook: opts.hook !== false, mcp: opts.mcp !== false };
285
+ const layers = { hook: opts.hook !== false, mcp: opts.mcp !== false };
286
+ /**
287
+ * QUAIS AGENTES ENTRAM - a escolha DELE, e nunca a nossa dedução (A1..A4 da etapa 12).
288
+ *
289
+ * MEDIDO em 10/09, com este comando num repositório que só tem `.codex/`: 10 arquivos escritos, e
290
+ * 8 do Claude Code. A régua da evidência protegia o Codex e o Cursor; o Claude Code era escrito
291
+ * sempre. A decisão dele: *"seria legal o próprio connect eu selecionar quais agentes eu quero
292
+ * colocar"*.
293
+ *
294
+ * A PERGUNTA SÓ EXISTE ONDE HÁ ALGUÉM PARA RESPONDER. Em `--ci`, num pipe ou dentro de um agente,
295
+ * `ask` vai `null` e a pasta decide - e a linha DIZ que foi ela, porque silêncio nunca é escolha.
296
+ */
297
+ const interactive = !opts.ci && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
298
+ const chosen = await chooseAgents({
299
+ root,
300
+ flag: opts.agents,
301
+ ask: interactive ? (pre) => askWhichAgents(pre) : null,
302
+ });
303
+ const want = { ...layers, agents: chosen.agents };
304
+ if (chosen.from === "asked")
305
+ echoChoice(chosen.agents);
306
+ /**
307
+ * A ESCOLHA FICA REGISTRADA quando foi ELE quem a fez - ver `rememberChoice`.
308
+ *
309
+ * Só `asked` e `flag`: uma dedução da pasta não é escolha, e gravá-la faria a próxima rodada
310
+ * tratar o palpite de hoje como decisão dele. É a mesma linha que separa `from: "evidence"` dos
311
+ * outros dois em toda esta etapa.
312
+ */
313
+ if (chosen.from !== "evidence")
314
+ await rememberChoice(root, chosen.agents).catch(() => { });
238
315
  /**
239
316
  * OS ARQUIVOS DO INSTALL ANTES DA FIAÇÃO: o `add` reescreve o `CLAUDE.md` por dentro, e o
240
317
  * `syncClaudeMd` abaixo tem que ser o último a falar sobre ele.
@@ -251,8 +328,26 @@ export async function connect(opts) {
251
328
  const wired = await wireAgent(root, opts.version, want);
252
329
  // The block reads the settings we just wrote, so it must be regenerated
253
330
  // after them, not before.
254
- const contract = await syncClaudeMd(root);
331
+ const contract = await syncClaudeMd(root, chosen.agents);
255
332
  console.log(section(say("Connected")));
333
+ /**
334
+ * NENHUM AGENTE ESCOLHIDO NÃO É COMANDO QUEBRADO (A3) - a frase diz o que falta e o caminho.
335
+ *
336
+ * Um comando que não escreve nada e não explica lê como defeito. E o caso é real: um repositório
337
+ * sem `.claude/` e sem `.codex/` não prova ferramenta nenhuma, e num CI não há quem responder.
338
+ */
339
+ if (chosen.agents.length === 0)
340
+ console.log(body(say(chosen.from === "evidence"
341
+ ? "· no agent chosen, and nothing here says which agent you use - run with `--agents claude` or `--agents codex`, or run this in a terminal to pick from the list"
342
+ : "· no agent chosen - nothing was wired. Run again with `--agents claude` or `--agents codex` when you want it")));
343
+ /** E DE ONDE A ESCOLHA VEIO, quando não foi ele quem respondeu: silêncio não é escolha. */
344
+ if (chosen.agents.length > 0 && chosen.from === "evidence")
345
+ console.log(body(paint.faint(fmt(say("· nobody to ask here, so the folders in this repo decided: {agents}"), { agents: chosen.agents.join(", ") }))));
346
+ /** UM NOME QUE A LISTA NÃO CONHECE VOLTA DITO - engoli-lo faria `--agents cursor` parecer aceito. */
347
+ if (chosen.unknown.length > 0)
348
+ console.log(body(fmt(say("· not an agent this CLI knows: {names}"), {
349
+ names: chosen.unknown.join(", "),
350
+ })));
256
351
  /**
257
352
  * A LISTA DA TELA, JUNTADA ANTES DE IMPRIMIR - e é isto que faz a rodada silenciosa caber numa
258
353
  * linha.
@@ -327,7 +422,16 @@ export async function connect(opts) {
327
422
  row(true, `✓ _synthesisui/ds/${refreshed.slug}/ rewritten by this CLI${refreshed.was
328
423
  ? ` - it was written by ${refreshed.was}`
329
424
  : " - it carried no CLI version, so it predates this"}`);
330
- if (want.hook) {
425
+ /**
426
+ * O HOOK É DO CLAUDE CODE, então a linha dele só sai quando ele foi escolhido (etapa 12).
427
+ *
428
+ * MEDIDO na primeira rodada real desta etapa, num repositório que só prova o Codex: a tela dizia
429
+ * `· Claude Code not chosen - nothing of it was written` e, três linhas abaixo,
430
+ * `· .claude/settings.json already had it` com o comando do hook embaixo. É a mesma contradição
431
+ * de duas frases que a etapa 11 fechou no `template`, aparecendo na tela que a 12 acabou de
432
+ * escrever - e `wired.hook` diz `skipped`, então a informação para suprimir já estava aqui.
433
+ */
434
+ if (want.hook && wired.hook !== "skipped") {
331
435
  row(wired.hook !== "already there", (() => {
332
436
  return wired.hook === "added"
333
437
  ? say("✓ .claude/settings.json the check now runs after every write")
@@ -431,7 +535,18 @@ export async function connect(opts) {
431
535
  * repositório é uma skill que só nós rodamos, e o caso de uso inteiro dela é o cliente rodando
432
536
  * sozinho. Meia jornada não é meio valor.
433
537
  */
434
- const skills = SKILLS;
538
+ /**
539
+ * AS SKILLS SÃO DO CLAUDE CODE, e por isso elas obedecem à escolha (A2 da etapa 12).
540
+ *
541
+ * O caminho é `.claude/skills/<nome>/SKILL.md` - a casa de UMA ferramenta. Cinco arquivos numa
542
+ * pasta de um editor que a pessoa não usa era metade dos 8 que a medição de 10/09 encontrou num
543
+ * repositório que só prova o Codex.
544
+ *
545
+ * O DIA EM QUE OUTRA FERRAMENTA TIVER SKILLS, a lista de `agents-chosen.ts` ganha o caminho dela e
546
+ * este laço passa a andar por agente. Hoje ela é uma, e fingir uma abstração para uma população
547
+ * de um é o que este repositório chama de configuração vestida de produto.
548
+ */
549
+ const skills = chosen.agents.includes("claude") ? SKILLS : [];
435
550
  /**
436
551
  * A PASTA VELHA SAI, e isto é obrigatório numa renomeação de skill distribuída.
437
552
  *
@@ -469,6 +584,82 @@ export async function connect(opts) {
469
584
  }
470
585
  /** A tela sai agora, junta: o que mudou por nome, e o silêncio numa linha. */
471
586
  flush();
587
+ /**
588
+ * O QUE ELE NÃO ESCOLHEU É DITO, com os caminhos (A2 e A5 da etapa 12) - lacuna declarada é
589
+ * confiança, lacuna calada é bug (lei 8).
590
+ *
591
+ * Os arquivos que a fiação daquele agente escreveria, nomeados: *"8 arquivos"* obrigaria a pessoa
592
+ * a adivinhar quais. E quando algum deles JÁ ESTÁ no repositório - ele desmarcou algo que estava
593
+ * fiado -, a linha diz que ele ficou lá e como remover a NOSSA parte: apagar arquivo dele não é
594
+ * nosso (decisão dele, 10/09), e o `.codex/config.toml` tem configuração DELE dentro - foi por
595
+ * isso que o `rm` do arquivo inteiro saiu daqui (revisão de DX, mesmo dia).
596
+ */
597
+ for (const agent of AGENTS) {
598
+ if (chosen.agents.includes(agent.id))
599
+ continue;
600
+ const left = [];
601
+ for (const wrote of filesOf(agent.id))
602
+ if (await exists(join(root, wrote.path.replace(/\/$/, ""))))
603
+ left.push(wrote);
604
+ console.log(body(paint.faint(left.length > 0
605
+ ? fmt(say("· {label} not chosen - these stay in the repo: {files}"), {
606
+ label: agent.label,
607
+ files: left.map((w) => w.path).join(", "),
608
+ })
609
+ : fmt(say("· {label} not chosen - nothing of it was written"), {
610
+ label: agent.label,
611
+ }))));
612
+ /**
613
+ * A INSTRUÇÃO DE REMOVER DEPENDE DE QUEM É O ARQUIVO - ver `Ours` em `agents-chosen.ts`.
614
+ *
615
+ * O `rm` só vale para o que a gente criou inteiro. Para o que é DELE com a nossa região dentro,
616
+ * a instrução é a MARCA: o `.codex/config.toml` tem a `status_line` dele, e o
617
+ * `.claude/settings.json` tem os hooks dele.
618
+ */
619
+ const ours = left.filter((w) => w.ours === "file").map((w) => w.path);
620
+ const blocks = left.filter((w) => w.ours === "block").map((w) => w.path);
621
+ const entries = left.filter((w) => w.ours === "entry").map((w) => w.path);
622
+ if (ours.length > 0)
623
+ console.log(body(paint.faint(` rm -rf ${ours.join(" ")}`)));
624
+ if (blocks.length > 0)
625
+ console.log(body(paint.faint(fmt(say(" our part in {files} sits between `synthesisui:start` and `synthesisui:end` - the rest of each file is yours"), { files: blocks.join(", ") }))));
626
+ /**
627
+ * E NUM JSON NÃO HÁ MARCA PARA PROCURAR - a nossa parte é uma CHAVE. Dizer "entre as marcas"
628
+ * sobre o `.claude/settings.json` seria mandar procurar um botão que a tela não mostra.
629
+ */
630
+ if (entries.length > 0)
631
+ console.log(body(paint.faint(fmt(say(" in {files} only our `synthesisui` entry is ours - the rest of the file is yours"), { files: entries.join(", ") }))));
632
+ /**
633
+ * E O `.claude/settings.json` É O ÚNICO QUE CONTINUA RODANDO, então ele é dito à parte - achado
634
+ * da revisão de DX: *"these stay in the repo"* lê como arquivo inerte, e o hook lá dentro é
635
+ * executado pelo editor a cada escrita, escolha ou não escolha. Quem desmarcou para parar de ser
636
+ * interrompido precisa saber que só apagar a região para isso.
637
+ */
638
+ if (entries.includes(".claude/settings.json"))
639
+ console.log(body(paint.faint(say(" the write check inside it keeps running until that entry is gone"))));
640
+ }
641
+ /**
642
+ * O CURSOR SAIU DO PRODUTO (A6 da etapa 12), e quem tem os arquivos dele é avisado com os
643
+ * caminhos - por decisão dele, 10/09.
644
+ *
645
+ * A gente escrevia dois arquivos naquela pasta: `.cursor/mcp.json` (as ferramentas) e
646
+ * `.cursor/rules/synthesisui.mdc` (as regras). Eles ficam onde estão - apagar arquivo do
647
+ * repositório dele não é nosso -, e a linha diz o que eles são e o que dentro deles é nosso: o
648
+ * arquivo de regras é nosso inteiro, e no `.cursor/mcp.json` só a entrada `synthesisui` é.
649
+ *
650
+ * O aviso sai enquanto os arquivos estiverem lá, e é o próprio disco que decide: ele desaparece
651
+ * quando ele age. Sem arquivo de estado nosso para lembrar de um aviso.
652
+ */
653
+ {
654
+ const left = [];
655
+ for (const rel of [".cursor/mcp.json", ".cursor/rules/synthesisui.mdc"])
656
+ if (await exists(join(root, rel)))
657
+ left.push(rel);
658
+ if (left.length > 0) {
659
+ console.log(body(fmt(say("· Cursor is no longer wired by this CLI - what we wrote is still here: {files}"), { files: left.join(", ") })));
660
+ console.log(body(paint.faint(say(" the rules file is ours to delete; in `.cursor/mcp.json` only the `synthesisui` entry is - the other servers there are yours"))));
661
+ }
662
+ }
472
663
  /**
473
664
  * O CI: escrito quando pedido, e NÃO MAIS OFERECIDO aqui.
474
665
  *
@@ -524,6 +715,8 @@ export async function connect(opts) {
524
715
  ];
525
716
  await reportWhatIsLeft(root, {
526
717
  cli: opts.version,
718
+ /** A ação recomendada abre o que ele ESCOLHEU - ver `agentOpenCommands` (etapa 12). */
719
+ agents: chosen.agents,
527
720
  ...(needsRestart || skillsMoved
528
721
  ? { freshSession: { loads, mcp: mcpMoved } }
529
722
  : {}),
@@ -541,7 +734,13 @@ export async function connect(opts) {
541
734
  * 2525ms -> 295ms num repo com o pacote em `node_modules`. "Ten times" era otimista; "several" é o
542
735
  * que se sustenta.
543
736
  */
544
- if (wired.command.startsWith("npx synthesisui@")) {
737
+ /**
738
+ * E SÓ QUANDO EXISTE HOOK - achado da revisão de QA no fecho da etapa 12: `wired.command` é
739
+ * preenchido mesmo quando o hook foi PULADO (é o comando que SERIA escrito), então num
740
+ * repositório que escolheu só o Codex a tela explicava o custo de um hook que não existe ali.
741
+ */
742
+ if (wired.hook !== "skipped" &&
743
+ wired.command.startsWith("npx synthesisui@")) {
545
744
  /**
546
745
  * O CUSTO DO HOOK, EM UMA LINHA - eram cinco, e elas fechavam a tela.
547
746
  *
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { pinnedHookVersion, wireAgent } from "../agent-wiring.js";
4
+ import { agentsToMaintain } from "../agents-chosen.js";
4
5
  import { isOlderCli } from "../cli-version.js";
5
6
  import { generateComponentFiles } from "../component-codegen.js";
6
7
  import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
@@ -157,7 +158,18 @@ async function rewireIfBehind(root, cli) {
157
158
  const pinned = await pinnedHookVersion(root).catch(() => null);
158
159
  if (!pinned || pinned === cli)
159
160
  return;
160
- await wireAgent(root, cli, { hook: true, mcp: true }).catch(() => null);
161
+ /**
162
+ * COM QUEM JÁ ESTÁ AQUI, e nunca com a lista inteira - ver `wiredAgents`.
163
+ *
164
+ * Este comando MANTÉM o que existe: ele não pergunta nada e não pode escolher por ele. Passar a
165
+ * lista do produto faria um `upgrade` plantar a fiação de um agente que ele nunca marcou, que é
166
+ * exatamente o defeito que a etapa 12 fechou no `connect`.
167
+ */
168
+ await wireAgent(root, cli, {
169
+ hook: true,
170
+ mcp: true,
171
+ agents: await agentsToMaintain(root),
172
+ }).catch(() => null);
161
173
  console.log(`↻ the check after every write moved from ${pinned} to ${cli}`);
162
174
  console.log(" Reopen your editor session - hooks are read at startup.");
163
175
  }
@@ -37,6 +37,27 @@ register("pt-BR", {
37
37
  "✓ .claude/settings.json each session opens with what is missing": "✓ .claude/settings.json cada sessão abre com o que falta",
38
38
  "✓ .claude/settings.json the session check moved to this one": "✓ .claude/settings.json a verificação de sessão veio para esta",
39
39
  "· .claude/settings.json the session check was already there": "· .claude/settings.json a verificação de sessão já estava lá",
40
+ // ── a escolha dos agentes (etapa 12) ──
41
+ /**
42
+ * A LISTA E OS NOMES DOS AGENTES NÃO TRADUZEM - `claude` e `codex` são o que ele DIGITA, e a mesma
43
+ * razão da frase `import my design system`: onde a máquina lê, a precisão vence a gentileza.
44
+ */
45
+ "Which agents": "Quais agentes",
46
+ "{dir} is here": "{dir} está aqui",
47
+ "no {dir} in this repo": "nenhum {dir} neste repo",
48
+ "Enter keeps this. To change it, type the ones you want - or `none`:": "Enter mantém isto. Para mudar, digite os que você quer - ou `none`:",
49
+ "· {label} not chosen - these stay in the repo: {files}": "· {label} não escolhido - estes ficam no repo: {files}",
50
+ "· {label} not chosen - nothing of it was written": "· {label} não escolhido - nada dele foi escrito",
51
+ "· Cursor is no longer wired by this CLI - what we wrote is still here: {files}": "· o Cursor não é mais fiado por este CLI - o que a gente escreveu continua aqui: {files}",
52
+ "· nobody to ask here, so the folders in this repo decided: {agents}": "· não havia ninguém para perguntar, então as pastas deste repo decidiram: {agents}",
53
+ "Selected: {agents}": "Selecionados: {agents}",
54
+ " our part in {files} sits between `synthesisui:start` and `synthesisui:end` - the rest of each file is yours": " a nossa parte em {files} fica entre `synthesisui:start` e `synthesisui:end` - o resto de cada arquivo é seu",
55
+ " in {files} only our `synthesisui` entry is ours - the rest of the file is yours": " em {files} só a nossa entrada `synthesisui` é nossa - o resto do arquivo é seu",
56
+ " the write check inside it keeps running until that entry is gone": " a verificação de escrita dentro dele continua rodando até aquela entrada sair",
57
+ " the rules file is ours to delete; in `.cursor/mcp.json` only the `synthesisui` entry is - the other servers there are yours": " o arquivo de regras é nosso para apagar; no `.cursor/mcp.json` só a entrada `synthesisui` é - os outros servidores ali são seus",
58
+ "· not an agent this CLI knows: {names}": "· não é um agente que este CLI conhece: {names}",
59
+ "· no agent chosen, and nothing here says which agent you use - run with `--agents claude` or `--agents codex`, or run this in a terminal to pick from the list": "· nenhum agente escolhido, e nada aqui diz qual agente você usa - rode com `--agents claude` ou `--agents codex`, ou rode isto num terminal para escolher da lista",
60
+ "· no agent chosen - nothing was wired. Run again with `--agents claude` or `--agents codex` when you want it": "· nenhum agente escolhido - nada foi fiado. Rode de novo com `--agents claude` ou `--agents codex` quando quiser",
40
61
  // ── o MCP e as skills ──
41
62
  "{n} tools the agent can ask": "{n} ferramentas que o agente pode consultar",
42
63
  "was pinned to {was} - now it follows the published reader": "estava pinado na {was} - agora acompanha o leitor publicado",
package/dist/index.js CHANGED
@@ -62,7 +62,8 @@ Usage - governance (deterministic, FREE):
62
62
  synthesisui adopt [--write] turn the design system you ALREADY have into a contract
63
63
  synthesisui import [--scope <p>] [--usage <p>] read what you already have and make it a system
64
64
  your agent follows - without touching your CSS
65
- synthesisui connect [--ci] wire your agent: the check as an editor hook, the system
65
+ synthesisui connect [--agents claude,codex] [--ci]
66
+ wire the agents YOU pick: the check as an editor hook, the system
66
67
  as MCP tools, and a contract that stops repeating itself
67
68
  synthesisui doctor [paths…] [--verbose] audit for DRIFT: every design value written by hand, the
68
69
  token your system already has for it, coherence, and the
@@ -268,6 +269,7 @@ async function main() {
268
269
  mcp: flags.mcp !== false,
269
270
  ci: flags.ci === true,
270
271
  shell: flags.shell === true,
272
+ ...(typeof flags.agents === "string" ? { agents: flags.agents } : {}),
271
273
  });
272
274
  return;
273
275
  case "hook":
@@ -200,7 +200,21 @@
200
200
  * O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex volta a ler o interpretador
201
201
  * publicado, em vez do que existia no dia em que ele conectou.
202
202
  */
203
- export const MATERIALISER_SINCE = "0.16.414";
203
+ /**
204
+ * 0.16.414 -> 0.16.415 em 10/09: o conjunto de arquivos que cai na pasta dele passa a ser o que ELE
205
+ * ESCOLHEU. Antes o Claude Code era escrito sempre, sem evidência nenhuma - medido no mesmo dia com
206
+ * o comando de verdade num repositório que só tem `.codex/`: **10 arquivos, e 8 do Claude Code**
207
+ * (`.claude/settings.json`, cinco skills, `.mcp.json`, `CLAUDE.md`). O Cursor saiu do produto na
208
+ * mesma decisão, então `.cursor/mcp.json` e `.cursor/rules/synthesisui.mdc` deixam de nascer.
209
+ *
210
+ * O `upgrade` chama `wireAgent`, e ele passa a manter APENAS quem já está fiado: um `upgrade` não
211
+ * pergunta nada, e plantar a fiação de um agente que ele nunca marcou seria o defeito voltando pela
212
+ * porta da manutenção.
213
+ *
214
+ * O que o cliente ganha ao rodar `upgrade`: nada de uma ferramenta que ele não usa é reescrito no
215
+ * repositório dele.
216
+ */
217
+ export const MATERIALISER_SINCE = "0.16.415";
204
218
  /**
205
219
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
206
220
  *
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { hasCodexBlock, pinnedInCodex } from "../codex-mcp.js";
3
4
  /**
4
5
  * O RECALL ESTÁ AO ALCANCE DESTE REPOSITÓRIO? - e é isto que decide o tamanho do `CLAUDE.md`.
5
6
  *
@@ -18,8 +19,32 @@ import { join } from "node:path";
18
19
  *
19
20
  * E ela não inventa configuração: o sinal é o registro que o `connect` já escreve.
20
21
  */
21
- /** Onde os agentes leem o servidor - o mesmo shape, dois caminhos. */
22
+ /**
23
+ * Onde os agentes leem o servidor - o mesmo shape, dois caminhos.
24
+ *
25
+ * `.cursor/mcp.json` CONTINUA SENDO LIDO depois da retirada do Cursor em 10/09, e a diferença é
26
+ * escrever contra ler: o produto não põe mais nada lá, e um repositório que recebeu aquele arquivo
27
+ * de uma versão anterior REALMENTE tem o servidor fiado. Deixar de ler seria dizer a ele que a
28
+ * memória não está disponível num repositório onde ela está.
29
+ */
22
30
  const MCP_FILES = [".mcp.json", ".cursor/mcp.json"];
31
+ /**
32
+ * E O CODEX PROVA PELO ARQUIVO DELE - achado da revisão de QA no fecho da etapa 12, e é uma
33
+ * regressão que ela criou.
34
+ *
35
+ * O Codex declara o servidor em TOML, sob `[mcp_servers.synthesisui]`, e esta função só lia JSON.
36
+ * Enquanto o `.mcp.json` nascia SEMPRE, um repositório só-Codex passava por acidente: o arquivo do
37
+ * Claude Code estava lá mesmo sem ninguém usar o Claude Code. Desde que a escolha dele decide, ele
38
+ * não nasce - e a capacidade sumiu para todo usuário de Codex.
39
+ *
40
+ * O QUE ELE VIVIA: o mesmo comando imprimia `✓ .codex/config.toml 20 tools the agent can ask` e,
41
+ * no `AGENTS.md` que aquele agente lê, escrevia a variante que diz que não há ferramenta nenhuma -
42
+ * o dobro de texto mandando ler o guia à mão. `recall` e `remember` sumiam.
43
+ *
44
+ * A pergunta desta função é CAPACIDADE, nunca nome de agente: existe, neste repositório, um lugar
45
+ * onde um agente lê o nosso servidor? O TOML é um desses lugares desde 03/09.
46
+ */
47
+ const MCP_TOML = ".codex/config.toml";
23
48
  /** A versão em que `recall` e `remember` passaram a existir. */
24
49
  export const RECALL_SINCE = "0.16.264";
25
50
  const older = (mine, than) => {
@@ -64,6 +89,20 @@ export async function recallAvailable(root) {
64
89
  because: `the MCP server is registered in ${rel}`,
65
90
  };
66
91
  }
92
+ /** E O TOML DO CODEX - ver `MCP_TOML`, e o pin é lido pelo mesmo leitor que o `connect` usa. */
93
+ const toml = await readFile(join(root, MCP_TOML), "utf8").catch(() => null);
94
+ if (toml && hasCodexBlock(toml)) {
95
+ const pin = pinnedInCodex(toml);
96
+ if (pin && older(pin, RECALL_SINCE))
97
+ return {
98
+ available: false,
99
+ because: `the MCP server in ${MCP_TOML} is pinned to ${pin}, older than ${RECALL_SINCE} - run \`synthesisui connect\` to let it float`,
100
+ };
101
+ return {
102
+ available: true,
103
+ because: `the MCP server is registered in ${MCP_TOML}`,
104
+ };
105
+ }
67
106
  return {
68
107
  available: false,
69
108
  because: "no MCP server is registered in this repository - run `synthesisui connect` so the agent can look memory up",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.414",
3
+ "version": "0.16.415",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {