@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,230 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+
7
+ const { validateVerificationReport } = require('../../verification/schema');
8
+
9
+ function isFileSafe(filePath) {
10
+ try {
11
+ return fs.lstatSync(filePath).isFile();
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ function listFilesSafe(dir) {
18
+ try {
19
+ return fs.readdirSync(dir, { withFileTypes: true });
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function readTextSafe(filePath) {
26
+ try {
27
+ return fs.readFileSync(filePath, 'utf8');
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function collectReportFiles(featureDirs) {
34
+ const seen = new Set();
35
+ const files = [];
36
+
37
+ for (const featureDir of featureDirs || []) {
38
+ const latest = path.join(featureDir, 'verification-report.md');
39
+ if (isFileSafe(latest)) {
40
+ seen.add(latest);
41
+ files.push(latest);
42
+ }
43
+
44
+ const runsDir = path.join(featureDir, 'verification-runs');
45
+ for (const entry of listFilesSafe(runsDir)) {
46
+ if (!entry.isFile()) continue;
47
+ if (!/-report\.md$/i.test(entry.name)) continue;
48
+ const full = path.join(runsDir, entry.name);
49
+ if (seen.has(full)) continue;
50
+ seen.add(full);
51
+ files.push(full);
52
+ }
53
+ }
54
+
55
+ files.sort();
56
+ return files;
57
+ }
58
+
59
+ function extractMachineReport(content) {
60
+ const text = String(content || '');
61
+ const heading = /^##\s+Machine Report\s*$/im.exec(text);
62
+ if (!heading) return { ok: false, reason: 'missing_machine_report' };
63
+ const afterHeading = text.slice(heading.index + heading[0].length);
64
+ const block = afterHeading.match(/```(?:json)?\s*([\s\S]*?)```/i);
65
+ if (!block) return { ok: false, reason: 'missing_machine_report_json' };
66
+ try {
67
+ return { ok: true, report: JSON.parse(block[1]) };
68
+ } catch {
69
+ return { ok: false, reason: 'invalid_machine_report_json' };
70
+ }
71
+ }
72
+
73
+ function stableStringify(value) {
74
+ if (Array.isArray(value)) {
75
+ return `[${value.map(stableStringify).join(',')}]`;
76
+ }
77
+ if (value && typeof value === 'object') {
78
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
79
+ }
80
+ return JSON.stringify(value);
81
+ }
82
+
83
+ function reportIdentity(report) {
84
+ return crypto.createHash('sha1').update(stableStringify(report)).digest('hex');
85
+ }
86
+
87
+ function isLatestReportPath(filePath) {
88
+ return path.basename(filePath) === 'verification-report.md';
89
+ }
90
+
91
+ function shouldSkipPromotedDuplicate(seenReports, identity, filePath) {
92
+ const seen = seenReports.get(identity);
93
+ if (!seen) {
94
+ seenReports.set(identity, {
95
+ paths: [filePath],
96
+ skippedPromotedDuplicate: false
97
+ });
98
+ return false;
99
+ }
100
+
101
+ const involvesLatest = isLatestReportPath(filePath) || seen.paths.some(isLatestReportPath);
102
+ seen.paths.push(filePath);
103
+ if (involvesLatest && !seen.skippedPromotedDuplicate) {
104
+ seen.skippedPromotedDuplicate = true;
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+
110
+ function timestampFromPath(filePath) {
111
+ const name = path.basename(filePath);
112
+ const match = name.match(/(\d{8}T\d{6}Z)/);
113
+ if (!match) return null;
114
+ return match[1].replace(
115
+ /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/,
116
+ '$1-$2-$3T$4:$5:$6Z'
117
+ );
118
+ }
119
+
120
+ function severityFromVerification(severity) {
121
+ const value = String(severity || '').toLowerCase();
122
+ if (value === 'blocking') return 'high';
123
+ if (value === 'warning') return 'medium';
124
+ if (value === 'info') return 'info';
125
+ return 'unknown';
126
+ }
127
+
128
+ function statusFromVerification(status) {
129
+ const value = String(status || '').toUpperCase();
130
+ if (value === 'CONFIRMS' || value === 'NOT_APPLICABLE') return 'fixed';
131
+ if (value === 'DOES_NOT_CONFIRM' || value === 'PARTIAL' || value === 'NOT_VERIFIED') return 'open';
132
+ return 'unknown';
133
+ }
134
+
135
+ function fileRefFromFinding(finding) {
136
+ if (!finding || !finding.file) return null;
137
+ return finding.line ? `${finding.file}:${finding.line}` : String(finding.file);
138
+ }
139
+
140
+ function titleFromFinding(report, finding) {
141
+ const parts = [
142
+ finding.kind || 'verification',
143
+ finding.status || report.verdict || 'UNKNOWN'
144
+ ];
145
+ if (finding.claim_id) parts.push(`claim ${finding.claim_id}`);
146
+ return parts.join(' ');
147
+ }
148
+
149
+ function isRetroRelevantFinding(finding) {
150
+ const status = String((finding && finding.status) || '').toUpperCase();
151
+ return status === 'DOES_NOT_CONFIRM' || status === 'PARTIAL' || status === 'NOT_VERIFIED';
152
+ }
153
+
154
+ function reportFindings({ report, slug, sourcePath, sourceDate, makeFinding }) {
155
+ const findings = Array.isArray(report.findings) ? report.findings : [];
156
+ return findings.filter(isRetroRelevantFinding).map((finding, index) => makeFinding({
157
+ source_type: 'verification_report',
158
+ feature_slug: slug,
159
+ finding_id: finding.id ? String(finding.id).slice(0, 80) : `VR-${index + 1}`,
160
+ severity: severityFromVerification(finding.severity),
161
+ title: titleFromFinding(report, finding),
162
+ file_ref: fileRefFromFinding(finding),
163
+ date: sourceDate,
164
+ status: statusFromVerification(finding.status),
165
+ source_path: sourcePath,
166
+ signature: null
167
+ }));
168
+ }
169
+
170
+ function readVerificationReports({ rootDir, slug, locations, makeFinding, relPath }) {
171
+ const warnings = [];
172
+ const findings = [];
173
+ const files = collectReportFiles(locations.featureDirs);
174
+ const countedFiles = [];
175
+ const seenReports = new Map();
176
+ let count = 0;
177
+
178
+ for (const full of files) {
179
+ const sourcePath = relPath(rootDir, full);
180
+ const text = readTextSafe(full);
181
+ if (text === null) {
182
+ warnings.push(`verification_report ilegível: ${sourcePath}`);
183
+ continue;
184
+ }
185
+
186
+ const parsed = extractMachineReport(text);
187
+ if (!parsed.ok) {
188
+ warnings.push(`verification_report ignorado (${parsed.reason}): ${sourcePath}`);
189
+ continue;
190
+ }
191
+
192
+ const errors = validateVerificationReport(parsed.report, {
193
+ slug,
194
+ requestedPolicy: parsed.report.policy || 'standard'
195
+ });
196
+ if (errors.length > 0) {
197
+ const compact = errors.map((error) => `${error.field}:${error.reason}`).join(', ');
198
+ warnings.push(`verification_report inválido (${compact}): ${sourcePath}`);
199
+ continue;
200
+ }
201
+
202
+ const identity = reportIdentity(parsed.report);
203
+ if (shouldSkipPromotedDuplicate(seenReports, identity, full)) continue;
204
+ countedFiles.push(full);
205
+ count += 1;
206
+ findings.push(...reportFindings({
207
+ report: parsed.report,
208
+ slug,
209
+ sourcePath,
210
+ sourceDate: timestampFromPath(full),
211
+ makeFinding
212
+ }));
213
+ }
214
+
215
+ return { findings, warnings, count, files: countedFiles };
216
+ }
217
+
218
+ module.exports = {
219
+ readVerificationReports,
220
+ _internal: {
221
+ collectReportFiles,
222
+ extractMachineReport,
223
+ reportIdentity,
224
+ shouldSkipPromotedDuplicate,
225
+ severityFromVerification,
226
+ statusFromVerification,
227
+ titleFromFinding,
228
+ isRetroRelevantFinding
229
+ }
230
+ };
package/src/parser.js CHANGED
@@ -38,6 +38,12 @@ function parseArgv(argv) {
38
38
  'flatten', 'no-index',
39
39
  // harness:validate — pure boolean; `--no-diff .` must not swallow the path.
40
40
  'no-diff',
41
+ // audit:code / harness:check — pure booleans; `--changed .` / `--strict .`
42
+ // must not swallow the trailing path positional.
43
+ 'changed', 'strict',
44
+ // verify:artifact — pure booleans; `--advisory .` / `--no-build .` must
45
+ // not swallow the path positional.
46
+ 'advisory', 'no-build',
41
47
  // `--resume` alone means "resume last"; `--resume=<id>` carries a value
42
48
  // and is handled by the `=` branch above. Without this entry, `--resume`
43
49
  // followed by `--tool=claude` would swallow the next token as its value.
@@ -257,11 +257,11 @@ async function scanArtifacts(targetDir, slug) {
257
257
  : [path.join(dir, 'readiness.md')];
258
258
 
259
259
  const results = {
260
- project_context: await check('project.context', path.join(dir, 'project.context.md')),
261
- prd: slug ? await check('prd', path.join(dir, `prd-${slug}.md`)) : { exists: false },
262
- sheldon_enrichment: slug ? await check('sheldon', path.join(dir, `sheldon-enrichment-${slug}.md`)) : { exists: false },
263
- sheldon_validation: slug ? await check('sheldon-validation', path.join(dir, `sheldon-validation-${slug}.md`)) : { exists: false },
264
- requirements: slug ? await check('requirements', path.join(dir, `requirements-${slug}.md`)) : { exists: false },
260
+ project_context: await check('project.context', path.join(dir, 'project.context.md')),
261
+ prd: slug ? await check('prd', path.join(dir, `prd-${slug}.md`)) : { exists: false },
262
+ sheldon_enrichment: slug ? await check('sheldon', path.join(dir, `sheldon-enrichment-${slug}.md`)) : { exists: false },
263
+ sheldon_validation: slug ? await check('sheldon-validation', path.join(dir, `sheldon-validation-${slug}.md`)) : { exists: false },
264
+ requirements: slug ? await check('requirements', path.join(dir, `requirements-${slug}.md`)) : { exists: false },
265
265
  spec: slug ? await check('spec', path.join(dir, `spec-${slug}.md`)) : await check('spec', path.join(dir, 'spec.md')),
266
266
  architecture: await check('architecture', path.join(dir, 'architecture.md')),
267
267
  design_doc: await checkFirst('design-doc', designDocCandidates),
@@ -480,11 +480,11 @@ function buildContextPackage(agent, slug, classification, artifacts, devState, m
480
480
  const downstreamAgents = ['discovery-design-doc', 'pm', 'orchestrator', 'dev', 'deyvin', 'qa'];
481
481
  const shouldCarryFullFeatureContext = downstreamAgents.includes(agent);
482
482
 
483
- if (shouldCarryFullFeatureContext && artifacts.prd.exists) pkg.push(artifacts.prd.path);
484
- if (shouldCarryFullFeatureContext && artifacts.sheldon_enrichment.exists) pkg.push(artifacts.sheldon_enrichment.path);
485
- if (shouldCarryFullFeatureContext && artifacts.sheldon_validation && artifacts.sheldon_validation.exists) {
486
- pkg.push(artifacts.sheldon_validation.path);
487
- }
483
+ if (shouldCarryFullFeatureContext && artifacts.prd.exists) pkg.push(artifacts.prd.path);
484
+ if (shouldCarryFullFeatureContext && artifacts.sheldon_enrichment.exists) pkg.push(artifacts.sheldon_enrichment.path);
485
+ if (shouldCarryFullFeatureContext && artifacts.sheldon_validation && artifacts.sheldon_validation.exists) {
486
+ pkg.push(artifacts.sheldon_validation.path);
487
+ }
488
488
  if ((shouldCarryFullFeatureContext || ['analyst', 'architect'].includes(agent)) && artifacts.requirements.exists) {
489
489
  pkg.push(artifacts.requirements.path);
490
490
  }
@@ -685,10 +685,10 @@ function evaluateReadiness(artifacts, phaseGates, classification, agent, devStat
685
685
  if (!artifacts.spec.exists) blockers.push('spec file missing');
686
686
  if (classification && classification !== 'MICRO') {
687
687
  if (!designDoc.exists) {
688
- blockers.push('design-doc.md missing — @discovery-design-doc must run before implementation');
688
+ blockers.push('design-doc.md missing — @architect (merged mode) or @sheldon must produce it before implementation');
689
689
  }
690
690
  if (!readiness.exists) {
691
- blockers.push('readiness.md missing — @discovery-design-doc must run before implementation');
691
+ blockers.push('readiness.md missing — @architect (merged mode) or @sheldon must produce it before implementation');
692
692
  }
693
693
  }
694
694
  if (classification === 'MEDIUM') {
@@ -1567,15 +1567,19 @@ function startRun(db, options) {
1567
1567
  finished_at: status === 'completed' || status === 'failed' ? now : null
1568
1568
  });
1569
1569
 
1570
- appendRunEvent(db, {
1571
- runKey,
1572
- eventType: String(options.eventType || 'start'),
1573
- phase: options.phase || 'run',
1574
- status,
1575
- message: String(options.message || options.title || 'Agent started'),
1576
- payload: options.payload,
1577
- createdAt: now
1578
- });
1570
+ appendRunEvent(db, {
1571
+ runKey,
1572
+ eventType: String(options.eventType || 'start'),
1573
+ phase: options.phase || 'run',
1574
+ status,
1575
+ toolName: options.toolName,
1576
+ message: String(options.message || options.title || 'Agent started'),
1577
+ payload: options.payload,
1578
+ verdict: options.verdict,
1579
+ tokenCount: options.tokenCount,
1580
+ progressPct: options.progressPct,
1581
+ createdAt: now
1582
+ });
1579
1583
 
1580
1584
  return runKey;
1581
1585
  }
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { execFileSync } = require('node:child_process');
6
+
7
+ const { resolveInsideRoot } = require('./path-policy');
8
+ const { createCounter, redactText, redactJson, totalRedactions } = require('./redaction');
9
+
10
+ const ARTIFACT_PREVIEW_CHARS = 700;
11
+ const ARTIFACT_PREVIEW_TOTAL_CHARS = 8000;
12
+ const ARTIFACT_SUMMARY_LIMIT = 60;
13
+
14
+ function runGit(rootDir, args) {
15
+ try {
16
+ return {
17
+ ok: true,
18
+ output: execFileSync('git', args, {
19
+ cwd: rootDir,
20
+ encoding: 'utf8',
21
+ stdio: ['ignore', 'pipe', 'pipe']
22
+ }).trim()
23
+ };
24
+ } catch (error) {
25
+ return {
26
+ ok: false,
27
+ output: '',
28
+ error: error.message
29
+ };
30
+ }
31
+ }
32
+
33
+ async function fileExists(rootDir, relPath) {
34
+ const safe = resolveInsideRoot(rootDir, relPath);
35
+ if (!safe.ok) return false;
36
+ try {
37
+ const stat = await fs.stat(safe.path);
38
+ return stat.isFile() || stat.isDirectory();
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ async function fileStat(rootDir, relPath) {
45
+ const safe = resolveInsideRoot(rootDir, relPath);
46
+ if (!safe.ok) return { ok: false, reason: safe.reason };
47
+ try {
48
+ const stat = await fs.stat(safe.path);
49
+ return { ok: true, path: safe.path, stat };
50
+ } catch {
51
+ return { ok: false, reason: 'not_found' };
52
+ }
53
+ }
54
+
55
+ function evidencePathsFromLedger(ledger) {
56
+ if (!ledger || !Array.isArray(ledger.claims)) return [];
57
+ const paths = [];
58
+ for (const claim of ledger.claims) {
59
+ if (!claim || !Array.isArray(claim.evidence)) continue;
60
+ for (const evidence of claim.evidence) {
61
+ if (evidence && evidence.path) paths.push(evidence.path);
62
+ }
63
+ }
64
+ return [...new Set(paths)];
65
+ }
66
+
67
+ async function knownChecks(rootDir, slug, ledger, policy = 'standard') {
68
+ const checks = [];
69
+ const addCheck = (candidate) => {
70
+ if (!candidate || !candidate.command) return;
71
+ const existing = checks.find((check) => check.command === candidate.command);
72
+ if (!existing) {
73
+ checks.push(candidate);
74
+ return;
75
+ }
76
+ existing.required = Boolean(existing.required || candidate.required);
77
+ existing.source = existing.source === candidate.source
78
+ ? existing.source
79
+ : `${existing.source},${candidate.source}`;
80
+ existing.last_status = candidate.last_status || existing.last_status || null;
81
+ };
82
+
83
+ if (await fileExists(rootDir, 'package.json')) {
84
+ addCheck({ command: 'npm test', source: 'package.json', required: false });
85
+ }
86
+ if (await fileExists(rootDir, 'scripts/check-js.js')) {
87
+ addCheck({ command: 'node scripts/check-js.js', source: 'scripts/check-js.js', required: false });
88
+ }
89
+ if (await fileExists(rootDir, `.aioson/context/prd-${slug}.md`)) {
90
+ const strictFlag = policy === 'strict' ? ' --strict' : '';
91
+ addCheck({ command: `aioson prototype:check . --feature=${slug}${strictFlag}`, source: 'prototype_contract', required: false });
92
+ }
93
+ if (await fileExists(rootDir, `.aioson/plans/${slug}/harness-contract.json`)) {
94
+ addCheck({ command: `aioson harness:check . --slug=${slug}`, source: 'harness_contract', required: false });
95
+ }
96
+ if (ledger && Array.isArray(ledger.verification_commands)) {
97
+ for (const item of ledger.verification_commands) {
98
+ if (item && item.command) {
99
+ addCheck({
100
+ command: item.command,
101
+ source: 'ledger',
102
+ required: Boolean(item.required),
103
+ last_status: item.last_status || null
104
+ });
105
+ }
106
+ }
107
+ }
108
+ return checks;
109
+ }
110
+
111
+ function commandPlan(commands) {
112
+ return (commands || []).map((command, index) => ({
113
+ order: index + 1,
114
+ command: command.command,
115
+ required: Boolean(command.required),
116
+ source: command.source || 'unknown',
117
+ last_status: command.last_status || null,
118
+ run_by: 'developer_or_clean_auditor',
119
+ expectation: command.required ? 'must be run or explicitly justified' : 'run when relevant to touched surface'
120
+ }));
121
+ }
122
+
123
+ function dirtyWorktree(statusOutput) {
124
+ return Boolean(String(statusOutput || '').trim());
125
+ }
126
+
127
+ function artifactCanPreview(relPath) {
128
+ return /\.(md|txt|json|js|ts|tsx|jsx|html|css|yaml|yml)$/i.test(String(relPath || ''));
129
+ }
130
+
131
+ async function artifactSummaries(rootDir, artifacts, redactionCounter) {
132
+ const summaries = [];
133
+ let usedPreviewChars = 0;
134
+
135
+ for (const artifact of (artifacts || []).slice(0, ARTIFACT_SUMMARY_LIMIT)) {
136
+ const relPath = artifact.path;
137
+ const stat = await fileStat(rootDir, relPath);
138
+ const summary = {
139
+ type: artifact.type,
140
+ role: artifact.role,
141
+ path: relPath,
142
+ exists: stat.ok,
143
+ size_bytes: stat.ok ? stat.stat.size : null
144
+ };
145
+
146
+ if (!stat.ok) {
147
+ summary.omitted_reason = stat.reason;
148
+ summaries.push(summary);
149
+ continue;
150
+ }
151
+ if (!stat.stat.isFile()) {
152
+ summary.omitted_reason = 'not_file';
153
+ summaries.push(summary);
154
+ continue;
155
+ }
156
+ if (!artifactCanPreview(relPath)) {
157
+ summary.omitted_reason = 'unsupported_preview_type';
158
+ summaries.push(summary);
159
+ continue;
160
+ }
161
+ if (usedPreviewChars >= ARTIFACT_PREVIEW_TOTAL_CHARS) {
162
+ summary.omitted_reason = 'preview_budget_exhausted';
163
+ summaries.push(summary);
164
+ continue;
165
+ }
166
+
167
+ const remaining = ARTIFACT_PREVIEW_TOTAL_CHARS - usedPreviewChars;
168
+ const limit = Math.min(ARTIFACT_PREVIEW_CHARS, remaining);
169
+ const handle = await fs.open(stat.path, 'r');
170
+ try {
171
+ const buffer = Buffer.alloc(limit);
172
+ const { bytesRead } = await handle.read(buffer, 0, limit, 0);
173
+ const preview = buffer.subarray(0, bytesRead).toString('utf8');
174
+ usedPreviewChars += preview.length;
175
+ summary.preview = redactText(preview, redactionCounter);
176
+ summary.preview_truncated = stat.stat.size > bytesRead;
177
+ } finally {
178
+ await handle.close();
179
+ }
180
+ summaries.push(summary);
181
+ }
182
+
183
+ if ((artifacts || []).length > ARTIFACT_SUMMARY_LIMIT) {
184
+ summaries.push({
185
+ type: 'budget_notice',
186
+ role: 'artifact_summary',
187
+ path: null,
188
+ exists: false,
189
+ omitted_reason: `artifact_summary_limit_${ARTIFACT_SUMMARY_LIMIT}`,
190
+ omitted_count: artifacts.length - ARTIFACT_SUMMARY_LIMIT
191
+ });
192
+ }
193
+
194
+ return summaries;
195
+ }
196
+
197
+ async function buildEvidenceBundle(rootDir, slug, ledger, sourceArtifacts, policy) {
198
+ const redactionCounter = createCounter();
199
+ const branch = runGit(rootDir, ['rev-parse', '--abbrev-ref', 'HEAD']);
200
+ const status = runGit(rootDir, ['status', '--short']);
201
+ const diffStat = runGit(rootDir, ['diff', '--stat']);
202
+ const artifactPaths = (sourceArtifacts || []).map((artifact) => artifact.path);
203
+ const evidencePaths = evidencePathsFromLedger(ledger);
204
+ const touchedPaths = [...new Set([...artifactPaths, ...evidencePaths])].filter(Boolean).slice(0, 25);
205
+ const recentCommits = touchedPaths.length > 0
206
+ ? runGit(rootDir, ['log', '--oneline', '-n', '5', '--', ...touchedPaths])
207
+ : { ok: true, output: '' };
208
+
209
+ const verificationCommands = redactJson(await knownChecks(rootDir, slug, ledger, policy), redactionCounter);
210
+ const sanitizedLedgerClaims = redactJson(ledger && Array.isArray(ledger.claims) ? ledger.claims : [], redactionCounter);
211
+ const sanitizedKnownGaps = redactJson(ledger && Array.isArray(ledger.known_gaps) ? ledger.known_gaps : [], redactionCounter);
212
+ const summaries = await artifactSummaries(rootDir, sourceArtifacts || [], redactionCounter);
213
+ const git = {
214
+ branch: branch.ok ? redactText(branch.output, redactionCounter) : null,
215
+ branch_error: branch.ok ? null : branch.error,
216
+ status: status.ok ? redactText(status.output, redactionCounter) : null,
217
+ dirty_worktree: status.ok ? dirtyWorktree(status.output) : null,
218
+ status_error: status.ok ? null : status.error,
219
+ diff_stat: diffStat.ok ? redactText(diffStat.output, redactionCounter) : null,
220
+ diff_stat_error: diffStat.ok ? null : diffStat.error,
221
+ recent_commits: recentCommits.ok ? redactText(recentCommits.output, redactionCounter) : null,
222
+ recent_commits_error: recentCommits.ok ? null : recentCommits.error
223
+ };
224
+
225
+ return {
226
+ feature_slug: slug,
227
+ policy,
228
+ git,
229
+ source_artifacts: sourceArtifacts || [],
230
+ artifact_summaries: summaries,
231
+ ledger_claims: sanitizedLedgerClaims,
232
+ known_gaps: sanitizedKnownGaps,
233
+ verification_commands: verificationCommands,
234
+ command_plan: commandPlan(verificationCommands),
235
+ redactions: {
236
+ ...redactionCounter,
237
+ total: totalRedactions(redactionCounter)
238
+ },
239
+ preview_budget: {
240
+ artifact_preview_chars: ARTIFACT_PREVIEW_CHARS,
241
+ artifact_preview_total_chars: ARTIFACT_PREVIEW_TOTAL_CHARS,
242
+ artifact_summary_limit: ARTIFACT_SUMMARY_LIMIT
243
+ },
244
+ generated_at: new Date().toISOString(),
245
+ project_root: path.resolve(rootDir)
246
+ };
247
+ }
248
+
249
+ module.exports = {
250
+ buildEvidenceBundle
251
+ };