@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,77 @@
1
+ 'use strict';
2
+
3
+ const SECRET_KEY_RE = /\b([A-Za-z0-9_.-]*(?:token|secret|password|passwd|pwd|api[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|auth)[A-Za-z0-9_.-]*)(\s*[:=]\s*)(["']?)([^"'\s,;}]+)/gi;
4
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/g;
5
+ const COMMON_SECRET_RE = /\b(?:sk-[A-Za-z0-9_-]{12,}|sk_(?:live|test)_[A-Za-z0-9_-]{12,}|pk_[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16})\b/g;
6
+ const PEM_BLOCK_RE = /-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g;
7
+ const BASIC_AUTH_URL_RE = /(https?:\/\/)([^/\s:@]+):([^/\s@]+)@/g;
8
+
9
+ function createCounter() {
10
+ return {
11
+ secret_assignments: 0,
12
+ bearer_tokens: 0,
13
+ common_tokens: 0,
14
+ pem_blocks: 0,
15
+ credential_urls: 0
16
+ };
17
+ }
18
+
19
+ function redactText(input, counter = createCounter()) {
20
+ let text = String(input || '');
21
+ text = text.replace(PEM_BLOCK_RE, () => {
22
+ counter.pem_blocks += 1;
23
+ return '[REDACTED_PEM_BLOCK]';
24
+ });
25
+ text = text.replace(BASIC_AUTH_URL_RE, (_match, protocol) => {
26
+ counter.credential_urls += 1;
27
+ return `${protocol}[REDACTED_CREDENTIALS]@`;
28
+ });
29
+ text = text.replace(BEARER_RE, () => {
30
+ counter.bearer_tokens += 1;
31
+ return 'Bearer [REDACTED_SECRET]';
32
+ });
33
+ text = text.replace(SECRET_KEY_RE, (_match, key, sep, quote) => {
34
+ counter.secret_assignments += 1;
35
+ return `${key}${sep}${quote}[REDACTED_SECRET]`;
36
+ });
37
+ text = text.replace(COMMON_SECRET_RE, () => {
38
+ counter.common_tokens += 1;
39
+ return '[REDACTED_SECRET]';
40
+ });
41
+ return text;
42
+ }
43
+
44
+ function isSecretKey(key) {
45
+ return /(?:token|secret|password|passwd|pwd|api[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|auth)/i.test(String(key || ''));
46
+ }
47
+
48
+ function redactJson(value, counter = createCounter()) {
49
+ if (Array.isArray(value)) {
50
+ return value.map((item) => redactJson(item, counter));
51
+ }
52
+ if (value && typeof value === 'object') {
53
+ const output = {};
54
+ for (const [key, item] of Object.entries(value)) {
55
+ if (isSecretKey(key)) {
56
+ counter.secret_assignments += 1;
57
+ output[key] = '[REDACTED_SECRET]';
58
+ } else {
59
+ output[key] = redactJson(item, counter);
60
+ }
61
+ }
62
+ return output;
63
+ }
64
+ if (typeof value === 'string') return redactText(value, counter);
65
+ return value;
66
+ }
67
+
68
+ function totalRedactions(counter) {
69
+ return Object.values(counter || {}).reduce((sum, value) => sum + Number(value || 0), 0);
70
+ }
71
+
72
+ module.exports = {
73
+ createCounter,
74
+ redactText,
75
+ redactJson,
76
+ totalRedactions
77
+ };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+
5
+ const {
6
+ VERDICTS,
7
+ validateVerificationReport,
8
+ makeError
9
+ } = require('./result');
10
+ const { resolveInsideRoot } = require('./path-policy');
11
+ const { extractJsonBlock } = require('./ledger-store');
12
+
13
+ const REQUIRED_REPORT_SECTIONS = [
14
+ { id: 'verdict', heading: 'Verdict' },
15
+ { id: 'commands_run', heading: 'Commands Run' },
16
+ { id: 'findings', heading: 'Findings' },
17
+ { id: 'before_and_now', heading: 'Before And Now' },
18
+ { id: 'residual_risk', heading: 'Residual Risk' },
19
+ { id: 'recommended_route', heading: 'Recommended Route' },
20
+ { id: 'machine_report', heading: 'Machine Report' }
21
+ ];
22
+
23
+ function sectionPattern(heading) {
24
+ return new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'im');
25
+ }
26
+
27
+ function missingReportSections(content) {
28
+ return REQUIRED_REPORT_SECTIONS
29
+ .filter((section) => !sectionPattern(section.heading).test(content))
30
+ .map((section) => section.id);
31
+ }
32
+
33
+ function extractSection(content, heading) {
34
+ const text = String(content || '');
35
+ const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, 'im');
36
+ const headingMatch = headingRe.exec(text);
37
+ if (!headingMatch) return '';
38
+ const start = headingMatch.index + headingMatch[0].length;
39
+ const rest = text.slice(start);
40
+ const nextHeading = rest.search(/^##\s+/m);
41
+ return (nextHeading === -1 ? rest : rest.slice(0, nextHeading)).trim();
42
+ }
43
+
44
+ function proseVerdictTokens(content) {
45
+ const verdictSection = extractSection(content, 'Verdict');
46
+ if (!verdictSection) return [];
47
+ const pattern = new RegExp(`\\b(${Array.from(VERDICTS).join('|')})\\b`, 'g');
48
+ return [...verdictSection.matchAll(pattern)].map((match) => match[1]);
49
+ }
50
+
51
+ function proseVerdict(content) {
52
+ const tokens = proseVerdictTokens(content);
53
+ return tokens.length > 0 ? tokens[0] : null;
54
+ }
55
+
56
+ async function parseVerificationReport(rootDir, slug, reportPath, requestedPolicy) {
57
+ const safe = resolveInsideRoot(rootDir, reportPath);
58
+ if (!safe.ok) return safe;
59
+
60
+ let content;
61
+ try {
62
+ content = await fs.readFile(safe.path, 'utf8');
63
+ } catch {
64
+ return makeError('report_not_found', { report_path: safe.relative_path });
65
+ }
66
+
67
+ const missingSections = missingReportSections(content);
68
+ if (missingSections.length > 0) {
69
+ return makeError('missing_report_sections', {
70
+ feature_slug: slug,
71
+ report_path: safe.relative_path,
72
+ missing_sections: missingSections
73
+ });
74
+ }
75
+
76
+ const rawJson = extractJsonBlock(content, 'Machine Report');
77
+ if (!rawJson) {
78
+ return makeError('missing_machine_report', {
79
+ feature_slug: slug,
80
+ report_path: safe.relative_path
81
+ });
82
+ }
83
+
84
+ let report;
85
+ try {
86
+ report = JSON.parse(rawJson);
87
+ } catch (error) {
88
+ return makeError('invalid_machine_report_json', {
89
+ feature_slug: slug,
90
+ report_path: safe.relative_path,
91
+ detail: error.message
92
+ });
93
+ }
94
+
95
+ // Conflict only when the Verdict prose names verdict token(s) and NONE of them
96
+ // match the machine verdict. Collecting all tokens (not just the first) avoids a
97
+ // false conflict when the auditor writes a negated mention, e.g.
98
+ // "Not a PASS — NEEDS_DEV_FIX" alongside a machine verdict of NEEDS_DEV_FIX.
99
+ const proseTokens = proseVerdictTokens(content);
100
+ if (proseTokens.length > 0 && report.verdict && !proseTokens.includes(report.verdict)) {
101
+ return makeError('report_conflict', {
102
+ feature_slug: slug,
103
+ report_path: safe.relative_path,
104
+ prose_verdict: proseTokens[0],
105
+ machine_verdict: report.verdict
106
+ });
107
+ }
108
+
109
+ const errors = validateVerificationReport(report, { slug, requestedPolicy });
110
+ if (errors.length > 0) {
111
+ return makeError('invalid_machine_report', {
112
+ feature_slug: slug,
113
+ report_path: safe.relative_path,
114
+ errors
115
+ });
116
+ }
117
+
118
+ return {
119
+ ok: true,
120
+ feature_slug: slug,
121
+ report_path: safe.relative_path,
122
+ report
123
+ };
124
+ }
125
+
126
+ module.exports = {
127
+ parseVerificationReport,
128
+ REQUIRED_REPORT_SECTIONS,
129
+ missingReportSections,
130
+ proseVerdict,
131
+ proseVerdictTokens
132
+ };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+
6
+ const { REPORT_SCHEMA_VERSION } = require('./schema');
7
+ const { featureContextDir, verificationRunsDir, relativeFromRoot } = require('./path-policy');
8
+ const { timestampForFile } = require('./prompt-package');
9
+
10
+ function safeSegment(value, fallback = 'default') {
11
+ const segment = String(value || fallback)
12
+ .trim()
13
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
14
+ .replace(/^-+|-+$/g, '')
15
+ .slice(0, 80);
16
+ return segment || fallback;
17
+ }
18
+
19
+ function runnerRunStem({ timestamp = timestampForFile(), tool, model }) {
20
+ return `${timestamp}-${safeSegment(tool, 'tool')}-${safeSegment(model || 'configured-default', 'configured-default')}`;
21
+ }
22
+
23
+ async function writeVerificationRunFile(rootDir, slug, stem, suffix, content) {
24
+ const runsDir = verificationRunsDir(rootDir, slug);
25
+ await fs.mkdir(runsDir, { recursive: true });
26
+ const targetPath = path.join(runsDir, `${stem}-${suffix}`);
27
+ await fs.writeFile(targetPath, String(content || ''), 'utf8');
28
+ return {
29
+ path: targetPath,
30
+ relative_path: relativeFromRoot(rootDir, targetPath)
31
+ };
32
+ }
33
+
34
+ async function promoteLatestReport(rootDir, slug, markdown) {
35
+ const targetPath = path.join(featureContextDir(rootDir, slug), 'verification-report.md');
36
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
37
+ await fs.writeFile(targetPath, String(markdown || ''), 'utf8');
38
+ return {
39
+ path: targetPath,
40
+ relative_path: relativeFromRoot(rootDir, targetPath)
41
+ };
42
+ }
43
+
44
+ function systemInconclusiveReport({ slug, policy, summary, route = 'qa', command, status, evidence }) {
45
+ const report = {
46
+ schema_version: REPORT_SCHEMA_VERSION,
47
+ feature_slug: slug,
48
+ policy,
49
+ verdict: 'INCONCLUSIVE',
50
+ summary,
51
+ commands_run: [
52
+ {
53
+ command: command || 'auditor runner',
54
+ status: status || 'failed',
55
+ evidence: evidence || summary
56
+ }
57
+ ],
58
+ findings: [],
59
+ recommended_route: route,
60
+ blocking_findings_count: 0
61
+ };
62
+
63
+ return `# Verification Report - ${slug}
64
+
65
+ ## Verdict
66
+ INCONCLUSIVE
67
+
68
+ ## Commands Run
69
+ ${command || 'auditor runner'} — ${status || 'failed'}.
70
+
71
+ ## Findings
72
+ No auditor findings were trusted because the runner did not produce a valid report.
73
+
74
+ ## Before And Now
75
+ Not evaluated by a valid auditor report.
76
+
77
+ ## Residual Risk
78
+ ${summary}
79
+
80
+ ## Recommended Route
81
+ ${route}
82
+
83
+ ## Machine Report
84
+
85
+ \`\`\`json
86
+ ${JSON.stringify(report, null, 2)}
87
+ \`\`\`
88
+ `;
89
+ }
90
+
91
+ module.exports = {
92
+ safeSegment,
93
+ runnerRunStem,
94
+ writeVerificationRunFile,
95
+ promoteLatestReport,
96
+ systemInconclusiveReport
97
+ };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const schema = require('./schema');
4
+
5
+ function makeError(reason, detail = {}) {
6
+ return {
7
+ ok: false,
8
+ reason,
9
+ ...detail
10
+ };
11
+ }
12
+
13
+ module.exports = {
14
+ ...schema,
15
+ makeError
16
+ };
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+
5
+ const SUPPORTED_RUNNER_TOOLS = new Set(['codex', 'claude', 'opencode']);
6
+ const DEFAULT_RUNNER_MODEL = 'configured-default';
7
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
8
+ const DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
9
+ const DETECTION_TIMEOUT_MS = 5000;
10
+ const MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$/;
11
+
12
+ function normalizeRunnerTool(tool) {
13
+ const value = String(tool || '').trim().toLowerCase();
14
+ if (!value) return { ok: false, reason: 'missing_tool' };
15
+ if (!SUPPORTED_RUNNER_TOOLS.has(value)) {
16
+ return {
17
+ ok: false,
18
+ reason: 'unsupported_tool',
19
+ tool: value,
20
+ supported_tools: Array.from(SUPPORTED_RUNNER_TOOLS)
21
+ };
22
+ }
23
+ return { ok: true, tool: value };
24
+ }
25
+
26
+ function normalizeRunnerModel(model) {
27
+ const value = String(model || DEFAULT_RUNNER_MODEL).trim();
28
+ if (!value || value === DEFAULT_RUNNER_MODEL) {
29
+ return { ok: true, model: DEFAULT_RUNNER_MODEL, uses_configured_default: true };
30
+ }
31
+ if (!MODEL_RE.test(value) || value.includes('..')) {
32
+ return { ok: false, reason: 'invalid_model', model: value };
33
+ }
34
+ return { ok: true, model: value, uses_configured_default: false };
35
+ }
36
+
37
+ function positiveInt(value, fallback, min, max) {
38
+ const parsed = Number(value);
39
+ if (!Number.isFinite(parsed)) return fallback;
40
+ const integer = Math.floor(parsed);
41
+ if (integer < min) return min;
42
+ if (integer > max) return max;
43
+ return integer;
44
+ }
45
+
46
+ function runnerLimits(options = {}) {
47
+ return {
48
+ timeout_ms: positiveInt(
49
+ options['timeout-ms'] || options.timeoutMs || options.timeout,
50
+ DEFAULT_TIMEOUT_MS,
51
+ 100,
52
+ 60 * 60 * 1000
53
+ ),
54
+ max_output_bytes: positiveInt(
55
+ options['max-output-bytes'] || options.maxOutputBytes || options['max-output'],
56
+ DEFAULT_MAX_OUTPUT_BYTES,
57
+ 1024,
58
+ 5 * 1024 * 1024
59
+ )
60
+ };
61
+ }
62
+
63
+ function adapterInvocation({ tool, model, rootDir, promptPath, promptText }) {
64
+ const usesDefault = model === DEFAULT_RUNNER_MODEL;
65
+ if (tool === 'codex') {
66
+ const args = [
67
+ 'exec',
68
+ '--cd', rootDir,
69
+ '--sandbox', 'read-only',
70
+ '--ephemeral',
71
+ '--color', 'never'
72
+ ];
73
+ if (!usesDefault) args.push('--model', model);
74
+ args.push('-');
75
+ return {
76
+ command: 'codex',
77
+ args,
78
+ stdin: promptText,
79
+ permission_mode: 'read-only',
80
+ destructive_commands_allowed: false
81
+ };
82
+ }
83
+
84
+ if (tool === 'claude') {
85
+ const args = [
86
+ '--print',
87
+ '--permission-mode', 'plan',
88
+ '--tools', ''
89
+ ];
90
+ if (!usesDefault) args.push('--model', model);
91
+ return {
92
+ command: 'claude',
93
+ args,
94
+ stdin: promptText,
95
+ permission_mode: 'plan',
96
+ destructive_commands_allowed: false
97
+ };
98
+ }
99
+
100
+ const args = [
101
+ 'run',
102
+ '--dir', rootDir,
103
+ '--pure',
104
+ '--file', promptPath
105
+ ];
106
+ if (!usesDefault) args.push('--model', model);
107
+ args.push('Run the implementation audit described in the attached prompt package. Return only the requested verification report with the Machine Report JSON block.');
108
+ return {
109
+ command: 'opencode',
110
+ args,
111
+ stdin: null,
112
+ permission_mode: 'default',
113
+ destructive_commands_allowed: false
114
+ };
115
+ }
116
+
117
+ function commandLabel(invocation) {
118
+ return [invocation.command, ...invocation.args].join(' ');
119
+ }
120
+
121
+ function runProcessWithLimits(invocation, { cwd, timeoutMs, maxOutputBytes, spawnImpl = spawn }) {
122
+ return new Promise((resolve) => {
123
+ const startedAt = Date.now();
124
+ let settled = false;
125
+ let timedOut = false;
126
+ let outputLimited = false;
127
+ let stdout = '';
128
+ let stderr = '';
129
+ let outputBytes = 0;
130
+ let timer = null;
131
+
132
+ function finish(result) {
133
+ if (settled) return;
134
+ settled = true;
135
+ if (timer) clearTimeout(timer);
136
+ resolve({
137
+ ...result,
138
+ duration_ms: Date.now() - startedAt,
139
+ stdout,
140
+ stderr,
141
+ output_bytes: outputBytes,
142
+ output_truncated: outputLimited
143
+ });
144
+ }
145
+
146
+ let child;
147
+ try {
148
+ child = spawnImpl(invocation.command, invocation.args, {
149
+ cwd,
150
+ windowsHide: true,
151
+ stdio: ['pipe', 'pipe', 'pipe'],
152
+ env: {
153
+ ...process.env,
154
+ NO_COLOR: '1',
155
+ CI: process.env.CI || '1'
156
+ }
157
+ });
158
+ } catch (error) {
159
+ finish({
160
+ status: 'spawn_error',
161
+ exit_code: null,
162
+ signal: null,
163
+ error: error.message
164
+ });
165
+ return;
166
+ }
167
+
168
+ timer = setTimeout(() => {
169
+ timedOut = true;
170
+ if (child && child.kill) child.kill('SIGTERM');
171
+ setTimeout(() => {
172
+ if (!settled && child && child.kill) child.kill('SIGKILL');
173
+ }, 1000).unref?.();
174
+ }, timeoutMs);
175
+
176
+ function capture(kind, chunk) {
177
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '');
178
+ outputBytes += Buffer.byteLength(text, 'utf8');
179
+ if (outputBytes <= maxOutputBytes) {
180
+ if (kind === 'stdout') stdout += text;
181
+ else stderr += text;
182
+ return;
183
+ }
184
+ outputLimited = true;
185
+ const remaining = Math.max(0, maxOutputBytes - Buffer.byteLength(stdout + stderr, 'utf8'));
186
+ if (remaining > 0) {
187
+ const slice = Buffer.from(text, 'utf8').subarray(0, remaining).toString('utf8');
188
+ if (kind === 'stdout') stdout += slice;
189
+ else stderr += slice;
190
+ }
191
+ if (child && child.kill) child.kill('SIGTERM');
192
+ }
193
+
194
+ if (child.stdout && child.stdout.on) child.stdout.on('data', (chunk) => capture('stdout', chunk));
195
+ if (child.stderr && child.stderr.on) child.stderr.on('data', (chunk) => capture('stderr', chunk));
196
+ if (child.on) {
197
+ child.on('error', (error) => {
198
+ finish({
199
+ status: 'spawn_error',
200
+ exit_code: null,
201
+ signal: null,
202
+ error: error.message
203
+ });
204
+ });
205
+ child.on('close', (code, signal) => {
206
+ if (timedOut) {
207
+ finish({ status: 'timeout', exit_code: code, signal });
208
+ } else if (outputLimited) {
209
+ finish({ status: 'output_limit', exit_code: code, signal });
210
+ } else {
211
+ finish({
212
+ status: code === 0 ? 'completed' : 'failed',
213
+ exit_code: code,
214
+ signal
215
+ });
216
+ }
217
+ });
218
+ }
219
+
220
+ if (child.stdin) {
221
+ if (invocation.stdin) child.stdin.write(invocation.stdin);
222
+ child.stdin.end();
223
+ }
224
+ });
225
+ }
226
+
227
+ async function detectRunnerTool(tool, { rootDir, spawnImpl = spawn }) {
228
+ const invocation = {
229
+ command: tool,
230
+ args: ['--version'],
231
+ stdin: null
232
+ };
233
+ const result = await runProcessWithLimits(invocation, {
234
+ cwd: rootDir,
235
+ timeoutMs: DETECTION_TIMEOUT_MS,
236
+ maxOutputBytes: 16 * 1024,
237
+ spawnImpl
238
+ });
239
+ if (result.status !== 'completed') {
240
+ return {
241
+ ok: false,
242
+ reason: result.status === 'spawn_error' ? 'tool_not_found' : `tool_detection_${result.status}`,
243
+ tool,
244
+ detail: result.error || result.stderr || result.stdout || null
245
+ };
246
+ }
247
+ return {
248
+ ok: true,
249
+ tool,
250
+ version_output: (result.stdout || result.stderr || '').trim().slice(0, 500)
251
+ };
252
+ }
253
+
254
+ async function runAuditorTool({
255
+ rootDir,
256
+ tool,
257
+ model,
258
+ promptPath,
259
+ promptText,
260
+ limits = {},
261
+ spawnImpl = spawn
262
+ }) {
263
+ const toolResult = normalizeRunnerTool(tool);
264
+ if (!toolResult.ok) return toolResult;
265
+ const modelResult = normalizeRunnerModel(model);
266
+ if (!modelResult.ok) return modelResult;
267
+
268
+ const detected = await detectRunnerTool(toolResult.tool, { rootDir, spawnImpl });
269
+ if (!detected.ok) return detected;
270
+
271
+ const invocation = adapterInvocation({
272
+ tool: toolResult.tool,
273
+ model: modelResult.model,
274
+ rootDir,
275
+ promptPath,
276
+ promptText
277
+ });
278
+ const run = await runProcessWithLimits(invocation, {
279
+ cwd: rootDir,
280
+ timeoutMs: limits.timeout_ms || DEFAULT_TIMEOUT_MS,
281
+ maxOutputBytes: limits.max_output_bytes || DEFAULT_MAX_OUTPUT_BYTES,
282
+ spawnImpl
283
+ });
284
+
285
+ return {
286
+ ok: run.status === 'completed',
287
+ status: run.status,
288
+ tool: toolResult.tool,
289
+ model: modelResult.model,
290
+ command: commandLabel(invocation),
291
+ permission_mode: invocation.permission_mode,
292
+ destructive_commands_allowed: invocation.destructive_commands_allowed,
293
+ timeout_ms: limits.timeout_ms || DEFAULT_TIMEOUT_MS,
294
+ max_output_bytes: limits.max_output_bytes || DEFAULT_MAX_OUTPUT_BYTES,
295
+ duration_ms: run.duration_ms,
296
+ exit_code: run.exit_code,
297
+ signal: run.signal,
298
+ stdout: run.stdout,
299
+ stderr: run.stderr,
300
+ output_bytes: run.output_bytes,
301
+ output_truncated: run.output_truncated,
302
+ error: run.error || null,
303
+ detected
304
+ };
305
+ }
306
+
307
+ module.exports = {
308
+ SUPPORTED_RUNNER_TOOLS,
309
+ DEFAULT_RUNNER_MODEL,
310
+ DEFAULT_TIMEOUT_MS,
311
+ DEFAULT_MAX_OUTPUT_BYTES,
312
+ normalizeRunnerTool,
313
+ normalizeRunnerModel,
314
+ runnerLimits,
315
+ adapterInvocation,
316
+ runProcessWithLimits,
317
+ detectRunnerTool,
318
+ runAuditorTool
319
+ };