@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,221 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+
6
+ const {
7
+ LEDGER_SCHEMA_VERSION,
8
+ validateImplementationLedger,
9
+ missingLedgerEvidence,
10
+ makeError
11
+ } = require('./result');
12
+ const {
13
+ featureContextDir,
14
+ verificationRunsDir,
15
+ resolveInsideRoot,
16
+ relativeFromRoot
17
+ } = require('./path-policy');
18
+
19
+ const REQUIRED_LEDGER_SECTIONS = [
20
+ { id: 'source_of_truth', heading: 'Source Of Truth' },
21
+ { id: 'intended_behavior_claims', heading: 'Intended Behavior Claims' },
22
+ { id: 'implementation_evidence', heading: 'Implementation Evidence' },
23
+ { id: 'verification_commands', heading: 'Verification Commands' },
24
+ { id: 'known_gaps', heading: 'Known Gaps' },
25
+ { id: 'handoff_notes', heading: 'Handoff Notes' },
26
+ { id: 'machine_ledger', heading: 'Machine Ledger' }
27
+ ];
28
+
29
+ async function exists(filePath) {
30
+ try {
31
+ await fs.access(filePath);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function sectionPattern(heading) {
39
+ return new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'im');
40
+ }
41
+
42
+ function findMissingSections(content) {
43
+ return REQUIRED_LEDGER_SECTIONS
44
+ .filter((section) => !sectionPattern(section.heading).test(content))
45
+ .map((section) => section.id);
46
+ }
47
+
48
+ function extractJsonBlock(content, heading) {
49
+ const text = String(content || '');
50
+ const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, 'im');
51
+ const headingMatch = headingRe.exec(text);
52
+ let scope = text;
53
+ if (headingMatch) {
54
+ const start = headingMatch.index + headingMatch[0].length;
55
+ const rest = text.slice(start);
56
+ const nextHeading = rest.search(/^##\s+/m);
57
+ scope = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
58
+ }
59
+ const block = scope.match(/```json\s*([\s\S]*?)```/i);
60
+ return block ? block[1].trim() : null;
61
+ }
62
+
63
+ function createLedgerTemplate(slug, sourceArtifacts = []) {
64
+ const machine = {
65
+ schema_version: LEDGER_SCHEMA_VERSION,
66
+ feature_slug: slug,
67
+ source_artifacts: sourceArtifacts.map((artifact) => ({
68
+ type: artifact.type,
69
+ path: artifact.path,
70
+ role: artifact.role || 'source'
71
+ })),
72
+ claims: [],
73
+ known_gaps: [],
74
+ verification_commands: []
75
+ };
76
+
77
+ return `# Implementation Ledger - ${slug}
78
+
79
+ ## Source Of Truth
80
+
81
+ List the PRD, requirements, spec, plan, prototype, harness, or source plan that defines this implementation.
82
+
83
+ ## Intended Behavior Claims
84
+
85
+ List every behavior the implementation claims to satisfy. Use stable claim IDs such as \`CLAIM-001\`.
86
+
87
+ ## Implementation Evidence
88
+
89
+ Link each claim to files, line ranges, tests, commands, or explicit N/A rationale.
90
+
91
+ ## Verification Commands
92
+
93
+ Record commands that should be run by the developer or clean auditor. Include last known status only when actually run.
94
+
95
+ ## Known Gaps
96
+
97
+ Record deferred, blocked, or not-applicable items with an owner and rationale.
98
+
99
+ ## Handoff Notes
100
+
101
+ Notes for @scope-check, @qa, or a clean auditor. This ledger is evidence, not proof.
102
+
103
+ ## Machine Ledger
104
+
105
+ \`\`\`json
106
+ ${JSON.stringify(machine, null, 2)}
107
+ \`\`\`
108
+ `;
109
+ }
110
+
111
+ async function prepareLedger(rootDir, slug, sourceArtifacts = []) {
112
+ const featureDir = featureContextDir(rootDir, slug);
113
+ const runsDir = verificationRunsDir(rootDir, slug);
114
+ const ledgerPath = path.join(featureDir, 'implementation-ledger.md');
115
+
116
+ await fs.mkdir(runsDir, { recursive: true });
117
+ const alreadyExists = await exists(ledgerPath);
118
+ if (!alreadyExists) {
119
+ await fs.writeFile(ledgerPath, createLedgerTemplate(slug, sourceArtifacts), 'utf8');
120
+ }
121
+
122
+ return {
123
+ ok: true,
124
+ feature_slug: slug,
125
+ ledger_path: relativeFromRoot(rootDir, ledgerPath),
126
+ created: !alreadyExists,
127
+ source_artifacts_found: sourceArtifacts.length > 0,
128
+ warnings: sourceArtifacts.length > 0 ? [] : ['source_artifacts_not_found']
129
+ };
130
+ }
131
+
132
+ async function readLedger(rootDir, slug) {
133
+ const ledgerPath = path.join(featureContextDir(rootDir, slug), 'implementation-ledger.md');
134
+ const resolved = resolveInsideRoot(rootDir, ledgerPath);
135
+ if (!resolved.ok) return resolved;
136
+ try {
137
+ const content = await fs.readFile(resolved.path, 'utf8');
138
+ return {
139
+ ok: true,
140
+ path: resolved.path,
141
+ relative_path: resolved.relative_path,
142
+ content
143
+ };
144
+ } catch {
145
+ return makeError('ledger_not_found', {
146
+ feature_slug: slug,
147
+ ledger_path: resolved.relative_path
148
+ });
149
+ }
150
+ }
151
+
152
+ function validateLedgerContent(content, rootDir, slug) {
153
+ const missingSections = findMissingSections(content);
154
+ if (missingSections.length > 0) {
155
+ return makeError('missing_ledger_sections', {
156
+ feature_slug: slug,
157
+ missing_sections: missingSections,
158
+ ready_for_prompt: false
159
+ });
160
+ }
161
+
162
+ const rawJson = extractJsonBlock(content, 'Machine Ledger');
163
+ if (!rawJson) {
164
+ return makeError('missing_machine_ledger', {
165
+ feature_slug: slug,
166
+ ready_for_prompt: false
167
+ });
168
+ }
169
+
170
+ let ledger;
171
+ try {
172
+ ledger = JSON.parse(rawJson);
173
+ } catch (error) {
174
+ return makeError('invalid_machine_ledger_json', {
175
+ feature_slug: slug,
176
+ detail: error.message,
177
+ ready_for_prompt: false
178
+ });
179
+ }
180
+
181
+ const errors = validateImplementationLedger(ledger, { rootDir, slug });
182
+ const claims = Array.isArray(ledger.claims) ? ledger.claims : [];
183
+ const missingEvidence = missingLedgerEvidence(ledger);
184
+
185
+ if (errors.length > 0) {
186
+ return makeError('invalid_machine_ledger', {
187
+ feature_slug: slug,
188
+ errors,
189
+ ready_for_prompt: false
190
+ });
191
+ }
192
+
193
+ return {
194
+ ok: true,
195
+ feature_slug: slug,
196
+ ledger,
197
+ missing_sections: [],
198
+ missing_evidence: missingEvidence,
199
+ ready_for_prompt: claims.length > 0 && missingEvidence.length === 0
200
+ };
201
+ }
202
+
203
+ async function checkLedger(rootDir, slug) {
204
+ const loaded = await readLedger(rootDir, slug);
205
+ if (!loaded.ok) return loaded;
206
+ const validation = validateLedgerContent(loaded.content, rootDir, slug);
207
+ return {
208
+ ...validation,
209
+ ledger_path: loaded.relative_path
210
+ };
211
+ }
212
+
213
+ module.exports = {
214
+ REQUIRED_LEDGER_SECTIONS,
215
+ createLedgerTemplate,
216
+ prepareLedger,
217
+ readLedger,
218
+ checkLedger,
219
+ validateLedgerContent,
220
+ extractJsonBlock
221
+ };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
+
7
+ function toPosixPath(value) {
8
+ return String(value || '').replace(/\\/g, '/');
9
+ }
10
+
11
+ function validateFeatureSlug(slug) {
12
+ const value = String(slug || '').trim();
13
+ if (!value) {
14
+ return { ok: false, reason: 'missing_feature' };
15
+ }
16
+ if (!SLUG_RE.test(value)) {
17
+ return { ok: false, reason: 'invalid_feature_slug', feature_slug: value };
18
+ }
19
+ return { ok: true, feature_slug: value };
20
+ }
21
+
22
+ function isInsideRoot(rootDir, candidatePath) {
23
+ const root = path.resolve(rootDir);
24
+ const candidate = path.resolve(candidatePath);
25
+ const relative = path.relative(root, candidate);
26
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
27
+ }
28
+
29
+ function resolveProjectRoot(cwd, targetArg) {
30
+ return path.resolve(cwd || process.cwd(), targetArg || '.');
31
+ }
32
+
33
+ function resolveInsideRoot(rootDir, inputPath) {
34
+ if (!inputPath) {
35
+ return { ok: false, reason: 'missing_path' };
36
+ }
37
+ const root = path.resolve(rootDir);
38
+ const resolved = path.resolve(root, String(inputPath));
39
+ if (!isInsideRoot(root, resolved)) {
40
+ return {
41
+ ok: false,
42
+ reason: 'path_outside_root',
43
+ path: String(inputPath)
44
+ };
45
+ }
46
+ return {
47
+ ok: true,
48
+ path: resolved,
49
+ relative_path: toPosixPath(path.relative(root, resolved))
50
+ };
51
+ }
52
+
53
+ function relativeFromRoot(rootDir, absolutePath) {
54
+ return toPosixPath(path.relative(path.resolve(rootDir), path.resolve(absolutePath)));
55
+ }
56
+
57
+ function featureContextDir(rootDir, slug) {
58
+ return path.join(path.resolve(rootDir), '.aioson', 'context', 'features', slug);
59
+ }
60
+
61
+ function verificationRunsDir(rootDir, slug) {
62
+ return path.join(featureContextDir(rootDir, slug), 'verification-runs');
63
+ }
64
+
65
+ module.exports = {
66
+ validateFeatureSlug,
67
+ resolveProjectRoot,
68
+ resolveInsideRoot,
69
+ relativeFromRoot,
70
+ featureContextDir,
71
+ verificationRunsDir,
72
+ isInsideRoot,
73
+ toPosixPath
74
+ };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const { normalizePolicy } = require('./result');
4
+
5
+ function ownerRoute(owner) {
6
+ if (owner === 'product' || owner === 'scope-check') return 'product';
7
+ if (owner === 'sheldon') return 'sheldon';
8
+ if (owner === 'qa' || owner === 'tester') return 'qa';
9
+ if (owner === 'pentester') return 'pentester';
10
+ return owner || 'dev';
11
+ }
12
+
13
+ function routeForFinding(finding) {
14
+ const route = finding.recommended_route || ownerRoute(finding.owner);
15
+ if (finding.kind === 'scope_constraint' || route === 'product' || route === 'sheldon') {
16
+ return { verdict: 'NEEDS_SCOPE_DECISION', route: route === 'sheldon' ? 'sheldon' : 'product' };
17
+ }
18
+ if (finding.kind === 'test_coverage' || route === 'qa' || route === 'tester') {
19
+ return { verdict: 'NEEDS_QA_RECHECK', route: route === 'tester' ? 'tester' : 'qa' };
20
+ }
21
+ if (finding.kind === 'security_constraint' || route === 'pentester') {
22
+ return { verdict: 'NEEDS_SECURITY_REVIEW', route: 'pentester' };
23
+ }
24
+ return { verdict: 'NEEDS_DEV_FIX', route: route === 'deyvin' ? 'deyvin' : 'dev' };
25
+ }
26
+
27
+ function isBlockingFinding(finding, policy) {
28
+ if (!finding) return false;
29
+ if (finding.severity === 'blocking' || finding.blocks === true) return true;
30
+ if (policy !== 'strict') return false;
31
+ return ['DOES_NOT_CONFIRM', 'PARTIAL', 'NOT_VERIFIED'].includes(finding.status)
32
+ && [
33
+ 'required_behavior',
34
+ 'acceptance_criterion',
35
+ 'prototype_contract',
36
+ 'security_constraint'
37
+ ].includes(finding.kind);
38
+ }
39
+
40
+ function commandIsMissing(command) {
41
+ if (!command || !command.required) return false;
42
+ const status = String(command.status || command.last_status || '').toLowerCase();
43
+ return !status || ['missing', 'not_run', 'failed', 'error', 'unknown'].includes(status);
44
+ }
45
+
46
+ function applyPolicy(report, requestedPolicy) {
47
+ const policy = normalizePolicy(requestedPolicy || report.policy) || 'standard';
48
+ const findings = Array.isArray(report.findings) ? report.findings : [];
49
+ const commands = Array.isArray(report.commands_run) ? report.commands_run : [];
50
+
51
+ if (policy === 'strict' && commands.some(commandIsMissing)) {
52
+ return {
53
+ policy,
54
+ verdict: 'INCONCLUSIVE',
55
+ recommended_route: 'qa',
56
+ blocking_findings_count: findings.filter((f) => isBlockingFinding(f, policy)).length,
57
+ reason: 'missing_required_command'
58
+ };
59
+ }
60
+
61
+ const blockingFindings = findings.filter((finding) => isBlockingFinding(finding, policy));
62
+ if (blockingFindings.length > 0 && policy !== 'advisory') {
63
+ const routed = routeForFinding(blockingFindings[0]);
64
+ return {
65
+ policy,
66
+ verdict: routed.verdict,
67
+ recommended_route: routed.route,
68
+ blocking_findings_count: blockingFindings.length,
69
+ reason: 'blocking_findings'
70
+ };
71
+ }
72
+
73
+ if (report.verdict && report.verdict !== 'PASS') {
74
+ return {
75
+ policy,
76
+ verdict: report.verdict,
77
+ recommended_route: report.recommended_route || 'dev',
78
+ blocking_findings_count: blockingFindings.length,
79
+ reason: 'auditor_verdict'
80
+ };
81
+ }
82
+
83
+ return {
84
+ policy,
85
+ verdict: 'PASS',
86
+ recommended_route: report.recommended_route || 'qa',
87
+ blocking_findings_count: blockingFindings.length,
88
+ reason: 'no_blocking_findings'
89
+ };
90
+ }
91
+
92
+ module.exports = {
93
+ applyPolicy,
94
+ routeForFinding
95
+ };
@@ -0,0 +1,314 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+
6
+ const { machineReportSchemaExample } = require('./schema');
7
+ const { verificationRunsDir, relativeFromRoot, resolveInsideRoot } = require('./path-policy');
8
+
9
+ const DEFAULT_PROMPT_BUDGET_CHARS = 24000;
10
+
11
+ function timestampForFile(date = new Date()) {
12
+ return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
13
+ }
14
+
15
+ function asJson(value) {
16
+ return JSON.stringify(value, null, 2);
17
+ }
18
+
19
+ function compactBundleForBudget(evidenceBundle) {
20
+ return {
21
+ ...evidenceBundle,
22
+ artifact_summaries: (evidenceBundle.artifact_summaries || []).map((artifact) => ({
23
+ type: artifact.type,
24
+ role: artifact.role,
25
+ path: artifact.path,
26
+ exists: artifact.exists,
27
+ size_bytes: artifact.size_bytes,
28
+ omitted_reason: artifact.omitted_reason || 'prompt_budget_compact_mode'
29
+ }))
30
+ };
31
+ }
32
+
33
+ function truncateText(value, maxChars) {
34
+ const text = String(value || '');
35
+ if (!maxChars || text.length <= maxChars) return text;
36
+ return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
37
+ }
38
+
39
+ function compactCommandPlan(commands, limit = 25) {
40
+ const list = Array.isArray(commands) ? commands : [];
41
+ const selected = list.slice(0, limit).map((command) => ({
42
+ order: command.order,
43
+ command: command.command,
44
+ required: command.required,
45
+ source: command.source,
46
+ last_status: command.last_status || command.status
47
+ }));
48
+ if (list.length > selected.length) {
49
+ selected.push({
50
+ omitted_count: list.length - selected.length,
51
+ omitted_reason: 'prompt_budget_command_plan_limit'
52
+ });
53
+ }
54
+ return selected;
55
+ }
56
+
57
+ function minimalBundleForBudget(evidenceBundle, limits = {}) {
58
+ const sourceLimit = limits.sourceLimit || 40;
59
+ const claimLimit = limits.claimLimit || 80;
60
+ const gapLimit = limits.gapLimit || 20;
61
+ const commandLimit = limits.commandLimit || 25;
62
+ const summaryMax = limits.summaryMax || 220;
63
+ return {
64
+ ...compactBundleForBudget(evidenceBundle),
65
+ source_artifacts: (evidenceBundle.source_artifacts || []).slice(0, sourceLimit).map((artifact) => ({
66
+ type: artifact.type,
67
+ role: artifact.role,
68
+ path: artifact.path
69
+ })),
70
+ artifact_summaries: [],
71
+ ledger_claims: compactLedgerClaims(evidenceBundle.ledger_claims || [], { summaryMax }).slice(0, claimLimit),
72
+ known_gaps: (evidenceBundle.known_gaps || []).slice(0, gapLimit).map((gap) => ({
73
+ id: gap.id,
74
+ owner: gap.owner,
75
+ blocks: gap.blocks,
76
+ gap: truncateText(gap.gap, summaryMax)
77
+ })),
78
+ command_plan: compactCommandPlan(evidenceBundle.command_plan || [], commandLimit),
79
+ verification_commands: compactCommandPlan(evidenceBundle.verification_commands || [], commandLimit)
80
+ };
81
+ }
82
+
83
+ function compactLedgerClaims(claims, options = {}) {
84
+ const summaryMax = options.summaryMax || 220;
85
+ return (claims || []).map((claim) => ({
86
+ id: claim.id,
87
+ kind: claim.kind,
88
+ summary: truncateText(claim.summary, summaryMax),
89
+ owner: claim.owner,
90
+ status: claim.status,
91
+ evidence_count: Array.isArray(claim.evidence) ? claim.evidence.length : 0
92
+ }));
93
+ }
94
+
95
+ function buildPromptMarkdown({ slug, policy, ledger, evidenceBundle, promptBudget = {} }) {
96
+ const budget = {
97
+ max_chars: promptBudget.max_chars || DEFAULT_PROMPT_BUDGET_CHARS,
98
+ compact_mode: Boolean(promptBudget.compact_mode),
99
+ artifact_summary_mode: promptBudget.artifact_summary_mode || 'preview'
100
+ };
101
+ const claims = budget.compact_mode
102
+ ? compactLedgerClaims(evidenceBundle.ledger_claims || ledger.claims || [])
103
+ : (evidenceBundle.ledger_claims || ledger.claims || []);
104
+ return `# Clean Auditor Prompt - ${slug}
105
+
106
+ You are an independent adversarial implementation auditor. Do not trust implementation summaries. Verify each claim against real files, git state, tests, and source artifacts.
107
+
108
+ ## Mission
109
+
110
+ Judge whether the implementation evidence confirms the approved behavior for \`${slug}\`. Your job is not to be nice; your job is to find mismatches without inventing false positives.
111
+
112
+ ## Hard Rules
113
+
114
+ - Read the ledger claims before judging.
115
+ - Treat tests passed as evidence, not proof of full behavior.
116
+ - Distinguish "not found" from "false".
117
+ - Report exact \`file:line\` for divergences when available.
118
+ - Respect explicit false-positive exceptions from source artifacts or the ledger.
119
+ - Do not execute destructive commands.
120
+ - Do not propose broad unrelated refactors.
121
+ - No courtesy PASS.
122
+ - Produce the \`Machine Report\` JSON block exactly.
123
+
124
+ ## PASS Criteria
125
+
126
+ PASS only when every implemented or partial required claim is confirmed against files/tests/artifacts, required commands are passed or explicitly justified, scope gaps are owned, and no blocking finding remains. A green test suite alone is not enough.
127
+
128
+ ## Owner Routing
129
+
130
+ - Implementation infidelity, missing behavior, broken wiring -> \`NEEDS_DEV_FIX\` / \`dev\`.
131
+ - Product or scope contradiction -> \`NEEDS_SCOPE_DECISION\` / \`product\` or \`sheldon\`.
132
+ - Tests not rerun, weak coverage, stale verification -> \`NEEDS_QA_RECHECK\` / \`qa\` or \`tester\`.
133
+ - Security-sensitive finding -> \`NEEDS_SECURITY_REVIEW\` / \`pentester\`.
134
+ - Missing required evidence -> \`INCONCLUSIVE\` with the missing command or artifact named.
135
+
136
+ ## Prompt Package Controls
137
+
138
+ \`\`\`json
139
+ ${asJson({
140
+ prompt_budget: budget,
141
+ redactions: evidenceBundle.redactions || {},
142
+ preview_budget: evidenceBundle.preview_budget || {}
143
+ })}
144
+ \`\`\`
145
+
146
+ ## Policy
147
+
148
+ \`${policy}\`
149
+
150
+ ## Source Artifacts
151
+
152
+ \`\`\`json
153
+ ${asJson(evidenceBundle.source_artifacts || [])}
154
+ \`\`\`
155
+
156
+ ## Artifact Summaries
157
+
158
+ \`\`\`json
159
+ ${asJson(evidenceBundle.artifact_summaries || [])}
160
+ \`\`\`
161
+
162
+ ## Git State
163
+
164
+ \`\`\`json
165
+ ${asJson(evidenceBundle.git || {})}
166
+ \`\`\`
167
+
168
+ ## Ledger Claims
169
+
170
+ \`\`\`json
171
+ ${asJson(claims)}
172
+ \`\`\`
173
+
174
+ ## Known Gaps
175
+
176
+ \`\`\`json
177
+ ${asJson(evidenceBundle.known_gaps || ledger.known_gaps || [])}
178
+ \`\`\`
179
+
180
+ ## Required Verification Command Plan
181
+
182
+ \`\`\`json
183
+ ${asJson(evidenceBundle.command_plan || evidenceBundle.verification_commands || [])}
184
+ \`\`\`
185
+
186
+ ## Required Output
187
+
188
+ Return Markdown with these sections:
189
+
190
+ ## Verdict
191
+ ## Commands Run
192
+ ## Findings
193
+ ## Before And Now
194
+ ## Residual Risk
195
+ ## Recommended Route
196
+ ## Machine Report
197
+
198
+ The \`Machine Report\` section must contain one fenced \`json\` block matching:
199
+
200
+ \`\`\`json
201
+ ${asJson(machineReportSchemaExample(slug, policy))}
202
+ \`\`\`
203
+ `;
204
+ }
205
+
206
+ function buildWithinBudget({ slug, policy, ledger, evidenceBundle, maxChars = DEFAULT_PROMPT_BUDGET_CHARS }) {
207
+ let promptBudget = {
208
+ max_chars: maxChars,
209
+ compact_mode: false,
210
+ artifact_summary_mode: 'preview'
211
+ };
212
+ let markdown = buildPromptMarkdown({ slug, policy, ledger, evidenceBundle, promptBudget });
213
+ if (markdown.length <= maxChars) {
214
+ return { markdown, prompt_budget: { ...promptBudget, actual_chars: markdown.length, over_budget: false } };
215
+ }
216
+
217
+ const compactBundle = compactBundleForBudget(evidenceBundle);
218
+ promptBudget = {
219
+ max_chars: maxChars,
220
+ compact_mode: true,
221
+ artifact_summary_mode: 'path_only'
222
+ };
223
+ markdown = buildPromptMarkdown({ slug, policy, ledger, evidenceBundle: compactBundle, promptBudget });
224
+ if (markdown.length <= maxChars) {
225
+ return {
226
+ markdown,
227
+ prompt_budget: {
228
+ ...promptBudget,
229
+ actual_chars: markdown.length,
230
+ over_budget: false
231
+ }
232
+ };
233
+ }
234
+
235
+ const tiers = [
236
+ { artifactSummaryMode: 'minimal_path_only', sourceLimit: 40, claimLimit: 80, gapLimit: 20, commandLimit: 25, summaryMax: 220 },
237
+ { artifactSummaryMode: 'minimal_tight', sourceLimit: 30, claimLimit: 50, gapLimit: 12, commandLimit: 20, summaryMax: 140 },
238
+ { artifactSummaryMode: 'minimal_floor', sourceLimit: 20, claimLimit: 35, gapLimit: 8, commandLimit: 14, summaryMax: 100 },
239
+ { artifactSummaryMode: 'minimal_core', sourceLimit: 12, claimLimit: 24, gapLimit: 5, commandLimit: 10, summaryMax: 80 }
240
+ ];
241
+
242
+ for (const tier of tiers) {
243
+ const minimalBundle = minimalBundleForBudget(evidenceBundle, tier);
244
+ promptBudget = {
245
+ max_chars: maxChars,
246
+ compact_mode: true,
247
+ artifact_summary_mode: tier.artifactSummaryMode,
248
+ claim_summary_chars: tier.summaryMax
249
+ };
250
+ markdown = buildPromptMarkdown({ slug, policy, ledger, evidenceBundle: minimalBundle, promptBudget });
251
+ if (markdown.length <= maxChars) {
252
+ return {
253
+ markdown,
254
+ prompt_budget: {
255
+ ...promptBudget,
256
+ actual_chars: markdown.length,
257
+ over_budget: false
258
+ }
259
+ };
260
+ }
261
+ }
262
+
263
+ return {
264
+ markdown,
265
+ prompt_budget: {
266
+ ...promptBudget,
267
+ actual_chars: markdown.length,
268
+ over_budget: markdown.length > maxChars
269
+ }
270
+ };
271
+ }
272
+
273
+ async function writePromptPackage(rootDir, slug, markdown, outPath = null) {
274
+ let targetPath;
275
+ if (outPath) {
276
+ const safe = resolveInsideRoot(rootDir, outPath);
277
+ if (!safe.ok) return safe;
278
+ targetPath = safe.path;
279
+ } else {
280
+ const runsDir = verificationRunsDir(rootDir, slug);
281
+ await fs.mkdir(runsDir, { recursive: true });
282
+ targetPath = path.join(runsDir, `${timestampForFile()}-prompt.md`);
283
+ }
284
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
285
+ await fs.writeFile(targetPath, markdown, 'utf8');
286
+ return { ok: true, prompt_path: relativeFromRoot(rootDir, targetPath) };
287
+ }
288
+
289
+ async function buildAndWritePromptPackage({ rootDir, slug, policy, ledger, evidenceBundle, outPath }) {
290
+ const { markdown, prompt_budget: promptBudget } = buildWithinBudget({ slug, policy, ledger, evidenceBundle });
291
+ const written = await writePromptPackage(rootDir, slug, markdown, outPath);
292
+ if (!written.ok) return written;
293
+ return {
294
+ ok: true,
295
+ feature_slug: slug,
296
+ policy,
297
+ prompt_path: written.prompt_path,
298
+ prompt_chars: markdown.length,
299
+ prompt_budget: promptBudget,
300
+ redactions: evidenceBundle.redactions || {},
301
+ source_artifacts: evidenceBundle.source_artifacts,
302
+ artifact_summaries: evidenceBundle.artifact_summaries,
303
+ verification_commands: evidenceBundle.verification_commands,
304
+ command_plan: evidenceBundle.command_plan
305
+ };
306
+ }
307
+
308
+ module.exports = {
309
+ buildPromptMarkdown,
310
+ buildAndWritePromptPackage,
311
+ timestampForFile,
312
+ machineReportSchemaExample,
313
+ DEFAULT_PROMPT_BUDGET_CHARS
314
+ };