@pathbit/agentdevkit 0.0.22 → 0.0.23
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/.pbai4devs/rules/ambiente-local/RULE.md +5 -0
- package/.pbai4devs/rules/git/RULE.md +5 -1
- package/.pbai4devs/scripts/README.md +1 -0
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
- package/.pbai4devs/scripts/verificar-configuracao/README.md +22 -0
- package/.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs +63 -0
- package/.pbai4devs/scripts/verificar-configuracao/tests/TEST.mjs +145 -0
- package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +15 -3
- package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +15 -3
- package/.pbai4devs/skills/git/git-diff/SKILL.md +3 -1
- package/.pbai4devs/skills/git/git-status/SKILL.md +2 -1
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +13 -8
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +4 -2
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +14 -2
- package/CHANGELOG.md +10 -0
- package/docs/guides/cli.md +9 -0
- package/docs/guides/troubleshooting.md +4 -0
- package/docs/specs/README.md +3 -0
- package/docs/specs/preimplementation-order/SPEC.md +23 -0
- package/docs/specs/target-paths/SPEC.md +19 -0
- package/package.json +1 -1
- package/src/install.mjs +21 -5
- package/src/next.mjs +26 -9
|
@@ -27,6 +27,11 @@ O Makefile deve oferecer `help`, `doctor`, `setup`, `up`, `down`, `logs`,
|
|
|
27
27
|
`test`, `lint` e `build`, com comandos reais da stack. Etapas ainda indefinidas
|
|
28
28
|
devem falhar com mensagem útil; não simule sucesso. Documente os comandos,
|
|
29
29
|
dependências e URL/porta no README; valide Compose e saúde antes de concluir.
|
|
30
|
+
Antes de existir aplicação, valide o runtime, o Compose e os serviços de
|
|
31
|
+
infraestrutura necessários. Build, saúde e fluxo da aplicação inexistente
|
|
32
|
+
permanecem pendentes; não crie código de negócio para simular essas provas. Se
|
|
33
|
+
uma dependência obrigatória estiver indisponível, mantenha a configuração
|
|
34
|
+
pendente e registre o impedimento. Após a implementação, prove a aplicação.
|
|
30
35
|
|
|
31
36
|
Use o [modelo de ambiente](../../templates/ambiente-local/TEMPLATE.md) para
|
|
32
37
|
perguntas e arquivos-base. Versões da aplicação seguem as decisões do projeto;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
trigger: model_decision
|
|
3
|
-
description: Aplique ao preparar, integrar ou publicar alterações Git.
|
|
3
|
+
description: Aplique ao consultar, preparar, integrar ou publicar alterações Git.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Regras Git
|
|
7
7
|
|
|
8
|
+
Em consultas sem alterações, defina `GIT_OPTIONAL_LOCKS=0` no ambiente do
|
|
9
|
+
subprocesso para evitar atualizações opcionais dos bytes do índice. Isso não
|
|
10
|
+
substitui as travas necessárias para operações que alteram o Git.
|
|
11
|
+
|
|
8
12
|
Use Conventional Commits (`tipo(escopo): descrição`, escopo opcional), inclusive
|
|
9
13
|
em checkpoints e merges; destaque mudanças incompatíveis com `!` ou
|
|
10
14
|
`BREAKING CHANGE:`. Autor e committer devem ser o usuário, nunca um agente;
|
|
@@ -26,6 +26,7 @@ node .pbai4devs/scripts/executar-script/SCRIPT.mjs diagnosticar-ambiente
|
|
|
26
26
|
| --- | --- |
|
|
27
27
|
| `diagnosticar-ambiente` | Runtimes, pacotes e navegadores disponíveis |
|
|
28
28
|
| `inspecionar-projeto` | Detecta manifests, comandos e estrutura |
|
|
29
|
+
| `verificar-configuracao` | Confere o estado antes de implementar código |
|
|
29
30
|
| `preparar-ferramentas` | Instala/atualiza ferramentas locais |
|
|
30
31
|
| `gerenciar-skills-externas` | Instala/check/read de skills curadas locais |
|
|
31
32
|
| `auditar-seguranca` | Scanners offline, bases locais e relatórios redigidos |
|
|
@@ -12,6 +12,10 @@ O JSON identifica tecnologia, comandos, estrutura e documentos existentes.
|
|
|
12
12
|
`documentation.files` mantém caminhos e caixa, papel provável, tamanho e hash
|
|
13
13
|
quando a leitura couber nos limites. Referências válidas do estado aparecem em
|
|
14
14
|
`configured`; nomes sugerem papéis, mas não comprovam conteúdo ou aprovação.
|
|
15
|
+
Na maturidade, o roadmap e as specs são consultados nos caminhos configurados;
|
|
16
|
+
status são declarações e um vínculo local não comprova aprovação ou revisão.
|
|
17
|
+
Uma lista de specs ambígua ou incompleta não autoriza escolher um incremento;
|
|
18
|
+
o estado do vínculo no roadmap também é apenas uma indicação para consulta.
|
|
15
19
|
|
|
16
20
|
Links simbólicos são listados como opacos, sem ler seus destinos. Conteúdo dos
|
|
17
21
|
documentos e valores de configuração não aparecem no relatório. `scan` e
|
|
@@ -20,6 +24,8 @@ condições. Uma pasta simbólica como `docs` também aparece, sem presumir que
|
|
|
20
24
|
esteja vazia. Links não classificados recebem `role: unclassified`; enquanto
|
|
21
25
|
existir uma fronteira não inspecionada, `scan.complete` será falso. Não use
|
|
22
26
|
inventário incompleto como prova de ausência de documentos.
|
|
27
|
+
O inspetor revalida a identidade dos caminhos durante a leitura; isso detecta
|
|
28
|
+
mudanças, mas não é uma garantia atômica contra toda troca simultânea.
|
|
23
29
|
|
|
24
30
|
`exclusions` descreve pastas de clientes, artefatos e caches fora do inventário.
|
|
25
31
|
Documentos humanos em `.github`, exemplos, wiki e RFCs são incluídos; os
|
|
@@ -41,12 +41,69 @@ const present = (root, name, directory) => {
|
|
|
41
41
|
const inside = candidate => { const relative = path.relative(repository, candidate); return relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative); };
|
|
42
42
|
const fixturePath = candidate => /^(?:\.agent-tests\/|tmp\/examples\/|examples\/generated\/)/.test(normalize(path.relative(repository, candidate)));
|
|
43
43
|
|
|
44
|
+
// Rechecking is a conservative race detection, not an atomic openat traversal.
|
|
45
|
+
function pathIdentity(root, file = '') {
|
|
46
|
+
try {
|
|
47
|
+
let current = root; const result = [], parts = file ? file.split('/') : [];
|
|
48
|
+
for (const [index, part] of ['', ...parts].entries()) {
|
|
49
|
+
if (part) current = path.join(current, part);
|
|
50
|
+
const item = fs.lstatSync(current);
|
|
51
|
+
if (item.isSymbolicLink() || (index < parts.length && !item.isDirectory())) return null;
|
|
52
|
+
result.push({ dev: item.dev, ino: item.ino, mode: item.mode, size: item.size, directory: item.isDirectory() });
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
const sameIdentity = (left, right) => left && right && left.length === right.length
|
|
58
|
+
&& left.every((item, index) => item.dev === right[index].dev && item.ino === right[index].ino && item.mode === right[index].mode);
|
|
59
|
+
|
|
60
|
+
function boundedEntries(folder, limit, verify = () => true) {
|
|
61
|
+
const handle = fs.opendirSync(folder), entries = [];
|
|
62
|
+
try {
|
|
63
|
+
if (!verify()) return { entries: [], truncated: true, changed: true };
|
|
64
|
+
while (true) {
|
|
65
|
+
const entry = handle.readSync();
|
|
66
|
+
if (!entry) return verify() ? { entries, truncated: false } : { entries: [], truncated: true, changed: true };
|
|
67
|
+
if (entries.length >= limit) return verify() ? { entries, truncated: true } : { entries: [], truncated: true, changed: true };
|
|
68
|
+
entries.push(entry);
|
|
69
|
+
}
|
|
70
|
+
} finally { handle.closeSync(); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stableDirectory(root, directory, limit) {
|
|
74
|
+
const identity = pathIdentity(root, directory);
|
|
75
|
+
if (!identity?.at(-1).directory) return { entries: [], truncated: true, changed: true, verify: () => false };
|
|
76
|
+
const verify = () => sameIdentity(identity, pathIdentity(root, directory));
|
|
77
|
+
return { ...boundedEntries(path.join(root, directory), limit, verify), verify };
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
function metadata(root, file) {
|
|
45
|
-
|
|
46
|
-
const
|
|
81
|
+
const empty = { fields: {}, documents: {}, text: null };
|
|
82
|
+
const identity = () => pathIdentity(root, file);
|
|
83
|
+
let descriptor, text;
|
|
84
|
+
try {
|
|
85
|
+
const name = path.join(root, file), ancestors = identity();
|
|
86
|
+
if (!ancestors || !sameIdentity(ancestors, identity())) return empty;
|
|
87
|
+
const before = ancestors.at(-1);
|
|
88
|
+
if (before.size > 1024 * 1024) return empty;
|
|
89
|
+
descriptor = fs.openSync(name, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
90
|
+
const opened = fs.fstatSync(descriptor);
|
|
91
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size || !sameIdentity(ancestors, identity())) return empty;
|
|
92
|
+
const buffer = Buffer.alloc(opened.size + 1); let length = 0;
|
|
93
|
+
while (length < buffer.length) {
|
|
94
|
+
const count = fs.readSync(descriptor, buffer, length, buffer.length - length, length);
|
|
95
|
+
if (!count) break;
|
|
96
|
+
length += count;
|
|
97
|
+
}
|
|
98
|
+
const after = fs.fstatSync(descriptor);
|
|
99
|
+
if (length !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || !sameIdentity(ancestors, identity())) return empty;
|
|
100
|
+
text = buffer.subarray(0, length).toString('utf8').replaceAll('\r', '');
|
|
101
|
+
} catch { return empty; }
|
|
102
|
+
finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
|
|
47
103
|
const header = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
|
|
48
|
-
const result = { fields: {}, documents: {} }; if (!header) return result;
|
|
49
|
-
let documents = false;
|
|
104
|
+
const result = { fields: {}, documents: {}, text }; if (!header) return result;
|
|
105
|
+
let documents = false, invalidDocuments = false;
|
|
106
|
+
const seen = new Set(), duplicates = new Set(), seenDocuments = new Set(), duplicateDocuments = new Set();
|
|
50
107
|
const scalar = raw => {
|
|
51
108
|
const value = raw.trim();
|
|
52
109
|
if (!value || ['null', '~'].includes(value)) return null;
|
|
@@ -54,16 +111,131 @@ function metadata(root, file) {
|
|
|
54
111
|
return value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1).replaceAll("''", "'") : value;
|
|
55
112
|
};
|
|
56
113
|
for (const line of header.split('\n')) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
114
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
115
|
+
const top = line.match(/^(?:([\p{L}_][\p{L}\p{N}_.-]*)|"([^"\n]+)"|'([^'\n]+)'):\s*(.*)$/u);
|
|
116
|
+
if (top) {
|
|
117
|
+
const key = top[1] || top[2] || top[3];
|
|
118
|
+
if (key.includes('\\')) return { ...empty, text };
|
|
119
|
+
if (seen.has(key)) duplicates.add(key);
|
|
120
|
+
seen.add(key); result.fields[key] = duplicates.has(key) ? null : scalar(top[4]);
|
|
121
|
+
if (key === 'documentos' && duplicates.has(key)) { invalidDocuments = true; result.documents = {}; }
|
|
122
|
+
documents = key === 'documentos' && !invalidDocuments && (!top[4].trim() || ['null', '~'].includes(top[4].trim()));
|
|
123
|
+
} else if (!/^\s/.test(line)) return { ...empty, text };
|
|
124
|
+
else if (documents) {
|
|
125
|
+
const item = line.match(/^ (?:([\p{L}_][\p{L}\p{N}_.-]*)|"([^"\n]+)"|'([^'\n]+)'):\s*(.*)$/u);
|
|
126
|
+
if (item) {
|
|
127
|
+
const key = item[1] || item[2] || item[3];
|
|
128
|
+
if (key.includes('\\')) { invalidDocuments = true; result.documents = {}; documents = false; continue; }
|
|
129
|
+
if (seenDocuments.has(key)) duplicateDocuments.add(key);
|
|
130
|
+
seenDocuments.add(key); result.documents[key] = duplicateDocuments.has(key) ? null : scalar(item[4]);
|
|
131
|
+
} else if (/^ \S/.test(line)) { invalidDocuments = true; result.documents = {}; documents = false; }
|
|
132
|
+
}
|
|
60
133
|
}
|
|
61
134
|
return result;
|
|
62
135
|
}
|
|
63
136
|
|
|
137
|
+
// The configured specs directory contains <slug>/SPEC.md. Refuse ambiguous
|
|
138
|
+
// layouts instead of guessing which increment or declared status to use.
|
|
139
|
+
function configuredSpecs(root, directory) {
|
|
140
|
+
const result = { path: directory, present: false, complete: false, items: [] };
|
|
141
|
+
if (!directory) return result;
|
|
142
|
+
let current = root;
|
|
143
|
+
for (const component of directory.split('/')) {
|
|
144
|
+
current = path.join(current, component);
|
|
145
|
+
try {
|
|
146
|
+
const info = fs.lstatSync(current);
|
|
147
|
+
if (info.isSymbolicLink() || !info.isDirectory()) return result;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.code === 'ENOENT') result.complete = true;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
result.present = true;
|
|
154
|
+
const incomplete = () => { result.items = []; return result; };
|
|
155
|
+
let scanned = 0;
|
|
156
|
+
function entriesAt(folder) {
|
|
157
|
+
const relative = normalize(path.relative(root, folder));
|
|
158
|
+
const result = stableDirectory(root, relative, Math.min(200, 500 - scanned));
|
|
159
|
+
scanned += result.entries.length;
|
|
160
|
+
return result.truncated ? null : result.entries;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const entries = entriesAt(current);
|
|
164
|
+
if (!entries) return incomplete();
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (/^readme(?:\.md)?$/i.test(entry.name) && !entry.isSymbolicLink()) continue;
|
|
167
|
+
if (['.git', '.pbai4devs', '.agent-tests', 'node_modules'].includes(entry.name.toLowerCase())) return incomplete();
|
|
168
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) return incomplete();
|
|
169
|
+
const folder = `${directory}/${entry.name}`;
|
|
170
|
+
if (!present(root, folder, true)) return incomplete();
|
|
171
|
+
const children = entriesAt(path.join(root, folder));
|
|
172
|
+
if (!children) return incomplete();
|
|
173
|
+
const names = children.filter(item => /^spec\.md$/i.test(item.name));
|
|
174
|
+
if (names.length !== 1 || !names[0].isFile() || names[0].isSymbolicLink()) return incomplete();
|
|
175
|
+
const file = `${folder}/${names[0].name}`;
|
|
176
|
+
if (!present(root, file, false)) return incomplete();
|
|
177
|
+
const document = metadata(root, file);
|
|
178
|
+
if (document.text === null) return incomplete();
|
|
179
|
+
result.items.push({ path: file, status: ['rascunho', 'pendente', 'aprovado'].includes(document.fields.status) ? document.fields.status : null });
|
|
180
|
+
if (result.items.length > 1) return incomplete();
|
|
181
|
+
}
|
|
182
|
+
if (!present(root, directory, true)) return incomplete();
|
|
183
|
+
result.complete = true;
|
|
184
|
+
} catch { return incomplete(); }
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function referenceState(roadmap, roadmapFile, specFile) {
|
|
189
|
+
if (!roadmap || !roadmapFile) return { linked: false, roadmapState: 'unknown' };
|
|
190
|
+
// Examples and orphan reference definitions do not select an increment.
|
|
191
|
+
const lines = []; let fence = null;
|
|
192
|
+
const body = roadmap.replace(/^---\n[^]*?\n---(?:\n|$)/, '').replace(/<!--[^]*?-->/g, '');
|
|
193
|
+
for (const line of body.split('\n')) {
|
|
194
|
+
const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
195
|
+
if (fence) {
|
|
196
|
+
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !line.slice(marker[0].length).trim()) fence = null;
|
|
197
|
+
} else if (marker) fence = marker[1];
|
|
198
|
+
else lines.push(line.replace(/(`+)[^`\n]*?\1/g, ''));
|
|
199
|
+
}
|
|
200
|
+
const normalizeLabel = label => label.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
201
|
+
const definitions = new Map();
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const match = line.match(/^ {0,3}\[([^\]]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+))(?:[ \t]+(?:"[^"\n]*"|'[^'\n]*'))?[ \t]*$/);
|
|
204
|
+
if (match) { const label = normalizeLabel(match[1]); definitions.set(label, [...(definitions.get(label) || []), match[2] || match[3]]); }
|
|
205
|
+
}
|
|
206
|
+
const references = [], ancestry = [];
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const list = line.match(/^([ \t]*)(?:[-+*]|\d+[.)])\s+(.*)$/);
|
|
209
|
+
const marker = list?.[2].match(/^\[([ xX-])\](?:\s|$)/);
|
|
210
|
+
const closed = /(?:^|[^\p{L}])(?:cancelad[ao]s?|conclu[ií]d[ao]s?|publicad[ao]s?|finalizad[ao]s?|descartad[ao]s?|cancelled|completed|published)(?=$|[^\p{L}])/iu.test(line);
|
|
211
|
+
let state = 'unknown';
|
|
212
|
+
if (list) {
|
|
213
|
+
const indent = list[1].length;
|
|
214
|
+
while (ancestry.length && ancestry.at(-1).indent >= indent) ancestry.pop();
|
|
215
|
+
const own = closed || marker && /x/i.test(marker[1]) ? 'closed' : marker?.[1] === ' ' ? 'open' : marker?.[1] === '-' ? 'in-progress' : 'unknown';
|
|
216
|
+
state = ancestry.some(item => item.state === 'closed') ? 'closed' : /\t/.test(list[1]) || ancestry.some(item => item.state === 'unknown') ? 'unknown' : own;
|
|
217
|
+
ancestry.push({ indent, state });
|
|
218
|
+
} else if (line.trim() && !/^\s/.test(line)) ancestry.length = 0;
|
|
219
|
+
for (const match of line.matchAll(/(?<![!\\])\[[^\]\n]+\]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+(?:"[^"\n]*"|'[^'\n]*'))?\s*\)/g)) references.push({ destination: match[1] || match[2], state });
|
|
220
|
+
for (const match of line.matchAll(/(?<![!\\])\[([^\]\n]+)\]\[([^\]\n]*)\]/g)) {
|
|
221
|
+
const label = normalizeLabel(match[2] || match[1]);
|
|
222
|
+
if (definitions.get(label)?.length === 1) references.push({ destination: definitions.get(label)[0], state });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const matches = references.filter(({ destination }) => {
|
|
226
|
+
try {
|
|
227
|
+
const value = decodeURIComponent(destination).split(/[?#]/, 1)[0];
|
|
228
|
+
if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|\\)/i.test(value) || value.includes('\\')) return false;
|
|
229
|
+
return path.posix.resolve('/', path.posix.dirname(roadmapFile), value) === '/' + specFile;
|
|
230
|
+
} catch { return false; }
|
|
231
|
+
});
|
|
232
|
+
if (matches.length !== 1) return { linked: matches.length > 0, roadmapState: matches.length ? 'ambiguous' : 'unknown' };
|
|
233
|
+
return { linked: true, roadmapState: matches[0].state };
|
|
234
|
+
}
|
|
235
|
+
|
|
64
236
|
function maturity(root, report) {
|
|
65
237
|
const stateFile = `${stateDirectory(root)}/configuracao/ESTADO.md`; const state = metadata(root, stateFile);
|
|
66
|
-
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) &&
|
|
238
|
+
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) && !/[\\\x00-\x1f\x7f:]/.test(value) && !value.split('/').some(part => !part || ['.', '..', '.git'].includes(part.toLowerCase()));
|
|
67
239
|
const planning = {};
|
|
68
240
|
for (const name of ['prd', 'ux_ui', 'design']) {
|
|
69
241
|
const file = state.documents[name];
|
|
@@ -72,10 +244,22 @@ function maturity(root, report) {
|
|
|
72
244
|
planning[name] = { path: safeDocument(file) ? file : null, present: Boolean(exists), status: ['rascunho', 'aprovado', 'nao-aplicavel', 'pendente'].includes(status) ? status : null };
|
|
73
245
|
}
|
|
74
246
|
planning.ready = planning.prd.status === 'aprovado' && planning.design.status === 'aprovado' && ['aprovado', 'nao-aplicavel'].includes(planning.ux_ui.status);
|
|
75
|
-
|
|
247
|
+
const roadmapPath = safeDocument(state.documents.roadmap) && /\.md$/i.test(state.documents.roadmap) ? state.documents.roadmap : null;
|
|
248
|
+
const roadmapPresent = Boolean(roadmapPath && present(root, roadmapPath, false));
|
|
249
|
+
const roadmapDocument = roadmapPresent ? metadata(root, roadmapPath) : { fields: {}, text: null };
|
|
250
|
+
planning.roadmap = { path: roadmapPath, present: roadmapPresent, status: ['pendente', 'rascunho', 'planejado', 'aprovado'].includes(roadmapDocument.fields.status) ? roadmapDocument.fields.status : null };
|
|
251
|
+
const specsPath = safeDocument(state.documents.specs) ? state.documents.specs : null;
|
|
252
|
+
planning.specs = configuredSpecs(root, specsPath);
|
|
253
|
+
for (const spec of planning.specs.items) Object.assign(spec, referenceState(roadmapDocument.text, roadmapPath, spec.path));
|
|
254
|
+
let released = false, releasesComplete = true; const releases = state.documents.releases;
|
|
76
255
|
if (safeDocument(releases) && present(root, releases, true)) {
|
|
77
256
|
const visit = (directory, depth = 0) => {
|
|
78
|
-
|
|
257
|
+
let listing;
|
|
258
|
+
try { listing = stableDirectory(root, directory, 100); }
|
|
259
|
+
catch { releasesComplete = false; return; }
|
|
260
|
+
if (listing.truncated) releasesComplete = false;
|
|
261
|
+
for (const entry of listing.entries) {
|
|
262
|
+
if (!listing.verify()) { releasesComplete = false; return; }
|
|
79
263
|
const file = normalize(path.join(directory, entry.name));
|
|
80
264
|
if (entry.isSymbolicLink()) continue;
|
|
81
265
|
if (entry.isDirectory() && depth < 2) visit(file, depth + 1);
|
|
@@ -91,7 +275,7 @@ function maturity(root, report) {
|
|
|
91
275
|
if (planning.ready) level = 'planejado';
|
|
92
276
|
if (report.mode === 'aplicacao-detectada') level = 'implementacao';
|
|
93
277
|
if (report.mode === 'aplicacao-detectada' && planning.ready && report.contract.complete && configuration.configuracao_tecnica === 'concluido') level = released ? 'manutencao' : 'configurado';
|
|
94
|
-
return { level, planning, configuration, released, signals: { application: report.mode === 'aplicacao-detectada', structure: report.contract.complete }, limits: ['Classificacao descritiva; status em arquivos nao substitui aprovacao, testes ou provas de funcionamento.'] };
|
|
278
|
+
return { level, planning, configuration, released, releasesComplete, signals: { application: report.mode === 'aplicacao-detectada', structure: report.contract.complete }, limits: ['Classificacao descritiva; status em arquivos nao substitui aprovacao, testes ou provas de funcionamento.'] };
|
|
95
279
|
}
|
|
96
280
|
|
|
97
281
|
const documentRoles = new Set(['readme', 'contribuicao', 'changelog', 'seguranca', 'adrs', 'prd', 'ux_ui', 'design', 'roadmap', 'releases', 'specs', 'tarefas']);
|
|
@@ -152,10 +336,12 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
152
336
|
}
|
|
153
337
|
let descriptor;
|
|
154
338
|
try {
|
|
339
|
+
const identity = pathIdentity(root, file);
|
|
340
|
+
if (!identity || !sameIdentity(identity, pathIdentity(root, file))) { warn(file, 'changed-during-read'); return; }
|
|
155
341
|
descriptor = fs.openSync(path.join(root, file), fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
156
342
|
const before = fs.fstatSync(descriptor);
|
|
157
343
|
if (!before.isFile() || before.dev !== expected.dev || before.ino !== expected.ino || before.size !== expected.size
|
|
158
|
-
|| before.mtimeMs !== expected.mtimeMs || before.ctimeMs !== expected.ctimeMs) { warn(file, 'changed-during-read'); return; }
|
|
344
|
+
|| before.mtimeMs !== expected.mtimeMs || before.ctimeMs !== expected.ctimeMs || !sameIdentity(identity, pathIdentity(root, file))) { warn(file, 'changed-during-read'); return; }
|
|
159
345
|
const buffer = Buffer.alloc(Math.min(before.size + 1, bounds.maxTotalBytes - scan.bytesRead));
|
|
160
346
|
let length = 0;
|
|
161
347
|
while (length < buffer.length) {
|
|
@@ -165,7 +351,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
165
351
|
scan.bytesRead += read;
|
|
166
352
|
}
|
|
167
353
|
const after = fs.fstatSync(descriptor);
|
|
168
|
-
if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
|
|
354
|
+
if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || !sameIdentity(identity, pathIdentity(root, file))) {
|
|
169
355
|
warn(file, 'changed-during-read'); return;
|
|
170
356
|
}
|
|
171
357
|
scan.bytesHashed += length;
|
|
@@ -194,8 +380,13 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
194
380
|
const found = locate(directory);
|
|
195
381
|
if (found.kind !== 'directory') { if (found.kind === 'symbolic-link') add(found.boundary, found, role); return; }
|
|
196
382
|
}
|
|
197
|
-
let children;
|
|
198
|
-
try {
|
|
383
|
+
let children, listing;
|
|
384
|
+
try {
|
|
385
|
+
listing = stableDirectory(root, directory, Math.max(0, bounds.maxEntries - scan.entries));
|
|
386
|
+
if (listing.changed) { scan.truncated = true; warn(directory || '.', 'changed-during-read'); return; }
|
|
387
|
+
if (listing.truncated) scan.truncated = true;
|
|
388
|
+
children = listing.entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
389
|
+
}
|
|
199
390
|
catch { warn(directory || '.', 'unreadable-directory'); return; }
|
|
200
391
|
// Discard a listing if the directory became a link while being inspected.
|
|
201
392
|
if (directory && locate(directory).kind !== 'directory') { warn(directory, 'changed-during-read'); return; }
|
|
@@ -204,6 +395,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
204
395
|
scan.entries++;
|
|
205
396
|
const file = directory ? `${directory}/${child.name}` : child.name;
|
|
206
397
|
if (excludedDocument(file)) continue;
|
|
398
|
+
if (!listing.verify()) { scan.truncated = true; warn(directory || '.', 'changed-during-read'); return; }
|
|
207
399
|
const found = locate(file);
|
|
208
400
|
if (found.kind === 'directory') {
|
|
209
401
|
if (depth < bounds.maxDepth) walk(file, depth + 1, role);
|
|
@@ -244,8 +436,11 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
244
436
|
const warning = (file, reason) => report.warnings.push({ file, reason });
|
|
245
437
|
function commands(file, name) {
|
|
246
438
|
if (!['package.json', 'composer.json', 'deno.json', 'deno.jsonc', 'Makefile', 'makefile', 'GNUmakefile', 'pyproject.toml'].includes(name)) return;
|
|
247
|
-
|
|
248
|
-
|
|
439
|
+
const identity = pathIdentity(root, file);
|
|
440
|
+
if (!identity) { warning(file, 'manifest-nao-inspecionado'); return; }
|
|
441
|
+
if (identity.at(-1).size > 1024 * 1024) { warning(file, 'manifest-grande-demais'); return; }
|
|
442
|
+
const text = metadata(root, file).text;
|
|
443
|
+
if (text === null) { warning(file, 'manifest-nao-inspecionado'); return; }
|
|
249
444
|
let names = [];
|
|
250
445
|
try {
|
|
251
446
|
if (['package.json', 'composer.json', 'deno.json', 'deno.jsonc'].includes(name)) {
|
|
@@ -268,7 +463,10 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
268
463
|
} catch { warning(file, 'manifest-nao-interpretado'); }
|
|
269
464
|
}
|
|
270
465
|
function walk(directory = '', depth = 0) {
|
|
271
|
-
const
|
|
466
|
+
const listing = stableDirectory(root, directory, Math.max(0, 25000 - entries));
|
|
467
|
+
if (listing.changed) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
468
|
+
if (listing.truncated) report.scan.truncated = true;
|
|
469
|
+
const children = listing.entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
272
470
|
for (const item of children) {
|
|
273
471
|
if (++entries > 25000) { report.scan.truncated = true; return; }
|
|
274
472
|
const file = normalize(path.join(directory, item.name));
|
|
@@ -276,10 +474,12 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
276
474
|
if (item.isSymbolicLink()) continue;
|
|
277
475
|
if (item.isDirectory()) {
|
|
278
476
|
if (excluded.has(item.name) || excluded.has(item.name.toLowerCase()) || /^\.?playwright/i.test(item.name)) continue;
|
|
477
|
+
if (!listing.verify()) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
279
478
|
if (depth < report.scan.maxDepth) walk(file, depth + 1); else report.scan.truncated = true;
|
|
280
479
|
continue;
|
|
281
480
|
}
|
|
282
481
|
if (!item.isFile()) continue;
|
|
482
|
+
if (!listing.verify()) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
283
483
|
let language = manifests.get(item.name);
|
|
284
484
|
if (/\.(?:csproj|fsproj|vbproj|sln)$/.test(item.name)) language = 'dotnet';
|
|
285
485
|
if (/^requirements(?:[.-][A-Za-z0-9_-]+)?\.txt$/.test(item.name)) language = 'python';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Confere os indicadores registrados antes da implementação.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Verificar configuração
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Somente leitura: retorna `1` se o estado estiver ausente, inacessível,
|
|
12
|
+
ambíguo ou com preparação ou configuração pendente. Retorna `0` e
|
|
13
|
+
`verificar-evidencias` quando ambos os indicadores registram `concluido`.
|
|
14
|
+
Para evitar ambiguidades, recusa chaves complexas e tags, âncoras, aliases ou
|
|
15
|
+
mesclagens no topo do cabeçalho. Links simbólicos detectados são recusados;
|
|
16
|
+
a leitura não é uma trava
|
|
17
|
+
atômica contra mudanças simultâneas de caminhos.
|
|
18
|
+
Não valida o YAML inteiro, a infraestrutura ou as provas; confira-os antes de
|
|
19
|
+
escrever código. O comando não bloqueia alterações de quem ignore seu resultado.
|
|
20
|
+
|
|
21
|
+
O estado legado é reconhecido sem migração; estados em conflito são recusados.
|
|
22
|
+
Erros de uso retornam `2`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
|
|
5
|
+
import { stateDirectory } from '../lib/state-path.mjs';
|
|
6
|
+
|
|
7
|
+
const limit = 'O estado não comprova evidências nem impede um agente de escrever.';
|
|
8
|
+
const blocked = motivo => ({ status: 'bloqueado', motivo, limite: limit });
|
|
9
|
+
|
|
10
|
+
// Read only the two indicators; never print arbitrary state content.
|
|
11
|
+
export function verifyConfiguration(root = projectRoot) {
|
|
12
|
+
requireNode();
|
|
13
|
+
try {
|
|
14
|
+
root = realpathSync(resolve(root));
|
|
15
|
+
const parts = `${stateDirectory(root)}/configuracao/ESTADO.md`.split('/');
|
|
16
|
+
let current = root; let info;
|
|
17
|
+
for (let index = 0; index < parts.length; index++) {
|
|
18
|
+
current = join(current, parts[index]);
|
|
19
|
+
info = lstatSync(current);
|
|
20
|
+
if (info.isSymbolicLink() || (index < parts.length - 1 && !info.isDirectory())) return blocked('estado-inseguro');
|
|
21
|
+
}
|
|
22
|
+
if (!info.isFile() || info.size > 1024 * 1024) return blocked('estado-invalido');
|
|
23
|
+
const text = readFileSync(current, 'utf8').replaceAll('\r\n', '\n');
|
|
24
|
+
const header = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
|
|
25
|
+
if (header === undefined) return blocked('estado-invalido');
|
|
26
|
+
const values = {};
|
|
27
|
+
for (const line of header.split('\n')) {
|
|
28
|
+
if (!line.trim() || line.trimStart().startsWith('#') || /^[ \t]/.test(line)) continue;
|
|
29
|
+
// Explicit mapping keys and merge keys may hide duplicate indicators.
|
|
30
|
+
if (/^(?:\?|<<\s*:)/.test(line)) return blocked('estado-invalido');
|
|
31
|
+
const match = line.match(/^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[a-zA-Z_][a-zA-Z0-9_.-]*)[ \t]*:(.*)$/);
|
|
32
|
+
if (!match) return blocked('estado-invalido');
|
|
33
|
+
if (/^[!&*]/.test(match[2].trim())) return blocked('estado-invalido');
|
|
34
|
+
let key = match[1];
|
|
35
|
+
if (key.startsWith('"')) {
|
|
36
|
+
try { key = JSON.parse(key); } catch { return blocked('estado-invalido'); }
|
|
37
|
+
} else if (key.startsWith("'")) key = key.slice(1, -1).replaceAll("''", "'");
|
|
38
|
+
if (!['preparacao', 'configuracao_tecnica'].includes(key)) continue;
|
|
39
|
+
if (Object.hasOwn(values, key)) return blocked('estado-invalido');
|
|
40
|
+
const value = match[2].trim().match(/^(?:concluido|pendente|'(?:concluido|pendente)'|"(?:concluido|pendente)")(?:[ \t]+#.*)?$/)?.[0];
|
|
41
|
+
if (!value) return blocked('estado-invalido');
|
|
42
|
+
values[key] = value.replace(/[ \t]+#.*$/, '').replace(/^['"]|['"]$/g, '');
|
|
43
|
+
}
|
|
44
|
+
if (!Object.hasOwn(values, 'preparacao') || !Object.hasOwn(values, 'configuracao_tecnica')) return blocked('estado-invalido');
|
|
45
|
+
if (values.preparacao !== 'concluido' || values.configuracao_tecnica !== 'concluido') return blocked('configuracao-pendente');
|
|
46
|
+
return { status: 'verificar-evidencias', estado: values, limite: limit };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return blocked('estado-ausente');
|
|
49
|
+
return blocked('estado-inacessivel-ou-inseguro');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function main(args = process.argv.slice(2)) {
|
|
54
|
+
if (args.length) throw new Error('Uso: verificar-configuracao sem argumentos.');
|
|
55
|
+
const report = verifyConfiguration();
|
|
56
|
+
console.log(JSON.stringify(report));
|
|
57
|
+
return report.status === 'bloqueado' ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isMain(import.meta.url)) {
|
|
61
|
+
try { process.exitCode = main(); }
|
|
62
|
+
catch (error) { console.error(error.message); process.exitCode = 2; }
|
|
63
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { projectRoot } from '../../lib/runtime.mjs';
|
|
7
|
+
import { verifyConfiguration } from '../SCRIPT.mjs';
|
|
8
|
+
|
|
9
|
+
function fixture(t) {
|
|
10
|
+
const base = join(projectRoot, '.agent-tests'); mkdirSync(base, { recursive: true });
|
|
11
|
+
const root = mkdtempSync(join(base, 'config-gate-'));
|
|
12
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
13
|
+
const write = (file, content) => {
|
|
14
|
+
const destination = join(root, file); mkdirSync(dirname(destination), { recursive: true });
|
|
15
|
+
writeFileSync(destination, content);
|
|
16
|
+
};
|
|
17
|
+
for (const file of ['lib/runtime.mjs', 'lib/state-path.mjs', 'lib/script-config.mjs', 'executar-script/SCRIPT.mjs', 'verificar-configuracao/SCRIPT.mjs']) {
|
|
18
|
+
const destination = join(root, '.pbai4devs/scripts', file);
|
|
19
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
20
|
+
copyFileSync(join(projectRoot, '.pbai4devs/scripts', file), destination);
|
|
21
|
+
}
|
|
22
|
+
const state = (body, folder = 'state') => write(`.pbai4devs/${folder}/configuracao/ESTADO.md`, body);
|
|
23
|
+
const run = (...args) => {
|
|
24
|
+
const file = join(root, '.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs');
|
|
25
|
+
const result = spawnSync(process.execPath, [file, ...args], { encoding: 'utf8' });
|
|
26
|
+
assert.ifError(result.error);
|
|
27
|
+
return { ...result, report: result.stdout ? JSON.parse(result.stdout) : null };
|
|
28
|
+
};
|
|
29
|
+
return { root, state, write, run };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const document = (preparation, configuration) => `---\npreparacao: ${preparation}\nconfiguracao_tecnica: ${configuration}\ndocumentos:\n prd: docs/PRD.md\n---\n\n# Estado\n`;
|
|
33
|
+
|
|
34
|
+
test('bloqueia antes do código quando o estado está ausente ou pendente', t => {
|
|
35
|
+
const { root, state, run } = fixture(t);
|
|
36
|
+
assert.deepEqual(run().report.motivo, 'estado-ausente');
|
|
37
|
+
assert.equal(run().status, 1);
|
|
38
|
+
for (const [preparation, configuration] of [['pendente', 'pendente'], ['concluido', 'pendente'], ['pendente', 'concluido']]) {
|
|
39
|
+
state(document(preparation, configuration));
|
|
40
|
+
const result = run(); assert.equal(result.status, 1);
|
|
41
|
+
assert.equal(result.report.motivo, 'configuracao-pendente');
|
|
42
|
+
assert.equal(existsSync(join(root, 'src')), false);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('aceita o modelo real e mantém provas da aplicação como verificação separada', t => {
|
|
47
|
+
const { root, state, run } = fixture(t);
|
|
48
|
+
const template = readFileSync(join(projectRoot, '.pbai4devs/templates/configuracao/TEMPLATE.md'), 'utf8');
|
|
49
|
+
state(template); assert.equal(run().report.motivo, 'configuracao-pendente');
|
|
50
|
+
state(template.replace('preparacao: pendente', 'preparacao: concluido')
|
|
51
|
+
.replace('configuracao_tecnica: pendente', 'configuracao_tecnica: concluido'));
|
|
52
|
+
assert.equal(run().report.status, 'verificar-evidencias');
|
|
53
|
+
assert.equal(existsSync(join(root, 'src')), false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('indicadores concluídos exigem revisão adicional de evidências e não alteram Git', t => {
|
|
57
|
+
const { root, state, write, run } = fixture(t);
|
|
58
|
+
state(document('"concluido" # aprovado', "'concluido'"));
|
|
59
|
+
write('.gitignore', 'node_modules/\n'); write('AGENTS.md', '# Instruções humanas\n');
|
|
60
|
+
assert.equal(spawnSync('git', ['init', '--quiet', root]).status, 0);
|
|
61
|
+
assert.equal(spawnSync('git', ['-C', root, 'add', '.gitignore', 'AGENTS.md']).status, 0);
|
|
62
|
+
const index = readFileSync(join(root, '.git/index'));
|
|
63
|
+
const statePath = join(root, '.pbai4devs/state/configuracao/ESTADO.md');
|
|
64
|
+
const original = readFileSync(statePath), ignore = readFileSync(join(root, '.gitignore'));
|
|
65
|
+
const result = run(); assert.equal(result.status, 0);
|
|
66
|
+
assert.equal(result.report.status, 'verificar-evidencias');
|
|
67
|
+
assert.match(result.report.limite, /não comprova evidências/);
|
|
68
|
+
assert.deepEqual(readFileSync(statePath), original);
|
|
69
|
+
assert.deepEqual(readFileSync(join(root, '.git/index')), index);
|
|
70
|
+
assert.deepEqual(readFileSync(join(root, '.gitignore')), ignore);
|
|
71
|
+
assert.equal(existsSync(join(root, 'src')), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('recusa indicadores ausentes, desconhecidos, duplicados e não expõe valores', t => {
|
|
75
|
+
const { state, run } = fixture(t);
|
|
76
|
+
const secret = 'SENSITIVE_VALUE_47849';
|
|
77
|
+
for (const value of [
|
|
78
|
+
'preparacao: concluido',
|
|
79
|
+
'preparacao: concluido\nconfiguracao_tecnica: ausente',
|
|
80
|
+
'preparacao: concluido\npreparacao: pendente\nconfiguracao_tecnica: concluido',
|
|
81
|
+
'preparacao: concluido\nconfiguracao_tecnica: concluido\n"configuracao_tecnica": pendente',
|
|
82
|
+
"'preparacao': concluido\npreparacao: pendente\nconfiguracao_tecnica: concluido",
|
|
83
|
+
'preparacao: concluido\n"prepara\\u0063ao": pendente\nconfiguracao_tecnica: concluido',
|
|
84
|
+
'preparacao: concluido\nconfiguracao_tecnica: concluido\n!!str configuracao_tecnica: pendente',
|
|
85
|
+
'preparacao: concluido\nconfiguracao_tecnica: concluido\npadrao: &base { configuracao_tecnica: pendente }',
|
|
86
|
+
'preparacao: concluido\nconfiguracao_tecnica: concluido\npadrao: *base',
|
|
87
|
+
'preparacao: concluido\nconfiguracao_tecnica: concluido\npadrao: !!str valor',
|
|
88
|
+
'preparacao: concluido\nconfiguracao_tecnica: concluido\n<<: *base',
|
|
89
|
+
'preparacao: concluido\nconfiguracao_tecnica: concluido\n? configuracao_tecnica\n: pendente',
|
|
90
|
+
`preparacao: concluido\nconfiguracao_tecnica: ${secret}`,
|
|
91
|
+
'preparacao: concluido\nconfiguracao_tecnica: true',
|
|
92
|
+
]) {
|
|
93
|
+
state(`---\n${value}\n---\n# Estado\n`);
|
|
94
|
+
const result = run(); assert.equal(result.status, 1);
|
|
95
|
+
assert.equal(result.report.motivo, 'estado-invalido');
|
|
96
|
+
assert.ok(!result.stdout.includes(secret)); assert.ok(!result.stderr.includes(secret));
|
|
97
|
+
}
|
|
98
|
+
state('preparacao: concluido\nconfiguracao_tecnica: concluido\n');
|
|
99
|
+
assert.equal(run().report.motivo, 'estado-invalido');
|
|
100
|
+
state('---\n"preparacao": concluido\n\'configuracao_tecnica\': concluido\n---\n# Estado\n');
|
|
101
|
+
assert.equal(run().report.status, 'verificar-evidencias');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('aceita estado legado e recusa coexistência de árvores de estado', t => {
|
|
105
|
+
const { state, run } = fixture(t);
|
|
106
|
+
state(document('concluido', 'concluido'), 'estado');
|
|
107
|
+
assert.equal(run().report.status, 'verificar-evidencias');
|
|
108
|
+
state(document('concluido', 'concluido'));
|
|
109
|
+
const result = run(); assert.equal(result.status, 1);
|
|
110
|
+
assert.equal(result.report.motivo, 'estado-inacessivel-ou-inseguro');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('não segue link humano em arquivo ou pasta de estado', t => {
|
|
114
|
+
const { root, state, run } = fixture(t);
|
|
115
|
+
const outside = join(root, 'fora'); mkdirSync(outside);
|
|
116
|
+
const target = join(outside, 'ESTADO.md'); writeFileSync(target, document('concluido', 'concluido'));
|
|
117
|
+
const directory = join(root, '.pbai4devs/state/configuracao'); mkdirSync(directory, { recursive: true });
|
|
118
|
+
try { symlinkSync(target, join(directory, 'ESTADO.md')); }
|
|
119
|
+
catch (error) { if (['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) { t.skip('Sem permissão para links.'); return; } throw error; }
|
|
120
|
+
assert.equal(run().report.motivo, 'estado-inseguro');
|
|
121
|
+
assert.equal(readFileSync(target, 'utf8'), document('concluido', 'concluido'));
|
|
122
|
+
unlinkSync(join(directory, 'ESTADO.md')); rmdirSync(directory);
|
|
123
|
+
symlinkSync(outside, directory, process.platform === 'win32' ? 'junction' : 'dir');
|
|
124
|
+
assert.equal(run().report.motivo, 'estado-inseguro');
|
|
125
|
+
assert.equal(readFileSync(target, 'utf8'), document('concluido', 'concluido'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('exige Node e rejeita argumentos extras sem ler outro projeto', t => {
|
|
129
|
+
const { root, run } = fixture(t);
|
|
130
|
+
const result = run('--root', root); assert.equal(result.status, 2);
|
|
131
|
+
assert.match(result.stderr, /sem argumentos/);
|
|
132
|
+
assert.equal(verifyConfiguration(root).status, 'bloqueado');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('seletor de scripts propaga bloqueio e exige revisão de evidências', t => {
|
|
136
|
+
const { root, state } = fixture(t);
|
|
137
|
+
const selector = join(root, '.pbai4devs/scripts/executar-script/SCRIPT.mjs');
|
|
138
|
+
const execute = () => spawnSync(process.execPath, [selector, 'verificar-configuracao'], { cwd: root, encoding: 'utf8' });
|
|
139
|
+
state(document('concluido', 'pendente'));
|
|
140
|
+
const pending = execute(); assert.ifError(pending.error); assert.equal(pending.status, 1);
|
|
141
|
+
assert.match(pending.stdout, /"motivo":"configuracao-pendente"/);
|
|
142
|
+
state(document('concluido', 'concluido'));
|
|
143
|
+
const recorded = execute(); assert.ifError(recorded.error); assert.equal(recorded.status, 0);
|
|
144
|
+
assert.match(recorded.stdout, /"status":"verificar-evidencias"/);
|
|
145
|
+
});
|
|
@@ -5,9 +5,21 @@ description: Implementa mudanças de backend com contratos, dados e provas de co
|
|
|
5
5
|
|
|
6
6
|
# Implementar backend
|
|
7
7
|
|
|
8
|
-
Siga
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Siga o escopo aprovado, os contratos e a stack existentes. Ao implementar uma
|
|
9
|
+
spec, confirme spec e plano aprovados e, antes de alterar código, execute
|
|
10
|
+
`node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao`.
|
|
11
|
+
Se o estado estiver ausente, inválido ou pendente, ou faltar prova obrigatória,
|
|
12
|
+
pare e informe o responsável. Saída zero ainda exige conferir as evidências.
|
|
13
|
+
Siga a ordem e as pré-condições já acordadas em
|
|
14
|
+
[implementar-spec](../../specs/implementar-spec/SKILL.md), sem reiniciar o fluxo
|
|
15
|
+
nem pedir novamente aprovações já dadas.
|
|
16
|
+
|
|
17
|
+
Em correção avulsa autorizada de projeto existente, preserve suas decisões e
|
|
18
|
+
use `preparar-dev` quando necessário, sem inventar spec ou estado. Uma tarefa
|
|
19
|
+
que cite uma spec ou esteja coberta por seu escopo é tarefa de spec, mesmo
|
|
20
|
+
sendo correção pontual; não a trate como avulsa. Esclareça dúvidas sobre
|
|
21
|
+
entradas, saídas, persistência, compatibilidade e falhas antes de decidir.
|
|
22
|
+
Altere somente o escopo atribuído.
|
|
11
23
|
|
|
12
24
|
Verifique validação de entrada, autorização por recurso e isolamento de dados.
|
|
13
25
|
Considere limites, concorrência, repetição, timeout, migração e recuperação
|
|
@@ -5,9 +5,21 @@ description: Implementa interfaces web conforme a spec, o design e as provas de
|
|
|
5
5
|
|
|
6
6
|
# Implementar frontend
|
|
7
7
|
|
|
8
|
-
Siga
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Siga o escopo aprovado, o design e os componentes existentes. Ao implementar
|
|
9
|
+
uma spec, confirme spec e plano aprovados e, antes de alterar código, execute
|
|
10
|
+
`node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao`.
|
|
11
|
+
Se o estado estiver ausente, inválido ou pendente, ou faltar prova obrigatória,
|
|
12
|
+
pare e informe o responsável. Saída zero ainda exige conferir as evidências.
|
|
13
|
+
Siga a ordem e as pré-condições já acordadas em
|
|
14
|
+
[implementar-spec](../../specs/implementar-spec/SKILL.md), sem reiniciar o fluxo
|
|
15
|
+
nem pedir novamente aprovações já dadas.
|
|
16
|
+
|
|
17
|
+
Em correção avulsa autorizada de projeto existente, preserve suas decisões e
|
|
18
|
+
use `preparar-dev` quando necessário, sem inventar spec ou estado. Uma tarefa
|
|
19
|
+
que cite uma spec ou esteja coberta por seu escopo é tarefa de spec, mesmo
|
|
20
|
+
sendo correção pontual; não a trate como avulsa. Esclareça comportamento,
|
|
21
|
+
conteúdo e acessibilidade quando faltarem decisões; não escolha framework nem
|
|
22
|
+
crie telas fora do escopo.
|
|
11
23
|
|
|
12
24
|
Considere estados de carregamento, vazio, erro e sucesso; navegação por teclado,
|
|
13
25
|
semântica acessível e diferentes tamanhos de tela. Preserve contratos com o
|
|
@@ -6,4 +6,6 @@ description: Examina e resume alterações Git.
|
|
|
6
6
|
# git-diff
|
|
7
7
|
|
|
8
8
|
Examine os diffs preparados e não preparados conforme o pedido, incluindo
|
|
9
|
-
arquivos não rastreados quando relevantes.
|
|
9
|
+
arquivos não rastreados quando relevantes. Execute consultas com
|
|
10
|
+
`GIT_OPTIONAL_LOCKS=0` no ambiente do subprocesso para evitar atualizar o índice;
|
|
11
|
+
não altere arquivos.
|
|
@@ -6,4 +6,5 @@ description: Inspeciona o estado do repositório e das branches locais e remotas
|
|
|
6
6
|
# git-status
|
|
7
7
|
|
|
8
8
|
Mostre branch, alterações preparadas e não preparadas, arquivos não rastreados e
|
|
9
|
-
divergência do upstream.
|
|
9
|
+
divergência do upstream. Execute a consulta com `GIT_OPTIONAL_LOCKS=0` no
|
|
10
|
+
ambiente do subprocesso para evitar atualizar o índice; não altere arquivos.
|
|
@@ -42,20 +42,25 @@ aprovações já dadas.
|
|
|
42
42
|
2. Prepare as ferramentas definidas em `.pbai4devs/tools/<nome>/TOOL.json` com
|
|
43
43
|
`preparar-ferramentas`; confira versões e compatibilidade com SO/arquitetura.
|
|
44
44
|
Reutilize instalações e lockfiles; não repita init nem troque a stack.
|
|
45
|
-
3. Aplique a [estrutura e o ambiente obrigatórios][estrutura],
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
3. Aplique a [estrutura e o ambiente obrigatórios][estrutura], preenchendo os
|
|
46
|
+
modelos de Makefile, Compose e dockerignore. Configure os serviços de
|
|
47
|
+
infraestrutura necessários, healthchecks, portas, volumes e comandos.
|
|
48
|
+
Antes de concluir a configuração, não implemente código da aplicação;
|
|
49
|
+
`src/` e `tests/` podem existir vazios ou conter apenas configuração.
|
|
48
50
|
4. Configure VS Code e recomende EditorConfig, sem regras conflitantes.
|
|
49
51
|
Termine `.gitignore` com o modelo CUSTOM IGNORE e os padrões da tecnologia;
|
|
50
52
|
confirme que templates, scripts, testes e fixtures continuam versionáveis.
|
|
51
53
|
5. Documente versões, estrutura, setup, comandos, origem do template/init e
|
|
52
54
|
decisões no README/estado. Use `atualizar-documentacao` para contribuição,
|
|
53
55
|
configuração e ADRs aplicáveis. Preserve arquivos e convenções existentes.
|
|
54
|
-
6. Use `verificar-trabalho`: valide
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
6. Use `verificar-trabalho`: valide runtime, configuração Compose e saúde dos
|
|
57
|
+
serviços de infraestrutura necessários; confira a estrutura com
|
|
58
|
+
`inspecionar-projeto --check`. Se Docker ou outra dependência obrigatória
|
|
59
|
+
estiver inacessível, mantenha a configuração pendente e informe a prova
|
|
60
|
+
faltante. Sem aplicação, registre build, saúde e fluxo dela como pendentes;
|
|
61
|
+
não os reporte como aprovados. Com aplicação existente, prove o fluxo real
|
|
62
|
+
conforme seu tipo. Marque `configuracao_tecnica: concluido` somente com
|
|
63
|
+
evidência da configuração prévia e dos serviços aplicáveis.
|
|
59
64
|
|
|
60
65
|
Após alterações técnicas, conclua `revisar-entrega`. Sincronize apontamentos
|
|
61
66
|
somente quando recursos dos agentes forem alterados.
|
|
@@ -17,7 +17,9 @@ Faça apenas as perguntas que impedem avançar.
|
|
|
17
17
|
Continue o trabalho já autorizado; não reinicie fases concluídas nem apresente
|
|
18
18
|
suposições como aprovações.
|
|
19
19
|
|
|
20
|
-
Projeto novo segue preparação → PRD → UX
|
|
21
|
-
|
|
20
|
+
Projeto novo segue preparação → PRD → UX → design (arquitetura e tecnologia) →
|
|
21
|
+
roadmap → spec aprovada → plano aprovado → configuração técnica → implementação
|
|
22
|
+
→ revisão. UX pode ser não aplicável com justificativa aprovada.
|
|
23
|
+
Projeto existente parte de suas evidências atuais;
|
|
22
24
|
instalar o kit não autoriza recriar documentos ou substituir decisões.
|
|
23
25
|
Se não houver pendências no escopo, diga isso claramente.
|
|
@@ -18,8 +18,20 @@ antes de implementar.
|
|
|
18
18
|
seus IDs/revisões. Não invente uma solução já atendida por elas.
|
|
19
19
|
3. Depois da aprovação, crie ou retome `feature/<slug-da-spec>` de `develop`.
|
|
20
20
|
Se faltar `develop`, use `main` quando seguro; sem base/remoto, combine como
|
|
21
|
-
proceder. Preserve alterações. Registre a aprovação no roadmap e
|
|
22
|
-
configuração real
|
|
21
|
+
proceder. Preserve alterações. Registre a aprovação no roadmap e complete a
|
|
22
|
+
configuração real com `configurar-projeto` antes de codificar.
|
|
23
|
+
Antes de qualquer patch de código da spec, execute:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
node .pbai4devs/scripts/executar-script/SCRIPT.mjs verificar-configuracao
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Releia o estado e confira as evidências da infraestrutura obrigatória. Se o
|
|
30
|
+
estado estiver ausente, inválido ou pendente, ou uma prova falhar, pare sem
|
|
31
|
+
escrever código; retome quando o impedimento for resolvido. Código zero do
|
|
32
|
+
script só confirma o estado registrado, não comprova as evidências nem impede
|
|
33
|
+
escrita por um agente que ignore o resultado. Não peça novamente aprovações
|
|
34
|
+
já concedidas.
|
|
23
35
|
4. Use o modelo de tarefas em `<documentos.tarefas>/<slug-da-spec>/TASKS.md`.
|
|
24
36
|
Vincule spec/release e registre tarefas, dependências, critérios e evidências.
|
|
25
37
|
Atualize `[ ]`, `[-]` e `[x]` ao avançar; conclusão exige prova. Ao retomar,
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ description: Alterações verificáveis por versão do AgentDevKit.
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## 0.0.23 — 2026-11-13
|
|
8
|
+
|
|
9
|
+
- Corrige a resolução de destinos inexistentes diretamente sob a raiz.
|
|
10
|
+
- Aceita aliases temporários confiáveis do macOS e recusa destinos que
|
|
11
|
+
atravessam links humanos, inclusive os indicados por variáveis de ambiente.
|
|
12
|
+
- Orienta roadmap e spec antes da configuração técnica e acrescenta uma
|
|
13
|
+
verificação do estado antes da implementação.
|
|
14
|
+
- Aplica a verificação também aos especialistas e orienta consultas Git que
|
|
15
|
+
preservam os bytes do índice.
|
|
16
|
+
|
|
7
17
|
## 0.0.22 — 2026-11-13
|
|
8
18
|
|
|
9
19
|
- Descoberta de skills do Codex sem duplicação, com alternativa segura quando a
|
package/docs/guides/cli.md
CHANGED
|
@@ -125,5 +125,14 @@ do kit. Conteúdo humano não é substituído por `--force`.
|
|
|
125
125
|
|
|
126
126
|
`doctor` verifica o kit, não o produto inteiro. `next` pode terminar com
|
|
127
127
|
sucesso enquanto orienta uma etapa ainda pendente.
|
|
128
|
+
Após produto e design, ele usa os caminhos de roadmap e specs registrados no
|
|
129
|
+
estado. Para orientar uma spec existente, exige uma única referência aberta
|
|
130
|
+
`[ ]` ou em andamento `[-]` no roadmap. Com mais de uma spec, entrega encerrada,
|
|
131
|
+
vínculo ausente ou leitura ambígua, orienta a revisão do roadmap. Um status
|
|
132
|
+
declarado não comprova aprovação: a skill confere decisões e evidências. Com uma
|
|
133
|
+
spec aberta e declarada aprovada, `implementar-spec` verifica o plano antes de
|
|
134
|
+
encaminhar a configuração técnica; `next` não presume aprovação do plano.
|
|
135
|
+
Se o inventário for truncado, `next` orienta inspeção manual, sem escolher uma
|
|
136
|
+
fase ou presumir que um projeto existente está vazio.
|
|
128
137
|
|
|
129
138
|
[Wiki](../README.md) · [Solução de problemas](troubleshooting.md)
|
|
@@ -39,6 +39,10 @@ agente; arquivos próprios também podem manter uma pasta após a desmarcação.
|
|
|
39
39
|
| `E_MANAGED_BLOCK` | Revise marcadores ou formato; preserve suas instruções. |
|
|
40
40
|
| `E_CHANGED` | Um arquivo mudou durante a operação; gere outro plano. |
|
|
41
41
|
|
|
42
|
+
No macOS, `/tmp` e `/var/folders` são aceitos quando forem os aliases de
|
|
43
|
+
sistema esperados. Destinos que atravessam links humanos continuam recusados,
|
|
44
|
+
inclusive quando apontados por `TMPDIR`, `TEMP` ou `TMP`.
|
|
45
|
+
|
|
42
46
|
## Conflitos e backups
|
|
43
47
|
|
|
44
48
|
Simule antes de substituir uma alteração própria:
|
package/docs/specs/README.md
CHANGED
|
@@ -28,6 +28,9 @@ description: Incrementos verificáveis do AgentDevKit.
|
|
|
28
28
|
- [Ignore de clientes](client-ignore/SPEC.md): padrão local e exceções do projeto.
|
|
29
29
|
- [Ferramenta e versões](tool-release/SPEC.md): atualização segura e tags de release.
|
|
30
30
|
- [Tags automáticas](automatic-release-tags/SPEC.md): publicação após CI aprovada.
|
|
31
|
+
- [Destinos temporários](target-paths/SPEC.md): caminhos corretos e links seguros.
|
|
32
|
+
- [Ordem antes da implementação](preimplementation-order/SPEC.md): produto,
|
|
33
|
+
spec e configuração antes do código.
|
|
31
34
|
- [Descoberta e preservação](discovery-preservation/SPEC.md): rotas de skills,
|
|
32
35
|
sincronização segura e validação em projetos reais.
|
|
33
36
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Orientação e verificação da ordem anterior à implementação.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Ordem antes da implementação
|
|
6
|
+
|
|
7
|
+
O projeto deve partir de decisões de produto e design, passar por roadmap e
|
|
8
|
+
spec e concluir a configuração técnica antes do código da aplicação.
|
|
9
|
+
|
|
10
|
+
## Critérios de aceite
|
|
11
|
+
|
|
12
|
+
- AC-001: `next` orienta roadmap e spec antes da configuração técnica e não
|
|
13
|
+
presume aprovação ou escolhe uma spec ambígua.
|
|
14
|
+
- AC-002: A verificação prévia informa bloqueio se o estado de preparação ou
|
|
15
|
+
configuração estiver ausente, inválido ou pendente; ela não modifica Git.
|
|
16
|
+
- AC-003: As skills exigem provas da infraestrutura antes de implementar e
|
|
17
|
+
registram como pendentes as provas de uma aplicação que ainda não existe.
|
|
18
|
+
- AC-004: Projetos existentes, arquivos humanos, links e índice são
|
|
19
|
+
preservados; testes e revisão cobrem decisões, lacunas e retomada.
|
|
20
|
+
|
|
21
|
+
O status registrado não comprova a evidência por si só. Instruções e scripts
|
|
22
|
+
não impedem escrita arbitrária por um agente que os ignore; essa proteção
|
|
23
|
+
depende das permissões ou hooks do cliente usado.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Resolução segura dos destinos de instalação.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Destinos de instalação
|
|
6
|
+
|
|
7
|
+
O instalador deve manter o caminho solicitado e preservar projetos existentes,
|
|
8
|
+
inclusive quando trabalha com diretórios temporários no macOS.
|
|
9
|
+
|
|
10
|
+
## Critérios de aceite
|
|
11
|
+
|
|
12
|
+
- AC-001: Destinos inexistentes preservam todos os nomes e sua ordem,
|
|
13
|
+
inclusive quando o primeiro componente está diretamente sob a raiz.
|
|
14
|
+
- AC-002: Os aliases temporários de sistema esperados no macOS funcionam para
|
|
15
|
+
instalação e consulta sem modificar o índice ou o `.gitignore` do projeto.
|
|
16
|
+
- AC-003: Destinos que atravessam links humanos são recusados, inclusive os
|
|
17
|
+
apontados por variáveis de ambiente; caminhos reservados continuam protegidos.
|
|
18
|
+
- AC-004: Testes de regressão, revisão independente e CI cobrem os cenários
|
|
19
|
+
pertinentes em Linux, Windows e macOS.
|
package/package.json
CHANGED
package/src/install.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, readSync, writeFileSync, readdirSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { synchronize, isReference } from '../.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
|
|
7
7
|
import { isGeneratedOutput, planLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, applyLocalIgnore, indexContentMatches } from '../.pbai4devs/scripts/lib/gitignore.mjs';
|
|
8
8
|
import { nativePathBoundary } from '../.pbai4devs/scripts/lib/native-paths.mjs';
|
|
@@ -138,12 +138,28 @@ const checkTarget = path => {
|
|
|
138
138
|
if (path.split(sep).some(part => reservedTargets.has(part.toLowerCase()))) fail('E_TARGET', 'Unsafe target directory.');
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
// Only normalize macOS aliases owned by the system. Environment-controlled
|
|
142
|
+
// TMPDIR is not evidence that a symbolic link is safe to follow.
|
|
143
|
+
function normalizeSystemTemporaryAlias(target) {
|
|
144
|
+
if (process.platform !== 'darwin') return target;
|
|
145
|
+
for (const { alias, allowed, physical } of [
|
|
146
|
+
{ alias: '/tmp', allowed: '/tmp', physical: '/private/tmp' },
|
|
147
|
+
{ alias: '/var', allowed: '/var/folders', physical: '/private/var' },
|
|
148
|
+
]) {
|
|
149
|
+
if (target !== allowed && !target.startsWith(`${allowed}${sep}`)) continue;
|
|
150
|
+
const link = stat(alias), directory = stat(physical);
|
|
151
|
+
if (!link?.isSymbolicLink() || link.uid !== 0 || !directory?.isDirectory() || directory.uid !== 0) return target;
|
|
152
|
+
try { if (realpathSync(alias) !== physical) return target; }
|
|
153
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ELOOP') return target; throw error; }
|
|
154
|
+
return join(physical, relative(alias, target));
|
|
155
|
+
}
|
|
156
|
+
return target;
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
export function resolveTarget(input, cwd = process.cwd()) {
|
|
142
160
|
let target = resolve(cwd, input || '.');
|
|
143
161
|
checkTarget(target);
|
|
144
|
-
|
|
145
|
-
const temporary = resolve(tmpdir()), insideTemporary = relative(temporary, target);
|
|
146
|
-
if (insideTemporary === '' || (!insideTemporary.startsWith(`..${sep}`) && insideTemporary !== '..' && !isAbsolute(insideTemporary))) target = join(realpathSync(temporary), insideTemporary);
|
|
162
|
+
target = normalizeSystemTemporaryAlias(target);
|
|
147
163
|
let ancestor = target;
|
|
148
164
|
while (true) {
|
|
149
165
|
const info = stat(ancestor);
|
|
@@ -151,7 +167,7 @@ export function resolveTarget(input, cwd = process.cwd()) {
|
|
|
151
167
|
const parent = dirname(ancestor); if (parent === ancestor) break; ancestor = parent;
|
|
152
168
|
}
|
|
153
169
|
const missing = []; let parent = target;
|
|
154
|
-
while (!stat(parent)) { missing.unshift(
|
|
170
|
+
while (!stat(parent)) { missing.unshift(basename(parent)); parent = dirname(parent); }
|
|
155
171
|
const info = stat(parent);
|
|
156
172
|
if (info.isSymbolicLink() || !info.isDirectory()) fail('E_TARGET', 'Target must be a real directory, without a symbolic link.');
|
|
157
173
|
const canonical = join(realpathSync(parent), ...missing);
|
package/src/next.mjs
CHANGED
|
@@ -8,9 +8,11 @@ const messages = {
|
|
|
8
8
|
install: 'Instale o AgentDevKit neste projeto.', verify: 'Confira a instalação.',
|
|
9
9
|
configure: 'Conclua a preparação inicial.', prepare: 'Prepare o ambiente existente.',
|
|
10
10
|
prd: 'Defina ou conclua o PRD.', ux: 'Defina UX/UI ou justifique sua não aplicação.',
|
|
11
|
-
design: 'Conclua o design técnico.',
|
|
12
|
-
spec: 'Defina a próxima spec.', roadmap: 'Revise o roadmap e escolha o próximo incremento.',
|
|
11
|
+
design: 'Conclua o design técnico.',
|
|
12
|
+
spec: 'Defina a próxima spec.', implement: 'Planeje a implementação da spec.', roadmap: 'Revise o roadmap e escolha o próximo incremento.',
|
|
13
13
|
inspect: 'Confira o estado do projeto antes de avançar.',
|
|
14
|
+
incomplete: 'O inventário do projeto está incompleto.',
|
|
15
|
+
incompleteInstruction: 'Inspecione manualmente a estrutura e os documentos antes de escolher uma etapa; não presuma que o projeto está vazio.',
|
|
14
16
|
ask: 'Use a skill indicada no agente; confirme as decisões ainda pendentes.',
|
|
15
17
|
existing: 'Preserve código e decisões existentes; não reinicie a definição do produto.',
|
|
16
18
|
},
|
|
@@ -18,9 +20,11 @@ const messages = {
|
|
|
18
20
|
install: 'Install AgentDevKit in this project.', verify: 'Check the installation.',
|
|
19
21
|
configure: 'Complete the initial preparation.', prepare: 'Prepare the existing environment.',
|
|
20
22
|
prd: 'Define or complete the PRD.', ux: 'Define UX/UI or document why it does not apply.',
|
|
21
|
-
design: 'Complete the technical design.',
|
|
22
|
-
spec: 'Define the next spec.', roadmap: 'Review the roadmap and select the next increment.',
|
|
23
|
+
design: 'Complete the technical design.',
|
|
24
|
+
spec: 'Define the next spec.', implement: 'Plan the spec implementation.', roadmap: 'Review the roadmap and select the next increment.',
|
|
23
25
|
inspect: 'Inspect the project before proceeding.',
|
|
26
|
+
incomplete: 'The project inventory is incomplete.',
|
|
27
|
+
incompleteInstruction: 'Inspect the structure and documents manually before choosing a step; do not assume the project is empty.',
|
|
24
28
|
ask: 'Use the indicated skill in your agent; confirm outstanding decisions.',
|
|
25
29
|
existing: 'Preserve existing code and decisions; do not restart product discovery.',
|
|
26
30
|
},
|
|
@@ -28,9 +32,11 @@ const messages = {
|
|
|
28
32
|
install: 'Instale AgentDevKit en este proyecto.', verify: 'Compruebe la instalación.',
|
|
29
33
|
configure: 'Complete la preparación inicial.', prepare: 'Prepare el entorno existente.',
|
|
30
34
|
prd: 'Defina o complete el PRD.', ux: 'Defina UX/UI o justifique que no se aplica.',
|
|
31
|
-
design: 'Complete el diseño técnico.',
|
|
32
|
-
spec: 'Defina la próxima spec.', roadmap: 'Revise el roadmap y elija el próximo incremento.',
|
|
35
|
+
design: 'Complete el diseño técnico.',
|
|
36
|
+
spec: 'Defina la próxima spec.', implement: 'Planifique la implementación de la spec.', roadmap: 'Revise el roadmap y elija el próximo incremento.',
|
|
33
37
|
inspect: 'Inspeccione el proyecto antes de continuar.',
|
|
38
|
+
incomplete: 'El inventario del proyecto está incompleto.',
|
|
39
|
+
incompleteInstruction: 'Inspeccione manualmente la estructura y los documentos antes de elegir una etapa; no suponga que el proyecto está vacío.',
|
|
34
40
|
ask: 'Use la skill indicada en su agente y confirme las decisiones pendientes.',
|
|
35
41
|
existing: 'Preserve el código y las decisiones existentes; no reinicie el producto.',
|
|
36
42
|
},
|
|
@@ -48,14 +54,21 @@ function commandLine(args, platform) {
|
|
|
48
54
|
function workflow(report) {
|
|
49
55
|
const state = report.maturity;
|
|
50
56
|
const app = state.signals.application;
|
|
57
|
+
if (report.scan.truncated) return ['incomplete', null];
|
|
51
58
|
if (app && (!state.configuration.present || !state.planning.ready)) return ['prepare', 'preparar-dev'];
|
|
52
59
|
if (state.configuration.preparacao !== 'concluido') return app ? ['prepare', 'preparar-dev'] : ['configure', 'configurar-projeto'];
|
|
53
60
|
if (state.planning.prd.status !== 'aprovado') return ['prd', 'criar-prd'];
|
|
54
61
|
if (!['aprovado', 'nao-aplicavel'].includes(state.planning.ux_ui.status)) return ['ux', 'criar-ux-ui'];
|
|
55
62
|
if (state.planning.design.status !== 'aprovado') return ['design', 'criar-design-doc'];
|
|
56
|
-
if (state.
|
|
57
|
-
if (app && !report.contract.complete) return ['prepare', 'preparar-dev'];
|
|
58
|
-
|
|
63
|
+
if (state.level === 'manutencao' || !state.releasesComplete) return ['roadmap', 'gerenciar-roadmap'];
|
|
64
|
+
if (app && state.configuration.configuracao_tecnica === 'concluido' && !report.contract.complete) return ['prepare', 'preparar-dev'];
|
|
65
|
+
const { roadmap, specs } = state.planning;
|
|
66
|
+
if (!roadmap?.present || !['planejado', 'aprovado'].includes(roadmap.status) || !specs?.path || !specs.complete || specs.items.length > 1) return ['roadmap', 'gerenciar-roadmap'];
|
|
67
|
+
if (!specs.items.length) return ['spec', 'criar-spec'];
|
|
68
|
+
const [spec] = specs.items;
|
|
69
|
+
if (!spec.linked || !['open', 'in-progress'].includes(spec.roadmapState) || !spec.status) return ['roadmap', 'gerenciar-roadmap'];
|
|
70
|
+
if (spec.status !== 'aprovado') return ['spec', 'criar-spec'];
|
|
71
|
+
return ['implement', 'implementar-spec'];
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
export function getNextSteps({ command, target, language = 'pt', installed, platform = process.platform }) {
|
|
@@ -68,6 +81,10 @@ export function getNextSteps({ command, target, language = 'pt', installed, plat
|
|
|
68
81
|
try { report = inspectProject(target); }
|
|
69
82
|
catch { return [verification]; }
|
|
70
83
|
const [key, skill] = workflow(report);
|
|
84
|
+
if (key === 'incomplete') {
|
|
85
|
+
const step = { label: text.incomplete, instruction: text.incompleteInstruction };
|
|
86
|
+
return ['init', 'update', 'sync', 'lang', 'agents', 'tools'].includes(command) ? [verification, step] : [step];
|
|
87
|
+
}
|
|
71
88
|
const inspector = join(target, '.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs');
|
|
72
89
|
const step = {
|
|
73
90
|
label: text[key],
|