@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,530 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson verify:artifact --kind=<kind> [--slug=<slug>] [path] [--advisory]
5
+ * [--strict] [--json] — the build-free "done = proven, not asserted" gate for
6
+ * the NON-code artifacts the peripheral agents produce.
7
+ *
8
+ * Where an SG-* criterion gates a code feature's harness contract, this gates a
9
+ * produced artifact (project context, a genome, a research report, the bootstrap
10
+ * cache, a hybrid skill, ...) the same cheap way: read the declared files and
11
+ * prove a required structure is present and no placeholder / truncation slipped
12
+ * in before the agent self-declares done. Pure fs + RegExp + JSON.parse — no
13
+ * shell, no build — so it costs ~milliseconds and runs at every agent's done
14
+ * gate, cross-platform by construction.
15
+ *
16
+ * Each `kind` resolves to EITHER an existing validator (project context →
17
+ * validateProjectContextFile; genome → genome:doctor) OR a declarative ruleset
18
+ * evaluated by the shared static-criteria engine. No new analysis logic per
19
+ * artifact — just data. Adding a kind is a registry entry, not a code path.
20
+ *
21
+ * Exit code: a hard failure sets exit 1 (blocking) unless --advisory (warn-only,
22
+ * always exit 0). Pass suppressExitCode for programmatic callers (an agent
23
+ * done-gate that wants to interpret the verdict itself). --strict promotes
24
+ * warnings to blocking issues.
25
+ */
26
+
27
+ const path = require('node:path');
28
+ const fs = require('node:fs');
29
+ const { spawnSync } = require('node:child_process');
30
+
31
+ const { evaluateStaticCriterion } = require('../harness/static-criteria');
32
+
33
+ const VERSION = '1.0.0';
34
+ const GENERATOR = `aioson verify:artifact@${VERSION}`;
35
+
36
+ // ─── ruleset engine ─────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Run a list of static criteria (the same {id, files, must_match, must_not_match}
40
+ * shape the harness SG-* gate uses) against the working tree. Returns a uniform
41
+ * { ok, issues, warnings, checks }.
42
+ *
43
+ * @param {Array} criteria
44
+ * @param {string} targetDir
45
+ */
46
+ function evaluateRuleset(criteria, targetDir) {
47
+ const checks = [];
48
+ const issues = [];
49
+ for (const criterion of Array.isArray(criteria) ? criteria : []) {
50
+ const res = evaluateStaticCriterion(criterion, targetDir);
51
+ checks.push(res);
52
+ if (!res.ok) {
53
+ issues.push(`[${res.id}] ${res.detail || 'failed'}`);
54
+ }
55
+ }
56
+ return { ok: issues.length === 0, issues, warnings: [], checks };
57
+ }
58
+
59
+ // ─── ruleset registry (declarative, data-only) ──────────────────────────────
60
+ //
61
+ // Each entry is (ctx) => { label, criteria[] }. ctx = { slug, targetDir }.
62
+ // Frontmatter/placeholder checks reuse the OR-across-files / absent-in-all
63
+ // semantics of the static engine; per-file structure is expressed as one
64
+ // criterion per file.
65
+
66
+ const PLACEHOLDER_PATTERNS = ['\\bTODO\\b', '\\bFIXME\\b', '\\bTBD\\b', 'Lorem ipsum', '\\[\\.\\.\\.\\]', 'XXXX'];
67
+
68
+ const BOOTSTRAP_DIR = '.aioson/context/bootstrap';
69
+ const BOOTSTRAP_FILES = ['what-is.md', 'what-it-does.md', 'how-it-works.md', 'current-state.md'];
70
+ const PROFILER_DIR = '.aioson/profiler-reports';
71
+
72
+ // Unfilled template tokens left in a produced artifact (a tell that the agent
73
+ // emitted the skeleton without filling it). Targeted, not a blanket `[...]`
74
+ // match, to avoid flagging legitimate `[1]`-style citations.
75
+ const TEMPLATE_TOKENS = ['\\[Full Name\\]', '\\[count\\]', '\\[low/medium/high'];
76
+
77
+ const RULESETS = {
78
+ // discover — the 4-file cold-start cache must all exist with real frontmatter.
79
+ bootstrap: () => ({
80
+ label: 'discovery bootstrap cache',
81
+ criteria: BOOTSTRAP_FILES.map((f) => ({
82
+ id: `bootstrap:${f}`,
83
+ files: [`${BOOTSTRAP_DIR}/${f}`],
84
+ must_match: ['generated_by', 'confidence'],
85
+ must_not_match: PLACEHOLDER_PATTERNS
86
+ }))
87
+ }),
88
+
89
+ // profiler-researcher — the research report must carry its frontmatter and the
90
+ // load-bearing skeleton (inventory + extracted material + gaps), with no
91
+ // unfilled template token.
92
+ 'research-report': (ctx) => ({
93
+ label: 'profiler research report',
94
+ criteria: [{
95
+ id: 'research-report',
96
+ files: [`${PROFILER_DIR}/${ctx.slug}/research-report.md`],
97
+ must_match: ['sources_found', '## Source Inventory', '## Extracted Material by Category', '## Gaps and Next Research Moves'],
98
+ must_not_match: [...PLACEHOLDER_PATTERNS, ...TEMPLATE_TOKENS]
99
+ }]
100
+ }),
101
+
102
+ // profiler-enricher — the enriched profile must carry the executive summary,
103
+ // psychometric profile, the operational method (the part that makes a genome
104
+ // *work* rather than simulate opinions), and the trait-interaction analysis.
105
+ 'enriched-profile': (ctx) => ({
106
+ label: 'profiler enriched profile',
107
+ criteria: [{
108
+ id: 'enriched-profile',
109
+ files: [`${PROFILER_DIR}/${ctx.slug}/enriched-profile.md`],
110
+ must_match: ['## Executive Summary', '## Psychometric Profile', '## Operational Method', '## Trait Interactions'],
111
+ must_not_match: [...PLACEHOLDER_PATTERNS, ...TEMPLATE_TOKENS]
112
+ }]
113
+ }),
114
+
115
+ // orache — the investigation report is date-stamped, so it's resolved via
116
+ // --file. It must carry the 7-dimension skeleton, an impact analysis, and
117
+ // source attribution, with no unfilled template token.
118
+ 'orache-report': (ctx) => ({
119
+ label: 'orache investigation report',
120
+ criteria: [{
121
+ id: 'orache-report',
122
+ files: [ctx.file || `squad-searches/${ctx.slug || 'MISSING'}/investigation.md`],
123
+ must_match: ['## D1', '## D4', '## D7', '## Impact Analysis', '\\*\\*Source:\\*\\*'],
124
+ must_not_match: [...PLACEHOLDER_PATTERNS, '\\{where discovered\\}']
125
+ }]
126
+ }),
127
+
128
+ // design-hybrid-forge — the hybrid skill package: a parseable .skill-meta.json
129
+ // recording its sources, a real SKILL.md, and both required previews.
130
+ 'hybrid-skill': (ctx) => ({
131
+ label: 'hybrid design skill package',
132
+ criteria: [
133
+ { id: 'hybrid:meta', files: [`.aioson/installed-skills/${ctx.slug}/.skill-meta.json`], must_match: ['sources'], must_not_match: [] },
134
+ { id: 'hybrid:skill', files: [`.aioson/installed-skills/${ctx.slug}/SKILL.md`], must_match: [], must_not_match: PLACEHOLDER_PATTERNS },
135
+ {
136
+ id: 'hybrid:previews',
137
+ files: [
138
+ `.aioson/installed-skills/${ctx.slug}/previews/${ctx.slug}.html`,
139
+ `.aioson/installed-skills/${ctx.slug}/previews/${ctx.slug}-website.html`
140
+ ],
141
+ must_match: [],
142
+ must_not_match: []
143
+ }
144
+ ]
145
+ }),
146
+
147
+ // copywriter — an advisory placeholder/template scan over the saved copy doc
148
+ // (the rich resonance checks stay in the agent's Phase-5 checklist; this just
149
+ // makes "no placeholder/Lorem/TODO/unfilled token" deterministic).
150
+ copy: (ctx) => ({
151
+ label: 'copywriter copy document',
152
+ criteria: [{
153
+ id: 'copy',
154
+ files: [`.aioson/context/copy-${ctx.slug}.md`],
155
+ must_match: [],
156
+ must_not_match: [...PLACEHOLDER_PATTERNS, ...TEMPLATE_TOKENS]
157
+ }]
158
+ }),
159
+
160
+ // reference-identity-extract — identity.md is the extracted token + per-component
161
+ // structure system-of-record. Its path varies by scope (briefing vs brand) so it is
162
+ // resolved via --file. It must carry the token skeleton plus the two anti-sameness
163
+ // anchors (pillars + signature moves) and the component-structure section, with no
164
+ // placeholder or unfilled hex/token left behind.
165
+ identity: (ctx) => ({
166
+ label: 'reference identity system',
167
+ criteria: [{
168
+ id: 'identity',
169
+ files: [ctx.file || `.aioson/briefings/${ctx.slug || 'MISSING'}/identity.md`],
170
+ must_match: [
171
+ 'generated_by',
172
+ '## Design pillars', '## Palette', '## Typography', '## Spacing',
173
+ '## Radius', '## Motion', '## Signature moves', '## Component structure notes'
174
+ ],
175
+ must_not_match: [...PLACEHOLDER_PATTERNS, '#RRGGBB', '#XXXXXX', '\\{hex\\}', '\\{token\\}']
176
+ }]
177
+ })
178
+ };
179
+
180
+ // Kinds whose target file path is keyed by --slug; without it we cannot resolve
181
+ // the artifact, so fail with a clear usage error instead of a `null/` path.
182
+ const REQUIRES_SLUG = new Set(['genome', 'research-report', 'enriched-profile', 'hybrid-skill', 'copy']);
183
+
184
+ // Kinds whose artifact has a date-stamped / caller-known path — resolved via
185
+ // --file=<path> rather than derived from a slug.
186
+ const REQUIRES_FILE = new Set(['orache-report', 'identity']);
187
+
188
+ // ─── adapters to existing validators ────────────────────────────────────────
189
+ //
190
+ // Each adapter is async (ctx, logger) => { ok, issues, warnings, checks }.
191
+
192
+ function quietLogger() {
193
+ return { log() {}, error() {}, warn() {} };
194
+ }
195
+
196
+ // ─── kind=site (runtime build gate for a generated Next.js site) ──────────────
197
+
198
+ const SITE_IGNORE = new Set(['node_modules', '.next', '.git', 'dist', 'build', 'out', 'coverage', '.turbo', '.vercel', '.aioson']);
199
+ const SITE_SCAN_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs', '.html', '.vue', '.svelte']);
200
+ const SITE_ENTRY_CANDIDATES = ['app/page.tsx', 'app/page.jsx', 'src/app/page.tsx', 'src/app/page.jsx', 'pages/index.tsx', 'pages/index.jsx', 'pages/index.js'];
201
+ const SITE_LEAKS = [
202
+ { re: /\balert\s*\(/, msg: 'native alert() dialog — use in-app UI chrome' },
203
+ { re: /\bconfirm\s*\(/, msg: 'native confirm() dialog — use in-app UI chrome' },
204
+ { re: /\bwindow\.prompt\s*\(/, msg: 'native window.prompt() dialog — use in-app UI chrome' },
205
+ { re: /Lorem ipsum/i, msg: 'Lorem ipsum placeholder copy' },
206
+ { re: /\bTODO\b|\bFIXME\b/, msg: 'TODO/FIXME left in shipped code' }
207
+ ];
208
+
209
+ function walkSiteFiles(root, max = 800) {
210
+ const out = [];
211
+ const stack = [root];
212
+ while (stack.length && out.length < max) {
213
+ const dir = stack.pop();
214
+ let entries;
215
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
216
+ for (const e of entries) {
217
+ const full = path.join(dir, e.name);
218
+ if (e.isDirectory()) {
219
+ if (!SITE_IGNORE.has(e.name)) stack.push(full);
220
+ } else if (e.isFile() && SITE_SCAN_EXTS.has(path.extname(e.name).toLowerCase())) {
221
+ out.push(full);
222
+ }
223
+ }
224
+ }
225
+ return out;
226
+ }
227
+
228
+ /** Scan a generated site for native-dialog and placeholder leaks (build-free). */
229
+ function scanSiteForLeaks(siteDir, maxHits = 20) {
230
+ const hits = [];
231
+ for (const abs of walkSiteFiles(siteDir)) {
232
+ let content;
233
+ try { content = fs.readFileSync(abs, 'utf8'); } catch { continue; }
234
+ const rel = path.relative(siteDir, abs).split(path.sep).join('/');
235
+ const lines = content.split('\n');
236
+ for (let i = 0; i < lines.length; i++) {
237
+ const stripped = lines[i].trim();
238
+ if (stripped.startsWith('//') || stripped.startsWith('*') || stripped.startsWith('/*')) continue;
239
+ for (const leak of SITE_LEAKS) {
240
+ if (leak.re.test(lines[i])) { hits.push(`${rel}:${i + 1} — ${leak.msg}`); break; }
241
+ }
242
+ if (hits.length >= maxHits) return hits;
243
+ }
244
+ }
245
+ return hits;
246
+ }
247
+
248
+ /** Build-free static floor for a site: build script, entry route, no leaks. */
249
+ function staticSiteChecks(siteDir) {
250
+ const issues = [];
251
+ const warnings = [];
252
+
253
+ let pkg = null;
254
+ try {
255
+ pkg = JSON.parse(fs.readFileSync(path.join(siteDir, 'package.json'), 'utf8'));
256
+ } catch {
257
+ issues.push('no readable package.json at the site root');
258
+ }
259
+ if (pkg && !(pkg.scripts && pkg.scripts.build)) issues.push('package.json has no "build" script');
260
+
261
+ if (!SITE_ENTRY_CANDIDATES.some((rel) => fs.existsSync(path.join(siteDir, rel)))) {
262
+ issues.push('no entry route found (app/page.* or pages/index.*)');
263
+ }
264
+
265
+ for (const leak of scanSiteForLeaks(siteDir)) issues.push(leak);
266
+ return { issues, warnings };
267
+ }
268
+
269
+ /**
270
+ * The RG-* runtime floor: the site must actually build on the real toolchain.
271
+ * `command` defaults to `npm run build`; it is overridable so a caller (or a
272
+ * test) can drive the same spawn/exit logic without the npm layer.
273
+ */
274
+ function runSiteBuild(siteDir, timeout = 600000, command = ['npm', 'run', 'build']) {
275
+ const [cmd, ...rest] = command;
276
+ const res = spawnSync(cmd, rest, { cwd: siteDir, encoding: 'utf8', shell: true, timeout });
277
+ if (res.error) return { ok: false, detail: `build could not start: ${res.error.message}` };
278
+ if (res.status === 0) return { ok: true, detail: null };
279
+ const tail = String(res.stderr || res.stdout || '')
280
+ .split('\n').map((l) => l.trim()).filter(Boolean).slice(-4).join(' | ');
281
+ return { ok: false, detail: `build failed (exit ${res.status}): ${tail || 'see build output'}` };
282
+ }
283
+
284
+ // ─── kind=commit-message (advisory subject-quality heuristics) ────────────────
285
+
286
+ /** Conservative, low-false-positive commit-subject checks. Returns issue list. */
287
+ function evaluateCommitMessage(message) {
288
+ const issues = [];
289
+ const subject = (String(message || '').replace(/^\s+/, '').split('\n')[0] || '').trim();
290
+ if (!subject) {
291
+ issues.push('empty commit subject');
292
+ return issues;
293
+ }
294
+ if (subject.length > 72) issues.push(`subject is ${subject.length} chars — keep it <= 72 (ideally <= 50)`);
295
+ if (/[.]$/.test(subject)) issues.push('subject ends with a period — drop it');
296
+ if (/^(wip|stuff|misc|various|things|update|updates|changes|tweaks?|minor)$/i.test(subject)) {
297
+ issues.push(`subject is a single vague word: "${subject}"`);
298
+ }
299
+ if (/^(fix|update|change|tweak|adjust)\s+(it|this|that|stuff|things?|bugs?|code|tests?)$/i.test(subject)) {
300
+ issues.push(`subject is vague: "${subject}" — say what changed and why`);
301
+ }
302
+ return issues;
303
+ }
304
+
305
+ const ADAPTERS = {
306
+ // setup — project.context.md is the root artifact every session reads first.
307
+ 'project-context': async (ctx) => {
308
+ const { validateProjectContextFile } = require('../context');
309
+ let res;
310
+ try {
311
+ res = await validateProjectContextFile(ctx.targetDir);
312
+ } catch (err) {
313
+ return { ok: false, issues: [`project.context.md could not be validated: ${err.message}`], warnings: [], checks: [] };
314
+ }
315
+ const issues = [];
316
+ if (!res.exists) {
317
+ issues.push(`project.context.md not found (${res.filePath || '.aioson/context/project.context.md'}) — run /setup`);
318
+ } else if (!res.parsed) {
319
+ issues.push(`project.context.md frontmatter does not parse (${res.parseError || 'invalid YAML'})`);
320
+ } else if (!res.valid) {
321
+ for (const issue of res.issues || []) {
322
+ issues.push(typeof issue === 'string' ? issue : (issue.key || JSON.stringify(issue)));
323
+ }
324
+ if (issues.length === 0) issues.push('project.context.md has invalid or missing required fields');
325
+ }
326
+ return { ok: issues.length === 0, issues, warnings: [], checks: [{ id: 'project-context', ok: issues.length === 0, detail: issues[0] || null }] };
327
+ },
328
+
329
+ // profiler-forge / genome — reuse the comprehensive genome doctor.
330
+ genome: async (ctx, logger) => {
331
+ const { runGenomeDoctor, isGenomeAvailable } = require('./genome-doctor');
332
+ if (!ctx.slug) {
333
+ return { ok: false, issues: ['kind=genome requires --slug=<genome-slug>'], warnings: [], checks: [] };
334
+ }
335
+ const avail = await isGenomeAvailable(ctx.targetDir, ctx.slug);
336
+ if (!avail.found) {
337
+ return {
338
+ ok: false,
339
+ issues: [`genome "${ctx.slug}" not found under .aioson/genomes/ (neither <slug>.md nor <slug>/SKILL.md)`],
340
+ warnings: [],
341
+ checks: []
342
+ };
343
+ }
344
+ const target = path.resolve(ctx.targetDir, avail.path);
345
+ let res;
346
+ try {
347
+ res = await runGenomeDoctor({ args: [target], options: { json: true }, logger: logger || quietLogger() });
348
+ } catch (err) {
349
+ return { ok: false, issues: [`genome:doctor failed: ${err.message}`], warnings: [], checks: [] };
350
+ }
351
+ return {
352
+ ok: Boolean(res.ok),
353
+ issues: res.ok ? [] : (res.issues || []).slice(),
354
+ warnings: (res.warnings || []).slice(),
355
+ checks: [{ id: `genome:${ctx.slug}`, ok: Boolean(res.ok), detail: res.ok ? null : (res.issues || []).join('; ') }]
356
+ };
357
+ },
358
+
359
+ // site-forge — a generated Next.js site is not done until it BUILDS on the
360
+ // real toolchain (the RG-* runtime floor), on top of a static floor: a build
361
+ // script, an entry route, and no native-dialog / placeholder leak. --no-build
362
+ // runs the static floor only; --dir points at the site root (default: cwd).
363
+ site: async (ctx) => {
364
+ const siteDir = ctx.dir ? path.resolve(ctx.targetDir, ctx.dir) : ctx.targetDir;
365
+ const { issues, warnings } = staticSiteChecks(siteDir);
366
+ if (ctx.noBuild) {
367
+ warnings.push('npm run build skipped (--no-build): static checks only — not a full runtime gate');
368
+ } else if (issues.length === 0) {
369
+ const b = runSiteBuild(siteDir, ctx.buildTimeout, ctx.buildCommand);
370
+ if (!b.ok) issues.push(b.detail);
371
+ } else {
372
+ warnings.push('npm run build skipped: static checks already failed');
373
+ }
374
+ return { ok: issues.length === 0, issues, warnings, checks: [{ id: 'site', ok: issues.length === 0, detail: issues.join('; ') || null }] };
375
+ },
376
+
377
+ // committer — advisory subject-quality audit. Reads --file if given, else the
378
+ // HEAD commit message (post-commit, so the agent can amend before push).
379
+ 'commit-message': async (ctx) => {
380
+ let message = null;
381
+ if (ctx.file) {
382
+ try {
383
+ message = fs.readFileSync(path.resolve(ctx.targetDir, ctx.file), 'utf8');
384
+ } catch {
385
+ return { ok: false, issues: [`cannot read commit message file: ${ctx.file}`], warnings: [], checks: [] };
386
+ }
387
+ } else {
388
+ const res = spawnSync('git', ['log', '-1', '--pretty=%B'], { cwd: ctx.targetDir, encoding: 'utf8' });
389
+ if (res.status !== 0) {
390
+ return { ok: false, issues: ['could not read HEAD commit message (no git repo or no commits)'], warnings: [], checks: [] };
391
+ }
392
+ message = res.stdout;
393
+ }
394
+ const found = evaluateCommitMessage(message);
395
+ return { ok: found.length === 0, issues: found, warnings: [], checks: [{ id: 'commit-message', ok: found.length === 0, detail: found.join('; ') || null }] };
396
+ }
397
+ };
398
+
399
+ // ─── kind resolution ────────────────────────────────────────────────────────
400
+
401
+ function availableKinds() {
402
+ return [...Object.keys(ADAPTERS), ...Object.keys(RULESETS)].sort();
403
+ }
404
+
405
+ async function evaluateKind(kind, ctx, logger) {
406
+ if (ADAPTERS[kind]) {
407
+ return ADAPTERS[kind](ctx, logger);
408
+ }
409
+ if (RULESETS[kind]) {
410
+ const { criteria } = RULESETS[kind](ctx);
411
+ return evaluateRuleset(criteria, ctx.targetDir);
412
+ }
413
+ return null; // unknown
414
+ }
415
+
416
+ // ─── main command ─────────────────────────────────────────────────────────────
417
+
418
+ async function runVerifyArtifact({ args, options = {}, logger }) {
419
+ const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
420
+ const kind = options.kind ? String(options.kind).trim() : '';
421
+ const slug = options.slug ? String(options.slug).trim() : null;
422
+ const file = options.file ? String(options.file).trim() : null;
423
+ const dir = options.dir ? String(options.dir).trim() : null;
424
+ const noBuild = Boolean(options['no-build'] || options.noBuild);
425
+ const buildTimeout = options['build-timeout'] ? Number(options['build-timeout']) : undefined;
426
+ const buildCommand = Array.isArray(options.buildCommand) ? options.buildCommand : undefined;
427
+ const advisory = Boolean(options.advisory);
428
+ const strict = Boolean(options.strict);
429
+ const suppressExitCode = Boolean(options.suppressExitCode);
430
+ const setExitCode = (code) => { if (!suppressExitCode) process.exitCode = code; };
431
+
432
+ if (!kind) {
433
+ const msg = `verify:artifact requires --kind=<kind>. Available: ${availableKinds().join(', ')}`;
434
+ if (options.json) { setExitCode(1); return { ok: false, kind: null, error: 'missing_kind', available: availableKinds() }; }
435
+ logger.error(msg);
436
+ setExitCode(1);
437
+ return { ok: false, kind: null };
438
+ }
439
+
440
+ if (REQUIRES_SLUG.has(kind) && !slug) {
441
+ const msg = `verify:artifact kind=${kind} requires --slug=<slug>`;
442
+ const blocking = !advisory;
443
+ if (options.json) {
444
+ setExitCode(blocking ? 1 : 0);
445
+ return { generator: GENERATOR, kind, slug: null, root: targetDir, mode: advisory ? 'advisory' : 'blocking', ok: false, blocking, issues: [msg], warnings: [], checks: [], error: 'missing_slug' };
446
+ }
447
+ logger.error(msg);
448
+ setExitCode(blocking ? 1 : 0);
449
+ return { ok: false, kind };
450
+ }
451
+
452
+ if (REQUIRES_FILE.has(kind) && !file) {
453
+ const msg = `verify:artifact kind=${kind} requires --file=<path>`;
454
+ const blocking = !advisory;
455
+ if (options.json) {
456
+ setExitCode(blocking ? 1 : 0);
457
+ return { generator: GENERATOR, kind, slug, root: targetDir, mode: advisory ? 'advisory' : 'blocking', ok: false, blocking, issues: [msg], warnings: [], checks: [], error: 'missing_file' };
458
+ }
459
+ logger.error(msg);
460
+ setExitCode(blocking ? 1 : 0);
461
+ return { ok: false, kind };
462
+ }
463
+
464
+ const result = await evaluateKind(kind, { slug, targetDir, file, dir, noBuild, buildTimeout, buildCommand }, logger);
465
+
466
+ if (result === null) {
467
+ const msg = `verify:artifact: unknown kind "${kind}". Available: ${availableKinds().join(', ')}`;
468
+ if (options.json) { setExitCode(1); return { ok: false, kind, error: 'unknown_kind', available: availableKinds() }; }
469
+ logger.error(msg);
470
+ setExitCode(1);
471
+ return { ok: false, kind };
472
+ }
473
+
474
+ // strict promotes warnings to blocking issues.
475
+ const issues = strict ? [...result.issues, ...result.warnings] : [...result.issues];
476
+ const warnings = strict ? [] : [...result.warnings];
477
+ const ok = issues.length === 0;
478
+ const blocking = !ok && !advisory;
479
+
480
+ const report = {
481
+ generator: GENERATOR,
482
+ kind,
483
+ slug,
484
+ root: targetDir,
485
+ mode: advisory ? 'advisory' : 'blocking',
486
+ ok,
487
+ blocking,
488
+ issues,
489
+ warnings,
490
+ checks: result.checks || []
491
+ };
492
+
493
+ // Persist for downstream consumption (mirrors audit:code).
494
+ try {
495
+ const ctxDir = path.join(targetDir, '.aioson', 'context');
496
+ fs.mkdirSync(ctxDir, { recursive: true });
497
+ fs.writeFileSync(path.join(ctxDir, `verify-artifact-${kind}.json`), JSON.stringify(report, null, 2), 'utf8');
498
+ } catch {
499
+ // best-effort persistence — never fail the gate on a write error
500
+ }
501
+
502
+ if (options.json) {
503
+ logger.log(JSON.stringify(report, null, 2));
504
+ setExitCode(blocking ? 1 : 0);
505
+ return report;
506
+ }
507
+
508
+ const verdict = ok ? 'OK' : (advisory ? 'ADVISORY' : 'FAIL');
509
+ logger.log(`verify:artifact — kind=${kind}${slug ? ` slug=${slug}` : ''} — ${verdict}`);
510
+ for (const issue of issues) logger.log(` ✗ ${issue}`);
511
+ for (const w of warnings) logger.log(` ⚠ ${w}`);
512
+ if (ok && warnings.length === 0) logger.log(' (no issues)');
513
+
514
+ setExitCode(blocking ? 1 : 0);
515
+ return report;
516
+ }
517
+
518
+ module.exports = {
519
+ runVerifyArtifact,
520
+ // exported for reuse / tests
521
+ evaluateRuleset,
522
+ availableKinds,
523
+ RULESETS,
524
+ ADAPTERS,
525
+ PLACEHOLDER_PATTERNS,
526
+ staticSiteChecks,
527
+ scanSiteForLeaks,
528
+ runSiteBuild,
529
+ evaluateCommitMessage
530
+ };