@jaimevalasek/aioson 1.21.7 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/docs/en/1-understand/ecosystem-map.md +1 -1
- package/docs/en/2-start/initial-decisions.md +1 -1
- package/docs/en/4-agents/README.md +8 -7
- package/docs/en/4-agents/discovery-design-doc.md +150 -0
- package/docs/en/5-reference/cli-reference.md +42 -16
- package/docs/en/README.md +2 -2
- package/docs/pt/4-agentes/README.md +8 -6
- package/docs/pt/4-agentes/briefing-refiner.md +122 -0
- package/docs/pt/4-agentes/discovery-design-doc.md +133 -74
- package/docs/pt/4-agentes/scope-check.md +65 -0
- package/docs/pt/5-referencia/README.md +1 -0
- package/docs/pt/5-referencia/comandos-cli.md +5 -4
- package/docs/pt/5-referencia/feature-archive.md +1 -0
- package/docs/pt/5-referencia/feature-export.md +155 -0
- package/docs/pt/README.md +2 -2
- package/docs/pt/agentes.md +3 -1
- package/package.json +1 -1
- package/src/agent-manifests.js +14 -3
- package/src/agents.js +21 -20
- package/src/cli.js +72 -52
- package/src/commands/briefing.js +28 -150
- package/src/commands/commit-prepare.js +5 -2
- package/src/commands/feature-archive.js +48 -12
- package/src/commands/feature-close.js +40 -0
- package/src/commands/feature-export.js +242 -0
- package/src/commands/gate-check.js +8 -3
- package/src/commands/git-guard.js +58 -0
- package/src/commands/harness-gate.js +120 -0
- package/src/commands/harness-status.js +157 -0
- package/src/commands/harness.js +18 -1
- package/src/commands/live.js +120 -115
- package/src/commands/parallel-doctor.js +2 -1
- package/src/commands/pulse-update.js +2 -2
- package/src/commands/scan-project.js +12 -2
- package/src/commands/self-implement-loop.js +305 -5
- package/src/commands/workflow-next.js +477 -425
- package/src/constants.js +21 -11
- package/src/context-search.js +3 -0
- package/src/doctor.js +24 -8
- package/src/dossier/schema.js +4 -3
- package/src/harness/active-contract.js +41 -0
- package/src/harness/attempt-artifacts.js +95 -0
- package/src/harness/budget-guard.js +127 -0
- package/src/harness/circuit-breaker.js +7 -0
- package/src/harness/contract-schema.js +324 -0
- package/src/harness/criteria-runner.js +136 -0
- package/src/harness/git-baseline.js +204 -0
- package/src/harness/glob-match.js +126 -0
- package/src/harness/guard-events.js +71 -0
- package/src/harness/human-gate.js +182 -0
- package/src/harness/scope-guard.js +115 -0
- package/src/i18n/messages/en.js +24 -21
- package/src/i18n/messages/es.js +11 -9
- package/src/i18n/messages/fr.js +11 -9
- package/src/i18n/messages/pt-BR.js +24 -21
- package/src/lib/briefing-refiner/apply-feedback.js +134 -0
- package/src/lib/briefing-refiner/briefing-paths.js +41 -0
- package/src/lib/briefing-refiner/briefing-registry.js +204 -0
- package/src/lib/briefing-refiner/briefing-sections.js +110 -0
- package/src/lib/briefing-refiner/feedback-schema.js +122 -0
- package/src/lib/briefing-refiner/refinement-report.js +39 -0
- package/src/lib/briefing-refiner/review-html.js +230 -0
- package/src/lib/dev-resume.js +94 -45
- package/src/parser.js +8 -5
- package/src/preflight-engine.js +88 -84
- package/src/runtime-store.js +2 -0
- package/src/sandbox.js +17 -3
- package/template/.aioson/agents/analyst.md +27 -23
- package/template/.aioson/agents/architect.md +7 -3
- package/template/.aioson/agents/briefing-refiner.md +121 -0
- package/template/.aioson/agents/briefing.md +83 -74
- package/template/.aioson/agents/committer.md +8 -0
- package/template/.aioson/agents/copywriter.md +19 -7
- package/template/.aioson/agents/design-hybrid-forge.md +16 -5
- package/template/.aioson/agents/dev.md +68 -66
- package/template/.aioson/agents/deyvin.md +97 -90
- package/template/.aioson/agents/discover.md +2 -2
- package/template/.aioson/agents/discovery-design-doc.md +34 -30
- package/template/.aioson/agents/genome.md +82 -71
- package/template/.aioson/agents/neo.md +11 -3
- package/template/.aioson/agents/orache.md +10 -0
- package/template/.aioson/agents/orchestrator.md +68 -68
- package/template/.aioson/agents/pentester.md +15 -6
- package/template/.aioson/agents/pm.md +30 -25
- package/template/.aioson/agents/product.md +108 -108
- package/template/.aioson/agents/profiler-enricher.md +10 -0
- package/template/.aioson/agents/profiler-forge.md +10 -0
- package/template/.aioson/agents/profiler-researcher.md +11 -0
- package/template/.aioson/agents/qa.md +28 -20
- package/template/.aioson/agents/scope-check.md +176 -164
- package/template/.aioson/agents/setup.md +11 -1
- package/template/.aioson/agents/sheldon.md +38 -38
- package/template/.aioson/agents/site-forge.md +15 -6
- package/template/.aioson/agents/squad.md +12 -0
- package/template/.aioson/agents/tester.md +209 -209
- package/template/.aioson/agents/ux-ui.md +2 -2
- package/template/.aioson/agents/validator.md +10 -2
- package/template/.aioson/config.md +31 -28
- package/template/.aioson/docs/autopilot-handoff.md +46 -0
- package/template/.aioson/docs/dossier/agent-templates.md +191 -0
- package/template/.aioson/docs/dossier/schema.md +218 -0
- package/template/.claude/commands/aioson/agent/briefing-refiner.md +17 -0
- package/template/AGENTS.md +50 -47
- package/template/CLAUDE.md +29 -27
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validação de schema do harness-contract.json (loop-guardrails REQ-1).
|
|
5
|
+
*
|
|
6
|
+
* Responsabilidade distinta de `harness:validate` (que valida a IMPLEMENTAÇÃO
|
|
7
|
+
* contra criteria via @validator). Aqui valida-se o CONTRATO em si, no
|
|
8
|
+
* preflight do `self:loop` — um typo em `allowed_files` não pode desligar o
|
|
9
|
+
* scope guard silenciosamente. Mensagens usam "contract schema invalid",
|
|
10
|
+
* nunca "validation verdict".
|
|
11
|
+
*
|
|
12
|
+
* Retrocompat (REQ-11 / EC-12): contratos antigos (feature/contract_mode/
|
|
13
|
+
* governor/criteria) passam; campos novos são todos opcionais.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { validateGlobPattern } = require('./glob-match');
|
|
17
|
+
|
|
18
|
+
/** REQ-4 — defaults proibidos, sempre aplicados e não-removíveis (SEC-SBD-05). */
|
|
19
|
+
const DEFAULT_FORBIDDEN_GLOBS = Object.freeze([
|
|
20
|
+
'.env*',
|
|
21
|
+
'*.pem',
|
|
22
|
+
'*.key',
|
|
23
|
+
'secrets/**',
|
|
24
|
+
'.git/**',
|
|
25
|
+
'node_modules/**',
|
|
26
|
+
'package-lock.json',
|
|
27
|
+
'yarn.lock',
|
|
28
|
+
'pnpm-lock.yaml',
|
|
29
|
+
'npm-shrinkwrap.json',
|
|
30
|
+
'bun.lockb'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const HUMAN_GATE_THEMES = Object.freeze([
|
|
34
|
+
'payment_logic_change',
|
|
35
|
+
'auth_permission_change',
|
|
36
|
+
'database_destructive_change',
|
|
37
|
+
'publish'
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/** Mapa default tema→globs (requirements §2.1); override via human_gate.themes[].paths. */
|
|
41
|
+
const DEFAULT_THEME_PATHS = Object.freeze({
|
|
42
|
+
payment_logic_change: Object.freeze(['**/billing/**', '**/payment/**']),
|
|
43
|
+
auth_permission_change: Object.freeze(['**/auth/**']),
|
|
44
|
+
database_destructive_change: Object.freeze(['**/migrations/**']),
|
|
45
|
+
publish: Object.freeze([]) // gate de comando (REQ-13), nunca diff
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Presets do contract_mode (REQ-19). Preenchem apenas valores do governor
|
|
50
|
+
* NÃO definidos explicitamente no contrato; valor explícito sempre vence.
|
|
51
|
+
* `BALANCED` mantém o comportamento atual (nenhum preenchimento).
|
|
52
|
+
*/
|
|
53
|
+
const CONTRACT_PRESETS = Object.freeze({
|
|
54
|
+
safe: Object.freeze({
|
|
55
|
+
max_steps: 10,
|
|
56
|
+
error_streak_limit: 3,
|
|
57
|
+
cost_ceiling_tokens: 200000,
|
|
58
|
+
max_runtime_minutes: 30,
|
|
59
|
+
max_changed_files: 20,
|
|
60
|
+
max_diff_lines: 1500
|
|
61
|
+
}),
|
|
62
|
+
builder: Object.freeze({
|
|
63
|
+
max_steps: 30,
|
|
64
|
+
error_streak_limit: 5,
|
|
65
|
+
cost_ceiling_tokens: 1000000,
|
|
66
|
+
max_runtime_minutes: 120,
|
|
67
|
+
max_changed_files: 60,
|
|
68
|
+
max_diff_lines: 6000
|
|
69
|
+
}),
|
|
70
|
+
autopilot: Object.freeze({
|
|
71
|
+
max_steps: 50,
|
|
72
|
+
error_streak_limit: 8,
|
|
73
|
+
cost_ceiling_tokens: 3000000,
|
|
74
|
+
max_runtime_minutes: 360,
|
|
75
|
+
max_changed_files: null,
|
|
76
|
+
max_diff_lines: null
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const KNOWN_TOP_FIELDS = new Set([
|
|
81
|
+
'feature', 'contract_mode', 'governor', 'criteria',
|
|
82
|
+
'allowed_files', 'forbidden_files', 'human_gate'
|
|
83
|
+
]);
|
|
84
|
+
const KNOWN_GOVERNOR_FIELDS = new Set([
|
|
85
|
+
'max_steps', 'error_streak_limit', 'cost_ceiling_tokens',
|
|
86
|
+
'max_runtime_minutes', 'max_changed_files', 'max_diff_lines'
|
|
87
|
+
]);
|
|
88
|
+
const KNOWN_HUMAN_GATE_FIELDS = new Set(['required_for', 'themes']);
|
|
89
|
+
const KNOWN_THEME_FIELDS = new Set(['name', 'paths']);
|
|
90
|
+
const KNOWN_CRITERIA_FIELDS = new Set(['id', 'description', 'assertion', 'binary', 'verification']);
|
|
91
|
+
|
|
92
|
+
const VALID_MODES = new Set(['balanced', 'safe', 'builder', 'autopilot']);
|
|
93
|
+
|
|
94
|
+
function isPlainObject(v) {
|
|
95
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isPositiveInt(v) {
|
|
99
|
+
return Number.isInteger(v) && v > 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checkOptionalLimit(errors, field, value) {
|
|
103
|
+
if (value === undefined || value === null) return;
|
|
104
|
+
if (!isPositiveInt(value)) {
|
|
105
|
+
errors.push({ field, reason: 'must be a positive integer or null' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function checkGlobArray(errors, field, value) {
|
|
110
|
+
if (!Array.isArray(value)) {
|
|
111
|
+
errors.push({ field, reason: 'must be an array of glob strings' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
value.forEach((pattern, i) => {
|
|
115
|
+
const result = validateGlobPattern(pattern);
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
errors.push({ field: `${field}[${i}]`, reason: result.reason });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Valida o contrato. Retorna { ok, errors: [{field, reason}], warnings: [{field, reason}] }.
|
|
124
|
+
* `ok === false` quando há ao menos um erro — o preflight deve encerrar antes
|
|
125
|
+
* de qualquer execução (REQ-1).
|
|
126
|
+
*/
|
|
127
|
+
function validateContract(contract) {
|
|
128
|
+
const errors = [];
|
|
129
|
+
const warnings = [];
|
|
130
|
+
|
|
131
|
+
if (!isPlainObject(contract)) {
|
|
132
|
+
return { ok: false, errors: [{ field: '(root)', reason: 'contract must be a JSON object' }], warnings };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const key of Object.keys(contract)) {
|
|
136
|
+
if (!KNOWN_TOP_FIELDS.has(key)) {
|
|
137
|
+
errors.push({ field: key, reason: 'unknown field — check for typos (contract schema invalid)' });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof contract.feature !== 'string' || !contract.feature.trim()) {
|
|
142
|
+
errors.push({ field: 'feature', reason: 'must be a non-empty string' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (contract.contract_mode !== undefined) {
|
|
146
|
+
if (typeof contract.contract_mode !== 'string' || !VALID_MODES.has(contract.contract_mode.toLowerCase())) {
|
|
147
|
+
errors.push({ field: 'contract_mode', reason: 'must be one of: BALANCED, safe, builder, autopilot' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!isPlainObject(contract.governor)) {
|
|
152
|
+
errors.push({ field: 'governor', reason: 'must be an object' });
|
|
153
|
+
} else {
|
|
154
|
+
for (const key of Object.keys(contract.governor)) {
|
|
155
|
+
if (!KNOWN_GOVERNOR_FIELDS.has(key)) {
|
|
156
|
+
errors.push({ field: `governor.${key}`, reason: 'unknown field — check for typos (contract schema invalid)' });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const g = contract.governor;
|
|
160
|
+
if (g.max_steps !== undefined && g.max_steps !== null && !(Number.isInteger(g.max_steps) && g.max_steps >= 0)) {
|
|
161
|
+
errors.push({ field: 'governor.max_steps', reason: 'must be a non-negative integer' });
|
|
162
|
+
}
|
|
163
|
+
if (g.error_streak_limit !== undefined && g.error_streak_limit !== null && !(Number.isInteger(g.error_streak_limit) && g.error_streak_limit >= 0)) {
|
|
164
|
+
errors.push({ field: 'governor.error_streak_limit', reason: 'must be a non-negative integer' });
|
|
165
|
+
}
|
|
166
|
+
checkOptionalLimit(errors, 'governor.cost_ceiling_tokens', g.cost_ceiling_tokens);
|
|
167
|
+
checkOptionalLimit(errors, 'governor.max_runtime_minutes', g.max_runtime_minutes);
|
|
168
|
+
checkOptionalLimit(errors, 'governor.max_changed_files', g.max_changed_files);
|
|
169
|
+
checkOptionalLimit(errors, 'governor.max_diff_lines', g.max_diff_lines);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (contract.allowed_files !== undefined) {
|
|
173
|
+
checkGlobArray(errors, 'allowed_files', contract.allowed_files);
|
|
174
|
+
// EC-5: allowlist vazia bloquearia tudo — warning + tratada como ausente
|
|
175
|
+
if (Array.isArray(contract.allowed_files) && contract.allowed_files.length === 0) {
|
|
176
|
+
warnings.push({
|
|
177
|
+
field: 'allowed_files',
|
|
178
|
+
reason: 'empty allowlist would block every write — treated as absent'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (contract.forbidden_files !== undefined) {
|
|
184
|
+
checkGlobArray(errors, 'forbidden_files', contract.forbidden_files);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (contract.human_gate !== undefined) {
|
|
188
|
+
if (!isPlainObject(contract.human_gate)) {
|
|
189
|
+
errors.push({ field: 'human_gate', reason: 'must be an object' });
|
|
190
|
+
} else {
|
|
191
|
+
for (const key of Object.keys(contract.human_gate)) {
|
|
192
|
+
if (!KNOWN_HUMAN_GATE_FIELDS.has(key)) {
|
|
193
|
+
errors.push({ field: `human_gate.${key}`, reason: 'unknown field — check for typos (contract schema invalid)' });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const hg = contract.human_gate;
|
|
197
|
+
if (!Array.isArray(hg.required_for)) {
|
|
198
|
+
errors.push({ field: 'human_gate.required_for', reason: 'must be an array of themes (required when human_gate is present)' });
|
|
199
|
+
} else {
|
|
200
|
+
hg.required_for.forEach((theme, i) => {
|
|
201
|
+
if (!HUMAN_GATE_THEMES.includes(theme)) {
|
|
202
|
+
errors.push({ field: `human_gate.required_for[${i}]`, reason: `unknown theme "${theme}" — valid: ${HUMAN_GATE_THEMES.join(', ')}` });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (hg.themes !== undefined) {
|
|
207
|
+
if (!Array.isArray(hg.themes)) {
|
|
208
|
+
errors.push({ field: 'human_gate.themes', reason: 'must be an array' });
|
|
209
|
+
} else {
|
|
210
|
+
hg.themes.forEach((theme, i) => {
|
|
211
|
+
if (!isPlainObject(theme)) {
|
|
212
|
+
errors.push({ field: `human_gate.themes[${i}]`, reason: 'must be an object { name, paths }' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
for (const key of Object.keys(theme)) {
|
|
216
|
+
if (!KNOWN_THEME_FIELDS.has(key)) {
|
|
217
|
+
errors.push({ field: `human_gate.themes[${i}].${key}`, reason: 'unknown field — check for typos (contract schema invalid)' });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!HUMAN_GATE_THEMES.includes(theme.name)) {
|
|
221
|
+
errors.push({ field: `human_gate.themes[${i}].name`, reason: `unknown theme "${theme.name}" — valid: ${HUMAN_GATE_THEMES.join(', ')}` });
|
|
222
|
+
}
|
|
223
|
+
checkGlobArray(errors, `human_gate.themes[${i}].paths`, theme.paths);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (contract.criteria !== undefined) {
|
|
231
|
+
if (!Array.isArray(contract.criteria)) {
|
|
232
|
+
errors.push({ field: 'criteria', reason: 'must be an array' });
|
|
233
|
+
} else {
|
|
234
|
+
contract.criteria.forEach((criterion, i) => {
|
|
235
|
+
if (!isPlainObject(criterion)) {
|
|
236
|
+
errors.push({ field: `criteria[${i}]`, reason: 'must be an object' });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
for (const key of Object.keys(criterion)) {
|
|
240
|
+
if (!KNOWN_CRITERIA_FIELDS.has(key)) {
|
|
241
|
+
errors.push({ field: `criteria[${i}].${key}`, reason: 'unknown field — check for typos (contract schema invalid)' });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (typeof criterion.id !== 'string' || !criterion.id.trim()) {
|
|
245
|
+
errors.push({ field: `criteria[${i}].id`, reason: 'must be a non-empty string' });
|
|
246
|
+
}
|
|
247
|
+
if (criterion.verification !== undefined && (typeof criterion.verification !== 'string' || !criterion.verification.trim())) {
|
|
248
|
+
errors.push({ field: `criteria[${i}].verification`, reason: 'must be a non-empty shell command string when present' });
|
|
249
|
+
}
|
|
250
|
+
if (criterion.binary !== undefined && typeof criterion.binary !== 'boolean') {
|
|
251
|
+
errors.push({ field: `criteria[${i}].binary`, reason: 'must be a boolean' });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Resolve o contrato VALIDADO para sua forma efetiva:
|
|
262
|
+
* - preset do contract_mode preenche valores do governor não definidos (REQ-19);
|
|
263
|
+
* - `forbidden_files` mesclado com os defaults não-removíveis (REQ-4);
|
|
264
|
+
* - `allowed_files: []` tratado como ausente (EC-5);
|
|
265
|
+
* - mapa tema→paths resolvido (override substitui, não mescla).
|
|
266
|
+
*
|
|
267
|
+
* Não muta o contrato original; retorna um objeto efetivo para os guards.
|
|
268
|
+
*/
|
|
269
|
+
function resolveContract(contract) {
|
|
270
|
+
const mode = typeof contract.contract_mode === 'string'
|
|
271
|
+
? contract.contract_mode.toLowerCase()
|
|
272
|
+
: 'balanced';
|
|
273
|
+
const preset = CONTRACT_PRESETS[mode] || null;
|
|
274
|
+
|
|
275
|
+
const governor = { ...(contract.governor || {}) };
|
|
276
|
+
if (preset) {
|
|
277
|
+
for (const [key, value] of Object.entries(preset)) {
|
|
278
|
+
if (governor[key] === undefined) governor[key] = value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const forbidden = [
|
|
283
|
+
...DEFAULT_FORBIDDEN_GLOBS,
|
|
284
|
+
...(Array.isArray(contract.forbidden_files) ? contract.forbidden_files : [])
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const allowed = Array.isArray(contract.allowed_files) && contract.allowed_files.length > 0
|
|
288
|
+
? contract.allowed_files.slice()
|
|
289
|
+
: null;
|
|
290
|
+
|
|
291
|
+
const themePaths = { ...DEFAULT_THEME_PATHS };
|
|
292
|
+
const requiredFor = contract.human_gate && Array.isArray(contract.human_gate.required_for)
|
|
293
|
+
? contract.human_gate.required_for.slice()
|
|
294
|
+
: [];
|
|
295
|
+
if (contract.human_gate && Array.isArray(contract.human_gate.themes)) {
|
|
296
|
+
for (const theme of contract.human_gate.themes) {
|
|
297
|
+
if (theme && HUMAN_GATE_THEMES.includes(theme.name) && Array.isArray(theme.paths)) {
|
|
298
|
+
themePaths[theme.name] = theme.paths.slice();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
feature: contract.feature,
|
|
305
|
+
contract_mode: mode,
|
|
306
|
+
governor,
|
|
307
|
+
criteria: Array.isArray(contract.criteria) ? contract.criteria : [],
|
|
308
|
+
allowed_files: allowed,
|
|
309
|
+
forbidden_files: forbidden,
|
|
310
|
+
human_gate: {
|
|
311
|
+
required_for: requiredFor,
|
|
312
|
+
theme_paths: themePaths
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
DEFAULT_FORBIDDEN_GLOBS,
|
|
319
|
+
HUMAN_GATE_THEMES,
|
|
320
|
+
DEFAULT_THEME_PATHS,
|
|
321
|
+
CONTRACT_PRESETS,
|
|
322
|
+
validateContract,
|
|
323
|
+
resolveContract
|
|
324
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Avaliação determinística de criteria[].verification (loop-guardrails
|
|
5
|
+
* REQ-16/17 + D7).
|
|
6
|
+
*
|
|
7
|
+
* Reusa `executeInSandbox` (src/sandbox.js) — timeout, kill de process tree e
|
|
8
|
+
* redaction já resolvidos (EC-7). NÃO cria runner novo. Critério sem
|
|
9
|
+
* `verification` mantém o comportamento atual (não avaliado).
|
|
10
|
+
*
|
|
11
|
+
* Assinatura de falha (D7): sha1(criterion_id + exitCode + primeira linha
|
|
12
|
+
* não-vazia de stderr normalizada — paths absolutos, números e timestamps
|
|
13
|
+
* removidos). 2 ocorrências no RUN (não precisam ser consecutivas — EC-13,
|
|
14
|
+
* diferente do error_streak) → failure_signature_repeat + parada.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const crypto = require('node:crypto');
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CHECK_TIMEOUT_MS = 120000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normaliza uma linha de erro para assinatura estável (D7):
|
|
23
|
+
* remove paths absolutos, troca dígitos por '#' (line numbers, timestamps,
|
|
24
|
+
* durações) e colapsa espaços.
|
|
25
|
+
*/
|
|
26
|
+
function normalizeErrorLine(line) {
|
|
27
|
+
return String(line || '')
|
|
28
|
+
// paths absolutos (posix e windows, com ou sem drive letter)
|
|
29
|
+
.replace(/(?:[A-Za-z]:)?[\\/][^\s:'"()]+/g, '<path>')
|
|
30
|
+
// dígitos (line numbers, timestamps, ms)
|
|
31
|
+
.replace(/\d+/g, '#')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function firstNonEmptyLine(text) {
|
|
38
|
+
for (const line of String(text || '').split('\n')) {
|
|
39
|
+
if (line.trim()) return line;
|
|
40
|
+
}
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** sha1 hex da assinatura de falha (D7). */
|
|
45
|
+
function failureSignature(criterionId, exitCode, stderr) {
|
|
46
|
+
const normalized = normalizeErrorLine(firstNonEmptyLine(stderr));
|
|
47
|
+
return crypto
|
|
48
|
+
.createHash('sha1')
|
|
49
|
+
.update(`${criterionId}|${exitCode === null || exitCode === undefined ? 'null' : exitCode}|${normalized}`)
|
|
50
|
+
.digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Executa os critérios com `verification` via sandbox (REQ-16).
|
|
55
|
+
*
|
|
56
|
+
* @param {object} params
|
|
57
|
+
* @param {Array} params.criteria — criteria[] do contrato resolvido
|
|
58
|
+
* @param {string} params.cwd — raiz do projeto
|
|
59
|
+
* @param {number} [params.timeoutMs]
|
|
60
|
+
* @param {Function} [params.sandboxExec] — injeção para teste; default executeInSandbox
|
|
61
|
+
* @returns {Promise<Array<{id, command, exitCode, durationMs, stdout, stderr, timedOut, ok, signature}>>}
|
|
62
|
+
*/
|
|
63
|
+
async function runCriteria({ criteria = [], cwd, timeoutMs = DEFAULT_CHECK_TIMEOUT_MS, sandboxExec = null }) {
|
|
64
|
+
const exec = sandboxExec || require('../sandbox').executeInSandbox;
|
|
65
|
+
const checks = [];
|
|
66
|
+
for (const criterion of criteria) {
|
|
67
|
+
if (!criterion || typeof criterion.verification !== 'string' || !criterion.verification.trim()) {
|
|
68
|
+
continue; // sem verification = não avaliado automaticamente (REQ-16)
|
|
69
|
+
}
|
|
70
|
+
const startedAt = Date.now();
|
|
71
|
+
let result;
|
|
72
|
+
try {
|
|
73
|
+
result = await exec(criterion.verification, {
|
|
74
|
+
cwd,
|
|
75
|
+
timeout: timeoutMs,
|
|
76
|
+
intent: `criteria:${criterion.id}`
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
result = { ok: false, stdout: '', stderr: String(err.message || err), exitCode: null, timedOut: false };
|
|
80
|
+
}
|
|
81
|
+
const check = {
|
|
82
|
+
id: criterion.id,
|
|
83
|
+
command: criterion.verification,
|
|
84
|
+
exitCode: result.exitCode,
|
|
85
|
+
durationMs: Date.now() - startedAt,
|
|
86
|
+
stdout: result.stdout || '',
|
|
87
|
+
stderr: result.stderr || '',
|
|
88
|
+
timedOut: Boolean(result.timedOut),
|
|
89
|
+
ok: Boolean(result.ok)
|
|
90
|
+
};
|
|
91
|
+
// EC-7: timeout = check falho com assinatura própria (stderr do sandbox
|
|
92
|
+
// "Command timed out after Xms" normaliza estável via '#')
|
|
93
|
+
check.signature = check.ok ? null : failureSignature(check.id, check.exitCode, check.stderr);
|
|
94
|
+
checks.push(check);
|
|
95
|
+
}
|
|
96
|
+
return checks;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Registra assinaturas de falha no run (D7) e detecta repetições.
|
|
101
|
+
* Muta `progress.failure_signatures[]` (caller persiste).
|
|
102
|
+
*
|
|
103
|
+
* @returns {Array<{signature, criterion_id}>} repetições (>= 2 no run)
|
|
104
|
+
*/
|
|
105
|
+
function registerFailureSignatures(progress, failedChecks) {
|
|
106
|
+
if (!Array.isArray(progress.failure_signatures)) progress.failure_signatures = [];
|
|
107
|
+
const repeats = [];
|
|
108
|
+
for (const check of failedChecks) {
|
|
109
|
+
if (!check.signature) continue;
|
|
110
|
+
const priorOccurrences = progress.failure_signatures.filter((s) => s.signature === check.signature).length;
|
|
111
|
+
progress.failure_signatures.push({
|
|
112
|
+
signature: check.signature,
|
|
113
|
+
criterion_id: check.id,
|
|
114
|
+
recorded_at: new Date().toISOString()
|
|
115
|
+
});
|
|
116
|
+
if (priorOccurrences + 1 >= 2) {
|
|
117
|
+
repeats.push({ signature: check.signature, criterion_id: check.id });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return repeats;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Zera as assinaturas para um run novo (chamado no preflight junto do budget). */
|
|
124
|
+
function startRunSignatures(progress) {
|
|
125
|
+
progress.failure_signatures = [];
|
|
126
|
+
return progress;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
DEFAULT_CHECK_TIMEOUT_MS,
|
|
131
|
+
normalizeErrorLine,
|
|
132
|
+
failureSignature,
|
|
133
|
+
runCriteria,
|
|
134
|
+
registerFailureSignatures,
|
|
135
|
+
startRunSignatures
|
|
136
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Baseline git do self:loop (loop-guardrails REQ-2/3 + D2).
|
|
5
|
+
*
|
|
6
|
+
* Única fronteira `child_process` da Fase 1 além do sandbox — todo o I/O git
|
|
7
|
+
* dos guards vive aqui. Módulos consumidores recebem objetos puros.
|
|
8
|
+
*
|
|
9
|
+
* - `captureBaseline`: no preflight, grava HEAD + dirty_paths (porcelain) e o
|
|
10
|
+
* `git hash-object` dos dirty paths que casam `forbidden_files` (D2 — fecha
|
|
11
|
+
* EC-2: tentativa que re-modifica um path sujo proibido ainda viola).
|
|
12
|
+
* - `computeChangedSet`: pós-attempt, changed set = porcelain atual −
|
|
13
|
+
* dirty_paths do baseline. NUNCA `git diff --name-only` (não vê untracked,
|
|
14
|
+
* EC-1). Paths normalizados `/` (EC-6). Rename conta os dois paths (EC-3);
|
|
15
|
+
* deleção conta (EC-4).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { execFileSync } = require('node:child_process');
|
|
21
|
+
|
|
22
|
+
const { normalizePath, matchGlob, matchAny } = require('./glob-match');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Estado do framework: o próprio loop escreve progress.json/baseline.json/
|
|
26
|
+
* attempts/ sob `.aioson/` durante a execução — esses paths são excluídos do
|
|
27
|
+
* changed-set para não gerar falsa violação (mesmo precedente do git ingest
|
|
28
|
+
* do neural-chain, que exclui `.aioson/*`).
|
|
29
|
+
*/
|
|
30
|
+
const FRAMEWORK_STATE_GLOB = '.aioson/**';
|
|
31
|
+
|
|
32
|
+
function git(targetDir, gitArgs) {
|
|
33
|
+
return execFileSync('git', gitArgs, {
|
|
34
|
+
cwd: targetDir,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
37
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Map porcelain XY → status do schema §2.3. */
|
|
42
|
+
function porcelainStatus(xy) {
|
|
43
|
+
if (xy.includes('R') || xy.includes('C')) return 'renamed';
|
|
44
|
+
if (xy === '??') return 'added';
|
|
45
|
+
if (xy.includes('A')) return 'added';
|
|
46
|
+
if (xy.includes('D')) return 'deleted';
|
|
47
|
+
return 'modified';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parseia `git status --porcelain` em entradas { path, status }.
|
|
52
|
+
* Rename (`R old -> new`) produz DUAS entradas (EC-3).
|
|
53
|
+
* Exportada pura para teste determinístico.
|
|
54
|
+
*/
|
|
55
|
+
function parsePorcelain(output) {
|
|
56
|
+
const entries = [];
|
|
57
|
+
for (const rawLine of String(output || '').split('\n')) {
|
|
58
|
+
const line = rawLine.replace(/\r$/, '');
|
|
59
|
+
if (!line.trim()) continue;
|
|
60
|
+
const xy = line.slice(0, 2);
|
|
61
|
+
let rest = line.slice(3);
|
|
62
|
+
// porcelain pode citar paths com espaços/especiais entre aspas
|
|
63
|
+
const unquote = (p) => {
|
|
64
|
+
const trimmed = p.trim();
|
|
65
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
66
|
+
try { return JSON.parse(trimmed); } catch { return trimmed.slice(1, -1); }
|
|
67
|
+
}
|
|
68
|
+
return trimmed;
|
|
69
|
+
};
|
|
70
|
+
const status = porcelainStatus(xy);
|
|
71
|
+
if (status === 'renamed' && rest.includes(' -> ')) {
|
|
72
|
+
const [from, to] = rest.split(' -> ');
|
|
73
|
+
entries.push({ path: normalizePath(unquote(from)), status: 'renamed' });
|
|
74
|
+
entries.push({ path: normalizePath(unquote(to)), status: 'renamed' });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
entries.push({ path: normalizePath(unquote(rest)), status });
|
|
78
|
+
}
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readPorcelain(targetDir) {
|
|
83
|
+
// -uall: untracked listados arquivo a arquivo — sem ele o porcelain colapsa
|
|
84
|
+
// dirs novos (`?? secrets/`) e `secrets/**` não casaria o dir vazio de sufixo.
|
|
85
|
+
const entries = parsePorcelain(git(targetDir, ['status', '--porcelain', '-uall']));
|
|
86
|
+
return entries.filter((entry) => !matchGlob(FRAMEWORK_STATE_GLOB, entry.path));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readHead(targetDir) {
|
|
90
|
+
try {
|
|
91
|
+
return git(targetDir, ['rev-parse', 'HEAD']).trim();
|
|
92
|
+
} catch {
|
|
93
|
+
return null; // repo sem commits ainda
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `git hash-object` de um path do working tree; null para path
|
|
99
|
+
* inexistente/deletado (deleção posterior será detectada por hash null≠hash).
|
|
100
|
+
*/
|
|
101
|
+
function hashWorkingTreePath(targetDir, relPath) {
|
|
102
|
+
const abs = path.join(targetDir, relPath);
|
|
103
|
+
if (!fs.existsSync(abs) || fs.statSync(abs).isDirectory()) return null;
|
|
104
|
+
try {
|
|
105
|
+
return git(targetDir, ['hash-object', '--', relPath]).trim();
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Captura o baseline no preflight (REQ-2 + D2) e grava
|
|
113
|
+
* `.aioson/plans/{slug}/baseline.json`.
|
|
114
|
+
*
|
|
115
|
+
* @returns {{ baseline, warnings: [{path, reason}] }}
|
|
116
|
+
*/
|
|
117
|
+
function captureBaseline(targetDir, planDir, { forbiddenGlobs = [] } = {}) {
|
|
118
|
+
const dirtyEntries = readPorcelain(targetDir);
|
|
119
|
+
const dirtyPaths = dirtyEntries.map((e) => e.path);
|
|
120
|
+
|
|
121
|
+
// D2: hash apenas dos dirty paths que casam forbidden (conjunto bounded)
|
|
122
|
+
const forbiddenDirtyHashes = {};
|
|
123
|
+
const warnings = [];
|
|
124
|
+
for (const dirtyPath of dirtyPaths) {
|
|
125
|
+
const matched = matchAny(forbiddenGlobs, dirtyPath);
|
|
126
|
+
if (matched) {
|
|
127
|
+
forbiddenDirtyHashes[dirtyPath] = hashWorkingTreePath(targetDir, dirtyPath);
|
|
128
|
+
warnings.push({
|
|
129
|
+
path: dirtyPath,
|
|
130
|
+
reason: `dirty path matches forbidden glob "${matched}" at loop start — re-modification will be a scope violation`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const baseline = {
|
|
136
|
+
captured_at: new Date().toISOString(),
|
|
137
|
+
head: readHead(targetDir),
|
|
138
|
+
dirty_paths: dirtyPaths,
|
|
139
|
+
forbidden_dirty_hashes: forbiddenDirtyHashes
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
fs.mkdirSync(planDir, { recursive: true });
|
|
143
|
+
fs.writeFileSync(path.join(planDir, 'baseline.json'), JSON.stringify(baseline, null, 2), 'utf8');
|
|
144
|
+
|
|
145
|
+
return { baseline, warnings };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function loadBaseline(planDir) {
|
|
149
|
+
const baselinePath = path.join(planDir, 'baseline.json');
|
|
150
|
+
if (!fs.existsSync(baselinePath)) return null;
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Changed set da tentativa (REQ-3): porcelain atual − dirty_paths do baseline,
|
|
160
|
+
* MAIS dirty paths proibidos cujo hash mudou desde o baseline (D2 / EC-2).
|
|
161
|
+
*
|
|
162
|
+
* @returns {{ files: [{path, status}], rehashViolations: [{path, reason}] }}
|
|
163
|
+
*/
|
|
164
|
+
function computeChangedSet(targetDir, baseline) {
|
|
165
|
+
const current = readPorcelain(targetDir);
|
|
166
|
+
const baselineDirty = new Set((baseline && baseline.dirty_paths) || []);
|
|
167
|
+
|
|
168
|
+
const files = current.filter((entry) => !baselineDirty.has(entry.path));
|
|
169
|
+
|
|
170
|
+
const rehashViolations = [];
|
|
171
|
+
const hashes = (baseline && baseline.forbidden_dirty_hashes) || {};
|
|
172
|
+
for (const [dirtyPath, baselineHash] of Object.entries(hashes)) {
|
|
173
|
+
const currentHash = hashWorkingTreePath(targetDir, dirtyPath);
|
|
174
|
+
if (currentHash !== baselineHash) {
|
|
175
|
+
rehashViolations.push({
|
|
176
|
+
path: dirtyPath,
|
|
177
|
+
reason: 'forbidden dirty path was re-modified after baseline (content hash changed)'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { files, rehashViolations };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `git diff` da tentativa para attempts/{n}/diff.patch (should-have REQ-9). */
|
|
186
|
+
function captureDiffPatch(targetDir) {
|
|
187
|
+
try {
|
|
188
|
+
return git(targetDir, ['diff', 'HEAD']);
|
|
189
|
+
} catch {
|
|
190
|
+
try {
|
|
191
|
+
return git(targetDir, ['diff']);
|
|
192
|
+
} catch {
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
parsePorcelain,
|
|
200
|
+
captureBaseline,
|
|
201
|
+
loadBaseline,
|
|
202
|
+
computeChangedSet,
|
|
203
|
+
captureDiffPatch
|
|
204
|
+
};
|