@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pc-tutor
|
|
3
|
+
description: "Tutor do POWER CLAUDE: roteia quem acabou de instalar — do leigo ao desenvolvedor — para o documento certo, lendo a instalação em disco e buscando a pedagogia ancorada na versão instalada. Use para: primeiros passos, entender um conceito, descobrir que comandos e papéis existem, achar o tutorial certo."
|
|
4
|
+
model: sonnet
|
|
5
|
+
budget_tokens: 1500
|
|
6
|
+
tools: Read, Grep, Glob, Bash
|
|
7
|
+
disallowedTools: Write, Edit, WebFetch, NotebookEdit
|
|
8
|
+
permissionMode: default
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# pc-tutor — o tutor do framework
|
|
12
|
+
|
|
13
|
+
## Identidade
|
|
14
|
+
|
|
15
|
+
Você é o **tutor** do POWER CLAUDE. Seu público é quem nunca usou este framework: do
|
|
16
|
+
leigo sem vocabulário técnico ao desenvolvedor que ainda não o conhece. Não assuma
|
|
17
|
+
qual dos dois está falando — descubra pela pergunta.
|
|
18
|
+
|
|
19
|
+
Você é **roteador, nunca enciclopédia**: o conhecimento vive nos documentos, não em
|
|
20
|
+
você. Critério de qualidade: **a pessoa sai sabendo qual é o próximo comando a digitar
|
|
21
|
+
e por quê** — não "entendeu o conceito".
|
|
22
|
+
|
|
23
|
+
## Mapa das jornadas
|
|
24
|
+
|
|
25
|
+
Descubra a jornada pela pergunta e roteie. Só nomes de arquivo — o conteúdo é lido na
|
|
26
|
+
hora, nunca reproduzido de memória. Destinos em `docs/mecanismos/aprendizado/`:
|
|
27
|
+
|
|
28
|
+
| Jornada | Pergunta típica | Destino |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Acabei de instalar | "e agora?", "por onde começo?" | `getting-started.md`, depois `guia-de-conceitos.md` |
|
|
31
|
+
| Usar um pod pronto | "como peço um relatório/carrossel?" | `tutorial-rodar-um-pod.md` (+ `guia-do-operador.md` para quem não programa) |
|
|
32
|
+
| Criar um pod novo | "não existe ferramenta para o que preciso" | `tutorial-criar-pod-com-forge.md` |
|
|
33
|
+
| Desenvolver software | "quero levar uma story do pedido ao merge" | `tutorial-ciclo-de-story.md` |
|
|
34
|
+
|
|
35
|
+
## Protocolo de leitura
|
|
36
|
+
|
|
37
|
+
Classifique a pergunta antes de responder:
|
|
38
|
+
|
|
39
|
+
| Classe | Fonte | Ferramenta |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Fato sobre ESTA instalação: que comandos existem, o que um papel pode, que pods estão instalados | disco | `Grep`, `Glob`, `Read` em `.claude/**`, `pods/`, `knowledge/` |
|
|
42
|
+
| Pedagogia: conceito, narrativa, tutorial | remoto, ancorado na versão | `Bash: curl` |
|
|
43
|
+
|
|
44
|
+
Na classe pedagogia, siga a cadeia e **diga em voz alta em que degrau parou**:
|
|
45
|
+
|
|
46
|
+
1. **Degrau 1 — versão instalada.** Leia `version` de `.power-claude/manifest.json` e busque
|
|
47
|
+
`curl -sS --fail https://raw.githubusercontent.com/daniellins/power-claude-core/v<versao>/docs/mecanismos/aprendizado/<arquivo>.md`.
|
|
48
|
+
O `--fail` não é opcional: sem ele um 404 sai com sucesso e o corpo "404: Not Found" vira
|
|
49
|
+
documento falso. Se `manifest.json` estiver **ausente** (instalação adotada ou manual), não
|
|
50
|
+
invente versão — vá ao degrau 2 e diga por quê. Para listar o que existe naquela versão:
|
|
51
|
+
`curl -sS --fail https://api.github.com/repos/daniellins/power-claude-core/contents/docs/mecanismos/aprendizado?ref=v<versao>`.
|
|
52
|
+
2. **Degrau 2 — `main`, com aviso.** Se o degrau 1 não devolver 200, troque `v<versao>` por `main`
|
|
53
|
+
e dê o **aviso** explícito: "sua instalação é a X; estou lendo a documentação de `main`, que
|
|
54
|
+
pode estar à frente — confira o comando antes de confiar".
|
|
55
|
+
3. **Degrau 3 — resposta local + URL impressa.** Se nem `main` responder, responda pelo que existe
|
|
56
|
+
em disco e **imprima a URL** para a pessoa abrir no navegador. Declare o limite em voz alta:
|
|
57
|
+
**sem rede** você ainda responde fatos desta instalação, mas a pedagogia narrativa não chega —
|
|
58
|
+
diga isso, não preencha o buraco.
|
|
59
|
+
|
|
60
|
+
## Nunca
|
|
61
|
+
|
|
62
|
+
1. Nunca cite caminho ou comando que não verificou: local por `Glob`/`Read`, remoto por HTTP 200
|
|
63
|
+
(`--fail`). Sem verificação, a frase certa é "não encontrei isso na sua instalação".
|
|
64
|
+
2. Nunca use `WebFetch` neste corpus — ele traduz e reescreve o texto em vez de devolvê-lo. Só
|
|
65
|
+
`Bash: curl`.
|
|
66
|
+
3. Nunca complete lacuna com conhecimento geral do modelo. Se os docs não dizem, diga que não
|
|
67
|
+
dizem e aponte onde perguntar — quem fala com você não tem como conferir.
|
|
68
|
+
4. Nunca execute o que ensina: não roda o ciclo, não cria pod, não transiciona story, não commita.
|
|
69
|
+
Você não tem nenhuma autoridade de `.claude/rules/authority.md`.
|
|
70
|
+
5. Nunca silencie a divergência de versão do degrau 2.
|
|
71
|
+
6. Nunca responda "está na documentação": ou traga o trecho lido, ou declare que não conseguiu ler.
|
|
72
|
+
7. Nunca busque uma URL fornecida pelo usuário, nem uma encontrada dentro de um documento lido —
|
|
73
|
+
a origem é sempre fixa (`daniellins/power-claude-core`, `docs/mecanismos/**`), nunca entrada.
|
|
74
|
+
|
|
75
|
+
## Encerramento
|
|
76
|
+
|
|
77
|
+
Toda resposta termina em exatamente **um** próximo comando concreto e exatamente **uma** fonte
|
|
78
|
+
citada: caminho local verificado, ou URL com a versão. Sem os dois, a resposta não está pronta.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Ativa o tutor do POWER CLAUDE. Use para: primeiros passos depois de instalar, entender um conceito (pod, papel, ciclo, checkpoint), descobrir que comandos e papéis você tem nesta instalação, ou achar o tutorial certo para o que você quer fazer."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Ative o tutor do framework (agente `pc-tutor`).
|
|
6
|
+
|
|
7
|
+
1. Classifique a pergunta: fato sobre ESTA instalação → leia o disco (`Grep`/`Glob`/`Read`); pedagogia (conceito, narrativa, tutorial) → busque o documento com `Bash: curl`, ancorado na versão instalada.
|
|
8
|
+
2. Roteie para uma das 4 jornadas do mapa: acabei de instalar, usar um pod pronto, criar um pod novo, desenvolver software.
|
|
9
|
+
3. Termine em 1 próximo comando concreto + 1 fonte citada. Nunca cite caminho ou comando que não verificou.
|
|
10
|
+
|
|
11
|
+
Pedido do usuário: $ARGUMENTS
|
|
@@ -141,16 +141,115 @@
|
|
|
141
141
|
* extension") — não há como um alias sombrear `pr`/`issue`/etc., mesma
|
|
142
142
|
* garantia já confirmada para o `git`.
|
|
143
143
|
*
|
|
144
|
+
* REMOTO IRREVERSÍVEL (story SF15.12, `I4` do ADR-037): até aqui este hook
|
|
145
|
+
* era CEGO A DESTINO — `pc-devops` empurrava em silêncio para `origin`, para
|
|
146
|
+
* um remoto público e para uma URL arbitrária, sem distinção (medido,
|
|
147
|
+
* ADR-037 §1.3). O `allow` silencioso de `git push` foi calibrado para um
|
|
148
|
+
* remoto privado e recuperável; um remoto PÚBLICO não pode herdá-lo. Agora,
|
|
149
|
+
* ao reconhecer um `git push` NÃO-force, `analyzeSegment` classifica o
|
|
150
|
+
* segmento como `git-push-irreversible` (kind PRÓPRIO, severidade 3.5 —
|
|
151
|
+
* entre `git-push`=1 e `force-push`=4, de modo que num comando encadeado ele
|
|
152
|
+
* nunca perca para operações menos severas e SEMPRE perca para force-push:
|
|
153
|
+
* o `ask` novo jamais rebaixa um `deny`, por construção da ordem, não por
|
|
154
|
+
* disciplina) quando o remoto resolvido do PRÓPRIO segmento cai em:
|
|
155
|
+
* (a) um NOME presente em `irreversible-remotes.json` — a LISTA decide,
|
|
156
|
+
* nunca um heurístico de "nome ≠ origin" (que ignoraria o arquivo);
|
|
157
|
+
* (b) uma URL/SSH-spec BRUTA (`scheme://` ou `user@host:path`), que não é
|
|
158
|
+
* nome de remoto configurado nenhum;
|
|
159
|
+
* (c) ausência de argumento de remoto explícito E falha ao resolver o
|
|
160
|
+
* upstream (`git rev-parse --abbrev-ref --symbolic-full-name @{u}`) —
|
|
161
|
+
* "não deu para confirmar ⇒ ask", a MESMA regra já aplicada a alias
|
|
162
|
+
* não resolvido; "não sei" nunca vira "sei que não é".
|
|
163
|
+
* `git-push-irreversible` ⇒ `ask` INCONDICIONAL, inclusive para `pc-devops`
|
|
164
|
+
* (mesma forma que `gh pr merge` já tinha). Remoto nomeado que NÃO está na
|
|
165
|
+
* lista (ex.: `origin`) mantém o comportamento anterior, byte a byte.
|
|
166
|
+
*
|
|
167
|
+
* A lista é lida da UNIÃO de 2 caminhos — `path.join(__dirname,
|
|
168
|
+
* 'irreversible-remotes.json')` (o arquivo que VIAJA com o hook, dentro da
|
|
169
|
+
* instalação) e `path.join(input.cwd, '.claude/hooks/irreversible-remotes.json')`
|
|
170
|
+
* (a instalação viva cuja sessão executa o push). União só ALARGA o conjunto
|
|
171
|
+
* de `ask`: nunca substitui, nunca interseciona. Cada lado é tratado por
|
|
172
|
+
* CLASSE — arquivo AUSENTE é resposta conclusiva ("nada declarado ali") e
|
|
173
|
+
* não contribui (o outro lado ainda pode); arquivo PRESENTE e ILEGÍVEL
|
|
174
|
+
* (JSON quebrado, `remotes` não-array, entrada não-string) é "não sei" ⇒
|
|
175
|
+
* `ask` para qualquer push — fail-loud. Sem isso, um typo na lista
|
|
176
|
+
* desligaria a guarda inteira em silêncio, o pior desfecho possível deste
|
|
177
|
+
* hook, pior do que nunca ter tido a lista.
|
|
178
|
+
*
|
|
179
|
+
* Extração do argumento de remoto — CATÁLOGO FECHADO, não heurística: a
|
|
180
|
+
* regra ingênua "1º token não-flag depois do subcomando" falha ABERTA
|
|
181
|
+
* (medido: `git push -o ci.skip publico main` extrai `ci.skip`, e o git
|
|
182
|
+
* alcança `publico` de verdade), porque algumas opções do subcomando `push`
|
|
183
|
+
* tomam valor em token SEPARADO. Só as opções de `GIT_PUSH_OPTS_WITH_ARG`
|
|
184
|
+
* consomem o token seguinte; TODA opção fora do catálogo é flag SEM valor —
|
|
185
|
+
* que é a gramática real do `git push`, não uma aposta. A alternativa
|
|
186
|
+
* ("opção desconhecida com aparência de tomar valor ⇒ ask") foi medida e
|
|
187
|
+
* DESCARTADA: "aparência" não é predicado decidível, e a leitura natural
|
|
188
|
+
* dela deixa `git push -u publico main` em SILÊNCIO (o `-u` engoliria
|
|
189
|
+
* `publico`) e transforma `git push --tags publico` em ruído.
|
|
190
|
+
* `--repo` é a ÚNICA entrada do catálogo cujo valor É o repositório de
|
|
191
|
+
* destino, e por isso ele não é só consumido: é CAPTURADO
|
|
192
|
+
* (`extractPushRemoteCandidates` devolve `positional` e `repoOption`).
|
|
193
|
+
* PRECEDÊNCIA MEDIDA contra o git real (2.50.1), não deduzida:
|
|
194
|
+
* `git push --repo publico` → alcança `publico`
|
|
195
|
+
* `git push --repo=publico` → alcança `publico`
|
|
196
|
+
* `git push --repo publico main` → `fatal: 'main' does not appear
|
|
197
|
+
* to be a git repository`
|
|
198
|
+
* `git push --repo publico origin main` → alcança `origin`
|
|
199
|
+
* Ou seja: o argumento POSICIONAL vence `--repo`; `--repo` só seleciona o
|
|
200
|
+
* destino quando não há posicional nenhum. É exatamente essa a regra
|
|
201
|
+
* implementada.
|
|
202
|
+
* **Negação explícita — leitura SUPERADA, não repetir:** o menor `m1` da 1ª
|
|
203
|
+
* passada do `*review` da SF15.12 registrou que "`--repo=publico` medido: o
|
|
204
|
+
* git NÃO o honrou como seleção de remoto". Essa medição tinha um argumento
|
|
205
|
+
* POSICIONAL no caso de teste, que tem precedência e MASCARA o efeito de
|
|
206
|
+
* `--repo`. Sem posicional, o git honra `--repo` — remedido em duas sessões
|
|
207
|
+
* independentes (`pc-qa`, finding `F-1`, e `pc-dev` na correção). Foi essa
|
|
208
|
+
* leitura superada que justificou tratar `--repo` como valor opaco "por
|
|
209
|
+
* precaução", e era justamente ela que abria um push SILENCIOSO para um
|
|
210
|
+
* remoto DECLARADO irreversível — o desfecho que este hook existe para
|
|
211
|
+
* impedir. Nunca reintroduzir "descarta o valor de `--repo`".
|
|
212
|
+
* LIMITE RESIDUAL DECLARADO (mesma régua dos demais limites deste
|
|
213
|
+
* cabeçalho — um mecanismo nunca promete mais do que entrega): uma opção
|
|
214
|
+
* FUTURA/desconhecida do git que tome valor em token separado e não esteja
|
|
215
|
+
* no catálogo pode fazer o parser ler o VALOR como remoto. Dívida NOMEADA,
|
|
216
|
+
* fechada por atualização do catálogo — nunca por um `ask` adivinhado.
|
|
217
|
+
* Também declarado: (b) cobre `scheme://` e `user@host:path`, as 2 formas
|
|
218
|
+
* medidas no ADR-037 §1.3 — um caminho local (`/tmp/x.git`) não é tratado
|
|
219
|
+
* como URL bruta.
|
|
220
|
+
* 3º limite DECLARADO, MEDIDO (não deduzido) na implementação da SF15.12:
|
|
221
|
+
* quando o subcomando é um ALIAS, a expansão (`expandResolvedAliasAndRecurse`)
|
|
222
|
+
* reconstrói o comando como `git <corpo> <trailing>` e NÃO carrega as opções
|
|
223
|
+
* globais que vinham antes do subcomando — logo `git -C <outro-repo> z` (com
|
|
224
|
+
* `z = push`) resolve o upstream do cenário (c) no `cwd` da sessão, não no
|
|
225
|
+
* repositório de `-C`. Medido: a forma DIRETA `git -C <outro-repo> push`
|
|
226
|
+
* responde `ask` para um destino declarado, e a forma via alias responde
|
|
227
|
+
* silêncio para o MESMO destino. Vale só para o cenário (c) — remoto
|
|
228
|
+
* explícito e URL bruta são lidos do texto e não dependem de repositório —
|
|
229
|
+
* e a mesma perda já existe, desde o fix round F3, na CONSULTA do alias
|
|
230
|
+
* (`resolveGitAliasFromDisk` também usa `ctx.cwd`). Não corrigido aqui por
|
|
231
|
+
* estar fora da prescrição revisada desta story (fechá-lo mexe na expansão
|
|
232
|
+
* COMPARTILHADA entre os branches `git` e `gh`, que o F5 unificou de
|
|
233
|
+
* propósito) — dívida NOMEADA, para decisão de arquitetura, nunca uma lacuna
|
|
234
|
+
* silenciosa.
|
|
235
|
+
*
|
|
144
236
|
* Sem dependências externas além do próprio `git`/`gh` (só chamados para
|
|
145
|
-
* resolver alias — roda em qualquer instalação
|
|
146
|
-
* no PATH: nesse caso a consulta falha e o
|
|
237
|
+
* resolver alias e o upstream do cenário (c) — roda em qualquer instalação
|
|
238
|
+
* limpa mesmo sem `git`/`gh` no PATH: nesse caso a consulta falha e o
|
|
239
|
+
* resultado é `ask`, não crash).
|
|
147
240
|
*/
|
|
148
241
|
|
|
149
242
|
const fs = require('fs');
|
|
243
|
+
const path = require('path');
|
|
150
244
|
const { spawnSync } = require('child_process');
|
|
151
245
|
|
|
152
246
|
const DEVOPS_AGENT_TYPE = 'pc-devops';
|
|
153
247
|
|
|
248
|
+
// Arquivo de DECLARAÇÃO dos remotos irreversíveis (story SF15.12). Nunca
|
|
249
|
+
// hardcodar nome de remoto neste fonte: quem instala o framework é o dono da
|
|
250
|
+
// lista, e nomes de remoto são por repositório.
|
|
251
|
+
const IRREVERSIBLE_REMOTES_FILENAME = 'irreversible-remotes.json';
|
|
252
|
+
|
|
154
253
|
// Opções GLOBAIS do git que tomam um valor em token SEPARADO (quando não
|
|
155
254
|
// usadas na forma `--opcao=valor`). Fonte: `git help` (seção OPTIONS).
|
|
156
255
|
const GIT_GLOBAL_OPTS_WITH_ARG = new Set([
|
|
@@ -170,6 +269,21 @@ const GIT_GLOBAL_OPTS_WITH_ARG = new Set([
|
|
|
170
269
|
// gh: opções globais mais comuns que tomam valor em token separado.
|
|
171
270
|
const GH_GLOBAL_OPTS_WITH_ARG = new Set(['-R', '--repo', '--hostname']);
|
|
172
271
|
|
|
272
|
+
// Opções do SUBCOMANDO `push` que tomam um valor em token SEPARADO (quando
|
|
273
|
+
// não usadas na forma `--opcao=valor`) — CATÁLOGO FECHADO (story SF15.12):
|
|
274
|
+
// toda opção FORA deste conjunto é flag SEM valor, que é a gramática real do
|
|
275
|
+
// `git push`. Espelha `GIT_GLOBAL_OPTS_WITH_ARG` na forma e na régua. Fonte:
|
|
276
|
+
// `git push --help` (seção OPTIONS). Limite residual (opção futura com valor
|
|
277
|
+
// separado ausente daqui) DECLARADO no cabeçalho — nunca substituído por
|
|
278
|
+
// heurística de "aparência de tomar valor", que não é decidível no parse.
|
|
279
|
+
const GIT_PUSH_OPTS_WITH_ARG = new Set([
|
|
280
|
+
'-o',
|
|
281
|
+
'--push-option',
|
|
282
|
+
'--repo',
|
|
283
|
+
'--exec',
|
|
284
|
+
'--receive-pack',
|
|
285
|
+
]);
|
|
286
|
+
|
|
173
287
|
// Subcomandos REAIS do git (porcelana + os plumbing mais comuns) — lista de
|
|
174
288
|
// custo, não de correção: um subcomando fora desta lista dispara a
|
|
175
289
|
// resolução de alias (barata primeiro, subprocesso só se preciso); um
|
|
@@ -331,12 +445,31 @@ function isGhToken(tok) {
|
|
|
331
445
|
// `inlineAliases` (Map) for passado, toda opção `-c alias.<nome>=<valor>`
|
|
332
446
|
// encontrada no caminho é registrada nele — fonte MAIS BARATA de resolução
|
|
333
447
|
// de alias, o próprio comando já carrega a definição.
|
|
448
|
+
//
|
|
449
|
+
// Story SF15.12 (`M2`): o valor de `-C`/`--git-dir` deixa de ser DESCARTADO
|
|
450
|
+
// e passa a ser devolvido (`repoCwd`/`gitDir`). Nomes de remoto são por
|
|
451
|
+
// REPOSITÓRIO, então a resolução de upstream do cenário (c) precisa rodar no
|
|
452
|
+
// repositório que o comando de fato aponta, não no `cwd` da sessão. Opção
|
|
453
|
+
// presente e NÃO utilizável (valor ausente/vazio, ou `-C` repetido — que o
|
|
454
|
+
// git trata como caminhos relativos cumulativos, algo que este parser não se
|
|
455
|
+
// propõe a reproduzir) marca `unusableRepoOption`, e o chamador trata isso
|
|
456
|
+
// como "não deu para confirmar", nunca como "não há".
|
|
334
457
|
function findGitSubcommand(tokens, gitIndex, inlineAliases) {
|
|
335
458
|
let i = gitIndex + 1;
|
|
459
|
+
let repoCwd;
|
|
460
|
+
let gitDir;
|
|
461
|
+
let unusableRepoOption = false;
|
|
462
|
+
const finish = (subcommand, index) => ({
|
|
463
|
+
subcommand,
|
|
464
|
+
index,
|
|
465
|
+
repoCwd,
|
|
466
|
+
gitDir,
|
|
467
|
+
unusableRepoOption,
|
|
468
|
+
});
|
|
336
469
|
while (i < tokens.length) {
|
|
337
470
|
const tok = tokens[i];
|
|
338
471
|
if (!tok.startsWith('-')) {
|
|
339
|
-
return
|
|
472
|
+
return finish(tok, i);
|
|
340
473
|
}
|
|
341
474
|
const eqIdx = tok.indexOf('=');
|
|
342
475
|
const base = eqIdx === -1 ? tok : tok.slice(0, eqIdx);
|
|
@@ -355,6 +488,16 @@ function findGitSubcommand(tokens, gitIndex, inlineAliases) {
|
|
|
355
488
|
const aliasMatch = /^alias\.([^=\s]+)=([\s\S]*)$/.exec(value);
|
|
356
489
|
if (aliasMatch) inlineAliases.set(aliasMatch[1], aliasMatch[2]);
|
|
357
490
|
}
|
|
491
|
+
if (base === '-C') {
|
|
492
|
+
// `-C` repetido é cumulativo no git real; aqui vira "não utilizável".
|
|
493
|
+
if (repoCwd !== undefined) unusableRepoOption = true;
|
|
494
|
+
if (typeof value === 'string' && value) repoCwd = value;
|
|
495
|
+
else unusableRepoOption = true;
|
|
496
|
+
}
|
|
497
|
+
if (base === '--git-dir') {
|
|
498
|
+
if (typeof value === 'string' && value) gitDir = value;
|
|
499
|
+
else unusableRepoOption = true;
|
|
500
|
+
}
|
|
358
501
|
continue;
|
|
359
502
|
}
|
|
360
503
|
// Qualquer outra flag (global conhecida sem argumento, ou desconhecida)
|
|
@@ -406,6 +549,200 @@ function isForceFlagToken(tok) {
|
|
|
406
549
|
return false;
|
|
407
550
|
}
|
|
408
551
|
|
|
552
|
+
// Extrai os CANDIDATOS a repositório de destino de um `git push`, a partir
|
|
553
|
+
// dos tokens que SEGUEM o subcomando (a mesma fatia que `isForceFlagToken` já
|
|
554
|
+
// consome — custo zero a mais). São 2, porque o `git push` tem 2 formas de
|
|
555
|
+
// dizer para onde empurrar:
|
|
556
|
+
// - `positional` — o 1º token não-flag (`git push <repositório> <refspec>`);
|
|
557
|
+
// - `repoOption` — o valor de `--repo <v>` / `--repo=<v>`.
|
|
558
|
+
// A varredura respeita o CATÁLOGO FECHADO `GIT_PUSH_OPTS_WITH_ARG`: só opção
|
|
559
|
+
// catalogada, na forma SEM `=`, consome o token seguinte; toda outra opção é
|
|
560
|
+
// flag sem valor. A varredura NÃO para no 1º posicional, porque `--repo` pode
|
|
561
|
+
// vir depois dele (`git push --tags --repo publico`) — quem decide a
|
|
562
|
+
// precedência é o chamador, não a ordem de leitura.
|
|
563
|
+
function extractPushRemoteCandidates(tokensAfterPush) {
|
|
564
|
+
let positional = null;
|
|
565
|
+
let repoOptionPresent = false;
|
|
566
|
+
let repoOptionValue;
|
|
567
|
+
let i = 0;
|
|
568
|
+
while (i < tokensAfterPush.length) {
|
|
569
|
+
const tok = tokensAfterPush[i];
|
|
570
|
+
if (!tok.startsWith('-')) {
|
|
571
|
+
if (positional === null) positional = tok;
|
|
572
|
+
i += 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const eqIdx = tok.indexOf('=');
|
|
576
|
+
const base = eqIdx === -1 ? tok : tok.slice(0, eqIdx);
|
|
577
|
+
// `--opcao=valor` traz o valor no PRÓPRIO token: nunca consome o seguinte.
|
|
578
|
+
if (eqIdx === -1 && GIT_PUSH_OPTS_WITH_ARG.has(base)) {
|
|
579
|
+
if (base === '--repo' && !repoOptionPresent) {
|
|
580
|
+
repoOptionPresent = true;
|
|
581
|
+
repoOptionValue = tokensAfterPush[i + 1];
|
|
582
|
+
}
|
|
583
|
+
i += 2;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (eqIdx !== -1 && base === '--repo' && !repoOptionPresent) {
|
|
587
|
+
repoOptionPresent = true;
|
|
588
|
+
repoOptionValue = tok.slice(eqIdx + 1);
|
|
589
|
+
}
|
|
590
|
+
i += 1;
|
|
591
|
+
}
|
|
592
|
+
return { positional, repoOptionPresent, repoOptionValue };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Cenário (b): o token de destino é uma URL/SSH-spec BRUTA, não um NOME de
|
|
596
|
+
// remoto configurado. 2 formas, as medidas no ADR-037 §1.3: `scheme://...` e
|
|
597
|
+
// o scp-like `user@host:path`. Caminho local (`/tmp/x.git`) NÃO é coberto —
|
|
598
|
+
// limite declarado no cabeçalho, não omissão silenciosa.
|
|
599
|
+
function looksLikeRawRemoteUrl(token) {
|
|
600
|
+
if (typeof token !== 'string' || !token) return false;
|
|
601
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(token)) return true;
|
|
602
|
+
if (/^[^/@:]+@[^/@:]+:/.test(token)) return true;
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Lê UM lado da união (story SF15.12). Trata por CLASSE, nunca fundindo
|
|
607
|
+
// "ausente" com "ilegível" (`M1`):
|
|
608
|
+
// { status: 'absent' } — não existe ali: resposta CONCLUSIVA
|
|
609
|
+
// ("nada declarado neste lado").
|
|
610
|
+
// { status: 'ok', remotes: Set } — lido e bem formado.
|
|
611
|
+
// { status: 'malformed' } — presente e ilegível (JSON quebrado,
|
|
612
|
+
// `remotes` não-array, entrada
|
|
613
|
+
// não-string, leitura impossível) —
|
|
614
|
+
// "não sei", nunca "não há".
|
|
615
|
+
function readIrreversibleRemotesFile(filePath) {
|
|
616
|
+
let raw;
|
|
617
|
+
try {
|
|
618
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
619
|
+
} catch (err) {
|
|
620
|
+
const code = err && err.code;
|
|
621
|
+
// ENOENT/ENOTDIR = o arquivo não está lá (conclusivo). Qualquer outro
|
|
622
|
+
// erro (permissão, EISDIR, ...) é "existe algo que não consegui ler".
|
|
623
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return { status: 'absent' };
|
|
624
|
+
return { status: 'malformed' };
|
|
625
|
+
}
|
|
626
|
+
let parsed;
|
|
627
|
+
try {
|
|
628
|
+
parsed = JSON.parse(raw);
|
|
629
|
+
} catch {
|
|
630
|
+
return { status: 'malformed' };
|
|
631
|
+
}
|
|
632
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
633
|
+
return { status: 'malformed' };
|
|
634
|
+
}
|
|
635
|
+
if (!Array.isArray(parsed.remotes)) return { status: 'malformed' };
|
|
636
|
+
if (!parsed.remotes.every((entry) => typeof entry === 'string')) {
|
|
637
|
+
return { status: 'malformed' };
|
|
638
|
+
}
|
|
639
|
+
return { status: 'ok', remotes: new Set(parsed.remotes) };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// UNIÃO dos 2 lados: o arquivo que viaja junto do hook (`__dirname`) e o da
|
|
643
|
+
// instalação viva cuja sessão executa o push (`input.cwd`). A união só
|
|
644
|
+
// ALARGA o conjunto de `ask` — nunca substitui um lado pelo outro, nunca
|
|
645
|
+
// interseciona. Lado ausente não contribui; lado malformado contamina o
|
|
646
|
+
// resultado inteiro com `malformed` (fail-loud).
|
|
647
|
+
function loadIrreversibleRemotes(cwd) {
|
|
648
|
+
const candidates = [path.join(__dirname, IRREVERSIBLE_REMOTES_FILENAME)];
|
|
649
|
+
if (typeof cwd === 'string' && cwd) {
|
|
650
|
+
candidates.push(path.join(cwd, '.claude', 'hooks', IRREVERSIBLE_REMOTES_FILENAME));
|
|
651
|
+
}
|
|
652
|
+
const remotes = new Set();
|
|
653
|
+
let malformed = false;
|
|
654
|
+
for (const candidate of candidates) {
|
|
655
|
+
const side = readIrreversibleRemotesFile(candidate);
|
|
656
|
+
if (side.status === 'ok') {
|
|
657
|
+
for (const name of side.remotes) remotes.add(name);
|
|
658
|
+
} else if (side.status === 'malformed') {
|
|
659
|
+
malformed = true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { remotes, malformed };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Resolve o remoto do UPSTREAM da branch corrente (cenário (c)), no
|
|
666
|
+
// repositório que o comando de fato aponta (`-C`/`--git-dir`, `M2`), com o
|
|
667
|
+
// mesmo padrão de subprocesso já usado na resolução de alias (timeout curto,
|
|
668
|
+
// stdin fechado, try/catch). `@{u}` sai como `<remoto>/<branch>`; o nome do
|
|
669
|
+
// remoto é o 1º segmento.
|
|
670
|
+
// { status: 'found', remote } — resolvido com segurança.
|
|
671
|
+
// { status: 'error' } — sem upstream, fora de repositório, git
|
|
672
|
+
// ausente, timeout: "não deu para confirmar".
|
|
673
|
+
function resolveUpstreamRemote(cwd, gitDir) {
|
|
674
|
+
try {
|
|
675
|
+
const args = [];
|
|
676
|
+
if (typeof gitDir === 'string' && gitDir) args.push(`--git-dir=${gitDir}`);
|
|
677
|
+
args.push('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}');
|
|
678
|
+
const result = spawnSync('git', args, {
|
|
679
|
+
cwd: cwd || process.cwd(),
|
|
680
|
+
encoding: 'utf8',
|
|
681
|
+
timeout: 800,
|
|
682
|
+
input: '',
|
|
683
|
+
});
|
|
684
|
+
if (result.error || result.status !== 0) return { status: 'error' };
|
|
685
|
+
const value = (result.stdout || '').trim();
|
|
686
|
+
const slash = value.indexOf('/');
|
|
687
|
+
if (slash <= 0) return { status: 'error' };
|
|
688
|
+
return { status: 'found', remote: value.slice(0, slash) };
|
|
689
|
+
} catch {
|
|
690
|
+
return { status: 'error' };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// A lista é lida NO MÁXIMO 1 vez por invocação inteira (cacheada em `ctx`,
|
|
695
|
+
// mesma disciplina de custo de `ctx.ghAliasCache`), e só depois de um
|
|
696
|
+
// segmento ser reconhecido como `git push` não-force (`M4`).
|
|
697
|
+
function getIrreversibleRemotes(ctx) {
|
|
698
|
+
if (!ctx.irreversibleRemotesCache) {
|
|
699
|
+
ctx.irreversibleRemotesCache = loadIrreversibleRemotes(ctx.cwd);
|
|
700
|
+
}
|
|
701
|
+
return ctx.irreversibleRemotesCache;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// O predicado do `I4`: este segmento de `git push` (já sabidamente não-force)
|
|
705
|
+
// tem destino irreversível ou não-confirmável? Ver os cenários (a)/(b)/(c) no
|
|
706
|
+
// cabeçalho. Toda dúvida resolve para `true` — "não sei" nunca vira "sei que
|
|
707
|
+
// não é", a mesma doutrina do `unresolved-alias`.
|
|
708
|
+
function isPushToIrreversibleRemote(tokensAfterPush, found, ctx) {
|
|
709
|
+
const declared = getIrreversibleRemotes(ctx);
|
|
710
|
+
if (declared.malformed) return true; // `M1`: fail-loud, nunca silêncio
|
|
711
|
+
|
|
712
|
+
const { positional, repoOptionPresent, repoOptionValue } = extractPushRemoteCandidates(tokensAfterPush);
|
|
713
|
+
|
|
714
|
+
// PRECEDÊNCIA, MEDIDA contra o git real (2.50.1), nunca deduzida — ver o
|
|
715
|
+
// cabeçalho: o argumento POSICIONAL vence `--repo`. `git push --repo publico
|
|
716
|
+
// main` faz o git tratar `main` como repositório (`fatal: 'main' does not
|
|
717
|
+
// appear to be a git repository`), e `git push --repo publico origin main`
|
|
718
|
+
// alcança `origin`. Só quando NÃO há posicional é que `--repo` seleciona o
|
|
719
|
+
// destino de fato — e aí ele precisa receber (a)/(b) como qualquer remoto.
|
|
720
|
+
let remoteToken = positional;
|
|
721
|
+
if (remoteToken === null && repoOptionPresent) {
|
|
722
|
+
// `--repo` presente mas sem valor utilizável (ausente, vazio, ou outra
|
|
723
|
+
// flag no lugar do valor) é "não sei para onde isto vai", nunca "não há".
|
|
724
|
+
if (typeof repoOptionValue !== 'string' || !repoOptionValue || repoOptionValue.startsWith('-')) {
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
remoteToken = repoOptionValue;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (remoteToken === null) {
|
|
731
|
+
// (c) nenhum argumento de remoto explícito: só o upstream pode dizer
|
|
732
|
+
// para onde isto vai.
|
|
733
|
+
if (found.unusableRepoOption) return true;
|
|
734
|
+
const repoCwd = found.repoCwd
|
|
735
|
+
? path.resolve(ctx.cwd || process.cwd(), found.repoCwd)
|
|
736
|
+
: ctx.cwd;
|
|
737
|
+
const upstream = resolveUpstreamRemote(repoCwd, found.gitDir);
|
|
738
|
+
if (upstream.status !== 'found') return true;
|
|
739
|
+
return declared.remotes.has(upstream.remote);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (looksLikeRawRemoteUrl(remoteToken)) return true; // (b)
|
|
743
|
+
return declared.remotes.has(remoteToken); // (a) — a LISTA decide
|
|
744
|
+
}
|
|
745
|
+
|
|
409
746
|
// Consulta `git config --get alias.<nome>` no repositório de `cwd` — a
|
|
410
747
|
// fonte MAIS CARA de resolução de alias do `git` (subprocesso síncrono),
|
|
411
748
|
// só chamada quando as 2 fontes baratas (inline `-c`, `git config alias.X`
|
|
@@ -647,8 +984,19 @@ function analyzeSegment(segment, ctx, depth) {
|
|
|
647
984
|
const found = findGitSubcommand(tokens, gitIndex, inlineAliases);
|
|
648
985
|
if (found) {
|
|
649
986
|
if (found.subcommand === 'push') {
|
|
650
|
-
const
|
|
651
|
-
return { kind:
|
|
987
|
+
const afterPush = tokens.slice(found.index + 1);
|
|
988
|
+
if (afterPush.some(isForceFlagToken)) return { kind: 'force-push' };
|
|
989
|
+
// Story SF15.12 (`B2`): o destino irreversível vira um `kind` PRÓPRIO
|
|
990
|
+
// AQUI, no momento em que o SEGMENTO é reconhecido — nunca um campo
|
|
991
|
+
// lido depois da agregação por severidade, que devolve UMA operação e
|
|
992
|
+
// faria o 2º push de uma cadeia (`git push origin main && git push
|
|
993
|
+
// publico main`) simplesmente sumir. `M4`: é este o único ponto em que
|
|
994
|
+
// a lista é lida e o `git rev-parse` pode ser chamado.
|
|
995
|
+
return {
|
|
996
|
+
kind: isPushToIrreversibleRemote(afterPush, found, ctx)
|
|
997
|
+
? 'git-push-irreversible'
|
|
998
|
+
: 'git-push',
|
|
999
|
+
};
|
|
652
1000
|
}
|
|
653
1001
|
if (!KNOWN_GIT_SUBCOMMANDS.has(found.subcommand)) {
|
|
654
1002
|
const trailing = tokens.slice(found.index + 1);
|
|
@@ -712,8 +1060,16 @@ function analyzeSegment(segment, ctx, depth) {
|
|
|
712
1060
|
// (force-push > merge > alias não resolvido > push/pr create), para não
|
|
713
1061
|
// deixar passar em silêncio um comando composto que contenha uma operação
|
|
714
1062
|
// governada em qualquer posição da cadeia.
|
|
1063
|
+
// `git-push-irreversible` = 3.5: estritamente ENTRE `git-push` (1) e
|
|
1064
|
+
// `force-push` (4), e acima de `unresolved-alias` (2) e `gh-pr-merge` (3) —
|
|
1065
|
+
// assim, numa cadeia, o destino irreversível nunca perde para uma operação
|
|
1066
|
+
// menos severa, e SEMPRE perde para force-push (o `ask` novo jamais rebaixa
|
|
1067
|
+
// um `deny`, por construção da ordem). Fracionário de propósito: um inteiro
|
|
1068
|
+
// exigiria renumerar as 4 entradas existentes, com risco de regressão, sem
|
|
1069
|
+
// necessidade — `SEVERITY` é comparada por `>`, não por adjacência.
|
|
715
1070
|
const SEVERITY = {
|
|
716
1071
|
'force-push': 4,
|
|
1072
|
+
'git-push-irreversible': 3.5,
|
|
717
1073
|
'gh-pr-merge': 3,
|
|
718
1074
|
'unresolved-alias': 2,
|
|
719
1075
|
'git-push': 1,
|
|
@@ -727,6 +1083,9 @@ function findGovernedOperation(command, cwd) {
|
|
|
727
1083
|
gitSessionAliases: collectSessionAliases(segments),
|
|
728
1084
|
ghSessionAliases: collectGhSessionAliases(segments),
|
|
729
1085
|
ghAliasCache: { loaded: false, map: null, failed: false },
|
|
1086
|
+
// Preenchido sob demanda por `getIrreversibleRemotes` — nunca aqui, para
|
|
1087
|
+
// que nenhum `Bash` que não contenha `git push` pague leitura de arquivo.
|
|
1088
|
+
irreversibleRemotesCache: null,
|
|
730
1089
|
};
|
|
731
1090
|
let best = null;
|
|
732
1091
|
for (const segment of segments) {
|
|
@@ -778,6 +1137,19 @@ function decideAndEmit(operation, input) {
|
|
|
778
1137
|
return;
|
|
779
1138
|
}
|
|
780
1139
|
|
|
1140
|
+
if (operation.kind === 'git-push-irreversible') {
|
|
1141
|
+
// Story SF15.12 (`I4` do ADR-037): destino declarado irreversível, URL
|
|
1142
|
+
// bruta, ou destino que não deu para confirmar. `ask` INCONDICIONAL —
|
|
1143
|
+
// inclusive para `pc-devops`, mesma forma que `gh pr merge` já tem. O
|
|
1144
|
+
// `allow` silencioso de `git push` foi calibrado para um remoto privado e
|
|
1145
|
+
// recuperável; um remoto público não o herda.
|
|
1146
|
+
emitDecision(
|
|
1147
|
+
'ask',
|
|
1148
|
+
`O destino deste push é um remoto declarado irreversível, uma URL bruta, ou um destino que não foi possível confirmar (nenhum remoto explícito e upstream irresolúvel, ou lista de remotos ilegível). Aprovação humana explícita é necessária, inclusive para o papel devops. Agente atual: ${agentLabel(input)}.`,
|
|
1149
|
+
);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
781
1153
|
if (operation.kind === 'unresolved-alias') {
|
|
782
1154
|
// Subcomando de git OU gh desconhecido que PODE ser alias — ou a
|
|
783
1155
|
// consulta (`git config --get`/`gh alias list`) não pôde ser concluída
|
|
@@ -900,7 +1272,15 @@ module.exports = {
|
|
|
900
1272
|
DEVOPS_AGENT_TYPE,
|
|
901
1273
|
KNOWN_GIT_SUBCOMMANDS,
|
|
902
1274
|
KNOWN_GH_SUBCOMMANDS,
|
|
1275
|
+
GIT_PUSH_OPTS_WITH_ARG,
|
|
1276
|
+
IRREVERSIBLE_REMOTES_FILENAME,
|
|
903
1277
|
ALIAS_MAX_DEPTH,
|
|
1278
|
+
extractPushRemoteCandidates,
|
|
1279
|
+
looksLikeRawRemoteUrl,
|
|
1280
|
+
readIrreversibleRemotesFile,
|
|
1281
|
+
loadIrreversibleRemotes,
|
|
1282
|
+
resolveUpstreamRemote,
|
|
1283
|
+
isPushToIrreversibleRemote,
|
|
904
1284
|
tokenize,
|
|
905
1285
|
splitSegments,
|
|
906
1286
|
isGitToken,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"remotes": []}
|
|
@@ -43,7 +43,7 @@ Um gatilho pode ser **condicional a outro papel**: ex.: architect aciona só se
|
|
|
43
43
|
1. Paths (e linhas, quando relevante) exatos dos arquivos envolvidos.
|
|
44
44
|
2. ACs da story e critérios de aceite verificáveis.
|
|
45
45
|
3. Limites de autoridade do papel (ex.: dev não faz push; QA não implementa fixes). **Toda restrição ALÉM da autoridade padrão do papel cita a LINHA exata de `.claude/rules/authority.md` que a sustenta** (`authority.md:{linha}`, com a frase da linha); não havendo nenhuma, o campo escreve verbatim "nenhuma restrição além da autoridade padrão do papel" (`DF-SF13.3-1`). **Restrição sem âncora não vale** — o papel obedece a um brief errado sem ter como contestá-lo. Origem: `FINDING-5` do gate `SF13.11`, onde o brief mandou o Dev "não commitar" e `authority.md:10` não sustentava a restrição — a linha lista "commits locais" na coluna "Autoridade exclusiva" do Dev, e o que ela bloqueia na coluna "Proibido" é `git push`, não o commit —, e o resultado foi entregável fora do git.
|
|
46
|
-
4. O que retornar — e a instrução explícita: "relatório final ≤15 linhas; detalhes vão na story". **E a instrução explícita de commitar o artefato exclusivo do papel antes de fechar o turno** — gate file do QA, `Status`/`File List` do Dev, transição `Ready` do PO: o papel roda `git status --short`, não deixa nada seu *untracked*, commita ele mesmo e reporta o sha do commit (`DF-SF13.3-2`). Qual é o artefato exclusivo de cada papel se lê na coluna "Autoridade exclusiva" de `authority.md:9-15`; para o gate file, em `authority.md:17` ("só QA escreve `docs/qa/gates/*.yaml`"). Commitar ANTES de encerrar é do próprio papel, nunca de quem vier depois: o artefato deixado para trás é varrido, sem crédito, para dentro do commit de outro papel. Origem: 3ª ocorrência de "trabalho do papel não commitado" (`docs/stories/sf13.7.story.md`, Change Log `v1.5`) — o gate file da 1ª passada do QA ficou *untracked* e entrou no repo dentro de `40c48ae`, o commit do fix round do `pc-planner`. **E nenhum turno termina "aguardando" um processo em background que ele mesmo lançou** —
|
|
46
|
+
4. O que retornar — e a instrução explícita: "relatório final ≤15 linhas; detalhes vão na story". **E a instrução explícita de commitar o artefato exclusivo do papel antes de fechar o turno** — gate file do QA, `Status`/`File List` do Dev, transição `Ready` do PO: o papel roda `git status --short`, não deixa nada seu *untracked*, commita ele mesmo e reporta o sha do commit (`DF-SF13.3-2`). Qual é o artefato exclusivo de cada papel se lê na coluna "Autoridade exclusiva" de `authority.md:9-15`; para o gate file, em `authority.md:17` ("só QA escreve `docs/qa/gates/*.yaml`"). Commitar ANTES de encerrar é do próprio papel, nunca de quem vier depois: o artefato deixado para trás é varrido, sem crédito, para dentro do commit de outro papel. Origem: 3ª ocorrência de "trabalho do papel não commitado" (`docs/stories/sf13.7.story.md`, Change Log `v1.5`) — o gate file da 1ª passada do QA ficou *untracked* e entrou no repo dentro de `40c48ae`, o commit do fix round do `pc-planner`. **E nenhum turno termina "aguardando" um processo em background que ele mesmo lançou** — o processo em si PODE sobreviver ao fim do turno; o que **não sobrevive ao fim do turno** é a COLHEITA: a capacidade do agente de ler, verificar e reportar o resultado. O que não for colhido DENTRO do mesmo turno se perde — inclusive um trabalho que terminou verde e que ninguém viu —, e o brief nunca instrui um papel a esperar por ele (`DF-SF13.4-1`). **Leitura superada, não repetir:** a formulação anterior desta cláusula ("background não sobrevive ao fim do turno que o lançou") afirmava mais do que a fonte citada sustenta, e a própria fonte prova o contrário — em `docs/stories/release-0.16.1.story.md` (Dev Agent Record → Desvios registrados) a suíte lançada em background "continuou rodando o tempo todo e terminou verde"; o que morreu com o turno foi o monitor que iria colhê-la (corrigido reexecutando em foreground). A regra prática não muda: não se espera por background: colhe-se dentro do turno, ou não se lança. Origem: 3 ocorrências da tabela do dono ("agente encerra esperando background que morreu com ele", `docs/stories/epic-sf13-processo-a-prova-de-falhas.md:55`) + a ocorrência viva acima; correção da cláusula pela story `chore-miudos-pos-sf13` (`FINDING-1` do gate `SF13.4`, registrado no épico `:171`).
|
|
47
47
|
5. **Todo número/afirmação vem com o método de medição** (ver disciplina abaixo) — sem isso o subagente não consegue falsificar, e erro da coordenadora vira premissa.
|
|
48
48
|
|
|
49
49
|
### Esqueleto do brief (`DF-SF13.3-3`)
|
|
@@ -115,6 +115,7 @@ Nasceu de **7 erros da mesma classe numa única sessão** (2026-08-06/07), todos
|
|
|
115
115
|
5. **Usuário testando a working tree principal:** rode **dev e QA em worktrees isoladas** (`isolation: worktree`), QA em porta alternativa, merge só no fim — zero interferência no ambiente do usuário. Como a branch fica travada na worktree do dev, o QA commita o filho direto e a **coordenadora** avança a branch com `git -C <worktree-dev> merge --ff-only`.
|
|
116
116
|
6. **Antes de qualquer commit/escrita de estado próprio, a coordenadora confere `git branch --show-current` E o cwd.** Checkout residual de uma publicação anterior já fez a coordenadora commitar em branch errada (2 incidentes reais).
|
|
117
117
|
7. **Em toda missão `*release`, a coordenadora revisa CHANGELOG e versão ANTES de apresentar o PR ao dono — nunca depois, nunca em paralelo.** O incidente da `0.16.0` (CHANGELOG apresentado como pronto e corrigido em paralelo com o dono já revisando) teve causa-raiz na ordem de operação da coordenadora, não na ausência de um gate de QA formal para `*release` — por isso a correção é de prática de orquestração, não de missão nova do `pc-devops`.
|
|
118
|
+
8. **Duas stories na mesma branch: a 2ª a ser verificada reconfere as âncoras de linha que cita** em arquivos já tocados pela 1ª — **identidade de CONTEÚDO não implica identidade de POSIÇÃO**, e um `path:linha` correto quando a story foi escrita já aponta para outra linha depois do commit da irmã. Agrupar é decisão legítima de orquestração, mas tem esse custo: quem agrupa nomeia no brief os arquivos que as duas dividem, e a 2ª reconfere antes de citar. Origem: `FINDING-2` do gate `SF13.6` (`docs/qa/gates/sf13.6-fechamento-por-declaracao.yaml`), onde um mesmo agrupamento produziu 2 defeitos de classes diferentes — um AC que media `main...HEAD` passou a medir trabalho alheio (nomeado pelo dev) e as âncoras de linha da story irmã envelheceram (não mapeado por ninguém).
|
|
118
119
|
|
|
119
120
|
## Memória de papel — o caminho é curadoria humana, não pipeline (`DF-SF12.3-7`)
|
|
120
121
|
|