@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 +111 -27
- package/bin/teste-preservacao.mjs +65 -0
- package/bin/teste-update-backup.py +89 -0
- package/bin/teste-update-cliente.py +133 -0
- package/bin/teste-update-entrega.py +88 -0
- package/bin/teste-update-hostil.py +147 -0
- package/bin/teste-update-preserva.py +112 -0
- package/bin/testes.sh +29 -0
- package/package.json +5 -2
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
|
|
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
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
600
|
+
// Pacote já 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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
+
limparTmp();
|
|
616
686
|
return 0;
|
|
617
687
|
}
|
|
618
688
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|