@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +932 -919
  2. package/docs/en/5-reference/cli-reference.md +85 -0
  3. package/docs/pt/4-agentes/pm.md +31 -4
  4. package/docs/pt/5-referencia/README.md +3 -0
  5. package/docs/pt/5-referencia/autopilot-handoff.md +131 -0
  6. package/docs/pt/5-referencia/comandos-cli.md +72 -6
  7. package/docs/pt/5-referencia/harness-retro.md +133 -0
  8. package/docs/pt/5-referencia/loop-guardrails.md +225 -0
  9. package/docs/pt/5-referencia/sdd-automation-scripts.md +25 -13
  10. package/package.json +1 -1
  11. package/src/agents.js +1 -1
  12. package/src/cli.js +70 -29
  13. package/src/commands/agent-epilogue.js +186 -0
  14. package/src/commands/context-select.js +33 -0
  15. package/src/commands/harness-preview.js +74 -0
  16. package/src/commands/harness-retro.js +221 -0
  17. package/src/commands/preflight-context.js +13 -9
  18. package/src/commands/review-cycle.js +328 -0
  19. package/src/commands/runtime.js +4 -4
  20. package/src/commands/self-implement-loop.js +12 -2
  21. package/src/commands/state-save.js +2 -0
  22. package/src/commands/workflow-execute.js +138 -28
  23. package/src/commands/workflow-next.js +11 -2
  24. package/src/commands/workflow-status.js +30 -10
  25. package/src/constants.js +15 -13
  26. package/src/context-memory.js +50 -25
  27. package/src/context-selector.js +394 -0
  28. package/src/harness/preview-artifact.js +85 -0
  29. package/src/i18n/messages/en.js +34 -7
  30. package/src/i18n/messages/es.js +34 -7
  31. package/src/i18n/messages/fr.js +34 -7
  32. package/src/i18n/messages/pt-BR.js +34 -7
  33. package/src/lib/retro/retro-aggregate.js +192 -0
  34. package/src/lib/retro/retro-render.js +185 -0
  35. package/src/lib/retro/retro-sources.js +624 -0
  36. package/src/parser.js +1 -1
  37. package/src/squad/preflight-context.js +26 -27
  38. package/template/.aioson/agents/analyst.md +41 -46
  39. package/template/.aioson/agents/architect.md +33 -46
  40. package/template/.aioson/agents/briefing.md +76 -67
  41. package/template/.aioson/agents/dev.md +73 -55
  42. package/template/.aioson/agents/deyvin.md +55 -50
  43. package/template/.aioson/agents/discovery-design-doc.md +35 -22
  44. package/template/.aioson/agents/manifests/architect.manifest.json +11 -1
  45. package/template/.aioson/agents/manifests/dev.manifest.json +15 -0
  46. package/template/.aioson/agents/manifests/pm.manifest.json +20 -0
  47. package/template/.aioson/agents/orchestrator.md +31 -18
  48. package/template/.aioson/agents/pentester.md +12 -4
  49. package/template/.aioson/agents/pm.md +41 -35
  50. package/template/.aioson/agents/product.md +116 -165
  51. package/template/.aioson/agents/qa.md +44 -13
  52. package/template/.aioson/agents/scope-check.md +46 -24
  53. package/template/.aioson/agents/sheldon.md +13 -0
  54. package/template/.aioson/agents/tester.md +28 -5
  55. package/template/.aioson/agents/ux-ui.md +36 -31
  56. package/template/.aioson/agents/validator.md +10 -2
  57. package/template/.aioson/config/autonomy-protocol.json +7 -0
  58. package/template/.aioson/design-docs/code-reuse.md +10 -5
  59. package/template/.aioson/design-docs/componentization.md +10 -5
  60. package/template/.aioson/design-docs/file-size.md +10 -5
  61. package/template/.aioson/design-docs/folder-structure.md +10 -5
  62. package/template/.aioson/design-docs/naming.md +10 -5
  63. package/template/.aioson/docs/autonomy-protocol.md +2 -2
  64. package/template/.aioson/docs/autopilot-handoff.md +82 -34
  65. package/template/.aioson/docs/briefing/briefing-craft.md +9 -3
  66. package/template/.aioson/docs/deyvin/continuity-recovery.md +18 -22
  67. package/template/.aioson/docs/product/conversation-playbook.md +8 -3
  68. package/template/.aioson/docs/product/prd-contract.md +8 -3
  69. package/template/.aioson/docs/product/quality-lens.md +8 -3
  70. package/template/.aioson/docs/product/research-loop.md +8 -3
  71. package/template/.aioson/docs/ux-ui/accessibility-audit.md +7 -2
  72. package/template/.aioson/docs/ux-ui/audit-mode.md +7 -2
  73. package/template/.aioson/docs/ux-ui/component-map.md +7 -2
  74. package/template/.aioson/docs/ux-ui/design-execution.md +7 -2
  75. package/template/.aioson/docs/ux-ui/design-gate.md +7 -2
  76. package/template/.aioson/docs/ux-ui/research-mode.md +7 -2
  77. package/template/.aioson/docs/ux-ui/site-delivery.md +7 -2
  78. package/template/.aioson/docs/ux-ui/token-contract.md +7 -2
  79. package/template/.aioson/rules/aioson-context-boundary.md +11 -9
  80. package/template/.aioson/rules/disk-first-artifacts.md +1 -1
  81. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +1 -1
  82. package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +3 -2
  83. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +21 -9
  84. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -1
  85. package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +2 -1
  86. package/template/.aioson/skills/static/web-research-cache.md +29 -8
  87. package/template/AGENTS.md +1 -1
  88. 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 --verbose
10
- * aioson preflight:context . --agent=dev --json
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 result = await estimateContext(targetDir, { agent, squad, verbose });
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