@daniellins/power-claude 0.16.3 → 0.16.4
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.
Potentially problematic release.
This version of @daniellins/power-claude might be problematic. Click here for more details.
- package/README.md +93 -29
- package/installer/motor-scripts.json +1 -1
- package/package.json +10 -2
- package/scripts/__tests__/enforce-git-push-authority.test.cjs +289 -2
- package/scripts/build-public-tree.cjs +372 -0
- package/scripts/derive-ready-transitions.cjs +40 -4
- package/scripts/validate-branding.js +19 -3
- package/scripts/validate-git-vetor.cjs +99 -0
- package/template/.claude/agents/pc-devops.md +2 -1
- package/template/.claude/agents/pc-tutor.md +78 -0
- package/template/.claude/commands/power/tutor.md +11 -0
- package/template/.claude/hooks/enforce-git-push-authority.cjs +385 -5
- package/template/.claude/hooks/irreversible-remotes.json +1 -0
- package/template/.claude/rules/orchestration.md +2 -1
package/README.md
CHANGED
|
@@ -2,47 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
Framework enxuto de agentes para Claude Code — **pods por job-to-be-done**, com budget de tokens como gate de qualidade.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Você o instala dentro de um projeto seu, e ele acrescenta ali agentes, regras, skills e verificações
|
|
6
|
+
automáticas que o Claude Code passa a usar. Não substitui o Claude Code: dá forma ao que ele faz, e
|
|
7
|
+
verifica o resultado em disco antes de você confiar nele.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
2. **Budget de tokens como gate** — todo agente declara `budget_tokens` no frontmatter e um validador determinístico (`scripts/validate-budgets.js`) bloqueia qualquer agente que estoure o próprio budget, o teto global (1.500 tokens) ou 150 linhas. Contexto enxuto não é convenção: é gate.
|
|
9
|
-
3. **Enforcement determinístico** — regras críticas viram hooks e scripts, não pedidos educados no prompt. Exemplo: o hook `enforce-git-push-authority.cjs` bloqueia `git push` fora do fluxo autorizado, direto no `PreToolUse`.
|
|
10
|
-
4. **Verificação adversarial embutida** — os pods incluem agentes críticos (red-team) que revisam o próprio output antes de você confiar nele.
|
|
11
|
-
5. **Garantia por código, não por prosa** — um pod é dado declarativo (`workflows/wf-*.yaml` + templates de etapa + `squad-io.yaml`), executado por um runner determinístico (`scripts/pc-run-pod.cjs`, skill `pc-run-pod`) que verifica o artefato EM DISCO — existe, casa com o template, todas as `veto.assertions` passam — antes de liberar a etapa seguinte. A garantia de processo é código, não prosa no prompt do agente.
|
|
9
|
+
## Para quem chega agora
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Se esta é a sua primeira vez, o caminho mais curto é este, nesta ordem:
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
1. **[Comece aqui](docs/mecanismos/aprendizado/getting-started.md)** — instalar, conferir a instalação
|
|
14
|
+
com o `doctor` e rodar o primeiro comando. Poucos minutos, sem pré-requisito de leitura.
|
|
15
|
+
2. **[Guia de conceitos](docs/mecanismos/aprendizado/guia-de-conceitos.md)** — os termos que o framework
|
|
16
|
+
usa o tempo todo (pod, workflow, etapa, veto, checkpoint, budget, skill), um parágrafo cada.
|
|
17
|
+
3. **Um tutorial de ponta a ponta**, conforme o que você quer fazer primeiro:
|
|
18
|
+
- [rodar um pod do começo ao fim](docs/mecanismos/aprendizado/tutorial-rodar-um-pod.md);
|
|
19
|
+
- [levar uma story do pedido ao merge](docs/mecanismos/aprendizado/tutorial-ciclo-de-story.md);
|
|
20
|
+
- [criar um pod novo com o forge](docs/mecanismos/aprendizado/tutorial-criar-pod-com-forge.md).
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
Prefere perguntar a ler? Depois de instalar, digite `/power:tutor` no Claude Code — ele aponta o
|
|
23
|
+
próximo passo certo para o seu caso, sem você ter que escolher sozinho por qual destes documentos
|
|
24
|
+
começar.
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
Se você prefere instalar antes de ler, vá direto para a seção **Instalação**, logo abaixo — o
|
|
27
|
+
getting-started continua valendo depois.
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
## Princípios
|
|
30
|
+
|
|
31
|
+
1. **Pods por job-to-be-done** — em vez de dezenas de agentes genéricos, cada pod resolve um trabalho concreto (criar conteúdo com autoridade, montar aulas, automatizar processos, desenvolver com IA, forjar novos pods). Você instala só o que usa.
|
|
32
|
+
2. **Budget de tokens como gate** — todo agente declara `budget_tokens` no frontmatter e um validador determinístico ([`scripts/validate-budgets.js`](scripts/validate-budgets.js)) bloqueia qualquer agente que estoure o próprio budget, o teto global (1.500 tokens) ou 150 linhas. Contexto enxuto não é convenção: é gate.
|
|
33
|
+
3. **Enforcement determinístico** — regras críticas viram hooks e scripts, não pedidos educados no prompt. Exemplo: o hook [`enforce-git-push-authority.cjs`](template/.claude/hooks/enforce-git-push-authority.cjs) bloqueia `git push` fora do fluxo autorizado, direto no `PreToolUse`.
|
|
34
|
+
4. **Verificação adversarial embutida** — os pods incluem agentes críticos (red-team) que revisam o próprio output antes de você confiar nele.
|
|
35
|
+
5. **Garantia por código, não por prosa** — um pod é dado declarativo (`workflows/wf-*.yaml` + templates de etapa + `squad-io.yaml`), executado por um runner determinístico ([`scripts/pc-run-pod.cjs`](scripts/pc-run-pod.cjs), skill `pc-run-pod`) que verifica o artefato EM DISCO — existe, casa com o template, todas as `veto.assertions` passam — antes de liberar a etapa seguinte. A garantia de processo é código, não prosa no prompt do agente.
|
|
22
36
|
|
|
23
37
|
## Instalação
|
|
24
38
|
|
|
25
|
-
|
|
39
|
+
O framework é distribuído pelo npm. Dentro do projeto onde você quer usá-lo:
|
|
26
40
|
|
|
27
41
|
```bash
|
|
28
42
|
cd seu-projeto
|
|
29
|
-
npx
|
|
43
|
+
npx @daniellins/power-claude install --yes
|
|
30
44
|
```
|
|
31
45
|
|
|
32
|
-
|
|
46
|
+
`--yes` é o modo não-interativo: instala todos os pods, assume os padrões e **nunca** sobrescreve nem
|
|
47
|
+
remove arquivo seu. Para escolher quais pods entram e decidir sobre o `git init` com commit inicial,
|
|
48
|
+
rode a forma interativa:
|
|
33
49
|
|
|
34
50
|
```bash
|
|
35
51
|
npx @daniellins/power-claude install
|
|
36
52
|
```
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
Em qualquer um dos dois, o instalador registra o que colocou em `.power-claude/manifest.json` — é esse
|
|
55
|
+
manifesto que dá base ao `update` e ao `uninstall`. Logo depois de instalar, confira a instalação:
|
|
39
56
|
|
|
40
57
|
```bash
|
|
41
|
-
npx @daniellins/power-claude
|
|
58
|
+
npx @daniellins/power-claude doctor
|
|
42
59
|
```
|
|
43
60
|
|
|
44
61
|
O instalador **nunca sobrescreve** arquivos seus: conflitos são perguntados (interativo) ou preservados (`--yes`). O `.claude/settings.json` recebe merge aditivo em `hooks`/`permissions` (a entrada nova do template é acrescentada sem remover nem alterar o que você já tem); nas demais chaves, suas chaves vencem, as do template entram só onde faltam.
|
|
45
62
|
|
|
63
|
+
O npm é o caminho de instalação — não `git clone`. Um clone do repositório serve para ler e auditar o
|
|
64
|
+
código, nunca para instalar; o que o instalador copia é o que o pacote publica.
|
|
65
|
+
|
|
46
66
|
## Comandos do CLI
|
|
47
67
|
|
|
48
68
|
| Comando | O que faz |
|
|
@@ -50,11 +70,12 @@ O instalador **nunca sobrescreve** arquivos seus: conflitos são perguntados (in
|
|
|
50
70
|
| `power-claude install` | Instala o framework no diretório atual (default) |
|
|
51
71
|
| `power-claude adopt` | Adota uma instalação montada à mão (sem manifest): classifica o disco contra o template (idêntico / modificado / ausente), gera o `.power-claude/manifest.json` sem sobrescrever nada e coloca a instalação ao alcance do `update` |
|
|
52
72
|
| `power-claude update` | Atualiza; arquivos que você modificou são preservados (com diff resumido no modo interativo) |
|
|
53
|
-
| `power-claude doctor` | Diagnóstico: integridade do manifest, hooks, `.gitignore`, gate de budgets, tamanho do CLAUDE.md |
|
|
73
|
+
| `power-claude doctor` | Diagnóstico: integridade do manifest, hooks, `.gitignore`, gate de budgets, tamanho do CLAUDE.md e execução dos workflows dos pods instalados |
|
|
54
74
|
| `power-claude uninstall` | Remove apenas o que o instalador colocou e continua intacto; o que você modificou fica |
|
|
55
75
|
| `power-claude --help` / `--version` | Ajuda / versão |
|
|
56
76
|
|
|
57
|
-
|
|
77
|
+
Flags globais: `--yes` (`-y`) para modo não-interativo; `--scope motor`, só em `update`, restringe a
|
|
78
|
+
atualização aos scripts do motor (ver a nota da próxima seção).
|
|
58
79
|
|
|
59
80
|
## Estrutura instalada
|
|
60
81
|
|
|
@@ -63,30 +84,50 @@ seu-projeto/
|
|
|
63
84
|
├── .claude/
|
|
64
85
|
│ ├── CLAUDE.md # instruções enxutas (budget ~2.000 tokens)
|
|
65
86
|
│ ├── settings.json # hooks + permissions (merge com o seu)
|
|
87
|
+
│ ├── agents/ # papéis do ciclo (pc-planner, pc-dev, pc-qa, pc-devops, ...)
|
|
88
|
+
│ ├── commands/ # comandos /power:*
|
|
89
|
+
│ ├── rules/ # autoridade, orquestração, matriz de modelos, curadoria
|
|
90
|
+
│ ├── skills/ # pc-run-pod, pc-full-cycle, pc-pipeline, pc-arena, ...
|
|
66
91
|
│ └── hooks/
|
|
67
|
-
│
|
|
92
|
+
│ ├── enforce-git-push-authority.cjs
|
|
93
|
+
│ ├── enforce-cycle-authority.cjs
|
|
94
|
+
│ ├── enforce-ready-oracles.cjs
|
|
95
|
+
│ └── context-budget.cjs
|
|
68
96
|
├── pods/
|
|
69
97
|
│ ├── academy/ # aulas, palestras, treinamentos
|
|
70
98
|
│ ├── authority-content/ # conteúdo com voz própria e fact-check
|
|
71
99
|
│ ├── automation/ # N8N + Langgraph
|
|
100
|
+
│ ├── content-carousel/ # carrosséis a partir de uma fonte, com fact-check
|
|
72
101
|
│ ├── dev-ia/ # ciclo de desenvolvimento com IA
|
|
102
|
+
│ ├── hormozi-method/ # oferta, aquisição e diagnóstico de escala
|
|
73
103
|
│ └── squad-forge/ # cria novos agentes e pods
|
|
74
104
|
├── knowledge/
|
|
75
|
-
│
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
│ ├── voice-dna/ # DNA de voz para conteúdo autoral (opcional)
|
|
106
|
+
│ └── analyst/ # base de pesquisa compartilhada
|
|
107
|
+
├── scripts/ # motor: runner, memória, validadores, vetos
|
|
78
108
|
└── .power-claude/
|
|
79
109
|
└── manifest.json # estado da instalação (base do update/uninstall)
|
|
80
110
|
```
|
|
81
111
|
|
|
82
|
-
|
|
112
|
+
O catálogo dos pods, com o job-to-be-done de cada um, está em [`template/pods/README.md`](template/pods/README.md).
|
|
113
|
+
|
|
114
|
+
**O que viaja para a sua instalação, e o que não viaja.** `install` copia o `template/` inteiro (o
|
|
115
|
+
`.claude/` acima, com agentes, regras, skills, comandos e hooks, mais os pods que você escolheu) **e**
|
|
116
|
+
os scripts do motor declarados em [`installer/motor-scripts.json`](installer/motor-scripts.json) — o
|
|
117
|
+
runner, a memória evolutiva dos pods, os validadores de workflow e os vetos vão junto, em `scripts/`,
|
|
118
|
+
com o mesmo contrato de preservação do template (o que você modificar não é sobrescrito; `update
|
|
119
|
+
--scope motor` é a via explícita para atualizar só esses scripts). O que **não** viaja é o que só faz
|
|
120
|
+
sentido no repositório do framework: testes, fixtures e um punhado de scripts nomeados na lista
|
|
121
|
+
`exclude` do mesmo manifesto. A documentação também não entra no pacote npm (`package.json` →
|
|
122
|
+
`files: ["bin","installer","template","scripts"]`) — ela vive neste repositório, que é o que você está
|
|
123
|
+
lendo agora.
|
|
83
124
|
|
|
84
|
-
## Personalização
|
|
125
|
+
## Personalização
|
|
85
126
|
|
|
86
|
-
**
|
|
87
|
-
perguntam quem é você — **uma vez, três campos** (nome, handle, uma linha de
|
|
88
|
-
mais — voz, tom, léxico, objetivos — é opcional, tem default funcional, e vai se
|
|
89
|
-
seu feedback durante o uso.
|
|
127
|
+
Opcional, em três degraus — e **nenhum pod exige que você construa um DNA de voz.** Os pods que assinam
|
|
128
|
+
peças **em seu nome** perguntam quem é você — **uma vez, três campos** (nome, handle, uma linha de
|
|
129
|
+
autoridade). Tudo o mais — voz, tom, léxico, objetivos — é opcional, tem default funcional, e vai se
|
|
130
|
+
ajustando com o seu feedback durante o uso.
|
|
90
131
|
|
|
91
132
|
| Degrau | O que você fornece | Onde mora | O que muda |
|
|
92
133
|
|---|---|---|---|
|
|
@@ -107,6 +148,29 @@ template distribui só os `*.example.*`, e um oráculo (`scripts/owner-layer.cjs
|
|
|
107
148
|
ser um placeholder marcado. Passo a passo de instanciação: `knowledge/voice-dna/README.md` na sua
|
|
108
149
|
instalação.
|
|
109
150
|
|
|
151
|
+
## Para quem avalia o framework
|
|
152
|
+
|
|
153
|
+
Se você já conhece o terreno e quer julgar profundidade em vez de aprender do zero:
|
|
154
|
+
|
|
155
|
+
- **[Índice dos mecanismos](docs/mecanismos/README.md)** — a referência densa: uma página por
|
|
156
|
+
mecanismo entregue, agrupada por área funcional, com comandos reais e ligação para o "porquê".
|
|
157
|
+
É o melhor lugar para medir o que o framework realmente faz.
|
|
158
|
+
- **[Releases do GitHub](../../releases)** — o histórico versão a versão, com o que entrou em cada
|
|
159
|
+
release; o corpo de cada Release deriva do `CHANGELOG.md` do repositório de desenvolvimento (privado
|
|
160
|
+
por decisão). Este README não mantém vitrine de novidades: quem responde por "o que mudou desde a
|
|
161
|
+
última vez que olhei" são as Releases.
|
|
162
|
+
- **[Decisões arquiteturais (ADR)](docs/adr)** — cada decisão não trivial com alternativas
|
|
163
|
+
consideradas, o que foi medido e o que foi recusado. É onde ver o critério, não só o resultado.
|
|
164
|
+
- **[Pesquisa e registros de investigação](docs/research)** — o que foi medido antes de decidir,
|
|
165
|
+
inclusive quando a medição derrubou a hipótese.
|
|
166
|
+
|
|
167
|
+
**Sobre as referências a `docs/stories` e `docs/qa` que você vai encontrar nos ADRs e na pesquisa.** Elas
|
|
168
|
+
apontam para o registro de execução do framework — as stories de desenvolvimento e os gates de qualidade que
|
|
169
|
+
as aprovam. Esse registro é **privado por decisão explícita, não por descuido**: o que se publica aqui é o
|
|
170
|
+
critério (o porquê e o que foi medido), enquanto o diário de execução, que carrega contexto operacional e
|
|
171
|
+
pessoal de quem trabalha nele, fica fora. Um link quebrado para `docs/stories/...` ou `docs/qa/...` num ADR
|
|
172
|
+
é, portanto, o comportamento esperado desta fronteira — não um arquivo perdido.
|
|
173
|
+
|
|
110
174
|
## Validação no repositório do framework
|
|
111
175
|
|
|
112
176
|
```bash
|
|
@@ -116,6 +180,6 @@ npm run validate # budgets + branding + validate-no-clone
|
|
|
116
180
|
|
|
117
181
|
## Licença
|
|
118
182
|
|
|
119
|
-
[MIT](
|
|
183
|
+
[MIT](LICENSE) — Copyright (c) 2026 **Daniel Lins**.
|
|
120
184
|
|
|
121
185
|
Use, copie e adapte à vontade, mantendo a atribuição a Daniel Lins conforme a licença.
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
{ "path": "scripts/pod-export-ui.cjs", "owner": "skill:pc-export", "reason": "referenced", "requiredBy": "skill:pc-export" },
|
|
36
36
|
{ "path": "scripts/validate-story-acs.cjs", "owner": "skill:pc-validate-story", "reason": "referenced" }
|
|
37
37
|
],
|
|
38
|
-
"exclude": ["*.test.cjs", "__tests__/", "__fixtures__/", "validate-install.js", "validate-adopt.js", "validate-update-scope-motor.js", "owner-layer.cjs", "derive-ready-transitions.cjs"]
|
|
38
|
+
"exclude": ["*.test.cjs", "__tests__/", "__fixtures__/", "validate-install.js", "validate-adopt.js", "validate-update-scope-motor.js", "owner-layer.cjs", "derive-ready-transitions.cjs", "validate-git-vetor.cjs", "build-public-tree.cjs"]
|
|
39
39
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daniellins/power-claude",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
4
4
|
"description": "Lean, budget-gated agent framework for Claude Code — pods by job-to-be-done",
|
|
5
5
|
"author": "Daniel Lins",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/daniellins/power-claude-core.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/daniellins/power-claude-core#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/daniellins/power-claude-core/issues"
|
|
14
|
+
},
|
|
7
15
|
"bin": {
|
|
8
16
|
"power-claude": "bin/cli.js"
|
|
9
17
|
},
|
|
@@ -21,6 +29,6 @@
|
|
|
21
29
|
],
|
|
22
30
|
"scripts": {
|
|
23
31
|
"test": "node scripts/validate-install.js && node scripts/validate-adopt.js && node scripts/validate-update-scope-motor.js && node scripts/__tests__/validate-pod-workflow.test.cjs && node scripts/__tests__/pc-run-pod.test.cjs && node scripts/__tests__/pc-run-pod-counter.test.cjs && node scripts/pc-run-pod-countgte.test.cjs && node scripts/forge.test.cjs && node scripts/forge-assess-tier.test.cjs && node scripts/forge-additive.test.cjs && node scripts/forge-canonical.test.cjs && node scripts/migration-manifest.test.cjs && node scripts/migration-manifest-sf3.2.test.cjs && node scripts/migration-manifest-sf3.3.test.cjs && node scripts/migration-manifest-sf5.5.test.cjs && node scripts/validate-no-clone.test.cjs && node scripts/migrate-squad-orchestration.test.cjs && node scripts/pod-memory.test.cjs && node scripts/pod-memory-budget.test.cjs && node scripts/pod-memory-note.test.cjs && node scripts/forge-harden-learning.test.cjs && node scripts/harden-knowledge.test.cjs && node scripts/consolidate-knowledge-runner.test.cjs && node scripts/smoke-pod-workflow.cjs scripts/__fixtures__/smoke-canary/workflows/wf-canary.yaml && node scripts/smoke-sweep.test.cjs && node scripts/smoke-isolation.test.cjs && node scripts/harness-g5-lite.test.cjs && node scripts/__tests__/motor-scripts.test.cjs && node scripts/validate-fidelity-output.test.cjs && node scripts/validate-source-audit.test.cjs && node scripts/arena.test.cjs && node scripts/veto-checks.test.cjs && node scripts/phash-gate.test.cjs && node scripts/render-png.test.cjs && node scripts/hn-carousel-contrato-entrada.test.cjs && node scripts/anti-path.test.cjs && node scripts/brief-leitura-obrigatoria.test.cjs && node scripts/pc-run-pipeline.test.cjs && node scripts/pod-export-compile.test.cjs && node scripts/pod-export-executor.test.cjs && node scripts/pod-export-postprocess.test.cjs && node scripts/pod-export-release.test.cjs && node scripts/pod-export-service.test.cjs && node scripts/pod-export-ui.test.cjs && node scripts/pod-export-hardening.test.cjs && node scripts/pod-export-queue-race.test.cjs && node scripts/npm-scope-invocation.test.cjs && node scripts/default-funcional.test.cjs && node scripts/owner-layer.test.cjs && node scripts/__tests__/enforce-cycle-authority.test.cjs && node scripts/__tests__/enforce-git-push-authority.test.cjs && node scripts/__tests__/validate-story-acs.test.cjs && node scripts/__tests__/enforce-ready-oracles.test.cjs && node scripts/__tests__/derive-ready-transitions.test.cjs && node installer/__tests__/doctor-ready-oracles.test.cjs && node installer/__tests__/merge-settings.test.cjs",
|
|
24
|
-
"validate": "node scripts/validate-budgets.js template/pods template/.claude/agents && node scripts/validate-branding.js && node scripts/validate-no-clone.cjs template"
|
|
32
|
+
"validate": "node scripts/validate-budgets.js template/pods template/.claude/agents && node scripts/validate-branding.js && node scripts/validate-no-clone.cjs template && node scripts/validate-git-vetor.cjs"
|
|
25
33
|
}
|
|
26
34
|
}
|
|
@@ -72,6 +72,23 @@
|
|
|
72
72
|
* desconhecido que não é alias; subcomando real do gh), `gh` inacessível,
|
|
73
73
|
* e a prova final de que `gh pr merge` não é alcançável sem `ask` por
|
|
74
74
|
* nenhuma forma testada, nem para pc-devops.
|
|
75
|
+
*
|
|
76
|
+
* Story SF15.12 (`I4` do ADR-037): até então NENHUMA asserção deste arquivo
|
|
77
|
+
* distinguia DESTINO — `pc-devops` empurrava em silêncio para `origin`, para
|
|
78
|
+
* um remoto público e para uma URL arbitrária, indistintamente.
|
|
79
|
+
* `testStorySF15_12()` cobre os 3 cenários do predicado de remoto (nome
|
|
80
|
+
* declarado na lista; URL/SSH bruta; destino não confirmável) e, sobretudo,
|
|
81
|
+
* as CONTRAPROVAS sem as quais o predicado passaria com uma implementação
|
|
82
|
+
* errada: o par que mata o cheat "tudo que não é `origin` pede confirmação"
|
|
83
|
+
* (`B5` — remoto não declarado com nome ≠ origin → silêncio; lista contendo
|
|
84
|
+
* `["origin"]` → ask), o catálogo FECHADO de opções com valor separado
|
|
85
|
+
* (`B1`/`B6` — `-o ci.skip` não vira remoto, `-u`/`--tags` não engolem o
|
|
86
|
+
* remoto), o encadeamento (`B2` — o 2º push da cadeia não some na agregação,
|
|
87
|
+
* e o `ask` novo nunca rebaixa um `deny`), malformado ≠ ausente (`M1`), o
|
|
88
|
+
* `-C` que passa a ser usado em vez de descartado (`M2`) e a UNIÃO dos 2
|
|
89
|
+
* lados de leitura da lista (`B4`). Os cenários vivem AQUI, na suíte que
|
|
90
|
+
* `npm test` roda, e não só no oráculo da story — sem isso o próximo refactor
|
|
91
|
+
* do hook regride o `I4` em silêncio.
|
|
75
92
|
*/
|
|
76
93
|
|
|
77
94
|
const fs = require('node:fs');
|
|
@@ -641,6 +658,264 @@ function testFixRoundF5() {
|
|
|
641
658
|
}
|
|
642
659
|
}
|
|
643
660
|
|
|
661
|
+
// Cria um repositório temporário real com (opcionalmente) uma DECLARAÇÃO de
|
|
662
|
+
// remotos irreversíveis em `.claude/hooks/irreversible-remotes.json` — o lado
|
|
663
|
+
// `input.cwd` da união (a instalação viva cuja sessão executa o push).
|
|
664
|
+
function initTmpRepoComDeclaracao(prefix, conteudoJson) {
|
|
665
|
+
const dir = initTmpGitRepo(prefix);
|
|
666
|
+
if (conteudoJson !== undefined) {
|
|
667
|
+
fs.mkdirSync(path.join(dir, '.claude', 'hooks'), { recursive: true });
|
|
668
|
+
fs.writeFileSync(path.join(dir, '.claude', 'hooks', 'irreversible-remotes.json'), conteudoJson);
|
|
669
|
+
}
|
|
670
|
+
return dir;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function declara(remotes) {
|
|
674
|
+
return JSON.stringify({ remotes });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Configura um upstream RESOLVÍVEL offline (sem rede, sem push real): o
|
|
678
|
+
// `git rev-parse --abbrev-ref --symbolic-full-name @{u}` só responde quando
|
|
679
|
+
// existem as 3 peças — remote com refspec de fetch, `branch.<b>.remote`/
|
|
680
|
+
// `.merge`, e a ref de rastreamento `refs/remotes/<remoto>/<branch>` (medido:
|
|
681
|
+
// sem a ref, sai `exit 128`, "not stored as a remote-tracking branch").
|
|
682
|
+
function configuraUpstream(repo, remoteName) {
|
|
683
|
+
spawnSync('git', ['commit', '-q', '--allow-empty', '-m', 'base'], { cwd: repo });
|
|
684
|
+
const branch = (spawnSync('git', ['branch', '--show-current'], { cwd: repo, encoding: 'utf8' }).stdout || '').trim();
|
|
685
|
+
const head = (spawnSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).stdout || '').trim();
|
|
686
|
+
spawnSync('git', ['remote', 'add', remoteName, `/tmp/repositorio-inexistente-${remoteName}.git`], { cwd: repo });
|
|
687
|
+
spawnSync('git', ['config', `branch.${branch}.remote`, remoteName], { cwd: repo });
|
|
688
|
+
spawnSync('git', ['config', `branch.${branch}.merge`, `refs/heads/${branch}`], { cwd: repo });
|
|
689
|
+
spawnSync('git', ['update-ref', `refs/remotes/${remoteName}/${branch}`, head], { cwd: repo });
|
|
690
|
+
return branch;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function testStorySF15_12() {
|
|
694
|
+
console.log('\n-- Story SF15.12 (`I4` do ADR-037): o hook deixa de ser cego a REMOTO --');
|
|
695
|
+
|
|
696
|
+
// Toda asserção usa `pc-devops` de propósito: é a identidade MAIS permissiva
|
|
697
|
+
// do hook (a única que hoje empurra em silêncio), então é nela que a
|
|
698
|
+
// regressão dói e é nela que o `ask` novo precisa valer — "inclusive para
|
|
699
|
+
// devops" não é retórica do desenho, é o caso testado.
|
|
700
|
+
const declarado = initTmpRepoComDeclaracao('egpa-i4-decl-', declara(['publico']));
|
|
701
|
+
try {
|
|
702
|
+
const push = (command) => runHook(payloadCmd({ agentType: 'pc-devops', command, cwd: declarado })).decision;
|
|
703
|
+
|
|
704
|
+
// (a) NOME declarado na lista → ask incondicional.
|
|
705
|
+
assert(push('git push publico main') === 'ask', 'I4 (a): remoto DECLARADO irreversível, pc-devops → ask (o allow silencioso não é herdado pelo remoto público)');
|
|
706
|
+
|
|
707
|
+
// Regressão: remoto nomeado NÃO declarado — comportamento idêntico ao de
|
|
708
|
+
// antes desta story. É o push do próprio ciclo do framework.
|
|
709
|
+
assert(push('git push origin main') === null, 'I4 regressão: `origin` (não declarado), pc-devops → silêncio (comportamento inalterado)');
|
|
710
|
+
|
|
711
|
+
// `B5`, metade 1: o cheat `remote !== "origin" ⇒ ask` morre aqui.
|
|
712
|
+
assert(push('git push upstream main') === null, 'I4 (`B5` metade 1): remoto NÃO declarado com nome ≠ origin (`upstream`) → silêncio (não é o nome que decide)');
|
|
713
|
+
|
|
714
|
+
// `B1`: opção do subcomando que toma valor em token SEPARADO não pode
|
|
715
|
+
// fazer o parser ler o VALOR como remoto.
|
|
716
|
+
assert(push('git push -o ci.skip publico main') === 'ask', 'I4 (`B1`): `-o ci.skip publico main` → ask (a opção catalogada consome `ci.skip`, o remoto extraído é `publico`)');
|
|
717
|
+
assert(push('git push --push-option ci.skip publico main') === 'ask', 'I4 (`B1`): `--push-option ci.skip publico main` → ask (forma longa da mesma opção)');
|
|
718
|
+
assert(push('git push --push-option=ci.skip publico main') === 'ask', 'I4 (`B1`): `--push-option=ci.skip publico main` → ask (forma com `=` não consome o token seguinte)');
|
|
719
|
+
|
|
720
|
+
// `B6`: catálogo FECHADO — toda opção FORA dele é flag SEM valor.
|
|
721
|
+
assert(push('git push -u publico main') === 'ask', 'I4 (`B6` fail-open): `-u publico main` → ask (`-u`, fora do catálogo, NÃO engole o remoto — a flag de push mais usada do ciclo)');
|
|
722
|
+
assert(push('git push --tags origin main') === null, 'I4 (`B6` ruído): `--tags origin main` → silêncio (`--tags` não engole o remoto nem vira "destino não resolvido")');
|
|
723
|
+
assert(push('git push --tags publico main') === 'ask', 'I4 (`B6`): `--tags publico main` → ask (a mesma flag sem valor, com remoto declarado, continua mordendo)');
|
|
724
|
+
|
|
725
|
+
// (b) URL/SSH-spec BRUTA — o vetor medido no ADR-037 §1.3.
|
|
726
|
+
assert(push('git push git@github.com:qualquer/outro.git HEAD:main') === 'ask', 'I4 (b): URL SSH bruta (`git@github.com:...`), pc-devops → ask (vetor medido no ADR-037 §1.3)');
|
|
727
|
+
assert(push('git push https://github.com/qualquer/outro.git main') === 'ask', 'I4 (b): URL `https://` bruta → ask');
|
|
728
|
+
|
|
729
|
+
// (c) nenhum remoto explícito e upstream irresolúvel.
|
|
730
|
+
assert(push('git push') === 'ask', 'I4 (c): `git push` pelado em repositório SEM upstream → ask ("não deu para confirmar" nunca vira "sei que não é")');
|
|
731
|
+
|
|
732
|
+
// `B2`: o 2º segmento da cadeia não pode SUMIR na agregação por severidade.
|
|
733
|
+
assert(push('git push origin main && git push publico main') === 'ask', 'I4 (`B2`): cadeia `origin && publico` → ask (o 2º segmento não some na agregação)');
|
|
734
|
+
|
|
735
|
+
// `B2`, invariante inegociável: o `ask` novo NUNCA rebaixa um `deny`.
|
|
736
|
+
assert(push('git push && git push --force origin main') === 'deny', 'I4 (`B2` invariante): cadeia com force-push → deny (severidade 3.5 < 4: o `ask` novo nunca rebaixa o `deny`)');
|
|
737
|
+
assert(push('git push publico main && git push --force origin main') === 'deny', 'I4 (`B2` invariante): remoto declarado + force-push na mesma cadeia → deny (não `ask`)');
|
|
738
|
+
} finally {
|
|
739
|
+
fs.rmSync(declarado, { recursive: true, force: true });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// `F-1` (gate FAIL do `pc-qa`, `b5b0d84`): `--repo` é a ÚNICA entrada do
|
|
743
|
+
// catálogo cujo valor É o repositório de destino. Consumi-lo como valor
|
|
744
|
+
// opaco (o que a leitura SUPERADA do menor `m1` justificou "por precaução")
|
|
745
|
+
// fazia `git push --repo publico` cair no cenário (c) e concluir SILÊNCIO,
|
|
746
|
+
// enquanto o git empurrava para `publico`, um remoto DECLARADO irreversível.
|
|
747
|
+
//
|
|
748
|
+
// ATENÇÃO ao ambiente destas asserções — elas SÓ discriminam num repositório
|
|
749
|
+
// cujo upstream RESOLVE para um remoto NÃO declarado. Medido: num repositório
|
|
750
|
+
// sem upstream, a versão com o defeito também responde `ask` (cai em (c) e
|
|
751
|
+
// não consegue confirmar), e o par inteiro passaria com o furo VIVO. É a
|
|
752
|
+
// condição exata em que o `pc-qa` mediu o `F-1`, e é por isso que este bloco
|
|
753
|
+
// monta `origin` como upstream real em vez de reusar o repositório acima.
|
|
754
|
+
const repoRepoOpt = initTmpRepoComDeclaracao('egpa-i4-repoopt-', declara(['publico']));
|
|
755
|
+
try {
|
|
756
|
+
configuraUpstream(repoRepoOpt, 'origin');
|
|
757
|
+
const p = (command) => runHook(payloadCmd({ agentType: 'pc-devops', command, cwd: repoRepoOpt })).decision;
|
|
758
|
+
|
|
759
|
+
// Pré-condição do instrumento: sem ela, o par abaixo não prova nada.
|
|
760
|
+
assert(p('git push') === null, 'I4 (`F-1`) pré-condição: neste repositório o upstream RESOLVE para `origin` (não declarado) ⇒ `git push` pelado é silêncio — é o que dá poder de discriminação ao par abaixo');
|
|
761
|
+
|
|
762
|
+
// Matam o fail-open: as 2 formas que o git HONRA quando não há posicional.
|
|
763
|
+
assert(p('git push --repo publico') === 'ask', 'I4 (`F-1`): `--repo publico` sem posicional → ask (o git alcança `publico`, medido contra o git real 2.50.1 — antes era SILÊNCIO)');
|
|
764
|
+
assert(p('git push --repo=publico') === 'ask', 'I4 (`F-1`): `--repo=publico` sem posicional → ask (a forma com `=` também é honrada pelo git)');
|
|
765
|
+
assert(p('git push --tags --repo publico') === 'ask', 'I4 (`F-1`): `--repo` DEPOIS de outra flag, sem posicional → ask (a varredura não para no 1º token)');
|
|
766
|
+
|
|
767
|
+
// Matam o ruído: a PRECEDÊNCIA do posicional, medida contra o git real.
|
|
768
|
+
assert(p('git push --repo publico main') === null, 'I4 (`F-1` precedência): `--repo publico main` → silêncio (o POSICIONAL vence — o git trata `main` como repositório: `fatal: \'main\' does not appear to be a git repository`)');
|
|
769
|
+
assert(p('git push --repo publico origin main') === null, 'I4 (`F-1` precedência): `--repo publico origin main` → silêncio (o git alcança `origin`, o posicional, não `publico`)');
|
|
770
|
+
assert(p('git push --repo origin') === null, 'I4 (`F-1` regressão): `--repo origin` (não declarado) → silêncio (capturar `--repo` não virou `ask` genérico)');
|
|
771
|
+
|
|
772
|
+
// Dúvida e invariantes.
|
|
773
|
+
assert(p('git push --repo') === 'ask', 'I4 (`F-1`): `--repo` sem valor utilizável → ask ("não sei para onde isto vai" nunca vira "não há")');
|
|
774
|
+
assert(p('git push --force --repo publico') === 'deny', 'I4 (`F-1` invariante): `--force --repo publico` → deny (o `ask` do destino nunca rebaixa o force-push)');
|
|
775
|
+
|
|
776
|
+
// Contraprova: as OUTRAS opções do catálogo continuam com valor opaco —
|
|
777
|
+
// o fix não transformou todo valor catalogado em candidato a remoto.
|
|
778
|
+
assert(p('git push --exec /bin/true publico') === 'ask', 'I4 (`F-1` contraprova): o valor de `--exec` NÃO é remoto — quem decide é `publico`, o posicional → ask');
|
|
779
|
+
assert(p('git push --exec /bin/true origin') === null, 'I4 (`F-1` contraprova): a mesma forma com `origin` → silêncio (o catálogo segue consumindo o valor das demais opções)');
|
|
780
|
+
assert(p('git push -o ci.skip publico') === 'ask', 'I4 (`F-1` contraprova): `-o ci.skip publico` → ask (o `B1` segue intacto: `ci.skip` não é remoto, `publico` é)');
|
|
781
|
+
} finally {
|
|
782
|
+
fs.rmSync(repoRepoOpt, { recursive: true, force: true });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// `B5`, metade 2: com `["origin"]` na lista, `git push origin main` — o
|
|
786
|
+
// comando mais comum do ciclo, silencioso em TODA outra asserção deste
|
|
787
|
+
// arquivo — passa a pedir confirmação. As 2 metades juntas provam que é a
|
|
788
|
+
// LISTA que decide, não o nome do remoto.
|
|
789
|
+
const originDeclarado = initTmpRepoComDeclaracao('egpa-i4-origin-', declara(['origin']));
|
|
790
|
+
try {
|
|
791
|
+
assert(
|
|
792
|
+
runHook(payloadCmd({ agentType: 'pc-devops', command: 'git push origin main', cwd: originDeclarado })).decision === 'ask',
|
|
793
|
+
'I4 (`B5` metade 2): lista contendo `["origin"]` → `git push origin main` → ask (é a LISTA que decide, não o nome)',
|
|
794
|
+
);
|
|
795
|
+
} finally {
|
|
796
|
+
fs.rmSync(originDeclarado, { recursive: true, force: true });
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// `M1` — malformado ≠ ausente. Um typo na lista NUNCA pode desligar a
|
|
800
|
+
// guarda em silêncio: o desfecho é o oposto, `ask` até para o que hoje é
|
|
801
|
+
// silencioso.
|
|
802
|
+
const ausente = initTmpRepoComDeclaracao('egpa-i4-ausente-', undefined);
|
|
803
|
+
try {
|
|
804
|
+
assert(
|
|
805
|
+
runHook(payloadCmd({ agentType: 'pc-devops', command: 'git push origin main', cwd: ausente })).decision === null,
|
|
806
|
+
'I4 (`M1` controle): arquivo AUSENTE é resposta conclusiva ("nada declarado") → silêncio (não quebra instalação que nunca ouviu falar desta story)',
|
|
807
|
+
);
|
|
808
|
+
} finally {
|
|
809
|
+
fs.rmSync(ausente, { recursive: true, force: true });
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const malformados = [
|
|
813
|
+
['{ isto nao e json valido', 'JSON com erro de SINTAXE'],
|
|
814
|
+
[JSON.stringify({ remotes: 'publico' }), 'FORMA errada (`remotes` string, não array)'],
|
|
815
|
+
[JSON.stringify({ remotes: ['publico', 42] }), 'FORMA errada (entrada não-string dentro do array)'],
|
|
816
|
+
[JSON.stringify({ semChaveRemotes: [] }), 'FORMA errada (sem a chave `remotes`)'],
|
|
817
|
+
];
|
|
818
|
+
for (const [conteudo, rotulo] of malformados) {
|
|
819
|
+
const repo = initTmpRepoComDeclaracao('egpa-i4-malformado-', conteudo);
|
|
820
|
+
try {
|
|
821
|
+
assert(
|
|
822
|
+
runHook(payloadCmd({ agentType: 'pc-devops', command: 'git push origin main', cwd: repo })).decision === 'ask',
|
|
823
|
+
`I4 (\`M1\` fail-loud): lista ilegível — ${rotulo} — → ask até para \`origin\` (um typo NUNCA desliga a guarda em silêncio)`,
|
|
824
|
+
);
|
|
825
|
+
} finally {
|
|
826
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// (c) quando o upstream RESOLVE: o predicado usa o remoto resolvido contra a
|
|
831
|
+
// lista, em vez de perguntar sempre. Sem estas 2, "(c) ⇒ ask" passaria por
|
|
832
|
+
// um `ask` cego que quebraria todo push do próprio ciclo.
|
|
833
|
+
const upstreamDeclarado = initTmpRepoComDeclaracao('egpa-i4-up-decl-', declara(['publico']));
|
|
834
|
+
try {
|
|
835
|
+
configuraUpstream(upstreamDeclarado, 'publico');
|
|
836
|
+
assert(
|
|
837
|
+
runHook(payloadCmd({ agentType: 'pc-devops', command: 'git push', cwd: upstreamDeclarado })).decision === 'ask',
|
|
838
|
+
'I4 (c): `git push` pelado com upstream que resolve para remoto DECLARADO → ask (o destino real é alcançado mesmo sem token de remoto)',
|
|
839
|
+
);
|
|
840
|
+
} finally {
|
|
841
|
+
fs.rmSync(upstreamDeclarado, { recursive: true, force: true });
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const upstreamNaoDeclarado = initTmpRepoComDeclaracao('egpa-i4-up-ndecl-', declara(['publico']));
|
|
845
|
+
try {
|
|
846
|
+
configuraUpstream(upstreamNaoDeclarado, 'origin');
|
|
847
|
+
assert(
|
|
848
|
+
runHook(payloadCmd({ agentType: 'pc-devops', command: 'git push', cwd: upstreamNaoDeclarado })).decision === null,
|
|
849
|
+
'I4 (c) contraprova: `git push` pelado com upstream que resolve para remoto NÃO declarado → silêncio (o cenário (c) não é um `ask` cego)',
|
|
850
|
+
);
|
|
851
|
+
} finally {
|
|
852
|
+
fs.rmSync(upstreamNaoDeclarado, { recursive: true, force: true });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// `M2` — `git -C <outro-repo> push` resolve o upstream no repositório que o
|
|
856
|
+
// comando aponta, não no `cwd` da sessão (nomes de remoto são POR
|
|
857
|
+
// repositório). Discrimina: se `-C` fosse descartado (como antes desta
|
|
858
|
+
// story), a resolução cairia no `cwd`, que não tem upstream, e o desfecho
|
|
859
|
+
// seria `ask` — o oposto do esperado aqui.
|
|
860
|
+
const cwdSemUpstream = initTmpRepoComDeclaracao('egpa-i4-c-cwd-', declara(['publico']));
|
|
861
|
+
const alvoComUpstream = initTmpGitRepo('egpa-i4-c-alvo-');
|
|
862
|
+
try {
|
|
863
|
+
configuraUpstream(alvoComUpstream, 'origin');
|
|
864
|
+
assert(
|
|
865
|
+
runHook(payloadCmd({ agentType: 'pc-devops', command: `git -C ${alvoComUpstream} push`, cwd: cwdSemUpstream })).decision === null,
|
|
866
|
+
'I4 (`M2`): `git -C <repo-com-upstream-não-declarado> push` → silêncio (o valor de `-C` é usado como repositório da resolução, não descartado)',
|
|
867
|
+
);
|
|
868
|
+
assert(
|
|
869
|
+
runHook(payloadCmd({ agentType: 'pc-devops', command: `git -C ${alvoComUpstream} -C ${cwdSemUpstream} push`, cwd: cwdSemUpstream })).decision === 'ask',
|
|
870
|
+
'I4 (`M2`): `-C` repetido (cumulativo no git real, não reproduzido por este parser) → ask ("presente e não utilizável" é dúvida, nunca silêncio)',
|
|
871
|
+
);
|
|
872
|
+
} finally {
|
|
873
|
+
fs.rmSync(cwdSemUpstream, { recursive: true, force: true });
|
|
874
|
+
fs.rmSync(alvoComUpstream, { recursive: true, force: true });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// `B4` — a UNIÃO dos 2 lados. O lado `__dirname` (o arquivo que VIAJA com o
|
|
878
|
+
// hook, dentro da instalação) precisa contribuir SOZINHO, sem depender de
|
|
879
|
+
// `cwd`: é ele que morde quando a sessão roda de um diretório sem `.claude/`
|
|
880
|
+
// nenhum. E os 2 lados juntos ALARGAM o conjunto — nunca se substituem.
|
|
881
|
+
const hookHome = fs.mkdtempSync(path.join(os.tmpdir(), 'egpa-i4-dirname-'));
|
|
882
|
+
const repoSemClaude = initTmpGitRepo('egpa-i4-sem-claude-');
|
|
883
|
+
const repoOutraLista = initTmpRepoComDeclaracao('egpa-i4-uniao-', declara(['soNoCwd']));
|
|
884
|
+
try {
|
|
885
|
+
const hookCopiado = path.join(hookHome, 'enforce-git-push-authority.cjs');
|
|
886
|
+
fs.copyFileSync(HOOK_PATH, hookCopiado);
|
|
887
|
+
fs.writeFileSync(path.join(hookHome, 'irreversible-remotes.json'), declara(['soNoDirname']));
|
|
888
|
+
const viaCopia = (command, cwd) => runHookAtPath(hookCopiado, payloadCmd({ agentType: 'pc-devops', command, cwd })).decision;
|
|
889
|
+
|
|
890
|
+
assert(!fs.existsSync(path.join(repoSemClaude, '.claude')), 'I4 (`B4`) pré-condição: o `cwd` do caso `__dirname` não tem `.claude/` nenhum');
|
|
891
|
+
assert(viaCopia('git push soNoDirname main', repoSemClaude) === 'ask', 'I4 (`B4`): o lado `__dirname` da união morde SOZINHO, com `cwd` sem `.claude/` (o hook não lia arquivo nenhum antes desta story)');
|
|
892
|
+
assert(viaCopia('git push origin main', repoSemClaude) === null, 'I4 (`B4`) controle: remoto não declarado em lado nenhum → silêncio');
|
|
893
|
+
assert(viaCopia('git push soNoDirname main', repoOutraLista) === 'ask', 'I4 (`B4`) união: o lado `__dirname` continua valendo mesmo quando o `cwd` declara OUTRA lista (união não é substituição)');
|
|
894
|
+
assert(viaCopia('git push soNoCwd main', repoOutraLista) === 'ask', 'I4 (`B4`) união: o lado `input.cwd` também morde na mesma invocação (união ALARGA os dois lados, nunca interseciona)');
|
|
895
|
+
} finally {
|
|
896
|
+
fs.rmSync(hookHome, { recursive: true, force: true });
|
|
897
|
+
fs.rmSync(repoSemClaude, { recursive: true, force: true });
|
|
898
|
+
fs.rmSync(repoOutraLista, { recursive: true, force: true });
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// O arquivo que VIAJA no template nasce vazio e assim precisa permanecer:
|
|
902
|
+
// hardcodar o nome do remoto deste projeto no framework distribuído é
|
|
903
|
+
// exatamente o que o ADR-037 §8.1.1 (b) proíbe — a lista é do dono de cada
|
|
904
|
+
// instalação. Guarda permanente contra essa regressão.
|
|
905
|
+
const templateDecl = path.join(REPO_ROOT, 'template', '.claude', 'hooks', 'irreversible-remotes.json');
|
|
906
|
+
assert(fs.existsSync(templateDecl), 'I4: `template/.claude/hooks/irreversible-remotes.json` existe e viaja com o hook para toda instalação');
|
|
907
|
+
let templateParsed = null;
|
|
908
|
+
try {
|
|
909
|
+
templateParsed = JSON.parse(fs.readFileSync(templateDecl, 'utf8'));
|
|
910
|
+
} catch {
|
|
911
|
+
templateParsed = null;
|
|
912
|
+
}
|
|
913
|
+
assert(
|
|
914
|
+
templateParsed && Array.isArray(templateParsed.remotes) && templateParsed.remotes.length === 0,
|
|
915
|
+
'I4: o arquivo do template é `{"remotes": []}` — nenhum nome de remoto hardcodado no framework distribuído (ADR-037 §8.1.1 b)',
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
644
919
|
function buildMutant(tmpDir) {
|
|
645
920
|
const source = fs.readFileSync(HOOK_PATH, 'utf8');
|
|
646
921
|
const target = `function isDevOpsAgent(input) {\n return typeof input?.agent_type === 'string' && input.agent_type === DEVOPS_AGENT_TYPE;\n}`;
|
|
@@ -670,7 +945,18 @@ function testMutacao() {
|
|
|
670
945
|
|
|
671
946
|
// A mutante deveria, no entanto, mudar o resultado do controle 4 (não-force):
|
|
672
947
|
// prova que a mutação realmente afeta a checagem de identidade (mutante viva).
|
|
673
|
-
|
|
948
|
+
//
|
|
949
|
+
// Story SF15.12 (`B3`, remediação prescrita ANTES da implementação): o
|
|
950
|
+
// payload deste instrumento era `git push` PELADO. Com o cenário (c) do
|
|
951
|
+
// `I4` (sem argumento de remoto + upstream irresolúvel ⇒ `ask`), esse
|
|
952
|
+
// comando passa a ser `ask` por razão INDEPENDENTE da identidade — a
|
|
953
|
+
// branch de trabalho não tem upstream (medido: `git rev-parse @{u}` sai
|
|
954
|
+
// exit 128) — e o instrumento perderia o poder de discriminar mutante ×
|
|
955
|
+
// hook real, que é a única coisa que ele existe para fazer. `git push
|
|
956
|
+
// origin main` (remoto EXPLÍCITO e NÃO-declarado) restaura essa
|
|
957
|
+
// discriminação: `origin` não participa da checagem de identidade que o
|
|
958
|
+
// AC43 testa, e o desfecho volta a depender só dela.
|
|
959
|
+
const nonForcePayload = payloadCmd({ agentType: 'pc-dev', command: 'git push origin main' });
|
|
674
960
|
assert(runHook(nonForcePayload).decision === 'ask', 'controle: hook REAL pergunta pc-dev + push (não-force) (pré-condição)');
|
|
675
961
|
assert(
|
|
676
962
|
runHookAtPath(mutantPath, nonForcePayload).decision === null,
|
|
@@ -682,12 +968,13 @@ function testMutacao() {
|
|
|
682
968
|
}
|
|
683
969
|
|
|
684
970
|
function main() {
|
|
685
|
-
console.log('POWER CLAUDE — suíte do hook enforce-git-push-authority.cjs (SF12.3, Bloco M, AC42-43 + fix rounds F1/F3/F4/F5)\n');
|
|
971
|
+
console.log('POWER CLAUDE — suíte do hook enforce-git-push-authority.cjs (SF12.3, Bloco M, AC42-43 + fix rounds F1/F3/F4/F5 + SF15.12/`I4`)\n');
|
|
686
972
|
testControlesAC43();
|
|
687
973
|
testFixRoundF1();
|
|
688
974
|
testFixRoundF3();
|
|
689
975
|
testFixRoundF4();
|
|
690
976
|
testFixRoundF5();
|
|
977
|
+
testStorySF15_12();
|
|
691
978
|
testMutacao();
|
|
692
979
|
|
|
693
980
|
if (failures > 0) {
|