@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
@@ -19,14 +19,166 @@ function safeJson(value) {
19
19
  return JSON.stringify(value).replace(/</g, '\\u003c');
20
20
  }
21
21
 
22
+ const FINDING_CATEGORIES = ['ambiguity', 'redundancy', 'gap', 'risk', 'pending-decision', 'scope-suggestion'];
23
+
24
+ // UI strings for the generated surface. The surface is user-facing, so it follows
25
+ // the interaction language (locale from the CLI); schema enum values (statuses,
26
+ // categories) stay canonical English because they round-trip into the JSON.
27
+ const LABELS = {
28
+ en: {
29
+ title: 'Briefing Review',
30
+ round: 'round',
31
+ source: 'source',
32
+ notes: 'Notes',
33
+ note_placeholder: 'Plain text note for this section',
34
+ finding_note_placeholder: 'Decision / note for this finding',
35
+ findings_title: 'Audit findings',
36
+ other_findings: 'Findings without a matching section',
37
+ recommendation: 'Recommendation',
38
+ blocking_chip: 'blocking',
39
+ summary_title: 'Review State',
40
+ summary_changed: 'Changed sections',
41
+ summary_blocked: 'Blocking items',
42
+ summary_notes: 'Notes',
43
+ summary_pending_findings: 'Pending findings',
44
+ filters_title: 'Finding filters',
45
+ filter_all: 'all',
46
+ btn_download: 'Download JSON',
47
+ btn_copy: 'Copy JSON',
48
+ btn_save: 'Save to file',
49
+ btn_copy_path: 'Copy path',
50
+ target_hint: 'Save target:',
51
+ toolbar_hint: 'Save writes structured feedback only. HTML edits are not canonical until exported.',
52
+ hint_title: 'How to use',
53
+ hint_body: [
54
+ 'Open this file in a real browser (double-click it) — editor previews are sandboxed and block saving and downloads.',
55
+ 'Edit section text, set a status per section, decide each finding, add notes.',
56
+ 'Then return the JSON one of three ways: “Save to file” directly over {feedbackPath}; “Download JSON” and move it over {feedbackPath}; or “Copy JSON” and paste it in the chat when you reactivate @briefing-refiner.',
57
+ 'Your edits autosave to this browser (local draft) — closing the tab loses nothing.'
58
+ ],
59
+ blocks_title: 'What blocks the PRD',
60
+ blocks_body: 'Any section marked blocked, and any blocking finding still pending.',
61
+ // runtime strings
62
+ section_blocked: 'Section marked as blocked',
63
+ blocks_prd: 'Blocks PRD:',
64
+ autosaved: 'Draft autosaved locally at',
65
+ draft_restored: 'Local draft restored',
66
+ discard_draft: 'Discard draft',
67
+ downloaded: 'Downloaded refinement-feedback.json — move it over the existing file in the briefing folder.',
68
+ copied: 'JSON copied — paste it in the chat or into refinement-feedback.json.',
69
+ saved: 'Saved refinement-feedback.json.',
70
+ path_copied: 'Path copied — paste it into the save dialog’s "File name" field to jump straight to the folder.',
71
+ no_fsa: 'Direct save unavailable in this browser; downloaded JSON instead.',
72
+ sandbox_fallback: 'Direct save blocked here (sandboxed preview) — downloaded JSON instead. Open this file in a real browser for direct save.'
73
+ },
74
+ pt: {
75
+ title: 'Revisão do Briefing',
76
+ round: 'rodada',
77
+ source: 'fonte',
78
+ notes: 'Notas',
79
+ note_placeholder: 'Nota em texto puro para esta seção',
80
+ finding_note_placeholder: 'Decisão / nota para este achado',
81
+ findings_title: 'Achados da auditoria',
82
+ other_findings: 'Achados sem seção correspondente',
83
+ recommendation: 'Recomendação',
84
+ blocking_chip: 'bloqueante',
85
+ summary_title: 'Estado da revisão',
86
+ summary_changed: 'Seções alteradas',
87
+ summary_blocked: 'Itens bloqueantes',
88
+ summary_notes: 'Notas',
89
+ summary_pending_findings: 'Achados pendentes',
90
+ filters_title: 'Filtros de achados',
91
+ filter_all: 'todos',
92
+ btn_download: 'Baixar JSON',
93
+ btn_copy: 'Copiar JSON',
94
+ btn_save: 'Salvar no arquivo',
95
+ btn_copy_path: 'Copiar caminho',
96
+ target_hint: 'Destino:',
97
+ toolbar_hint: 'Salvar grava apenas o feedback estruturado. Edições no HTML não são canônicas até exportar.',
98
+ hint_title: 'Como usar',
99
+ hint_body: [
100
+ 'Abra este arquivo num navegador de verdade (duplo clique) — previews de editor são sandboxed e bloqueiam salvar e baixar.',
101
+ 'Edite o texto das seções, marque o status de cada uma, decida cada achado e anote comentários.',
102
+ 'Depois devolva o JSON de uma destas formas: “Salvar no arquivo” direto sobre {feedbackPath}; “Baixar JSON” e mover por cima de {feedbackPath}; ou “Copiar JSON” e colar no chat ao reativar o @briefing-refiner.',
103
+ 'Suas edições são salvas automaticamente neste navegador (rascunho local) — fechar a aba não perde nada.'
104
+ ],
105
+ blocks_title: 'O que bloqueia o PRD',
106
+ blocks_body: 'Qualquer seção marcada como blocked e qualquer achado bloqueante ainda pendente.',
107
+ // runtime strings
108
+ section_blocked: 'Seção marcada como bloqueada',
109
+ blocks_prd: 'Bloqueia o PRD:',
110
+ autosaved: 'Rascunho salvo localmente às',
111
+ draft_restored: 'Rascunho local restaurado',
112
+ discard_draft: 'Descartar rascunho',
113
+ downloaded: 'refinement-feedback.json baixado — mova por cima do arquivo existente na pasta do briefing.',
114
+ copied: 'JSON copiado — cole no chat ou dentro de refinement-feedback.json.',
115
+ saved: 'refinement-feedback.json salvo.',
116
+ path_copied: 'Caminho copiado — cole no campo "Nome do arquivo" da janela de salvar para ir direto à pasta.',
117
+ no_fsa: 'Salvamento direto indisponível neste navegador; o JSON foi baixado.',
118
+ sandbox_fallback: 'Salvamento direto bloqueado aqui (preview sandboxed) — o JSON foi baixado. Abra este arquivo num navegador de verdade para salvar direto.'
119
+ }
120
+ };
121
+
122
+ function resolveLabels(locale) {
123
+ return String(locale || '').toLowerCase().startsWith('pt') ? LABELS.pt : LABELS.en;
124
+ }
125
+
126
+ function renderFinding(finding, labels) {
127
+ const rec = finding.recommendation
128
+ ? `<div class="f-rec"><b>${escapeHtml(labels.recommendation)}:</b> ${escapeHtml(finding.recommendation)}</div>`
129
+ : '';
130
+ const blockChip = finding.blocking ? `<span class="chip chip-block">${escapeHtml(labels.blocking_chip)}</span>` : '';
131
+ return `
132
+ <div class="finding" data-finding="${escapeHtml(finding.id)}" data-cat="${escapeHtml(finding.category)}">
133
+ <div class="f-head">
134
+ <span class="chip chip-cat">${escapeHtml(finding.category)}</span>
135
+ <span class="chip chip-sev-${escapeHtml(finding.severity)}">${escapeHtml(finding.severity)}</span>
136
+ ${blockChip}
137
+ <span class="f-id">${escapeHtml(finding.id)}</span>
138
+ <select data-role="f-status">
139
+ <option value="pending"${finding.status === 'pending' ? ' selected' : ''}>pending</option>
140
+ <option value="accepted"${finding.status === 'accepted' ? ' selected' : ''}>accepted</option>
141
+ <option value="rejected"${finding.status === 'rejected' ? ' selected' : ''}>rejected</option>
142
+ <option value="deferred"${finding.status === 'deferred' ? ' selected' : ''}>deferred</option>
143
+ </select>
144
+ </div>
145
+ <div class="f-text">${escapeHtml(finding.text)}</div>
146
+ ${rec}
147
+ <input class="f-note" data-role="f-note" type="text" value="${escapeHtml(finding.note || '')}" placeholder="${escapeHtml(labels.finding_note_placeholder)}">
148
+ </div>`;
149
+ }
150
+
22
151
  function buildReviewHtml(data) {
23
152
  const feedback = data.feedback;
153
+ const labels = resolveLabels(data.locale);
24
154
  const sections = feedback.sections || [];
155
+ const findings = feedback.findings || [];
156
+ const feedbackPath = `.aioson/briefings/${feedback.briefing_slug}/refinement-feedback.json`;
157
+ const feedbackAbsPath = data.feedbackAbsPath || '';
158
+
159
+ const findingsBySection = new Map();
160
+ const orphanFindings = [];
161
+ const sectionIds = new Set(sections.map((section) => section.id));
162
+ for (const finding of findings) {
163
+ if (sectionIds.has(finding.section_id)) {
164
+ if (!findingsBySection.has(finding.section_id)) findingsBySection.set(finding.section_id, []);
165
+ findingsBySection.get(finding.section_id).push(finding);
166
+ } else {
167
+ orphanFindings.push(finding);
168
+ }
169
+ }
170
+
25
171
  const nav = sections.map((section) => `<a href="#${escapeHtml(section.id)}">${escapeHtml(section.title)}</a>`).join('');
26
- const sectionMarkup = sections.map((section) => `
172
+ const sectionMarkup = sections.map((section) => {
173
+ const own = findingsBySection.get(section.id) || [];
174
+ const findingsBlock = own.length > 0
175
+ ? `<div class="findings"><div class="findings-title">${escapeHtml(labels.findings_title)} (${own.length})</div>${own.map((f) => renderFinding(f, labels)).join('\n')}</div>`
176
+ : '';
177
+ return `
27
178
  <section class="section" id="${escapeHtml(section.id)}" data-section="${escapeHtml(section.id)}">
28
179
  <div class="section-head">
29
180
  <h2>${escapeHtml(section.title)}</h2>
181
+ <span class="chip chip-state" data-role="state-chip"></span>
30
182
  <select data-role="status">
31
183
  <option value="unchanged">unchanged</option>
32
184
  <option value="accepted">accepted</option>
@@ -36,16 +188,49 @@ function buildReviewHtml(data) {
36
188
  </select>
37
189
  </div>
38
190
  <div class="editor" contenteditable="plaintext-only" spellcheck="true">${escapeHtml(section.current_text || '')}</div>
39
- <label class="note-label">Notes</label>
40
- <textarea class="note" data-role="note" rows="3" placeholder="Plain text note for this section"></textarea>
41
- </section>`).join('\n');
191
+ <label class="note-label">${escapeHtml(labels.notes)}</label>
192
+ <textarea class="note" data-role="note" rows="3" placeholder="${escapeHtml(labels.note_placeholder)}"></textarea>
193
+ ${findingsBlock}
194
+ </section>`;
195
+ }).join('\n');
196
+
197
+ const orphanMarkup = orphanFindings.length > 0
198
+ ? `
199
+ <section class="section" data-section="">
200
+ <div class="section-head"><h2>${escapeHtml(labels.other_findings)}</h2></div>
201
+ <div class="findings">${orphanFindings.map((f) => renderFinding(f, labels)).join('\n')}</div>
202
+ </section>`
203
+ : '';
204
+
205
+ const filterButtons = [`<button type="button" data-cat="all" class="active">${escapeHtml(labels.filter_all)}</button>`]
206
+ .concat(FINDING_CATEGORIES.map((cat) => `<button type="button" data-cat="${cat}">${cat}</button>`))
207
+ .join('');
208
+
209
+ const hintBody = labels.hint_body
210
+ .map((line) => escapeHtml(line).split(escapeHtml('{feedbackPath}')).join(`<code>${escapeHtml(feedbackPath)}</code>`))
211
+ .map((line) => `<p>${line}</p>`)
212
+ .join('');
213
+
214
+ const runtimeLabels = {
215
+ section_blocked: labels.section_blocked,
216
+ blocks_prd: labels.blocks_prd,
217
+ autosaved: labels.autosaved,
218
+ draft_restored: labels.draft_restored,
219
+ downloaded: labels.downloaded,
220
+ copied: labels.copied,
221
+ saved: labels.saved,
222
+ path_copied: labels.path_copied,
223
+ no_fsa: labels.no_fsa,
224
+ sandbox_fallback: labels.sandbox_fallback
225
+ };
42
226
 
43
227
  return `<!doctype html>
44
- <html lang="en">
228
+ <!-- aioson:review schema=${escapeHtml(feedback.schema_version)} slug=${escapeHtml(feedback.briefing_slug)} source_hash=${escapeHtml(feedback.source_hash)} -->
229
+ <html lang="${labels === LABELS.pt ? 'pt-BR' : 'en'}">
45
230
  <head>
46
231
  <meta charset="utf-8">
47
232
  <meta name="viewport" content="width=device-width, initial-scale=1">
48
- <title>Briefing Review - ${escapeHtml(feedback.briefing_slug)}</title>
233
+ <title>${escapeHtml(labels.title)} - ${escapeHtml(feedback.briefing_slug)}</title>
49
234
  <style>
50
235
  :root { color-scheme: light; --bg: #f6f7f9; --panel: #ffffff; --ink: #172026; --muted: #5c6975; --line: #d8dee6; --accent: #0b6f85; --block: #b42318; --change: #8a5a00; --ok: #1b7f3a; }
51
236
  * { box-sizing: border-box; }
@@ -54,111 +239,205 @@ function buildReviewHtml(data) {
54
239
  h1 { margin: 0 0 4px; font-size: 20px; }
55
240
  h2 { margin: 0; font-size: 16px; }
56
241
  .meta { color: var(--muted); font-size: 12px; }
242
+ .banner { margin-top: 8px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px; background: #fff8e6; font-size: 12px; display: flex; gap: 10px; align-items: center; }
243
+ .banner button { min-height: 24px; padding: 2px 8px; font-size: 12px; }
57
244
  .layout { display: grid; grid-template-columns: 220px minmax(0, 1fr) 300px; gap: 16px; padding: 16px; }
58
245
  nav, aside { position: sticky; top: 82px; align-self: start; max-height: calc(100vh - 100px); overflow: auto; }
59
246
  nav a { display: block; padding: 8px 10px; color: var(--ink); text-decoration: none; border-left: 3px solid transparent; }
60
247
  nav a:hover { border-left-color: var(--accent); background: #eaf3f6; }
61
248
  .toolbar, nav, aside, .section { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; }
62
249
  .toolbar { padding: 10px; margin-bottom: 12px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
63
- button, select { min-height: 32px; border: 1px solid var(--line); background: #fff; border-radius: 4px; padding: 5px 8px; color: var(--ink); }
250
+ .toolbar .target { flex-basis: 100%; display: flex; gap: 8px; align-items: center; min-width: 0; }
251
+ .toolbar .target code { font-size: 12px; background: var(--bg); border: 1px solid var(--line); border-radius: 4px; padding: 3px 6px; overflow-x: auto; white-space: nowrap; }
252
+ .toolbar .target button { min-height: 26px; padding: 2px 8px; font-size: 12px; flex-shrink: 0; }
253
+ button, select, input.f-note { border: 1px solid var(--line); background: #fff; border-radius: 4px; padding: 5px 8px; color: var(--ink); font: inherit; }
254
+ button, select { min-height: 32px; }
255
+ button { cursor: pointer; }
64
256
  button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
65
257
  main { min-width: 0; }
66
258
  .section { margin-bottom: 12px; overflow: hidden; }
67
- .section-head { display: flex; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--line); background: #fbfcfd; }
259
+ .section-head { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--line); background: #fbfcfd; }
260
+ .section-head h2 { flex: 1; }
261
+ .section.is-blocked .section-head { background: #fdf0ee; }
68
262
  .editor { min-height: 96px; padding: 12px; white-space: pre-wrap; outline: none; }
69
263
  .editor:focus { box-shadow: inset 0 0 0 2px var(--accent); }
70
264
  .note-label { display: block; padding: 0 12px 4px; color: var(--muted); font-size: 12px; }
71
265
  .note { width: calc(100% - 24px); margin: 0 12px 12px; resize: vertical; border: 1px solid var(--line); border-radius: 4px; padding: 8px; font: inherit; }
266
+ .findings { border-top: 1px dashed var(--line); padding: 10px 12px 12px; }
267
+ .findings-title { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
268
+ .finding { border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; background: #fbfcfd; }
269
+ .finding[hidden] { display: none; }
270
+ .f-head { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; flex-wrap: wrap; }
271
+ .f-head select { margin-left: auto; min-height: 26px; padding: 2px 6px; font-size: 12px; }
272
+ .f-id { color: var(--muted); font-size: 11px; }
273
+ .f-text { margin-bottom: 6px; }
274
+ .f-rec { color: var(--muted); font-size: 13px; margin-bottom: 6px; }
275
+ .f-note { width: 100%; font-size: 13px; }
276
+ .chip { display: inline-block; border-radius: 999px; padding: 1px 8px; font-size: 11px; border: 1px solid var(--line); background: #fff; }
277
+ .chip-cat { border-color: var(--accent); color: var(--accent); }
278
+ .chip-sev-high { border-color: var(--block); color: var(--block); }
279
+ .chip-sev-medium { border-color: var(--change); color: var(--change); }
280
+ .chip-sev-low { color: var(--muted); }
281
+ .chip-block { background: var(--block); border-color: var(--block); color: #fff; }
282
+ .chip-state { display: none; font-size: 11px; }
283
+ .section.is-changed .chip-state { display: inline-block; border-color: var(--change); color: var(--change); }
284
+ .section.is-blocked .chip-state { display: inline-block; border-color: var(--block); color: var(--block); }
72
285
  aside { padding: 12px; }
73
286
  .summary-item { display: flex; justify-content: space-between; border-bottom: 1px solid var(--line); padding: 8px 0; }
74
- .filters label { display: block; margin: 6px 0; color: var(--muted); }
287
+ .filters { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
288
+ .filters button { border-radius: 999px; font-size: 12px; min-height: 26px; padding: 2px 10px; }
289
+ .filters button.active { background: var(--accent); border-color: var(--accent); color: #fff; }
75
290
  .danger { color: var(--block); font-weight: 600; }
76
- .status { margin-top: 10px; color: var(--muted); min-height: 20px; }
291
+ .status { margin-top: 10px; color: var(--muted); min-height: 20px; font-size: 12px; }
292
+ .autosave { color: var(--muted); font-size: 11px; min-height: 16px; }
293
+ .hint { margin-top: 12px; color: var(--muted); font-size: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
294
+ .hint p { margin: 0 0 6px; }
295
+ #blockers { white-space: pre-wrap; color: var(--block); font-size: 12px; margin-top: 8px; }
77
296
  @media (max-width: 900px) { .layout { grid-template-columns: 1fr; } nav, aside { position: static; max-height: none; } }
78
297
  </style>
79
298
  </head>
80
299
  <body>
81
300
  <header>
82
- <h1>Briefing Review: ${escapeHtml(feedback.briefing_slug)}</h1>
83
- <div class="meta">${escapeHtml(feedback.source_briefing_path)} | source hash ${escapeHtml(feedback.source_hash)}</div>
301
+ <h1>${escapeHtml(labels.title)}: ${escapeHtml(feedback.briefing_slug)}</h1>
302
+ <div class="meta">${escapeHtml(labels.source)}: ${escapeHtml(feedback.source_briefing_path)} | source hash ${escapeHtml(feedback.source_hash)} | ${escapeHtml(labels.round)} ${escapeHtml(String(feedback.round || 1))}</div>
303
+ <div class="banner" id="restore-banner" hidden><span></span><button type="button" id="discard-draft">${escapeHtml(labels.discard_draft)}</button></div>
84
304
  </header>
85
305
  <div class="layout">
86
306
  <nav aria-label="Sections">${nav}</nav>
87
307
  <main>
88
308
  <div class="toolbar">
89
- <button class="primary" id="download">Download JSON</button>
90
- <button id="copy">Copy JSON</button>
91
- <button id="save">Save JSON</button>
92
- <span class="meta">Save writes structured feedback only. HTML edits are not canonical until exported.</span>
309
+ <button type="button" class="primary" id="download">${escapeHtml(labels.btn_download)}</button>
310
+ <button type="button" id="copy">${escapeHtml(labels.btn_copy)}</button>
311
+ <button type="button" id="save">${escapeHtml(labels.btn_save)}</button>
312
+ <span class="meta">${escapeHtml(labels.toolbar_hint)}</span>${feedbackAbsPath ? `
313
+ <div class="target"><span class="meta">${escapeHtml(labels.target_hint)}</span> <code id="target-path">${escapeHtml(feedbackAbsPath)}</code> <button type="button" id="copy-path">${escapeHtml(labels.btn_copy_path)}</button></div>` : ''}
93
314
  </div>
94
315
  ${sectionMarkup}
316
+ ${orphanMarkup}
95
317
  </main>
96
318
  <aside>
97
- <h2>Review State</h2>
98
- <div class="summary-item"><span>Changed</span><strong id="changed">0</strong></div>
99
- <div class="summary-item"><span>Blocked</span><strong id="blocked" class="danger">0</strong></div>
100
- <div class="summary-item"><span>Notes</span><strong id="notes">0</strong></div>
101
- <h2 style="margin-top:16px">Filters</h2>
102
- <div class="filters">
103
- <label><input type="checkbox" value="ambiguity"> ambiguity</label>
104
- <label><input type="checkbox" value="redundancy"> redundancy</label>
105
- <label><input type="checkbox" value="gap"> gap</label>
106
- <label><input type="checkbox" value="risk"> risk</label>
107
- <label><input type="checkbox" value="pending-decision"> pending decision</label>
108
- <label><input type="checkbox" value="scope"> scope suggestion</label>
109
- </div>
319
+ <h2>${escapeHtml(labels.summary_title)}</h2>
320
+ <div class="summary-item"><span>${escapeHtml(labels.summary_changed)}</span><strong id="changed">0</strong></div>
321
+ <div class="summary-item"><span>${escapeHtml(labels.summary_blocked)}</span><strong id="blocked" class="danger">0</strong></div>
322
+ <div class="summary-item"><span>${escapeHtml(labels.summary_notes)}</span><strong id="notes">0</strong></div>
323
+ <div class="summary-item"><span>${escapeHtml(labels.summary_pending_findings)}</span><strong id="pending-findings">0</strong></div>
324
+ <div id="blockers"></div>
325
+ <h2 style="margin-top:16px">${escapeHtml(labels.filters_title)}</h2>
326
+ <div class="filters" id="filters">${filterButtons}</div>
110
327
  <div class="status" id="status"></div>
328
+ <div class="autosave" id="autosave"></div>
329
+ <div class="hint"><b>${escapeHtml(labels.hint_title)}:</b>${hintBody}<b>${escapeHtml(labels.blocks_title)}:</b> ${escapeHtml(labels.blocks_body)}</div>
111
330
  </aside>
112
331
  </div>
113
332
  <script>
114
333
  const feedback = ${safeJson(feedback)};
115
- const byId = new Map(feedback.sections.map(section => [section.id, section]));
334
+ const L = ${safeJson(runtimeLabels)};
335
+ const LS_KEY = 'aioson-review:' + feedback.briefing_slug + ':' + feedback.source_hash;
336
+ const sectionById = new Map(feedback.sections.map(section => [section.id, section]));
337
+ const findingById = new Map((feedback.findings || []).map(finding => [finding.id, finding]));
116
338
  const statusEl = document.getElementById('status');
339
+ const autosaveEl = document.getElementById('autosave');
117
340
 
118
341
  function plainText(node) {
119
- return (node.innerText || node.textContent || '').replace(/\\r\\n/g, '\\n');
342
+ return (node.innerText || node.textContent || '').replace(/\\r\\n?/g, '\\n');
120
343
  }
121
344
 
122
- function touch() {
345
+ function setText(id, value) { document.getElementById(id).textContent = String(value); }
346
+
347
+ function collect() {
123
348
  feedback.last_modified_at = new Date().toISOString();
124
349
  let changed = 0;
125
- let blocked = 0;
126
350
  let notes = 0;
127
- document.querySelectorAll('.section').forEach(sectionEl => {
128
- const id = sectionEl.dataset.section;
129
- const section = byId.get(id);
130
- const text = plainText(sectionEl.querySelector('.editor'));
131
- const status = sectionEl.querySelector('[data-role="status"]').value;
351
+ document.querySelectorAll('.section[data-section]').forEach(sectionEl => {
352
+ const section = sectionById.get(sectionEl.dataset.section);
353
+ if (!section) return;
354
+ section.current_text = plainText(sectionEl.querySelector('.editor'));
355
+ section.status = sectionEl.querySelector('[data-role="status"]').value;
132
356
  const note = sectionEl.querySelector('[data-role="note"]').value.trim();
133
- section.current_text = text;
134
- section.status = status;
135
357
  section.comments_count = note ? 1 : 0;
136
- if (text !== section.original_text || status !== 'unchanged') changed += 1;
137
- if (status === 'blocked') blocked += 1;
358
+ const isChanged = section.current_text !== section.original_text || section.status !== 'unchanged';
359
+ if (isChanged) changed += 1;
138
360
  if (note) notes += 1;
361
+ sectionEl.classList.toggle('is-changed', isChanged);
362
+ sectionEl.classList.toggle('is-blocked', section.status === 'blocked');
363
+ const chip = sectionEl.querySelector('[data-role="state-chip"]');
364
+ if (chip) chip.textContent = section.status === 'blocked' ? 'blocked' : (isChanged ? section.status : '');
365
+ });
366
+ document.querySelectorAll('.finding').forEach(findingEl => {
367
+ const finding = findingById.get(findingEl.dataset.finding);
368
+ if (!finding) return;
369
+ finding.status = findingEl.querySelector('[data-role="f-status"]').value;
370
+ finding.note = findingEl.querySelector('[data-role="f-note"]').value.trim();
139
371
  });
140
372
  feedback.comments = [];
141
373
  feedback.blocking_items = [];
142
- document.querySelectorAll('.section').forEach(sectionEl => {
143
- const id = sectionEl.dataset.section;
374
+ document.querySelectorAll('.section[data-section]').forEach(sectionEl => {
375
+ const section = sectionById.get(sectionEl.dataset.section);
376
+ if (!section) return;
144
377
  const note = sectionEl.querySelector('[data-role="note"]').value.trim();
145
- const status = sectionEl.querySelector('[data-role="status"]').value;
146
378
  if (note) {
147
- const severity = status === 'blocked' ? 'blocking' : 'note';
148
- feedback.comments.push({ id: 'comment-' + id, section_id: id, target_text_hash: null, note, severity, resolved: false });
379
+ const severity = section.status === 'blocked' ? 'blocking' : 'note';
380
+ feedback.comments.push({ id: 'comment-' + section.id, section_id: section.id, target_text_hash: null, note, severity, resolved: false });
381
+ }
382
+ if (section.status === 'blocked') {
383
+ feedback.blocking_items.push({ id: 'block-' + section.id, section_id: section.id, note: note || L.section_blocked, resolved: false });
149
384
  }
150
- if (status === 'blocked') {
151
- feedback.blocking_items.push({ id: 'block-' + id, section_id: id, note: note || 'Section marked as blocked', resolved: false });
385
+ });
386
+ (feedback.findings || []).forEach(finding => {
387
+ if (finding.blocking && finding.status === 'pending') {
388
+ feedback.blocking_items.push({ id: 'block-' + finding.id, section_id: finding.section_id, note: finding.text, resolved: false });
152
389
  }
153
390
  });
154
- document.getElementById('changed').textContent = String(changed);
155
- document.getElementById('blocked').textContent = String(blocked);
156
- document.getElementById('notes').textContent = String(notes);
391
+ const pendingFindings = (feedback.findings || []).filter(finding => finding.status === 'pending').length;
392
+ setText('changed', changed);
393
+ setText('blocked', feedback.blocking_items.length);
394
+ setText('notes', notes);
395
+ setText('pending-findings', pendingFindings);
396
+ const blockersEl = document.getElementById('blockers');
397
+ blockersEl.textContent = feedback.blocking_items.length
398
+ ? L.blocks_prd + '\\n' + feedback.blocking_items.map(item => '- ' + item.note).join('\\n')
399
+ : '';
400
+ }
401
+
402
+ function autosave() {
403
+ try {
404
+ localStorage.setItem(LS_KEY, JSON.stringify(feedback));
405
+ autosaveEl.textContent = L.autosaved + ' ' + new Date().toTimeString().slice(0, 5);
406
+ } catch (error) { /* storage unavailable (some sandboxes) — export still works */ }
407
+ }
408
+
409
+ function touch() { collect(); autosave(); }
410
+
411
+ function restoreDraft() {
412
+ let raw = null;
413
+ try { raw = localStorage.getItem(LS_KEY); } catch (error) { return; }
414
+ if (!raw) return;
415
+ let draft;
416
+ try { draft = JSON.parse(raw); } catch (error) { return; }
417
+ if (!draft || draft.source_hash !== feedback.source_hash) return;
418
+ (draft.sections || []).forEach(ds => {
419
+ const sectionEl = document.querySelector('.section[data-section="' + ds.id + '"]');
420
+ if (!sectionEl) return;
421
+ if (typeof ds.current_text === 'string') sectionEl.querySelector('.editor').innerText = ds.current_text;
422
+ sectionEl.querySelector('[data-role="status"]').value = ds.status || 'unchanged';
423
+ const draftNote = (draft.comments || []).find(comment => comment.section_id === ds.id);
424
+ sectionEl.querySelector('[data-role="note"]').value = draftNote ? draftNote.note : '';
425
+ });
426
+ (draft.findings || []).forEach(df => {
427
+ const findingEl = document.querySelector('.finding[data-finding="' + df.id + '"]');
428
+ if (!findingEl) return;
429
+ findingEl.querySelector('[data-role="f-status"]').value = df.status || 'pending';
430
+ findingEl.querySelector('[data-role="f-note"]').value = df.note || '';
431
+ });
432
+ const banner = document.getElementById('restore-banner');
433
+ banner.hidden = false;
434
+ banner.querySelector('span').textContent = L.draft_restored
435
+ + (draft.last_modified_at ? ' (' + draft.last_modified_at.slice(0, 16).replace('T', ' ') + ')' : '');
157
436
  }
158
437
 
159
438
  function jsonText(method) {
160
439
  feedback.export_method = method;
161
- touch();
440
+ collect();
162
441
  return JSON.stringify(feedback, null, 2);
163
442
  }
164
443
 
@@ -169,54 +448,95 @@ function buildReviewHtml(data) {
169
448
  link.download = 'refinement-feedback.json';
170
449
  link.click();
171
450
  URL.revokeObjectURL(link.href);
172
- statusEl.textContent = 'Downloaded refinement-feedback.json';
451
+ statusEl.textContent = L.downloaded;
452
+ autosave();
173
453
  }
174
454
 
175
455
  async function copyJson() {
176
456
  await navigator.clipboard.writeText(jsonText('copy-paste'));
177
- statusEl.textContent = 'Copied JSON to clipboard';
457
+ statusEl.textContent = L.copied;
458
+ autosave();
178
459
  }
179
460
 
180
- async function saveJson() {
461
+ let fileHandle = null;
462
+ async function saveDirect() {
181
463
  if (!window.showSaveFilePicker) {
182
464
  download();
183
- statusEl.textContent = 'Direct save unavailable; downloaded JSON instead';
465
+ statusEl.textContent = L.no_fsa;
184
466
  return;
185
467
  }
186
- const handle = await window.showSaveFilePicker({ suggestedName: 'refinement-feedback.json', types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }] });
187
- const writable = await handle.createWritable();
188
- await writable.write(jsonText('file-system-access'));
189
- await writable.close();
190
- statusEl.textContent = 'Saved refinement-feedback.json';
468
+ try {
469
+ if (!fileHandle) {
470
+ // id: Chromium remembers the last-used directory per picker id, so after the
471
+ // first save the dialog reopens in the briefing folder on later rounds.
472
+ fileHandle = await window.showSaveFilePicker({ id: 'aioson-refinement-feedback', suggestedName: 'refinement-feedback.json', types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }] });
473
+ }
474
+ const writable = await fileHandle.createWritable();
475
+ await writable.write(jsonText('file-system-access'));
476
+ await writable.close();
477
+ statusEl.textContent = L.saved;
478
+ autosave();
479
+ } catch (error) {
480
+ if (error && error.name === 'AbortError') return; // user cancelled the picker
481
+ // SecurityError/NotAllowedError: sandboxed preview (or revoked handle) —
482
+ // degrade to download instead of dead-ending with a raw error message.
483
+ fileHandle = null;
484
+ download();
485
+ statusEl.textContent = L.sandbox_fallback;
486
+ }
191
487
  }
192
488
 
193
489
  document.addEventListener('input', touch);
194
490
  document.addEventListener('change', touch);
195
491
  document.getElementById('download').addEventListener('click', download);
196
492
  document.getElementById('copy').addEventListener('click', () => copyJson().catch(() => download()));
197
- document.getElementById('save').addEventListener('click', () => saveJson().catch(error => { statusEl.textContent = error.message; }));
493
+ document.getElementById('save').addEventListener('click', saveDirect);
494
+ const copyPathBtn = document.getElementById('copy-path');
495
+ if (copyPathBtn) copyPathBtn.addEventListener('click', async () => {
496
+ try {
497
+ await navigator.clipboard.writeText(document.getElementById('target-path').textContent);
498
+ statusEl.textContent = L.path_copied;
499
+ } catch (error) { /* clipboard unavailable (sandboxed preview) — path stays visible for manual copy */ }
500
+ });
501
+ document.getElementById('discard-draft').addEventListener('click', () => {
502
+ try { localStorage.removeItem(LS_KEY); } catch (error) { /* ignore */ }
503
+ location.reload();
504
+ });
505
+ document.getElementById('filters').addEventListener('click', (event) => {
506
+ const btn = event.target.closest('button[data-cat]');
507
+ if (!btn) return;
508
+ document.querySelectorAll('#filters button').forEach(other => other.classList.remove('active'));
509
+ btn.classList.add('active');
510
+ const cat = btn.dataset.cat;
511
+ document.querySelectorAll('.finding').forEach(findingEl => {
512
+ findingEl.hidden = !(cat === 'all' || findingEl.dataset.cat === cat);
513
+ });
514
+ });
515
+ restoreDraft();
198
516
  touch();
199
517
  </script>
200
518
  </body>
201
519
  </html>`;
202
520
  }
203
521
 
204
- async function writeReviewArtifacts(projectDir, { slug, sourceMarkdown, sections, sourceHash }) {
522
+ async function writeReviewArtifacts(projectDir, { slug, sourceMarkdown, sections, sourceHash, findings = [], round = 1, locale = 'en' }) {
205
523
  // Validates slug as a safe segment and asserts containment before any write.
206
524
  const briefingDir = resolveBriefingPath(projectDir, slug);
207
525
  const sourcePath = `.aioson/briefings/${slug}/briefings.md`;
208
- const feedback = buildInitialFeedback({ slug, sourcePath, sourceHash, sections });
209
- const html = buildReviewHtml({ feedback, sourceMarkdown });
526
+ const feedback = buildInitialFeedback({ slug, sourcePath, sourceHash, sections, findings, round });
527
+ const html = buildReviewHtml({ feedback, sourceMarkdown, locale, feedbackAbsPath: path.join(briefingDir, 'refinement-feedback.json') });
210
528
  const report = buildRefinementReport({
211
529
  briefing_slug: slug,
212
530
  source_briefing_path: sourcePath,
213
531
  source_hash: sourceHash,
214
532
  status: 'review_generated',
215
- next_action: 'rerun_review',
533
+ next_action: 'collect_feedback',
534
+ round: feedback.round,
216
535
  applied_changes: [],
217
536
  skipped_changes: [],
218
537
  unresolved_comments: [],
219
- blocking_items: []
538
+ blocking_items: [],
539
+ findings: feedback.findings
220
540
  });
221
541
 
222
542
  await fs.mkdir(briefingDir, { recursive: true });
package/src/parser.js CHANGED
@@ -30,6 +30,9 @@ function parseArgv(argv) {
30
30
  'help', 'version', 'no-launch', 'attach', 'tmux',
31
31
  'allow-warnings', 'install-hook', 'uninstall-hook', 'remove-hook',
32
32
  'agent-safe', 'agentic',
33
+ // workflow:execute — pure booleans; `--seed .` / `--step .` must not
34
+ // swallow the path positional as their value.
35
+ 'seed', 'seed-only', 'step',
33
36
  'selective',
34
37
  'status', 'suggest', 'apply',
35
38
  'runtime-only', 'template-only', 'inception', 'locales',
@@ -44,6 +47,9 @@ function parseArgv(argv) {
44
47
  // verify:artifact — pure booleans; `--advisory .` / `--no-build .` must
45
48
  // not swallow the path positional.
46
49
  'advisory', 'no-build',
50
+ // briefing:apply-feedback — pure booleans; `--confirm .` / `--declined .`
51
+ // / `--allow-stale .` must not swallow the path positional.
52
+ 'confirm', 'declined', 'allow-stale',
47
53
  // `--resume` alone means "resume last"; `--resume=<id>` carries a value
48
54
  // and is handled by the `=` branch above. Without this entry, `--resume`
49
55
  // followed by `--tool=claude` would swallow the next token as its value.