@fefsbenson/jarvis 1.2.0 → 1.3.0

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/bin/cli.js CHANGED
@@ -364,11 +364,28 @@ const INTOCAVEIS = [
364
364
  '.claude/settings.local.json',
365
365
  '.claude/jarvis/', // memória, estado, ponteiros
366
366
  '.claude/sessions/', // histórico de sessões
367
+ '.claude/mission-control/', // estado de missão e índices
368
+ '.claude/trash/', // o que o dono mandou para a lixeira
367
369
  'workspace/', // o negócio do dono
368
370
  'knowledge/business/', 'knowledge/personal/',
371
+ 'knowledge/external/inbox/', // o material que o DONO ingere
369
372
  'logs/', '.data/', 'artifacts/', 'processing/',
373
+ 'outputs/', 'research/', // L3 no directory-contract: produção do dono
370
374
  ];
371
375
 
376
+ /**
377
+ * Sufixos protegidos DENTRO de área do sistema.
378
+ *
379
+ * `agents/` é do pacote — AGENT.md, DNA-CONFIG.yaml e ACTIVATION.yaml vêm daqui
380
+ * e devem receber correção. Mas MEMORY.md e SOUL.md são a memória que o agente
381
+ * ACUMULA conversando com o dono: sobrescrevê-los apaga meses de uso.
382
+ *
383
+ * Medido no pacote 1.1.24: 91 arquivos nessa condição. Sem esta lista, um
384
+ * `npx jarvis update` os zeraria em silêncio — o dono só descobriria quando o
385
+ * agente esquecesse tudo o que sabia sobre ele.
386
+ */
387
+ const SUFIXOS_DO_DONO = ['/MEMORY.md', '/SOUL.md'];
388
+
372
389
  /**
373
390
  * Exceções DENTRO de pasta protegida: arquivos que o sistema mantém e o dono
374
391
  * não escreve. Sem esta lista, `.claude/jarvis/` inteiro fica congelado — e o
@@ -384,6 +401,7 @@ const EXCECOES = [
384
401
  function ehIntocavel(rel) {
385
402
  const n = rel.split('\\').join('/');
386
403
  if (EXCECOES.includes(n)) return false;
404
+ if (SUFIXOS_DO_DONO.some(s => n.endsWith(s))) return true;
387
405
  return INTOCAVEIS.some(i => i.endsWith('/') ? n.startsWith(i) : n === i);
388
406
  }
389
407
 
@@ -411,17 +429,43 @@ function listar(dir, base = dir, saida = []) {
411
429
  * sem ele, "atualizar" seria apagar o trabalho de alguém.
412
430
  */
413
431
  /**
414
- * O "motor": o que faz o sistema funcionar, por oposição ao que ele guarda.
432
+ * O "motor": o que VEM DO PACOTE, por oposição ao que o dono produz.
433
+ *
415
434
  * Numa instalação sem manifesto é a única forma de distinguir a correção
416
- * chegando de uma edição do dono — e a chance de alguém ter editado um hook
417
- * à mão é muito menor que a de ficar sem o conserto.
435
+ * chegando de uma edição do dono — e a chance de alguém ter editado um
436
+ * arquivo do sistema à mão é muito menor que a de ficar sem o conserto.
437
+ *
438
+ * A fronteira já foi estreita demais: cobria só hooks, scripts e core, e
439
+ * medido no pacote 1.1.24 isso deixava **4.642 arquivos sem correção** —
440
+ * 2.118 skills, 1.897 de conhecimento curado, 266 agentes, 73 rules. Uma
441
+ * instalação antiga atualizava e recebia quase nada: um update que parece
442
+ * seguro por não fazer nada.
443
+ *
444
+ * O que torna a fronteira larga SEGURA é o backup: tudo que entra por aqui
445
+ * e difere ganha `.antigo` ao lado (ver `aplicar`). Nada se perde sem volta.
446
+ *
447
+ * O que o dono produz nunca chega aqui — `ehIntocavel` roda antes e já tirou
448
+ * workspace, buckets business/personal, o inbox dele, outputs, research,
449
+ * estado, sessões, credenciais, e MEMORY.md/SOUL.md dos agentes.
418
450
  */
419
451
  function ehDoMotor(rel) {
420
452
  const n = rel.split('\\').join('/');
421
453
  return n.startsWith('.claude/hooks/') ||
422
454
  n.startsWith('.claude/scripts/') ||
455
+ n.startsWith('.claude/skills/') || // 2.118 arquivos
456
+ n.startsWith('.claude/rules/') || // 73
457
+ n.startsWith('.claude/commands/') ||
458
+ n.startsWith('.claude/get-shit-done/') ||
423
459
  n.startsWith('core/') ||
424
460
  n.startsWith('bin/') ||
461
+ n.startsWith('reference/') ||
462
+ n.startsWith('system/') ||
463
+ // conhecimento CURADO (L2): dna, dossiers, playbooks, sources, mmos.
464
+ // O inbox do dono já saiu antes, por ehIntocavel.
465
+ n.startsWith('knowledge/external/') ||
466
+ // definição do agente vem do pacote; MEMORY.md e SOUL.md dele não
467
+ // chegam aqui — ehIntocavel os barra antes.
468
+ n.startsWith('agents/') ||
425
469
  n === 'install.py' ||
426
470
  n === 'requirements.txt' ||
427
471
  n === 'requirements-opcional.txt';
@@ -553,25 +597,51 @@ async function atualizar(args) {
553
597
  console.log(` ${cinza('O que você alterou fica como está — nada seu é sobrescrito.')}`);
554
598
  console.log();
555
599
 
556
- const token = process.env.JARVIS_TOKEN || await perguntar('Token:', true);
557
- if (!token) {
558
- console.log(`\n ${fraco('Sem token, não o que baixar. Cancelado.')}\n`);
559
- return 130;
560
- }
600
+ // Pacote em disco: pula o download. Serve a dois casos reais — o dono que
601
+ // recebeu o pacote por outro meio (rede local, pen drive), e o TESTE, que
602
+ // precisa exercitar este caminho sem rede nem token. Sem esta porta, a única
603
+ // forma de provar o update é baixando de verdade, e o que não se testa não
604
+ // se garante.
605
+ const localIdx = args.indexOf('--de');
606
+ const pacoteLocal = localIdx >= 0 ? resolve(args[localIdx + 1] || '') : null;
607
+
608
+ let tmp;
609
+ if (pacoteLocal) {
610
+ if (!existsSync(join(pacoteLocal, 'VERSION'))) {
611
+ console.log(`\n ${vermelho(neg('Isso não parece um pacote do JARVIS.'))}`);
612
+ console.log(` ${cinza(pacoteLocal + ' — não tem VERSION')}\n`);
613
+ return 1;
614
+ }
615
+ tmp = pacoteLocal;
616
+ console.log(` ${verde('✓')} ${branco('Usando o pacote local')} ${fraco(tmp)}`);
617
+ } else {
618
+ const token = process.env.JARVIS_TOKEN || await perguntar('Token:', true);
619
+ if (!token) {
620
+ console.log(`\n ${fraco('Sem token, não há o que baixar. Cancelado.')}\n`);
621
+ return 130;
622
+ }
561
623
 
562
- const tmp = join(tmpdir(), `jarvis-update-${Date.now()}`);
563
- console.log();
564
- try {
565
- await baixar(token, tmp, plataforma);
566
- } catch (e) {
567
- if (e.message === 'TOKEN_INVALIDO' || e.message === 'SEM_ACESSO') erroDeAcesso(e.message);
568
- else {
569
- console.log(`\n ${vermelho(neg('Não consegui baixar.'))}`);
570
- console.log(` ${cinza(e.message)}\n`);
624
+ tmp = join(tmpdir(), `jarvis-update-${Date.now()}`);
625
+ console.log();
626
+ try {
627
+ await baixar(token, tmp, plataforma);
628
+ } catch (e) {
629
+ if (e.message === 'TOKEN_INVALIDO' || e.message === 'SEM_ACESSO') erroDeAcesso(e.message);
630
+ else {
631
+ console.log(`\n ${vermelho(neg('Não consegui baixar.'))}`);
632
+ console.log(` ${cinza(e.message)}\n`);
633
+ }
634
+ return 1;
571
635
  }
572
- return 1;
573
636
  }
574
637
 
638
+ // NUNCA apagar um diretório que o dono nos deu. Com `--de`, `tmp` É o pacote
639
+ // dele — removê-lo seria destruir a origem no meio de uma operação cujo
640
+ // propósito inteiro é não destruir nada.
641
+ const limparTmp = () => {
642
+ if (!pacoteLocal) rmSync(tmp, { recursive: true, force: true });
643
+ };
644
+
575
645
  const versaoNova = existsSync(join(tmp, 'VERSION'))
576
646
  ? readFileSync(join(tmp, 'VERSION'), 'utf8').trim() : '?';
577
647
 
@@ -579,7 +649,7 @@ async function atualizar(args) {
579
649
  console.log();
580
650
  console.log(` ${verde('✓')} ${branco('Você já está na versão mais recente')} ${cinza('(v' + versaoNova + ')')}`);
581
651
  console.log();
582
- rmSync(tmp, { recursive: true, force: true });
652
+ limparTmp();
583
653
  return 0;
584
654
  }
585
655
 
@@ -598,7 +668,7 @@ async function atualizar(args) {
598
668
  console.log();
599
669
  console.log(` ${branco('Sua instalação NÃO foi tocada.')} ${fraco('Tente de novo.')}`);
600
670
  console.log();
601
- rmSync(tmp, { recursive: true, force: true });
671
+ limparTmp();
602
672
  return 1;
603
673
  }
604
674
 
@@ -612,21 +682,34 @@ async function atualizar(args) {
612
682
  if (!plano.novo.length && !plano.atualiza.length) {
613
683
  console.log(` ${verde('✓')} ${branco('Nada a mudar nos arquivos do sistema.')}`);
614
684
  console.log();
615
- rmSync(tmp, { recursive: true, force: true });
685
+ limparTmp();
616
686
  return 0;
617
687
  }
618
688
 
619
- const ok = (await perguntar(`Aplicar? ${fraco('(enter = sim, n = cancelar)')}`)).trim().toLowerCase();
620
- if (ok === 'n' || ok === 'nao' || ok === 'não') {
621
- console.log(`\n ${fraco('Cancelado. Nada foi alterado.')}\n`);
622
- rmSync(tmp, { recursive: true, force: true });
623
- return 130;
689
+ // `--seco`: mostra o plano e para. É o "ver antes de aplicar" que o dono
690
+ // deve usar sempre na primeira vez e o único modo em que este comando
691
+ // não escreve nada, por construção.
692
+ if (args.includes('--seco')) {
693
+ console.log(` ${fraco('(--seco: nada foi escrito)')}\n`);
694
+ limparTmp();
695
+ return 0;
696
+ }
697
+
698
+ // `--sim` pula a confirmação. Existe para automação e teste; o caminho
699
+ // normal do dono continua perguntando, porque a decisão é dele.
700
+ if (!args.includes('--sim')) {
701
+ const ok = (await perguntar(`Aplicar? ${fraco('(enter = sim, n = cancelar)')}`)).trim().toLowerCase();
702
+ if (ok === 'n' || ok === 'nao' || ok === 'não') {
703
+ console.log(`\n ${fraco('Cancelado. Nada foi alterado.')}\n`);
704
+ limparTmp();
705
+ return 130;
706
+ }
624
707
  }
625
708
 
626
709
  console.log();
627
710
  const n = aplicar(raiz, tmp, plano);
628
711
  console.log(` ${verde('✓')} ${branco('Aplicado')} ${fraco(n.toLocaleString('pt-BR') + ' arquivo(s)')}`);
629
- rmSync(tmp, { recursive: true, force: true });
712
+ limparTmp();
630
713
 
631
714
  // O settings.json que veio no pacote traz o caminho como `$CLAUDE_PROJECT_DIR`
632
715
  // — sintaxe POSIX. Precisa virar o caminho ABSOLUTO desta máquina, senão no
@@ -686,6 +769,7 @@ function ajuda() {
686
769
  console.log(` ${branco('npx @fefsbenson/jarvis update')} ${fraco('atualiza o JARVIS desta pasta')}`);
687
770
  console.log(` ${branco('npx @fefsbenson/jarvis update PASTA')} ${fraco('atualiza o de outra pasta')}`);
688
771
  console.log(` ${fraco(' o update preserva o que você alterou — nada seu é sobrescrito')}`);
772
+ console.log(` ${branco('npx @fefsbenson/jarvis update --seco')} ${fraco('mostra o que mudaria, sem escrever nada')}`);
689
773
  console.log();
690
774
  console.log(` ${fraco('Precisa de: Node 18+, Python 3.10+ e o token de acesso.')}`);
691
775
  console.log(` ${fraco('O token também pode vir da variável JARVIS_TOKEN.')}`);
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * O update do cli.js preserva o que é do dono?
4
+ *
5
+ * Existe porque a política de preservação vive em DOIS lugares — aqui e no
6
+ * `atualizar.py` da instalação. Divergir significa que o mesmo arquivo é
7
+ * sagrado num caminho e apagado no outro, e ninguém percebe até um cliente
8
+ * perder trabalho.
9
+ *
10
+ * Rodar: node bin/teste-preservacao.mjs
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+ import { join, dirname } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const cli = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'cli.js'), 'utf8');
17
+ const trecho = cli.slice(cli.indexOf('const INTOCAVEIS'), cli.indexOf('function listar('));
18
+ const { ehIntocavel } = await import(
19
+ 'data:text/javascript,' + encodeURIComponent(trecho + '\nexport { ehIntocavel };'));
20
+
21
+ // [caminho, protegido?, por quê]
22
+ const CASOS = [
23
+ ['.env', true, 'credencial'],
24
+ ['jarvis.config.yaml', true, 'config do dono'],
25
+ ['.claude/settings.local.json', true, 'overrides da máquina dele'],
26
+ ['.claude/jarvis/STATE.json', true, 'estado'],
27
+ ['.claude/sessions/s.md', true, 'histórico'],
28
+ ['.claude/mission-control/S.json', true, 'estado de missão'],
29
+ ['.claude/trash/x.md', true, 'lixeira dele'],
30
+ ['workspace/businesses/x/brand.md', true, 'o negócio dele'],
31
+ ['knowledge/business/insights/r.md', true, 'bucket dele'],
32
+ ['knowledge/personal/cognitive/d.md', true, 'bucket dele'],
33
+ ['knowledge/external/inbox/CURSO/a.txt', true, 'material que ELE ingeriu'],
34
+ ['outputs/relatorio.md', true, 'produção dele (L3)'],
35
+ ['research/analise.md', true, 'produção dele (L3)'],
36
+ ['logs/x.jsonl', true, 'registro'],
37
+ ['.data/rag/x.db', true, 'índice'],
38
+ ['artifacts/x.json', true, 'saída de pipeline'],
39
+ ['processing/x.json', true, 'artefato de pipeline'],
40
+ ['agents/external/ah/MEMORY.md', true, 'memória acumulada do agente'],
41
+ ['agents/external/ah/SOUL.md', true, 'voz calibrada do agente'],
42
+ // do sistema — DEVEM receber a correção
43
+ ['agents/external/ah/AGENT.md', false, 'definição vem do pacote'],
44
+ ['agents/external/ah/DNA-CONFIG.yaml', false, 'ponteiro vem do pacote'],
45
+ ['knowledge/external/dna/persons/ah/H.yaml',false, 'DNA curado — L2'],
46
+ ['knowledge/external/dossiers/d.md', false, 'dossiê curado — L2'],
47
+ ['knowledge/external/playbooks/p.md', false, 'playbook curado — L2'],
48
+ ['.claude/rules/goal-mindset.md', false, 'rule do sistema'],
49
+ ['.claude/hooks/x.py', false, 'hook do sistema'],
50
+ ['.claude/skills/jarvis/SKILL.md', false, 'skill do sistema'],
51
+ ['core/paths.py', false, 'motor'],
52
+ ['install.py', false, 'instalador'],
53
+ ['VERSION', false, 'metadado — atualiza'],
54
+ ['.claude/jarvis/instalacao-original.json', false, 'baseline — precisa renovar'],
55
+ ];
56
+
57
+ let falhas = 0;
58
+ for (const [rel, esperado, porque] of CASOS) {
59
+ const got = ehIntocavel(rel);
60
+ const ok = got === esperado;
61
+ if (!ok) falhas++;
62
+ console.log(` ${ok ? 'PASS' : 'FALHA'} protegido=${String(got).padEnd(5)} ${rel.padEnd(44)} ${porque}`);
63
+ }
64
+ console.log(`\n ${falhas ? `${falhas} FALHA(S)` : `${CASOS.length}/${CASOS.length} PASSARAM`}`);
65
+ process.exit(falhas ? 1 : 0);
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """BATERIA 6 — o risco que a fronteira larga criou.
3
+
4
+ Ampliar `ehDoMotor` para skills/rules/agents significa que, SEM manifesto, um
5
+ arquivo desses que o dono editou é sobrescrito. Isso só é aceitável porque o
6
+ `.antigo` existe. Esta bateria prova que ele existe SEMPRE, e que o conteúdo
7
+ do dono está lá dentro — recuperável com um `mv`.
8
+ """
9
+ import hashlib, json, shutil, subprocess, sys, tempfile
10
+ from pathlib import Path
11
+
12
+ PAC = Path.home()/"jarvis-dist"
13
+ CLI = Path.home()/"jarvis-npx/bin/cli.js"
14
+
15
+
16
+ def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
17
+
18
+
19
+ # O dono editou arquivos do SISTEMA — cada família que a fronteira agora cobre
20
+ EDITADOS = {
21
+ ".claude/rules/goal-mindset.md": "# regra do sistema COM MINHA ANOTACAO\n",
22
+ ".claude/skills/jarvis/SKILL.md": "# skill do sistema QUE EU AJUSTEI\n",
23
+ ".claude/hooks/hook_output.py": "# hook QUE EU MEXI\n",
24
+ "agents/system/decisor/AGENT.md": "# agente QUE EU CUSTOMIZEI\n",
25
+ "core/intelligence/agents/activation_generator.py": "# core QUE EU MUDEI\n",
26
+ }
27
+
28
+
29
+ def montar(com_manifesto):
30
+ B = Path(tempfile.gettempdir())/f"bkp-{'m' if com_manifesto else 's'}"
31
+ if B.exists(): shutil.rmtree(B, ignore_errors=True)
32
+ inst = B/"inst"
33
+ for a in (".claude/hooks", ".claude/rules", ".claude/skills/jarvis",
34
+ "agents/system", "core/intelligence/agents"):
35
+ shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
36
+ ignore=shutil.ignore_patterns("__pycache__"))
37
+ for f in ("VERSION", ".claude/settings.json", "install.py"):
38
+ (inst/f).parent.mkdir(parents=True, exist_ok=True)
39
+ shutil.copy2(PAC/f, inst/f)
40
+ (inst/"VERSION").write_text("1.1.23\n")
41
+ if com_manifesto:
42
+ # manifesto do estado LIMPO (antes das edições do dono)
43
+ arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
44
+ for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
45
+ mp = inst/".claude/jarvis/instalacao-original.json"
46
+ mp.parent.mkdir(parents=True, exist_ok=True)
47
+ mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
48
+ for rel, txt in EDITADOS.items():
49
+ (inst/rel).write_text(txt, encoding="utf-8")
50
+ return inst
51
+
52
+
53
+ falhas = 0
54
+ for com_man in (True, False):
55
+ rot = "COM manifesto (deve PRESERVAR)" if com_man else "SEM manifesto (sobrescreve COM backup)"
56
+ print(f"\n ── {rot} ──")
57
+ inst = montar(com_man)
58
+ subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
59
+ capture_output=True, text=True, timeout=600)
60
+ for rel, meu in EDITADOS.items():
61
+ p = inst/rel
62
+ atual = p.read_text(encoding="utf-8", errors="replace")
63
+ marca = meu.strip()
64
+ no_lugar = marca in atual
65
+ bkp = inst/(rel + ".antigo")
66
+ no_backup = bkp.is_file() and marca in bkp.read_text(encoding="utf-8", errors="replace")
67
+ novo = inst/(rel + ".novo")
68
+
69
+ if com_man:
70
+ # com manifesto: o arquivo do dono FICA, e a versão nova vai .novo
71
+ ok = no_lugar and novo.is_file()
72
+ detalhe = f"no_lugar={no_lugar} tem_.novo={novo.is_file()}"
73
+ else:
74
+ # sem manifesto: é sobrescrito, MAS o dele tem que estar no .antigo
75
+ ok = no_backup
76
+ detalhe = f"no_lugar={no_lugar} no_backup={no_backup}"
77
+ falhas += not ok
78
+ print(f" {'PASS' if ok else 'FALHA'} {rel}" + ("" if ok else f" -> {detalhe}"))
79
+
80
+ # recuperável com um comando? (o teste que prova que o backup SERVE)
81
+ if not com_man:
82
+ rel = ".claude/rules/goal-mindset.md"
83
+ shutil.move(inst/(rel + ".antigo"), inst/rel)
84
+ ok = EDITADOS[rel].strip() in (inst/rel).read_text(encoding="utf-8")
85
+ falhas += not ok
86
+ print(f" {'PASS' if ok else 'FALHA'} recuperável com um `mv arquivo.antigo arquivo`")
87
+
88
+ print(f"\n BATERIA 6: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
89
+ sys.exit(1 if falhas else 0)
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """BATERIA 4 — o cliente que usou o JARVIS por meses, e atualiza.
3
+
4
+ Não são casos isolados: é UMA instalação com 60+ artefatos espalhados por todas
5
+ as áreas onde um dono real trabalha. Depois do update, TUDO tem que estar lá.
6
+ E roda a cadeia inteira de versões (1.1.20 -> 1.1.23 -> 1.1.24) para provar que
7
+ o update repetido não acumula estrago.
8
+ """
9
+ import hashlib, json, shutil, subprocess, sys, tempfile
10
+ from pathlib import Path
11
+
12
+ PAC = Path.home()/"jarvis-dist"
13
+ CLI = Path.home()/"jarvis-npx/bin/cli.js"
14
+
15
+
16
+ def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
17
+
18
+
19
+ # 60+ artefatos: tudo que um dono acumula em meses de uso
20
+ TRABALHO = {}
21
+ for i in range(8):
22
+ TRABALHO[f"workspace/businesses/acme/copy/pagina-{i}.md"] = f"copy de venda {i}"
23
+ TRABALHO[f"workspace/ops/sprints/sprint-{i}.md"] = f"sprint {i}"
24
+ for i in range(6):
25
+ TRABALHO[f"knowledge/business/insights/reuniao-{i}.md"] = f"ata {i} CONFIDENCIAL"
26
+ TRABALHO[f"knowledge/personal/cognitive/diario-{i}.md"] = f"reflexao {i}"
27
+ TRABALHO[f"outputs/entrega-cliente-{i}.md"] = f"entrega paga {i}"
28
+ for i in range(5):
29
+ TRABALHO[f"knowledge/external/inbox/CURSO-{i}/aula.txt"] = f"material comprado {i}"
30
+ TRABALHO[f"research/analise-{i}.md"] = f"pesquisa {i}"
31
+ TRABALHO[f"logs/batches/b-{i}.jsonl"] = '{"batch":%d}' % i
32
+ for ag in ("decisor", "conclave/sintetizador"):
33
+ TRABALHO[f"agents/system/{ag}/MEMORY.md"] = f"MEMORIA ACUMULADA de {ag}"
34
+ TRABALHO[f"agents/system/{ag}/SOUL.md"] = f"voz calibrada de {ag}"
35
+ TRABALHO.update({
36
+ ".env": "OPENAI_API_KEY=sk-real\nVOYAGE_API_KEY=vk-real",
37
+ "jarvis.config.yaml": "voz: onyx\ntratamento: chefe",
38
+ ".claude/settings.local.json": '{"hooks":{"Stop":[]}}',
39
+ ".claude/jarvis/JARVIS-MEMORY.md": "memoria relacional de 6 meses",
40
+ ".claude/jarvis/PENDING.md": "- pendencia do dono",
41
+ ".claude/sessions/SESSION-antiga.md": "sessao de marco",
42
+ ".claude/mission-control/MISSION-STATE.json": '{"fase":4}',
43
+ ".claude/trash/apagado.md": "mandei pra lixeira",
44
+ ".claude/rules/minha-regra-propria.md": "regra que EU escrevi",
45
+ ".claude/hooks/meu_hook_proprio.py": "# hook que EU escrevi",
46
+ ".claude/skills/minha-skill/SKILL.md": "skill que EU criei",
47
+ "artifacts/audit/relatorio.json": '{"ok":true}',
48
+ "processing/speakers.json": '{"s":1}',
49
+ ".data/rag_expert/vectors.json": '{"dim":1024}',
50
+ })
51
+
52
+ def montar(versao, com_manifesto=True):
53
+ B = Path(tempfile.gettempdir())/"cliente-real"
54
+ if B.exists(): shutil.rmtree(B, ignore_errors=True)
55
+ inst = B/"inst"
56
+ for a in (".claude/hooks", ".claude/rules", ".claude/skills/jarvis",
57
+ "agents/system", "core/intelligence/agents",
58
+ "knowledge/external/dna"):
59
+ if (PAC/a).is_dir():
60
+ shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
61
+ ignore=shutil.ignore_patterns("__pycache__"))
62
+ for f in ("VERSION", ".claude/settings.json", "install.py"):
63
+ (inst/f).parent.mkdir(parents=True, exist_ok=True)
64
+ shutil.copy2(PAC/f, inst/f)
65
+ (inst/"VERSION").write_text(versao + "\n")
66
+ if com_manifesto:
67
+ arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
68
+ for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
69
+ mp = inst/".claude/jarvis/instalacao-original.json"
70
+ mp.parent.mkdir(parents=True, exist_ok=True)
71
+ mp.write_text(json.dumps({"versao": versao, "arquivos": arq}, indent=2))
72
+ for rel, txt in TRABALHO.items():
73
+ p = inst/rel; p.parent.mkdir(parents=True, exist_ok=True)
74
+ p.write_text(txt, encoding="utf-8")
75
+ return inst
76
+
77
+
78
+ def conferir(inst, rot):
79
+ perdidos = []
80
+ for rel, esperado in TRABALHO.items():
81
+ p = inst/rel
82
+ if not p.is_file():
83
+ perdidos.append(f"{rel}(SUMIU)"); continue
84
+ atual = p.read_text(encoding="utf-8", errors="replace")
85
+ if esperado in atual:
86
+ continue
87
+ if rel.endswith(".json"):
88
+ try:
89
+ if all(json.loads(atual).get(k) == v
90
+ for k, v in json.loads(esperado).items()):
91
+ continue
92
+ except Exception: pass
93
+ if (inst/(rel+".antigo")).is_file() and \
94
+ esperado in (inst/(rel+".antigo")).read_text(encoding="utf-8", errors="replace"):
95
+ continue
96
+ perdidos.append(f"{rel}(PERDIDO)")
97
+ ok = not perdidos
98
+ print(f" {'PASS' if ok else 'FALHA'} [{rot}] {len(TRABALHO)} artefatos do dono"
99
+ + ("" if ok else f" -> {len(perdidos)} perdidos: {perdidos[:6]}"))
100
+ return ok
101
+
102
+
103
+ falhas = 0
104
+ print(f" simulando um cliente com {len(TRABALHO)} artefatos acumulados\n")
105
+
106
+ # A — com manifesto, uma atualização
107
+ inst = montar("1.1.23")
108
+ subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
109
+ capture_output=True, text=True, timeout=600)
110
+ falhas += not conferir(inst, "1.1.23 -> 1.1.24, com manifesto")
111
+
112
+ # B — sem manifesto (instalação antiga, o caso do Fábio)
113
+ inst = montar("1.1.20", com_manifesto=False)
114
+ subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
115
+ capture_output=True, text=True, timeout=600)
116
+ falhas += not conferir(inst, "1.1.20 -> 1.1.24, SEM manifesto")
117
+
118
+ # C — três updates seguidos: o estrago não pode acumular
119
+ inst = montar("1.1.23")
120
+ for n in range(3):
121
+ subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
122
+ capture_output=True, text=True, timeout=600)
123
+ falhas += not conferir(inst, "três updates seguidos")
124
+
125
+ # D — o caminho Python, sobre a mesma instalação
126
+ inst = montar("1.1.23")
127
+ subprocess.run([sys.executable, str(PAC/".claude/scripts/atualizar.py"),
128
+ "--novo", str(PAC), "--destino", str(inst), "--sim"],
129
+ capture_output=True, text=True, timeout=600)
130
+ falhas += not conferir(inst, "caminho Python (atualizar.py)")
131
+
132
+ print(f"\n BATERIA 4: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
133
+ sys.exit(1 if falhas else 0)
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env python3
2
+ """BATERIA 5 — o update ENTREGA a correção?
3
+
4
+ Preservar tudo é trivial se você não atualiza nada. Esta bateria prova o outro
5
+ lado: o que é do sistema e o dono não tocou TEM que receber a versão nova.
6
+ Um update que só preserva é um update quebrado que parece seguro.
7
+ """
8
+ import hashlib, json, shutil, subprocess, sys, tempfile
9
+ from pathlib import Path
10
+
11
+ PAC = Path.home()/"jarvis-dist"
12
+ CLI = Path.home()/"jarvis-npx/bin/cli.js"
13
+
14
+
15
+ def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
16
+
17
+
18
+ def montar(nome, envelhecer: dict, com_manifesto=True):
19
+ """Instala e depois 'envelhece' arquivos do sistema (simula versão antiga)."""
20
+ B = Path(tempfile.gettempdir())/f"entrega-{nome}"
21
+ if B.exists(): shutil.rmtree(B, ignore_errors=True)
22
+ inst = B/"inst"
23
+ for a in (".claude/hooks", ".claude/rules", ".claude/scripts",
24
+ ".claude/skills/jarvis", "agents/system", "core/intelligence/agents",
25
+ "knowledge/external/dna"):
26
+ if (PAC/a).is_dir():
27
+ shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
28
+ ignore=shutil.ignore_patterns("__pycache__"))
29
+ for f in ("VERSION", ".claude/settings.json", "install.py"):
30
+ (inst/f).parent.mkdir(parents=True, exist_ok=True)
31
+ shutil.copy2(PAC/f, inst/f)
32
+ (inst/"VERSION").write_text("1.1.23\n")
33
+ # ENVELHECE: escreve a "versão antiga" nesses arquivos do sistema
34
+ for rel, txt in envelhecer.items():
35
+ p = inst/rel; p.parent.mkdir(parents=True, exist_ok=True)
36
+ p.write_text(txt, encoding="utf-8")
37
+ # manifesto reflete o estado ENVELHECIDO (o dono não tocou nesses arquivos)
38
+ if com_manifesto:
39
+ arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
40
+ for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
41
+ mp = inst/".claude/jarvis/instalacao-original.json"
42
+ mp.parent.mkdir(parents=True, exist_ok=True)
43
+ mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
44
+ return inst
45
+
46
+
47
+ # Arquivos do SISTEMA "na versão antiga" — o dono nunca os tocou.
48
+ # Cada um representa uma família: hook, rule, script, skill, agente, DNA.
49
+ ANTIGOS = {
50
+ ".claude/hooks/hook_output.py": "# VERSAO ANTIGA COM BUG\n",
51
+ ".claude/rules/goal-mindset.md": "# regra desatualizada\n",
52
+ ".claude/scripts/jarvis_doctor.py": "# doctor antigo\n",
53
+ ".claude/skills/jarvis/SKILL.md": "# skill antiga\n",
54
+ "agents/system/decisor/AGENT.md": "# agente desatualizado\n",
55
+ "core/intelligence/agents/activation_generator.py": "# gerador antigo\n",
56
+ }
57
+
58
+ falhas = 0
59
+ for com_man in (True, False):
60
+ rot = "com manifesto" if com_man else "SEM manifesto"
61
+ print(f"\n ── {rot} ──")
62
+ inst = montar("m" if com_man else "s", ANTIGOS, com_man)
63
+ r = subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
64
+ capture_output=True, text=True, timeout=600)
65
+ for rel, antigo in ANTIGOS.items():
66
+ p, orig = inst/rel, PAC/rel
67
+ if not orig.is_file():
68
+ print(f" --- {rel} não existe no pacote (pulando)"); continue
69
+ atual = p.read_text(encoding="utf-8", errors="replace") if p.is_file() else ""
70
+ recebeu = h(p) == h(orig) if p.is_file() else False
71
+ ainda_antigo = antigo.strip() in atual
72
+ ok = recebeu and not ainda_antigo
73
+ falhas += not ok
74
+ print(f" {'PASS' if ok else 'FALHA'} {rel}"
75
+ + ("" if ok else f" -> recebeu_versao_nova={recebeu} ainda_antigo={ainda_antigo}"))
76
+ # e a versão tem que ter subido
77
+ v = (inst/"VERSION").read_text().strip()
78
+ ok = v == "1.1.24"; falhas += not ok
79
+ print(f" {'PASS' if ok else 'FALHA'} VERSION -> {v}")
80
+ # e os arquivos NOVOS da 1.1.24 têm que ter chegado
81
+ for rel in (".claude/skills/jarvis-update/SKILL.md",
82
+ ".claude/rules/design-por-publico.md",
83
+ ".claude/scripts/atualizar.py"):
84
+ ok = (inst/rel).is_file(); falhas += not ok
85
+ print(f" {'PASS' if ok else 'FALHA'} novo da 1.1.24: {rel}")
86
+
87
+ print(f"\n BATERIA 5: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
88
+ sys.exit(1 if falhas else 0)
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ """BATERIA 3 — casos hostis: o que quebra um update no mundo real.
3
+
4
+ Cada caso é uma situação que existe na máquina de gente de verdade e que um
5
+ update ingênuo trata mal: rodar duas vezes, arquivo somente-leitura, nome com
6
+ acento e espaço, link simbólico, arquivo enorme, pasta que não é JARVIS,
7
+ pacote inválido, e interrupção no meio.
8
+ """
9
+ import hashlib, json, os, shutil, stat, subprocess, sys, tempfile
10
+ from pathlib import Path
11
+
12
+ PAC = Path.home()/"jarvis-dist"
13
+ CLI = Path.home()/"jarvis-npx/bin/cli.js"
14
+ UPD = PAC/".claude/scripts/atualizar.py"
15
+
16
+
17
+ def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
18
+
19
+
20
+ def base(nome, com_manifesto=True):
21
+ B = Path(tempfile.gettempdir())/f"host-{nome}"
22
+ if B.exists():
23
+ shutil.rmtree(B, ignore_errors=True)
24
+ inst = B/"inst"
25
+ for a in (".claude/hooks", ".claude/rules"):
26
+ shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
27
+ ignore=shutil.ignore_patterns("__pycache__"))
28
+ (inst/"VERSION").write_text("1.1.23\n")
29
+ if com_manifesto:
30
+ arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
31
+ for f in inst.rglob("*") if f.is_file()}
32
+ mp = inst/".claude/jarvis/instalacao-original.json"
33
+ mp.parent.mkdir(parents=True, exist_ok=True)
34
+ mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
35
+ return inst
36
+
37
+
38
+ def upd(inst, *extra, pacote=None):
39
+ return subprocess.run(
40
+ ["node", str(CLI), "update", str(inst), "--de", str(pacote or PAC), "--sim", *extra],
41
+ capture_output=True, text=True, timeout=600)
42
+
43
+
44
+ resultados = []
45
+ def caso(nome, ok, detalhe=""):
46
+ resultados.append((nome, ok, detalhe))
47
+ print(f" {'PASS' if ok else 'FALHA'} {nome}" + (f" -> {detalhe}" if not ok else ""))
48
+
49
+
50
+ # 1 — IDEMPOTÊNCIA: rodar duas vezes seguidas não pode mudar nada na segunda
51
+ inst = base("idem")
52
+ (inst/"workspace").mkdir(exist_ok=True); (inst/"workspace/x.md").write_text("do dono")
53
+ upd(inst)
54
+ estado1 = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*")
55
+ if f.is_file() and ".claude/jarvis" not in str(f) and "sessions" not in str(f)}
56
+ r2 = upd(inst)
57
+ estado2 = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*")
58
+ if f.is_file() and ".claude/jarvis" not in str(f) and "sessions" not in str(f)}
59
+ dif = [k for k in estado2 if estado1.get(k) != estado2[k]]
60
+ caso("idempotente (2a rodada não muda nada)", not dif, f"{len(dif)} mudaram: {dif[:3]}")
61
+
62
+ # 2 — a 2a rodada deve dizer "já está na versão mais recente"
63
+ caso("2a rodada reconhece que já está atualizado",
64
+ "mais recente" in r2.stdout or "Nada a mudar" in r2.stdout,
65
+ r2.stdout[-200:])
66
+
67
+ # 3 — NOME COM ACENTO E ESPAÇO (Windows/pt-BR é cheio disso)
68
+ inst = base("acento")
69
+ d = inst/"workspace/Relatórios do Cliente"
70
+ d.mkdir(parents=True); (d/"análise final.md").write_text("conteúdo com acento")
71
+ r = upd(inst)
72
+ caso("nome com acento e espaço preservado",
73
+ r.returncode == 0 and (d/"análise final.md").read_text() == "conteúdo com acento",
74
+ f"rc={r.returncode}")
75
+
76
+ # 4 — ARQUIVO SOMENTE-LEITURA do dono
77
+ inst = base("readonly")
78
+ f = inst/"workspace/protegido.md"
79
+ f.parent.mkdir(parents=True, exist_ok=True); f.write_text("read-only do dono")
80
+ os.chmod(f, stat.S_IRUSR)
81
+ r = upd(inst)
82
+ ok = r.returncode == 0 and f.read_text() == "read-only do dono"
83
+ os.chmod(f, stat.S_IRUSR | stat.S_IWUSR)
84
+ caso("arquivo somente-leitura do dono intacto", ok, f"rc={r.returncode}")
85
+
86
+ # 5 — LINK SIMBÓLICO (quem move o workspace para outro disco faz isso)
87
+ inst = base("symlink")
88
+ alvo = inst.parent/"fora"; alvo.mkdir()
89
+ (alvo/"real.md").write_text("mora fora do JARVIS")
90
+ (inst/"workspace").mkdir(exist_ok=True)
91
+ os.symlink(alvo/"real.md", inst/"workspace/link.md")
92
+ r = upd(inst)
93
+ caso("link simbólico não é seguido nem quebrado",
94
+ r.returncode == 0 and (alvo/"real.md").read_text() == "mora fora do JARVIS",
95
+ f"rc={r.returncode}")
96
+
97
+ # 6 — PASTA QUE NÃO É JARVIS: tem que recusar, não estragar
98
+ vazio = Path(tempfile.gettempdir())/"host-naojarvis"
99
+ if vazio.exists(): shutil.rmtree(vazio)
100
+ vazio.mkdir(); (vazio/"documento-importante.docx").write_text("nada a ver")
101
+ r = upd(vazio)
102
+ caso("recusa pasta que não é JARVIS",
103
+ r.returncode != 0 and (vazio/"documento-importante.docx").is_file(),
104
+ f"rc={r.returncode}")
105
+
106
+ # 7 — PACOTE INVÁLIDO: recusa sem tocar na instalação
107
+ inst = base("pacinval")
108
+ (inst/"workspace").mkdir(exist_ok=True); (inst/"workspace/x.md").write_text("do dono")
109
+ lixo = Path(tempfile.gettempdir())/"host-pacote-lixo"
110
+ if lixo.exists(): shutil.rmtree(lixo)
111
+ lixo.mkdir(); (lixo/"README").write_text("não sou um pacote")
112
+ antes = h(inst/"workspace/x.md")
113
+ r = upd(inst, pacote=lixo)
114
+ caso("recusa pacote inválido sem tocar na instalação",
115
+ r.returncode != 0 and h(inst/"workspace/x.md") == antes,
116
+ f"rc={r.returncode}")
117
+
118
+ # 8 — --seco NÃO ESCREVE NADA (a promessa mais importante)
119
+ inst = base("seco")
120
+ antes = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*") if f.is_file()}
121
+ r = upd(inst, "--seco")
122
+ depois = {str(f.relative_to(inst)): h(f) for f in inst.rglob("*") if f.is_file()}
123
+ caso("--seco não escreve absolutamente nada",
124
+ r.returncode == 0 and antes == depois,
125
+ f"rc={r.returncode} · {len(set(depois)-set(antes))} novos")
126
+
127
+ # 9 — o PACOTE DE ORIGEM nunca é modificado nem apagado
128
+ inst = base("origem")
129
+ pac_v, pac_n = (PAC/"VERSION").read_text(), sum(1 for _ in (PAC/".claude/scripts").iterdir())
130
+ upd(inst)
131
+ caso("pacote de origem intacto após o update",
132
+ (PAC/"VERSION").read_text() == pac_v
133
+ and sum(1 for _ in (PAC/".claude/scripts").iterdir()) == pac_n)
134
+
135
+ # 10 — arquivo GRANDE do dono não é corrompido
136
+ inst = base("grande")
137
+ g = inst/"workspace/base.csv"
138
+ g.parent.mkdir(parents=True, exist_ok=True)
139
+ g.write_text("col\n" + "\n".join(str(i) for i in range(200_000)))
140
+ antes = h(g)
141
+ r = upd(inst)
142
+ caso("arquivo grande do dono intacto", r.returncode == 0 and h(g) == antes)
143
+
144
+ falhas = sum(1 for _, ok, _ in resultados if not ok)
145
+ print(f"\n BATERIA 3: {len(resultados)-falhas}/{len(resultados)}"
146
+ + (" TODOS PASSARAM" if not falhas else f" — {falhas} FALHA(S)"))
147
+ sys.exit(1 if falhas else 0)
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """BATERIA 2 — o comando REAL (`npx jarvis update`), adversarial.
3
+
4
+ Roda `node cli.js update <inst> --de <pacote>` e confere o DISCO depois.
5
+ É o caminho que o cliente usa; testar a função extraída seria aproximação.
6
+ """
7
+ import hashlib, json, shutil, subprocess, sys, tempfile
8
+ from pathlib import Path
9
+
10
+ PAC = Path.home()/"jarvis-dist"
11
+ CLI = Path.home()/"jarvis-npx/bin/cli.js"
12
+
13
+
14
+ def h(p): return hashlib.sha256(p.read_bytes()).hexdigest()
15
+
16
+
17
+ def montar(nome, trabalho, com_manifesto=True):
18
+ B = Path(tempfile.gettempdir())/f"real-{nome}"
19
+ if B.exists(): shutil.rmtree(B)
20
+ inst = B/"inst"
21
+ for a in (".claude/hooks", ".claude/rules", ".claude/skills/jarvis",
22
+ "agents/system", "core/intelligence/agents"):
23
+ if (PAC/a).is_dir():
24
+ shutil.copytree(PAC/a, inst/a, dirs_exist_ok=True,
25
+ ignore=shutil.ignore_patterns("__pycache__"))
26
+ for f in ("VERSION", ".claude/settings.json", "install.py"):
27
+ (inst/f).parent.mkdir(parents=True, exist_ok=True)
28
+ shutil.copy2(PAC/f, inst/f)
29
+ (inst/"VERSION").write_text("1.1.23\n")
30
+ if com_manifesto:
31
+ arq = {str(f.relative_to(inst)).replace("\\","/"): h(f)
32
+ for f in inst.rglob("*") if f.is_file() and "__pycache__" not in str(f)}
33
+ mp = inst/".claude/jarvis/instalacao-original.json"
34
+ mp.parent.mkdir(parents=True, exist_ok=True)
35
+ mp.write_text(json.dumps({"versao":"1.1.23","arquivos":arq}, indent=2))
36
+ for rel, txt in trabalho.items():
37
+ p = inst/rel; p.parent.mkdir(parents=True, exist_ok=True)
38
+ p.write_text(txt, encoding="utf-8")
39
+ return inst
40
+
41
+
42
+ CASOS = [
43
+ ("workspace", {"workspace/businesses/acme/brand.md": "marca do cliente",
44
+ "workspace/gestao/cockpit.yaml": "receita: 1000000"}),
45
+ ("memoria", {"agents/system/decisor/MEMORY.md": "SEIS MESES DE USO",
46
+ "agents/system/decisor/SOUL.md": "voz calibrada"}),
47
+ ("credenciais", {".env": "OPENAI_API_KEY=sk-real", "jarvis.config.yaml": "voz: onyx"}),
48
+ ("conhecimento",{"knowledge/business/i.md": "ata confidencial",
49
+ "knowledge/personal/d.md": "diario",
50
+ "knowledge/external/inbox/CURSO/a.txt": "material comprado"}),
51
+ ("producao", {"outputs/entrega.md": "trabalho pago",
52
+ "research/analise.md": "pesquisa", "logs/s.jsonl": "{}"}),
53
+ ("estado", {".claude/jarvis/STATE.json": '{"fase":9}',
54
+ ".claude/sessions/L.md": "sessao",
55
+ ".claude/settings.local.json": '{"hooks":{}}'}),
56
+ ("hook-editado",{".claude/hooks/hook_output.py": "# EU EDITEI ESTE\n"}),
57
+ ]
58
+
59
+ falhas = 0
60
+ for com_man in (True, False):
61
+ rot = "COM manifesto" if com_man else "SEM manifesto (instalação antiga)"
62
+ print(f"\n ── {rot} ──")
63
+ for nome, trabalho in CASOS:
64
+ inst = montar(nome, trabalho, com_man)
65
+ antes = {r: h(inst/r) for r in trabalho}
66
+ pac_antes = (PAC/"VERSION").read_text(), len(list((PAC/".claude/scripts").iterdir()))
67
+ r = subprocess.run(["node", str(CLI), "update", str(inst), "--de", str(PAC), "--sim"],
68
+ capture_output=True, text=True, timeout=600)
69
+ perdidos = []
70
+ for rel, conteudo_dono in trabalho.items():
71
+ p = inst/rel
72
+ if not p.is_file():
73
+ perdidos.append(f"{rel}(SUMIU)")
74
+ continue
75
+ if h(p) == antes[rel]:
76
+ continue # intacto byte-a-byte
77
+ # Mudou. Duas razões legítimas, e nenhuma perde o dado do dono:
78
+ # 1. o doctor roda ao final e atualiza arquivos de ESTADO
79
+ # (STATE.json ganha session/mis) — o dado do dono continua lá;
80
+ # 2. sem manifesto, o motor atualiza COM backup .antigo — e a
81
+ # versão do dono está recuperável ao lado.
82
+ # Perda de verdade = o conteúdo dele sumiu E não há backup.
83
+ atual = p.read_text(encoding="utf-8", errors="replace")
84
+ marca = conteudo_dono.strip().splitlines()[0] if conteudo_dono.strip() else ""
85
+ if marca and marca in atual:
86
+ continue # o dado do dono sobreviveu
87
+ # JSON: o doctor reformata e ACRESCENTA chaves de estado. Comparar
88
+ # texto acusaria perda onde não houve — o que importa é se cada
89
+ # chave do dono continua lá, com o mesmo valor.
90
+ if rel.endswith(".json"):
91
+ try:
92
+ meu, agora = json.loads(conteudo_dono), json.loads(atual)
93
+ if all(agora.get(k) == v for k, v in meu.items()):
94
+ continue
95
+ except (json.JSONDecodeError, AttributeError):
96
+ pass
97
+ if (inst/(rel + ".antigo")).is_file():
98
+ bkp = (inst/(rel + ".antigo")).read_text(encoding="utf-8", errors="replace")
99
+ if marca and marca in bkp:
100
+ continue # preservado no backup
101
+ perdidos.append(f"{rel}(PERDIDO — sem backup)")
102
+ # o pacote de origem NÃO pode ter sido tocado
103
+ pac_depois = (PAC/"VERSION").read_text(), len(list((PAC/".claude/scripts").iterdir()))
104
+ if pac_antes != pac_depois:
105
+ perdidos.append(f"PACOTE DE ORIGEM MUDOU ({pac_antes}->{pac_depois})")
106
+ ok = not perdidos and r.returncode == 0
107
+ falhas += not ok
108
+ print(f" {'PASS' if ok else 'FALHA'} [{nome}] rc={r.returncode}"
109
+ + ("" if ok else f" -> {perdidos} {r.stderr[:150]}"))
110
+
111
+ print(f"\n BATERIA 2: {'TODOS PASSARAM' if not falhas else f'{falhas} FALHA(S)'}")
112
+ sys.exit(1 if falhas else 0)
package/bin/testes.sh ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Toda a bateria do update, de uma vez.
3
+ #
4
+ # Um update que apaga trabalho de cliente é o pior defeito que este pacote pode
5
+ # ter — pior que não atualizar, porque não tem volta e ninguém percebe na hora.
6
+ # Por isso a bateria é grande e roda contra o comando REAL, não contra funções
7
+ # extraídas: aproximação do comando mede outra coisa.
8
+ #
9
+ # Precisa de um pacote em ~/jarvis-dist para comparar.
10
+ set -u
11
+ cd "$(dirname "$0")"
12
+ falhas=0
13
+ echo
14
+ for t in teste-preservacao.mjs teste-update-preserva.py teste-update-hostil.py \
15
+ teste-update-entrega.py teste-update-backup.py teste-update-cliente.py; do
16
+ echo "── $t ──"
17
+ case "$t" in
18
+ *.mjs) node "$t" | tail -2 ;;
19
+ *.py) python3 "$t" | tail -2 ;;
20
+ esac
21
+ [ "${PIPESTATUS[0]}" -eq 0 ] || { falhas=$((falhas+1)); echo " ^^ FALHOU"; }
22
+ echo
23
+ done
24
+ if [ "$falhas" -eq 0 ]; then
25
+ echo " TODAS AS BATERIAS PASSARAM"
26
+ else
27
+ echo " $falhas BATERIA(S) COM FALHA — não publique"
28
+ fi
29
+ exit "$falhas"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fefsbenson/jarvis",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "JARVIS — sistema operacional de conhecimento e agentes para Claude Code (Windows e Linux)",
5
5
  "bin": {
6
6
  "jarvis": "bin/cli.js"
@@ -26,5 +26,8 @@
26
26
  "agents",
27
27
  "knowledge-management",
28
28
  "pragma"
29
- ]
29
+ ],
30
+ "scripts": {
31
+ "test": "bash bin/testes.sh"
32
+ }
30
33
  }