@jaimevalasek/aioson 1.21.8 → 1.23.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 +950 -923
- package/package.json +1 -1
- package/src/agents.js +21 -20
- package/src/cli.js +31 -0
- package/src/commands/feature-close.js +40 -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-preview.js +74 -0
- package/src/commands/harness-retro.js +221 -0
- package/src/commands/harness-status.js +157 -0
- package/src/commands/harness.js +18 -1
- package/src/commands/self-implement-loop.js +315 -5
- package/src/commands/workflow-next.js +45 -2
- package/src/doctor.js +24 -8
- 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/preview-artifact.js +85 -0
- package/src/harness/scope-guard.js +115 -0
- package/src/i18n/messages/en.js +23 -0
- package/src/i18n/messages/es.js +32 -9
- package/src/i18n/messages/fr.js +32 -9
- package/src/i18n/messages/pt-BR.js +23 -0
- package/src/lib/dev-resume.js +94 -45
- package/src/lib/retro/retro-aggregate.js +192 -0
- package/src/lib/retro/retro-render.js +185 -0
- package/src/lib/retro/retro-sources.js +624 -0
- package/src/preflight-engine.js +88 -84
- package/template/.aioson/agents/analyst.md +4 -0
- package/template/.aioson/agents/architect.md +4 -0
- package/template/.aioson/agents/dev.md +14 -1
- package/template/.aioson/agents/discovery-design-doc.md +4 -0
- package/template/.aioson/agents/pentester.md +8 -0
- package/template/.aioson/agents/pm.md +10 -5
- package/template/.aioson/agents/qa.md +46 -14
- package/template/.aioson/agents/scope-check.md +176 -172
- package/template/.aioson/agents/sheldon.md +13 -0
- package/template/.aioson/agents/tester.md +17 -0
- package/template/.aioson/agents/validator.md +8 -0
- package/template/.aioson/config.md +31 -28
- package/template/.aioson/docs/autopilot-handoff.md +83 -0
- package/template/.aioson/rules/aioson-context-boundary.md +10 -8
- package/template/AGENTS.md +57 -57
- package/template/CLAUDE.md +33 -33
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `aioson harness:retro [path] --feature=<slug> | --last=<N> [--json] [--locale=<l>]`
|
|
5
|
+
*
|
|
6
|
+
* Minera deterministicamente a trilha de falhas já coletada e materializa um
|
|
7
|
+
* dossiê retrospectivo em `.aioson/context/retro/{slug}.md` (ou
|
|
8
|
+
* `window-last-{N}.md`). Leitura-apenas sobre as fontes; única escrita: o dossiê.
|
|
9
|
+
* Sem LLM, sem rede (requirements §5.1, RHO-lite).
|
|
10
|
+
*
|
|
11
|
+
* Exit codes (D4 — devolvidos em `result.exitCode`, propagados por cli.js:1649
|
|
12
|
+
* em --json e por process.exitCode no modo texto; mesmo caminho de código para
|
|
13
|
+
* fechar a classe recorrente exit-code-collapsed-in-json-mode):
|
|
14
|
+
* 0 sucesso (inclusive dossiê vazio)
|
|
15
|
+
* 1 erro de I/O inesperado
|
|
16
|
+
* 12 erro de input (slug inválido, flags conflitantes, feature inexistente)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
collectSources, resolvePassDate, resolveFeatureExists, enumerateClosedFeatures, relPath
|
|
24
|
+
} = require('../lib/retro/retro-sources');
|
|
25
|
+
const { aggregate } = require('../lib/retro/retro-aggregate');
|
|
26
|
+
const { renderDossier } = require('../lib/retro/retro-render');
|
|
27
|
+
|
|
28
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
29
|
+
const EXIT_OK = 0;
|
|
30
|
+
const EXIT_IO = 1;
|
|
31
|
+
const EXIT_INPUT = 12;
|
|
32
|
+
|
|
33
|
+
function tr(t, key, params, fallback) {
|
|
34
|
+
if (typeof t !== 'function') return fallback;
|
|
35
|
+
const msg = t(key, params);
|
|
36
|
+
return msg && msg !== key ? msg : fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Encerra em erro de input: registra mensagem + devolve resultado com exitCode. */
|
|
40
|
+
function inputError(logger, message, error) {
|
|
41
|
+
if (logger && typeof logger.error === 'function') logger.error(message);
|
|
42
|
+
process.exitCode = EXIT_INPUT;
|
|
43
|
+
return { ok: false, exitCode: EXIT_INPUT, error, message };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Datas `completed` do MANIFEST de arquivadas (fallback de ordenação). */
|
|
47
|
+
function readManifestDates(targetDir) {
|
|
48
|
+
const map = {};
|
|
49
|
+
try {
|
|
50
|
+
const text = fs.readFileSync(path.join(targetDir, '.aioson', 'context', 'done', 'MANIFEST.md'), 'utf8');
|
|
51
|
+
for (const line of text.split(/\r?\n/)) {
|
|
52
|
+
const m = line.match(/^\|\s*([a-z0-9][a-z0-9-]*)\s*\|\s*([0-9-]{8,10})\s*\|/i);
|
|
53
|
+
if (m) map[m[1]] = m[2].trim();
|
|
54
|
+
}
|
|
55
|
+
} catch { /* best-effort */ }
|
|
56
|
+
return map;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Ordena uma lista de slugs por data de PASS desc (trail vence → QA → MANIFEST).
|
|
61
|
+
* Undatáveis são excluídos com aviso, salvo se marcados como âncora obrigatória.
|
|
62
|
+
*/
|
|
63
|
+
function rankByPassDate(targetDir, slugs, { anchor = null } = {}) {
|
|
64
|
+
const manifest = readManifestDates(targetDir);
|
|
65
|
+
const dated = [];
|
|
66
|
+
const undated = [];
|
|
67
|
+
for (const slug of slugs) {
|
|
68
|
+
const d = resolvePassDate(targetDir, slug) || manifest[slug] || null;
|
|
69
|
+
if (d) dated.push({ slug, date: d });
|
|
70
|
+
else if (slug === anchor) dated.push({ slug, date: '' }); // âncora entra mesmo sem data
|
|
71
|
+
else undated.push(slug);
|
|
72
|
+
}
|
|
73
|
+
dated.sort((a, b) => {
|
|
74
|
+
if (a.slug === anchor) return -1;
|
|
75
|
+
if (b.slug === anchor) return 1;
|
|
76
|
+
if (a.date !== b.date) return a.date < b.date ? 1 : -1; // desc
|
|
77
|
+
return a.slug < b.slug ? -1 : 1;
|
|
78
|
+
});
|
|
79
|
+
return { ordered: dated.map((x) => x.slug), undated };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function runHarnessRetro({ args, options = {}, logger, t } = {}) {
|
|
83
|
+
const log = logger || { log() {}, error() {} };
|
|
84
|
+
const targetDir = path.resolve(process.cwd(), (args && args[0]) || '.');
|
|
85
|
+
|
|
86
|
+
const hasFeature = options.feature !== undefined && options.feature !== null
|
|
87
|
+
&& options.feature !== true && String(options.feature).length > 0;
|
|
88
|
+
const hasLast = options.last !== undefined && options.last !== null && options.last !== true;
|
|
89
|
+
|
|
90
|
+
// --- Validação de input (antes de qualquer toque no filesystem — REQ-8) ---
|
|
91
|
+
if (!hasFeature && !hasLast) {
|
|
92
|
+
return inputError(log,
|
|
93
|
+
tr(t, 'cli.harnessRetro.need_target', null, 'harness:retro requer --feature=<slug> ou --last=<N>'),
|
|
94
|
+
'missing_target');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let slug = null;
|
|
98
|
+
if (hasFeature) {
|
|
99
|
+
slug = String(options.feature).trim();
|
|
100
|
+
if (!SLUG_RE.test(slug)) {
|
|
101
|
+
return inputError(log,
|
|
102
|
+
tr(t, 'cli.harnessRetro.invalid_slug', { slug }, `Slug inválido: ${slug} (deve casar ^[a-z0-9][a-z0-9-]*$)`),
|
|
103
|
+
'invalid_slug');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let lastN = null;
|
|
108
|
+
if (hasLast) {
|
|
109
|
+
lastN = Number(options.last);
|
|
110
|
+
if (!Number.isInteger(lastN) || lastN < 1) {
|
|
111
|
+
return inputError(log,
|
|
112
|
+
tr(t, 'cli.harnessRetro.invalid_last', { value: String(options.last) }, `Valor inválido para --last: ${options.last} (use inteiro ≥ 1)`),
|
|
113
|
+
'invalid_last');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Resolve modo, janela e arquivo de saída -----------------------------
|
|
118
|
+
let mode;
|
|
119
|
+
let slugs;
|
|
120
|
+
let outRel;
|
|
121
|
+
const warnings = [];
|
|
122
|
+
|
|
123
|
+
if (hasFeature && !hasLast) {
|
|
124
|
+
if (!resolveFeatureExists(targetDir, slug)) {
|
|
125
|
+
return inputError(log,
|
|
126
|
+
tr(t, 'cli.harnessRetro.feature_not_found', { slug },
|
|
127
|
+
`Feature não encontrada: ${slug} (procurado em .aioson/context/, .aioson/plans/${slug}/, .aioson/context/features/${slug}/, .aioson/context/done/${slug}/)`),
|
|
128
|
+
'feature_not_found');
|
|
129
|
+
}
|
|
130
|
+
mode = 'feature';
|
|
131
|
+
slugs = [slug];
|
|
132
|
+
outRel = path.join('.aioson', 'context', 'retro', `${slug}.md`);
|
|
133
|
+
} else {
|
|
134
|
+
// Qualquer uso de --last produz window-last-{N}.md (D6, modo combinado incluído).
|
|
135
|
+
mode = 'window';
|
|
136
|
+
if (hasFeature && !resolveFeatureExists(targetDir, slug)) {
|
|
137
|
+
return inputError(log,
|
|
138
|
+
tr(t, 'cli.harnessRetro.feature_not_found', { slug },
|
|
139
|
+
`Feature não encontrada: ${slug}`),
|
|
140
|
+
'feature_not_found');
|
|
141
|
+
}
|
|
142
|
+
const closed = enumerateClosedFeatures(targetDir);
|
|
143
|
+
const pool = hasFeature ? [slug, ...closed.filter((s) => s !== slug)] : closed;
|
|
144
|
+
if (pool.length === 0) {
|
|
145
|
+
return inputError(log,
|
|
146
|
+
tr(t, 'cli.harnessRetro.no_closed_features', null, 'Nenhuma feature fechada em .aioson/context/done/ para minerar'),
|
|
147
|
+
'no_closed_features');
|
|
148
|
+
}
|
|
149
|
+
const { ordered, undated } = rankByPassDate(targetDir, pool, { anchor: hasFeature ? slug : null });
|
|
150
|
+
if (undated.length > 0) {
|
|
151
|
+
warnings.push(tr(t, 'cli.harnessRetro.undatable_excluded', { count: undated.length, slugs: undated.join(', ') },
|
|
152
|
+
`${undated.length} feature(s) sem data de PASS determinável excluída(s) da janela: ${undated.join(', ')}`));
|
|
153
|
+
}
|
|
154
|
+
if (lastN > ordered.length) {
|
|
155
|
+
warnings.push(tr(t, 'cli.harnessRetro.window_truncated', { n: lastN, available: ordered.length },
|
|
156
|
+
`--last=${lastN} excede features disponíveis (${ordered.length}); minerando todas`));
|
|
157
|
+
}
|
|
158
|
+
slugs = ordered.slice(0, lastN);
|
|
159
|
+
outRel = path.join('.aioson', 'context', 'retro', `window-last-${lastN}.md`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Mineração + agregação + render --------------------------------------
|
|
163
|
+
const sources = collectSources(targetDir, slugs);
|
|
164
|
+
const allWarnings = warnings.concat(sources.warnings);
|
|
165
|
+
const { candidates, observations } = aggregate(sources);
|
|
166
|
+
|
|
167
|
+
const outRelPosix = outRel.replaceAll('\\', '/');
|
|
168
|
+
const generatedAt = new Date().toISOString();
|
|
169
|
+
const markdown = renderDossier({
|
|
170
|
+
mode,
|
|
171
|
+
slug: mode === 'feature' ? slug : undefined,
|
|
172
|
+
windowN: mode === 'window' ? lastN : undefined,
|
|
173
|
+
featuresMined: sources.features_mined,
|
|
174
|
+
counts: sources.counts,
|
|
175
|
+
candidates,
|
|
176
|
+
observations,
|
|
177
|
+
minedPaths: sources.minedPaths,
|
|
178
|
+
warnings: allWarnings,
|
|
179
|
+
dossierRelPath: outRelPosix,
|
|
180
|
+
generatedAt
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// --- Escrita (única do comando) ------------------------------------------
|
|
184
|
+
const outAbs = path.join(targetDir, outRel);
|
|
185
|
+
try {
|
|
186
|
+
fs.mkdirSync(path.dirname(outAbs), { recursive: true }); // edge 7
|
|
187
|
+
fs.writeFileSync(outAbs, markdown, 'utf8'); // edge 8 (idempotente, sobrescreve)
|
|
188
|
+
} catch (err) {
|
|
189
|
+
log.error(tr(t, 'cli.harnessRetro.io_error', { error: err.message }, `Erro de I/O ao escrever o dossiê: ${err.message}`));
|
|
190
|
+
process.exitCode = EXIT_IO;
|
|
191
|
+
return { ok: false, exitCode: EXIT_IO, error: 'io_error', message: err.message };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const report = {
|
|
195
|
+
ok: true,
|
|
196
|
+
exitCode: EXIT_OK,
|
|
197
|
+
mode,
|
|
198
|
+
feature: mode === 'feature' ? slug : null,
|
|
199
|
+
window: mode === 'window' ? `last-${lastN}` : null,
|
|
200
|
+
features_mined: sources.features_mined,
|
|
201
|
+
output: outRelPosix,
|
|
202
|
+
candidates: candidates.length,
|
|
203
|
+
observations: observations.length,
|
|
204
|
+
sources: sources.counts,
|
|
205
|
+
warnings: allWarnings
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (candidates.length === 0 && observations.length === 0) {
|
|
209
|
+
log.log(tr(t, 'cli.harnessRetro.empty', { path: outRelPosix },
|
|
210
|
+
`Dossiê gerado sem propostas: ${outRelPosix} (fontes sem trilha minerável)`));
|
|
211
|
+
} else {
|
|
212
|
+
log.log(tr(t, 'cli.harnessRetro.written',
|
|
213
|
+
{ path: outRelPosix, candidates: candidates.length, observations: observations.length },
|
|
214
|
+
`Dossiê retrospectivo gerado: ${outRelPosix} (${candidates.length} candidatos, ${observations.length} observações)`));
|
|
215
|
+
}
|
|
216
|
+
for (const w of allWarnings) log.log(` ⚠ ${w}`);
|
|
217
|
+
|
|
218
|
+
return report;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { runHarnessRetro };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson harness:status . --slug=X [--json] — visibilidade do estado do loop
|
|
5
|
+
* (loop-guardrails REQ-18).
|
|
6
|
+
*
|
|
7
|
+
* Agrega: circuito, iteração N/M, budget, checks da última tentativa,
|
|
8
|
+
* última falha, gates pendentes e a próxima ação. Escopo distinto de
|
|
9
|
+
* `spec:status` (planos+learnings) — referenciado no rodapé.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
const { resolveContract, validateContract } = require('../harness/contract-schema');
|
|
16
|
+
const { pendingGates } = require('../harness/human-gate');
|
|
17
|
+
|
|
18
|
+
function readJson(file) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Última tentativa = maior diretório numérico em attempts/. */
|
|
27
|
+
function latestAttempt(planDir) {
|
|
28
|
+
const dir = path.join(planDir, 'attempts');
|
|
29
|
+
if (!fs.existsSync(dir)) return null;
|
|
30
|
+
const numbers = fs.readdirSync(dir)
|
|
31
|
+
.map((name) => Number(name))
|
|
32
|
+
.filter((n) => Number.isInteger(n) && n > 0);
|
|
33
|
+
return numbers.length ? Math.max(...numbers) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Conta checks pass/fail da tentativa pelos logs `# exit_code:`. */
|
|
37
|
+
function readChecks(planDir, attempt) {
|
|
38
|
+
const checksDir = path.join(planDir, 'attempts', String(attempt), 'checks');
|
|
39
|
+
if (!fs.existsSync(checksDir)) return { total: 0, passed: 0, failed: 0, failed_ids: [] };
|
|
40
|
+
const summary = { total: 0, passed: 0, failed: 0, failed_ids: [] };
|
|
41
|
+
for (const file of fs.readdirSync(checksDir)) {
|
|
42
|
+
if (!file.endsWith('.log')) continue;
|
|
43
|
+
summary.total += 1;
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(path.join(checksDir, file), 'utf8');
|
|
46
|
+
const match = content.match(/^# exit_code: (.+)$/m);
|
|
47
|
+
if (match && match[1].trim() === '0') {
|
|
48
|
+
summary.passed += 1;
|
|
49
|
+
} else {
|
|
50
|
+
summary.failed += 1;
|
|
51
|
+
summary.failed_ids.push(file.replace(/\.log$/, ''));
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
summary.failed += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return summary;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nextAction(progress, pending, slug) {
|
|
61
|
+
if (pending.length > 0) {
|
|
62
|
+
return `aioson harness:approve . --slug=${slug} --gate=${pending[0].id} (ou harness:reject --reason="...")`;
|
|
63
|
+
}
|
|
64
|
+
const status = progress?.status || 'unknown';
|
|
65
|
+
if (status === 'circuit_open' || progress?.circuit_state === 'OPEN') {
|
|
66
|
+
return 'circuito aberto — revisar last_error e corrigir antes de novo run';
|
|
67
|
+
}
|
|
68
|
+
if (status === 'waiting_validation') {
|
|
69
|
+
return `aioson harness:validate . --slug=${slug}`;
|
|
70
|
+
}
|
|
71
|
+
if (progress?.ready_for_done_gate) {
|
|
72
|
+
return `pronto para o done gate — aioson feature:close . --feature=${slug}`;
|
|
73
|
+
}
|
|
74
|
+
return `re-executar self:loop para continuar (iteração persiste em progress.json)`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runHarnessStatus({ args, options = {}, logger }) {
|
|
78
|
+
const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
|
|
79
|
+
const slug = String(options.slug || '').trim();
|
|
80
|
+
|
|
81
|
+
if (!slug) {
|
|
82
|
+
logger.error('Error: --slug is required');
|
|
83
|
+
return { ok: false, error: 'missing_slug' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const planDir = path.join(targetDir, '.aioson', 'plans', slug);
|
|
87
|
+
const contract = readJson(path.join(planDir, 'harness-contract.json'));
|
|
88
|
+
if (!contract) {
|
|
89
|
+
logger.error(`Contract not found for slug: ${slug}`);
|
|
90
|
+
return { ok: false, error: 'contract_not_found' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const progress = readJson(path.join(planDir, 'progress.json'));
|
|
94
|
+
const schema = validateContract(contract);
|
|
95
|
+
const resolved = schema.ok ? resolveContract(contract) : null;
|
|
96
|
+
const pending = pendingGates(planDir);
|
|
97
|
+
const attempt = latestAttempt(planDir);
|
|
98
|
+
const checks = attempt ? readChecks(planDir, attempt) : { total: 0, passed: 0, failed: 0, failed_ids: [] };
|
|
99
|
+
|
|
100
|
+
const maxSteps = resolved ? resolved.governor.max_steps : (contract.governor && contract.governor.max_steps) || null;
|
|
101
|
+
const ceiling = resolved ? (resolved.governor.cost_ceiling_tokens ?? null) : null;
|
|
102
|
+
const budget = progress?.budget || null;
|
|
103
|
+
|
|
104
|
+
const report = {
|
|
105
|
+
ok: true,
|
|
106
|
+
slug,
|
|
107
|
+
contract_mode: contract.contract_mode || 'BALANCED',
|
|
108
|
+
contract_schema_ok: schema.ok,
|
|
109
|
+
circuit_state: progress?.circuit_state || 'CLOSED',
|
|
110
|
+
status: progress?.status || 'unknown',
|
|
111
|
+
iterations: progress?.iterations ?? 0,
|
|
112
|
+
max_steps: maxSteps,
|
|
113
|
+
budget: budget ? {
|
|
114
|
+
tokens_estimated: budget.tokens_estimated || 0,
|
|
115
|
+
cost_ceiling_tokens: ceiling,
|
|
116
|
+
run_id: budget.run_id || null,
|
|
117
|
+
run_started_at: budget.run_started_at || null
|
|
118
|
+
} : null,
|
|
119
|
+
last_attempt: attempt,
|
|
120
|
+
checks,
|
|
121
|
+
last_error: progress?.last_error || null,
|
|
122
|
+
pending_gates: pending.map((g) => ({ id: g.id, theme: g.theme, attempt: g.attempt, requested_at: g.requested_at })),
|
|
123
|
+
next_action: nextAction(progress, pending, slug)
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (options.json) {
|
|
127
|
+
logger.log(JSON.stringify(report, null, 2));
|
|
128
|
+
return report;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.log(`Harness status — ${slug}`);
|
|
132
|
+
logger.log(` Mode: ${report.contract_mode}${schema.ok ? '' : ' (⚠ contract schema invalid)'}`);
|
|
133
|
+
logger.log(` Circuit: ${report.circuit_state} | status: ${report.status}`);
|
|
134
|
+
logger.log(` Iteration: ${report.iterations}${maxSteps ? `/${maxSteps}` : ''}`);
|
|
135
|
+
if (budget) {
|
|
136
|
+
logger.log(` Budget: ${report.budget.tokens_estimated} tokens estimados${ceiling ? ` / ${ceiling} (${Math.round((report.budget.tokens_estimated / ceiling) * 100)}%)` : ' (sem teto)'}`);
|
|
137
|
+
}
|
|
138
|
+
if (attempt) {
|
|
139
|
+
logger.log(` Last attempt: ${attempt} — checks ${checks.passed}/${checks.total} pass${checks.failed ? ` (failed: ${checks.failed_ids.join(', ')})` : ''}`);
|
|
140
|
+
}
|
|
141
|
+
if (report.last_error) logger.log(` Last error: ${report.last_error}`);
|
|
142
|
+
if (pending.length > 0) {
|
|
143
|
+
logger.log(` ⛔ Pending gates (${pending.length}):`);
|
|
144
|
+
for (const gate of pending) {
|
|
145
|
+
logger.log(` - ${gate.id} [${gate.theme}] attempt ${gate.attempt}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
logger.log(` Next: ${report.next_action}`);
|
|
149
|
+
logger.log('');
|
|
150
|
+
logger.log(' Planos e learnings: aioson spec:status');
|
|
151
|
+
|
|
152
|
+
return report;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
runHarnessStatus
|
|
157
|
+
};
|
package/src/commands/harness.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { createCircuitBreaker } = require('../harness/circuit-breaker');
|
|
6
|
+
const { validateContract } = require('../harness/contract-schema');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* aioson harness:init — Inicializa o contrato e progresso da feature.
|
|
@@ -36,8 +37,17 @@ async function runHarnessInit({ args, options = {}, logger, t }) {
|
|
|
36
37
|
governor: {
|
|
37
38
|
max_steps: 50,
|
|
38
39
|
error_streak_limit: 5,
|
|
39
|
-
cost_ceiling_tokens: null
|
|
40
|
+
cost_ceiling_tokens: null,
|
|
41
|
+
max_runtime_minutes: null,
|
|
42
|
+
max_changed_files: null,
|
|
43
|
+
max_diff_lines: null
|
|
40
44
|
},
|
|
45
|
+
// Scope guard (loop-guardrails): allowed_files ausente = sem allowlist;
|
|
46
|
+
// forbidden_files é SEMPRE mesclado com os defaults embutidos (.env*, *.pem,
|
|
47
|
+
// *.key, secrets/**, .git/**, node_modules/**, lockfiles) — não-removíveis.
|
|
48
|
+
forbidden_files: [],
|
|
49
|
+
// human_gate ausente = nenhum gate (retrocompat). Exemplo:
|
|
50
|
+
// "human_gate": { "required_for": ["payment_logic_change", "publish"] }
|
|
41
51
|
criteria: [
|
|
42
52
|
{
|
|
43
53
|
id: "C1",
|
|
@@ -48,6 +58,13 @@ async function runHarnessInit({ args, options = {}, logger, t }) {
|
|
|
48
58
|
]
|
|
49
59
|
};
|
|
50
60
|
|
|
61
|
+
const schemaResult = validateContract(contract);
|
|
62
|
+
if (!schemaResult.ok) {
|
|
63
|
+
const first = schemaResult.errors[0];
|
|
64
|
+
logger.error(`Contract schema invalid: ${first.field} — ${first.reason}`);
|
|
65
|
+
return { ok: false, error: 'contract_schema_invalid', errors: schemaResult.errors };
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
const cb = createCircuitBreaker(contractPath, progressPath);
|
|
52
69
|
fs.writeFileSync(contractPath, JSON.stringify(contract, null, 2), 'utf8');
|
|
53
70
|
await cb.load();
|