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 +27 -5
- package/dist/commands/add.js +2 -0
- package/dist/commands/connect.js +7 -3
- package/dist/commands/hook.js +197 -5
- package/dist/commands/init.js +18 -5
- package/dist/commands/read.js +355 -0
- package/dist/door.js +176 -0
- package/dist/index.js +25 -2
- package/dist/install-marks.js +25 -2
- package/dist/reading/classify.js +181 -0
- package/dist/reading/page-script.js +64 -0
- package/dist/reading/png.js +66 -0
- package/dist/role-rules.js +237 -0
- package/package.json +1 -1
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
|
-
|
|
340
|
-
"
|
|
341
|
-
"
|
|
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
|
package/dist/commands/add.js
CHANGED
|
@@ -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" +
|
package/dist/commands/connect.js
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
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).
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { ourSpellingLines } from "../our-spelling.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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).
|