@jaimevalasek/aioson 1.22.0 → 1.23.1
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 +932 -919
- package/docs/en/5-reference/cli-reference.md +85 -0
- package/docs/pt/4-agentes/pm.md +31 -4
- package/docs/pt/5-referencia/README.md +3 -0
- package/docs/pt/5-referencia/autopilot-handoff.md +131 -0
- package/docs/pt/5-referencia/comandos-cli.md +72 -6
- package/docs/pt/5-referencia/harness-retro.md +133 -0
- package/docs/pt/5-referencia/loop-guardrails.md +225 -0
- package/docs/pt/5-referencia/sdd-automation-scripts.md +25 -13
- package/package.json +1 -1
- package/src/agents.js +1 -1
- package/src/cli.js +70 -29
- package/src/commands/agent-epilogue.js +186 -0
- package/src/commands/context-select.js +33 -0
- package/src/commands/harness-preview.js +74 -0
- package/src/commands/harness-retro.js +221 -0
- package/src/commands/preflight-context.js +13 -9
- package/src/commands/review-cycle.js +328 -0
- package/src/commands/runtime.js +4 -4
- package/src/commands/self-implement-loop.js +12 -2
- package/src/commands/state-save.js +2 -0
- package/src/commands/workflow-execute.js +138 -28
- package/src/commands/workflow-next.js +11 -2
- package/src/commands/workflow-status.js +30 -10
- package/src/constants.js +15 -13
- package/src/context-memory.js +50 -25
- package/src/context-selector.js +394 -0
- package/src/harness/preview-artifact.js +85 -0
- package/src/i18n/messages/en.js +34 -7
- package/src/i18n/messages/es.js +34 -7
- package/src/i18n/messages/fr.js +34 -7
- package/src/i18n/messages/pt-BR.js +34 -7
- 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/parser.js +1 -1
- package/src/squad/preflight-context.js +26 -27
- package/template/.aioson/agents/analyst.md +41 -46
- package/template/.aioson/agents/architect.md +33 -46
- package/template/.aioson/agents/briefing.md +76 -67
- package/template/.aioson/agents/dev.md +73 -55
- package/template/.aioson/agents/deyvin.md +55 -50
- package/template/.aioson/agents/discovery-design-doc.md +35 -22
- package/template/.aioson/agents/manifests/architect.manifest.json +11 -1
- package/template/.aioson/agents/manifests/dev.manifest.json +15 -0
- package/template/.aioson/agents/manifests/pm.manifest.json +20 -0
- package/template/.aioson/agents/orchestrator.md +31 -18
- package/template/.aioson/agents/pentester.md +12 -4
- package/template/.aioson/agents/pm.md +41 -35
- package/template/.aioson/agents/product.md +116 -165
- package/template/.aioson/agents/qa.md +44 -13
- package/template/.aioson/agents/scope-check.md +46 -24
- package/template/.aioson/agents/sheldon.md +13 -0
- package/template/.aioson/agents/tester.md +28 -5
- package/template/.aioson/agents/ux-ui.md +36 -31
- package/template/.aioson/agents/validator.md +10 -2
- package/template/.aioson/config/autonomy-protocol.json +7 -0
- package/template/.aioson/design-docs/code-reuse.md +10 -5
- package/template/.aioson/design-docs/componentization.md +10 -5
- package/template/.aioson/design-docs/file-size.md +10 -5
- package/template/.aioson/design-docs/folder-structure.md +10 -5
- package/template/.aioson/design-docs/naming.md +10 -5
- package/template/.aioson/docs/autonomy-protocol.md +2 -2
- package/template/.aioson/docs/autopilot-handoff.md +82 -34
- package/template/.aioson/docs/briefing/briefing-craft.md +9 -3
- package/template/.aioson/docs/deyvin/continuity-recovery.md +18 -22
- package/template/.aioson/docs/product/conversation-playbook.md +8 -3
- package/template/.aioson/docs/product/prd-contract.md +8 -3
- package/template/.aioson/docs/product/quality-lens.md +8 -3
- package/template/.aioson/docs/product/research-loop.md +8 -3
- package/template/.aioson/docs/ux-ui/accessibility-audit.md +7 -2
- package/template/.aioson/docs/ux-ui/audit-mode.md +7 -2
- package/template/.aioson/docs/ux-ui/component-map.md +7 -2
- package/template/.aioson/docs/ux-ui/design-execution.md +7 -2
- package/template/.aioson/docs/ux-ui/design-gate.md +7 -2
- package/template/.aioson/docs/ux-ui/research-mode.md +7 -2
- package/template/.aioson/docs/ux-ui/site-delivery.md +7 -2
- package/template/.aioson/docs/ux-ui/token-contract.md +7 -2
- package/template/.aioson/rules/aioson-context-boundary.md +11 -9
- package/template/.aioson/rules/disk-first-artifacts.md +1 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +1 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +3 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +21 -9
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -1
- package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +2 -1
- package/template/.aioson/skills/static/web-research-cache.md +29 -8
- package/template/AGENTS.md +1 -1
- package/template/CLAUDE.md +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { runPulseUpdate } = require('./pulse-update');
|
|
5
|
+
const { runDossierAddFinding } = require('./dossier');
|
|
6
|
+
const { runGateApprove } = require('./gate-approve');
|
|
7
|
+
const { runAgentDone } = require('./runtime');
|
|
8
|
+
|
|
9
|
+
function resolveTargetDir(args) {
|
|
10
|
+
return path.resolve(process.cwd(), args[0] || '.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeAgent(value) {
|
|
14
|
+
return String(value || '').trim().replace(/^@/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeList(value) {
|
|
18
|
+
if (!value) return [];
|
|
19
|
+
if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
|
|
20
|
+
return String(value).split(',').map((item) => item.trim()).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeSilentLogger() {
|
|
24
|
+
return { log() {}, error() {}, warn() {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pushStep(steps, name, result) {
|
|
28
|
+
steps.push({
|
|
29
|
+
name,
|
|
30
|
+
ok: Boolean(result && result.ok),
|
|
31
|
+
skipped: Boolean(result && result.skipped),
|
|
32
|
+
reason: result && (result.reason || result.error || null)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatAutoAdvance(autoAdvance) {
|
|
37
|
+
if (!autoAdvance) return null;
|
|
38
|
+
if (autoAdvance.advanced) {
|
|
39
|
+
const nextStage = autoAdvance.result && (autoAdvance.result.next || autoAdvance.result.nextStage);
|
|
40
|
+
return `workflow auto-advanced${nextStage ? ` -> ${nextStage}` : ''}`;
|
|
41
|
+
}
|
|
42
|
+
return `workflow skip: ${autoAdvance.skipped || 'not_advanced'}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runAgentEpilogue({ args, options = {}, logger, t }) {
|
|
46
|
+
const targetDir = resolveTargetDir(args);
|
|
47
|
+
const agent = normalizeAgent(options.agent);
|
|
48
|
+
const summary = String(options.summary || options.message || '').trim();
|
|
49
|
+
const feature = options.feature ? String(options.feature).trim() : null;
|
|
50
|
+
const action = options.action ? String(options.action).trim() : summary;
|
|
51
|
+
const next = options.next ? String(options.next).trim() : null;
|
|
52
|
+
const gate = options.gate ? String(options.gate).trim() : null;
|
|
53
|
+
const approveGate = options['approve-gate'] || options.approveGate
|
|
54
|
+
? String(options['approve-gate'] || options.approveGate).trim().toUpperCase()
|
|
55
|
+
: null;
|
|
56
|
+
const verdict = options.verdict ? String(options.verdict).trim().toUpperCase() : null;
|
|
57
|
+
const artifacts = normalizeList(options.artifacts);
|
|
58
|
+
const strict = Boolean(options.strict);
|
|
59
|
+
const steps = [];
|
|
60
|
+
const errors = [];
|
|
61
|
+
const silentLogger = makeSilentLogger();
|
|
62
|
+
|
|
63
|
+
if (!agent) {
|
|
64
|
+
const failure = { ok: false, reason: 'missing_agent' };
|
|
65
|
+
if (options.json) return failure;
|
|
66
|
+
logger.error('--agent=<agent> is required.');
|
|
67
|
+
return failure;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!summary) {
|
|
71
|
+
const failure = { ok: false, reason: 'missing_summary' };
|
|
72
|
+
if (options.json) return failure;
|
|
73
|
+
logger.error('--summary="<summary>" is required.');
|
|
74
|
+
return failure;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (approveGate) {
|
|
78
|
+
const gateResult = await runGateApprove({
|
|
79
|
+
args: [targetDir],
|
|
80
|
+
options: {
|
|
81
|
+
feature,
|
|
82
|
+
gate: approveGate,
|
|
83
|
+
agent,
|
|
84
|
+
json: true
|
|
85
|
+
},
|
|
86
|
+
logger: silentLogger
|
|
87
|
+
});
|
|
88
|
+
pushStep(steps, 'gate:approve', gateResult);
|
|
89
|
+
if (!gateResult.ok) {
|
|
90
|
+
errors.push({ step: 'gate:approve', reason: gateResult.reason || 'gate_failed', result: gateResult });
|
|
91
|
+
if (strict) {
|
|
92
|
+
const failure = { ok: false, reason: 'gate_approve_failed', steps, errors };
|
|
93
|
+
if (options.json) return failure;
|
|
94
|
+
logger.error(`agent:epilogue blocked: gate ${approveGate} approval failed.`);
|
|
95
|
+
return failure;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!options['no-pulse'] && !options.noPulse) {
|
|
101
|
+
const pulseResult = await runPulseUpdate({
|
|
102
|
+
args: [targetDir],
|
|
103
|
+
options: {
|
|
104
|
+
agent,
|
|
105
|
+
...(feature ? { feature } : {}),
|
|
106
|
+
...(gate || approveGate ? { gate: gate || `Gate ${approveGate}: approved` } : {}),
|
|
107
|
+
...(action ? { action } : {}),
|
|
108
|
+
...(next ? { next } : {}),
|
|
109
|
+
...(verdict ? { verdict } : {}),
|
|
110
|
+
json: true
|
|
111
|
+
},
|
|
112
|
+
logger: silentLogger
|
|
113
|
+
});
|
|
114
|
+
pushStep(steps, 'pulse:update', pulseResult);
|
|
115
|
+
if (!pulseResult.ok) {
|
|
116
|
+
errors.push({ step: 'pulse:update', reason: pulseResult.reason || 'pulse_failed', result: pulseResult });
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
pushStep(steps, 'pulse:update', { ok: true, skipped: true, reason: 'disabled' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (feature && !options['no-dossier'] && !options.noDossier) {
|
|
123
|
+
const dossierResult = await runDossierAddFinding({
|
|
124
|
+
args: [targetDir],
|
|
125
|
+
options: {
|
|
126
|
+
slug: feature,
|
|
127
|
+
agent,
|
|
128
|
+
section: options.section ? String(options.section) : 'Agent Trail',
|
|
129
|
+
content: options.content ? String(options.content) : summary,
|
|
130
|
+
json: true
|
|
131
|
+
},
|
|
132
|
+
logger: silentLogger
|
|
133
|
+
});
|
|
134
|
+
pushStep(steps, 'dossier:add-finding', dossierResult);
|
|
135
|
+
if (!dossierResult.ok) {
|
|
136
|
+
errors.push({ step: 'dossier:add-finding', reason: dossierResult.reason || 'dossier_failed', result: dossierResult });
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
pushStep(steps, 'dossier:add-finding', { ok: true, skipped: true, reason: feature ? 'disabled' : 'missing_feature' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const doneResult = await runAgentDone({
|
|
143
|
+
args: [targetDir],
|
|
144
|
+
options: {
|
|
145
|
+
agent,
|
|
146
|
+
summary,
|
|
147
|
+
...(feature ? { feature } : {}),
|
|
148
|
+
...(verdict ? { verdict } : {}),
|
|
149
|
+
...(artifacts.length > 0 ? { artifacts: artifacts.join(',') } : {}),
|
|
150
|
+
json: true
|
|
151
|
+
},
|
|
152
|
+
logger: silentLogger,
|
|
153
|
+
t
|
|
154
|
+
});
|
|
155
|
+
pushStep(steps, 'agent:done', doneResult);
|
|
156
|
+
if (!doneResult.ok) {
|
|
157
|
+
errors.push({ step: 'agent:done', reason: doneResult.reason || doneResult.error || 'agent_done_failed', result: doneResult });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ok = doneResult.ok && (strict ? errors.length === 0 : !errors.some((error) => error.step === 'agent:done'));
|
|
161
|
+
const result = {
|
|
162
|
+
ok,
|
|
163
|
+
targetDir,
|
|
164
|
+
agent: `@${agent}`,
|
|
165
|
+
feature,
|
|
166
|
+
summary,
|
|
167
|
+
steps,
|
|
168
|
+
errors,
|
|
169
|
+
agent_done: doneResult
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (options.json) return result;
|
|
173
|
+
|
|
174
|
+
logger.log(`agent:epilogue — @${agent} (${ok ? 'ok' : 'issues'})`);
|
|
175
|
+
for (const step of steps) {
|
|
176
|
+
const marker = step.skipped ? 'skip' : step.ok ? 'ok' : 'fail';
|
|
177
|
+
logger.log(` ${marker} ${step.name}${step.reason ? ` (${step.reason})` : ''}`);
|
|
178
|
+
}
|
|
179
|
+
const autoAdvanceMessage = formatAutoAdvance(doneResult.auto_advance);
|
|
180
|
+
if (autoAdvanceMessage) logger.log(` ${autoAdvanceMessage}`);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
runAgentEpilogue
|
|
186
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { selectContext } = require('../context-selector');
|
|
5
|
+
|
|
6
|
+
async function runContextSelect({ args, options = {}, logger }) {
|
|
7
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
8
|
+
const result = await selectContext(targetDir, {
|
|
9
|
+
agent: options.agent || options.a || 'dev',
|
|
10
|
+
mode: options.mode || 'planning',
|
|
11
|
+
task: options.task || options.goal || '',
|
|
12
|
+
paths: options.paths || options.path || '',
|
|
13
|
+
feature: options.feature || options.slug || ''
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (options.json) return result;
|
|
17
|
+
|
|
18
|
+
logger.log(`Context selection for @${result.agent} (${result.mode})`);
|
|
19
|
+
if (result.task) logger.log(`Task: ${result.task}`);
|
|
20
|
+
if (result.paths.length > 0) logger.log(`Paths: ${result.paths.join(', ')}`);
|
|
21
|
+
if (result.selected.length === 0) {
|
|
22
|
+
logger.log('No context files selected.');
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const item of result.selected) {
|
|
27
|
+
logger.log(`- ${item.path} [${item.surface}; ${item.load_tier}] ${item.reason}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { runContextSelect };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `aioson harness:preview <file> [--max-bytes=8192] [--json]` (requirements §5.2).
|
|
5
|
+
*
|
|
6
|
+
* Wrapper fino e read-only de `previewArtifact` sobre um arquivo já persistido
|
|
7
|
+
* (ex.: `npm test > test.log`). Devolve preview + ponteiro para o agente de teste
|
|
8
|
+
* consumir sem despejar o log integral no contexto. Tema 2 (should-have).
|
|
9
|
+
*
|
|
10
|
+
* Exit codes: 0 sucesso; 12 input inválido (arquivo ausente/ilegível).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
const { previewArtifact, DEFAULT_MAX_BYTES } = require('../harness/preview-artifact');
|
|
17
|
+
|
|
18
|
+
const EXIT_OK = 0;
|
|
19
|
+
const EXIT_INPUT = 12;
|
|
20
|
+
|
|
21
|
+
function tr(t, key, params, fallback) {
|
|
22
|
+
if (typeof t !== 'function') return fallback;
|
|
23
|
+
const msg = t(key, params);
|
|
24
|
+
return msg && msg !== key ? msg : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runHarnessPreview({ args, options = {}, logger, t } = {}) {
|
|
28
|
+
const log = logger || { log() {}, error() {} };
|
|
29
|
+
const file = args && args[0];
|
|
30
|
+
|
|
31
|
+
if (!file || typeof file !== 'string') {
|
|
32
|
+
log.error(tr(t, 'cli.harnessPreview.file_required', null, 'harness:preview requires a <file> path argument.'));
|
|
33
|
+
process.exitCode = EXIT_INPUT;
|
|
34
|
+
return { ok: false, exitCode: EXIT_INPUT, error: 'file_required' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// SF-02 (decisão de design, não bug): leitor read-only operador-local. Lê
|
|
38
|
+
// qualquer path legível por design — o caso de uso é previewar logs de teste
|
|
39
|
+
// (ex.: `npm test > test.log`) que podem viver fora do cwd. Sem cruzamento de
|
|
40
|
+
// fronteira de confiança (o operador já tem acesso ao FS); por isso não há
|
|
41
|
+
// contenção de workspace aqui. Mantém-se intencionalmente irrestrito.
|
|
42
|
+
const abs = path.resolve(process.cwd(), file);
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = fs.readFileSync(abs, 'utf8');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.code === 'ENOENT') {
|
|
48
|
+
log.error(tr(t, 'cli.harnessPreview.not_found', { path: file }, `File not found: ${file}`));
|
|
49
|
+
process.exitCode = EXIT_INPUT;
|
|
50
|
+
return { ok: false, exitCode: EXIT_INPUT, error: 'not_found' };
|
|
51
|
+
}
|
|
52
|
+
log.error(tr(t, 'cli.harnessPreview.read_error', { path: file, error: err.message }, `Could not read file: ${file} (${err.message})`));
|
|
53
|
+
process.exitCode = EXIT_INPUT;
|
|
54
|
+
return { ok: false, exitCode: EXIT_INPUT, error: 'read_error' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let maxBytes = Number(options['max-bytes'] ?? options.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
58
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) maxBytes = DEFAULT_MAX_BYTES;
|
|
59
|
+
|
|
60
|
+
// Modo leitura: o arquivo já está persistido — não reescrever (persist:false).
|
|
61
|
+
const result = previewArtifact(content, { maxBytes, artifactPath: abs, persist: false });
|
|
62
|
+
log.log(result.preview);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
exitCode: EXIT_OK,
|
|
67
|
+
file: file.replaceAll('\\', '/'),
|
|
68
|
+
totalBytes: result.totalBytes,
|
|
69
|
+
truncated: result.truncated,
|
|
70
|
+
preview: result.preview
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { runHarnessPreview };
|
|
@@ -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 };
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* aioson preflight:context — Estimate context budget before a session
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* aioson preflight:context . --agent=dev
|
|
8
|
-
* aioson preflight:context . --agent=orchestrator --squad=content-team
|
|
9
|
-
* aioson preflight:context . --agent=dev --
|
|
10
|
-
* aioson preflight:context . --agent=dev --
|
|
7
|
+
* aioson preflight:context . --agent=dev
|
|
8
|
+
* aioson preflight:context . --agent=orchestrator --squad=content-team
|
|
9
|
+
* aioson preflight:context . --agent=dev --mode=executing --task="create command" --paths=src/commands/foo.js
|
|
10
|
+
* aioson preflight:context . --agent=dev --verbose
|
|
11
|
+
* aioson preflight:context . --agent=dev --json
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
const path = require('node:path');
|
|
@@ -15,11 +16,14 @@ const { estimateContext, formatReport } = require('../squad/preflight-context');
|
|
|
15
16
|
|
|
16
17
|
async function runPreflightContext({ args, options = {}, logger }) {
|
|
17
18
|
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
18
|
-
const agent = String(options.agent || options.a || 'dev').trim();
|
|
19
|
-
const squad = options.squad ? String(options.squad).trim() : undefined;
|
|
20
|
-
const verbose = Boolean(options.verbose || options.v);
|
|
21
|
-
|
|
22
|
-
const
|
|
19
|
+
const agent = String(options.agent || options.a || 'dev').trim();
|
|
20
|
+
const squad = options.squad ? String(options.squad).trim() : undefined;
|
|
21
|
+
const verbose = Boolean(options.verbose || options.v);
|
|
22
|
+
const mode = String(options.mode || 'planning').trim();
|
|
23
|
+
const task = String(options.task || options.goal || '').trim();
|
|
24
|
+
const paths = String(options.paths || options.path || '').trim();
|
|
25
|
+
|
|
26
|
+
const result = await estimateContext(targetDir, { agent, squad, verbose, mode, task, paths });
|
|
23
27
|
|
|
24
28
|
if (options.json) return result;
|
|
25
29
|
|