@jaimevalasek/aioson 1.23.3 → 1.28.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 +56 -0
- package/docs/en/4-agents/README.md +11 -8
- package/docs/en/4-agents/forge-run.md +165 -0
- package/docs/en/5-reference/README.md +1 -0
- package/docs/en/5-reference/cli-reference.md +199 -85
- package/docs/en/5-reference/executable-verification.md +165 -0
- package/docs/pt/4-agentes/README.md +2 -1
- package/docs/pt/4-agentes/forge-run.md +150 -0
- package/docs/pt/4-agentes/pm.md +8 -0
- package/docs/pt/4-agentes/qa.md +2 -0
- package/docs/pt/4-agentes/scope-check.md +19 -1
- package/docs/pt/4-agentes/sheldon.md +2 -0
- package/docs/pt/4-agentes/validator.md +20 -0
- package/docs/pt/5-referencia/autopilot-handoff.md +33 -0
- package/docs/pt/5-referencia/comandos-cli.md +64 -9
- package/docs/pt/5-referencia/fluxo-artefatos.md +40 -15
- package/docs/pt/5-referencia/loop-guardrails.md +19 -0
- package/docs/pt/5-referencia/sdd-automation-scripts.md +130 -26
- package/package.json +1 -1
- package/src/cli.js +70 -54
- package/src/commands/forge-compile.js +330 -0
- package/src/commands/harness-check.js +159 -0
- package/src/commands/harness.js +37 -2
- package/src/commands/spec-analyze.js +324 -0
- package/src/constants.js +118 -108
- package/src/harness/contract-schema.js +8 -0
- package/src/harness/plan-waves.js +77 -0
- package/src/harness/review-payload.js +230 -0
- package/src/i18n/messages/en.js +21 -15
- package/src/i18n/messages/es.js +15 -13
- package/src/i18n/messages/fr.js +15 -13
- package/src/i18n/messages/pt-BR.js +21 -15
- package/src/parser.js +3 -1
- package/template/.aioson/agents/dev.md +67 -66
- package/template/.aioson/agents/forge-run.md +57 -0
- package/template/.aioson/agents/pm.md +51 -45
- package/template/.aioson/agents/qa.md +22 -22
- package/template/.aioson/agents/scope-check.md +49 -46
- package/template/.aioson/agents/sheldon.md +1 -1
- package/template/.aioson/agents/validator.md +16 -5
- package/template/.aioson/docs/autopilot-handoff.md +34 -32
- package/template/.aioson/docs/sheldon/harness-contract.md +19 -2
- package/template/.claude/commands/aioson/agent/forge-run.md +17 -0
- package/template/AGENTS.md +15 -13
- package/template/CLAUDE.md +10 -9
- package/template/OPENCODE.md +24 -23
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson forge:compile — compila os artefatos SDD de uma feature MEDIUM num
|
|
5
|
+
* workflow script determinístico (Lane B / Fase 5 do plano de verificação
|
|
6
|
+
* executável).
|
|
7
|
+
*
|
|
8
|
+
* Entradas (todas produzidas pelas fases 1-4):
|
|
9
|
+
* - `.aioson/plans/{slug}/harness-contract.json` — critérios binários com
|
|
10
|
+
* `verification` (convergência) + governor (bounds do loop)
|
|
11
|
+
* - `.aioson/context/implementation-plan-{slug}.md` — Execution Sequence com
|
|
12
|
+
* coluna Wave (fases disjuntas em arquivos = estágios paralelos)
|
|
13
|
+
* - `spec:analyze` limpo — erros bloqueiam a compilação; `wave_file_overlap`
|
|
14
|
+
* (warning no analyze) é ERRO aqui: compilar paralelismo sobre arquivos
|
|
15
|
+
* sobrepostos é pedir merge conflict.
|
|
16
|
+
*
|
|
17
|
+
* Saída: `.aioson/plans/{slug}/forge-run.workflow.js` — script auditável e
|
|
18
|
+
* versionável (commita junto da spec) para o runtime de dynamic workflows do
|
|
19
|
+
* Claude Code. O script NUNCA roda feature:close — o fechamento é gate humano.
|
|
20
|
+
*
|
|
21
|
+
* Restrições do runtime respeitadas no código gerado: `export const meta`
|
|
22
|
+
* literal puro, JS plano (sem TS), sem Date.now()/Math.random()/new Date(),
|
|
23
|
+
* fix-loop limitado pelo governor, fixes sequenciais (criterios podem
|
|
24
|
+
* compartilhar arquivos — paralelo só onde as waves provam disjunção).
|
|
25
|
+
* Texto vindo de artefatos entra no script via JSON.stringify (nunca
|
|
26
|
+
* interpolação crua — neutraliza injeção de template/backtick).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
|
|
32
|
+
const { validateContract, resolveContract } = require('../harness/contract-schema');
|
|
33
|
+
const { parseExecutionWaves, groupByWave } = require('../harness/plan-waves');
|
|
34
|
+
|
|
35
|
+
const ADVERSARIAL_VOTES = 3;
|
|
36
|
+
const DEFAULT_MAX_FIX_ROUNDS = 5;
|
|
37
|
+
|
|
38
|
+
/** String JS segura (literal via JSON.stringify — nunca interpolar cru). */
|
|
39
|
+
function js(value) {
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildDevPrompt(slug, row) {
|
|
44
|
+
return [
|
|
45
|
+
`You are @dev executing ONLY phase ${row.phase} of feature ${slug} (AIOSON Lane B).`,
|
|
46
|
+
`Read .aioson/context/implementation-plan-${slug}.md (Required Context Package + Pre-Taken Decisions) and follow .aioson/agents/dev.md conventions.`,
|
|
47
|
+
`Phase scope: ${row.scope || '(see plan)'}.`,
|
|
48
|
+
`You may ONLY create/modify these files and their tests: ${row.files.length ? row.files.join(', ') : '(see plan phase row)'}. Other phases run concurrently on disjoint files — never touch files outside your list.`,
|
|
49
|
+
`Done criteria for this phase: ${row.done || '(see plan)'}.`,
|
|
50
|
+
'Implement, run the project tests for your scope, and report. Your final message is machine-consumed.'
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildScript({ slug, waves, execCriteria, judgedCriteria, maxFixRounds }) {
|
|
55
|
+
const metaPhases = [
|
|
56
|
+
...waves.map((w) => ({
|
|
57
|
+
title: `Wave ${w.wave}`,
|
|
58
|
+
detail: w.phases.map((p) => `phase ${p.phase}`).join(' + ')
|
|
59
|
+
})),
|
|
60
|
+
{ title: 'Verify', detail: 'deterministic harness:check + bounded fix loop + adversarial review' },
|
|
61
|
+
{ title: 'Validate', detail: 'fresh-context validator verdict through the harness:validate cycle' }
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const meta = {
|
|
65
|
+
name: `forge-run-${slug}`,
|
|
66
|
+
description: `AIOSON Lane B compiled harness for feature ${slug}`,
|
|
67
|
+
phases: metaPhases
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const lines = [];
|
|
71
|
+
lines.push(`export const meta = ${JSON.stringify(meta, null, 2)}`);
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(`// Compiled by \`aioson forge:compile\` from harness-contract.json +`);
|
|
74
|
+
lines.push(`// implementation-plan-${slug}.md. Regenerate instead of hand-editing.`);
|
|
75
|
+
lines.push(`// HUMAN GATE: this script never runs feature:close/publish.`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(`const SLUG = ${js(slug)}`);
|
|
78
|
+
lines.push(`const MAX_FIX_ROUNDS = ${maxFixRounds}`);
|
|
79
|
+
lines.push(`const EXEC_CRITERIA = ${JSON.stringify(execCriteria, null, 2)}`);
|
|
80
|
+
lines.push(`const JUDGED_CRITERIA = ${JSON.stringify(judgedCriteria, null, 2)}`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(`const SUMMARY_SCHEMA = {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
phase: { type: 'string' },
|
|
86
|
+
status: { type: 'string', enum: ['done', 'blocked'] },
|
|
87
|
+
summary: { type: 'string' },
|
|
88
|
+
files_changed: { type: 'array', items: { type: 'string' } },
|
|
89
|
+
blockers: { type: ['string', 'null'] }
|
|
90
|
+
},
|
|
91
|
+
required: ['phase', 'status', 'summary']
|
|
92
|
+
}`);
|
|
93
|
+
lines.push(`const CHECK_SCHEMA = {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
ok: { type: 'boolean' },
|
|
97
|
+
passed: { type: 'number' },
|
|
98
|
+
failed: { type: 'number' },
|
|
99
|
+
skipped_no_verification: { type: 'number' },
|
|
100
|
+
checks: {
|
|
101
|
+
type: 'array',
|
|
102
|
+
items: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
id: { type: 'string' },
|
|
106
|
+
command: { type: 'string' },
|
|
107
|
+
exitCode: { type: ['number', 'null'] },
|
|
108
|
+
ok: { type: 'boolean' }
|
|
109
|
+
},
|
|
110
|
+
required: ['id', 'ok']
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
required: ['ok', 'checks']
|
|
115
|
+
}`);
|
|
116
|
+
lines.push(`const REFUTE_SCHEMA = {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
refuted: { type: 'boolean' },
|
|
120
|
+
reason: { type: 'string' }
|
|
121
|
+
},
|
|
122
|
+
required: ['refuted', 'reason']
|
|
123
|
+
}`);
|
|
124
|
+
lines.push(`const APPLY_SCHEMA = {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
verdict: { type: 'string' },
|
|
128
|
+
ready_for_done_gate: { type: 'boolean' },
|
|
129
|
+
last_error: { type: ['string', 'null'] }
|
|
130
|
+
},
|
|
131
|
+
required: ['verdict']
|
|
132
|
+
}`);
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push('const waveResults = []');
|
|
135
|
+
|
|
136
|
+
for (const wave of waves) {
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push(`phase(${js(`Wave ${wave.wave}`)})`);
|
|
139
|
+
lines.push(`const wave${wave.wave} = await parallel([`);
|
|
140
|
+
for (const row of wave.phases) {
|
|
141
|
+
lines.push(` () => agent(${js(buildDevPrompt(slug, row))}, { label: ${js(`dev:phase-${row.phase}`)}, phase: ${js(`Wave ${wave.wave}`)}, schema: SUMMARY_SCHEMA }),`);
|
|
142
|
+
}
|
|
143
|
+
lines.push('])');
|
|
144
|
+
lines.push(`waveResults.push(...wave${wave.wave}.filter(Boolean))`);
|
|
145
|
+
lines.push(`const blocked${wave.wave} = wave${wave.wave}.filter(Boolean).filter(r => r.status === 'blocked')`);
|
|
146
|
+
lines.push(`if (blocked${wave.wave}.length > 0) {`);
|
|
147
|
+
lines.push(` log('wave ${wave.wave} blocked: ' + blocked${wave.wave}.map(r => r.phase).join(', ') + ' — stopping before downstream waves')`);
|
|
148
|
+
lines.push(` return { slug: SLUG, stopped_at: 'Wave ${wave.wave}', waves: waveResults, blocked: blocked${wave.wave} }`);
|
|
149
|
+
lines.push('}');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push(`phase('Verify')`);
|
|
154
|
+
lines.push(`const checkPrompt = 'Run exactly: aioson harness:check . --slug=' + SLUG + ' --json — and return that JSON object as your structured output, unmodified. Do not fix anything in this step.'`);
|
|
155
|
+
lines.push('let checkReport = null');
|
|
156
|
+
lines.push('let fixRound = 0');
|
|
157
|
+
lines.push('while (true) {');
|
|
158
|
+
lines.push(` checkReport = await agent(checkPrompt, { label: 'check:run', phase: 'Verify', schema: CHECK_SCHEMA })`);
|
|
159
|
+
lines.push(' const failed = (checkReport && checkReport.checks ? checkReport.checks : []).filter(c => !c.ok)');
|
|
160
|
+
lines.push(' if (failed.length === 0) break');
|
|
161
|
+
lines.push(' if (fixRound >= MAX_FIX_ROUNDS) {');
|
|
162
|
+
lines.push(` log('governor: error_streak_limit (' + MAX_FIX_ROUNDS + ') reached with ' + failed.length + ' criteria still failing — human review required')`);
|
|
163
|
+
lines.push(' break');
|
|
164
|
+
lines.push(' }');
|
|
165
|
+
lines.push(' if (budget.total && budget.remaining() < 30000) {');
|
|
166
|
+
lines.push(` log('governor: token budget nearly exhausted — stopping fix loop')`);
|
|
167
|
+
lines.push(' break');
|
|
168
|
+
lines.push(' }');
|
|
169
|
+
lines.push(' fixRound += 1');
|
|
170
|
+
lines.push(` log('fix round ' + fixRound + '/' + MAX_FIX_ROUNDS + ': ' + failed.map(c => c.id).join(', '))`);
|
|
171
|
+
lines.push(' // Fixes são SEQUENCIAIS de propósito: criterios não provam disjunção de');
|
|
172
|
+
lines.push(' // arquivos entre si (só as waves provam) — paralelizar aqui convida conflito.');
|
|
173
|
+
lines.push(' for (const failure of failed) {');
|
|
174
|
+
lines.push(` await agent('Criterion ' + failure.id + ' of feature ' + SLUG + ' is failing. Its verification command is: ' + (failure.command || '(see contract)') + ' (exit ' + failure.exitCode + '). Read .aioson/plans/' + SLUG + '/harness-contract.json and .aioson/plans/' + SLUG + '/last-check-output.json for the failure detail, fix the UNDERLYING issue in the implementation (never weaken or delete the check/test), run the verification command locally until it passes, and report what changed.', { label: 'fix:' + failure.id, phase: 'Verify' })`);
|
|
175
|
+
lines.push(' }');
|
|
176
|
+
lines.push('}');
|
|
177
|
+
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push('// Critérios binários SEM verification: revisão adversarial (3 lentes que');
|
|
180
|
+
lines.push('// tentam REFUTAR; sobrevive com maioria). Filtra ruído antes do @validator.');
|
|
181
|
+
lines.push('const adversarial = JUDGED_CRITERIA.length === 0 ? [] : await parallel(JUDGED_CRITERIA.map(criterion => () =>');
|
|
182
|
+
lines.push(` parallel(['correctness', 'completeness', 'regression-risk'].map(lens => () =>`);
|
|
183
|
+
lines.push(` agent('Adversarial reviewer (' + lens + ' lens) for feature ' + SLUG + '. Try to REFUTE that this criterion holds in the current working tree: "' + criterion.description + '" (assertion: ' + criterion.assertion + '). Inspect the actual code/files. Default refuted=true if uncertain.', { label: 'refute:' + criterion.id + ':' + lens, phase: 'Verify', schema: REFUTE_SCHEMA })`);
|
|
184
|
+
lines.push(' )).then(votes => ({');
|
|
185
|
+
lines.push(' id: criterion.id,');
|
|
186
|
+
lines.push(' survives: votes.filter(Boolean).filter(v => !v.refuted).length >= 2,');
|
|
187
|
+
lines.push(' reasons: votes.filter(Boolean).map(v => v.reason)');
|
|
188
|
+
lines.push(' }))');
|
|
189
|
+
lines.push('))');
|
|
190
|
+
lines.push('const refutedCriteria = adversarial.filter(Boolean).filter(a => !a.survives)');
|
|
191
|
+
lines.push('if (refutedCriteria.length > 0) {');
|
|
192
|
+
lines.push(` log('adversarial review refuted: ' + refutedCriteria.map(a => a.id).join(', ') + ' — fixing before validation')`);
|
|
193
|
+
lines.push(' for (const refuted of refutedCriteria) {');
|
|
194
|
+
lines.push(` await agent('Adversarial review refuted criterion ' + refuted.id + ' of feature ' + SLUG + '. Reviewer reasons: ' + refuted.reasons.join(' | ') + '. Address the gaps in the implementation (never argue with the reviewers in prose — fix code) and report what changed.', { label: 'fix:' + refuted.id, phase: 'Verify' })`);
|
|
195
|
+
lines.push(' }');
|
|
196
|
+
lines.push('}');
|
|
197
|
+
|
|
198
|
+
lines.push('');
|
|
199
|
+
lines.push(`phase('Validate')`);
|
|
200
|
+
lines.push(`const verdict = await agent('You are the AIOSON fresh-context @validator for feature ' + SLUG + '. Steps, in order: (1) run aioson harness:validate . --slug=' + SLUG + ' to generate the self-contained prompt; (2) read .aioson/plans/' + SLUG + '/validator-prompt.txt and follow it EXACTLY — criteria with executable verification take their verdict from the harness:check exit codes verbatim, you only judge the rest; (3) write your verdict JSON to .aioson/plans/' + SLUG + '/last-validator-output.json; (4) run aioson harness:apply-validation . --slug=' + SLUG + ' --json and return ITS JSON as your structured output.', { label: 'validator:fresh', phase: 'Validate', schema: APPLY_SCHEMA })`);
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push('return {');
|
|
203
|
+
lines.push(' slug: SLUG,');
|
|
204
|
+
lines.push(' waves: waveResults,');
|
|
205
|
+
lines.push(' fix_rounds: fixRound,');
|
|
206
|
+
lines.push(' deterministic_checks: checkReport,');
|
|
207
|
+
lines.push(' adversarial,');
|
|
208
|
+
lines.push(' verdict,');
|
|
209
|
+
lines.push(` human_gate: 'feature:close is yours to run: aioson feature:close . --feature=' + SLUG`);
|
|
210
|
+
lines.push('}');
|
|
211
|
+
lines.push('');
|
|
212
|
+
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function runForgeCompile({ args, options = {}, logger }) {
|
|
217
|
+
const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
|
|
218
|
+
const slug = String(options.feature || options.slug || '').trim();
|
|
219
|
+
|
|
220
|
+
if (!slug) {
|
|
221
|
+
logger.error('--feature=<slug> is required.');
|
|
222
|
+
return { ok: false, error: 'missing_feature' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Preflight 1: contrato válido com critérios ───────────────────────────
|
|
226
|
+
const planDir = path.join(targetDir, '.aioson', 'plans', slug);
|
|
227
|
+
const contractPath = path.join(planDir, 'harness-contract.json');
|
|
228
|
+
if (!fs.existsSync(contractPath)) {
|
|
229
|
+
logger.error(`Contract not found: ${path.relative(targetDir, contractPath)} — @sheldon RF-05 produces it for MEDIUM features.`);
|
|
230
|
+
return { ok: false, error: 'contract_not_found', slug };
|
|
231
|
+
}
|
|
232
|
+
let contract;
|
|
233
|
+
try {
|
|
234
|
+
contract = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logger.error(`Invalid JSON in contract: ${err.message}`);
|
|
237
|
+
return { ok: false, error: 'invalid_json', slug };
|
|
238
|
+
}
|
|
239
|
+
const schema = validateContract(contract);
|
|
240
|
+
if (!schema.ok) {
|
|
241
|
+
const first = schema.errors[0];
|
|
242
|
+
logger.error(`Contract schema invalid: ${first.field} — ${first.reason}`);
|
|
243
|
+
return { ok: false, error: 'contract_schema_invalid', slug, errors: schema.errors };
|
|
244
|
+
}
|
|
245
|
+
const resolved = resolveContract(contract);
|
|
246
|
+
const binaryCriteria = resolved.criteria.filter((c) => c && c.binary === true);
|
|
247
|
+
if (binaryCriteria.length === 0) {
|
|
248
|
+
logger.error('Contract has no binary criteria — nothing to converge on. Lane B needs a machine-checkable definition of done.');
|
|
249
|
+
return { ok: false, error: 'no_binary_criteria', slug };
|
|
250
|
+
}
|
|
251
|
+
const execCriteria = binaryCriteria
|
|
252
|
+
.filter((c) => typeof c.verification === 'string' && c.verification.trim())
|
|
253
|
+
.map((c) => ({ id: c.id, description: c.description || '', verification: c.verification }));
|
|
254
|
+
const judgedCriteria = binaryCriteria
|
|
255
|
+
.filter((c) => !(typeof c.verification === 'string' && c.verification.trim()))
|
|
256
|
+
.map((c) => ({ id: c.id, description: c.description || '', assertion: c.assertion || '' }));
|
|
257
|
+
if (execCriteria.length === 0) {
|
|
258
|
+
logger.error('No criterion has an executable `verification` command — the fix loop would have no deterministic convergence signal. Author verification commands first (see .aioson/docs/sheldon/harness-contract.md §2b).');
|
|
259
|
+
return { ok: false, error: 'no_executable_criteria', slug };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Preflight 2: plano com coluna Wave ───────────────────────────────────
|
|
263
|
+
const planPath = path.join(targetDir, '.aioson', 'context', `implementation-plan-${slug}.md`);
|
|
264
|
+
if (!fs.existsSync(planPath)) {
|
|
265
|
+
logger.error(`Implementation plan not found: ${path.relative(targetDir, planPath)} — @pm produces it (Gate C).`);
|
|
266
|
+
return { ok: false, error: 'plan_not_found', slug };
|
|
267
|
+
}
|
|
268
|
+
const rows = parseExecutionWaves(fs.readFileSync(planPath, 'utf8'));
|
|
269
|
+
if (!rows || rows.length === 0) {
|
|
270
|
+
logger.error('Execution Sequence has no Wave column (or no parseable rows) — re-run @pm to annotate waves (pm.md Wave column rules).');
|
|
271
|
+
return { ok: false, error: 'no_wave_column', slug };
|
|
272
|
+
}
|
|
273
|
+
const waves = groupByWave(rows);
|
|
274
|
+
|
|
275
|
+
// ── Preflight 3: spec:analyze limpo (wave overlap promovido a erro) ──────
|
|
276
|
+
const { runSpecAnalyze } = require('./spec-analyze');
|
|
277
|
+
const silentLogger = { log: () => {}, error: () => {} };
|
|
278
|
+
const analysis = await runSpecAnalyze({ args: [targetDir], options: { feature: slug }, logger: silentLogger });
|
|
279
|
+
const blockers = [
|
|
280
|
+
...(analysis.findings || []).filter((f) => f.severity === 'error'),
|
|
281
|
+
...(analysis.findings || []).filter((f) => f.check === 'wave_file_overlap')
|
|
282
|
+
];
|
|
283
|
+
if (blockers.length > 0) {
|
|
284
|
+
logger.error(`spec:analyze blockers (${blockers.length}) — compilation refused:`);
|
|
285
|
+
for (const finding of blockers) {
|
|
286
|
+
logger.error(` - [${finding.check}] ${finding.message}`);
|
|
287
|
+
}
|
|
288
|
+
return { ok: false, error: 'spec_analyze_blockers', slug, blockers };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Compilação ────────────────────────────────────────────────────────────
|
|
292
|
+
const maxFixRounds = Number.isInteger(resolved.governor.error_streak_limit) && resolved.governor.error_streak_limit > 0
|
|
293
|
+
? resolved.governor.error_streak_limit
|
|
294
|
+
: DEFAULT_MAX_FIX_ROUNDS;
|
|
295
|
+
|
|
296
|
+
const script = buildScript({ slug, waves, execCriteria, judgedCriteria, maxFixRounds });
|
|
297
|
+
const scriptPath = path.join(planDir, 'forge-run.workflow.js');
|
|
298
|
+
fs.writeFileSync(scriptPath, script, 'utf8');
|
|
299
|
+
|
|
300
|
+
const report = {
|
|
301
|
+
ok: true,
|
|
302
|
+
slug,
|
|
303
|
+
scriptPath: path.relative(targetDir, scriptPath),
|
|
304
|
+
waves: waves.map((w) => ({ wave: w.wave, phases: w.phases.map((p) => p.phase) })),
|
|
305
|
+
executable_criteria: execCriteria.length,
|
|
306
|
+
judged_criteria: judgedCriteria.length,
|
|
307
|
+
max_fix_rounds: maxFixRounds,
|
|
308
|
+
adversarial_votes: ADVERSARIAL_VOTES
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
if (options.json) {
|
|
312
|
+
logger.log(JSON.stringify(report, null, 2));
|
|
313
|
+
return report;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
logger.log(`Forge compile — ${slug}`);
|
|
317
|
+
logger.log(` Script: ${report.scriptPath}`);
|
|
318
|
+
logger.log(` Waves: ${waves.map((w) => `W${w.wave}[${w.phases.length}]`).join(' → ')} (${rows.length} phases)`);
|
|
319
|
+
logger.log(` Criteria: ${execCriteria.length} executable + ${judgedCriteria.length} adversarially judged`);
|
|
320
|
+
logger.log(` Governor: fix loop capped at ${maxFixRounds} rounds`);
|
|
321
|
+
logger.log('');
|
|
322
|
+
logger.log('Next steps:');
|
|
323
|
+
logger.log(' 1. Review the script and commit it with the spec (it is the execution plan as code).');
|
|
324
|
+
logger.log(' 2. In a Claude Code session, activate /forge-run (or ask: "run the workflow at ' + report.scriptPath + '").');
|
|
325
|
+
logger.log(' 3. The run ends BEFORE feature:close — closing the feature is always yours.');
|
|
326
|
+
|
|
327
|
+
return report;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = { runForgeCompile, buildScript };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson harness:check [path] --slug=<slug> [--criteria=C1,C2] [--timeout=<ms>]
|
|
5
|
+
* [--json] — runner standalone e determinístico de criteria[].verification.
|
|
6
|
+
*
|
|
7
|
+
* Executa os checks executáveis do contrato FORA do self:loop, para consumo
|
|
8
|
+
* pelo @validator (verificação binária antes de julgamento LLM), pelo @dev
|
|
9
|
+
* (feedback rápido pré-done) ou por CI. Reusa runCriteria/executeInSandbox —
|
|
10
|
+
* NÃO cria runner novo.
|
|
11
|
+
*
|
|
12
|
+
* Read-only sobre progress.json: quem muda estado do circuito continua sendo
|
|
13
|
+
* o ciclo harness:validate/apply-validation. Persiste o resultado em
|
|
14
|
+
* `last-check-output.json` no plan dir (espelha a convenção
|
|
15
|
+
* last-validator-output.json) e emite telemetria criteria_check_failed
|
|
16
|
+
* (best-effort, nunca quebra).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
const { validateContract, resolveContract } = require('../harness/contract-schema');
|
|
23
|
+
const { runCriteria, DEFAULT_CHECK_TIMEOUT_MS } = require('../harness/criteria-runner');
|
|
24
|
+
const { emitGuardEvent } = require('../harness/guard-events');
|
|
25
|
+
const { findActiveContract } = require('../harness/active-contract');
|
|
26
|
+
|
|
27
|
+
function resolveSlug(targetDir, options) {
|
|
28
|
+
const explicit = String(options.slug || '').trim();
|
|
29
|
+
if (explicit) return explicit;
|
|
30
|
+
try {
|
|
31
|
+
const active = findActiveContract(targetDir);
|
|
32
|
+
return active ? active.slug : '';
|
|
33
|
+
} catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function runHarnessCheck({ args, options = {}, logger, t }) {
|
|
39
|
+
const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
|
|
40
|
+
const slug = resolveSlug(targetDir, options);
|
|
41
|
+
|
|
42
|
+
if (!slug) {
|
|
43
|
+
logger.error(t('errors.missing_slug') || 'Error: --slug is required');
|
|
44
|
+
return { ok: false, error: 'missing_slug' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const planDir = path.join(targetDir, '.aioson', 'plans', slug);
|
|
48
|
+
const contractPath = path.join(planDir, 'harness-contract.json');
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(contractPath)) {
|
|
51
|
+
logger.error(t('harness.contract_not_found', { slug }) || `Contract not found for slug: ${slug}`);
|
|
52
|
+
return { ok: false, error: 'contract_not_found', slug };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let contract;
|
|
56
|
+
try {
|
|
57
|
+
contract = JSON.parse(fs.readFileSync(contractPath, 'utf8'));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
logger.error(`Invalid JSON in contract: ${err.message}`);
|
|
60
|
+
return { ok: false, error: 'invalid_json', slug, detail: err.message };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const schemaResult = validateContract(contract);
|
|
64
|
+
if (!schemaResult.ok) {
|
|
65
|
+
const first = schemaResult.errors[0];
|
|
66
|
+
logger.error(`Contract schema invalid: ${first.field} — ${first.reason}`);
|
|
67
|
+
await emitGuardEvent(targetDir, {
|
|
68
|
+
eventType: 'contract_invalid',
|
|
69
|
+
agent: 'harness-check',
|
|
70
|
+
message: `${first.field}: ${first.reason}`,
|
|
71
|
+
payload: { slug }
|
|
72
|
+
});
|
|
73
|
+
return { ok: false, error: 'contract_schema_invalid', slug, errors: schemaResult.errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resolved = resolveContract(contract);
|
|
77
|
+
|
|
78
|
+
let criteria = resolved.criteria;
|
|
79
|
+
if (options.criteria) {
|
|
80
|
+
const wanted = new Set(
|
|
81
|
+
String(options.criteria).split(',').map((id) => id.trim()).filter(Boolean)
|
|
82
|
+
);
|
|
83
|
+
criteria = criteria.filter((c) => c && wanted.has(c.id));
|
|
84
|
+
const found = new Set(criteria.map((c) => c.id));
|
|
85
|
+
const missing = [...wanted].filter((id) => !found.has(id));
|
|
86
|
+
if (missing.length) {
|
|
87
|
+
logger.error(t('harness.check_unknown_criteria', { ids: missing.join(', ') }) || `Unknown criteria ids: ${missing.join(', ')}`);
|
|
88
|
+
return { ok: false, error: 'unknown_criteria', slug, missing };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const timeoutMs = Number.isInteger(Number(options.timeout)) && Number(options.timeout) > 0
|
|
93
|
+
? Number(options.timeout)
|
|
94
|
+
: DEFAULT_CHECK_TIMEOUT_MS;
|
|
95
|
+
|
|
96
|
+
const executable = criteria.filter(
|
|
97
|
+
(c) => c && typeof c.verification === 'string' && c.verification.trim()
|
|
98
|
+
);
|
|
99
|
+
const skipped = criteria.length - executable.length;
|
|
100
|
+
|
|
101
|
+
const checks = await runCriteria({ criteria, cwd: targetDir, timeoutMs });
|
|
102
|
+
const failed = checks.filter((c) => !c.ok);
|
|
103
|
+
|
|
104
|
+
for (const check of failed) {
|
|
105
|
+
await emitGuardEvent(targetDir, {
|
|
106
|
+
eventType: 'criteria_check_failed',
|
|
107
|
+
agent: 'harness-check',
|
|
108
|
+
message: `${check.id}: exit ${check.exitCode}${check.timedOut ? ' (timeout)' : ''}`,
|
|
109
|
+
payload: { slug, criterion_id: check.id, exit_code: check.exitCode, signature: check.signature }
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const report = {
|
|
114
|
+
ok: failed.length === 0,
|
|
115
|
+
slug,
|
|
116
|
+
checked_at: new Date().toISOString(),
|
|
117
|
+
criteria_total: criteria.length,
|
|
118
|
+
executable_total: executable.length,
|
|
119
|
+
passed: checks.length - failed.length,
|
|
120
|
+
failed: failed.length,
|
|
121
|
+
skipped_no_verification: skipped,
|
|
122
|
+
checks
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (!fs.existsSync(planDir)) fs.mkdirSync(planDir, { recursive: true });
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(planDir, 'last-check-output.json'),
|
|
129
|
+
JSON.stringify(report, null, 2),
|
|
130
|
+
'utf8'
|
|
131
|
+
);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// Persistência é conveniência, não gate — reporta sem falhar o run.
|
|
134
|
+
logger.error(`Could not persist last-check-output.json: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.json) {
|
|
138
|
+
logger.log(JSON.stringify(report, null, 2));
|
|
139
|
+
return report;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
logger.log(t('harness.check_header', { slug }) || `Harness check — ${slug}`);
|
|
143
|
+
if (executable.length === 0) {
|
|
144
|
+
logger.log(t('harness.check_no_executable', { total: criteria.length }) || ` No criteria with verification commands (${criteria.length} criteria total). @validator judges them all.`);
|
|
145
|
+
return report;
|
|
146
|
+
}
|
|
147
|
+
for (const check of checks) {
|
|
148
|
+
const mark = check.ok ? '✓' : '✗';
|
|
149
|
+
const extra = check.ok ? '' : ` (exit ${check.exitCode}${check.timedOut ? ', timeout' : ''})`;
|
|
150
|
+
logger.log(` ${mark} ${check.id} — ${check.command}${extra} [${check.durationMs}ms]`);
|
|
151
|
+
}
|
|
152
|
+
logger.log(
|
|
153
|
+
t('harness.check_summary', { passed: report.passed, executable: executable.length, skipped }) ||
|
|
154
|
+
` Checks: ${report.passed}/${executable.length} passed (${skipped} without verification — judged by @validator)`
|
|
155
|
+
);
|
|
156
|
+
return report;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { runHarnessCheck };
|
package/src/commands/harness.js
CHANGED
|
@@ -291,6 +291,27 @@ async function runHarnessValidate({ args, options = {}, logger, t }) {
|
|
|
291
291
|
return { ok: false, error: 'agent_prompt_failed', detail: promptResult };
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// Fase 2 (fresh-context review): anexa diff + check results + instrução de
|
|
295
|
+
// saída ao prompt, tornando-o autocontido para um contexto isolado.
|
|
296
|
+
// Best-effort: payload degradado (sem git) ainda é anexado; falha de I/O
|
|
297
|
+
// no append não derruba o fluxo de validação.
|
|
298
|
+
let reviewPayload = null;
|
|
299
|
+
const skipDiff = options['no-diff'] === true || options.noDiff === true;
|
|
300
|
+
if (!skipDiff) {
|
|
301
|
+
const { buildReviewPayload } = require('../harness/review-payload');
|
|
302
|
+
reviewPayload = buildReviewPayload(targetDir, planDir, {
|
|
303
|
+
slug,
|
|
304
|
+
baseRef: options.base ? String(options.base) : null,
|
|
305
|
+
maxDiffBytes: options['max-diff-bytes'] || options.maxDiffBytes,
|
|
306
|
+
outputPath: validatorOutputPath
|
|
307
|
+
});
|
|
308
|
+
try {
|
|
309
|
+
fs.appendFileSync(promptPath, reviewPayload.text, 'utf8');
|
|
310
|
+
} catch (err) {
|
|
311
|
+
logger.error(`Could not append review payload to prompt: ${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
294
315
|
// Mark feature as awaiting validation — drives workflow:next routing (AC-HD-14).
|
|
295
316
|
// Reset by runHarnessApplyValidation after the validator output is consumed.
|
|
296
317
|
cb.progress.status = 'waiting_validation';
|
|
@@ -299,9 +320,12 @@ async function runHarnessValidate({ args, options = {}, logger, t }) {
|
|
|
299
320
|
|
|
300
321
|
logger.log('');
|
|
301
322
|
logger.log(`Validator prompt saved to: ${promptPath}`);
|
|
323
|
+
if (reviewPayload && reviewPayload.ok) {
|
|
324
|
+
logger.log(` Review payload: diff vs ${reviewPayload.base} (${reviewPayload.baseSource}), ${reviewPayload.changedFiles.length} changed file(s)${reviewPayload.truncated ? ', diff truncated' : ''}${reviewPayload.hasChecks ? ', harness:check results included' : ''}`);
|
|
325
|
+
}
|
|
302
326
|
logger.log('');
|
|
303
327
|
logger.log('Next steps:');
|
|
304
|
-
logger.log(` 1.
|
|
328
|
+
logger.log(` 1. Run the prompt in a FRESH isolated context — a subagent/Task tool of the orchestrating session, or a separate LLM session with @validator activated. Never inline in the implementing session.`);
|
|
305
329
|
logger.log(` 2. Save the JSON output to: ${validatorOutputPath}`);
|
|
306
330
|
logger.log(` 3. Re-run: aioson harness:validate . --slug=${slug}`);
|
|
307
331
|
logger.log(` (or: aioson harness:apply-validation . --slug=${slug})`);
|
|
@@ -311,7 +335,18 @@ async function runHarnessValidate({ args, options = {}, logger, t }) {
|
|
|
311
335
|
status: 'awaiting_validation',
|
|
312
336
|
slug,
|
|
313
337
|
promptPath,
|
|
314
|
-
expectedOutputPath: validatorOutputPath
|
|
338
|
+
expectedOutputPath: validatorOutputPath,
|
|
339
|
+
reviewPayload: reviewPayload
|
|
340
|
+
? {
|
|
341
|
+
ok: reviewPayload.ok,
|
|
342
|
+
base: reviewPayload.base,
|
|
343
|
+
baseSource: reviewPayload.baseSource,
|
|
344
|
+
changedFiles: reviewPayload.changedFiles.length,
|
|
345
|
+
untracked: reviewPayload.untracked.length,
|
|
346
|
+
truncated: reviewPayload.truncated,
|
|
347
|
+
hasChecks: reviewPayload.hasChecks
|
|
348
|
+
}
|
|
349
|
+
: null
|
|
315
350
|
};
|
|
316
351
|
}
|
|
317
352
|
|