@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.
- package/CHANGELOG.md +27 -0
- package/README.md +19 -6
- package/docs/en/5-reference/cli-reference.md +1 -1
- package/docs/pt/5-referencia/comandos-cli.md +1 -1
- package/docs/pt/5-referencia/harness-retro.md +2 -1
- package/package.json +1 -1
- package/src/cli.js +38 -21
- package/src/commands/classify.js +389 -327
- package/src/commands/harness-retro-promote.js +387 -0
- package/src/commands/prototype-check.js +163 -0
- package/src/commands/verify-implementation.js +428 -0
- package/src/commands/workflow-next.js +222 -9
- package/src/constants.js +2 -0
- package/src/lib/retro/retro-render.js +10 -1
- package/src/lib/retro/retro-sources.js +45 -27
- package/src/lib/retro/verification-reports.js +230 -0
- package/src/runtime-store.js +13 -9
- package/src/verification/evidence-bundle.js +251 -0
- package/src/verification/ledger-store.js +221 -0
- package/src/verification/path-policy.js +74 -0
- package/src/verification/policy-engine.js +95 -0
- package/src/verification/prompt-package.js +314 -0
- package/src/verification/redaction.js +77 -0
- package/src/verification/report-parser.js +132 -0
- package/src/verification/report-store.js +97 -0
- package/src/verification/result.js +16 -0
- package/src/verification/runners/index.js +319 -0
- package/src/verification/runtime-telemetry.js +144 -0
- package/src/verification/schema.js +276 -0
- package/src/verification/source-discovery.js +153 -0
- package/template/.aioson/agents/analyst.md +9 -6
- package/template/.aioson/agents/briefing-refiner.md +22 -0
- package/template/.aioson/agents/briefing.md +69 -12
- package/template/.aioson/agents/design-hybrid-forge.md +18 -14
- package/template/.aioson/agents/dev.md +23 -10
- package/template/.aioson/agents/deyvin.md +3 -2
- package/template/.aioson/agents/product.md +15 -11
- package/template/.aioson/agents/qa.md +13 -4
- package/template/.aioson/agents/scope-check.md +19 -5
- package/template/.aioson/agents/sheldon.md +4 -3
- package/template/.aioson/agents/ux-ui.md +3 -2
- package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
- package/template/.aioson/docs/prototype-contract.md +81 -0
- package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
- package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
- package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
- package/template/.aioson/skills/process/prototype-forge/SKILL.md +92 -0
- 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
|
```
|