@softize/opus 8.8.2 → 8.9.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/bin/cli.mjs +45 -15
  4. package/bin/lib/create.mjs +5 -5
  5. package/bin/lib/init.mjs +20 -228
  6. package/bin/lib/materialize.mjs +279 -0
  7. package/bin/lib/mcp.mjs +3 -3
  8. package/bin/lib/postinstall.mjs +7 -4
  9. package/bin/lib/validate-skill.mjs +40 -0
  10. package/docs/code-style.md +4 -4
  11. package/docs/releasing.md +12 -14
  12. package/package.json +17 -15
  13. package/registry/git/pre-push +8 -0
  14. package/registry/git/pre-push.d/opus +9 -0
  15. package/registry/github/opus.yml +22 -0
  16. package/registry/instructions/opus.md +15 -0
  17. package/registry/skills/build-opus-ui/SKILL.md +39 -0
  18. package/registry/skills/build-opus-ui/agents/openai.yaml +4 -0
  19. package/registry/skills/build-opus-ui/references/evaluations.md +5 -0
  20. package/registry/skills/build-opus-ui/references/ui-patterns.md +8 -0
  21. package/registry/skills/create-opus-action/SKILL.md +43 -0
  22. package/registry/skills/create-opus-action/agents/openai.yaml +4 -0
  23. package/registry/skills/create-opus-action/references/contract-and-binding.md +12 -0
  24. package/registry/skills/create-opus-action/references/evaluations.md +5 -0
  25. package/registry/skills/create-opus-action/scripts/scaffold.mjs +100 -0
  26. package/registry/skills/implement-opus-change/SKILL.md +43 -0
  27. package/registry/skills/implement-opus-change/agents/openai.yaml +4 -0
  28. package/registry/skills/implement-opus-change/references/evaluations.md +5 -0
  29. package/registry/skills/implement-opus-change/references/protocol-boundaries.md +11 -0
  30. package/registry/skills/test-opus-action/SKILL.md +38 -0
  31. package/registry/skills/test-opus-action/agents/openai.yaml +4 -0
  32. package/registry/skills/test-opus-action/references/evaluations.md +5 -0
  33. package/registry/skills/test-opus-action/references/harness.md +10 -0
  34. package/registry/skills/upgrade-opus/SKILL.md +37 -0
  35. package/registry/skills/upgrade-opus/agents/openai.yaml +4 -0
  36. package/registry/skills/upgrade-opus/references/evaluations.md +5 -0
  37. package/registry/skills/upgrade-opus/references/upgrade-checklist.md +8 -0
  38. package/registry/templates/app/src/domains/tasks/index.ts +1 -1
  39. package/src/ui/docs/content/actions.md +1 -1
  40. package/src/ui/docs/content/cli.md +2 -2
  41. package/src/ui/docs/content/customization.md +1 -1
  42. package/src/ui/docs/content/data.md +1 -1
  43. package/src/ui/docs/content/microcopy.md +1 -1
  44. package/src/ui/docs/content/scroll-area.md +1 -1
  45. package/src/ui/docs/content/tokens.md +1 -1
  46. package/src/ui/docs/content/ui.md +1 -1
  47. package/src/ui/docs/doc-client.tsx +2 -2
  48. package/src/ui/theme.css +1 -1
  49. package/registry/hooks/hooks.json +0 -26
  50. package/registry/hooks/link-memory-on-start.mjs +0 -46
  51. package/registry/skills/create-action/SKILL.md +0 -49
  52. package/registry/skills/create-action/scaffold.mjs +0 -122
package/CHANGELOG.md CHANGED
@@ -11,6 +11,31 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
11
11
  > tinha ficado sem registro nenhum, o que é exatamente o caso que este arquivo existe
12
12
  > pra cobrir.
13
13
 
14
+ ## 8.9.1 — 2026-08-04
15
+
16
+ **A migração deixa de esconder o hook Opus no Git.** `opus setup` agora remove também a
17
+ exclusão local legada e específica de `.claude/hooks/opus-check-on-stop.mjs`, preservando
18
+ as preferências realmente locais. Isso fecha o caso em que o check passava na máquina que
19
+ já tinha o hook, mas o arquivo não entrava no commit para um clone novo.
20
+
21
+ ## 8.9.0 — 2026-08-04
22
+
23
+ **O Opus passa a materializar e versionar suas próprias skills e garantias.** `opus setup`
24
+ agora projeta as cinco skills do SDK para Claude e Codex, instala o hook Stop, participa do
25
+ pre-push composto, cria o gate `opus.yml` e mantém um bloco próprio em `AGENTS.md` e
26
+ `CLAUDE.md`. Tudo viaja no Git e `opus check` recusa drift entre o pacote instalado,
27
+ `base.json` e os arquivos comitados. O Maestro deixa de ser requisito operacional.
28
+
29
+ O catálogo é `implement-opus-change`, `create-opus-action`, `test-opus-action`,
30
+ `build-opus-ui` e `upgrade-opus`. `create-opus-action` gera o split atual
31
+ `defineContract` + `bindAction`; a skill legada `create-action`, que ainda emitia handler
32
+ inline, foi removida.
33
+
34
+ **Migração:** depois do bump, rode `pnpm exec opus setup`, remova do índice local quaisquer
35
+ exclusões mais amplas de `.claude/` ou `.agents/` que o setup reportar e comite `base.json`,
36
+ as duas projeções de skills, hooks, instruções, pre-push e workflow. O marcador per-app
37
+ `opus.json` passa de `base/baseVersion` para `package/version`.
38
+
14
39
  ## 8.6.6 — 2026-07-30
15
40
 
16
41
  **O pacote passa a ser publicado no npm PÚBLICO.** O registry próprio
package/README.md CHANGED
@@ -81,7 +81,7 @@ await app.listen({ port: 3000 })
81
81
  src/
82
82
  core/ schema/ server/ client/ ui/
83
83
  data/ auth/ audit/ log/ queue/ events/ scheduler/ dsl/
84
- registry/ # scaffolds + skills (create-action) — geração, não runtime
84
+ registry/ # scaffolds + skills Opus — geração e conhecimento do SDK, não runtime
85
85
  bin/ # CLI (opus setup/gen/check/introspect/mcp) + libs
86
86
  docs/
87
87
  protocol.md # contrato completo (16 seções)
package/bin/cli.mjs CHANGED
@@ -20,8 +20,9 @@ import { cmdGen, helpGen } from './lib/gen.mjs'
20
20
  import { cmdDb } from './lib/db.mjs'
21
21
  import { scanDir, emptyScanVerdict } from './lib/check.mjs'
22
22
  import { createMonorepo, createProject } from './lib/create.mjs'
23
- import { initProject, setupUiFoundation } from './lib/init.mjs'
23
+ import { initProject, repoRootOf, setupUiFoundation } from './lib/init.mjs'
24
24
  import { introspect } from './lib/introspect.mjs'
25
+ import { materializeOpus } from './lib/materialize.mjs'
25
26
 
26
27
  const __filename = fileURLToPath(import.meta.url)
27
28
  const __dirname = path.dirname(__filename)
@@ -167,6 +168,12 @@ async function cmdAdd(name, options) {
167
168
  async function cmdCheck(dir) {
168
169
  const root = path.resolve(process.cwd(), dir ?? '.')
169
170
  const rel = path.relative(process.cwd(), root) || '.'
171
+ const repoRoot = (await repoRootOf(root)) ?? root
172
+ const materialization = materializeOpus(repoRoot, 'check')
173
+ if (!materialization.ok) {
174
+ for (const error of materialization.errors) log('error', `✗ opus check: ${error}`)
175
+ process.exit(1)
176
+ }
170
177
  const { files, actions, contracts, findings, hasMarker } = await scanDir(root)
171
178
 
172
179
  if (actions === 0) {
@@ -202,6 +209,16 @@ async function cmdCheck(dir) {
202
209
  process.exit(1)
203
210
  }
204
211
 
212
+ async function cmdMaterializationCheck() {
213
+ const root = (await repoRootOf(process.cwd())) ?? process.cwd()
214
+ const result = materializeOpus(root, 'check')
215
+ if (!result.ok) {
216
+ for (const error of result.errors) log('error', `✗ opus check: ${error}`)
217
+ process.exit(1)
218
+ }
219
+ log('success', '✓ opus check: artefatos materializados estão atualizados.')
220
+ }
221
+
205
222
  async function cmdCreate(dir, flags) {
206
223
  if (!dir) {
207
224
  log('error', 'Uso: opus create <dir> — o nome do diretório vira o nome do app (--monorepo: a raiz do workspace).')
@@ -224,6 +241,9 @@ async function cmdCreate(dir, flags) {
224
241
  }
225
242
 
226
243
  const r = await createProject(REGISTRY_DIR, target, name)
244
+ if (!r.setup.materialization.ok) {
245
+ throw new Error(`setup incompleto: ${r.setup.materialization.errors.join(' | ')}`)
246
+ }
227
247
  const modeLabel = r.mode === 'app' ? 'app no monorepo (workspace detectado)' : 'repo standalone'
228
248
  log('success', `✓ opus create: app "${name}" criado (base v${r.version}, ${modeLabel}).`)
229
249
  console.log(`\n ${r.files.length} arquivos do esqueleto + setup (${r.setup.created.join(', ')}).`)
@@ -238,7 +258,7 @@ async function cmdCreate(dir, flags) {
238
258
  pnpm install
239
259
  pnpm --filter ${name} test && pnpm --filter ${name} manifest
240
260
 
241
- Depois: cadastro do app no admin (root/run) sessão no Maestro.
261
+ Opcional: cadastre o app em uma interface de desenvolvimento como o Maestro.
242
262
  `)
243
263
  } else {
244
264
  console.log(`
@@ -248,7 +268,7 @@ async function cmdCreate(dir, flags) {
248
268
  pnpm install
249
269
  pnpm test && pnpm exec opus check src && pnpm manifest
250
270
 
251
- Depois: repo no GitHub + cadastro no admin (workspace/repo/app/agentes) sessão no Maestro.
271
+ Opcional: cadastre o repo em uma interface de desenvolvimento como o Maestro.
252
272
  `)
253
273
  }
254
274
  }
@@ -256,6 +276,10 @@ async function cmdCreate(dir, flags) {
256
276
  async function cmdInit() {
257
277
  const cwd = process.cwd()
258
278
  const r = await initProject(REGISTRY_DIR, cwd)
279
+ if (!r.materialization.ok) {
280
+ for (const error of r.materialization.errors) log('error', `✗ opus setup: ${error}`)
281
+ process.exitCode = 1
282
+ }
259
283
  log('success', `✓ opus setup: app v${r.version} ${r.wasInitialized ? 'atualizado' : 'inicializado'}.`)
260
284
  if (r.created.length) {
261
285
  console.log('\n criados (per-app):')
@@ -287,9 +311,8 @@ async function cmdInit() {
287
311
  }
288
312
 
289
313
  console.log(
290
- '\n Per-app: opus.json (pin) + CLAUDE.md. A spec vive nas declarações (description → manifest).\n' +
291
- ' A camada-base (.claude agents+skills) é REPO-LEVEL o Maestro a materializa.\n' +
292
- ' Gate de build: opus check.\n',
314
+ '\n Per-app: opus.json. No repo: base.json + skills/hooks/instruções comitados.\n' +
315
+ ' A spec vive nas declarações (description manifest). Gate: opus check.\n',
293
316
  )
294
317
  }
295
318
 
@@ -378,14 +401,15 @@ async function main() {
378
401
  Comandos:
379
402
  create <dir> Scaffolda um app opus-based canônico (vite+react+ui+domínio-exemplo);
380
403
  detecta workspace (app em monorepo); --monorepo cria a RAIZ do workspace
381
- setup Bootstrap PER-APP: grava opus.json + CLAUDE.md (se faltam)
404
+ setup Reconcilia opus.json e os artefatos Opus versionados para Claude/Codex
382
405
  add <name> Copia template do registry pro consumer
383
406
  list Lista templates disponíveis
384
407
  gen Gera manifest/openapi/docs/stubs a partir do opus.config.ts
385
408
  check [dir] Valida as convenções das actions (régua de padrão; exit ≠ 0 se violar)
409
+ pre-push Gate Git: freshness dos artefatos + convenções das actions
386
410
  db <verbo> Comandos de banco. v1: db check (drift-check entidade ↔ banco)
387
411
  introspect [dir] Modelo da estrutura (actions/reactions/schedules + wiring); --json
388
- mcp Server MCP (introspect/check/create-action) — agentes via mcp_config
412
+ mcp Server MCP (introspect/check/scaffold de action) — agentes via mcp_config
389
413
  help Mostra esta mensagem
390
414
 
391
415
  Flags:
@@ -411,8 +435,8 @@ Scaffolda um app opus-based CANÔNICO no diretório <dir> (recusa dir não-vazio
411
435
  O esqueleto versiona com a base — nasce com os pré-requisitos plugados:
412
436
  protocolo opus.config.ts + domínio-exemplo + manifest/scripts (+ teste verde)
413
437
  UI vite + react + tailwind v4 CSS-first (tema + @source do Opus)
414
- Maestro dev server na porta injetada (PORT) + allowedHosts do preview
415
- dia zero opus.json, CLAUDE.md (bloco), .claude/memory/, CI de fábrica
438
+ desenvolvimento dev server na porta injetada (PORT) + allowedHosts do preview
439
+ dia zero opus.json + skills/hooks/instruções comitados no repo
416
440
 
417
441
  Depois: git init && pnpm install && pnpm test — e cadastre repo/app no admin.
418
442
  `)
@@ -427,16 +451,16 @@ Depois: git init && pnpm install && pnpm test — e cadastre repo/app no admin.
427
451
  console.log(`
428
452
  @softize/opus setup
429
453
 
430
- Bootstrap PER-APP de um projeto pra usar a base (idempotente):
431
- opus.json marcador com a versão da base pinada (fonte da verdade)
432
- CLAUDE.md pointer do protocolo criado só se faltar (não sobrescreve)
454
+ Setup idempotente de um projeto Opus:
455
+ opus.json marcador per-app com a versão do SDK
456
+ base.json versão aplicada e eventuais skills excluídas
457
+ agentes skills Claude/Codex, hook, instruções, pre-push e CI comitados
433
458
 
434
459
  Fundação de UI (só apps web — preset Tailwind + tema + flags do Opus):
435
460
  tailwind.config criado se faltar; senão avisa pra estender o preset
436
461
  tema/tsconfig/dep — avisa o que falta plugar (não edita seus arquivos: sem clobber)
437
462
 
438
- A camada-base (agents/skills) é REPO-LEVEL materializada pelo MAESTRO (o regente),
439
- NÃO pelo setup. Roda manual ou via postinstall.
463
+ O próprio pacote materializa somente seus artefatos. Base e Maestro são opcionais.
440
464
  `)
441
465
  return
442
466
  }
@@ -500,6 +524,12 @@ passa verde). Default dir: cwd.
500
524
  return
501
525
  }
502
526
 
527
+ if (command === 'pre-push') {
528
+ if (rest[0] === 'materialization') await cmdMaterializationCheck()
529
+ else await cmdCheck(rest[0] ?? '.')
530
+ return
531
+ }
532
+
503
533
  if (command === 'db') {
504
534
  await cmdDb(rest, flags)
505
535
  return
@@ -3,7 +3,7 @@
3
3
  * (`registry/templates/app`). O esqueleto versiona com a base: sai do mesmo tarball que
4
4
  * o SDK que ele configura, então nasce certo por construção — os pré-requisitos do
5
5
  * protocolo (opus.config/manifest/scripts), da UI (preset+tema v4 CSS-first) e do
6
- * Maestro (porta injetada, allowedHosts do preview) já vêm plugados.
6
+ * previews externos (porta injetada, allowedHosts configurável) já vêm plugados.
7
7
  *
8
8
  * DOIS MODOS, por detecção (sobe do destino procurando `pnpm-workspace.yaml`):
9
9
  * • standalone — repo próprio: o template inteiro (raiz inclusa);
@@ -17,7 +17,7 @@
17
17
  * o npm pack descarta dotfiles do tarball, então a fonte não pode tê-los;
18
18
  * • tokens `{{APP_NAME}}` e `{{OPUS_VERSION}}` são substituídos em todo arquivo.
19
19
  *
20
- * Depois da cópia roda o `initProject` (setup): opus.json, CLAUDE.md (bloco),
20
+ * Depois da cópia roda o `initProject` (setup): opus.json e artefatos Opus rastreáveis,
21
21
  * `.claude/memory/` e o CI de fábrica (repo-level: acha a raiz pelo `.git`).
22
22
  */
23
23
 
@@ -201,8 +201,8 @@ export async function createProject(registryDir, targetDir, appName) {
201
201
 
202
202
  const warnings = mode === 'app' ? await workspaceWarnings(wsRoot, path.resolve(targetDir)) : []
203
203
 
204
- // Setup por cima: opus.json (pin), CLAUDE.md (bloco), .claude/memory/, CI de fábrica
205
- // (os repo-level acham a raiz pelo .git no monorepo caem na raiz, só-se-faltar).
204
+ // Setup por cima: opus.json per-app e materialização repo-level. Em monorepo, a raiz
205
+ // é descoberta pelo Git e recebe skills, hooks, instruções e CI específicos do Opus.
206
206
  const setup = await initProject(registryDir, targetDir)
207
- return { version, mode, files, warnings, setup: { created: setup.created, warnings: setup.warnings } }
207
+ return { version, mode, files, warnings, setup }
208
208
  }
package/bin/lib/init.mjs CHANGED
@@ -1,18 +1,9 @@
1
- /**
2
- * opus setup — faz um projeto "leigo" passar a CONHECER a base. PER-APP, idempotente:
3
- * • grava o marcador `opus.json` (versão da base pinada) — fonte da verdade
4
- * que torna o projeto auto-anunciável (Maestro/CI/agente leem isso);
5
- * • cria `CLAUDE.md` SE faltar; se existir, atualiza SÓ o bloco gerenciado
6
- * (`<!-- opus:base -->…<!-- /opus:base -->`) — fora dele o arquivo é do projeto
7
- * e nunca é tocado; arquivo antigo sem marcadores fica 100% intocado.
8
- *
9
- * NÃO materializa a camada-base (`.claude`) — isso é REPO-LEVEL, do MAESTRO (pull do
10
- * admin). Re-rodar atualiza pin + bloco. Roda manual (`opus setup`) ou via postinstall.
11
- */
1
+ /** opus setup: aplica o marcador per-app e os artefatos Opus rastreáveis no repo. */
12
2
 
13
3
  import { promises as fs } from "node:fs";
14
4
  import path from "node:path";
15
- import { execFileSync } from "node:child_process";
5
+
6
+ import { materializeOpus, PACKAGE_VERSION } from "./materialize.mjs";
16
7
 
17
8
  const MARKER = "opus.json";
18
9
 
@@ -34,7 +25,7 @@ async function readJson(p) {
34
25
  }
35
26
 
36
27
  /** Raiz do repo (sobe até achar `.git`); sem repo → null (o chamador decide o fallback). */
37
- async function repoRootOf(dir) {
28
+ export async function repoRootOf(dir) {
38
29
  let cur = path.resolve(dir);
39
30
  for (;;) {
40
31
  if (await exists(path.join(cur, ".git"))) return cur;
@@ -44,86 +35,6 @@ async function repoRootOf(dir) {
44
35
  }
45
36
  }
46
37
 
47
- /** CI de fábrica do projeto (só se faltar — depois é seu): roda o que existir em cada
48
- * package via --if-present, então cresce junto com o projeto sem editar o workflow. */
49
- function checkWorkflowTemplate() {
50
- return `name: check
51
-
52
- # Gate de PR/push (gerado pelo \`opus setup\` — edite à vontade, é seu).
53
- # Roda o que existir em cada package: typecheck, test e manifest:check.
54
- on:
55
- push:
56
- branches: [main]
57
- pull_request: {}
58
- workflow_dispatch: {}
59
-
60
- jobs:
61
- check:
62
- runs-on: ubuntu-latest
63
- steps:
64
- - uses: actions/checkout@v4
65
- - uses: pnpm/action-setup@v4
66
- with:
67
- version: 11
68
- - uses: actions/setup-node@v4
69
- with:
70
- node-version: 22
71
- cache: pnpm
72
- - run: pnpm install --frozen-lockfile
73
- - name: Typecheck
74
- run: pnpm -r --if-present run typecheck
75
- - name: Testes
76
- run: pnpm -r --if-present run test
77
- - name: Formatação
78
- run: pnpm -r --if-present run format:check
79
- # Manifest commitado tem que estar fresco (spec defasada = diff que mente).
80
- - name: Manifest fresco
81
- run: pnpm -r --if-present run manifest:check
82
- `;
83
- }
84
-
85
- const BASE_OPEN = "<!-- opus:base -->";
86
- const BASE_CLOSE = "<!-- /opus:base -->";
87
- const BASE_BLOCK_RE = /<!-- opus:base -->[\s\S]*?<!-- \/opus:base -->/;
88
-
89
- /**
90
- * Bloco GERENCIADO do CLAUDE.md — o miolo que o setup mantém em dia quando o Opus
91
- * evolui (era only-if-missing: template melhorava e projeto antigo nunca via). Fora dos
92
- * marcadores o arquivo é do projeto e o setup nunca toca. Sem número de versão no texto
93
- * de propósito: a versão mora no `opus.json` (nada se duplica).
94
- */
95
- export function claudeMdBaseBlock() {
96
- return `${BASE_OPEN}
97
- # Opus
98
-
99
- Este projeto usa o **Opus** (\`@softize/opus\`), um SDK/protocolo de actions; a versão
100
- fica pinada em \`opus.json\`.
101
-
102
- ## Antes de mexer
103
- - **A camada \`.claude/\` é REPO-LEVEL, materializada — não editar à mão.** Skills, agents
104
- e hooks vivem na raiz do repo, postos pelo **orquestrador da base** (o Opus semeia as
105
- skills do framework; agents e método são o seu setup). Num monorepo o agente enxerga o
106
- repo todo (libs compartilhadas inclusas).
107
- - **Seu, deste app (per-app):** \`opus.json\` (pin), este \`CLAUDE.md\` (fora do bloco) e o código.
108
- - **A spec vive nas DECLARAÇÕES:** cada action/entidade carrega o \`description\` (o doc
109
- de negócio — a fonte); o \`opus gen\` projeta no manifest. Doc colado na declaração não
110
- defasa (a \`domain.md\` à parte foi aposentada).
111
- - **Gate de build:** \`opus check\` (valida as convenções das actions — defineAction e
112
- o split defineContract+bindAction; exit ≠ 0 se violar OU se não achar action nenhuma).
113
- Rode antes de commitar/buildar.
114
- - **Atualizar o Opus:** depois do bump, leia \`node_modules/@softize/opus/CHANGELOG.md\`
115
- (breaking = seção **Breaking**, com a migração) e rode os gates — eles apontam o
116
- que a mudança cobra do código.
117
- - **Estado/contratos ao vivo:** \`opus mcp\` (introspect/check/create-action).
118
- - **Nada se duplica:** estado/contratos+negócio → declarações (\`description\`) → manifest ·
119
- quem executa → agents · como se faz uma tarefa → skills. Regra duplicada em vez de
120
- apontada é defeito — aponte.
121
-
122
- > Bloco gerenciado pelo \`opus setup\` — acompanha o Opus. O que você escrever fora
123
- > dele é seu; o setup nunca toca.
124
- ${BASE_CLOSE}`;
125
- }
126
-
127
38
  // =============================================================================
128
39
  // Fundação de UI (apps web) — preset Tailwind + tema + flags
129
40
  // =============================================================================
@@ -395,152 +306,34 @@ export async function setupUiFoundation(projectDir) {
395
306
  return { applicable: true, created, warnings };
396
307
  }
397
308
 
398
- /**
399
- * Inicializa/atualiza um projeto pra usar a base.
400
- * @returns {Promise<{version:string, created:string[], synced:string[], warnings:string[], wasInitialized:boolean}>}
401
- */
402
- /**
403
- * Onde o `info/exclude` deste repo mora de verdade. `null` = não há onde escrever.
404
- *
405
- * Montar `<raiz>/.git/info/exclude` na mão parece óbvio e está errado em **worktree**,
406
- * que é como toda sessão do Maestro nasce: ali `.git` não é pasta, é um ARQUIVO com um
407
- * ponteiro (`gitdir: …`). Checar só existência passava, e o `mkdir` seguinte estourava
408
- * `ENOTDIR` — derrubando o postinstall, e com ele o `pnpm install` inteiro. O sintoma
409
- * chegava longe da causa: ambiente "subindo" pra sempre, sem erro na tela.
410
- *
411
- * Perguntar ao git responde certo nos dois layouts. O caminho montado à mão FICA como
412
- * rede: `opus setup` roda em postinstall, e num ambiente sem `git` no PATH trocar a
413
- * resposta por `null` faria a exclusão parar em silêncio — a base voltaria a sujar o
414
- * `git status` de todo mundo. Só se aplica quando `.git` é pasta; sendo arquivo, sem o
415
- * git não há como saber pra onde o ponteiro aponta, e chutar é o defeito original.
416
- */
417
- function excludePathOf(repoRoot) {
418
- try {
419
- const p = execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], {
420
- cwd: repoRoot,
421
- encoding: "utf-8",
422
- stdio: ["ignore", "pipe", "ignore"],
423
- }).trim();
424
- // `--git-path` devolve relativo ao cwd quando o repo é o próprio cwd.
425
- if (p !== "") return path.isAbsolute(p) ? p : path.join(repoRoot, p);
426
- } catch {
427
- /* sem git no PATH, ou fora de repo — cai na rede abaixo. */
428
- }
429
- return null;
430
- }
431
-
432
- /** A rede: só vale com `.git` PASTA, que é o layout onde montar o caminho acerta. */
433
- async function excludePathFallback(repoRoot) {
434
- const dotGit = path.join(repoRoot, ".git");
435
- try {
436
- if (!(await fs.stat(dotGit)).isDirectory()) return null;
437
- } catch {
438
- return null;
439
- }
440
- return path.join(dotGit, "info", "exclude");
441
- }
442
-
443
- /** Anexa padrões ao info/exclude do repo (exclusão local, fora do .gitignore). */
444
- async function gitExclude(repoRoot, patterns) {
445
- const excludePath = excludePathOf(repoRoot) ?? (await excludePathFallback(repoRoot));
446
- if (excludePath === null) return;
447
- let cur = "";
448
- try {
449
- cur = await fs.readFile(excludePath, "utf-8");
450
- } catch {
451
- /* exclude novo. */
452
- }
453
- const have = new Set(cur.split("\n").map((l) => l.trim()));
454
- const add = patterns.filter((p) => !have.has(p));
455
- if (add.length === 0) return;
456
- await fs.mkdir(path.dirname(excludePath), { recursive: true });
457
- await fs.writeFile(
458
- excludePath,
459
- `${cur}${cur.endsWith("\n") || cur === "" ? "" : "\n"}${add.join("\n")}\n`,
460
- );
461
- }
462
-
463
- /**
464
- * Semeia as skills DO PACOTE (registry/skills) em `.claude/skills/` do repo, sem
465
- * clobber (o Maestro é o reconciliador quando o projeto for regido). Untracked por
466
- * design: projeção da base, não código do projeto — vai pro info/exclude.
467
- */
468
- async function seedRegistrySkills(registryDir, repoRoot) {
469
- const from = path.join(registryDir, "skills");
470
- if (!(await exists(from))) return 0;
471
- let seeded = 0;
472
- for (const slug of await fs.readdir(from)) {
473
- const src = path.join(from, slug);
474
- if (!(await fs.stat(src)).isDirectory()) continue;
475
- const dest = path.join(repoRoot, ".claude", "skills", slug);
476
- if (await exists(dest)) continue;
477
- await fs.cp(src, dest, { recursive: true });
478
- seeded += 1;
479
- }
480
- if (seeded > 0) await gitExclude(repoRoot, [".claude/skills/"]);
481
- return seeded;
482
- }
483
-
484
309
  export async function initProject(registryDir, projectDir) {
310
+ void registryDir;
485
311
  const created = [];
486
312
  const synced = [];
487
- const pkg = await readJson(path.join(registryDir, "..", "package.json"));
488
- const version = pkg?.version ?? "0.0.0";
313
+ const version = PACKAGE_VERSION;
489
314
 
490
- // Marcador (sempre regravado — é da base; pina a versão).
491
315
  const markerPath = path.join(projectDir, MARKER);
492
316
  const prev = await readJson(markerPath);
493
- await fs.writeFile(
494
- markerPath,
495
- JSON.stringify({ base: "@softize/opus", baseVersion: version }, null, 2) +
496
- "\n",
497
- );
317
+ const marker = `${JSON.stringify({ package: "@softize/opus", version }, null, 2)}\n`;
318
+ if (!(await exists(markerPath)) || (await fs.readFile(markerPath, "utf8")) !== marker) {
319
+ await fs.writeFile(markerPath, marker);
320
+ (prev === null ? created : synced).push(MARKER);
321
+ }
498
322
 
499
- // CLAUDE.md cria se faltar; se existir, atualiza o bloco gerenciado (o resto é
500
- // do projeto). Arquivo antigo sem marcadores = 100% do projeto, intocado. A domain.md
501
- // foi APOSENTADA (13/06/2026): a spec vive nas declarações (`description`).
323
+ // Remove somente o bloco legado que o próprio setup antigo declarava como gerenciado.
502
324
  const claudePath = path.join(projectDir, "CLAUDE.md");
503
- const block = claudeMdBaseBlock();
504
- if (!(await exists(claudePath))) {
505
- await fs.writeFile(claudePath, `${block}\n`);
506
- created.push("CLAUDE.md");
507
- } else {
325
+ if (await exists(claudePath)) {
508
326
  const cur = await fs.readFile(claudePath, "utf-8");
509
- if (BASE_BLOCK_RE.test(cur)) {
510
- // Replacement por função: o bloco tem `$` em potencial (template) — literal, sem
511
- // os padrões especiais do replace.
512
- const next = cur.replace(BASE_BLOCK_RE, () => block);
513
- if (next !== cur) {
514
- await fs.writeFile(claudePath, next);
515
- synced.push("CLAUDE.md");
516
- }
327
+ const next = cur.replace(/<!-- opus:base -->[\s\S]*?<!-- \/opus:base -->\s*/g, "");
328
+ if (next !== cur) {
329
+ await fs.writeFile(claudePath, next);
330
+ synced.push("CLAUDE.md (bloco legado removido)");
517
331
  }
518
332
  }
519
333
 
520
- // Dia zero REPO-level — mas só do que é DO PROJETO (tracked, viaja no git):
521
- // • semente da memória versionada (política da skill memory; sem o dir, o hook
522
- // link-memory-on-start no-opa e a sessão roda amnésica);
523
- // • CI de fábrica (check.yml) se faltar.
524
- // A camada-base do ADMIN (agents + skills de metodologia) segue sendo do MAESTRO;
525
- // as skills DO PACOTE (registry/skills) entram aqui: esqueleto standalone não pode
526
- // nascer sem o que o próprio tarball carrega (projeção untracked, git-excluded —
527
- // o Maestro re-materializa por cima quando reger o projeto).
528
334
  const repoRoot = (await repoRootOf(projectDir)) ?? projectDir;
529
- const seededSkills = await seedRegistrySkills(registryDir, repoRoot);
530
- if (seededSkills > 0)
531
- created.push(`.claude/skills/ (${seededSkills} da base, untracked)`);
532
- const memDir = path.join(repoRoot, ".claude", "memory");
533
- if (!(await exists(memDir))) {
534
- await fs.mkdir(memDir, { recursive: true });
535
- await fs.writeFile(path.join(memDir, "MEMORY.md"), "");
536
- created.push(".claude/memory/ (repo)");
537
- }
538
- const ciPath = path.join(repoRoot, ".github", "workflows", "check.yml");
539
- if (!(await exists(ciPath))) {
540
- await fs.mkdir(path.dirname(ciPath), { recursive: true });
541
- await fs.writeFile(ciPath, checkWorkflowTemplate());
542
- created.push(".github/workflows/check.yml (repo)");
543
- }
335
+ const materialization = materializeOpus(repoRoot, "setup");
336
+ synced.push(...materialization.changes);
544
337
 
545
338
  // Avisos de dia zero (sem clobber — igual à fundação de UI): o que falta plugar à mão.
546
339
  const warnings = [];
@@ -551,6 +344,5 @@ export async function initProject(registryDir, projectDir) {
551
344
  );
552
345
  }
553
346
 
554
- if (!prev) created.unshift(MARKER);
555
- return { version, created, synced, warnings, wasInitialized: prev !== null };
347
+ return { version, created, synced, warnings, wasInitialized: prev !== null, materialization };
556
348
  }