@jaimevalasek/aioson 1.36.0 → 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 (85) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/docs/en/1-understand/ecosystem-map.md +1 -1
  3. package/docs/en/1-understand/glossary.md +1 -1
  4. package/docs/en/2-start/first-project.md +1 -1
  5. package/docs/en/2-start/initial-decisions.md +4 -2
  6. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +8 -1
  7. package/docs/en/3-recipes/full-feature-with-sheldon.md +3 -1
  8. package/docs/en/4-agents/README.md +6 -3
  9. package/docs/en/4-agents/briefing-refiner.md +146 -0
  10. package/docs/en/5-reference/README.md +1 -0
  11. package/docs/en/5-reference/autopilot-handoff.md +286 -0
  12. package/docs/en/5-reference/cli-reference.md +6 -0
  13. package/docs/pt/1-entender/glossario.md +1 -1
  14. package/docs/pt/1-entender/mapa-do-ecossistema.md +1 -1
  15. package/docs/pt/2-comecar/decisoes-iniciais.md +4 -2
  16. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  17. package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
  18. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +3 -1
  19. package/docs/pt/4-agentes/README.md +13 -11
  20. package/docs/pt/4-agentes/briefing-refiner.md +64 -40
  21. package/docs/pt/4-agentes/briefing.md +6 -1
  22. package/docs/pt/4-agentes/dev.md +19 -1
  23. package/docs/pt/4-agentes/deyvin.md +4 -0
  24. package/docs/pt/4-agentes/discover.md +4 -0
  25. package/docs/pt/4-agentes/neo.md +4 -0
  26. package/docs/pt/4-agentes/orache.md +6 -0
  27. package/docs/pt/4-agentes/orchestrator.md +12 -0
  28. package/docs/pt/4-agentes/pentester.md +6 -0
  29. package/docs/pt/4-agentes/product.md +19 -1
  30. package/docs/pt/4-agentes/qa.md +10 -2
  31. package/docs/pt/4-agentes/setup.md +3 -1
  32. package/docs/pt/4-agentes/sheldon.md +12 -0
  33. package/docs/pt/4-agentes/tester.md +6 -0
  34. package/docs/pt/4-agentes/ux-ui.md +2 -1
  35. package/docs/pt/5-referencia/README.md +1 -1
  36. package/docs/pt/5-referencia/agent-chain-continuity.md +1 -1
  37. package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
  38. package/docs/pt/5-referencia/comandos-cli.md +16 -7
  39. package/docs/pt/5-referencia/skills.md +2 -0
  40. package/docs/pt/agentes.md +3 -1
  41. package/package.json +1 -1
  42. package/src/agents.js +1 -1
  43. package/src/artifact-kinds.js +2 -1
  44. package/src/autopilot-signal.js +71 -0
  45. package/src/cli.js +9 -1
  46. package/src/commands/agents.js +18 -2
  47. package/src/commands/briefing.js +337 -1
  48. package/src/commands/feature-close.js +136 -43
  49. package/src/commands/live.js +47 -11
  50. package/src/commands/update.js +5 -1
  51. package/src/commands/verification-plan.js +28 -1
  52. package/src/commands/verify-artifact.js +64 -1
  53. package/src/commands/workflow-execute.js +149 -31
  54. package/src/commands/workflow-next.js +60 -16
  55. package/src/doctor.js +4 -2
  56. package/src/i18n/messages/en.js +2 -1
  57. package/src/i18n/messages/es.js +2 -1
  58. package/src/i18n/messages/fr.js +2 -1
  59. package/src/i18n/messages/pt-BR.js +2 -1
  60. package/src/lib/briefing-refiner/apply-feedback.js +18 -4
  61. package/src/lib/briefing-refiner/feedback-schema.js +73 -4
  62. package/src/lib/briefing-refiner/refinement-report.js +11 -0
  63. package/src/lib/briefing-refiner/review-html.js +388 -68
  64. package/src/parser.js +6 -0
  65. package/template/.aioson/agents/briefing-refiner.md +87 -47
  66. package/template/.aioson/agents/briefing.md +4 -0
  67. package/template/.aioson/agents/dev.md +9 -2
  68. package/template/.aioson/agents/deyvin.md +4 -0
  69. package/template/.aioson/agents/discover.md +4 -0
  70. package/template/.aioson/agents/neo.md +4 -0
  71. package/template/.aioson/agents/orache.md +4 -0
  72. package/template/.aioson/agents/orchestrator.md +16 -0
  73. package/template/.aioson/agents/pentester.md +4 -0
  74. package/template/.aioson/agents/product.md +25 -1
  75. package/template/.aioson/agents/qa.md +5 -1
  76. package/template/.aioson/agents/sheldon.md +9 -1
  77. package/template/.aioson/agents/tester.md +4 -0
  78. package/template/.aioson/agents/ux-ui.md +1 -1
  79. package/template/.aioson/docs/agent-help.md +126 -0
  80. package/template/.aioson/docs/autopilot-handoff.md +26 -16
  81. package/template/.aioson/docs/dev/phase-loop.md +8 -5
  82. package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
  83. package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
  84. package/template/AGENTS.md +36 -36
  85. package/template/CLAUDE.md +1 -1
package/src/doctor.js CHANGED
@@ -228,8 +228,10 @@ async function runDoctor(targetDir) {
228
228
  }
229
229
 
230
230
  // Autopilot handoff: protocol doc installed but flag never declared in the
231
- // context frontmatter autopilot stays silently inactive (absent = manual
232
- // handoffs). An explicit true/false is a deliberate choice and passes.
231
+ // context frontmatter. Absent is no longer "silently manual" @product asks
232
+ // the run mode on screen at each feature kickoff. Declaring an explicit
233
+ // true/false sets a project default (skip the kickoff question) and passes;
234
+ // the advisory just surfaces that a default can be set.
233
235
  const autopilotDocExists = await exists(path.join(targetDir, '.aioson/docs/autopilot-handoff.md'));
234
236
  if (autopilotDocExists && contextValidation.exists && contextValidation.data) {
235
237
  const autoHandoffDeclared = Object.prototype.hasOwnProperty.call(contextValidation.data, 'auto_handoff');
@@ -412,6 +412,7 @@ module.exports = {
412
412
  update: {
413
413
  not_installed: 'No AIOSON installation found in {targetDir}.',
414
414
  done_at: 'Update completed at: {targetDir}',
415
+ template_version: 'Template version applied: {version}',
415
416
  files_updated: 'Files updated: {count}',
416
417
  backups_created: 'Backups created: {count}',
417
418
  profile_renamed: 'i Profile `beginner` renamed to `creator` in project.context.md to better describe the user. Behavior unchanged. Edit the file to switch to `developer` if desired.',
@@ -491,7 +492,7 @@ module.exports = {
491
492
  features_dir_present: 'Features directory present (.aioson/context/features/)',
492
493
  features_dir_present_hint: 'Create .aioson/context/features/ to host per-feature dossiers (doctor --fix will create it).',
493
494
  auto_handoff_declared: 'Autopilot handoff flag declared (auto_handoff in project.context.md)',
494
- auto_handoff_declared_hint: 'The autopilot-handoff protocol is installed but auto_handoff is not set in project.context.md frontmatter autopilot stays inactive. Set auto_handoff: true to enable it, or auto_handoff: false to silence this warning.',
495
+ auto_handoff_declared_hint: 'The autopilot-handoff protocol is installed and auto_handoff is not set @product asks the run mode on screen at each feature kickoff (Autopilot / Step by step / Always). Set auto_handoff: true to always autopilot and skip the question, or false to always go step-by-step.',
495
496
  claude_commands_present: 'Claude slash commands present ({missing} missing of {required})',
496
497
  claude_commands_present_hint: 'Missing: {paths}. Run `aioson doctor . --fix` to restore them from the template.',
497
498
  version_drift: 'CLI version matches project.context.md (context: {context}, CLI: {cli})',
@@ -280,6 +280,7 @@ module.exports = {
280
280
  update: {
281
281
  not_installed: 'No se encontro instalacion de AIOSON en {targetDir}.',
282
282
  done_at: 'Actualizacion completada en: {targetDir}',
283
+ template_version: 'Version de template aplicada: {version}',
283
284
  files_updated: 'Archivos actualizados: {count}',
284
285
  backups_created: 'Backups creados: {count}',
285
286
  profile_renamed: 'i Perfil `beginner` renombrado a `creator` en project.context.md para describir mejor al usuario. Comportamiento sin cambios. Edita el archivo para cambiar a `developer` si lo prefieres.',
@@ -365,7 +366,7 @@ module.exports = {
365
366
  features_dir_present: 'Directorio de features presente (.aioson/context/features/)',
366
367
  features_dir_present_hint: 'Cree .aioson/context/features/ para hospedar dossiers por feature (doctor --fix lo crea).',
367
368
  auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff en project.context.md)',
368
- auto_handoff_declared_hint: 'El protocolo autopilot-handoff esta instalado pero auto_handoff no esta definido en el frontmatter de project.context.md el autopilot queda inactivo. Defina auto_handoff: true para activarlo, o auto_handoff: false para silenciar este aviso.',
369
+ auto_handoff_declared_hint: 'El protocolo autopilot-handoff esta instalado y auto_handoff no esta definido @product pregunta el modo en pantalla al inicio de cada feature (Autopilot / Paso a paso / Siempre). Defina auto_handoff: true para siempre autopilot y omitir la pregunta, o false para siempre paso a paso.',
369
370
  claude_commands_present: 'Slash commands de Claude presentes ({missing} ausentes de {required})',
370
371
  claude_commands_present_hint: 'Ausentes: {paths}. Ejecute `aioson doctor . --fix` para restaurarlos.',
371
372
  version_drift: 'Version del CLI coincide con project.context.md (contexto: {context}, CLI: {cli})',
@@ -279,6 +279,7 @@ module.exports = {
279
279
  update: {
280
280
  not_installed: 'Aucune installation AIOSON trouvee dans {targetDir}.',
281
281
  done_at: 'Mise a jour terminee dans : {targetDir}',
282
+ template_version: 'Version du template appliquee : {version}',
282
283
  files_updated: 'Fichiers mis a jour : {count}',
283
284
  backups_created: 'Sauvegardes creees : {count}',
284
285
  profile_renamed: 'i Profil `beginner` renomme en `creator` dans project.context.md pour mieux decrire l utilisateur. Comportement inchange. Modifiez le fichier pour passer a `developer` si vous le souhaitez.',
@@ -364,7 +365,7 @@ module.exports = {
364
365
  features_dir_present: 'Repertoire features present (.aioson/context/features/)',
365
366
  features_dir_present_hint: 'Creez .aioson/context/features/ pour heberger les dossiers par feature (doctor --fix le cree).',
366
367
  auto_handoff_declared: 'Flag autopilot handoff declare (auto_handoff dans project.context.md)',
367
- auto_handoff_declared_hint: 'Le protocole autopilot-handoff est installe mais auto_handoff n\'est pas defini dans le frontmatter de project.context.md l\'autopilot reste inactif. Definissez auto_handoff: true pour l\'activer, ou auto_handoff: false pour faire taire cet avertissement.',
368
+ auto_handoff_declared_hint: 'Le protocole autopilot-handoff est installe et auto_handoff n\'est pas defini @product demande le mode a l\'ecran au debut de chaque feature (Autopilot / Pas a pas / Toujours). Definissez auto_handoff: true pour toujours autopilot et sauter la question, ou false pour toujours pas a pas.',
368
369
  claude_commands_present: 'Slash commands de Claude presents ({missing} absents sur {required})',
369
370
  claude_commands_present_hint: 'Absents : {paths}. Executez `aioson doctor . --fix` pour les restaurer.',
370
371
  version_drift: 'Version du CLI conforme a project.context.md (contexte : {context}, CLI : {cli})',
@@ -379,6 +379,7 @@ module.exports = {
379
379
  update: {
380
380
  not_installed: 'Nenhuma instalacao do AIOSON encontrada em {targetDir}.',
381
381
  done_at: 'Atualizacao concluida em: {targetDir}',
382
+ template_version: 'Versao do template aplicada: {version}',
382
383
  files_updated: 'Arquivos atualizados: {count}',
383
384
  backups_created: 'Backups criados: {count}',
384
385
  profile_renamed: 'i Perfil `beginner` renomeado para `creator` em project.context.md para descrever melhor o usuario. Comportamento inalterado. Edite o arquivo para mudar para `developer` se preferir.',
@@ -465,7 +466,7 @@ module.exports = {
465
466
  features_dir_present: 'Diretorio de features presente (.aioson/context/features/)',
466
467
  features_dir_present_hint: 'Crie .aioson/context/features/ para hospedar dossies por feature (doctor --fix cria automaticamente).',
467
468
  auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff no project.context.md)',
468
- auto_handoff_declared_hint: 'O protocolo autopilot-handoff esta instalado mas auto_handoff nao esta definido no frontmatter do project.context.md o autopilot fica inativo. Defina auto_handoff: true para ativar, ou auto_handoff: false para silenciar este aviso.',
469
+ auto_handoff_declared_hint: 'O protocolo autopilot-handoff esta instalado e auto_handoff nao esta definido — o @product pergunta o modo na tela no inicio de cada feature (Autopilot / Passo a passo / Sempre). Defina auto_handoff: true para sempre autopilot e pular a pergunta, ou false para sempre passo a passo.',
469
470
  claude_commands_present: 'Slash commands do Claude presentes ({missing} ausentes de {required})',
470
471
  claude_commands_present_hint: 'Ausentes: {paths}. Rode `aioson doctor . --fix` para restaurar a partir do template.',
471
472
  version_drift: 'Versao do CLI bate com project.context.md (contexto: {context}, CLI: {cli})',
@@ -39,7 +39,10 @@ function summarizeSkippedChanges(feedback) {
39
39
  return [...sectionChanges, ...decisionChanges];
40
40
  }
41
41
 
42
- async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale = false } = {}) {
42
+ // Declining never writes to briefings.md, so a stale source hash is irrelevant
43
+ // to safety — validating it would only dead-end the user whose briefing moved
44
+ // on. Hence allowStale defaults to TRUE here (and only here).
45
+ async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale = true } = {}) {
43
46
  const briefingPath = resolveBriefingPath(projectDir, slug, 'briefings.md');
44
47
  const currentMarkdown = await fs.readFile(briefingPath, 'utf8');
45
48
  const currentSourceHash = hashText(currentMarkdown);
@@ -57,10 +60,12 @@ async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale =
57
60
  source_hash: feedback.source_hash,
58
61
  applied_hash: currentSourceHash,
59
62
  status: 'declined',
63
+ round: feedback.round || 1,
60
64
  applied_changes: [],
61
65
  skipped_changes: skippedChanges,
62
66
  unresolved_comments: unresolvedComments,
63
67
  blocking_items: feedback.blocking_items || [],
68
+ findings: feedback.findings || [],
64
69
  next_action: 'rerun_review'
65
70
  });
66
71
 
@@ -104,15 +109,22 @@ async function applyConfirmedFeedback(projectDir, slug, feedback, { confirmed =
104
109
  if (returnedToDraft) {
105
110
  returnApprovedBriefingToDraft(registry, slug);
106
111
  }
112
+ // A blocking finding the user left pending blocks the PRD the same way an
113
+ // explicit blocking item does — even if the export step never mirrored it
114
+ // into blocking_items (hand-written feedback).
115
+ const pendingBlockingFindings = (feedback.findings || []).filter(
116
+ (finding) => finding.blocking && finding.status === 'pending'
117
+ );
118
+ const hasBlockers = (feedback.blocking_items || []).length > 0 || pendingBlockingFindings.length > 0;
107
119
  markRefinementState(registry, slug, {
108
- refinement_status: feedback.blocking_items.length > 0 ? 'blocked' : 'applied',
120
+ refinement_status: hasBlockers ? 'blocked' : 'applied',
109
121
  review_html: `.aioson/briefings/${slug}/review.html`,
110
122
  refinement_report: `.aioson/briefings/${slug}/refinement-report.md`
111
123
  });
112
124
  await writeBriefingRegistry(projectDir, registry);
113
125
 
114
126
  const unresolvedComments = (feedback.comments || []).filter((comment) => !comment.resolved);
115
- const nextAction = feedback.blocking_items.length > 0 ? 'resolve_blockers' : 'approve_briefing';
127
+ const nextAction = hasBlockers ? 'resolve_blockers' : 'approve_briefing';
116
128
  const report = buildRefinementReport({
117
129
  briefing_slug: slug,
118
130
  source_briefing_path: `.aioson/briefings/${slug}/briefings.md`,
@@ -120,15 +132,17 @@ async function applyConfirmedFeedback(projectDir, slug, feedback, { confirmed =
120
132
  source_hash: feedback.source_hash,
121
133
  applied_hash: appliedHash,
122
134
  status: 'applied',
135
+ round: feedback.round || 1,
123
136
  applied_changes: appliedChanges,
124
137
  skipped_changes: [],
125
138
  unresolved_comments: unresolvedComments,
126
139
  blocking_items: feedback.blocking_items || [],
140
+ findings: feedback.findings || [],
127
141
  next_action: nextAction
128
142
  });
129
143
  await fs.writeFile(resolveBriefingPath(projectDir, slug, 'refinement-report.md'), report, 'utf8');
130
144
 
131
- return { ok: true, appliedChanges, nextAction, appliedHash, returnedToDraft };
145
+ return { ok: true, appliedChanges, nextAction, appliedHash, returnedToDraft, pendingBlockingFindings: pendingBlockingFindings.length };
132
146
  }
133
147
 
134
148
  module.exports = { applyConfirmedFeedback, applyDeclinedFeedback };
@@ -7,6 +7,12 @@ const VALID_SECTION_STATUSES = new Set(['unchanged', 'accepted', 'change_request
7
7
  const VALID_DECISION_STATUSES = new Set(['accepted', 'change_requested', 'remove', 'blocked', 'note']);
8
8
  const VALID_SEVERITIES = new Set(['note', 'suggestion', 'important', 'blocking']);
9
9
  const VALID_EXPORT_METHODS = new Set(['file-system-access', 'download', 'copy-paste', 'manual-save']);
10
+ const VALID_FINDING_CATEGORIES = new Set(['ambiguity', 'redundancy', 'gap', 'risk', 'pending-decision', 'scope-suggestion']);
11
+ const VALID_FINDING_SEVERITIES = new Set(['low', 'medium', 'high']);
12
+ const VALID_FINDING_STATUSES = new Set(['pending', 'accepted', 'rejected', 'deferred']);
13
+ // 1.0 = sections/comments/decisions/blocking_items only; 1.1 adds findings[] (the
14
+ // agent's audit surfaced as reviewable items) and the refinement round counter.
15
+ const SUPPORTED_SCHEMA_VERSIONS = new Set(['1.0', '1.1']);
10
16
 
11
17
  // Normalization only — NOT a security boundary. This strips well-formed tags
12
18
  // and normalizes newlines so the text round-trips cleanly into briefings.md.
@@ -34,17 +40,64 @@ function normalizeSection(section) {
34
40
  };
35
41
  }
36
42
 
37
- function buildInitialFeedback({ slug, sourcePath, sourceHash, sections, exportMethod = 'manual-save', now = new Date() }) {
43
+ function normalizeFinding(finding, index) {
44
+ return {
45
+ id: String(finding.id || `F${index + 1}`),
46
+ section_id: String(finding.section_id || ''),
47
+ category: VALID_FINDING_CATEGORIES.has(finding.category) ? finding.category : 'gap',
48
+ severity: VALID_FINDING_SEVERITIES.has(finding.severity) ? finding.severity : 'medium',
49
+ blocking: Boolean(finding.blocking),
50
+ text: sanitizeText(finding.text),
51
+ recommendation: sanitizeText(finding.recommendation || ''),
52
+ status: VALID_FINDING_STATUSES.has(finding.status) ? finding.status : 'pending',
53
+ note: sanitizeText(finding.note || '')
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Validate agent-supplied audit findings BEFORE they are baked into the review
59
+ * surface. Strict where normalizeFinding is lenient: the agent must name a real
60
+ * section and a valid category, and every finding must carry text.
61
+ */
62
+ function validateFindingsInput(findings, { sectionIds = [] } = {}) {
63
+ const errors = [];
64
+ if (!Array.isArray(findings)) {
65
+ return { ok: false, errors: ['findings must be an array'] };
66
+ }
67
+ const known = new Set(sectionIds);
68
+ findings.forEach((finding, index) => {
69
+ const label = finding && finding.id ? String(finding.id) : `#${index + 1}`;
70
+ if (!finding || typeof finding !== 'object') {
71
+ errors.push(`finding ${label} must be an object`);
72
+ return;
73
+ }
74
+ if (!String(finding.text || '').trim()) errors.push(`finding ${label} has no text`);
75
+ if (!VALID_FINDING_CATEGORIES.has(finding.category)) {
76
+ errors.push(`finding ${label} has invalid category: ${finding.category}`);
77
+ }
78
+ if (finding.severity !== undefined && !VALID_FINDING_SEVERITIES.has(finding.severity)) {
79
+ errors.push(`finding ${label} has invalid severity: ${finding.severity}`);
80
+ }
81
+ if (known.size > 0 && !known.has(String(finding.section_id || ''))) {
82
+ errors.push(`finding ${label} points at unknown section_id: ${finding.section_id}`);
83
+ }
84
+ });
85
+ return { ok: errors.length === 0, errors };
86
+ }
87
+
88
+ function buildInitialFeedback({ slug, sourcePath, sourceHash, sections, findings = [], round = 1, exportMethod = 'manual-save', now = new Date() }) {
38
89
  const timestamp = now.toISOString();
39
90
  return {
40
- schema_version: '1.0',
91
+ schema_version: '1.1',
41
92
  briefing_slug: slug,
42
93
  source_briefing_path: sourcePath,
43
94
  source_hash: sourceHash,
44
95
  review_generated_at: timestamp,
45
96
  last_modified_at: timestamp,
46
97
  export_method: exportMethod,
98
+ round: Number.isInteger(round) && round > 0 ? round : 1,
47
99
  sections: sections.map(normalizeSection),
100
+ findings: findings.map(normalizeFinding),
48
101
  comments: [],
49
102
  decisions: [],
50
103
  blocking_items: []
@@ -66,7 +119,7 @@ function validateFeedback(feedback, { slug, currentSourceHash, allowStale = fals
66
119
  if (!feedback || typeof feedback !== 'object') errors.push('feedback must be an object');
67
120
  if (errors.length > 0) return { ok: false, stale: false, errors, warnings };
68
121
 
69
- if (feedback.schema_version !== '1.0') errors.push('schema_version must be 1.0');
122
+ if (!SUPPORTED_SCHEMA_VERSIONS.has(feedback.schema_version)) errors.push('schema_version must be 1.0 or 1.1');
70
123
  if (slug && feedback.briefing_slug !== slug) errors.push('briefing_slug does not match selected slug');
71
124
  if (!isPathInsideBriefing(feedback.source_briefing_path, feedback.briefing_slug || slug)) {
72
125
  errors.push('source_briefing_path must stay inside the selected briefing directory');
@@ -99,6 +152,17 @@ function validateFeedback(feedback, { slug, currentSourceHash, allowStale = fals
99
152
  }
100
153
  }
101
154
 
155
+ // findings[] is a 1.1 field; a 1.0 payload legitimately has none.
156
+ if (feedback.schema_version === '1.1' && !Array.isArray(feedback.findings)) {
157
+ errors.push('findings must be an array');
158
+ }
159
+ if (Array.isArray(feedback.findings)) {
160
+ for (const finding of feedback.findings) {
161
+ if (!VALID_FINDING_STATUSES.has(finding.status)) errors.push(`invalid finding status: ${finding.status}`);
162
+ if (!VALID_FINDING_CATEGORIES.has(finding.category)) errors.push(`invalid finding category: ${finding.category}`);
163
+ }
164
+ }
165
+
102
166
  const stale = Boolean(currentSourceHash && feedback.source_hash !== currentSourceHash);
103
167
  if (stale) warnings.push('source_hash differs from current briefings.md');
104
168
  if (stale && !allowStale) errors.push('feedback is stale');
@@ -116,7 +180,12 @@ function assertFeedbackPath(projectDir, slug, candidatePath) {
116
180
 
117
181
  module.exports = {
118
182
  buildInitialFeedback,
183
+ normalizeFinding,
119
184
  sanitizeText,
120
185
  validateFeedback,
121
- assertFeedbackPath
186
+ validateFindingsInput,
187
+ assertFeedbackPath,
188
+ VALID_FINDING_CATEGORIES,
189
+ VALID_FINDING_SEVERITIES,
190
+ VALID_FINDING_STATUSES
122
191
  };
@@ -7,6 +7,15 @@ function list(items, formatter) {
7
7
 
8
8
  function buildRefinementReport(data) {
9
9
  const nextAction = data.next_action || 'rerun_review';
10
+ const findings = Array.isArray(data.findings) ? data.findings : [];
11
+ const findingsBlock = findings.length > 0
12
+ ? [
13
+ '## Findings',
14
+ '',
15
+ list(findings, (finding) => `${finding.id} [${finding.category}/${finding.severity}${finding.blocking ? '/blocking' : ''}] (${finding.status}) ${finding.section_id}: ${finding.text}`),
16
+ ''
17
+ ]
18
+ : [];
10
19
  return [
11
20
  `# Refinement Report — ${data.briefing_slug}`,
12
21
  '',
@@ -15,8 +24,10 @@ function buildRefinementReport(data) {
15
24
  `- Source hash: ${data.source_hash || '-'}`,
16
25
  `- Applied hash: ${data.applied_hash || '-'}`,
17
26
  `- Status: ${data.status || 'review_generated'}`,
27
+ `- Round: ${data.round || 1}`,
18
28
  `- Next action: ${nextAction}`,
19
29
  '',
30
+ ...findingsBlock,
20
31
  '## Applied Changes',
21
32
  '',
22
33
  list(data.applied_changes, (change) => `${change.section_id || change.title}: ${change.summary || 'updated'}`),