auditor-lambda 0.2.8 → 0.2.9
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/README.md +6 -0
- package/audit-code-wrapper-lib.mjs +1 -1
- package/dist/adapters/eslint.js +9 -5
- package/dist/cli.d.ts +42 -1
- package/dist/cli.js +114 -64
- package/dist/extractors/bucketing.d.ts +4 -0
- package/dist/extractors/bucketing.js +6 -2
- package/dist/extractors/disposition.d.ts +4 -0
- package/dist/extractors/disposition.js +6 -2
- package/dist/extractors/fileInventory.js +24 -28
- package/dist/extractors/flows.d.ts +5 -0
- package/dist/extractors/flows.js +18 -38
- package/dist/extractors/pathPatterns.d.ts +10 -3
- package/dist/extractors/pathPatterns.js +109 -61
- package/dist/extractors/surfaces.d.ts +4 -0
- package/dist/extractors/surfaces.js +11 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/io/artifacts.d.ts +55 -40
- package/dist/io/artifacts.js +73 -110
- package/dist/io/json.js +52 -21
- package/dist/io/runArtifacts.d.ts +1 -1
- package/dist/io/runArtifacts.js +26 -3
- package/dist/orchestrator/advance.js +83 -62
- package/dist/orchestrator/flowCoverage.js +11 -5
- package/dist/orchestrator/flowPlanning.d.ts +7 -2
- package/dist/orchestrator/flowPlanning.js +46 -21
- package/dist/orchestrator/flowRequeue.js +28 -8
- package/dist/orchestrator/internalExecutors.js +12 -8
- package/dist/orchestrator/planning.js +25 -3
- package/dist/orchestrator/requeue.js +11 -1
- package/dist/orchestrator/taskBuilder.d.ts +4 -2
- package/dist/orchestrator/taskBuilder.js +153 -52
- package/dist/orchestrator/unitBuilder.d.ts +3 -1
- package/dist/orchestrator/unitBuilder.js +24 -16
- package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
- package/dist/prompts/renderWorkerPrompt.js +16 -8
- package/dist/providers/claudeCodeProvider.d.ts +4 -1
- package/dist/providers/claudeCodeProvider.js +8 -5
- package/dist/providers/localSubprocessProvider.d.ts +4 -0
- package/dist/providers/localSubprocessProvider.js +7 -2
- package/dist/providers/spawnLoggedCommand.d.ts +9 -1
- package/dist/providers/spawnLoggedCommand.js +77 -29
- package/dist/reporting/synthesis.d.ts +2 -0
- package/dist/reporting/synthesis.js +12 -9
- package/dist/supervisor/operatorHandoff.js +48 -18
- package/dist/supervisor/runLedger.d.ts +1 -1
- package/dist/supervisor/runLedger.js +112 -5
- package/dist/supervisor/sessionConfig.js +10 -10
- package/dist/types/externalAnalyzer.d.ts +3 -0
- package/dist/types/flowCoverage.d.ts +5 -1
- package/dist/types/flowCoverage.js +5 -1
- package/dist/types/flows.d.ts +5 -1
- package/dist/types/flows.js +1 -1
- package/dist/types/runLedger.d.ts +5 -1
- package/dist/types/runLedger.js +6 -1
- package/dist/types/runtimeValidation.d.ts +12 -3
- package/dist/types/runtimeValidation.js +16 -1
- package/dist/types/sessionConfig.d.ts +15 -2
- package/dist/types/sessionConfig.js +15 -1
- package/dist/types/surfaces.d.ts +4 -1
- package/dist/types/surfaces.js +1 -1
- package/dist/types/workerSession.d.ts +9 -0
- package/dist/types/workerSession.js +5 -1
- package/dist/validation/artifacts.d.ts +1 -1
- package/dist/validation/artifacts.js +33 -20
- package/dist/validation/auditResults.d.ts +2 -2
- package/dist/validation/auditResults.js +7 -15
- package/dist/validation/basic.d.ts +9 -1
- package/dist/validation/basic.js +40 -3
- package/dist/validation/sessionConfig.d.ts +4 -2
- package/dist/validation/sessionConfig.js +62 -15
- package/docs/agent-integrations.md +29 -9
- package/docs/next-steps.md +21 -4
- package/docs/packaging.md +14 -0
- package/docs/product-direction.md +22 -0
- package/docs/production-launch-bar.md +2 -0
- package/docs/releasing.md +17 -0
- package/docs/remediation-baseline.md +75 -0
- package/docs/run-flow.md +23 -11
- package/docs/session-config.md +50 -5
- package/docs/supervisor.md +7 -0
- package/docs/workflow-refactor-brief.md +177 -0
- package/package.json +1 -1
- package/schemas/audit_result.schema.json +4 -1
- package/schemas/audit_task.schema.json +3 -1
- package/schemas/coverage_matrix.schema.json +3 -3
- package/schemas/critical_flows.schema.json +6 -2
- package/schemas/file_disposition.schema.json +2 -2
- package/schemas/finding.schema.json +9 -4
- package/schemas/flow_coverage.schema.json +2 -2
- package/schemas/repo_manifest.schema.json +4 -4
- package/schemas/risk_register.schema.json +2 -2
- package/schemas/runtime_validation_report.schema.json +2 -2
- package/schemas/runtime_validation_tasks.schema.json +8 -2
- package/schemas/surface_manifest.schema.json +6 -3
- package/schemas/unit_manifest.schema.json +3 -2
- package/skills/audit-code/SKILL.md +5 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { usesDeferredWorkerCommand, } from "../types/workerSession.js";
|
|
2
|
+
function renderArgv(task) {
|
|
3
|
+
return JSON.stringify(task.worker_command);
|
|
3
4
|
}
|
|
4
5
|
export function renderWorkerPrompt(task) {
|
|
5
|
-
const
|
|
6
|
+
const commandArgv = renderArgv(task);
|
|
6
7
|
if (task.preferred_executor === "agent" && task.audit_results_path) {
|
|
7
8
|
const tasksPath = task.pending_audit_tasks_path ??
|
|
8
9
|
`${task.artifacts_dir}/audit_tasks.json`;
|
|
@@ -29,14 +30,17 @@ export function renderWorkerPrompt(task) {
|
|
|
29
30
|
" Example evidence entry: src/foo.ts:42 - variable overwritten before use",
|
|
30
31
|
" Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
|
|
31
32
|
" Low-priority tasks still require a real review. Use findings: [] only when you genuinely found nothing notable.",
|
|
33
|
+
task.timeout_ms
|
|
34
|
+
? ` Time budget for this task: ${task.timeout_ms} ms.`
|
|
35
|
+
: " Keep the task bounded to the assigned files only.",
|
|
32
36
|
`Reference schema: ${task.artifacts_dir}/dispatch/audit-result.schema.json`,
|
|
33
37
|
`Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
|
|
34
38
|
];
|
|
35
|
-
if (task
|
|
36
|
-
lines.push("", "Stop after writing the results file.");
|
|
39
|
+
if (usesDeferredWorkerCommand(task)) {
|
|
40
|
+
lines.push("", "This run is using deferred worker-command ingestion.", "Do not execute worker_command in this session.", "Stop after writing the results file.");
|
|
37
41
|
}
|
|
38
42
|
else {
|
|
39
|
-
lines.push("", "Then
|
|
43
|
+
lines.push("", "Then execute the worker_command array from task.json exactly as written.", "Preserve argv boundaries instead of reconstructing shell quoting.", `worker_command argv JSON: ${commandArgv}`, "Stop after the command completes.");
|
|
40
44
|
}
|
|
41
45
|
return lines.join("\n");
|
|
42
46
|
}
|
|
@@ -46,10 +50,14 @@ export function renderWorkerPrompt(task) {
|
|
|
46
50
|
`Repository root: ${task.repo_root}`,
|
|
47
51
|
`Obligation: ${task.obligation_id ?? "unknown"}`,
|
|
48
52
|
`Executor: ${task.preferred_executor}`,
|
|
49
|
-
"Execute the
|
|
50
|
-
|
|
53
|
+
"Execute the worker_command array from task.json exactly as written.",
|
|
54
|
+
"Preserve argv boundaries instead of reconstructing shell quoting.",
|
|
55
|
+
`worker_command argv JSON: ${commandArgv}`,
|
|
51
56
|
"Do not continue the audit recursively.",
|
|
52
57
|
"Do not choose another task.",
|
|
58
|
+
task.timeout_ms
|
|
59
|
+
? `The worker command is budgeted for ${task.timeout_ms} ms.`
|
|
60
|
+
: "If the command hangs or fails, stop and let the supervisor handle it.",
|
|
53
61
|
`The command must write the worker result JSON to: ${task.result_path}`,
|
|
54
62
|
"After the command completes, stop.",
|
|
55
63
|
].join("\n");
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { FreshSessionProvider, LaunchFreshSessionInput } from "./types.js";
|
|
2
2
|
import type { ClaudeCodeConfig } from "../types/sessionConfig.js";
|
|
3
|
+
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
4
|
+
export declare const ACTIVE_CLAUDE_CODE_SESSION_MESSAGE: string;
|
|
3
5
|
export declare class ClaudeCodeProvider implements FreshSessionProvider {
|
|
4
6
|
name: string;
|
|
5
7
|
private readonly config;
|
|
6
|
-
|
|
8
|
+
private readonly launchCommand;
|
|
9
|
+
constructor(config?: ClaudeCodeConfig, launchCommand?: typeof spawnLoggedCommand);
|
|
7
10
|
launch(input: LaunchFreshSessionInput): Promise<import("./types.js").LaunchFreshSessionResult>;
|
|
8
11
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
3
|
+
export const ACTIVE_CLAUDE_CODE_SESSION_MESSAGE = "claude-code provider cannot be used inside an active Claude Code session. " +
|
|
4
|
+
'Set provider to "local-subprocess" in .audit-artifacts/session-config.json, ' +
|
|
5
|
+
"then run /audit-code conversationally and follow the dispatch prompts manually.";
|
|
3
6
|
export class ClaudeCodeProvider {
|
|
4
7
|
name = "claude-code";
|
|
5
8
|
config;
|
|
6
|
-
|
|
9
|
+
launchCommand;
|
|
10
|
+
constructor(config = {}, launchCommand = spawnLoggedCommand) {
|
|
7
11
|
this.config = config;
|
|
12
|
+
this.launchCommand = launchCommand;
|
|
8
13
|
}
|
|
9
14
|
async launch(input) {
|
|
10
15
|
if (process.env.CLAUDECODE) {
|
|
11
|
-
throw new Error(
|
|
12
|
-
"Set provider to \"local-subprocess\" in .audit-artifacts/session-config.json, " +
|
|
13
|
-
"then run /audit-code conversationally and follow the dispatch prompts manually.");
|
|
16
|
+
throw new Error(ACTIVE_CLAUDE_CODE_SESSION_MESSAGE);
|
|
14
17
|
}
|
|
15
18
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
16
19
|
const command = this.config.command ?? "claude";
|
|
@@ -20,6 +23,6 @@ export class ClaudeCodeProvider {
|
|
|
20
23
|
...(this.config.extra_args ?? []),
|
|
21
24
|
"--dangerously-skip-permissions",
|
|
22
25
|
];
|
|
23
|
-
return await
|
|
26
|
+
return await this.launchCommand(command, args, input);
|
|
24
27
|
}
|
|
25
28
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { FreshSessionProvider, LaunchFreshSessionInput } from "./types.js";
|
|
2
|
+
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
3
|
+
export declare const MISSING_WORKER_COMMAND_MESSAGE = "local-subprocess provider requires task.worker_command.";
|
|
2
4
|
export declare class LocalSubprocessProvider implements FreshSessionProvider {
|
|
3
5
|
name: string;
|
|
6
|
+
private readonly launchCommand;
|
|
7
|
+
constructor(launchCommand?: typeof spawnLoggedCommand);
|
|
4
8
|
launch(input: LaunchFreshSessionInput): Promise<import("./types.js").LaunchFreshSessionResult>;
|
|
5
9
|
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { readJsonFile } from "../io/json.js";
|
|
2
2
|
import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
|
|
3
|
+
export const MISSING_WORKER_COMMAND_MESSAGE = "local-subprocess provider requires task.worker_command.";
|
|
3
4
|
export class LocalSubprocessProvider {
|
|
4
5
|
name = "local-subprocess";
|
|
6
|
+
launchCommand;
|
|
7
|
+
constructor(launchCommand = spawnLoggedCommand) {
|
|
8
|
+
this.launchCommand = launchCommand;
|
|
9
|
+
}
|
|
5
10
|
async launch(input) {
|
|
6
11
|
const task = await readJsonFile(input.taskPath);
|
|
7
12
|
if (!task.worker_command.length) {
|
|
8
|
-
throw new Error(
|
|
13
|
+
throw new Error(MISSING_WORKER_COMMAND_MESSAGE);
|
|
9
14
|
}
|
|
10
15
|
const [command, ...args] = task.worker_command;
|
|
11
|
-
return await
|
|
16
|
+
return await this.launchCommand(command, args, input);
|
|
12
17
|
}
|
|
13
18
|
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
1
3
|
import type { LaunchFreshSessionInput, LaunchFreshSessionResult } from "./types.js";
|
|
2
|
-
|
|
4
|
+
interface SpawnLoggedCommandOptions {
|
|
5
|
+
createWriteStream?: typeof createWriteStream;
|
|
6
|
+
spawn?: typeof spawn;
|
|
7
|
+
killGraceMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function spawnLoggedCommand(command: string, args: string[], input: LaunchFreshSessionInput, env?: Record<string, string>, options?: SpawnLoggedCommandOptions): Promise<LaunchFreshSessionResult>;
|
|
10
|
+
export {};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createWriteStream } from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
const TERMINATION_SIGNAL = "SIGTERM";
|
|
4
|
+
const FORCE_KILL_SIGNAL = "SIGKILL";
|
|
5
|
+
const FORCE_KILL_GRACE_MS = 1_000;
|
|
3
6
|
function tee(write, chunk) {
|
|
4
7
|
write.write(chunk);
|
|
5
8
|
}
|
|
@@ -7,22 +10,77 @@ function tee(write, chunk) {
|
|
|
7
10
|
// does not consult PATH for executables without a shell. Callers should use
|
|
8
11
|
// `platformCommand()` (scripts/smoke-packaged-audit-code.mjs) or similar to
|
|
9
12
|
// supply the correct command form for the host OS.
|
|
10
|
-
export async function spawnLoggedCommand(command, args, input, env) {
|
|
13
|
+
export async function spawnLoggedCommand(command, args, input, env, options = {}) {
|
|
14
|
+
const openWriteStream = options.createWriteStream ?? createWriteStream;
|
|
15
|
+
const spawnProcess = options.spawn ?? spawn;
|
|
16
|
+
const killGraceMs = options.killGraceMs ?? FORCE_KILL_GRACE_MS;
|
|
11
17
|
return await new Promise((resolve, reject) => {
|
|
12
|
-
const stdoutLog =
|
|
13
|
-
const stderrLog =
|
|
18
|
+
const stdoutLog = openWriteStream(input.stdoutPath, { flags: "a" });
|
|
19
|
+
const stderrLog = openWriteStream(input.stderrPath, { flags: "a" });
|
|
14
20
|
const startedAt = Date.now();
|
|
15
21
|
let timedOut = false;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
+
let settled = false;
|
|
23
|
+
let child = null;
|
|
24
|
+
let timer;
|
|
25
|
+
let heartbeat;
|
|
26
|
+
let forceKillTimer;
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
if (timer) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
if (heartbeat) {
|
|
32
|
+
clearInterval(heartbeat);
|
|
33
|
+
}
|
|
34
|
+
if (forceKillTimer) {
|
|
35
|
+
clearTimeout(forceKillTimer);
|
|
36
|
+
}
|
|
37
|
+
stdoutLog.end();
|
|
38
|
+
stderrLog.end();
|
|
39
|
+
};
|
|
40
|
+
const settle = (callback) => {
|
|
41
|
+
if (settled) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
settled = true;
|
|
45
|
+
cleanup();
|
|
46
|
+
callback();
|
|
47
|
+
};
|
|
48
|
+
const fail = (error) => {
|
|
49
|
+
if (child && !child.killed) {
|
|
50
|
+
child.kill(FORCE_KILL_SIGNAL);
|
|
51
|
+
}
|
|
52
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
53
|
+
settle(() => reject(normalized));
|
|
54
|
+
};
|
|
55
|
+
stdoutLog.on("error", fail);
|
|
56
|
+
stderrLog.on("error", fail);
|
|
57
|
+
let spawnedChild;
|
|
58
|
+
try {
|
|
59
|
+
spawnedChild = spawnProcess(command, args, {
|
|
60
|
+
cwd: input.repoRoot,
|
|
61
|
+
env: { ...process.env, ...env },
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
child = spawnedChild;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
fail(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!spawnedChild.stdout || !spawnedChild.stderr) {
|
|
71
|
+
fail(new Error(`Fresh session spawn for run ${input.runId} did not provide pipe-backed stdout/stderr streams.`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
timer = setTimeout(() => {
|
|
22
75
|
timedOut = true;
|
|
23
|
-
|
|
76
|
+
spawnedChild.kill(TERMINATION_SIGNAL);
|
|
77
|
+
forceKillTimer = setTimeout(() => {
|
|
78
|
+
if (!settled) {
|
|
79
|
+
spawnedChild.kill(FORCE_KILL_SIGNAL);
|
|
80
|
+
}
|
|
81
|
+
}, killGraceMs);
|
|
24
82
|
}, input.timeoutMs);
|
|
25
|
-
|
|
83
|
+
heartbeat = setInterval(() => {
|
|
26
84
|
const elapsedMs = Date.now() - startedAt;
|
|
27
85
|
const message = `[provider] run ${input.runId} still running after ${elapsedMs}ms\n`;
|
|
28
86
|
tee(stderrLog, message);
|
|
@@ -30,40 +88,30 @@ export async function spawnLoggedCommand(command, args, input, env) {
|
|
|
30
88
|
process.stderr.write(message);
|
|
31
89
|
}
|
|
32
90
|
}, 30_000);
|
|
33
|
-
|
|
91
|
+
spawnedChild.stdout.on("data", (chunk) => {
|
|
34
92
|
tee(stdoutLog, chunk);
|
|
35
93
|
if (input.uiMode === "visible") {
|
|
36
94
|
process.stdout.write(chunk);
|
|
37
95
|
}
|
|
38
96
|
});
|
|
39
|
-
|
|
97
|
+
spawnedChild.stderr.on("data", (chunk) => {
|
|
40
98
|
tee(stderrLog, chunk);
|
|
41
99
|
if (input.uiMode === "visible") {
|
|
42
100
|
process.stderr.write(chunk);
|
|
43
101
|
}
|
|
44
102
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
clearInterval(heartbeat);
|
|
48
|
-
stdoutLog.end();
|
|
49
|
-
stderrLog.end();
|
|
50
|
-
reject(error);
|
|
51
|
-
});
|
|
52
|
-
child.on("exit", (code, signal) => {
|
|
53
|
-
clearTimeout(timer);
|
|
54
|
-
clearInterval(heartbeat);
|
|
55
|
-
stdoutLog.end();
|
|
56
|
-
stderrLog.end();
|
|
103
|
+
spawnedChild.on("error", fail);
|
|
104
|
+
spawnedChild.on("exit", (code, signal) => {
|
|
57
105
|
if (timedOut) {
|
|
58
|
-
reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`));
|
|
106
|
+
settle(() => reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`)));
|
|
59
107
|
return;
|
|
60
108
|
}
|
|
61
|
-
resolve({
|
|
109
|
+
settle(() => resolve({
|
|
62
110
|
accepted: true,
|
|
63
|
-
processId:
|
|
111
|
+
processId: spawnedChild.pid,
|
|
64
112
|
exitCode: code,
|
|
65
113
|
signal,
|
|
66
|
-
});
|
|
114
|
+
}));
|
|
67
115
|
});
|
|
68
116
|
});
|
|
69
117
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuditResult, CoverageMatrix, Finding, UnitManifest } from "../types.js";
|
|
2
|
+
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
2
3
|
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
3
4
|
import type { GraphBundle } from "../types/graph.js";
|
|
4
5
|
import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
|
|
@@ -23,5 +24,6 @@ export declare function buildAuditReportModel(params: {
|
|
|
23
24
|
criticalFlows?: CriticalFlowManifest;
|
|
24
25
|
coverageMatrix?: CoverageMatrix;
|
|
25
26
|
runtimeValidationReport?: RuntimeValidationReport;
|
|
27
|
+
externalAnalyzerResults?: ExternalAnalyzerResults;
|
|
26
28
|
}): AuditReportModel;
|
|
27
29
|
export declare function renderAuditReportMarkdown(model: AuditReportModel): string;
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { buildWorkBlocks } from "./workBlocks.js";
|
|
2
2
|
import { mergeFindings } from "./mergeFindings.js";
|
|
3
|
-
function
|
|
3
|
+
function countBy(items, selectKey) {
|
|
4
4
|
const breakdown = {};
|
|
5
|
-
for (const
|
|
6
|
-
|
|
5
|
+
for (const item of items) {
|
|
6
|
+
const key = selectKey(item);
|
|
7
|
+
if (!key) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
breakdown[key] = (breakdown[key] ?? 0) + 1;
|
|
7
11
|
}
|
|
8
12
|
return breakdown;
|
|
9
13
|
}
|
|
14
|
+
function severityBreakdown(findings) {
|
|
15
|
+
return countBy(findings, (finding) => finding.severity);
|
|
16
|
+
}
|
|
10
17
|
function runtimeStatusBreakdown(report) {
|
|
11
|
-
|
|
12
|
-
for (const result of report?.results ?? []) {
|
|
13
|
-
breakdown[result.status] = (breakdown[result.status] ?? 0) + 1;
|
|
14
|
-
}
|
|
15
|
-
return breakdown;
|
|
18
|
+
return countBy(report?.results ?? [], (result) => result.status);
|
|
16
19
|
}
|
|
17
20
|
function coverageSummary(coverage) {
|
|
18
21
|
const files = coverage?.files ?? [];
|
|
@@ -29,7 +32,7 @@ function formatSeverityList(summary) {
|
|
|
29
32
|
return parts.length > 0 ? parts.join(", ") : "none";
|
|
30
33
|
}
|
|
31
34
|
export function buildAuditReportModel(params) {
|
|
32
|
-
const findings = mergeFindings(params.results, params.runtimeValidationReport);
|
|
35
|
+
const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults);
|
|
33
36
|
const workBlocks = buildWorkBlocks({
|
|
34
37
|
findings,
|
|
35
38
|
unitManifest: params.unitManifest,
|
|
@@ -3,22 +3,52 @@ import { join } from "node:path";
|
|
|
3
3
|
import { writeJsonFile } from "../io/json.js";
|
|
4
4
|
import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "../providers/constants.js";
|
|
5
5
|
export const CONFIG_ERROR_BLOCKER_PREFIX = "config-error:";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const INCOMING_DIRNAME = "incoming";
|
|
7
|
+
const OPERATOR_HANDOFF_JSON_FILENAME = "operator-handoff.json";
|
|
8
|
+
const OPERATOR_HANDOFF_MARKDOWN_FILENAME = "operator-handoff.md";
|
|
9
|
+
const SESSION_CONFIG_FILENAME = "session-config.json";
|
|
10
|
+
const RUN_LEDGER_FILENAME = "run-ledger.json";
|
|
11
|
+
const AUDIT_TASKS_FILENAME = "audit_tasks.json";
|
|
12
|
+
const RUNTIME_VALIDATION_TASKS_FILENAME = "runtime_validation_tasks.json";
|
|
13
|
+
const BLOCKED_STATUS = "blocked";
|
|
14
|
+
const COMPLETE_STATUS = "complete";
|
|
15
|
+
const NOT_STARTED_STATUS = "not_started";
|
|
16
|
+
const NON_PENDING_OBLIGATION_STATES = new Set([
|
|
17
|
+
"present",
|
|
18
|
+
"satisfied",
|
|
19
|
+
]);
|
|
20
|
+
const INTERACTIVE_PROVIDER_OPTIONS = [
|
|
21
|
+
"auto",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"opencode",
|
|
24
|
+
"subprocess-template",
|
|
25
|
+
"vscode-task",
|
|
26
|
+
];
|
|
27
|
+
function quoteShellPath(filePath) {
|
|
28
|
+
// The handoff renders a single shell argument, so the snippet only needs
|
|
29
|
+
// double-quote wrapping plus escaping embedded double quotes.
|
|
30
|
+
return `"${filePath.replace(/"/g, '\\"')}"`;
|
|
8
31
|
}
|
|
9
32
|
function buildPendingObligations(state) {
|
|
10
33
|
return state.obligations
|
|
11
|
-
.filter((item) =>
|
|
34
|
+
.filter((item) => !NON_PENDING_OBLIGATION_STATES.has(item.state))
|
|
12
35
|
.map((item) => item.id);
|
|
13
36
|
}
|
|
37
|
+
function formatQuotedList(values) {
|
|
38
|
+
if (values.length === 1) {
|
|
39
|
+
return `"${values[0]}"`;
|
|
40
|
+
}
|
|
41
|
+
const head = values.slice(0, -1).map((value) => `"${value}"`).join(", ");
|
|
42
|
+
return `${head}, or "${values[values.length - 1]}"`;
|
|
43
|
+
}
|
|
14
44
|
function buildSummary(status, providerName, fallbackSummary) {
|
|
15
|
-
if (status ===
|
|
45
|
+
if (status === COMPLETE_STATUS) {
|
|
16
46
|
return "No operator handoff is required. All known obligations are currently satisfied.";
|
|
17
47
|
}
|
|
18
|
-
if (status ===
|
|
48
|
+
if (status === BLOCKED_STATUS) {
|
|
19
49
|
return fallbackSummary;
|
|
20
50
|
}
|
|
21
|
-
if (status ===
|
|
51
|
+
if (status === NOT_STARTED_STATUS) {
|
|
22
52
|
return "The artifact bundle is not initialized yet. Run the wrapper from the repository root to create the initial audit artifacts.";
|
|
23
53
|
}
|
|
24
54
|
return providerName
|
|
@@ -26,10 +56,10 @@ function buildSummary(status, providerName, fallbackSummary) {
|
|
|
26
56
|
: "Automatic work can continue. Re-run the same wrapper or inspect the listed artifacts if you need operator context.";
|
|
27
57
|
}
|
|
28
58
|
function buildSuggestedInputs(artifactsDir, status, isConfigError) {
|
|
29
|
-
if (status !==
|
|
59
|
+
if (status !== BLOCKED_STATUS || isConfigError) {
|
|
30
60
|
return [];
|
|
31
61
|
}
|
|
32
|
-
const incomingDir = join(artifactsDir,
|
|
62
|
+
const incomingDir = join(artifactsDir, INCOMING_DIRNAME);
|
|
33
63
|
return [
|
|
34
64
|
{
|
|
35
65
|
flag: "--results",
|
|
@@ -49,20 +79,20 @@ function buildSuggestedInputs(artifactsDir, status, isConfigError) {
|
|
|
49
79
|
];
|
|
50
80
|
}
|
|
51
81
|
function buildSuggestedCommands(suggestedInputs, status) {
|
|
52
|
-
if (status !==
|
|
82
|
+
if (status !== BLOCKED_STATUS) {
|
|
53
83
|
return [];
|
|
54
84
|
}
|
|
55
85
|
return suggestedInputs.map((item) => `audit-code ${item.flag} ${quoteShellPath(item.suggested_path)}`);
|
|
56
86
|
}
|
|
57
87
|
function buildInteractiveProviderHint(status, providerName, sessionConfigPath, isConfigError) {
|
|
58
|
-
if (status !==
|
|
88
|
+
if (status !== BLOCKED_STATUS) {
|
|
59
89
|
return null;
|
|
60
90
|
}
|
|
61
91
|
if (isConfigError) {
|
|
62
92
|
return `A project configuration issue is blocking the audit. Verify that --root points to the repository root containing a project file (package.json, go.mod, etc.), then run audit-code again.`;
|
|
63
93
|
}
|
|
64
94
|
const providerLabel = providerName ?? LOCAL_SUBPROCESS_PROVIDER_NAME;
|
|
65
|
-
return `Current provider is ${providerLabel}. If you want the backend to continue through
|
|
95
|
+
return `Current backend worker provider is ${providerLabel}. Remaining semantic review belongs to the active conversation agent by default. If you intentionally want the backend to continue through a compatibility bridge instead, configure ${sessionConfigPath} for ${formatQuotedList(INTERACTIVE_PROVIDER_OPTIONS)} and re-run audit-code with an explicit --provider value from the repository root.`;
|
|
66
96
|
}
|
|
67
97
|
function renderMarkdown(handoff) {
|
|
68
98
|
const lines = [
|
|
@@ -115,18 +145,18 @@ function renderMarkdown(handoff) {
|
|
|
115
145
|
}
|
|
116
146
|
export function buildAuditCodeHandoff(params) {
|
|
117
147
|
const isConfigError = params.isConfigError ?? false;
|
|
118
|
-
const incomingDir = join(params.artifactsDir,
|
|
148
|
+
const incomingDir = join(params.artifactsDir, INCOMING_DIRNAME);
|
|
119
149
|
const artifactPaths = {
|
|
120
150
|
incoming_dir: incomingDir,
|
|
121
|
-
operator_handoff_json: join(params.artifactsDir,
|
|
122
|
-
operator_handoff_markdown: join(params.artifactsDir,
|
|
123
|
-
session_config: join(params.artifactsDir,
|
|
124
|
-
run_ledger: join(params.artifactsDir,
|
|
151
|
+
operator_handoff_json: join(params.artifactsDir, OPERATOR_HANDOFF_JSON_FILENAME),
|
|
152
|
+
operator_handoff_markdown: join(params.artifactsDir, OPERATOR_HANDOFF_MARKDOWN_FILENAME),
|
|
153
|
+
session_config: join(params.artifactsDir, SESSION_CONFIG_FILENAME),
|
|
154
|
+
run_ledger: join(params.artifactsDir, RUN_LEDGER_FILENAME),
|
|
125
155
|
audit_tasks: params.bundle.audit_tasks
|
|
126
|
-
? join(params.artifactsDir,
|
|
156
|
+
? join(params.artifactsDir, AUDIT_TASKS_FILENAME)
|
|
127
157
|
: null,
|
|
128
158
|
runtime_validation_tasks: params.bundle.runtime_validation_tasks
|
|
129
|
-
? join(params.artifactsDir,
|
|
159
|
+
? join(params.artifactsDir, RUNTIME_VALIDATION_TASKS_FILENAME)
|
|
130
160
|
: null,
|
|
131
161
|
};
|
|
132
162
|
const suggestedInputs = buildSuggestedInputs(params.artifactsDir, params.state.status, isConfigError);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RunLedger, type RunLedgerEntry } from "../types/runLedger.js";
|
|
2
2
|
export declare function loadRunLedger(artifactsDir: string): Promise<RunLedger>;
|
|
3
3
|
export declare function appendRunLedgerEntry(artifactsDir: string, entry: RunLedgerEntry): Promise<void>;
|
|
@@ -1,20 +1,127 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { RUN_LEDGER_STATUSES, } from "../types/runLedger.js";
|
|
1
5
|
import { isFileMissingError, readJsonFile, writeJsonFile } from "../io/json.js";
|
|
6
|
+
const RUN_LEDGER_FILENAME = "run-ledger.json";
|
|
7
|
+
const RUN_LEDGER_LOCK_FILENAME = "run-ledger.lock";
|
|
8
|
+
const LOCK_RETRY_DELAY_MS = 20;
|
|
9
|
+
const LOCK_RETRY_LIMIT = 100;
|
|
10
|
+
const VALID_RUN_LEDGER_STATUSES = new Set(RUN_LEDGER_STATUSES);
|
|
2
11
|
function ledgerPath(artifactsDir) {
|
|
3
|
-
return
|
|
12
|
+
return join(artifactsDir, RUN_LEDGER_FILENAME);
|
|
13
|
+
}
|
|
14
|
+
function ledgerLockPath(artifactsDir) {
|
|
15
|
+
return join(artifactsDir, RUN_LEDGER_LOCK_FILENAME);
|
|
16
|
+
}
|
|
17
|
+
function buildTempLedgerPath(artifactsDir) {
|
|
18
|
+
return join(artifactsDir, `${RUN_LEDGER_FILENAME}.${process.pid}.${randomUUID()}.tmp`);
|
|
19
|
+
}
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function isFileExistsError(error) {
|
|
24
|
+
return (typeof error === "object" &&
|
|
25
|
+
error !== null &&
|
|
26
|
+
"code" in error &&
|
|
27
|
+
error.code === "EEXIST");
|
|
28
|
+
}
|
|
29
|
+
function assertRunLedgerEntry(value, fieldPath) {
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
throw new Error(`Invalid run ledger in ${fieldPath}: expected an object.`);
|
|
32
|
+
}
|
|
33
|
+
const requireString = (field) => {
|
|
34
|
+
const entry = value[field];
|
|
35
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
36
|
+
throw new Error(`Invalid run ledger in ${fieldPath}.${field}: expected a non-empty string.`);
|
|
37
|
+
}
|
|
38
|
+
return entry;
|
|
39
|
+
};
|
|
40
|
+
const requireNullableString = (field) => {
|
|
41
|
+
const entry = value[field];
|
|
42
|
+
if (entry === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
46
|
+
throw new Error(`Invalid run ledger in ${fieldPath}.${field}: expected a string or null.`);
|
|
47
|
+
}
|
|
48
|
+
return entry;
|
|
49
|
+
};
|
|
50
|
+
const status = value.status;
|
|
51
|
+
if (typeof status !== "string" ||
|
|
52
|
+
!VALID_RUN_LEDGER_STATUSES.has(status)) {
|
|
53
|
+
throw new Error(`Invalid run ledger in ${fieldPath}.status: expected one of ${Array.from(VALID_RUN_LEDGER_STATUSES).join(", ")}.`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
run_id: requireString("run_id"),
|
|
57
|
+
provider: requireString("provider"),
|
|
58
|
+
obligation_id: requireNullableString("obligation_id"),
|
|
59
|
+
selected_executor: requireNullableString("selected_executor"),
|
|
60
|
+
status: status,
|
|
61
|
+
started_at: requireString("started_at"),
|
|
62
|
+
ended_at: requireString("ended_at"),
|
|
63
|
+
result_path: requireString("result_path"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function parseRunLedger(value, path) {
|
|
67
|
+
if (!isRecord(value)) {
|
|
68
|
+
throw new Error(`Invalid run ledger in ${path}: expected an object.`);
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(value.runs)) {
|
|
71
|
+
throw new Error(`Invalid run ledger in ${path}: expected runs to be an array.`);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
runs: value.runs.map((entry, index) => assertRunLedgerEntry(entry, `${path}.runs[${index}]`)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function sleep(ms) {
|
|
78
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
79
|
+
}
|
|
80
|
+
async function acquireLedgerLock(artifactsDir) {
|
|
81
|
+
const lockPath = ledgerLockPath(artifactsDir);
|
|
82
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
83
|
+
for (let attempt = 0; attempt < LOCK_RETRY_LIMIT; attempt += 1) {
|
|
84
|
+
try {
|
|
85
|
+
return await open(lockPath, "wx");
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!isFileExistsError(error)) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
if (attempt === LOCK_RETRY_LIMIT - 1) {
|
|
92
|
+
throw new Error(`Timed out waiting to update ${ledgerPath(artifactsDir)} because ${lockPath} is locked.`);
|
|
93
|
+
}
|
|
94
|
+
await sleep(LOCK_RETRY_DELAY_MS);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Failed to acquire lock for ${ledgerPath(artifactsDir)}.`);
|
|
4
98
|
}
|
|
5
99
|
export async function loadRunLedger(artifactsDir) {
|
|
100
|
+
const path = ledgerPath(artifactsDir);
|
|
6
101
|
try {
|
|
7
|
-
return await readJsonFile(
|
|
102
|
+
return parseRunLedger(await readJsonFile(path), path);
|
|
8
103
|
}
|
|
9
104
|
catch (error) {
|
|
10
105
|
if (isFileMissingError(error)) {
|
|
106
|
+
// A missing run ledger just means no worker runs have been recorded yet.
|
|
11
107
|
return { runs: [] };
|
|
12
108
|
}
|
|
13
109
|
throw error;
|
|
14
110
|
}
|
|
15
111
|
}
|
|
16
112
|
export async function appendRunLedgerEntry(artifactsDir, entry) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
113
|
+
const lockHandle = await acquireLedgerLock(artifactsDir);
|
|
114
|
+
const path = ledgerPath(artifactsDir);
|
|
115
|
+
const tempPath = buildTempLedgerPath(artifactsDir);
|
|
116
|
+
try {
|
|
117
|
+
const ledger = await loadRunLedger(artifactsDir);
|
|
118
|
+
ledger.runs.push(entry);
|
|
119
|
+
await writeJsonFile(tempPath, ledger);
|
|
120
|
+
await rename(tempPath, path);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await lockHandle.close();
|
|
124
|
+
await rm(ledgerLockPath(artifactsDir), { force: true });
|
|
125
|
+
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
126
|
+
}
|
|
20
127
|
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { readOptionalJsonFile } from "../io/json.js";
|
|
3
|
+
import { formatValidationIssues, } from "../validation/basic.js";
|
|
2
4
|
import { validateSessionConfig } from "../validation/sessionConfig.js";
|
|
3
5
|
import { writeJsonFile } from "../io/json.js";
|
|
6
|
+
const SESSION_CONFIG_FILENAME = "session-config.json";
|
|
7
|
+
const DEFAULT_SESSION_CONFIG = { provider: "local-subprocess" };
|
|
4
8
|
export function getSessionConfigPath(artifactsDir) {
|
|
5
|
-
return
|
|
9
|
+
return join(artifactsDir, SESSION_CONFIG_FILENAME);
|
|
6
10
|
}
|
|
7
11
|
export async function readSessionConfigFile(artifactsDir) {
|
|
8
12
|
return await readOptionalJsonFile(getSessionConfigPath(artifactsDir));
|
|
9
13
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
|
13
|
-
.join("\n");
|
|
14
|
-
return `Invalid ${configPath}:\n${details}`;
|
|
14
|
+
function formatConfigValidationIssues(configPath, issues) {
|
|
15
|
+
return `Invalid ${configPath}:\n${formatValidationIssues(issues).replace(/^ /gm, "- ")}`;
|
|
15
16
|
}
|
|
16
17
|
export async function loadSessionConfig(artifactsDir) {
|
|
17
18
|
const configPath = getSessionConfigPath(artifactsDir);
|
|
18
19
|
const rawConfig = await readOptionalJsonFile(configPath);
|
|
19
20
|
if (rawConfig === undefined) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return defaultConfig;
|
|
21
|
+
await writeJsonFile(configPath, DEFAULT_SESSION_CONFIG);
|
|
22
|
+
return { ...DEFAULT_SESSION_CONFIG };
|
|
23
23
|
}
|
|
24
24
|
const issues = validateSessionConfig(rawConfig);
|
|
25
25
|
if (issues.length > 0) {
|
|
26
|
-
throw new Error(
|
|
26
|
+
throw new Error(formatConfigValidationIssues(configPath, issues));
|
|
27
27
|
}
|
|
28
28
|
return rawConfig;
|
|
29
29
|
}
|