@jaimevalasek/aioson 1.33.1 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/docs/en/1-understand/ecosystem-map.md +45 -29
  3. package/docs/en/1-understand/glossary.md +6 -6
  4. package/docs/en/1-understand/what-is-aioson.md +5 -5
  5. package/docs/en/2-start/existing-project.md +7 -7
  6. package/docs/en/2-start/first-project.md +33 -39
  7. package/docs/en/2-start/initial-decisions.md +22 -19
  8. package/docs/en/3-recipes/README.md +2 -2
  9. package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
  10. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +13 -3
  11. package/docs/en/3-recipes/full-feature-with-sheldon.md +329 -338
  12. package/docs/en/4-agents/README.md +33 -16
  13. package/docs/en/4-agents/briefing-refiner.md +146 -0
  14. package/docs/en/4-agents/discovery-design-doc.md +20 -8
  15. package/docs/en/4-agents/forge-run.md +3 -2
  16. package/docs/en/5-reference/README.md +1 -0
  17. package/docs/en/5-reference/autopilot-handoff.md +286 -0
  18. package/docs/en/5-reference/cli-reference.md +57 -48
  19. package/docs/en/5-reference/executable-verification.md +10 -7
  20. package/docs/en/5-reference/parallel.md +2 -0
  21. package/docs/en/5-reference/qa-browser.md +2 -2
  22. package/docs/en/README.md +3 -3
  23. package/docs/pt/1-entender/glossario.md +5 -5
  24. package/docs/pt/1-entender/mapa-do-ecossistema.md +33 -22
  25. package/docs/pt/2-comecar/decisoes-iniciais.md +20 -18
  26. package/docs/pt/2-comecar/primeiro-projeto.md +2 -2
  27. package/docs/pt/3-receitas/README.md +1 -1
  28. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  29. package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
  30. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +291 -338
  31. package/docs/pt/4-agentes/README.md +13 -11
  32. package/docs/pt/4-agentes/analyst.md +9 -5
  33. package/docs/pt/4-agentes/architect.md +9 -5
  34. package/docs/pt/4-agentes/briefing-refiner.md +64 -40
  35. package/docs/pt/4-agentes/briefing.md +6 -1
  36. package/docs/pt/4-agentes/dev.md +38 -8
  37. package/docs/pt/4-agentes/deyvin.md +4 -0
  38. package/docs/pt/4-agentes/discover.md +4 -0
  39. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  40. package/docs/pt/4-agentes/neo.md +4 -0
  41. package/docs/pt/4-agentes/orache.md +6 -0
  42. package/docs/pt/4-agentes/orchestrator.md +176 -118
  43. package/docs/pt/4-agentes/pentester.md +11 -5
  44. package/docs/pt/4-agentes/pm.md +15 -7
  45. package/docs/pt/4-agentes/product.md +19 -1
  46. package/docs/pt/4-agentes/qa.md +10 -2
  47. package/docs/pt/4-agentes/setup.md +3 -1
  48. package/docs/pt/4-agentes/sheldon.md +178 -99
  49. package/docs/pt/4-agentes/tester.md +6 -0
  50. package/docs/pt/4-agentes/ux-ui.md +9 -5
  51. package/docs/pt/5-referencia/README.md +1 -1
  52. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  53. package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
  54. package/docs/pt/5-referencia/comandos-cli.md +16 -7
  55. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  56. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  57. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  58. package/docs/pt/5-referencia/skills.md +2 -0
  59. package/docs/pt/README.md +4 -4
  60. package/docs/pt/agentes.md +50 -50
  61. package/package.json +2 -2
  62. package/src/agents.js +1 -1
  63. package/src/artifact-kinds.js +111 -0
  64. package/src/autopilot-signal.js +71 -0
  65. package/src/cli.js +112 -81
  66. package/src/commands/agent-epilogue.js +251 -186
  67. package/src/commands/agents.js +122 -50
  68. package/src/commands/audit-code.js +344 -0
  69. package/src/commands/briefing.js +337 -1
  70. package/src/commands/classify.js +389 -389
  71. package/src/commands/feature-close.js +122 -4
  72. package/src/commands/harness-check.js +259 -175
  73. package/src/commands/live.js +71 -11
  74. package/src/commands/review-feature.js +189 -0
  75. package/src/commands/runtime.js +81 -66
  76. package/src/commands/sync-agents-copy.js +115 -0
  77. package/src/commands/update.js +5 -1
  78. package/src/commands/verification-plan.js +143 -0
  79. package/src/commands/verify-artifact.js +593 -0
  80. package/src/commands/workflow-execute.js +434 -316
  81. package/src/commands/workflow-next.js +193 -20
  82. package/src/commands/workflow-plan.js +5 -5
  83. package/src/constants.js +2 -0
  84. package/src/doctor.js +4 -2
  85. package/src/gateway-pointer-merge.js +7 -1
  86. package/src/handoff-contract.js +267 -172
  87. package/src/harness/contract-integrity-gate.js +172 -0
  88. package/src/harness/contract-integrity.js +111 -0
  89. package/src/harness/contract-schema.js +377 -332
  90. package/src/harness/detect-runtime-feature.js +90 -0
  91. package/src/harness/static-criteria.js +192 -0
  92. package/src/i18n/messages/en.js +10 -5
  93. package/src/i18n/messages/es.js +10 -5
  94. package/src/i18n/messages/fr.js +10 -5
  95. package/src/i18n/messages/pt-BR.js +10 -5
  96. package/src/install-wizard.js +8 -8
  97. package/src/installer.js +13 -6
  98. package/src/lib/briefing-refiner/apply-feedback.js +18 -4
  99. package/src/lib/briefing-refiner/feedback-schema.js +73 -4
  100. package/src/lib/briefing-refiner/refinement-report.js +11 -0
  101. package/src/lib/briefing-refiner/review-html.js +388 -68
  102. package/src/parser.js +12 -0
  103. package/src/preflight-engine.js +12 -12
  104. package/src/verification/policy-engine.js +95 -95
  105. package/src/verification-policy.js +398 -0
  106. package/src/version.js +52 -1
  107. package/template/.aioson/agents/architect.md +34 -5
  108. package/template/.aioson/agents/briefing-refiner.md +91 -48
  109. package/template/.aioson/agents/briefing.md +4 -0
  110. package/template/.aioson/agents/committer.md +2 -1
  111. package/template/.aioson/agents/copywriter.md +30 -21
  112. package/template/.aioson/agents/design-hybrid-forge.md +10 -1
  113. package/template/.aioson/agents/dev.md +35 -23
  114. package/template/.aioson/agents/deyvin.md +4 -0
  115. package/template/.aioson/agents/discover.md +16 -3
  116. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  117. package/template/.aioson/agents/genome.md +19 -10
  118. package/template/.aioson/agents/neo.md +34 -30
  119. package/template/.aioson/agents/orache.md +24 -11
  120. package/template/.aioson/agents/orchestrator.md +100 -7
  121. package/template/.aioson/agents/pentester.md +4 -0
  122. package/template/.aioson/agents/pm.md +8 -8
  123. package/template/.aioson/agents/product.md +25 -1
  124. package/template/.aioson/agents/profiler-enricher.md +20 -11
  125. package/template/.aioson/agents/profiler-forge.md +20 -11
  126. package/template/.aioson/agents/profiler-researcher.md +20 -11
  127. package/template/.aioson/agents/qa.md +96 -31
  128. package/template/.aioson/agents/setup.md +12 -1
  129. package/template/.aioson/agents/sheldon.md +103 -13
  130. package/template/.aioson/agents/site-forge.md +11 -2
  131. package/template/.aioson/agents/squad.md +20 -5
  132. package/template/.aioson/agents/tester.md +4 -0
  133. package/template/.aioson/agents/ux-ui.md +2 -1
  134. package/template/.aioson/agents/validator.md +33 -1
  135. package/template/.aioson/config/verification.json +61 -0
  136. package/template/.aioson/config.md +13 -9
  137. package/template/.aioson/docs/LAYERS.md +2 -2
  138. package/template/.aioson/docs/agent-help.md +126 -0
  139. package/template/.aioson/docs/autopilot-handoff.md +30 -20
  140. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  141. package/template/.aioson/docs/dev/phase-loop.md +50 -0
  142. package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
  143. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  144. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  145. package/template/.aioson/docs/product/prd-contract.md +12 -12
  146. package/template/.aioson/docs/prototype-contract.md +21 -4
  147. package/template/.aioson/docs/reference-identity.md +94 -0
  148. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  149. package/template/.aioson/docs/verification-config.md +82 -0
  150. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  151. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  152. package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
  153. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  154. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  155. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  156. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  157. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  158. package/template/.aioson/skills/process/prototype-forge/SKILL.md +6 -0
  159. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  160. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  161. package/template/AGENTS.md +36 -36
  162. package/template/CLAUDE.md +15 -11
@@ -14,13 +14,20 @@
14
14
  * Format: YAML frontmatter (briefings: array) + Markdown table
15
15
  */
16
16
 
17
+ const fsp = require('node:fs/promises');
17
18
  const path = require('node:path');
18
19
  const readline = require('node:readline');
19
20
  const {
20
21
  configPath: registryConfigPath,
22
+ markRefinementState,
21
23
  readBriefingRegistry,
22
24
  writeBriefingRegistry
23
25
  } = require('../lib/briefing-refiner/briefing-registry');
26
+ const { hashText, parseBriefingSections } = require('../lib/briefing-refiner/briefing-sections');
27
+ const { assertFeedbackPath, validateFeedback, validateFindingsInput } = require('../lib/briefing-refiner/feedback-schema');
28
+ const { resolveBriefingPath } = require('../lib/briefing-refiner/briefing-paths');
29
+ const { writeReviewArtifacts } = require('../lib/briefing-refiner/review-html');
30
+ const { applyConfirmedFeedback, applyDeclinedFeedback } = require('../lib/briefing-refiner/apply-feedback');
24
31
 
25
32
  // ─── Interactive prompt helpers ───────────────────────────────────────────────
26
33
 
@@ -219,4 +226,333 @@ async function runBriefingUnapprove({ args, options = {}, logger }) {
219
226
  return { ok: true, unapproved: names };
220
227
  }
221
228
 
222
- module.exports = { runBriefingApprove, runBriefingUnapprove };
229
+ // ─── briefing:review / briefing:apply-feedback ────────────────────────────────
230
+ //
231
+ // The deterministic half of @briefing-refiner: the agent does the intelligent
232
+ // audit (findings), the CLI owns the surface — parse briefings.md, render
233
+ // review.html + refinement-feedback.json + refinement-report.md, and later
234
+ // validate + apply the exported feedback. This kills the per-run hand-written
235
+ // HTML (token cost + schema drift) that used to stand in for these commands.
236
+
237
+ const USER_EXPORT_METHODS = new Set(['download', 'copy-paste', 'file-system-access']);
238
+
239
+ /**
240
+ * Resolve which briefing a refinement command targets. Refinable = `draft`, or
241
+ * `approved` with no PRD yet (same rule as the @briefing-refiner contract).
242
+ * Unambiguous cases resolve without --slug; ambiguity is an error (these
243
+ * commands are agent-driven, so no interactive prompt).
244
+ */
245
+ async function resolveRefinableSlug(projectDir, slugOpt) {
246
+ let data;
247
+ try {
248
+ data = await readBriefingRegistry(projectDir);
249
+ } catch (error) {
250
+ return { ok: false, error: error && error.code === 'invalid_frontmatter' ? 'invalid_frontmatter' : 'no_config' };
251
+ }
252
+
253
+ const refinable = data.briefings.filter(
254
+ (b) => b.status === 'draft' || (b.status === 'approved' && !b.prd_generated)
255
+ );
256
+ const slug = String(slugOpt || '').trim() || null;
257
+
258
+ if (slug) {
259
+ if (!refinable.some((b) => b.slug === slug)) {
260
+ return { ok: false, error: 'slug_not_refinable', slug, candidates: refinable.map((b) => b.slug) };
261
+ }
262
+ return { ok: true, slug };
263
+ }
264
+ if (refinable.length === 1) return { ok: true, slug: refinable[0].slug };
265
+ if (refinable.length === 0) return { ok: false, error: 'no_refinable' };
266
+ return { ok: false, error: 'ambiguous_slug', candidates: refinable.map((b) => b.slug) };
267
+ }
268
+
269
+ function logSlugResolutionError(resolved, logger) {
270
+ if (resolved.error === 'no_config') {
271
+ logger.error('No briefing registry found. Activate @briefing to create the first briefing.');
272
+ } else if (resolved.error === 'invalid_frontmatter') {
273
+ logger.error('briefings/config.md has invalid frontmatter. Fix it manually.');
274
+ } else if (resolved.error === 'no_refinable') {
275
+ logger.error('No refinable briefing (draft, or approved without a PRD).');
276
+ } else if (resolved.error === 'ambiguous_slug') {
277
+ logger.error(`Multiple refinable briefings — pass --slug=<slug>. Candidates: ${resolved.candidates.join(', ')}`);
278
+ } else if (resolved.error === 'slug_not_refinable') {
279
+ logger.error(`Briefing "${resolved.slug}" is not refinable. Candidates: ${(resolved.candidates || []).join(', ') || 'none'}`);
280
+ }
281
+ }
282
+
283
+ async function readOptionalJson(filePath) {
284
+ let raw;
285
+ try {
286
+ raw = await fsp.readFile(filePath, 'utf8');
287
+ } catch (error) {
288
+ if (error && error.code === 'ENOENT') return { exists: false, value: null };
289
+ return { exists: true, error: error.message, value: null };
290
+ }
291
+ try {
292
+ return { exists: true, value: JSON.parse(raw) };
293
+ } catch (error) {
294
+ return { exists: true, error: `invalid JSON: ${error.message}`, value: null };
295
+ }
296
+ }
297
+
298
+ async function runBriefingReview({ args, options = {}, logger }) {
299
+ const projectDir = path.resolve(process.cwd(), args[0] || '.');
300
+ const resolved = await resolveRefinableSlug(projectDir, options.slug);
301
+ if (!resolved.ok) {
302
+ logSlugResolutionError(resolved, logger);
303
+ return resolved;
304
+ }
305
+ const slug = resolved.slug;
306
+
307
+ let markdown;
308
+ try {
309
+ markdown = await fsp.readFile(resolveBriefingPath(projectDir, slug, 'briefings.md'), 'utf8');
310
+ } catch {
311
+ logger.error(`briefings.md not found for "${slug}".`);
312
+ return { ok: false, error: 'briefing_not_found', slug };
313
+ }
314
+
315
+ const parsed = parseBriefingSections(markdown, `.aioson/briefings/${slug}/briefings.md`);
316
+ if (parsed.missing.length > 0) {
317
+ logger.error(`briefings.md is missing mandatory sections: ${parsed.missing.join(', ')}`);
318
+ return { ok: false, error: 'missing_sections', slug, missing: parsed.missing };
319
+ }
320
+
321
+ // Findings: --findings=<path>, else the canonical drop path the agent writes.
322
+ const findingsExplicit = Boolean(options.findings);
323
+ const findingsPath = findingsExplicit
324
+ ? path.resolve(projectDir, String(options.findings))
325
+ : resolveBriefingPath(projectDir, slug, 'refinement-findings.json');
326
+ const findingsRead = await readOptionalJson(findingsPath);
327
+ if (findingsRead.error) {
328
+ logger.error(`cannot read findings file (${findingsPath}): ${findingsRead.error}`);
329
+ return { ok: false, error: 'invalid_findings', slug };
330
+ }
331
+ if (findingsExplicit && !findingsRead.exists) {
332
+ logger.error(`findings file not found: ${findingsPath}`);
333
+ return { ok: false, error: 'invalid_findings', slug };
334
+ }
335
+ let findings = [];
336
+ if (findingsRead.exists) {
337
+ const value = findingsRead.value;
338
+ findings = Array.isArray(value) ? value : (value && Array.isArray(value.findings) ? value.findings : null);
339
+ if (!findings) {
340
+ logger.error('findings file must be a JSON array or { "findings": [...] }');
341
+ return { ok: false, error: 'invalid_findings', slug };
342
+ }
343
+ const check = validateFindingsInput(findings, { sectionIds: parsed.sections.map((s) => s.id) });
344
+ if (!check.ok) {
345
+ for (const err of check.errors) logger.error(`findings: ${err}`);
346
+ return { ok: false, error: 'invalid_findings', slug, errors: check.errors };
347
+ }
348
+ }
349
+
350
+ // Round counter + pending-feedback protection: never silently clobber
351
+ // feedback the user already exported for the CURRENT briefing text. The
352
+ // round survives apply (which archives the canonical feedback) by also
353
+ // counting the applied-round archives.
354
+ const feedbackPath = resolveBriefingPath(projectDir, slug, 'refinement-feedback.json');
355
+ let lastRound = 0;
356
+ const prior = await readOptionalJson(feedbackPath);
357
+ if (prior.exists && prior.value && typeof prior.value === 'object') {
358
+ lastRound = Number.isInteger(prior.value.round) ? prior.value.round : 0;
359
+ const userExported = USER_EXPORT_METHODS.has(prior.value.export_method);
360
+ const fresh = prior.value.source_hash === parsed.source_hash;
361
+ if (userExported && fresh && !options.force) {
362
+ logger.error('refinement-feedback.json holds user-exported feedback for the current briefing text.');
363
+ logger.error('Apply it first (aioson briefing:apply-feedback) or pass --force to overwrite it.');
364
+ return { ok: false, error: 'pending_feedback', slug };
365
+ }
366
+ }
367
+ try {
368
+ const entries = await fsp.readdir(resolveBriefingPath(projectDir, slug));
369
+ for (const entry of entries) {
370
+ const match = entry.match(/^refinement-feedback\.(?:applied|declined)-round(\d+)/);
371
+ if (match) lastRound = Math.max(lastRound, Number(match[1]));
372
+ }
373
+ } catch { /* directory listing is best-effort */ }
374
+ const round = lastRound + 1;
375
+
376
+ const locale = String(options.locale || process.env.AIOS_LITE_LOCALE || 'en');
377
+ await writeReviewArtifacts(projectDir, {
378
+ slug,
379
+ sourceMarkdown: markdown,
380
+ sections: parsed.sections,
381
+ sourceHash: parsed.source_hash,
382
+ findings,
383
+ round,
384
+ locale
385
+ });
386
+
387
+ // Best-effort registry pointer so the dashboard/agents see the active review.
388
+ try {
389
+ const registry = await readBriefingRegistry(projectDir);
390
+ markRefinementState(registry, slug, {
391
+ refinement_status: 'review_generated',
392
+ review_html: `.aioson/briefings/${slug}/review.html`,
393
+ refinement_report: `.aioson/briefings/${slug}/refinement-report.md`
394
+ });
395
+ await writeBriefingRegistry(projectDir, registry);
396
+ } catch { /* registry pointer is advisory */ }
397
+
398
+ const reviewRel = `.aioson/briefings/${slug}/review.html`;
399
+ logger.log(`✓ Review generated for "${slug}" (round ${round}): ${parsed.sections.length} sections, ${findings.length} findings.`);
400
+ logger.log(` Open ${reviewRel} in a real browser (not an editor preview), review, then export the JSON.`);
401
+ if (!findingsRead.exists) {
402
+ logger.log(' No findings file was provided — the review carries sections only.');
403
+ }
404
+ return {
405
+ ok: true,
406
+ slug,
407
+ round,
408
+ sections: parsed.sections.length,
409
+ findings: findings.length,
410
+ source_hash: parsed.source_hash,
411
+ review: reviewRel,
412
+ feedback: `.aioson/briefings/${slug}/refinement-feedback.json`,
413
+ report: `.aioson/briefings/${slug}/refinement-report.md`
414
+ };
415
+ }
416
+
417
+ function summarizeFeedback(feedback) {
418
+ const sections = feedback.sections || [];
419
+ const findings = feedback.findings || [];
420
+ const changed = sections.filter((s) => s.status !== 'unchanged' || s.current_text !== s.original_text);
421
+ const findingsByStatus = {};
422
+ for (const finding of findings) {
423
+ findingsByStatus[finding.status] = (findingsByStatus[finding.status] || 0) + 1;
424
+ }
425
+ return {
426
+ changed_sections: changed.map((s) => ({ id: s.id, status: s.status, text_changed: s.current_text !== s.original_text })),
427
+ blocked_sections: sections.filter((s) => s.status === 'blocked').map((s) => s.id),
428
+ comments: (feedback.comments || []).length,
429
+ findings_total: findings.length,
430
+ findings_by_status: findingsByStatus,
431
+ pending_blocking_findings: findings.filter((f) => f.blocking && f.status === 'pending').map((f) => f.id),
432
+ blocking_items: (feedback.blocking_items || []).length
433
+ };
434
+ }
435
+
436
+ async function runBriefingApplyFeedback({ args, options = {}, logger }) {
437
+ const projectDir = path.resolve(process.cwd(), args[0] || '.');
438
+ const resolved = await resolveRefinableSlug(projectDir, options.slug);
439
+ if (!resolved.ok) {
440
+ logSlugResolutionError(resolved, logger);
441
+ return resolved;
442
+ }
443
+ const slug = resolved.slug;
444
+ const allowStale = Boolean(options['allow-stale'] || options.allowStale);
445
+
446
+ const feedbackRel = options.feedback
447
+ ? String(options.feedback)
448
+ : `.aioson/briefings/${slug}/refinement-feedback.json`;
449
+ try {
450
+ assertFeedbackPath(projectDir, slug, feedbackRel);
451
+ } catch (error) {
452
+ logger.error(error.message);
453
+ return { ok: false, error: 'invalid_feedback_path', slug };
454
+ }
455
+ const feedbackPath = path.resolve(projectDir, feedbackRel);
456
+ const feedbackRead = await readOptionalJson(feedbackPath);
457
+ if (!feedbackRead.exists || feedbackRead.error) {
458
+ logger.error(`cannot read feedback (${feedbackRel}): ${feedbackRead.error || 'file not found'}`);
459
+ return { ok: false, error: 'feedback_not_found', slug };
460
+ }
461
+ const feedback = feedbackRead.value;
462
+
463
+ if (options.declined) {
464
+ const result = await applyDeclinedFeedback(projectDir, slug, feedback, { allowStale: true });
465
+ if (!result.ok) {
466
+ for (const err of (result.validation && result.validation.errors) || []) logger.error(`feedback: ${err}`);
467
+ return { ...result, slug, mode: 'declined' };
468
+ }
469
+ // Archive the declined feedback so "file present = pending" stays true and
470
+ // the next briefing:review does not dead-end on pending_feedback. Findings
471
+ // are NOT archived: the briefing text is unchanged, so they stay valid.
472
+ const canonicalPath = resolveBriefingPath(projectDir, slug, 'refinement-feedback.json');
473
+ if (feedbackPath === canonicalPath) {
474
+ const round = feedback.round || 1;
475
+ let archiveName = `refinement-feedback.declined-round${round}.json`;
476
+ try {
477
+ await fsp.access(resolveBriefingPath(projectDir, slug, archiveName));
478
+ archiveName = `refinement-feedback.declined-round${round}-${Date.now()}.json`;
479
+ } catch { /* target free */ }
480
+ try {
481
+ await fsp.rename(canonicalPath, resolveBriefingPath(projectDir, slug, archiveName));
482
+ result.archived = `.aioson/briefings/${slug}/${archiveName}`;
483
+ } catch { /* archive is best-effort */ }
484
+ }
485
+ logger.log(`✓ Feedback declined for "${slug}" — briefings.md unchanged, ${result.skippedChanges.length} change(s) recorded as skipped.`);
486
+ return { ...result, slug, mode: 'declined' };
487
+ }
488
+
489
+ if (!options.confirm) {
490
+ // Dry-run: validate + summarize so the agent can present the plan and ask
491
+ // for the human go-ahead. Never touches briefings.md.
492
+ let current;
493
+ try {
494
+ current = await fsp.readFile(resolveBriefingPath(projectDir, slug, 'briefings.md'), 'utf8');
495
+ } catch {
496
+ logger.error(`briefings.md not found for "${slug}".`);
497
+ return { ok: false, error: 'briefing_not_found', slug };
498
+ }
499
+ const validation = validateFeedback(feedback, { slug, currentSourceHash: hashText(current), allowStale });
500
+ const summary = summarizeFeedback(feedback);
501
+ if (!validation.ok) {
502
+ for (const err of validation.errors) logger.error(`feedback: ${err}`);
503
+ return { ok: false, error: 'invalid_feedback', slug, mode: 'dry-run', validation, summary };
504
+ }
505
+ logger.log(`Dry-run for "${slug}" (feedback round ${feedback.round || 1}):`);
506
+ logger.log(` changed sections: ${summary.changed_sections.map((c) => c.id).join(', ') || 'none'}`);
507
+ logger.log(` blocked sections: ${summary.blocked_sections.join(', ') || 'none'}`);
508
+ logger.log(` findings: ${summary.findings_total} (${Object.entries(summary.findings_by_status).map(([k, v]) => `${k}: ${v}`).join(', ') || '-'})`);
509
+ logger.log(` pending blocking findings: ${summary.pending_blocking_findings.join(', ') || 'none'}`);
510
+ for (const warning of validation.warnings) logger.log(` warning: ${warning}`);
511
+ logger.log('Re-run with --confirm to apply.');
512
+ return { ok: true, slug, mode: 'dry-run', pending_confirmation: true, validation, summary };
513
+ }
514
+
515
+ const result = await applyConfirmedFeedback(projectDir, slug, feedback, { confirmed: true, allowStale });
516
+ if (!result.ok) {
517
+ for (const err of (result.validation && result.validation.errors) || []) logger.error(`feedback: ${err}`);
518
+ if (result.error === 'invalid_feedback' && result.validation && result.validation.stale && !allowStale) {
519
+ logger.error('Feedback is stale (briefings.md changed since the review). Regenerate the review or pass --allow-stale.');
520
+ }
521
+ return { ...result, slug, mode: 'apply' };
522
+ }
523
+
524
+ // Archive the consumed canonical feedback so "pending feedback" stays a
525
+ // deterministic signal (file present = not yet applied).
526
+ const canonicalPath = resolveBriefingPath(projectDir, slug, 'refinement-feedback.json');
527
+ if (feedbackPath === canonicalPath) {
528
+ const round = feedback.round || 1;
529
+ let archiveName = `refinement-feedback.applied-round${round}.json`;
530
+ try {
531
+ await fsp.access(resolveBriefingPath(projectDir, slug, archiveName));
532
+ archiveName = `refinement-feedback.applied-round${round}-${Date.now()}.json`;
533
+ } catch { /* target free */ }
534
+ try {
535
+ await fsp.rename(canonicalPath, resolveBriefingPath(projectDir, slug, archiveName));
536
+ result.archived = `.aioson/briefings/${slug}/${archiveName}`;
537
+ } catch { /* archive is best-effort */ }
538
+ // Retire the consumed findings too — the next round must audit the UPDATED
539
+ // briefing, not silently reuse last round's findings.
540
+ try {
541
+ await fsp.rename(
542
+ resolveBriefingPath(projectDir, slug, 'refinement-findings.json'),
543
+ resolveBriefingPath(projectDir, slug, `refinement-findings.applied-round${round}.json`)
544
+ );
545
+ } catch { /* no findings file, or archive failed — best-effort */ }
546
+ }
547
+
548
+ logger.log(`✓ Applied ${result.appliedChanges.length} change(s) to "${slug}".`);
549
+ if (result.returnedToDraft) logger.log(' Briefing returned from approved to draft.');
550
+ if (result.nextAction === 'resolve_blockers') {
551
+ logger.log(' Blockers remain — resolve them and regenerate the review (aioson briefing:review).');
552
+ } else {
553
+ logger.log(' No blockers — approve with `aioson briefing:approve` and hand off to @product, or regenerate the review for another round.');
554
+ }
555
+ return { ...result, slug, mode: 'apply' };
556
+ }
557
+
558
+ module.exports = { runBriefingApprove, runBriefingUnapprove, runBriefingReview, runBriefingApplyFeedback };