@fontdata/ai-harness 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -0
- package/dist/install/plan.js +9 -5
- package/dist/install/settings.js +54 -13
- package/dist/update/update.js +13 -4
- package/package.json +1 -1
- package/templates/commands/git-commit-push.md +21 -2
package/README.md
CHANGED
|
@@ -103,6 +103,21 @@ npx @fontdata/ai-harness init --comandos resolver-bug,nova-tarefa --sem-harness
|
|
|
103
103
|
Com o plugin `fontdata@ai-harness` habilitado, o `init` não instala os hooks no projeto, para eles não rodarem
|
|
104
104
|
duas vezes.
|
|
105
105
|
|
|
106
|
+
### Permissões para o modo auto
|
|
107
|
+
|
|
108
|
+
O `init` e o `update` gravam em `.claude/settings.json` as permissões que os fluxos usam, para rodarem sem pedir
|
|
109
|
+
aprovação, inclusive no modo auto do Claude Code:
|
|
110
|
+
|
|
111
|
+
- `permissions.allow`: o rastro, a checagem de conflitos, o `doctor` e a `memoria` do harness; `git add`,
|
|
112
|
+
`git commit` e `git push` (o `/git-commit-push`); a abertura de sub-agents (`Agent`); e consultas só de leitura
|
|
113
|
+
(`git status`, `diff`, `log`, `show`, `blame`, `gh pr view`, `gh pr diff`, `npm outdated`...);
|
|
114
|
+
- `permissions.deny`: o push forçado (`--force`, `-f`, `--force-with-lease` e refspec com `+`), que vence o `allow`.
|
|
115
|
+
|
|
116
|
+
Ficam de fora os comandos de teste e build, que variam por projeto, e os que regravam a instalação (`init`,
|
|
117
|
+
`update`, `migrar`). O projeto acrescenta os seus no próprio `settings.json`; o harness nunca remove regras.
|
|
118
|
+
Nos modos 1 e 2 nada é gravado no projeto: rode `npx @fontdata/ai-harness update --yes` num projeto já instalado,
|
|
119
|
+
ou `init`, para receber as regras.
|
|
120
|
+
|
|
106
121
|
## Subcomandos do CLI
|
|
107
122
|
|
|
108
123
|
| Subcomando | O que faz |
|
package/dist/install/plan.js
CHANGED
|
@@ -7,7 +7,7 @@ import { buildLockItem, LOCK_VERSION, readLock, writeLock } from './lock.js';
|
|
|
7
7
|
import { HOOKS_ITEM_ID, PROJECT_HOOKS_DIR, projectHooksConfig } from './hooks.js';
|
|
8
8
|
import { neededSkillsFor, resolveSelection } from './selection.js';
|
|
9
9
|
import { COMMENTS_RULE_PATH, commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
|
|
10
|
-
import { combineHooks, HARNESS_PERMISSIONS, mergeSettings } from './settings.js';
|
|
10
|
+
import { combineHooks, HARNESS_DENY, HARNESS_PERMISSIONS, mergeSettings } from './settings.js';
|
|
11
11
|
/** Rule de convenção com o que é específico do projeto (D-38): é do projeto, então fica fora do lock. */
|
|
12
12
|
export const PROJECT_RULE_PATH = '.claude/rules/harness-projeto.md';
|
|
13
13
|
/** Esqueleto da rule de projeto; os comandos `-legado` renomeados pelo update entram na última seção. */
|
|
@@ -161,8 +161,9 @@ export function executePlan(root, plan, pkg, now = new Date()) {
|
|
|
161
161
|
applyAtomically(root, plan.files);
|
|
162
162
|
const permission = needsEventPermission(plan);
|
|
163
163
|
const hooks = combineHooks(plan.registerHooks ? projectHooksConfig() : {}, plan.registerComments ? commentsHooksConfig() : {});
|
|
164
|
-
if (Object.keys(hooks).length || permission)
|
|
165
|
-
mergeSettings(root, { hooks, allow: permission ? HARNESS_PERMISSIONS : [] }, now);
|
|
164
|
+
if (Object.keys(hooks).length || permission) {
|
|
165
|
+
mergeSettings(root, { hooks, allow: permission ? HARNESS_PERMISSIONS : [], deny: permission ? HARNESS_DENY : [] }, now);
|
|
166
|
+
}
|
|
166
167
|
if (plan.registerComments)
|
|
167
168
|
ensureCommentsBaseline(root);
|
|
168
169
|
const items = new Map();
|
|
@@ -200,8 +201,11 @@ export function describePlan(plan) {
|
|
|
200
201
|
lines.push('O CLAUDE.md atual será copiado para CLAUDE.md.bak.');
|
|
201
202
|
if (plan.registerHooks)
|
|
202
203
|
lines.push('Hooks do harness registrados em .claude/settings.json (sem duplicar os existentes).');
|
|
203
|
-
if (needsEventPermission(plan))
|
|
204
|
-
lines.push(`Permissões ${HARNESS_PERMISSIONS.join('
|
|
204
|
+
if (needsEventPermission(plan)) {
|
|
205
|
+
lines.push(`Permissões ${HARNESS_PERMISSIONS.join(', ')} em .claude/settings.json, para o rastro, a checagem de conflitos, ` +
|
|
206
|
+
'o /git-commit-push, os sub-agents e as consultas de leitura não pedirem aprovação (inclusive no modo auto); ' +
|
|
207
|
+
'o push forçado fica bloqueado em permissions.deny.');
|
|
208
|
+
}
|
|
205
209
|
if (plan.registerComments) {
|
|
206
210
|
lines.push('Padrão de comentários: rule carregada em toda sessão, hook que bloqueia a violação e catraca do legado (gerada só na primeira instalação).');
|
|
207
211
|
}
|
package/dist/install/settings.js
CHANGED
|
@@ -5,8 +5,43 @@ export const SETTINGS_PATH = join('.claude', 'settings.json');
|
|
|
5
5
|
export const EVENT_PERMISSION = 'Bash(npx @fontdata/ai-harness evento:*)';
|
|
6
6
|
/** O /executar-plan checa (e aguarda) conflito de arquivos com outros trabalhos por este comando. */
|
|
7
7
|
export const CONFLICTS_PERMISSION = 'Bash(npx @fontdata/ai-harness conflitos:*)';
|
|
8
|
+
/** O /git-commit-push roda sem confirmação, o que inclui o modo auto do Claude Code. */
|
|
9
|
+
export const GIT_PERMISSIONS = ['Bash(git add:*)', 'Bash(git commit:*)', 'Bash(git push:*)'];
|
|
10
|
+
/** O /executar-plan e as skills de fluxo delegam a sub-agents a cada task e gate. */
|
|
11
|
+
export const AGENT_PERMISSION = 'Agent';
|
|
12
|
+
/**
|
|
13
|
+
* Comandos só de leitura que as skills, os comandos e os agents rodam. Ficam de fora os que regravam a instalação
|
|
14
|
+
* (`init`, `update`, `migrar`), os de teste e build, que variam por projeto, e o `npm audit`, cujo prefixo
|
|
15
|
+
* liberaria também o `npm audit fix`.
|
|
16
|
+
*/
|
|
17
|
+
export const READ_PERMISSIONS = [
|
|
18
|
+
'Bash(npx @fontdata/ai-harness doctor:*)',
|
|
19
|
+
'Bash(npx @fontdata/ai-harness memoria:*)',
|
|
20
|
+
'Bash(git status:*)',
|
|
21
|
+
'Bash(git diff:*)',
|
|
22
|
+
'Bash(git log:*)',
|
|
23
|
+
'Bash(git show:*)',
|
|
24
|
+
'Bash(git blame:*)',
|
|
25
|
+
'Bash(git describe:*)',
|
|
26
|
+
'Bash(git rev-list:*)',
|
|
27
|
+
'Bash(git rev-parse:*)',
|
|
28
|
+
'Bash(git remote -v)',
|
|
29
|
+
'Bash(git symbolic-ref --short refs/remotes/origin/HEAD)',
|
|
30
|
+
'Bash(gh pr view:*)',
|
|
31
|
+
'Bash(gh pr diff:*)',
|
|
32
|
+
'Bash(npm outdated:*)',
|
|
33
|
+
];
|
|
8
34
|
/** Regras de `permissions.allow` que o init e o update gravam para as skills e os agents do harness. */
|
|
9
|
-
export const HARNESS_PERMISSIONS = [EVENT_PERMISSION, CONFLICTS_PERMISSION];
|
|
35
|
+
export const HARNESS_PERMISSIONS = [EVENT_PERMISSION, CONFLICTS_PERMISSION, ...GIT_PERMISSIONS, AGENT_PERMISSION, ...READ_PERMISSIONS];
|
|
36
|
+
/** O push liberado não vale para reescrever o histórico remoto: `deny` vence o `allow`. */
|
|
37
|
+
export const HARNESS_DENY = [
|
|
38
|
+
'Bash(git push --force:*)',
|
|
39
|
+
'Bash(git push *--force*)',
|
|
40
|
+
'Bash(git push -f:*)',
|
|
41
|
+
'Bash(git push * -f)',
|
|
42
|
+
'Bash(git push * -f *)',
|
|
43
|
+
'Bash(git push * +*)',
|
|
44
|
+
];
|
|
10
45
|
function isPlainObject(value) {
|
|
11
46
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
47
|
}
|
|
@@ -24,12 +59,12 @@ export function combineHooks(...configs) {
|
|
|
24
59
|
}
|
|
25
60
|
return combined;
|
|
26
61
|
}
|
|
27
|
-
/** A regra já está em `permissions.allow` do `.claude/settings.json` do projeto? */
|
|
28
|
-
export function hasAllowRule(root, rule) {
|
|
62
|
+
/** A regra já está em `permissions.allow` (ou na lista pedida) do `.claude/settings.json` do projeto? */
|
|
63
|
+
export function hasAllowRule(root, rule, key = 'allow') {
|
|
29
64
|
try {
|
|
30
65
|
const settings = JSON.parse(readFileSync(join(root, SETTINGS_PATH), 'utf8'));
|
|
31
|
-
const
|
|
32
|
-
return Array.isArray(
|
|
66
|
+
const list = settings.permissions?.[key];
|
|
67
|
+
return Array.isArray(list) && list.includes(rule);
|
|
33
68
|
}
|
|
34
69
|
catch {
|
|
35
70
|
return false;
|
|
@@ -60,7 +95,11 @@ export function mergeSettings(root, additions, now = new Date()) {
|
|
|
60
95
|
const invalid = !isPlainObject(parsed) ||
|
|
61
96
|
(parsed.hooks !== undefined && !isPlainObject(parsed.hooks)) ||
|
|
62
97
|
(parsed.permissions !== undefined && !isPlainObject(parsed.permissions)) ||
|
|
63
|
-
(isPlainObject(parsed.permissions) &&
|
|
98
|
+
(isPlainObject(parsed.permissions) &&
|
|
99
|
+
['allow', 'deny'].some((key) => {
|
|
100
|
+
const list = parsed.permissions[key];
|
|
101
|
+
return list !== undefined && !Array.isArray(list);
|
|
102
|
+
}));
|
|
64
103
|
if (invalid) {
|
|
65
104
|
throw new Error(`O ${SETTINGS_PATH} está com JSON inválido; corrija-o antes de instalar os hooks.`);
|
|
66
105
|
}
|
|
@@ -87,17 +126,19 @@ export function mergeSettings(root, additions, now = new Date()) {
|
|
|
87
126
|
}
|
|
88
127
|
next.hooks = merged;
|
|
89
128
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
for (const key of ['allow', 'deny']) {
|
|
130
|
+
const rules = additions[key] ?? [];
|
|
131
|
+
if (!rules.length)
|
|
132
|
+
continue;
|
|
133
|
+
const permissions = { ...(next.permissions ?? {}) };
|
|
134
|
+
const list = [...(permissions[key] ?? [])];
|
|
94
135
|
for (const rule of rules) {
|
|
95
|
-
if (
|
|
136
|
+
if (list.includes(rule))
|
|
96
137
|
continue;
|
|
97
|
-
|
|
138
|
+
list.push(rule);
|
|
98
139
|
changed = true;
|
|
99
140
|
}
|
|
100
|
-
next.permissions = { ...permissions,
|
|
141
|
+
next.permissions = { ...permissions, [key]: list };
|
|
101
142
|
}
|
|
102
143
|
if (!changed)
|
|
103
144
|
return { changed: false, backup: null };
|
package/dist/update/update.js
CHANGED
|
@@ -9,7 +9,7 @@ import { HOOKS_ITEM_ID, isHarnessPluginEnabled, projectHooksConfig } from '../in
|
|
|
9
9
|
import { hashContent, LOCK_PATH, LOCK_VERSION, readLock, writeLock } from '../install/lock.js';
|
|
10
10
|
import { itemSources, packageInfo, PROJECT_RULE_PATH, projectRuleContent } from '../install/plan.js';
|
|
11
11
|
import { neededSkillsFor } from '../install/selection.js';
|
|
12
|
-
import { combineHooks, HARNESS_PERMISSIONS, hasAllowRule, mergeSettings } from '../install/settings.js';
|
|
12
|
+
import { combineHooks, HARNESS_DENY, HARNESS_PERMISSIONS, hasAllowRule, mergeSettings } from '../install/settings.js';
|
|
13
13
|
import { adoptLegacyInstall } from './legado.js';
|
|
14
14
|
/** `<id>-legado.md`, com sufixo numérico quando o nome já está em uso. */
|
|
15
15
|
function legacyPath(root, path) {
|
|
@@ -108,6 +108,10 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
|
108
108
|
addItem('rule', 'comentarios', true);
|
|
109
109
|
addItem('hook', 'comentarios', true);
|
|
110
110
|
}
|
|
111
|
+
const permissoesFaltando = [
|
|
112
|
+
...HARNESS_PERMISSIONS.filter((rule) => !hasAllowRule(root, rule)),
|
|
113
|
+
...HARNESS_DENY.filter((rule) => !hasAllowRule(root, rule, 'deny')).map((rule) => `deny ${rule}`),
|
|
114
|
+
];
|
|
111
115
|
return {
|
|
112
116
|
changes,
|
|
113
117
|
lock,
|
|
@@ -115,7 +119,8 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home) {
|
|
|
115
119
|
adotada,
|
|
116
120
|
registrarHooks: adotada && !pluginHooks,
|
|
117
121
|
registrarComentarios: adotada,
|
|
118
|
-
permissaoAusente:
|
|
122
|
+
permissaoAusente: permissoesFaltando.length > 0,
|
|
123
|
+
permissoesFaltando,
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
126
|
/**
|
|
@@ -154,7 +159,7 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
|
|
|
154
159
|
return { de: path, para };
|
|
155
160
|
});
|
|
156
161
|
const hooks = combineHooks(report.registrarHooks ? projectHooksConfig() : {}, report.registrarComentarios ? commentsHooksConfig() : {});
|
|
157
|
-
mergeSettings(root, { hooks, allow: HARNESS_PERMISSIONS }, now);
|
|
162
|
+
mergeSettings(root, { hooks, allow: HARNESS_PERMISSIONS, deny: HARNESS_DENY }, now);
|
|
158
163
|
if (report.registrarComentarios)
|
|
159
164
|
ensureCommentsBaseline(root);
|
|
160
165
|
if (report.adotada) {
|
|
@@ -220,7 +225,11 @@ export function describeUpdate(report) {
|
|
|
220
225
|
'a rule de projeto e a permissão do `ai-harness evento`.',
|
|
221
226
|
]
|
|
222
227
|
: [];
|
|
223
|
-
const permission = report.permissaoAusente && !report.adotada
|
|
228
|
+
const permission = report.permissaoAusente && !report.adotada
|
|
229
|
+
? [
|
|
230
|
+
`Faltam permissões do harness em .claude/settings.json (${report.permissoesFaltando.join(', ')}); o --yes as acrescenta.`,
|
|
231
|
+
]
|
|
232
|
+
: [];
|
|
224
233
|
if (!report.changes.length) {
|
|
225
234
|
const status = adopted.length || permission.length ? 'Nenhum arquivo a atualizar.' : 'A instalação está em dia: nenhum arquivo a atualizar.';
|
|
226
235
|
return [...adopted, ...permission, status].join('\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fontdata/ai-harness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "Harness de engenharia com IA da Fontdata para Claude Code: roda os slash commands direto na pasta do projeto via npx, sem instalar nada — ou instala-os em .claude/commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,14 +25,33 @@ No repositório atual, execute commit e push conforme o uso abaixo.
|
|
|
25
25
|
- tipos aceitos: `feat`, `fix`, `refactor`, `docs`, `chore`, `style`, `test`
|
|
26
26
|
- corpo detalhado com descrição completa do que foi feito no commit (itens objetivos)
|
|
27
27
|
- Se o usuário **passou** texto após o comando: use esse texto como mensagem do commit (multilinha permitida).
|
|
28
|
-
3. Execute **imediatamente** (sem etapa de confirmação):
|
|
29
|
-
- `git add -A`
|
|
28
|
+
3. Execute **imediatamente** (sem etapa de confirmação), deixando de fora a configuração do Claude Code:
|
|
29
|
+
- `git add -A -- . ':(exclude,glob).claude/settings*.json' ':(exclude,glob).claude/hooks/**'` — o padrão glob
|
|
30
|
+
cobre `.claude/settings.json` e `.claude/settings.local.json`; com o nome exato, um arquivo que está no
|
|
31
|
+
`.gitignore` faz o `git add` falhar inteiro
|
|
32
|
+
- se algum desses caminhos já estava no stage (`git diff --cached --name-only`), tire-o com
|
|
33
|
+
`git restore --staged -- <caminho>`
|
|
30
34
|
- `git commit` com a mensagem definida no passo 2 (título + corpo quando aplicável)
|
|
31
35
|
- `git push`
|
|
36
|
+
|
|
37
|
+
Se as únicas mudanças forem nesses caminhos, não faça commit nem push: vá direto ao passo 4.
|
|
32
38
|
4. Retorne um resumo final com branch, hash e status do push.
|
|
39
|
+
5. **Configuração do Claude Code alterada.** Se `git status` mostrou mudança em `.claude/settings.json` ou em
|
|
40
|
+
`.claude/hooks/`, avise que ela ficou fora do commit: no modo auto, o agente versionar as próprias permissões e
|
|
41
|
+
hooks é bloqueado como `[Self-Modification]`, e quem aprova essa mudança é o usuário. Entregue o comando pronto
|
|
42
|
+
para ele rodar com `!` no prompt, que executa como o próprio usuário:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
! git add .claude/settings.json .claude/hooks/ && git commit -m "chore: Atualiza a configuração do Claude Code" && git push
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Liste só os caminhos que mudaram. `.claude/settings.local.json` é pessoal e nunca vai para o repositório: se ele
|
|
49
|
+
aparecer no `git status`, sugira incluí-lo no `.gitignore`.
|
|
33
50
|
|
|
34
51
|
## Guardrails
|
|
35
52
|
|
|
36
53
|
- Se não houver alterações, interrompa com aviso.
|
|
37
54
|
- Não execute push forçado.
|
|
55
|
+
- Não tente versionar a configuração do Claude Code por outro caminho quando o modo auto bloquear: o passo 5 é
|
|
56
|
+
do usuário.
|
|
38
57
|
- Em caso de erro, pare e explique o próximo passo recomendado.
|