@jaimevalasek/aioson 1.30.2 → 1.33.1

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 (49) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +19 -6
  3. package/docs/en/5-reference/cli-reference.md +1 -1
  4. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  5. package/docs/pt/5-referencia/harness-retro.md +2 -1
  6. package/package.json +1 -1
  7. package/src/cli.js +38 -21
  8. package/src/commands/classify.js +389 -327
  9. package/src/commands/harness-retro-promote.js +387 -0
  10. package/src/commands/prototype-check.js +163 -0
  11. package/src/commands/verify-implementation.js +428 -0
  12. package/src/commands/workflow-next.js +222 -9
  13. package/src/constants.js +2 -0
  14. package/src/lib/retro/retro-render.js +10 -1
  15. package/src/lib/retro/retro-sources.js +45 -27
  16. package/src/lib/retro/verification-reports.js +230 -0
  17. package/src/runtime-store.js +13 -9
  18. package/src/verification/evidence-bundle.js +251 -0
  19. package/src/verification/ledger-store.js +221 -0
  20. package/src/verification/path-policy.js +74 -0
  21. package/src/verification/policy-engine.js +95 -0
  22. package/src/verification/prompt-package.js +314 -0
  23. package/src/verification/redaction.js +77 -0
  24. package/src/verification/report-parser.js +132 -0
  25. package/src/verification/report-store.js +97 -0
  26. package/src/verification/result.js +16 -0
  27. package/src/verification/runners/index.js +319 -0
  28. package/src/verification/runtime-telemetry.js +144 -0
  29. package/src/verification/schema.js +276 -0
  30. package/src/verification/source-discovery.js +153 -0
  31. package/template/.aioson/agents/analyst.md +9 -6
  32. package/template/.aioson/agents/briefing-refiner.md +22 -0
  33. package/template/.aioson/agents/briefing.md +69 -12
  34. package/template/.aioson/agents/design-hybrid-forge.md +18 -14
  35. package/template/.aioson/agents/dev.md +23 -10
  36. package/template/.aioson/agents/deyvin.md +3 -2
  37. package/template/.aioson/agents/product.md +15 -11
  38. package/template/.aioson/agents/qa.md +13 -4
  39. package/template/.aioson/agents/scope-check.md +19 -5
  40. package/template/.aioson/agents/sheldon.md +4 -3
  41. package/template/.aioson/agents/ux-ui.md +3 -2
  42. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  43. package/template/.aioson/docs/prototype-contract.md +81 -0
  44. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  45. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  46. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  47. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  48. package/template/.aioson/skills/process/prototype-forge/SKILL.md +92 -0
  49. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Best-effort runtime telemetry for `verify:implementation`.
5
+ *
6
+ * The verifier already stores durable artifacts under
7
+ * `.aioson/context/features/{slug}/verification-runs/`. Runtime telemetry is a
8
+ * query/index layer only: it must never store raw auditor output, stderr,
9
+ * prompt text, or finding evidence.
10
+ */
11
+
12
+ const { openRuntimeDb, startTask, startRun } = require('../runtime-store');
13
+
14
+ const SOURCE = 'verify_implementation';
15
+ const EVENT_TYPE = 'implementation_verification_completed';
16
+ const PHASE = 'implementation_verification';
17
+
18
+ function bool(value) {
19
+ return Boolean(value);
20
+ }
21
+
22
+ function safeString(value) {
23
+ if (value === null || value === undefined) return null;
24
+ const text = String(value).trim();
25
+ return text || null;
26
+ }
27
+
28
+ function safeNumber(value) {
29
+ if (value === null || value === undefined) return null;
30
+ const number = Number(value);
31
+ return Number.isFinite(number) ? number : null;
32
+ }
33
+
34
+ function pathOrNull(value) {
35
+ const text = safeString(value);
36
+ return text ? text.replace(/\\/g, '/') : null;
37
+ }
38
+
39
+ function summarizeRunner(runner) {
40
+ if (!runner || typeof runner !== 'object') return null;
41
+ return {
42
+ status: safeString(runner.status),
43
+ tool_detected: bool(runner.detected),
44
+ permission_mode: safeString(runner.permission_mode),
45
+ destructive_commands_allowed: bool(runner.destructive_commands_allowed),
46
+ timeout_ms: safeNumber(runner.timeout_ms),
47
+ max_output_bytes: safeNumber(runner.max_output_bytes),
48
+ duration_ms: safeNumber(runner.duration_ms),
49
+ exit_code: safeNumber(runner.exit_code),
50
+ signal: safeString(runner.signal),
51
+ output_bytes: safeNumber(runner.output_bytes),
52
+ output_truncated: bool(runner.output_truncated)
53
+ };
54
+ }
55
+
56
+ function buildVerificationTelemetryPayload(result, { durationMs = null } = {}) {
57
+ const runner = summarizeRunner(result.runner);
58
+ const payload = {
59
+ feature_slug: safeString(result.feature_slug),
60
+ mode: safeString(result.mode),
61
+ policy: safeString(result.policy),
62
+ ok: bool(result.ok),
63
+ verdict: safeString(result.verdict),
64
+ recommended_route: safeString(result.recommended_route),
65
+ blocking_findings_count: safeNumber(result.blocking_findings_count),
66
+ reason: safeString(result.reason),
67
+ tool: safeString(result.tool),
68
+ model: safeString(result.model),
69
+ ledger_path: pathOrNull(result.ledger_path),
70
+ prompt_path: pathOrNull(result.prompt_path),
71
+ report_path: pathOrNull(result.report_path),
72
+ run_report_path: pathOrNull(result.run_report_path),
73
+ report_json_path: pathOrNull(result.report_json_path),
74
+ raw_output_stored: bool(result.raw_report_path),
75
+ stderr_stored: bool(result.stderr_path),
76
+ duration_ms: safeNumber(durationMs)
77
+ };
78
+
79
+ if (runner) payload.runner = runner;
80
+ return payload;
81
+ }
82
+
83
+ function runtimeStatus(result) {
84
+ if (!result || result.mode === 'verify-implementation') return 'failed';
85
+ return 'completed';
86
+ }
87
+
88
+ async function recordVerificationTelemetry(rootDir, result, { startedAt = null } = {}) {
89
+ if (!rootDir || !result || typeof result !== 'object') {
90
+ return { emitted: false, reason: 'invalid_input' };
91
+ }
92
+
93
+ try {
94
+ const durationMs = startedAt ? Math.max(0, Date.now() - Number(startedAt)) : null;
95
+ const payload = buildVerificationTelemetryPayload(result, { durationMs });
96
+ const { db } = await openRuntimeDb(rootDir);
97
+ try {
98
+ const status = runtimeStatus(result);
99
+ const title = `Implementation verification: ${payload.feature_slug || 'unknown'}`;
100
+ const taskKey = startTask(db, {
101
+ title,
102
+ taskKind: 'implementation_verification',
103
+ status,
104
+ createdBy: 'verify:implementation',
105
+ metaJson: {
106
+ feature_slug: payload.feature_slug,
107
+ mode: payload.mode,
108
+ policy: payload.policy,
109
+ source: SOURCE
110
+ }
111
+ });
112
+ const runKey = startRun(db, {
113
+ taskKey,
114
+ agentName: 'verify:implementation',
115
+ agentKind: 'system',
116
+ source: SOURCE,
117
+ title: `verify:implementation ${payload.mode || 'unknown'} ${payload.feature_slug || 'unknown'}`,
118
+ status,
119
+ summary: `${payload.verdict || (result.ok ? 'PASS' : 'INCONCLUSIVE')} via ${payload.mode || 'unknown'}`,
120
+ outputPath: payload.report_path || payload.prompt_path || payload.ledger_path || null,
121
+ eventType: EVENT_TYPE,
122
+ phase: PHASE,
123
+ toolName: payload.tool,
124
+ verdict: payload.verdict,
125
+ message: `Implementation verification completed: ${payload.verdict || (result.ok ? 'ok' : 'blocked')}`,
126
+ payload
127
+ });
128
+ return { emitted: true, task_key: taskKey, run_key: runKey, event_type: EVENT_TYPE };
129
+ } finally {
130
+ db.close();
131
+ }
132
+ } catch (err) {
133
+ return { emitted: false, reason: 'telemetry_error', message: err && err.message ? err.message : String(err) };
134
+ }
135
+ }
136
+
137
+ module.exports = {
138
+ SOURCE,
139
+ EVENT_TYPE,
140
+ PHASE,
141
+ buildVerificationTelemetryPayload,
142
+ recordVerificationTelemetry
143
+ };
144
+
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const { resolveInsideRoot } = require('./path-policy');
4
+
5
+ const LEDGER_SCHEMA_VERSION = 'implementation-ledger/v1';
6
+ const REPORT_SCHEMA_VERSION = 'verification-report/v1';
7
+
8
+ const POLICIES = new Set(['advisory', 'standard', 'strict']);
9
+
10
+ const CLAIM_STATUSES = new Set([
11
+ 'planned',
12
+ 'implemented',
13
+ 'partial',
14
+ 'blocked',
15
+ 'not_applicable'
16
+ ]);
17
+
18
+ const CLAIM_KINDS = new Set([
19
+ 'required_behavior',
20
+ 'acceptance_criterion',
21
+ 'test_coverage',
22
+ 'scope_constraint',
23
+ 'security_constraint',
24
+ 'prototype_contract',
25
+ 'migration_or_data'
26
+ ]);
27
+
28
+ const OWNERS = new Set([
29
+ 'dev',
30
+ 'deyvin',
31
+ 'product',
32
+ 'sheldon',
33
+ 'architect',
34
+ 'ux-ui',
35
+ 'qa',
36
+ 'tester',
37
+ 'pentester',
38
+ 'scope-check'
39
+ ]);
40
+
41
+ const VERDICTS = new Set([
42
+ 'PASS',
43
+ 'NEEDS_DEV_FIX',
44
+ 'NEEDS_SCOPE_DECISION',
45
+ 'NEEDS_QA_RECHECK',
46
+ 'NEEDS_SECURITY_REVIEW',
47
+ 'INCONCLUSIVE'
48
+ ]);
49
+
50
+ const FINDING_STATUSES = new Set([
51
+ 'CONFIRMS',
52
+ 'DOES_NOT_CONFIRM',
53
+ 'PARTIAL',
54
+ 'NOT_VERIFIED',
55
+ 'NOT_APPLICABLE'
56
+ ]);
57
+
58
+ const SEVERITIES = new Set([
59
+ 'info',
60
+ 'warning',
61
+ 'blocking'
62
+ ]);
63
+
64
+ function normalizePolicy(policy) {
65
+ const value = String(policy || 'standard').toLowerCase();
66
+ return POLICIES.has(value) ? value : null;
67
+ }
68
+
69
+ function validatePathInsideRoot(rootDir, relPath, field, errors) {
70
+ if (!relPath) {
71
+ errors.push({ field, reason: 'missing' });
72
+ return;
73
+ }
74
+ const safe = resolveInsideRoot(rootDir, relPath);
75
+ if (!safe.ok) errors.push({ field, reason: safe.reason });
76
+ }
77
+
78
+ function validateSourceArtifacts(rootDir, artifacts, errors) {
79
+ if (!Array.isArray(artifacts)) {
80
+ errors.push({ field: 'source_artifacts', reason: 'must_be_array' });
81
+ return;
82
+ }
83
+ for (const [index, artifact] of artifacts.entries()) {
84
+ if (!artifact || typeof artifact !== 'object') {
85
+ errors.push({ field: `source_artifacts[${index}]`, reason: 'must_be_object' });
86
+ continue;
87
+ }
88
+ if (!artifact.type) errors.push({ field: `source_artifacts[${index}].type`, reason: 'missing' });
89
+ validatePathInsideRoot(rootDir, artifact.path, `source_artifacts[${index}].path`, errors);
90
+ }
91
+ }
92
+
93
+ function validateClaims(rootDir, claims, errors) {
94
+ if (!Array.isArray(claims)) {
95
+ errors.push({ field: 'claims', reason: 'must_be_array' });
96
+ return;
97
+ }
98
+
99
+ for (const [index, claim] of claims.entries()) {
100
+ if (!claim || typeof claim !== 'object') {
101
+ errors.push({ field: `claims[${index}]`, reason: 'must_be_object' });
102
+ continue;
103
+ }
104
+ if (!claim.id) errors.push({ field: `claims[${index}].id`, reason: 'missing' });
105
+ if (!claim.summary) errors.push({ field: `claims[${index}].summary`, reason: 'missing' });
106
+ if (!CLAIM_KINDS.has(claim.kind)) errors.push({ field: `claims[${index}].kind`, reason: 'invalid' });
107
+ if (!OWNERS.has(claim.owner)) errors.push({ field: `claims[${index}].owner`, reason: 'invalid' });
108
+ if (!CLAIM_STATUSES.has(claim.status)) errors.push({ field: `claims[${index}].status`, reason: 'invalid' });
109
+ if (!Array.isArray(claim.evidence)) {
110
+ errors.push({ field: `claims[${index}].evidence`, reason: 'must_be_array' });
111
+ continue;
112
+ }
113
+ for (const [evidenceIndex, evidence] of claim.evidence.entries()) {
114
+ if (evidence && evidence.path) {
115
+ validatePathInsideRoot(
116
+ rootDir,
117
+ evidence.path,
118
+ `claims[${index}].evidence[${evidenceIndex}].path`,
119
+ errors
120
+ );
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ function validateLedgerVerificationCommands(commands, errors) {
127
+ if (!Array.isArray(commands)) {
128
+ errors.push({ field: 'verification_commands', reason: 'must_be_array' });
129
+ return;
130
+ }
131
+ for (const [index, command] of commands.entries()) {
132
+ if (!command || typeof command !== 'object') {
133
+ errors.push({ field: `verification_commands[${index}]`, reason: 'must_be_object' });
134
+ continue;
135
+ }
136
+ if (!command.command) errors.push({ field: `verification_commands[${index}].command`, reason: 'missing' });
137
+ }
138
+ }
139
+
140
+ function validateImplementationLedger(ledger, { rootDir, slug }) {
141
+ const errors = [];
142
+ if (!ledger || typeof ledger !== 'object') {
143
+ return [{ field: 'machine_ledger', reason: 'must_be_object' }];
144
+ }
145
+ if (ledger.schema_version !== LEDGER_SCHEMA_VERSION) {
146
+ errors.push({ field: 'schema_version', reason: 'invalid' });
147
+ }
148
+ if (ledger.feature_slug !== slug) {
149
+ errors.push({ field: 'feature_slug', reason: 'mismatch' });
150
+ }
151
+ validateSourceArtifacts(rootDir, ledger.source_artifacts, errors);
152
+ validateClaims(rootDir, ledger.claims, errors);
153
+ validateLedgerVerificationCommands(ledger.verification_commands, errors);
154
+ if (!Array.isArray(ledger.known_gaps)) {
155
+ errors.push({ field: 'known_gaps', reason: 'must_be_array' });
156
+ }
157
+ return errors;
158
+ }
159
+
160
+ function missingLedgerEvidence(ledger) {
161
+ const claims = Array.isArray(ledger && ledger.claims) ? ledger.claims : [];
162
+ return claims
163
+ .filter((claim) => claim && ['implemented', 'partial'].includes(claim.status))
164
+ .filter((claim) => !Array.isArray(claim.evidence) || claim.evidence.length === 0)
165
+ .map((claim) => claim.id || '(missing id)');
166
+ }
167
+
168
+ function validateReportCommands(commands, errors) {
169
+ if (!Array.isArray(commands)) {
170
+ errors.push({ field: 'commands_run', reason: 'must_be_array' });
171
+ return;
172
+ }
173
+ for (const [index, command] of commands.entries()) {
174
+ if (!command || typeof command !== 'object') {
175
+ errors.push({ field: `commands_run[${index}]`, reason: 'must_be_object' });
176
+ continue;
177
+ }
178
+ if (!command.command) errors.push({ field: `commands_run[${index}].command`, reason: 'missing' });
179
+ }
180
+ }
181
+
182
+ function validateFindings(findings, errors) {
183
+ if (!Array.isArray(findings)) {
184
+ errors.push({ field: 'findings', reason: 'must_be_array' });
185
+ return;
186
+ }
187
+ for (const [index, finding] of findings.entries()) {
188
+ if (!finding || typeof finding !== 'object') {
189
+ errors.push({ field: `findings[${index}]`, reason: 'must_be_object' });
190
+ continue;
191
+ }
192
+ if (!finding.id) errors.push({ field: `findings[${index}].id`, reason: 'missing' });
193
+ if (!FINDING_STATUSES.has(finding.status)) errors.push({ field: `findings[${index}].status`, reason: 'invalid' });
194
+ if (!SEVERITIES.has(finding.severity)) errors.push({ field: `findings[${index}].severity`, reason: 'invalid' });
195
+ if (!OWNERS.has(finding.owner)) errors.push({ field: `findings[${index}].owner`, reason: 'invalid' });
196
+ if (finding.kind && !CLAIM_KINDS.has(finding.kind)) errors.push({ field: `findings[${index}].kind`, reason: 'invalid' });
197
+ if (finding.recommended_route && !OWNERS.has(finding.recommended_route)) {
198
+ errors.push({ field: `findings[${index}].recommended_route`, reason: 'invalid' });
199
+ }
200
+ }
201
+ }
202
+
203
+ function validateVerificationReport(report, { slug, requestedPolicy }) {
204
+ const errors = [];
205
+ if (!report || typeof report !== 'object') {
206
+ return [{ field: 'machine_report', reason: 'must_be_object' }];
207
+ }
208
+ if (report.schema_version !== REPORT_SCHEMA_VERSION) {
209
+ errors.push({ field: 'schema_version', reason: 'invalid' });
210
+ }
211
+ if (report.feature_slug !== slug) {
212
+ errors.push({ field: 'feature_slug', reason: 'mismatch' });
213
+ }
214
+ if (!normalizePolicy(requestedPolicy || report.policy)) {
215
+ errors.push({ field: 'policy', reason: 'invalid' });
216
+ }
217
+ if (!VERDICTS.has(report.verdict)) {
218
+ errors.push({ field: 'verdict', reason: 'invalid' });
219
+ }
220
+ if (report.recommended_route && !OWNERS.has(report.recommended_route)) {
221
+ errors.push({ field: 'recommended_route', reason: 'invalid' });
222
+ }
223
+ validateReportCommands(report.commands_run, errors);
224
+ validateFindings(report.findings, errors);
225
+ return errors;
226
+ }
227
+
228
+ function machineReportSchemaExample(slug, policy) {
229
+ return {
230
+ schema_version: REPORT_SCHEMA_VERSION,
231
+ feature_slug: slug,
232
+ policy,
233
+ verdict: 'PASS|NEEDS_DEV_FIX|NEEDS_SCOPE_DECISION|NEEDS_QA_RECHECK|NEEDS_SECURITY_REVIEW|INCONCLUSIVE',
234
+ summary: 'Short evidence-grounded summary.',
235
+ commands_run: [
236
+ {
237
+ command: 'npm test',
238
+ status: 'passed|failed|not_run',
239
+ evidence: 'Short result summary'
240
+ }
241
+ ],
242
+ findings: [
243
+ {
244
+ id: 'FIND-001',
245
+ claim_id: 'CLAIM-001',
246
+ kind: 'required_behavior',
247
+ status: 'CONFIRMS|DOES_NOT_CONFIRM|PARTIAL|NOT_VERIFIED|NOT_APPLICABLE',
248
+ severity: 'info|warning|blocking',
249
+ owner: 'dev',
250
+ file: 'src/example.js',
251
+ line: 1,
252
+ evidence: 'Exact reason with file:line when available.',
253
+ recommended_route: 'dev'
254
+ }
255
+ ],
256
+ recommended_route: 'qa',
257
+ blocking_findings_count: 0
258
+ };
259
+ }
260
+
261
+ module.exports = {
262
+ LEDGER_SCHEMA_VERSION,
263
+ REPORT_SCHEMA_VERSION,
264
+ POLICIES,
265
+ CLAIM_STATUSES,
266
+ CLAIM_KINDS,
267
+ OWNERS,
268
+ VERDICTS,
269
+ FINDING_STATUSES,
270
+ SEVERITIES,
271
+ normalizePolicy,
272
+ validateImplementationLedger,
273
+ missingLedgerEvidence,
274
+ validateVerificationReport,
275
+ machineReportSchemaExample
276
+ };
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+
6
+ const { resolveInsideRoot, relativeFromRoot, toPosixPath } = require('./path-policy');
7
+
8
+ async function fileExists(filePath) {
9
+ try {
10
+ const stat = await fs.stat(filePath);
11
+ return stat.isFile() || stat.isDirectory();
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ async function listDirSafe(dir) {
18
+ try {
19
+ return await fs.readdir(dir, { withFileTypes: true });
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ async function walkFiles(rootDir, relativeDir, predicate, limit = 100) {
26
+ const start = path.join(rootDir, relativeDir);
27
+ const found = [];
28
+
29
+ async function walk(absDir) {
30
+ if (found.length >= limit) return;
31
+ const entries = await listDirSafe(absDir);
32
+ for (const entry of entries) {
33
+ if (found.length >= limit) break;
34
+ const full = path.join(absDir, entry.name);
35
+ if (entry.isDirectory()) {
36
+ await walk(full);
37
+ } else if (entry.isFile() && predicate(entry.name, full)) {
38
+ found.push(relativeFromRoot(rootDir, full));
39
+ }
40
+ }
41
+ }
42
+
43
+ if (await fileExists(start)) {
44
+ await walk(start);
45
+ }
46
+ return found;
47
+ }
48
+
49
+ function addArtifact(artifacts, seen, artifact) {
50
+ if (!artifact || !artifact.path) return;
51
+ const key = `${artifact.type}:${toPosixPath(artifact.path)}`;
52
+ if (seen.has(key)) return;
53
+ seen.add(key);
54
+ artifacts.push({
55
+ type: artifact.type,
56
+ path: toPosixPath(artifact.path),
57
+ role: artifact.role || 'source'
58
+ });
59
+ }
60
+
61
+ async function addIfExists(rootDir, artifacts, seen, type, relPath, role = 'source') {
62
+ const safe = resolveInsideRoot(rootDir, relPath);
63
+ if (!safe.ok) return;
64
+ if (await fileExists(safe.path)) {
65
+ addArtifact(artifacts, seen, { type, path: safe.relative_path, role });
66
+ }
67
+ }
68
+
69
+ function ledgerSourceArtifacts(ledger) {
70
+ if (!ledger || !Array.isArray(ledger.source_artifacts)) return [];
71
+ return ledger.source_artifacts
72
+ .filter((artifact) => artifact && artifact.path)
73
+ .map((artifact) => ({
74
+ type: artifact.type || 'ledger_source',
75
+ path: artifact.path,
76
+ role: artifact.role || 'ledger_declared'
77
+ }));
78
+ }
79
+
80
+ function ledgerEvidenceFiles(ledger) {
81
+ if (!ledger || !Array.isArray(ledger.claims)) return [];
82
+ const files = [];
83
+ for (const claim of ledger.claims) {
84
+ if (!claim || !Array.isArray(claim.evidence)) continue;
85
+ for (const evidence of claim.evidence) {
86
+ if (evidence && evidence.path) {
87
+ files.push({
88
+ type: 'implementation_evidence',
89
+ path: evidence.path,
90
+ role: claim.id ? `claim:${claim.id}` : 'claim'
91
+ });
92
+ }
93
+ }
94
+ }
95
+ return files;
96
+ }
97
+
98
+ async function discoverSourceArtifacts(rootDir, slug, ledger = null) {
99
+ const artifacts = [];
100
+ const seen = new Set();
101
+
102
+ await addIfExists(rootDir, artifacts, seen, 'prd', `.aioson/context/prd-${slug}.md`, 'product_authority');
103
+ await addIfExists(rootDir, artifacts, seen, 'requirements', `.aioson/context/requirements-${slug}.md`, 'acceptance_criteria');
104
+ await addIfExists(rootDir, artifacts, seen, 'spec', `.aioson/context/spec-${slug}.md`, 'living_memory');
105
+ await addIfExists(rootDir, artifacts, seen, 'sheldon_enrichment', `.aioson/context/sheldon-enrichment-${slug}.md`, 'product_review');
106
+ await addIfExists(rootDir, artifacts, seen, 'sheldon_validation', `.aioson/context/sheldon-validation-${slug}.md`, 'product_review');
107
+ await addIfExists(rootDir, artifacts, seen, 'design_doc', `.aioson/context/design-doc-${slug}.md`, 'design_authority');
108
+ await addIfExists(rootDir, artifacts, seen, 'readiness', `.aioson/context/readiness-${slug}.md`, 'implementation_readiness');
109
+ await addIfExists(rootDir, artifacts, seen, 'ui_spec', `.aioson/context/ui-spec-${slug}.md`, 'ui_contract');
110
+ await addIfExists(rootDir, artifacts, seen, 'implementation_plan', `.aioson/context/implementation-plan-${slug}.md`, 'execution_plan');
111
+ await addIfExists(rootDir, artifacts, seen, 'simple_plan', `.aioson/context/simple-plans/${slug}.md`, 'simple_plan');
112
+ await addIfExists(rootDir, artifacts, seen, 'scope_check', `.aioson/context/scope-check-${slug}.md`, 'scope_alignment');
113
+ await addIfExists(rootDir, artifacts, seen, 'qa_report', `.aioson/context/qa-report-${slug}.md`, 'qa_findings');
114
+ await addIfExists(rootDir, artifacts, seen, 'security_findings', `.aioson/context/security-findings-${slug}.json`, 'security_findings');
115
+ await addIfExists(rootDir, artifacts, seen, 'dossier', `.aioson/context/features/${slug}/dossier.md`, 'feature_dossier');
116
+ await addIfExists(rootDir, artifacts, seen, 'sheldon_plan', `.aioson/plans/${slug}/manifest.md`, 'phased_plan');
117
+ await addIfExists(rootDir, artifacts, seen, 'harness', `.aioson/plans/${slug}/harness-contract.json`, 'binary_criteria');
118
+ await addIfExists(rootDir, artifacts, seen, 'harness_progress', `.aioson/plans/${slug}/progress.json`, 'binary_criteria_state');
119
+ await addIfExists(rootDir, artifacts, seen, 'harness_output', `.aioson/plans/${slug}/last-check-output.json`, 'binary_criteria_output');
120
+ await addIfExists(rootDir, artifacts, seen, 'prototype', `.aioson/briefings/${slug}/prototype.html`, 'prototype_contract');
121
+ await addIfExists(rootDir, artifacts, seen, 'prototype_manifest', `.aioson/briefings/${slug}/prototype-manifest.md`, 'prototype_contract');
122
+ await addIfExists(rootDir, artifacts, seen, 'prototype_report', `.aioson/briefings/${slug}/prototype-report.md`, 'prototype_contract');
123
+
124
+ const prdFiles = await walkFiles(rootDir, 'prds', (name) => name.includes(slug) && name.endsWith('.md'), 25);
125
+ for (const rel of prdFiles) addArtifact(artifacts, seen, { type: 'source_prd', path: rel, role: 'draft_source' });
126
+
127
+ const rootPlanFiles = await walkFiles(rootDir, 'plans', (name, full) => {
128
+ const rel = relativeFromRoot(rootDir, full);
129
+ return rel.includes(slug);
130
+ }, 50);
131
+ for (const rel of rootPlanFiles) addArtifact(artifacts, seen, { type: 'source_plan', path: rel, role: 'preproduction_source' });
132
+
133
+ const testFiles = await walkFiles(rootDir, 'tests', (name) => name.includes(slug), 50);
134
+ for (const rel of testFiles) addArtifact(artifacts, seen, { type: 'test', path: rel, role: 'verification' });
135
+
136
+ const contextReviewFiles = await walkFiles(rootDir, '.aioson/context', (name) => (
137
+ /^qa-report-/.test(name) && name.includes(slug) && name.endsWith('.md')
138
+ ), 25);
139
+ for (const rel of contextReviewFiles) addArtifact(artifacts, seen, { type: 'qa_report', path: rel, role: 'qa_findings' });
140
+
141
+ for (const artifact of [...ledgerSourceArtifacts(ledger), ...ledgerEvidenceFiles(ledger)]) {
142
+ const safe = resolveInsideRoot(rootDir, artifact.path);
143
+ if (safe.ok && await fileExists(safe.path)) {
144
+ addArtifact(artifacts, seen, { ...artifact, path: safe.relative_path });
145
+ }
146
+ }
147
+
148
+ return artifacts;
149
+ }
150
+
151
+ module.exports = {
152
+ discoverSourceArtifacts
153
+ };
@@ -9,7 +9,7 @@ Evaluate this immediately after reading this file and before loading any other c
9
9
  If the user only activates `@analyst` without naming a feature, PRD, or concrete analysis task:
10
10
 
11
11
  1. When the CLI is available, run `aioson workflow:status .` and `aioson context:select . --agent=analyst --mode=planning --task="agent activation without concrete task" --paths=""`.
12
- 2. Load only: `.aioson/context/project.context.md` and a filename listing of `.aioson/context/prd*.md` / `requirements-*.md` (names only — no contents).
12
+ 2. Load only: `.aioson/context/project.context.md` and a filename listing of `.aioson/context/prd*.md` / `requirements-*.md` (names only — no contents).
13
13
  3. Report the current stage, ask which feature or discovery scope to analyze, and stop.
14
14
 
15
15
  Do NOT load on activation: PRD/requirements contents, `discovery.md`, `spec*.md`, dossiers, scan artifacts, bootstrap files, or skills (including `aioson-spec-driven`). Run the full tool-first preflight only after a concrete task or feature is named.
@@ -75,7 +75,7 @@ If the CLI is not available, compare modification dates manually:
75
75
 
76
76
  ## Mode detection
77
77
 
78
- Resolve the active feature first: run `aioson feature:current . 2>/dev/null` (single source of truth — pulse `active_feature`, else the unique `in_progress` feature). A non-empty slug pins feature mode to that `{slug}`; this disambiguates when several `prd-{slug}.md` files coexist. If it returns `ambiguous: true` (`--json`), ask which feature before loading. Without the CLI, read `active_feature` from `.aioson/context/project-pulse.md`. Then check:
78
+ Resolve the active feature first: run `aioson feature:current . 2>/dev/null` (single source of truth — pulse `active_feature`, else the unique `in_progress` feature). A non-empty slug pins feature mode to that `{slug}`; this disambiguates when several `prd-{slug}.md` files coexist. If it returns `ambiguous: true` (`--json`), ask which feature before loading. Without the CLI, read `active_feature` from `.aioson/context/project-pulse.md`. Then check:
79
79
 
80
80
  **Feature mode** — a `prd-{slug}.md` file exists in `.aioson/context/`:
81
81
  - Read `prd-{slug}.md` to understand the feature scope.
@@ -144,7 +144,7 @@ Run after Sheldon enrichment context check. Check the frontmatter of the PRD bei
144
144
 
145
145
  ## Context integrity
146
146
 
147
- Read `.aioson/context/project.context.md` before starting discovery.
147
+ Read `.aioson/context/project.context.md` before starting discovery.
148
148
 
149
149
  Rules:
150
150
  - If the file is inconsistent with the scope artifacts already present (`prd.md`, `prd-{slug}.md`, `discovery.md`, `spec.md`, `features.md`), fix the objectively inferable metadata inside the workflow before proceeding.
@@ -154,7 +154,7 @@ Rules:
154
154
 
155
155
  ## Brownfield pre-flight
156
156
 
157
- Check `framework_installed` in `.aioson/context/project.context.md` before starting any phase.
157
+ Check `framework_installed` in `.aioson/context/project.context.md` before starting any phase.
158
158
 
159
159
  **If `framework_installed=true` AND `.aioson/context/discovery.md` exists:**
160
160
  - Skip Phases 1–3 below.
@@ -292,6 +292,9 @@ For each new or modified entity, produce field-level detail (same format as Phas
292
292
  ### Output contract — feature mode
293
293
 
294
294
  **`requirements-{slug}.md`** — implementation spec for the feature:
295
+
296
+ > When `prd-{slug}.md` has a `## Prototype reference`, load `.aioson/docs/prototype-contract.md` and turn the prototype's Core screens and interactions into explicit acceptance criteria (e.g. "add card persists and re-renders", "board has a management surface"). This is how the prototype reaches @validator — as binary criteria, not a file it reads. After writing `requirements-{slug}.md`, run `aioson prototype:check . --feature={slug}` and resolve any `fail`/`warn` before handoff — it deterministically verifies every Core interaction in the prototype manifest is echoed by an acceptance criterion.
297
+
295
298
  1. Feature summary (1–2 lines from prd-{slug}.md)
296
299
  2. Requirement IDs (`REQ-{slug}-01...`) with source references
297
300
  3. Acceptance criteria IDs (`AC-{slug}-01...`) mapped to requirement IDs
@@ -379,7 +382,7 @@ Generate `.aioson/context/discovery.md` with the following sections:
379
382
 
380
383
  ## Dev handoff producer
381
384
 
382
- Before the final `agent:epilogue`/`agent:done` call, when the next agent in the workflow is `@dev`, produce `.aioson/context/dev-state.md` so the next `/aioson:agent:dev` session auto-resumes on cold start instead of pinging the user for context:
385
+ Before the final `agent:epilogue`/`agent:done` call, when the next agent in the workflow is `@dev`, produce `.aioson/context/dev-state.md` so the next `/aioson:agent:dev` session auto-resumes on cold start instead of pinging the user for context:
383
386
 
384
387
  ```bash
385
388
  aioson dev:state:write . --feature={slug} --phase=1 \
@@ -389,7 +392,7 @@ aioson dev:state:write . --feature={slug} --phase=1 \
389
392
 
390
393
  `--context` accepts canonical tokens (`prd`, `requirements`, `spec`, `architecture`, `impl-plan`, `sheldon`, `design-doc`, `readiness`, `ui-spec`, `dossier`, `simple-plan`), max 4 entries total; missing files emit a warning and are skipped. Always include the artifacts @dev will need to start the first slice — typically `spec` + `requirements` for SMALL features. Idempotent: re-running with the same args does not duplicate state.
391
394
 
392
- If any workflow stage remains before `@dev` (`@scope-check`, `@architect`, `@discovery-design-doc`, or `@pm`), do not guess the final implementation package here. The last pre-dev stage writes the final `.aioson/context/dev-state.md`; `@analyst` only produces it for direct-to-dev shortcuts.
395
+ If any workflow stage remains before `@dev` (`@scope-check`, `@architect`, `@discovery-design-doc`, or `@pm`), do not guess the final implementation package here. The last pre-dev stage writes the final `.aioson/context/dev-state.md`; `@analyst` only produces it for direct-to-dev shortcuts.
393
396
 
394
397
  **Handoff message:**
395
398
  ```