synthesisui 0.16.242 → 0.16.244

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * DE ONDE VEIO A RESPOSTA - e é um DADO, não uma frase (dono, 18/08).
3
+ *
4
+ * O pedido veio com a razão embutida: *"eu trataria a linha Source como algo
5
+ * estrutural mesmo, não só visual. Isso pode virar uma métrica de maturidade
6
+ * depois - porcentagem de respostas por receita, por composição, por
7
+ * improviso."*
8
+ *
9
+ * Então a procedência nasce como estrutura e o texto é DERIVADO dela. O
10
+ * contrário - escrever a frase à mão em cada resposta - produz duas verdades no
11
+ * dia em que uma delas for editada, e a que vai para a métrica é sempre a que
12
+ * ninguém leu.
13
+ *
14
+ * ─────────────────────────────────────────────────────────────────────────
15
+ * O QUE CADA KIND SIGNIFICA, e a fronteira é a GARANTIA
16
+ *
17
+ * contract veio do que este sistema declara - receita, token, regra,
18
+ * vocabulário. A plataforma responde por isso.
19
+ * composed montado a partir de camadas porque não havia receita para o
20
+ * pedido. Vale, e vale menos que um contrato. (Nasce com o
21
+ * compose_context; hoje nada devolve isto, e é de propósito -
22
+ * um kind que ninguém emite é um número que mente por otimismo.)
23
+ * pointer um ponteiro para conhecimento de terceiro. Não é nosso, não é
24
+ * garantido, e a resposta diz o pacote. Mesma gramática que a
25
+ * plataforma já usa: "3rd-party · <pacote>".
26
+ * none este sistema não tinha nada para o pedido. É o kind mais
27
+ * valioso dos quatro: é ele que mede o improviso, e é o evento
28
+ * que a plataforma não contava.
29
+ *
30
+ * A TAXA DE IMPROVISO é `none` sobre o total, e ela é a pergunta que decide se
31
+ * a composição vale ser construída - respondida com dado em vez de sensação.
32
+ */
33
+ /**
34
+ * A LINHA QUE O AGENTE E A PESSOA LEEM - derivada, nunca escrita à mão.
35
+ *
36
+ * Ela serve dois leitores de uma vez: o dev sabe o quanto confiar no que
37
+ * acabou de receber, e o agente sabe se está pisando em contrato ou em
38
+ * composição. Um agente que não sabe a diferença trata as duas com a mesma
39
+ * confiança, e é assim que uma suposição nossa entra no repositório dele com
40
+ * cara de regra.
41
+ */
42
+ export function sourceLine(source) {
43
+ const label = source.kind === "pointer"
44
+ ? "3rd-party pointer"
45
+ : source.kind === "none"
46
+ ? "none"
47
+ : source.kind;
48
+ return source.detail
49
+ ? `Source: ${label} · ${source.detail}`
50
+ : `Source: ${label}`;
51
+ }
52
+ /**
53
+ * O texto com a procedência colada no fim - uma linha em branco antes, para
54
+ * ela não se misturar com a última frase da resposta.
55
+ */
56
+ export function withSource(answer) {
57
+ return `${answer.body}\n\n${sourceLine(answer.source)}`;
58
+ }
59
+ /**
60
+ * O VALOR GRAVADO, e o prefixo existe para o eixo ser legível numa tabela que
61
+ * já carrega outra coisa.
62
+ *
63
+ * `agent_reads.action` é `text` sem CHECK no banco (medido em produção), então
64
+ * um valor novo não custa migração - e uma coluna nova custaria a tabela
65
+ * inteira até a migração rodar. `src-` marca de qual eixo a linha fala:
66
+ * `describe`/`add` são gesto, `tool` é chamada, `src-*` é procedência.
67
+ *
68
+ * E UMA LACUNA LEVA O MOTIVO NO PRÓPRIO VALOR: `src-none-recipe`. Assim a
69
+ * pergunta agregada continua sendo um `like 'src-none%'` e a pergunta por
70
+ * motivo é a mesma consulta com o valor inteiro - sem coluna nova, e sem
71
+ * perder o total.
72
+ *
73
+ * Recebe a procedência INTEIRA e não o `kind`: com o kind sozinho, o motivo
74
+ * seria um segundo argumento que alguém esquece, e a linha gravada voltaria a
75
+ * ser um `none` cego.
76
+ */
77
+ export function provenanceAction(source) {
78
+ return source.kind === "none"
79
+ ? `src-none-${source.reason}`
80
+ : `src-${source.kind}`;
81
+ }
82
+ /**
83
+ * O NOME DA FERRAMENTA COMO A TABELA O GUARDA.
84
+ *
85
+ * A coluna `component` aceita `^[a-z][a-z0-9-]*$` na rota - a mesma forma de um
86
+ * slug de receita. As ferramentas são snake_case, então elas viajam em
87
+ * kebab-case; sem isto o ping seria recusado com 400 e a instrumentação
88
+ * silenciaria justamente na ferramenta nova.
89
+ */
90
+ export function toolSlug(tool) {
91
+ return tool.replace(/_/g, "-").toLowerCase();
92
+ }
@@ -5,6 +5,7 @@ import { pinnedHookVersion } from "../agent-wiring.js";
5
5
  import { isOlderCli } from "../cli-version.js";
6
6
  import { readToken, resolveRegistry } from "../config.js";
7
7
  import { unsentEvents } from "../doctor/ledger.js";
8
+ import { readRequests } from "../doctor/requests.js";
8
9
  import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
9
10
  import { measuredScope } from "../measured-scope.js";
10
11
  import { body } from "../output.js";
@@ -314,6 +315,32 @@ export function repairSaid(body, lock) {
314
315
  then: "claude --continue --dangerously-skip-permissions",
315
316
  };
316
317
  }
318
+ /**
319
+ * QUANTAS RESPOSTAS ESTÃO ESPERANDO PARA SEREM BUSCADAS - o cruzamento das duas listas.
320
+ *
321
+ * A plataforma manda os ids que ela já decidiu; o arquivo local diz o que ainda está aberto AQUI.
322
+ * A interseção é a notícia: pedidos que foram respondidos e cuja resposta esta máquina nunca viu.
323
+ *
324
+ * `null` quando não há nada, quando o arquivo não existe, ou quando a plataforma é velha demais para
325
+ * mandar o campo - um CLI novo contra um servidor antigo fica exatamente como estava, calado.
326
+ */
327
+ async function decidedWaiting(root, decided) {
328
+ if (!decided || decided.length === 0)
329
+ return null;
330
+ const open = await readRequests(root).catch(() => []);
331
+ if (open.length === 0)
332
+ return null;
333
+ const answered = new Set(decided);
334
+ const n = open.filter((r) => answered.has(r.id)).length;
335
+ if (n === 0)
336
+ return null;
337
+ return {
338
+ says: n === 1
339
+ ? "A request you filed has been answered on the platform, and this machine has not picked it up yet."
340
+ : `${n} requests you filed have been answered on the platform, and this machine has not picked them up yet.`,
341
+ run: "npx synthesisui sync",
342
+ };
343
+ }
317
344
  export async function versionBehind(root, opts = {}) {
318
345
  const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
319
346
  const lock = locks[0];
@@ -351,6 +378,24 @@ export async function versionBehind(root, opts = {}) {
351
378
  * a versão é a consequência - dizer a consequência primeiro manda a pessoa rodar o comando sem
352
379
  * saber por quê.
353
380
  */
381
+ /**
382
+ * ALGUÉM JÁ RESPONDEU UM PEDIDO SEU, E VOCÊ NÃO TEM COMO SABER.
383
+ *
384
+ * O agente arquiva a lacuna, a pessoa decide na plataforma - e a decisão fica lá. O arquivo local
385
+ * nunca é tocado por design: o próximo `sync` lê o status, fecha o pedido na máquina e imprime o
386
+ * próximo passo. O buraco está no "próximo": até alguém rodar o comando, uma resposta dada há dias
387
+ * não existe deste lado, e quem esperava por ela descobre por acaso.
388
+ *
389
+ * O cruzamento tem que ser AQUI, e é por isso que a plataforma manda ids em vez de uma frase: só
390
+ * esta máquina sabe o que ainda está ABERTO na fila local. Um pedido decidido que já foi fechado
391
+ * aqui não é notícia, e contá-lo faria o hook cobrar para sempre um trabalho já feito.
392
+ *
393
+ * Vem antes da versão e do reparo porque é a única linha desta lista sobre uma pergunta que a
394
+ * PESSOA fez - e uma resposta que ela pediu vale mais que uma novidade que ela não pediu.
395
+ */
396
+ const waiting = await decidedWaiting(root, body.decided);
397
+ if (waiting)
398
+ return waiting;
354
399
  const repaired = repairSaid(body, lock);
355
400
  if (repaired)
356
401
  return repaired;
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
3
  import { join, relative, resolve } from "node:path";
4
+ import { provenanceAction, toolSlug, withSource, } from "../agent-provenance.js";
4
5
  import { pinnedHookVersion } from "../agent-wiring.js";
5
6
  import { readToken, resolveRegistry } from "../config.js";
6
7
  import { readEvents } from "../doctor/ledger.js";
@@ -14,6 +15,16 @@ import { installedSlug, remeasure } from "./sync.js";
14
15
  const send = (msg) => {
15
16
  process.stdout.write(`${JSON.stringify(msg)}\n`);
16
17
  };
18
+ /**
19
+ * A RESPOSTA QUE VEIO DO CONTRATO - o atalho, porque é o caso comum.
20
+ *
21
+ * `detail` fica de fora quando a própria resposta já nomeia a fonte (o token,
22
+ * o componente, a versão): repetir aquilo na linha de procedência é ruído.
23
+ */
24
+ const fromContract = (body, detail) => ({
25
+ body,
26
+ source: { kind: "contract", ...(detail ? { detail } : null) },
27
+ });
17
28
  /** A tool result is always text: readable by the model, and by a person
18
29
  * watching the transcript when something goes wrong. */
19
30
  const text = (body, isError = false) => ({
@@ -254,7 +265,10 @@ export const MCP_TOOL_COUNT = TOOLS.length;
254
265
  async function checkFile(root, path) {
255
266
  const { table } = await loadSystem(root);
256
267
  if (table.byName.size === 0)
257
- return "No design system installed here, so there is nothing to check against. Run `synthesisui init` or `synthesisui adopt`.";
268
+ return {
269
+ body: "No design system installed here, so there is nothing to check against. Run `synthesisui init` or `synthesisui adopt`.",
270
+ source: { kind: "none", reason: "system", detail: "no system installed" },
271
+ };
258
272
  const scope = resolve(root, path);
259
273
  const reports = [];
260
274
  for await (const file of walkAll([scope])) {
@@ -263,7 +277,7 @@ async function checkFile(root, path) {
263
277
  reports.push(scanSource(relative(root, file), src, table));
264
278
  }
265
279
  if (reports.length === 0)
266
- return `Nothing readable at ${path}.`;
280
+ return fromContract(`Nothing readable at ${path}.`);
267
281
  const d = diagnose(reports);
268
282
  const out = [
269
283
  `${table.name ?? table.slug}: ${d.coverage}% of design values come from the system.`,
@@ -284,12 +298,16 @@ async function checkFile(root, path) {
284
298
  }
285
299
  if (d.findings.length === 0 && phantoms.length === 0)
286
300
  out.push("", "Nothing to fix here.");
287
- return out.join("\n");
301
+ return fromContract(out.join("\n"));
288
302
  }
289
303
  async function findToken(root, value) {
290
304
  const { table } = await loadSystem(root);
291
- if (table.byName.size === 0)
292
- return "No design system installed here.";
305
+ if (table.byName.size === 0) {
306
+ return {
307
+ body: "No design system installed here.",
308
+ source: { kind: "none", reason: "system", detail: "no system installed" },
309
+ };
310
+ }
293
311
  /**
294
312
  * O NOME DELE PRIMEIRO, como no doctor (`nameToWrite`): `loadSystem` já devolve a tabela COM os
295
313
  * aliases dele (`withTheirNames`) e este era o único leitor que nunca os consultava. Sem o `kind`
@@ -306,16 +324,27 @@ async function findToken(root, value) {
306
324
  const exact = tokenFor(table, value);
307
325
  if (exact) {
308
326
  const shown = theirNameFor(value) ?? exact;
309
- return `${value} is ${shown} in this project. Use var(${shown}).`;
327
+ return fromContract(`${value} is ${shown} in this project. Use var(${shown}).`);
310
328
  }
311
329
  const near = nearestToken(table, value);
312
330
  if (near) {
313
331
  const shown = theirNameFor(near.value) ?? near.name;
314
- return `No token holds ${value}. The closest is ${shown} at ${near.value}. If that is what you meant, use it - if it genuinely is not, say so rather than inventing a token.`;
332
+ /** Perto não é o valor: o sistema NÃO tem este, e a métrica precisa saber. */
333
+ return {
334
+ body: `No token holds ${value}. The closest is ${shown} at ${near.value}. If that is what you meant, use it - if it genuinely is not, say so rather than inventing a token.`,
335
+ source: {
336
+ kind: "none",
337
+ reason: "token",
338
+ detail: `no token for ${value}`,
339
+ },
340
+ };
315
341
  }
316
342
  // Same words the managed block uses. A refusal is only useful if it is the
317
343
  // same refusal every time.
318
- return `No token in this system holds ${value}, and nothing is close. Do NOT invent one. Say which value you need and what you would call it, and let a person decide.`;
344
+ return {
345
+ body: `No token in this system holds ${value}, and nothing is close. Do NOT invent one. Say which value you need and what you would call it, and let a person decide.`,
346
+ source: { kind: "none", reason: "token", detail: `no token for ${value}` },
347
+ };
319
348
  }
320
349
  /**
321
350
  * A DOUTRINA DO SISTEMA - as regras e a filosofia, servidas em vez de materializadas.
@@ -801,18 +830,28 @@ async function validateRecipes(root, entries) {
801
830
  * had 26 names against a catalogue of 41.
802
831
  */
803
832
  /**
804
- * O CONTADOR DO AGENTE, fire-and-forget: a tela do UI Kit mostra "vezes que esta receita
805
- * foi consultada pelo Agente", e este é o único ponto onde a consulta acontece de
806
- * verdade. Nunca bloqueia a resposta: sem sessão ou sem rede, não conta e não quebra -
807
- * telemetria que atrasa a ferramenta é telemetria que alguém desliga.
833
+ * O CONTADOR DO AGENTE, fire-and-forget e agora CENTRAL.
834
+ *
835
+ * Ele nasceu para uma tela: o UI Kit mostra "vezes que esta receita foi consultada pelo
836
+ * Agente". E era chamado à mão em duas ferramentas de treze, o que quer dizer que onze
837
+ * ferramentas nunca contaram nada - inclusive a única que sabe dizer que o sistema NÃO
838
+ * tinha o que foi pedido.
839
+ *
840
+ * Agora o `callTool` o chama uma vez, para toda ferramenta, com a procedência da
841
+ * resposta. O dono foi explícito sobre por que centralizar (18/08): *"isso vira
842
+ * comportamento padrão e não depende de disciplina futura."* Um contador chamado dentro
843
+ * de um `case` é um contador que a próxima ferramenta esquece.
844
+ *
845
+ * Nunca bloqueia a resposta: sem sessão ou sem rede, não conta e não quebra - telemetria
846
+ * que atrasa a ferramenta é telemetria que alguém desliga.
808
847
  */
809
- function countAgentRead(root, component, action,
848
+ function recordAgentCall(root, call,
810
849
  /** A versão que está reportando - ver `repoStateOf`. Ausente = não reporta estado. */
811
850
  cli) {
812
851
  void (async () => {
813
852
  try {
814
853
  const token = await readToken();
815
- if (!token || !component)
854
+ if (!token)
816
855
  return;
817
856
  // O slug vem do .lock do sistema instalado - a mesma resolução do sync.
818
857
  const dsDir = join(root, "_synthesisui", "ds");
@@ -853,8 +892,10 @@ cli) {
853
892
  */
854
893
  body: JSON.stringify({
855
894
  slug,
856
- component,
857
- action,
895
+ tool: toolSlug(call.tool),
896
+ source: provenanceAction(call.source),
897
+ ...(call.component ? { component: call.component } : {}),
898
+ ...(call.action ? { action: call.action } : {}),
858
899
  ...(repo ? { repo } : {}),
859
900
  }),
860
901
  });
@@ -912,7 +953,22 @@ async function describeComponent(root, name) {
912
953
  break;
913
954
  }
914
955
  if (!recipe) {
915
- return `This system has no component called "${name}". Run list_components to see what it does have - and if nothing covers what you need, file request_component rather than writing one from scratch.`;
956
+ /**
957
+ * O EVENTO MAIS VALIOSO DA PLATAFORMA, e até 18/08 ele não era contado.
958
+ *
959
+ * Aqui é onde o agente descobre que vai ter que improvisar. A frase já
960
+ * existia; o que não existia era o REGISTRO - então ninguém sabia quantas
961
+ * vezes, nem para quê. É este número que decide se compor conhecimento
962
+ * vale ser construído, e para quais famílias primeiro.
963
+ */
964
+ return {
965
+ body: `This system has no component called "${name}". Run list_components to see what it does have - and if nothing covers what you need, file request_component rather than writing one from scratch.`,
966
+ source: {
967
+ kind: "none",
968
+ reason: "recipe",
969
+ detail: `no recipe for "${name}"`,
970
+ },
971
+ };
916
972
  }
917
973
  const out = [
918
974
  `${name}${recipe.description ? ` - ${recipe.description}` : ""}`,
@@ -1036,51 +1092,98 @@ async function describeComponent(root, name) {
1036
1092
  if (laws.length === 0 && needed.length === 0) {
1037
1093
  out.push("", "No rules govern this component yet. Build with it, and if you find yourself working around it, file request_component with the reasoning.");
1038
1094
  }
1039
- return out.join("\n");
1095
+ return fromContract(out.join("\n"));
1040
1096
  }
1041
1097
  async function addComponent(root, name) {
1042
1098
  const { table } = await loadSystem(root);
1043
- if (!table.slug)
1044
- return "No design system installed here.";
1099
+ if (!table.slug) {
1100
+ return {
1101
+ body: "No design system installed here.",
1102
+ source: { kind: "none", reason: "system", detail: "no system installed" },
1103
+ };
1104
+ }
1045
1105
  const { out } = await capturing(() => component(table.slug, name, { dir: root }));
1046
- return `${out}\n\nIt is real code in this project now - import it and extend it rather than writing your own.`;
1106
+ return fromContract(`${out}\n\nIt is real code in this project now - import it and extend it rather than writing your own.`);
1047
1107
  }
1048
1108
  // ── The protocol ─────────────────────────────────────────────────────────────
1109
+ /**
1110
+ * AS FERRAMENTAS QUE FALAM DE UM COMPONENTE PELO NOME - as únicas cujo alvo é
1111
+ * um slug de receita, e por isso as únicas que gravam o alvo.
1112
+ *
1113
+ * Um valor como `#2563eb` (o alvo de `find_token`) não é nome de componente e
1114
+ * seria recusado pela rota, que valida a coluna como slug. Então o alvo só
1115
+ * viaja quando é um: para o resto, o eixo da procedência é a própria
1116
+ * ferramenta.
1117
+ */
1118
+ const BY_COMPONENT = {
1119
+ describe_component: "describe",
1120
+ add_component: "add",
1121
+ };
1122
+ /**
1123
+ * O ÚNICO PONTO DE INSTRUMENTAÇÃO - e é isto que o dono pediu (18/08): *"vira
1124
+ * comportamento padrão e não depende de disciplina futura."*
1125
+ *
1126
+ * A ferramenta responde, a chamada é registrada com a procedência daquela
1127
+ * resposta, e a linha `Source:` é colada ao texto a partir do MESMO dado que
1128
+ * foi gravado. Não há como o número e a frase discordarem, porque não há duas
1129
+ * fontes.
1130
+ *
1131
+ * Uma ferramenta nova entra no switch e passa a contar sem tocar nesta função.
1132
+ */
1049
1133
  async function callTool(root, name, args,
1050
- /** A versão que reporta o estado do repo no ping - ver `countAgentRead`. */
1134
+ /** A versão que reporta o estado do repo no ping - ver `repoStateOf`. */
1051
1135
  cli) {
1136
+ const answer = await runTool(root, name, args);
1137
+ const gesture = BY_COMPONENT[name];
1138
+ const target = gesture
1139
+ ? String(args.name ?? "")
1140
+ .trim()
1141
+ .toLowerCase()
1142
+ : "";
1143
+ recordAgentCall(root, {
1144
+ tool: name,
1145
+ source: answer.source,
1146
+ ...(gesture ? { action: gesture } : {}),
1147
+ ...(target ? { component: target } : {}),
1148
+ }, cli);
1149
+ return text(withSource(answer), answer.isError);
1150
+ }
1151
+ /**
1152
+ * O SWITCH PURO - ele resolve a ferramenta e devolve a resposta COM a
1153
+ * procedência. Nenhuma linha dele conta nada: quem instrumenta é `callTool`,
1154
+ * uma vez, para todas.
1155
+ */
1156
+ async function runTool(root, name, args) {
1052
1157
  switch (name) {
1053
1158
  case "check_file":
1054
- return text(await checkFile(root, String(args.path ?? ".")));
1159
+ return checkFile(root, String(args.path ?? "."));
1055
1160
  case "find_token":
1056
- return text(await findToken(root, String(args.value ?? "")));
1161
+ return findToken(root, String(args.value ?? ""));
1057
1162
  case "system_doctrine":
1058
- return text(await systemDoctrine(root));
1163
+ return fromContract(await systemDoctrine(root));
1059
1164
  case "list_components":
1060
- return text(await listComponents(root));
1165
+ return fromContract(await listComponents(root));
1061
1166
  case "describe_component":
1062
- countAgentRead(root, String(args.name ?? ""), "describe", cli);
1063
- return text(await describeComponent(root, String(args.name ?? "")));
1167
+ return describeComponent(root, String(args.name ?? ""));
1064
1168
  case "add_component":
1065
- countAgentRead(root, String(args.name ?? ""), "add", cli);
1066
- return text(await addComponent(root, String(args.name ?? "")));
1169
+ return addComponent(root, String(args.name ?? ""));
1067
1170
  case "playbook": {
1068
1171
  const skill = String(args?.skill ?? "");
1069
1172
  const section = args?.section ? String(args.section) : undefined;
1070
- return text(await playbook(skill, section));
1173
+ return fromContract(await playbook(skill, section));
1071
1174
  }
1072
1175
  case "recipe_vocabulary":
1073
- return text(await recipeVocabulary());
1176
+ return fromContract(await recipeVocabulary());
1074
1177
  case "validate_recipe": {
1075
1178
  // Same resolution as the batch: an `at` the validator cannot see reads as
1076
1179
  // an empty node and earns a false floor verdict (dono, 01/08).
1077
1180
  const [only] = await resolveAtNodes(root, [
1078
1181
  { name: String(args.name ?? "this component"), recipe: args.recipe },
1079
1182
  ]);
1080
- return text(await validateRecipe(String(args.name ?? "this component"), only?.recipe ?? args.recipe));
1183
+ return fromContract(await validateRecipe(String(args.name ?? "this component"), only?.recipe ?? args.recipe));
1081
1184
  }
1082
1185
  case "validate_recipes":
1083
- return text(await validateRecipes(root, Array.isArray(args.recipes)
1186
+ return fromContract(await validateRecipes(root, Array.isArray(args.recipes)
1084
1187
  ? args.recipes
1085
1188
  : []));
1086
1189
  case "request_component": {
@@ -1091,7 +1194,7 @@ cli) {
1091
1194
  considered: args.considered ? String(args.considered) : undefined,
1092
1195
  file: args.file ? String(args.file) : undefined,
1093
1196
  });
1094
- return text(`Filed as ${r.id}. It shows up in \`synthesisui doctor\` until a person authors it or closes it - keep building with your workaround, and say in your summary that the request exists.`);
1197
+ return fromContract(`Filed as ${r.id}. It shows up in \`synthesisui doctor\` until a person authors it or closes it - keep building with your workaround, and say in your summary that the request exists.`);
1095
1198
  }
1096
1199
  case "refresh_system": {
1097
1200
  /**
@@ -1111,10 +1214,10 @@ cli) {
1111
1214
  */
1112
1215
  const slug = await installedSlug(root);
1113
1216
  if (!slug)
1114
- return text("No system installed here that belongs to a dashboard row, so there is nothing to refresh. `synthesisui add <slug>` installs one; an adopted system is described here and owned elsewhere.");
1217
+ return fromContract("No system installed here that belongs to a dashboard row, so there is nothing to refresh. `synthesisui add <slug>` installs one; an adopted system is described here and owned elsewhere.");
1115
1218
  const token = await readToken();
1116
1219
  if (!token)
1117
- return text("Not signed in, so nothing could be sent. A person has to run `synthesisui login` once in the terminal - it opens a browser, and I cannot complete it for them.");
1220
+ return fromContract("Not signed in, so nothing could be sent. A person has to run `synthesisui login` once in the terminal - it opens a browser, and I cannot complete it for them.");
1118
1221
  const { out } = await capturing(() => remeasure({
1119
1222
  root,
1120
1223
  base: resolveRegistry(),
@@ -1137,7 +1240,7 @@ cli) {
1137
1240
  const behind = pinned && pinned !== VERSION ? pinned : null;
1138
1241
  const said = out.trim() ||
1139
1242
  "Nothing was reported by the refresh - which means it found nothing to measure.";
1140
- return text(behind
1243
+ return fromContract(behind
1141
1244
  ? `${said}\n\nUPDATE THE DESIGN SYSTEM BEFORE CONTINUING.\nThe check that runs after your writes is pinned to ${behind} and these tools are ${VERSION}.\n\n npx synthesisui@latest connect\n\nAsk the person, run it for them, and then say this: close and reopen the editor. The hook and the tools only rewire on a new session, so nothing you write before that is checked by the reader you are holding.`
1142
1245
  : said);
1143
1246
  }
@@ -1159,7 +1262,7 @@ cli) {
1159
1262
  file: args.file ? String(args.file) : undefined,
1160
1263
  ...(area ? { area } : null),
1161
1264
  });
1162
- return text(area
1265
+ return fromContract(area
1163
1266
  ? `Filed as ${r.id} and routed to the synthesisui platform team - the system's contract already promises this and the shipped css does not deliver it. No one needs to act: it closes itself when an update lands. Keep the quiet base meanwhile.`
1164
1267
  : `Filed as ${r.id}. Do not add the token yourself - the request shows up in \`synthesisui doctor\` for a person to decide.`);
1165
1268
  }
@@ -1179,10 +1282,14 @@ cli) {
1179
1282
  considered: args.considered ? String(args.considered) : undefined,
1180
1283
  file: args.file ? String(args.file) : undefined,
1181
1284
  });
1182
- return text(`Filed as ${r.id}. Do not adopt the convention as if it were a rule - it shows up in \`synthesisui doctor\` and travels to the system's queue, for the owner to make it a rule or decline it.`);
1285
+ return fromContract(`Filed as ${r.id}. Do not adopt the convention as if it were a rule - it shows up in \`synthesisui doctor\` and travels to the system's queue, for the owner to make it a rule or decline it.`);
1183
1286
  }
1184
1287
  default:
1185
- return text(`No tool named ${name}.`, true);
1288
+ return {
1289
+ body: `No tool named ${name}.`,
1290
+ source: { kind: "none", reason: "tool", detail: "unknown tool" },
1291
+ isError: true,
1292
+ };
1186
1293
  }
1187
1294
  }
1188
1295
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.242",
3
+ "version": "0.16.244",
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": {