synthesisui 0.16.499 → 0.16.501

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/claude-md.js CHANGED
@@ -1,9 +1,18 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
+ import { loadRoleRules, rolesContract } from "./role-rules.js";
2
4
  import { dirname, join } from "node:path";
3
5
  import { hasHook } from "./agent-wiring.js";
4
6
  import { declaredReference } from "./group-role.js";
5
7
  import { recallAvailable } from "./memory/availability.js";
6
8
  import { readResponsiveForm, responsiveLine } from "./responsive-form.js";
9
+ /**
10
+ * A LEITURA - o caminho do sistema desde a fase 1 (18/10). Escrita uma vez e usada nos dois contratos: o de quem já tem
11
+ * sistema sem `rules.json` e o de quem chegou pela porta e ainda não tem sistema nenhum (fase 2).
12
+ */
13
+ const READ_THE_PAGE = "**Once the first screen is done, offer to read it - once.** If their next message does not take the offer up, do not offer it again; they can still ask. When the person asks you to read their page - or right after the creation of the first screen ends (with impeccable, once it has written `DESIGN.md`; never while it still asks for another round) - run `npx synthesisui read` with the dev server up, tell them in a few lines the system it found, then ask the questions in `_synthesisui/reading.json` ONE AT A TIME with your question tool (the first option is the default). Save their answers as `{ \"<question id>\": <option index> }` and run `npx synthesisui read --confirm <that file>`: their answers become the roles and breaks this project keeps.";
14
+ /** A frase do revisor depois da criação - ver o comentário em `renderRegion`, junto dos papéis. */
15
+ const REVIEWER_AFTER_CREATION = "\n\n**How a change closes here.** The visual world is decided - the roles and breaks above. From here on SynthesisUI's check closes a change (`check_file`, and the end-of-turn check). Impeccable's reviewer is for creating: spawn `impeccable-finish-reviewer` or run `impeccable detect` only when the person asks for a new visual direction or a large new surface with its own look - never as the routine close of a change, and never its degraded self-review in its place.";
7
16
  const START = "<!-- synthesisui:start -->";
8
17
  const END = "<!-- synthesisui:end -->";
9
18
  /** Reads the installed DSs from the .lock files in _synthesisui/ds/<slug>/. */
@@ -336,9 +345,10 @@ async function renderRegion(projectRoot, installed) {
336
345
  "This project has the synthesisui agent set up and **no design system yet** - so there is",
337
346
  "no token vocabulary to follow, and nothing here to obey.",
338
347
  "",
339
- "To turn this repository into one, run `/sui-import-ds` and I will read what is",
340
- "already in the code: the colours, the type, the shapes and the components. Nothing",
341
- "is invented, and nothing is copied out.",
348
+ READ_THE_PAGE,
349
+ "",
350
+ "A system kept somewhere else - a published one, or a repository's own - comes in with",
351
+ "`/sui-import-ds` instead: it reads what is already in the code. Nothing is invented.",
342
352
  "",
343
353
  "The `synthesisui` MCP server is registered, so ASK it instead of guessing - it",
344
354
  "answers what exists and what does not. This block fills itself in once a system is",
@@ -468,6 +478,18 @@ written now** - read the code before you edit it, never a remembered signature.`
468
478
  * livres passa pelo `landing` antes de qualquer linha - ele lista as seções do catálogo e devolve o prompt da pilha, com
469
479
  * as decisões tomadas. Sem servidor também: o `landing` fala com o registro direto.
470
480
  */
481
+ /**
482
+ * O REVISOR DA CAMADA DE ESTÉTICA SÓ NA CRIAÇÃO - decisão dele, 18/10 15:23 ("só na criação"). Antes da confirmação o
483
+ * impeccable segue as próprias instruções, revisor incluído: o mundo ainda está sendo criado. Com o sistema confirmado,
484
+ * fecha a mudança a nossa checagem. Pela instrução, nunca apagando o agente dele - compor, não forkar (15:25). A medida
485
+ * que motivou: no fluxo com o revisor em todo turno, 52 de 103 min e metade dos tokens (estudo de 18/10, seção 12).
486
+ */
487
+ /** Os papéis e as quebras que a página dele já decidiu - ver `role-rules.ts` (18/10). Só existe com `rules.json`. */
488
+ const roleRules = await loadRoleRules(projectRoot).catch(() => null);
489
+ const rolesLine = roleRules
490
+ ? rolesContract(roleRules) + (existsSync(join(projectRoot, ".claude", "skills", "impeccable")) ? REVIEWER_AFTER_CREATION : "")
491
+ : /** Sem rules.json ainda: o agente oferece a leitura (jornada "a leitura no agente", E03, 18/10). */
492
+ `\n\n${READ_THE_PAGE}`;
471
493
  const landingLine = '\n\n**A landing asked in free words:** run `npx synthesisui@latest landing <slug> "<their words>"` first, and follow it.';
472
494
  const preferencesLine = recall.available
473
495
  ? `
@@ -497,7 +519,7 @@ is \`remember_preference\`.`
497
519
  ? `**These are true without asking anyone:** use the project's OWN custom properties, exactly as
498
520
  this system declares them. No raw colours, spacings or radii that a token already covers, and never a
499
521
  new token invented in silence - say so instead, because a new token is a decision for a person. There
500
- is no component index for an adopted system: the tokens ARE the contract.${theirsLine}
522
+ is no component index for an adopted system: the tokens ARE the contract.${theirsLine}${rolesLine}
501
523
 
502
524
  **What the vocabulary IS is not written here.** Fetch only the piece the task requires:
503
525
  \`system_doctrine\` for the rules and the voice, \`find_token\` for a value you are about to write.${memoryLine}${preferencesLine}${selfCheck}`
@@ -529,7 +551,7 @@ If nothing covers it, say which indexed entry you considered and why it did not
529
551
  project's OWN custom properties, exactly as the guide lists them. Do not write raw colours,
530
552
  spacings or radii that a token already covers, and do not invent a new token silently - say so
531
553
  instead, because a new token is a decision for a person to make. There is no component
532
- manifest for an adopted system - the tokens ARE the contract.${theirsLine}${landingLine}${selfCheck}`
554
+ manifest for an adopted system - the tokens ARE the contract.${theirsLine}${rolesLine}${landingLine}${selfCheck}`
533
555
  : `**When creating or editing components, read the system's GUIDE.md and follow it:** use the
534
556
  vocabulary it lists - every name in it is a name THIS project already declares, written exactly as
535
557
  you would write it. Do not use raw values outside the system's scale, and never a name from a
@@ -81,6 +81,8 @@ const IGNORED = [
81
81
  * da mesma versão, em branches separadas, darem conflito numa linha só.
82
82
  */
83
83
  ".fetched-at.json",
84
+ /** A oferta da leitura, feita uma vez neste clone - ver `offerTheReading` (fase 2, 18/10). */
85
+ ".read-offered",
84
86
  ];
85
87
  const IGNORE_HEADER = "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
86
88
  "# clone is governed; the measurement and the local record are not, because\n" +
@@ -316,7 +316,10 @@ export async function connect(opts) {
316
316
  * A PERGUNTA SÓ EXISTE ONDE HÁ ALGUÉM PARA RESPONDER. Em `--ci`, num pipe ou dentro de um agente,
317
317
  * `ask` vai `null` e a pasta decide - e a linha DIZ que foi ela, porque silêncio nunca é escolha.
318
318
  */
319
- const interactive = !opts.ci && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
319
+ const interactive = !opts.ci &&
320
+ opts.interactive !== false &&
321
+ Boolean(process.stdin.isTTY) &&
322
+ Boolean(process.stdout.isTTY);
320
323
  const chosen = await chooseAgents({
321
324
  root,
322
325
  flag: opts.agents,
@@ -471,7 +474,7 @@ export async function connect(opts) {
471
474
  */
472
475
  if (want.hook && wired.hook !== "skipped") {
473
476
  const before = await readAutoSync(root).catch(() => null);
474
- const now = before ?? (await askToSendAtTurnEnd());
477
+ const now = before ?? (opts.interactive === false ? null : await askToSendAtTurnEnd());
475
478
  if (before === null && now !== null)
476
479
  await writeAutoSync(root, now);
477
480
  row(before === null, now === true
@@ -784,7 +787,8 @@ export async function connect(opts) {
784
787
  }
785
788
  }
786
789
  /** A mesma versão que a fiação do editor recebe - os dois pinam no mesmo número. */
787
- await offerShellHook(opts.shell === true, opts.version);
790
+ if (opts.interactive !== false)
791
+ await offerShellHook(opts.shell === true, opts.version);
788
792
  /**
789
793
  * O REINÍCIO SÓ QUANDO ELE É NECESSÁRIO - e pedi-lo sempre é o que fez o dono achar que reiniciar
790
794
  * o editor fazia parte do fluxo (07/08).
@@ -1,6 +1,7 @@
1
1
  import { readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { ourSpellingLines } from "../our-spelling.js";
3
- import { join, relative, resolve } from "node:path";
3
+ import { existsSync, readdirSync, statSync } from "node:fs";
4
+ import { dirname, join, relative, resolve } from "node:path";
4
5
  import { readAutoSync, startSync } from "../auto-sync.js";
5
6
  import { changedSince } from "../changed-files.js";
6
7
  import { emptyTally, exportedNames, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
@@ -8,6 +9,7 @@ import { checkContracts } from "../doctor/contract-check.js";
8
9
  import { componentOf } from "../doctor/component-of.js";
9
10
  import { appendEvent, ledgerPath } from "../doctor/ledger.js";
10
11
  import { describeOffScale, diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
12
+ import { checkRoles, loadRoleRules, onlyOnce } from "../role-rules.js";
11
13
  import { declarados, falaDaPrateleira, naPrateleira } from "../doctor/shelf.js";
12
14
  import { governs, ungovernedIn } from "../governed.js";
13
15
  import { readMode } from "../mode.js";
@@ -313,6 +315,9 @@ async function report(root, filePath, mode) {
313
315
  const offScale = describeOffScale(d.files);
314
316
  /** A nossa grafia no arquivo dele - ver `our-spelling.ts`. Entra nas mesmas portas que os `phantoms`. */
315
317
  const ours = d.tokenUses;
318
+ /** Os papéis e as quebras que a página dele já decidiu - ver `role-rules.ts` (18/10). */
319
+ const roleRules = await loadRoleRules(root).catch(() => null);
320
+ const roles = roleRules ? await onlyOnce(root, rel, checkRoles(rel, src, roleRules, root)) : [];
316
321
  // Every check leaves one line in the ledger - INCLUDING the clean ones,
317
322
  // because a fix is itself a write, so the clean re-check of a file that was
318
323
  // just flagged is the evidence the agent read the report and acted. Without
@@ -327,6 +332,7 @@ async function report(root, filePath, mode) {
327
332
  phantoms: phantoms.length,
328
333
  unnamed: unnamed.length,
329
334
  offScale: d.files.reduce((n, f) => n + (f.offScale?.length ?? 0), 0),
335
+ ...(roleRules ? { roles: roles.length } : {}),
330
336
  ...(component ? { component } : {}),
331
337
  });
332
338
  /** O bloqueio de CONSUMIR é a única saída que recusa, e ele acompanha o relatório seja qual for. */
@@ -338,6 +344,7 @@ async function report(root, filePath, mode) {
338
344
  phantoms.length === 0 &&
339
345
  offScale.length === 0 &&
340
346
  unnamed.length === 0 &&
347
+ roles.length === 0 &&
341
348
  ours === 0) {
342
349
  const hello = await greet(root, rel);
343
350
  return hello ? say(hello) : null;
@@ -376,6 +383,7 @@ async function report(root, filePath, mode) {
376
383
  named.length === 0 &&
377
384
  phantoms.length === 0 &&
378
385
  offScale.length === 0 &&
386
+ roles.length === 0 &&
379
387
  ours === 0)
380
388
  return say(broke.join("\n"));
381
389
  const sample = unnamed
@@ -387,6 +395,7 @@ async function report(root, filePath, mode) {
387
395
  named.length === 0 &&
388
396
  phantoms.length === 0 &&
389
397
  offScale.length === 0 &&
398
+ roles.length === 0 &&
390
399
  ours === 0)
391
400
  return say([
392
401
  `${rel} - ${unnamed.length} value${unnamed.length === 1 ? "" : "s"} here ${unnamed.length === 1 ? "has" : "have"} no name in this system, and nothing to replace ${unnamed.length === 1 ? "it" : "them"} with: ${sample}${unnamed.length > 3 ? `, +${unnamed.length - 3} more` : ""}.`,
@@ -413,6 +422,9 @@ async function report(root, filePath, mode) {
413
422
  ...(broke.length > 0 ? [...broke, ""] : []),
414
423
  `${rel} - checked against ${table.name ?? table.slug}.`,
415
424
  ];
425
+ if (roles.length > 0) {
426
+ lines.push("", "Headings that leave the role this project already decided (read from its own pages):", ...roles, "", "Fix these now, while you have the file open - one role, one class.");
427
+ }
416
428
  if (named.length > 0) {
417
429
  lines.push("", "Values written by hand that this system already has a name for:", ...named
418
430
  .slice(0, 20)
@@ -459,8 +471,28 @@ const namedInPayload = (input) => {
459
471
  input.tool_name === "MultiEdit";
460
472
  return writes ? (input.tool_input?.file_path ?? null) : null;
461
473
  };
474
+ /**
475
+ * A RAIZ É O PROJETO, NÃO A PASTA ONDE A SESSÃO PAROU - jornada "a leitura no agente", E01 (18/10).
476
+ *
477
+ * O Claude Code roda o gancho na pasta em que o terminal do agente está. Na rodada dos relógios, o agente terminou os
478
+ * turnos 3 e 7 com `cd .impeccable/review`; o fim do turno rodou ali, não achou `_synthesisui/` e passou calado - a
479
+ * página de cada relógio, o trabalho inteiro do turno 3, nunca foi conferida. A ordem: a pasta que o Claude Code
480
+ * declara (`CLAUDE_PROJECT_DIR`), quando ela é um projeto nosso; senão, a pasta mais próxima, subindo, que tenha
481
+ * `_synthesisui/`; senão, onde estamos (o comportamento de antes).
482
+ */
483
+ export function projectRootFrom(cwd, env = process.env) {
484
+ const declared = env.CLAUDE_PROJECT_DIR;
485
+ if (declared && existsSync(join(declared, "_synthesisui")))
486
+ return resolve(declared);
487
+ for (let dir = resolve(cwd);; dir = dirname(dir)) {
488
+ if (existsSync(join(dir, "_synthesisui")))
489
+ return dir;
490
+ if (dirname(dir) === dir)
491
+ return resolve(cwd);
492
+ }
493
+ }
462
494
  export async function hook(opts) {
463
- const root = resolve(opts.dir ?? process.cwd());
495
+ const root = resolve(opts.dir ?? projectRootFrom(process.cwd()));
464
496
  let raw = "";
465
497
  for await (const chunk of process.stdin)
466
498
  raw += chunk;
@@ -711,7 +743,11 @@ async function endOfTurn(root, input) {
711
743
  await startSync(root).catch(() => false);
712
744
  const out = (frame) => process.stdout.write(`${JSON.stringify(frame)}\n`);
713
745
  if (input.stop_hook_active) {
714
- out(pass());
746
+ /** A única exceção à passagem calada: a oferta que ainda não chegou à resposta final - ver `offerTheReading`. */
747
+ const again = existsSync(join(root, "_synthesisui", READ_OFFERED_FILE))
748
+ ? await offerTheReading(root, [], input.transcript_path).catch(() => null)
749
+ : null;
750
+ out(again ? { decision: "block", reason: again } : pass());
715
751
  return;
716
752
  }
717
753
  const clock = await turnClockOf(root);
@@ -731,10 +767,166 @@ async function endOfTurn(root, input) {
731
767
  const grown = await takeGrowthNote(root).catch(() => null);
732
768
  if (grown)
733
769
  said.push(grown);
770
+ const offer = await offerTheReading(root, changed, input.transcript_path, clock).catch(() => null);
734
771
  await moveTurnClock(root);
735
- out(said.length === 0
772
+ out(said.length === 0 && !offer
736
773
  ? pass()
737
- : { decision: "block", reason: [...said, CLOSE_THE_TURN].join("\n\n") });
774
+ : { decision: "block", reason: [...said, offer ?? CLOSE_THE_TURN].join("\n\n") });
775
+ }
776
+ /** O arquivo que lembra que a oferta já foi feita neste clone - ignorado pelo git (ver `add.ts`). */
777
+ export const READ_OFFERED_FILE = ".read-offered";
778
+ /** Uma tela: o que o agente escreve quando faz interface. CSS conta - a primeira tela do impeccable mexe no globals. */
779
+ const SCREEN = /\.(tsx|jsx|vue|svelte|astro|html|css|scss)$/;
780
+ /**
781
+ * A LEITURA OFERECIDA SOZINHA - E03 da jornada "um instalador, uma porta" (fase 2, 18/10). O dono: *"o agente oferece a
782
+ * leitura sozinho depois da primeira tela"*. A linha do contrato já dizia "depois da primeira tela, ofereça" - e o agente
783
+ * só oferecia quando a pessoa pedia (medido na fase 1). O fim do turno é o único momento que vê o turno inteiro, então é
784
+ * ele quem pede.
785
+ *
786
+ * QUANDO A CRIAÇÃO ACABOU, NÃO NO MEIO DELA - decisão dele em 19/10, depois de seis rodadas reais. O impeccable tem um
787
+ * fim: a última coisa que ele escreve é o `DESIGN.md` (1-2 min antes do fim nas 3 rodadas que terminaram, porta-1, 3 e 6;
788
+ * ausente nas 3 em que a criação ainda perguntava "outra rodada?", porta-2, 4 e 5). Oferecer antes disso punha duas
789
+ * perguntas na mesma resposta, e a do impeccable ganhava. Então: com o impeccable no projeto, o sinal é o `DESIGN.md`
790
+ * escrito; sem ele, a primeira tela.
791
+ *
792
+ * FEITA É QUANDO ESTÁ NA RESPOSTA FINAL. Enquanto a última resposta do agente não oferece a leitura, o fim do turno pede de
793
+ * novo - três vezes no máximo. Depois de oferecida, ela vale até a PESSOA falar: na porta-6 a oferta saiu certa e os
794
+ * subagentes do impeccable acordaram o agente mais 4 vezes, e a resposta final ficou sem ela. Enquanto a pessoa não falou
795
+ * e a última resposta não tem a oferta, ela volta no fim (cinco renovações no máximo, a trava contra laço). Quando a
796
+ * pessoa fala, acabou: a recusa não vira insistência.
797
+ *
798
+ * SÓ NA PORTA: projeto sem sistema nenhum. Quem já tem um (do catálogo, importado, ou o da leitura com `rules.json`)
799
+ * escolheu o caminho dele (achado na suíte, 18/10: o fecho do turno de um sistema instalado passou a terminar na oferta).
800
+ * Sem o registro da conversa não há como ler a resposta - e aí se pede uma vez só.
801
+ */
802
+ const MAX_OFFERS = 3;
803
+ const MAX_RENEWALS = 5;
804
+ export async function offerTheReading(root, changed, transcript, since = 0) {
805
+ const ds = join(root, "_synthesisui", "ds");
806
+ if (existsSync(ds) && readdirSync(ds).some((d) => statSync(join(ds, d)).isDirectory()))
807
+ return null;
808
+ const marker = join(root, "_synthesisui", READ_OFFERED_FILE);
809
+ const save = (s) => writeFile(marker, `${JSON.stringify(s)}\n`);
810
+ if (!existsSync(marker)) {
811
+ if (!(await creationDone(root, changed, since)))
812
+ return null;
813
+ await save({ count: 1, at: Date.now() });
814
+ return OFFER;
815
+ }
816
+ let state = null;
817
+ try {
818
+ state = JSON.parse(await readFile(marker, "utf8"));
819
+ }
820
+ catch {
821
+ return null;
822
+ }
823
+ if (!state || !transcript)
824
+ return null;
825
+ if (state.delivered) {
826
+ if ((state.renewals ?? 0) >= MAX_RENEWALS)
827
+ return null;
828
+ if (await lastReplyOffers(transcript))
829
+ return null;
830
+ if (typeof state.deliveredAt !== "number" || (await personSpokeAfter(transcript, state.deliveredAt))) {
831
+ await save({ ...state, renewals: MAX_RENEWALS });
832
+ return null;
833
+ }
834
+ await save({ ...state, renewals: (state.renewals ?? 0) + 1 });
835
+ return `You were woken up again after offering it, so it is no longer at the end of your reply. ${OFFER}`;
836
+ }
837
+ if ((state.count ?? MAX_OFFERS) >= MAX_OFFERS)
838
+ return null;
839
+ if (await lastReplyOffers(transcript)) {
840
+ await save({ ...state, delivered: true, deliveredAt: Date.now() });
841
+ return null;
842
+ }
843
+ const count = (state.count ?? 0) + 1;
844
+ await save({ ...state, count, at: Date.now() });
845
+ return `Your last reply did not offer it. ${OFFER}`;
846
+ }
847
+ /**
848
+ * A CRIAÇÃO ACABOU NESTE TURNO? Com o impeccable (a skill que o instalador DELE escreve), o `DESIGN.md` na raiz, e escrito
849
+ * ou acompanhado de uma tela neste turno - um `DESIGN.md` antigo, sem tela nova, não é fim de criação nenhuma. Sem o
850
+ * impeccable, a primeira tela.
851
+ */
852
+ async function creationDone(root, changed, since) {
853
+ const screen = changed.some((c) => SCREEN.test(c.rel));
854
+ if (!existsSync(join(root, ".claude", "skills", "impeccable")))
855
+ return screen;
856
+ const design = await stat(join(root, "DESIGN.md")).catch(() => null);
857
+ if (!design?.isFile())
858
+ return false;
859
+ return screen || design.mtimeMs >= since;
860
+ }
861
+ const OFFER = "The creation of this project's first screen is done, and it has no system yet. Answer anything above in a line or two, then the summary of what this turn did - and END your reply with ONE sentence offering to read the page and turn what it already decided into their design system (about a minute, a few questions). That sentence is the LAST line, after any question of your own. If they say yes, follow the contract: `npx synthesisui read` with the dev server up. Offer it ONCE: if their next message does not take it up, that is a no - never offer it again in later turns.";
862
+ /**
863
+ * A ÚLTIMA RESPOSTA DO AGENTE OFERECE A LEITURA? A última entrada `assistant` com texto, no registro. Nas quatro rodadas
864
+ * em que ele ofereceu, a frase tinha as duas coisas: ler a página (ou o site) e o sistema - "Want me to read the page and
865
+ * turn what it's already decided into your design system?". Na porta-7 (19/10) a oferta veio pelo nome do comando, sem
866
+ * "page": "I can run `npx synthesisui read` and ask you a few questions to lock in the design system".
867
+ */
868
+ export async function lastReplyOffers(transcript) {
869
+ const raw = await readFile(transcript, "utf8").catch(() => null);
870
+ if (raw === null)
871
+ return false;
872
+ for (const line of raw.split("\n").reverse()) {
873
+ if (!/"type":\s*"assistant"/.test(line))
874
+ continue;
875
+ let e;
876
+ try {
877
+ e = JSON.parse(line);
878
+ }
879
+ catch {
880
+ continue;
881
+ }
882
+ const c = e.message?.content;
883
+ const text = Array.isArray(c)
884
+ ? c.filter((x) => x?.type === "text").map((x) => x.text ?? "").join(" ")
885
+ : typeof c === "string" ? c : "";
886
+ if (!text.trim())
887
+ continue;
888
+ if (!/\bsystem\b/i.test(text))
889
+ return false;
890
+ return /\bread\b[^.?!]{0,60}\b(page|site|landing)\b/i.test(text) || /synthesisui read\b/i.test(text);
891
+ }
892
+ return false;
893
+ }
894
+ /**
895
+ * A PESSOA FALOU DEPOIS DE `at`? No registro, a fala dela é uma entrada `user` que não é meta, não é resultado de
896
+ * ferramenta, não é o retorno de um gancho e não é o aviso de um subagente - os três últimos também chegam como `user`.
897
+ * Sem o registro, conta como falou: na dúvida, não se insiste.
898
+ */
899
+ export async function personSpokeAfter(transcript, at) {
900
+ const raw = await readFile(transcript, "utf8").catch(() => null);
901
+ if (raw === null)
902
+ return true;
903
+ for (const line of raw.split("\n").reverse()) {
904
+ if (!/"type":\s*"user"/.test(line))
905
+ continue;
906
+ let e;
907
+ try {
908
+ e = JSON.parse(line);
909
+ }
910
+ catch {
911
+ continue;
912
+ }
913
+ if (e.type !== "user" || e.isMeta || !e.timestamp)
914
+ continue;
915
+ if (!(Date.parse(e.timestamp) > at))
916
+ return false;
917
+ const c = e.message?.content;
918
+ const text = typeof c === "string"
919
+ ? c
920
+ : Array.isArray(c) && !c.some((x) => x?.type === "tool_result")
921
+ ? c.map((x) => x?.text ?? "").join(" ")
922
+ : null;
923
+ if (text === null)
924
+ continue;
925
+ if (/^\s*(<task-notification>|<command-|<system-reminder>|Stop hook feedback)/.test(text))
926
+ continue;
927
+ return true;
928
+ }
929
+ return false;
738
930
  }
739
931
  /**
740
932
  * A ÚLTIMA MENSAGEM É O RESUMO, NÃO O RODAPÉ - bateria ampliada, item 19 (29/09). Este bloqueio faz o agente responder
@@ -2,10 +2,12 @@ import { readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { DEFAULT_CONFIG, resolveRegistry, writeProjectConfig, } from "../config.js";
4
4
  import { readProjectTheme, writeBesideTheirs, writeNewProjectRoot, writeNewProjectTheme, } from "../new-project-theme.js";
5
+ import { openTheDoor } from "../door.js";
5
6
  import { body, section, snippet } from "../output.js";
6
7
  import { fetchDesignSystem } from "../registry.js";
7
8
  import { resolveDeps, stylesFor, tailwindMajor } from "../stack.js";
8
9
  import { formMix, measuredStyle } from "../styles-flavour.js";
10
+ import { desligarCaptura, ligarCaptura } from "../transcript.js";
9
11
  import { add } from "./add.js";
10
12
  const whereOf = (answer) => answer === "file" || answer === "1"
11
13
  ? "file"
@@ -57,6 +59,14 @@ export async function init(opts) {
57
59
  */
58
60
  if (opts.ds)
59
61
  await fetchDesignSystem(resolveRegistry(opts.registry), opts.ds);
62
+ /**
63
+ * SEM `--ds`, O `init` É A PORTA (fase 2, 18/10) - ver `door.ts`. O que ele imprimia sobre o config continua sendo
64
+ * calculado igual, só que guardado: a porta fecha com um resumo de cinco linhas, e o detalhe do config é o que menos
65
+ * importa para quem acabou de chegar.
66
+ */
67
+ const door = !opts.ds;
68
+ if (door)
69
+ ligarCaptura();
60
70
  const target = opts.target === "general" ? "general" : "next";
61
71
  const config = {
62
72
  target,
@@ -270,11 +280,14 @@ export async function init(opts) {
270
280
  console.log(body("first clean file and goes quiet after that."));
271
281
  return;
272
282
  }
273
- console.log("");
274
- console.log("Next steps:");
275
- console.log(" • synthesisui connect wire your agent to the system");
276
- console.log(" • synthesisui add <slug> bring a design system in");
277
- console.log(" • synthesisui template <slug> <name> materialize a full page");
283
+ await openTheDoor({
284
+ root,
285
+ version: opts.cli ?? "latest",
286
+ ...(opts.registry ? { registry: opts.registry } : {}),
287
+ ...(opts.pkg ? { pkg: opts.pkg } : {}),
288
+ ...(opts.aesthetic ? { aesthetic: opts.aesthetic } : {}),
289
+ configLines: desligarCaptura(),
290
+ });
278
291
  }
279
292
  /**
280
293
  * O QUE O `init` ACABOU DE DECLARAR VAI PARA O `.lock` (jornada "o pedido livre sai mastigado", 15/10).