@jaimevalasek/aioson 1.30.2 → 1.36.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 (158) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +19 -6
  3. package/docs/en/1-understand/ecosystem-map.md +45 -29
  4. package/docs/en/1-understand/glossary.md +5 -5
  5. package/docs/en/1-understand/what-is-aioson.md +5 -5
  6. package/docs/en/2-start/existing-project.md +7 -7
  7. package/docs/en/2-start/first-project.md +32 -38
  8. package/docs/en/2-start/initial-decisions.md +18 -17
  9. package/docs/en/3-recipes/README.md +2 -2
  10. package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
  11. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +5 -2
  12. package/docs/en/3-recipes/full-feature-with-sheldon.md +327 -338
  13. package/docs/en/4-agents/README.md +28 -14
  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/cli-reference.md +51 -48
  17. package/docs/en/5-reference/executable-verification.md +10 -7
  18. package/docs/en/5-reference/parallel.md +2 -0
  19. package/docs/en/5-reference/qa-browser.md +2 -2
  20. package/docs/en/README.md +3 -3
  21. package/docs/pt/1-entender/glossario.md +4 -4
  22. package/docs/pt/1-entender/mapa-do-ecossistema.md +32 -21
  23. package/docs/pt/2-comecar/decisoes-iniciais.md +16 -16
  24. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  25. package/docs/pt/3-receitas/README.md +1 -1
  26. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  27. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +289 -338
  28. package/docs/pt/4-agentes/analyst.md +9 -5
  29. package/docs/pt/4-agentes/architect.md +9 -5
  30. package/docs/pt/4-agentes/dev.md +19 -7
  31. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  32. package/docs/pt/4-agentes/orchestrator.md +164 -118
  33. package/docs/pt/4-agentes/pentester.md +5 -5
  34. package/docs/pt/4-agentes/pm.md +15 -7
  35. package/docs/pt/4-agentes/sheldon.md +166 -99
  36. package/docs/pt/4-agentes/ux-ui.md +7 -4
  37. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  38. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  39. package/docs/pt/5-referencia/harness-retro.md +2 -1
  40. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  41. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  42. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  43. package/docs/pt/README.md +4 -4
  44. package/docs/pt/agentes.md +48 -50
  45. package/package.json +2 -2
  46. package/src/artifact-kinds.js +110 -0
  47. package/src/cli.js +82 -42
  48. package/src/commands/agent-epilogue.js +251 -186
  49. package/src/commands/agents.js +104 -48
  50. package/src/commands/audit-code.js +344 -0
  51. package/src/commands/classify.js +75 -13
  52. package/src/commands/feature-close.js +43 -18
  53. package/src/commands/harness-check.js +259 -175
  54. package/src/commands/harness-retro-promote.js +387 -0
  55. package/src/commands/live.js +24 -0
  56. package/src/commands/prototype-check.js +163 -0
  57. package/src/commands/review-feature.js +189 -0
  58. package/src/commands/runtime.js +81 -66
  59. package/src/commands/sync-agents-copy.js +115 -0
  60. package/src/commands/verification-plan.js +116 -0
  61. package/src/commands/verify-artifact.js +530 -0
  62. package/src/commands/verify-implementation.js +428 -0
  63. package/src/commands/workflow-execute.js +309 -309
  64. package/src/commands/workflow-next.js +361 -19
  65. package/src/commands/workflow-plan.js +5 -5
  66. package/src/constants.js +4 -0
  67. package/src/gateway-pointer-merge.js +7 -1
  68. package/src/handoff-contract.js +267 -172
  69. package/src/harness/contract-integrity-gate.js +172 -0
  70. package/src/harness/contract-integrity.js +111 -0
  71. package/src/harness/contract-schema.js +377 -332
  72. package/src/harness/detect-runtime-feature.js +90 -0
  73. package/src/harness/static-criteria.js +192 -0
  74. package/src/i18n/messages/en.js +8 -4
  75. package/src/i18n/messages/es.js +8 -4
  76. package/src/i18n/messages/fr.js +8 -4
  77. package/src/i18n/messages/pt-BR.js +8 -4
  78. package/src/install-wizard.js +8 -8
  79. package/src/installer.js +13 -6
  80. package/src/lib/retro/retro-render.js +10 -1
  81. package/src/lib/retro/retro-sources.js +45 -27
  82. package/src/lib/retro/verification-reports.js +230 -0
  83. package/src/parser.js +6 -0
  84. package/src/preflight-engine.js +12 -12
  85. package/src/runtime-store.js +13 -9
  86. package/src/verification/evidence-bundle.js +251 -0
  87. package/src/verification/ledger-store.js +221 -0
  88. package/src/verification/path-policy.js +74 -0
  89. package/src/verification/policy-engine.js +95 -0
  90. package/src/verification/prompt-package.js +314 -0
  91. package/src/verification/redaction.js +77 -0
  92. package/src/verification/report-parser.js +132 -0
  93. package/src/verification/report-store.js +97 -0
  94. package/src/verification/result.js +16 -0
  95. package/src/verification/runners/index.js +319 -0
  96. package/src/verification/runtime-telemetry.js +144 -0
  97. package/src/verification/schema.js +276 -0
  98. package/src/verification/source-discovery.js +153 -0
  99. package/src/verification-policy.js +398 -0
  100. package/src/version.js +52 -1
  101. package/template/.aioson/agents/analyst.md +9 -6
  102. package/template/.aioson/agents/architect.md +34 -5
  103. package/template/.aioson/agents/briefing-refiner.md +25 -0
  104. package/template/.aioson/agents/briefing.md +69 -12
  105. package/template/.aioson/agents/committer.md +2 -1
  106. package/template/.aioson/agents/copywriter.md +30 -21
  107. package/template/.aioson/agents/design-hybrid-forge.md +28 -15
  108. package/template/.aioson/agents/dev.md +28 -10
  109. package/template/.aioson/agents/deyvin.md +3 -2
  110. package/template/.aioson/agents/discover.md +12 -3
  111. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  112. package/template/.aioson/agents/genome.md +19 -10
  113. package/template/.aioson/agents/neo.md +30 -30
  114. package/template/.aioson/agents/orache.md +20 -11
  115. package/template/.aioson/agents/orchestrator.md +84 -7
  116. package/template/.aioson/agents/pm.md +8 -8
  117. package/template/.aioson/agents/product.md +15 -11
  118. package/template/.aioson/agents/profiler-enricher.md +20 -11
  119. package/template/.aioson/agents/profiler-forge.md +20 -11
  120. package/template/.aioson/agents/profiler-researcher.md +20 -11
  121. package/template/.aioson/agents/qa.md +87 -17
  122. package/template/.aioson/agents/scope-check.md +19 -5
  123. package/template/.aioson/agents/setup.md +12 -1
  124. package/template/.aioson/agents/sheldon.md +92 -9
  125. package/template/.aioson/agents/site-forge.md +11 -2
  126. package/template/.aioson/agents/squad.md +20 -5
  127. package/template/.aioson/agents/ux-ui.md +4 -2
  128. package/template/.aioson/agents/validator.md +33 -1
  129. package/template/.aioson/config/verification.json +61 -0
  130. package/template/.aioson/config.md +13 -9
  131. package/template/.aioson/docs/LAYERS.md +2 -2
  132. package/template/.aioson/docs/autopilot-handoff.md +10 -10
  133. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  134. package/template/.aioson/docs/dev/phase-loop.md +47 -0
  135. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  136. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  137. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  138. package/template/.aioson/docs/product/prd-contract.md +12 -12
  139. package/template/.aioson/docs/prototype-contract.md +98 -0
  140. package/template/.aioson/docs/reference-identity.md +94 -0
  141. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  142. package/template/.aioson/docs/verification-config.md +82 -0
  143. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  144. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  145. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  146. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  147. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  148. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  149. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  150. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  151. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  152. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  153. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  154. package/template/.aioson/skills/process/prototype-forge/SKILL.md +98 -0
  155. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  156. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
  157. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  158. package/template/CLAUDE.md +15 -11
@@ -0,0 +1,387 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `aioson harness:retro-promote [path] --feature=<slug> [--to=learnings|rules]`
5
+ *
6
+ * Human-approved promotion path for candidates mined by `harness:retro`.
7
+ * Default is dry-run. `--apply --select=<candidate-key|all>` is required before
8
+ * writing `.aioson/learnings/` or `.aioson/rules/`.
9
+ */
10
+
11
+ const fs = require('node:fs/promises');
12
+ const fsSync = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const {
16
+ collectSources,
17
+ resolveFeatureExists
18
+ } = require('../lib/retro/retro-sources');
19
+ const { aggregate } = require('../lib/retro/retro-aggregate');
20
+ const { openRuntimeDb, promoteProjectLearning } = require('../runtime-store');
21
+ const { upsertProjectLearning } = require('./devlog-process');
22
+
23
+ const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
24
+ const TARGETS = new Set(['learnings', 'rules']);
25
+ const EXIT_OK = 0;
26
+ const EXIT_IO = 1;
27
+ const EXIT_INPUT = 12;
28
+ const BAR = '-'.repeat(42);
29
+
30
+ function relPosix(value) {
31
+ return String(value || '').replace(/\\/g, '/');
32
+ }
33
+
34
+ function nowIso() {
35
+ return new Date().toISOString();
36
+ }
37
+
38
+ function inputError(logger, message, reason) {
39
+ if (logger && typeof logger.error === 'function') logger.error(message);
40
+ process.exitCode = EXIT_INPUT;
41
+ return { ok: false, exitCode: EXIT_INPUT, reason, message };
42
+ }
43
+
44
+ function ioError(logger, message, err) {
45
+ if (logger && typeof logger.error === 'function') logger.error(message);
46
+ process.exitCode = EXIT_IO;
47
+ return { ok: false, exitCode: EXIT_IO, reason: 'io_error', message: err && err.message ? err.message : String(err) };
48
+ }
49
+
50
+ function sanitizeInline(value) {
51
+ return String(value || '')
52
+ .replace(/[\u0000-\u001F\u007F\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g, ' ')
53
+ .replace(/\s+/g, ' ')
54
+ .trim();
55
+ }
56
+
57
+ function yamlString(value) {
58
+ return JSON.stringify(sanitizeInline(value));
59
+ }
60
+
61
+ function slugify(value, fallback = 'retro-finding') {
62
+ const slug = String(value || '')
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9]+/g, '-')
65
+ .replace(/^-+|-+$/g, '')
66
+ .slice(0, 90);
67
+ return slug || fallback;
68
+ }
69
+
70
+ function candidateTitle(candidate) {
71
+ const first = candidate.occurrences && candidate.occurrences[0] ? candidate.occurrences[0] : null;
72
+ const title = first && first.title ? sanitizeInline(first.title) : null;
73
+ const anchor = candidate.finding_id || (candidate.signature ? `sig:${candidate.signature.slice(0, 12)}` : candidate.key);
74
+ return title
75
+ ? `Retro check: ${title}`
76
+ : `Retro check: ${candidate.feature_slug} ${anchor}`;
77
+ }
78
+
79
+ function candidateSummary(candidate) {
80
+ const reasons = Array.isArray(candidate.reasons) ? candidate.reasons.join(', ') : 'unknown';
81
+ return [
82
+ `key=${candidate.key}`,
83
+ `severity=${candidate.max_severity || 'unknown'}`,
84
+ `reasons=${reasons}`,
85
+ `occurrences=${candidate.cost ? candidate.cost.occurrences : (candidate.occurrences || []).length}`,
86
+ `fail_pass_cycles=${candidate.cost ? candidate.cost.fail_pass_cycles : 0}`
87
+ ].join('; ');
88
+ }
89
+
90
+ function occurrenceLines(candidate) {
91
+ const occurrences = Array.isArray(candidate.occurrences) ? candidate.occurrences : [];
92
+ if (occurrences.length === 0) {
93
+ return ['- No single finding occurrence; candidate came from repeated FAIL/PASS cycles.'];
94
+ }
95
+ return occurrences.map((o) => {
96
+ const id = o.finding_id || (o.signature ? `sig:${String(o.signature).slice(0, 12)}` : 'n/a');
97
+ return `- ${relPosix(o.source_path)} | id=${sanitizeInline(id)} | severity=${sanitizeInline(o.severity || 'unknown')} | status=${sanitizeInline(o.status || 'unknown')} | date=${sanitizeInline(o.date || 'unknown')}`;
98
+ });
99
+ }
100
+
101
+ function buildLearningContent(candidate, { dossierRelPath, generatedAt }) {
102
+ const title = candidateTitle(candidate);
103
+ const lines = [
104
+ '---',
105
+ `title: ${yamlString(title)}`,
106
+ `feature: ${candidate.feature_slug}`,
107
+ 'signal_type: retro-verification',
108
+ 'source: harness-retro-promote',
109
+ `source_dossier: ${dossierRelPath}`,
110
+ 'status: active',
111
+ `promoted_at: ${generatedAt}`,
112
+ '---',
113
+ '',
114
+ `# ${sanitizeInline(title)}`,
115
+ '',
116
+ '## Rule of Thumb',
117
+ '',
118
+ 'Before closing a similar implementation, check that this recurring verification issue is explicitly covered by code evidence and tests.',
119
+ '',
120
+ '## Retro Candidate',
121
+ '',
122
+ `- key: ${candidate.key}`,
123
+ `- severity: ${candidate.max_severity || 'unknown'}`,
124
+ `- reasons: ${(candidate.reasons || []).join(', ') || 'unknown'}`,
125
+ `- source dossier: ${dossierRelPath}`,
126
+ '',
127
+ '## Bounded Evidence',
128
+ '',
129
+ ...occurrenceLines(candidate),
130
+ '',
131
+ 'No raw auditor output, stderr, prompt package, or finding evidence text is stored in this learning.',
132
+ ''
133
+ ];
134
+ return lines.join('\n');
135
+ }
136
+
137
+ function buildRuleContent(candidate, { dossierRelPath, generatedAt, ruleName }) {
138
+ const title = candidateTitle(candidate);
139
+ const lines = [
140
+ '---',
141
+ `name: ${ruleName}`,
142
+ `description: ${yamlString(`Prevent recurring implementation verification issue: ${title}`)}`,
143
+ 'agents: [dev, deyvin, scope-check, qa]',
144
+ 'priority: 6',
145
+ 'version: 1.0.0',
146
+ 'modes: [planning, executing, reviewing]',
147
+ 'task_types: [implementation, verification, testing]',
148
+ 'load_tier: trigger',
149
+ `triggers: [${candidate.feature_slug}, implementation verification, retro candidate, recurring miss]`,
150
+ 'retrieval_intents: [implementation, testing, memory]',
151
+ 'paths: [src/**, tests/**, .aioson/context/features/**]',
152
+ '---',
153
+ '',
154
+ `# ${sanitizeInline(title)}`,
155
+ '',
156
+ 'When implementing or verifying similar behavior, explicitly check this recurring retro issue before claiming the feature is done.',
157
+ '',
158
+ '## Candidate Metadata',
159
+ '',
160
+ `- key: ${candidate.key}`,
161
+ `- feature: ${candidate.feature_slug}`,
162
+ `- severity: ${candidate.max_severity || 'unknown'}`,
163
+ `- reasons: ${(candidate.reasons || []).join(', ') || 'unknown'}`,
164
+ `- source dossier: ${dossierRelPath}`,
165
+ `- promoted at: ${generatedAt}`,
166
+ '',
167
+ '## Bounded Evidence',
168
+ '',
169
+ ...occurrenceLines(candidate),
170
+ '',
171
+ 'This rule intentionally excludes raw auditor output, stderr, prompt packages, and finding evidence text.',
172
+ ''
173
+ ];
174
+ return lines.join('\n');
175
+ }
176
+
177
+ function selectCandidates(candidates, selectValue) {
178
+ if (!selectValue || selectValue === true) return { ok: true, selected: candidates, selection: 'preview_all' };
179
+ const raw = String(selectValue).trim();
180
+ if (raw === 'all') return { ok: true, selected: candidates, selection: 'all' };
181
+ const wanted = new Set(raw.split(',').map((item) => item.trim()).filter(Boolean));
182
+ const selected = candidates.filter((candidate) => wanted.has(candidate.key));
183
+ const missing = [...wanted].filter((key) => !candidates.some((candidate) => candidate.key === key));
184
+ if (missing.length > 0) return { ok: false, reason: 'unknown_selection', missing };
185
+ return { ok: true, selected, selection: raw };
186
+ }
187
+
188
+ function promotionItems(candidates, to) {
189
+ return candidates.map((candidate) => {
190
+ const fileSlug = slugify(`${candidate.feature_slug}-${candidate.key}`);
191
+ const fileName = to === 'rules' ? `retro-${fileSlug}.md` : `${fileSlug}.md`;
192
+ const targetPath = to === 'rules'
193
+ ? path.join('.aioson', 'rules', fileName)
194
+ : path.join('.aioson', 'learnings', 'gotchas', fileName);
195
+ return {
196
+ key: candidate.key,
197
+ feature_slug: candidate.feature_slug,
198
+ title: candidateTitle(candidate),
199
+ severity: candidate.max_severity || 'unknown',
200
+ reasons: candidate.reasons || [],
201
+ occurrences: candidate.cost ? candidate.cost.occurrences : (candidate.occurrences || []).length,
202
+ target: to,
203
+ target_path: relPosix(targetPath)
204
+ };
205
+ });
206
+ }
207
+
208
+ async function ensureLearningIndex(rootDir, item) {
209
+ const indexRel = path.join('.aioson', 'learnings', 'INDEX.md');
210
+ const indexAbs = path.join(rootDir, indexRel);
211
+ let content = '# Project Learnings\n\n';
212
+ try {
213
+ content = await fs.readFile(indexAbs, 'utf8');
214
+ } catch {
215
+ // create below
216
+ }
217
+
218
+ const targetRel = relPosix(path.relative(path.join(rootDir, '.aioson', 'learnings'), path.join(rootDir, item.target_path)));
219
+ if (content.includes(`](${targetRel})`)) return false;
220
+
221
+ const entry = `- [${sanitizeInline(item.title)}](${targetRel}) - retro candidate ${item.key}\n`;
222
+ const next = content
223
+ .replace(/\r\n/g, '\n')
224
+ .replace(/\n?_No project learnings yet\._\n?/i, '\n')
225
+ .replace(/\s*$/, '\n');
226
+ await fs.mkdir(path.dirname(indexAbs), { recursive: true });
227
+ await fs.writeFile(indexAbs, `${next}${entry}`, 'utf8');
228
+ return true;
229
+ }
230
+
231
+ async function writePromotionFile(rootDir, candidate, item, { dossierRelPath, generatedAt }) {
232
+ const abs = path.join(rootDir, item.target_path);
233
+ if (fsSync.existsSync(abs)) return { action: 'skipped', reason: 'already_exists', path: item.target_path };
234
+ await fs.mkdir(path.dirname(abs), { recursive: true });
235
+ const content = item.target === 'rules'
236
+ ? buildRuleContent(candidate, {
237
+ dossierRelPath,
238
+ generatedAt,
239
+ ruleName: path.basename(item.target_path, '.md')
240
+ })
241
+ : buildLearningContent(candidate, { dossierRelPath, generatedAt });
242
+ await fs.writeFile(abs, content, 'utf8');
243
+ return { action: 'written', path: item.target_path };
244
+ }
245
+
246
+ async function recordRuntimeLearning(rootDir, candidate, item) {
247
+ const handle = await openRuntimeDb(rootDir);
248
+ try {
249
+ const result = upsertProjectLearning(handle.db, {
250
+ title: item.title,
251
+ type: 'quality',
252
+ kind: 'gotcha',
253
+ featureSlug: item.feature_slug,
254
+ evidence: candidateSummary(candidate),
255
+ sourceSession: `harness-retro-promote:${candidate.key}`
256
+ });
257
+ if (item.target === 'rules') {
258
+ promoteProjectLearning(handle.db, result.learningId, item.target_path);
259
+ }
260
+ return result;
261
+ } finally {
262
+ handle.db.close();
263
+ }
264
+ }
265
+
266
+ async function applyPromotions(rootDir, selectedCandidates, items, { dossierRelPath, generatedAt, target }) {
267
+ const written = [];
268
+ const skipped = [];
269
+ const runtime = [];
270
+
271
+ for (let index = 0; index < selectedCandidates.length; index += 1) {
272
+ const candidate = selectedCandidates[index];
273
+ const item = items[index];
274
+ const fileResult = await writePromotionFile(rootDir, candidate, item, { dossierRelPath, generatedAt });
275
+ if (fileResult.action === 'written') written.push(fileResult);
276
+ else skipped.push(fileResult);
277
+
278
+ if (target === 'learnings') {
279
+ await ensureLearningIndex(rootDir, item);
280
+ }
281
+
282
+ runtime.push({
283
+ key: item.key,
284
+ ...(await recordRuntimeLearning(rootDir, candidate, item))
285
+ });
286
+ }
287
+
288
+ return { written, skipped, runtime };
289
+ }
290
+
291
+ async function runHarnessRetroPromote({ args, options = {}, logger } = {}) {
292
+ const log = logger || { log() {}, error() {} };
293
+ const rootDir = path.resolve(process.cwd(), (args && args[0]) || '.');
294
+ const slug = options.feature !== undefined && options.feature !== true
295
+ ? String(options.feature || '').trim()
296
+ : '';
297
+ const target = options.to === undefined || options.to === true
298
+ ? 'learnings'
299
+ : String(options.to).trim();
300
+ const apply = Boolean(options.apply);
301
+
302
+ if (!slug) return inputError(log, 'harness:retro-promote requires --feature=<slug>', 'missing_feature');
303
+ if (!SLUG_RE.test(slug)) return inputError(log, `Invalid feature slug: ${slug}`, 'invalid_slug');
304
+ if (!TARGETS.has(target)) return inputError(log, `Invalid --to target: ${target} (use learnings or rules)`, 'invalid_target');
305
+ if (!resolveFeatureExists(rootDir, slug)) return inputError(log, `Feature not found: ${slug}`, 'feature_not_found');
306
+
307
+ const dossierRelPath = relPosix(path.join('.aioson', 'context', 'retro', `${slug}.md`));
308
+ if (!fsSync.existsSync(path.join(rootDir, dossierRelPath))) {
309
+ return inputError(log, `Retro dossier not found: ${dossierRelPath}. Run harness:retro first.`, 'dossier_missing');
310
+ }
311
+
312
+ const sources = collectSources(rootDir, [slug]);
313
+ const { candidates } = aggregate(sources);
314
+ const selection = selectCandidates(candidates, options.select);
315
+ if (!selection.ok) {
316
+ return inputError(log, `Unknown candidate key(s): ${selection.missing.join(', ')}`, selection.reason);
317
+ }
318
+
319
+ if (apply && (options.select === undefined || options.select === true || String(options.select).trim() === '')) {
320
+ return inputError(log, 'Applying retro promotion requires --select=<candidate-key|all>', 'selection_required');
321
+ }
322
+
323
+ const items = promotionItems(selection.selected, target);
324
+ const result = {
325
+ ok: true,
326
+ exitCode: EXIT_OK,
327
+ dry_run: !apply,
328
+ applied: apply,
329
+ target,
330
+ feature: slug,
331
+ dossier: dossierRelPath,
332
+ candidates: candidates.length,
333
+ selected: selection.selected.length,
334
+ selection: selection.selection,
335
+ items,
336
+ written: [],
337
+ skipped: [],
338
+ runtime: []
339
+ };
340
+
341
+ if (apply && selection.selected.length > 0) {
342
+ try {
343
+ const applied = await applyPromotions(rootDir, selection.selected, items, {
344
+ dossierRelPath,
345
+ generatedAt: nowIso(),
346
+ target
347
+ });
348
+ result.written = applied.written;
349
+ result.skipped = applied.skipped;
350
+ result.runtime = applied.runtime;
351
+ } catch (err) {
352
+ return ioError(log, `Failed to apply retro promotion: ${err.message}`, err);
353
+ }
354
+ }
355
+
356
+ if (options.json) return result;
357
+
358
+ log.log('');
359
+ log.log('Retro Promotion');
360
+ log.log(BAR);
361
+ log.log(`Feature: ${slug}`);
362
+ log.log(`Target: ${target}`);
363
+ log.log(`Mode: ${apply ? 'apply' : 'dry-run'}`);
364
+ log.log(`Candidates: ${candidates.length}; selected: ${selection.selected.length}`);
365
+ for (const item of items) {
366
+ log.log(`- ${item.key} -> ${item.target_path}`);
367
+ }
368
+ if (!apply && items.length > 0) {
369
+ log.log('');
370
+ log.log('Apply explicitly with: --apply --select=<candidate-key|all>');
371
+ }
372
+ log.log('');
373
+
374
+ return result;
375
+ }
376
+
377
+ module.exports = {
378
+ runHarnessRetroPromote,
379
+ _internal: {
380
+ buildLearningContent,
381
+ buildRuleContent,
382
+ candidateTitle,
383
+ selectCandidates,
384
+ promotionItems
385
+ }
386
+ };
387
+
@@ -262,6 +262,24 @@ function spawnExecutable(binaryPath) {
262
262
  return process.platform === 'win32' ? `"${binaryPath}"` : binaryPath;
263
263
  }
264
264
 
265
+ function isWindowsPidAlive(pid) {
266
+ // process.kill(pid, 0) is not authoritative on Windows: it throws EPERM both
267
+ // for "process exists but no access" AND (commonly) for PIDs that are already
268
+ // gone. Probe the real process table so a dead PID reconciles instead of
269
+ // looking 'unknown' forever (which blocks live:start with "session already
270
+ // active"). Returns true/false when known, null when the probe itself failed.
271
+ try {
272
+ const out = require('node:child_process').execFileSync(
273
+ 'tasklist',
274
+ ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
275
+ { encoding: 'utf8', windowsHide: true, timeout: 3000 }
276
+ );
277
+ return out.includes(`"${pid}"`);
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
265
283
  function detectProcessState(pid) {
266
284
  if (!pid) return 'not_tracked';
267
285
  try {
@@ -271,6 +289,12 @@ function detectProcessState(pid) {
271
289
  if (error && error.code === 'ESRCH') {
272
290
  return 'dead';
273
291
  }
292
+ if (process.platform === 'win32') {
293
+ const alive = isWindowsPidAlive(Number(pid));
294
+ if (alive === true) return 'alive';
295
+ if (alive === false) return 'dead';
296
+ // probe inconclusive → fall through to 'unknown'
297
+ }
274
298
  return 'unknown';
275
299
  }
276
300
  }
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson prototype:check — deterministic fidelity guard for the prototype contract.
5
+ *
6
+ * When a PRD carries a `## Prototype reference` (the carrier @product writes), this
7
+ * command verifies that the prototype actually reaches the build:
8
+ * 1. the referenced prototype.html + manifest exist (no dangling pointer);
9
+ * 2. a requirements-{slug}.md bridge exists;
10
+ * 3. the Core interactions the manifest lists are echoed as acceptance criteria
11
+ * in requirements (this is the only place infidelity becomes machine-checkable —
12
+ * @validator never reads the prototype, only the AC authored by @analyst).
13
+ *
14
+ * It is a STRUCTURAL check, not a semantic one: coverage is a folded substring match
15
+ * of each manifest interaction phrase against the requirements text. The prototype
16
+ * contract instructs @analyst to echo the interaction names verbatim
17
+ * (e.g. "add card persists and re-renders"), so the match is deterministic, not fuzzy.
18
+ *
19
+ * Features with no `## Prototype reference` are a no-op (status: not_applicable).
20
+ *
21
+ * Usage:
22
+ * aioson prototype:check . --feature=kanban
23
+ * aioson prototype:check . --feature=kanban --json
24
+ */
25
+
26
+ const path = require('node:path');
27
+ const { readFileSafe, contextDir } = require('../preflight-engine');
28
+ const { resolveInsideRoot } = require('../verification/path-policy');
29
+
30
+ const BAR = '━'.repeat(30);
31
+
32
+ // Match the surface detectors: fold diacritics so a localized requirements file
33
+ // (pt-BR ACs) is compared the same way as an English one.
34
+ function fold(s) {
35
+ return String(s || '').normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
36
+ }
37
+
38
+ // The `## Prototype reference` section body, if present. Captures from the heading
39
+ // to the next `## ` heading or end of file (no `m` flag, so `$` means end-of-input).
40
+ function prototypeReferenceSection(prd) {
41
+ const m = String(prd || '').match(/##\s+Prototype reference[^\n]*\n([\s\S]*?)(?=\n##\s|$)/i);
42
+ return m ? m[1] : null;
43
+ }
44
+
45
+ function parsePath(section, key) {
46
+ const m = String(section || '').match(new RegExp(`^[-*]\\s*${key}:\\s*(\\S+)`, 'mi'));
47
+ return m ? m[1] : null;
48
+ }
49
+
50
+ // Core interactions are listed in the manifest as backtick-quoted tokens
51
+ // (e.g. - `add card` — ...). Prefer a "Core interactions" section; fall back to the
52
+ // whole manifest so older manifests still yield tokens. Deduped, normalized.
53
+ function extractInteractions(manifest) {
54
+ const text = String(manifest || '');
55
+ const section = text.match(/##\s+Core interactions[\s\S]*?(?=\n##\s|$)/i);
56
+ const scope = section ? section[0] : text;
57
+ const tokens = [...scope.matchAll(/`([^`]+)`/g)].map((t) => t[1].trim()).filter(Boolean);
58
+ return [...new Set(tokens)];
59
+ }
60
+
61
+ async function runPrototypeCheck({ args, options = {}, logger }) {
62
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
63
+ const slug = options.feature ? String(options.feature) : null;
64
+ const strict = Boolean(options.strict || String(options.policy || '').toLowerCase() === 'strict');
65
+ const dir = contextDir(targetDir);
66
+
67
+ const prdFile = slug ? `prd-${slug}.md` : 'prd.md';
68
+ const reqFile = slug ? `requirements-${slug}.md` : 'requirements.md';
69
+ const prd = await readFileSafe(path.join(dir, prdFile));
70
+
71
+ const emit = (result) => {
72
+ if (options.json) return result;
73
+ logger.log('');
74
+ logger.log(slug ? `Prototype check — ${slug}` : 'Prototype check');
75
+ logger.log(BAR);
76
+ logger.log(`Status: ${result.status}`);
77
+ if (result.message) logger.log(result.message);
78
+ if (result.interactions && result.interactions.total > 0) {
79
+ logger.log(`Interactions covered: ${result.interactions.covered}/${result.interactions.total}`);
80
+ if (result.interactions.uncovered.length) {
81
+ logger.log(`Uncovered: ${result.interactions.uncovered.map((i) => `"${i}"`).join(', ')}`);
82
+ }
83
+ }
84
+ logger.log('');
85
+ return result;
86
+ };
87
+
88
+ if (!prd) {
89
+ return emit({ ok: true, status: 'skipped', reason: 'no_prd', feature_slug: slug,
90
+ message: `No ${prdFile} found — nothing to check.` });
91
+ }
92
+
93
+ const section = prototypeReferenceSection(prd);
94
+ if (!section) {
95
+ return emit({ ok: true, status: 'not_applicable', feature_slug: slug,
96
+ message: 'PRD has no `## Prototype reference` — feature has no prototype contract.' });
97
+ }
98
+
99
+ // Resolve prototype + manifest paths (from the section, else the default location).
100
+ const protoRel = parsePath(section, 'prototype')
101
+ || (slug ? `.aioson/briefings/${slug}/prototype.html` : null);
102
+ const manifestRel = parsePath(section, 'manifest')
103
+ || (slug ? `.aioson/briefings/${slug}/prototype-manifest.md` : null);
104
+
105
+ const checks = { prototype_exists: false, manifest_exists: false, requirements_exists: false };
106
+
107
+ const protoSafe = protoRel ? resolveInsideRoot(targetDir, protoRel) : { ok: false, reason: 'missing_path' };
108
+ if (!protoSafe.ok) {
109
+ return emit({ ok: false, status: 'fail', reason: protoSafe.reason, field: 'prototype', feature_slug: slug, checks,
110
+ message: `\`## Prototype reference\` prototype path is invalid: ${protoRel || '(unspecified)'}.` });
111
+ }
112
+ const protoContent = await readFileSafe(protoSafe.path);
113
+ checks.prototype_exists = protoContent !== null;
114
+ if (!checks.prototype_exists) {
115
+ return emit({ ok: false, status: 'fail', reason: 'dangling_prototype', feature_slug: slug, checks,
116
+ message: `\`## Prototype reference\` points to ${protoRel || '(unspecified)'}, but that file is missing.` });
117
+ }
118
+
119
+ const manifestSafe = manifestRel ? resolveInsideRoot(targetDir, manifestRel) : { ok: false, reason: 'missing_path' };
120
+ if (!manifestSafe.ok) {
121
+ return emit({ ok: false, status: 'fail', reason: manifestSafe.reason, field: 'manifest', feature_slug: slug, checks,
122
+ message: `\`## Prototype reference\` manifest path is invalid: ${manifestRel || '(unspecified)'}.` });
123
+ }
124
+ const manifest = await readFileSafe(manifestSafe.path);
125
+ checks.manifest_exists = manifest !== null;
126
+ if (!checks.manifest_exists) {
127
+ return emit({ ok: false, status: 'fail', reason: 'missing_manifest', feature_slug: slug, checks,
128
+ message: `Prototype exists but its manifest ${manifestRel || '(unspecified)'} is missing.` });
129
+ }
130
+
131
+ const requirements = await readFileSafe(path.join(dir, reqFile));
132
+ checks.requirements_exists = requirements !== null;
133
+ if (!checks.requirements_exists) {
134
+ return emit({ ok: false, status: 'fail', reason: 'missing_requirements', feature_slug: slug, checks,
135
+ message: `PRD references a prototype but ${reqFile} is missing — @analyst has not authored the acceptance-criteria bridge.` });
136
+ }
137
+
138
+ const interactions = extractInteractions(manifest);
139
+ const reqFolded = fold(requirements);
140
+ const uncovered = interactions.filter((i) => !reqFolded.includes(fold(i)));
141
+ const covered = interactions.length - uncovered.length;
142
+ const interactionsResult = { total: interactions.length, covered, uncovered };
143
+
144
+ if (interactions.length === 0) {
145
+ return emit({ ok: true, status: 'ok', feature_slug: slug, checks, interactions: interactionsResult,
146
+ message: 'Prototype, manifest, and requirements all present. Manifest lists no machine-readable Core interactions to cover.' });
147
+ }
148
+
149
+ if (covered === 0) {
150
+ return emit({ ok: false, status: 'fail', reason: 'no_ac_coverage', feature_slug: slug, checks, interactions: interactionsResult,
151
+ message: 'None of the prototype Core interactions appear in the requirements ACs — the prototype is not reaching @validator.' });
152
+ }
153
+
154
+ if (uncovered.length > 0) {
155
+ return emit({ ok: !strict, status: strict ? 'fail' : 'warn', reason: 'partial_ac_coverage', feature_slug: slug, checks, interactions: interactionsResult,
156
+ message: 'Some prototype Core interactions have no matching acceptance criterion. Add an AC per interaction (or defer it explicitly in the PRD).' });
157
+ }
158
+
159
+ return emit({ ok: true, status: 'ok', feature_slug: slug, checks, interactions: interactionsResult,
160
+ message: 'Every prototype Core interaction is echoed in the requirements ACs.' });
161
+ }
162
+
163
+ module.exports = { runPrototypeCheck };