@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/docs/en/1-understand/ecosystem-map.md +1 -1
  3. package/docs/en/2-start/initial-decisions.md +1 -1
  4. package/docs/en/4-agents/README.md +8 -7
  5. package/docs/en/4-agents/discovery-design-doc.md +150 -0
  6. package/docs/en/5-reference/cli-reference.md +42 -16
  7. package/docs/en/README.md +2 -2
  8. package/docs/pt/4-agentes/README.md +8 -6
  9. package/docs/pt/4-agentes/briefing-refiner.md +122 -0
  10. package/docs/pt/4-agentes/discovery-design-doc.md +133 -74
  11. package/docs/pt/4-agentes/scope-check.md +65 -0
  12. package/docs/pt/5-referencia/README.md +1 -0
  13. package/docs/pt/5-referencia/comandos-cli.md +5 -4
  14. package/docs/pt/5-referencia/feature-archive.md +1 -0
  15. package/docs/pt/5-referencia/feature-export.md +155 -0
  16. package/docs/pt/README.md +2 -2
  17. package/docs/pt/agentes.md +3 -1
  18. package/package.json +1 -1
  19. package/src/agent-manifests.js +14 -3
  20. package/src/agents.js +21 -20
  21. package/src/cli.js +72 -52
  22. package/src/commands/briefing.js +28 -150
  23. package/src/commands/commit-prepare.js +5 -2
  24. package/src/commands/feature-archive.js +48 -12
  25. package/src/commands/feature-close.js +40 -0
  26. package/src/commands/feature-export.js +242 -0
  27. package/src/commands/gate-check.js +8 -3
  28. package/src/commands/git-guard.js +58 -0
  29. package/src/commands/harness-gate.js +120 -0
  30. package/src/commands/harness-status.js +157 -0
  31. package/src/commands/harness.js +18 -1
  32. package/src/commands/live.js +120 -115
  33. package/src/commands/parallel-doctor.js +2 -1
  34. package/src/commands/pulse-update.js +2 -2
  35. package/src/commands/scan-project.js +12 -2
  36. package/src/commands/self-implement-loop.js +305 -5
  37. package/src/commands/workflow-next.js +477 -425
  38. package/src/constants.js +21 -11
  39. package/src/context-search.js +3 -0
  40. package/src/doctor.js +24 -8
  41. package/src/dossier/schema.js +4 -3
  42. package/src/harness/active-contract.js +41 -0
  43. package/src/harness/attempt-artifacts.js +95 -0
  44. package/src/harness/budget-guard.js +127 -0
  45. package/src/harness/circuit-breaker.js +7 -0
  46. package/src/harness/contract-schema.js +324 -0
  47. package/src/harness/criteria-runner.js +136 -0
  48. package/src/harness/git-baseline.js +204 -0
  49. package/src/harness/glob-match.js +126 -0
  50. package/src/harness/guard-events.js +71 -0
  51. package/src/harness/human-gate.js +182 -0
  52. package/src/harness/scope-guard.js +115 -0
  53. package/src/i18n/messages/en.js +24 -21
  54. package/src/i18n/messages/es.js +11 -9
  55. package/src/i18n/messages/fr.js +11 -9
  56. package/src/i18n/messages/pt-BR.js +24 -21
  57. package/src/lib/briefing-refiner/apply-feedback.js +134 -0
  58. package/src/lib/briefing-refiner/briefing-paths.js +41 -0
  59. package/src/lib/briefing-refiner/briefing-registry.js +204 -0
  60. package/src/lib/briefing-refiner/briefing-sections.js +110 -0
  61. package/src/lib/briefing-refiner/feedback-schema.js +122 -0
  62. package/src/lib/briefing-refiner/refinement-report.js +39 -0
  63. package/src/lib/briefing-refiner/review-html.js +230 -0
  64. package/src/lib/dev-resume.js +94 -45
  65. package/src/parser.js +8 -5
  66. package/src/preflight-engine.js +88 -84
  67. package/src/runtime-store.js +2 -0
  68. package/src/sandbox.js +17 -3
  69. package/template/.aioson/agents/analyst.md +27 -23
  70. package/template/.aioson/agents/architect.md +7 -3
  71. package/template/.aioson/agents/briefing-refiner.md +121 -0
  72. package/template/.aioson/agents/briefing.md +83 -74
  73. package/template/.aioson/agents/committer.md +8 -0
  74. package/template/.aioson/agents/copywriter.md +19 -7
  75. package/template/.aioson/agents/design-hybrid-forge.md +16 -5
  76. package/template/.aioson/agents/dev.md +68 -66
  77. package/template/.aioson/agents/deyvin.md +97 -90
  78. package/template/.aioson/agents/discover.md +2 -2
  79. package/template/.aioson/agents/discovery-design-doc.md +34 -30
  80. package/template/.aioson/agents/genome.md +82 -71
  81. package/template/.aioson/agents/neo.md +11 -3
  82. package/template/.aioson/agents/orache.md +10 -0
  83. package/template/.aioson/agents/orchestrator.md +68 -68
  84. package/template/.aioson/agents/pentester.md +15 -6
  85. package/template/.aioson/agents/pm.md +30 -25
  86. package/template/.aioson/agents/product.md +108 -108
  87. package/template/.aioson/agents/profiler-enricher.md +10 -0
  88. package/template/.aioson/agents/profiler-forge.md +10 -0
  89. package/template/.aioson/agents/profiler-researcher.md +11 -0
  90. package/template/.aioson/agents/qa.md +28 -20
  91. package/template/.aioson/agents/scope-check.md +176 -164
  92. package/template/.aioson/agents/setup.md +11 -1
  93. package/template/.aioson/agents/sheldon.md +38 -38
  94. package/template/.aioson/agents/site-forge.md +15 -6
  95. package/template/.aioson/agents/squad.md +12 -0
  96. package/template/.aioson/agents/tester.md +209 -209
  97. package/template/.aioson/agents/ux-ui.md +2 -2
  98. package/template/.aioson/agents/validator.md +10 -2
  99. package/template/.aioson/config.md +31 -28
  100. package/template/.aioson/docs/autopilot-handoff.md +46 -0
  101. package/template/.aioson/docs/dossier/agent-templates.md +191 -0
  102. package/template/.aioson/docs/dossier/schema.md +218 -0
  103. package/template/.claude/commands/aioson/agent/briefing-refiner.md +17 -0
  104. package/template/AGENTS.md +50 -47
  105. 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
+ };