auditor-lambda 0.3.39 → 0.3.41
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/audit-code-wrapper-lib.mjs +20 -2
- package/dist/cli/args.d.ts +59 -0
- package/dist/cli/args.js +244 -0
- package/dist/cli/dispatch.d.ts +80 -0
- package/dist/cli/dispatch.js +528 -0
- package/dist/cli/prompts.d.ts +18 -0
- package/dist/cli/prompts.js +130 -0
- package/dist/cli/steps.d.ts +29 -0
- package/dist/cli/steps.js +30 -0
- package/dist/cli/waveManifest.d.ts +40 -0
- package/dist/cli/waveManifest.js +41 -0
- package/dist/cli/workerResult.d.ts +18 -0
- package/dist/cli/workerResult.js +42 -0
- package/dist/cli.d.ts +2 -22
- package/dist/cli.js +373 -990
- package/dist/extractors/browserExtension.d.ts +1 -3
- package/dist/extractors/browserExtension.js +2 -2
- package/dist/extractors/designAssessment.d.ts +1 -3
- package/dist/extractors/disposition.d.ts +2 -1
- package/dist/extractors/disposition.js +3 -0
- package/dist/extractors/flows.d.ts +1 -3
- package/dist/extractors/flows.js +2 -2
- package/dist/extractors/graph.d.ts +1 -2
- package/dist/extractors/graph.js +4 -326
- package/dist/extractors/graphManifestEdges.d.ts +1 -1
- package/dist/extractors/graphPathUtils.d.ts +1 -1
- package/dist/extractors/graphPythonImports.d.ts +3 -0
- package/dist/extractors/graphPythonImports.js +326 -0
- package/dist/extractors/risk.d.ts +1 -2
- package/dist/extractors/surfaces.d.ts +1 -3
- package/dist/extractors/surfaces.js +2 -2
- package/dist/io/artifacts.d.ts +1 -5
- package/dist/io/artifacts.js +1 -1
- package/dist/io/runArtifacts.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/orchestrator/advance.d.ts +1 -0
- package/dist/orchestrator/advance.js +8 -5
- package/dist/orchestrator/auditTaskUtils.d.ts +4 -0
- package/dist/orchestrator/auditTaskUtils.js +27 -0
- package/dist/orchestrator/fileAnchors.d.ts +1 -1
- package/dist/orchestrator/fileIntegrity.d.ts +7 -0
- package/dist/orchestrator/fileIntegrity.js +41 -0
- package/dist/orchestrator/flowCoverage.d.ts +1 -1
- package/dist/orchestrator/flowPlanning.d.ts +1 -1
- package/dist/orchestrator/flowRequeue.d.ts +1 -1
- package/dist/orchestrator/internalExecutors.d.ts +3 -1
- package/dist/orchestrator/internalExecutors.js +23 -5
- package/dist/orchestrator/nextStep.d.ts +2 -1
- package/dist/orchestrator/nextStep.js +1 -1
- package/dist/orchestrator/planning.d.ts +1 -1
- package/dist/orchestrator/requeueCommand.d.ts +1 -1
- package/dist/orchestrator/reviewPackets.d.ts +1 -1
- package/dist/orchestrator/reviewPackets.js +21 -113
- package/dist/orchestrator/runtimeValidation.d.ts +1 -1
- package/dist/orchestrator/taskBuilder.d.ts +1 -1
- package/dist/orchestrator/taskBuilder.js +1 -12
- package/dist/orchestrator/unionFind.d.ts +7 -0
- package/dist/orchestrator/unionFind.js +32 -0
- package/dist/orchestrator/unitBuilder.d.ts +2 -2
- package/dist/orchestrator/unitBuilder.js +4 -18
- package/dist/prompts/renderWorkerPrompt.js +18 -1
- package/dist/providers/claudeCodeProvider.d.ts +4 -4
- package/dist/providers/claudeCodeProvider.js +9 -3
- package/dist/providers/constants.d.ts +1 -1
- package/dist/providers/constants.js +1 -1
- package/dist/providers/index.d.ts +1 -2
- package/dist/providers/index.js +5 -4
- package/dist/providers/localSubprocessProvider.d.ts +2 -2
- package/dist/providers/localSubprocessProvider.js +1 -1
- package/dist/providers/opencodeProvider.d.ts +4 -4
- package/dist/providers/opencodeProvider.js +7 -2
- package/dist/providers/spawnLoggedCommand.d.ts +3 -1
- package/dist/providers/spawnLoggedCommand.js +21 -0
- package/dist/providers/subprocessTemplateProvider.d.ts +4 -4
- package/dist/providers/subprocessTemplateProvider.js +8 -3
- package/dist/providers/vscodeTaskProvider.d.ts +3 -4
- package/dist/providers/vscodeTaskProvider.js +2 -2
- package/dist/quota/discoveredLimits.js +1 -1
- package/dist/quota/hostLimits.d.ts +1 -2
- package/dist/quota/hostLimits.js +4 -46
- package/dist/quota/index.d.ts +18 -15
- package/dist/quota/index.js +4 -9
- package/dist/quota/scheduler.d.ts +1 -3
- package/dist/quota/scheduler.js +1 -2
- package/dist/reporting/synthesis.d.ts +1 -2
- package/dist/reporting/synthesis.js +2 -0
- package/dist/reporting/workBlocks.d.ts +1 -2
- package/dist/supervisor/operatorHandoff.js +1 -1
- package/dist/supervisor/runLedger.d.ts +1 -1
- package/dist/supervisor/runLedger.js +2 -2
- package/dist/supervisor/sessionConfig.d.ts +1 -1
- package/dist/supervisor/sessionConfig.js +1 -3
- package/dist/types/reviewPlanning.d.ts +1 -1
- package/dist/types/workerSession.d.ts +6 -0
- package/dist/validation/artifacts.d.ts +1 -1
- package/dist/validation/artifacts.js +1 -1
- package/dist/validation/auditResults.d.ts +2 -1
- package/dist/validation/auditResults.js +31 -13
- package/dist/validation/sessionConfig.d.ts +2 -3
- package/dist/validation/sessionConfig.js +2 -3
- package/package.json +4 -2
- package/scripts/postinstall.mjs +0 -1
- package/dist/io/json.d.ts +0 -10
- package/dist/io/json.js +0 -142
- package/dist/providers/types.d.ts +0 -33
- package/dist/providers/types.js +0 -1
- package/dist/quota/compositeQuotaSource.d.ts +0 -7
- package/dist/quota/compositeQuotaSource.js +0 -20
- package/dist/quota/errorParsers/claudeCodeErrorParser.d.ts +0 -6
- package/dist/quota/errorParsers/claudeCodeErrorParser.js +0 -39
- package/dist/quota/errorParsers/genericErrorParser.d.ts +0 -9
- package/dist/quota/errorParsers/genericErrorParser.js +0 -7
- package/dist/quota/errorParsers/index.d.ts +0 -5
- package/dist/quota/errorParsers/index.js +0 -12
- package/dist/quota/errorParsing.d.ts +0 -7
- package/dist/quota/errorParsing.js +0 -69
- package/dist/quota/fileLock.d.ts +0 -6
- package/dist/quota/fileLock.js +0 -64
- package/dist/quota/learnedQuotaSource.d.ts +0 -7
- package/dist/quota/learnedQuotaSource.js +0 -25
- package/dist/quota/limits.d.ts +0 -16
- package/dist/quota/limits.js +0 -77
- package/dist/quota/quotaSource.d.ts +0 -12
- package/dist/quota/quotaSource.js +0 -1
- package/dist/quota/slidingWindow.d.ts +0 -4
- package/dist/quota/slidingWindow.js +0 -28
- package/dist/quota/state.d.ts +0 -15
- package/dist/quota/state.js +0 -148
- package/dist/quota/types.d.ts +0 -67
- package/dist/quota/types.js +0 -1
- package/dist/reporting/rootCause.d.ts +0 -10
- package/dist/reporting/rootCause.js +0 -146
- package/dist/types/disposition.d.ts +0 -9
- package/dist/types/disposition.js +0 -1
- package/dist/types/flows.d.ts +0 -17
- package/dist/types/flows.js +0 -1
- package/dist/types/graph.d.ts +0 -22
- package/dist/types/graph.js +0 -1
- package/dist/types/risk.d.ts +0 -9
- package/dist/types/risk.js +0 -1
- package/dist/types/runLedger.d.ts +0 -17
- package/dist/types/runLedger.js +0 -6
- package/dist/types/sessionConfig.d.ts +0 -79
- package/dist/types/sessionConfig.js +0 -15
- package/dist/types/surfaces.d.ts +0 -15
- package/dist/types/surfaces.js +0 -1
- package/dist/validation/basic.d.ts +0 -13
- package/dist/validation/basic.js +0 -46
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, rename, rm, unlink
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
1
|
+
import { mkdir, readFile, readdir, rename, rm, unlink } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
4
|
import { fileURLToPath } from "node:url";
|
|
7
5
|
import { buildRepoManifest } from "./extractors/fileInventory.js";
|
|
8
6
|
import { buildFileDisposition } from "./extractors/disposition.js";
|
|
@@ -13,14 +11,14 @@ import { buildFlowCoverage } from "./orchestrator/flowCoverage.js";
|
|
|
13
11
|
import { buildRuntimeValidationTasks, } from "./orchestrator/runtimeValidation.js";
|
|
14
12
|
import { initializeCoverageFromPlan } from "./orchestrator/planning.js";
|
|
15
13
|
import { loadArtifactBundle, writeCoreArtifacts, promoteFinalAuditReport, } from "./io/artifacts.js";
|
|
16
|
-
import { isFileMissingError, readJsonFile, writeJsonFile } from "
|
|
14
|
+
import { isFileMissingError, readJsonFile, writeJsonFile, prefixValidationIssues } from "@audit-tools/shared";
|
|
17
15
|
import { validateArtifactBundle } from "./validation/artifacts.js";
|
|
18
16
|
import { validateAuditResults, formatAuditResultIssues, } from "./validation/auditResults.js";
|
|
19
|
-
import { prefixValidationIssues } from "./validation/basic.js";
|
|
20
17
|
import { validateConfiguredProviderEnvironment, validateSessionConfig, } from "./validation/sessionConfig.js";
|
|
21
18
|
import { buildAuditReportModel, renderAuditReportMarkdown, } from "./reporting/synthesis.js";
|
|
22
19
|
import { deriveAuditState } from "./orchestrator/state.js";
|
|
23
20
|
import { advanceAudit } from "./orchestrator/advance.js";
|
|
21
|
+
import { checkFileIntegrity } from "./orchestrator/fileIntegrity.js";
|
|
24
22
|
import { decideNextStep } from "./orchestrator/nextStep.js";
|
|
25
23
|
import { renderDesignReviewPrompt } from "./orchestrator/designReviewPrompt.js";
|
|
26
24
|
import { createFreshSessionProvider, resolveFreshSessionProviderName, } from "./providers/index.js";
|
|
@@ -29,228 +27,26 @@ import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./superv
|
|
|
29
27
|
import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
|
|
30
28
|
import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
|
|
31
29
|
import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
|
|
32
|
-
import {
|
|
33
|
-
import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
|
|
30
|
+
import { estimateTaskGroupTokens, } from "./orchestrator/reviewPackets.js";
|
|
34
31
|
import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
|
|
35
32
|
import { runAuditCodeMcpServer } from "./mcp/server.js";
|
|
36
|
-
import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, detectRateLimitError, computeCooldownUntil, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, getHeaderExtractorForProvider, } from "./quota/index.js";
|
|
33
|
+
import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, detectRateLimitError, computeCooldownUntil, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, getHeaderExtractorForProvider, setQuotaStateDir, } from "./quota/index.js";
|
|
34
|
+
// Re-exports from extracted modules
|
|
35
|
+
export { resolveHostDispatchCapability, DIRECT_CLI_DEFAULTS, getFlag, hasFlag, getOptionalBooleanFlag, getArtifactsDir, getRootDir, getBatchResultsDir, getMaxRuns, getAgentBatchSize, getParallelWorkers, getTimeoutMs, chunkArray, getUiMode, looksLikeCliFlag, countLines, warnIfNotGitRepo, } from "./cli/args.js";
|
|
36
|
+
import { DIRECT_CLI_DEFAULTS, getFlag, hasFlag, getOptionalBooleanFlag, fromBase64Url, renderCommand, summarizeLaunchExit, taskResultPath, readStdinText, getArtifactsDir, getRootDir, warnIfNotGitRepo, getBatchResultsDir, getMaxRuns, getAgentBatchSize, getParallelWorkers, getTimeoutMs, getExplicitProvider, getHostModel, getHostMaxActiveSubagents, getQuotaProbeMode, resolveRunProviderName, chunkArray, getUiMode, looksLikeCliFlag, resolveHostDispatchCapability, countLines, listBatchResultFiles, } from "./cli/args.js";
|
|
37
|
+
import { nextStepCommand, mergeAndIngestCommand, renderDispatchReviewPrompt, renderSingleTaskFallbackStepPrompt, renderPresentReportPrompt, renderBlockedStepPrompt, } from "./cli/prompts.js";
|
|
38
|
+
import { writeCurrentStep, } from "./cli/steps.js";
|
|
39
|
+
import { WORKER_RESULT_CONTRACT_VERSION, buildWorkerResult, persistWorkerRunArtifacts, isWorkerResult, buildWorkerFailureBlocker, formatAuditResultValidationError, } from "./cli/workerResult.js";
|
|
40
|
+
import { DISPATCH_RESULT_MAP_FILENAME, ACTIVE_DISPATCH_FILENAME, resolveRunScopedArg, loadDispatchResultMap, entriesByTaskId, buildPendingAuditTasks, prepareDispatchArtifacts, } from "./cli/dispatch.js";
|
|
41
|
+
import { readWaveManifest, writeWaveManifest, removeWaveManifest, buildWaveSlotEntry, } from "./cli/waveManifest.js";
|
|
37
42
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
38
43
|
const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
|
|
39
|
-
const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
|
|
40
|
-
const STEP_CONTRACT_VERSION = "audit-code-step/v1alpha1";
|
|
41
|
-
const LARGE_FILE_PACKET_TARGET_LINES = 2500;
|
|
42
|
-
const SMALL_MODEL_HINT_MAX_LINES = 500;
|
|
43
|
-
const SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS = 3000;
|
|
44
|
-
const DEEP_MODEL_HINT_MIN_ESTIMATED_TOKENS = 9000;
|
|
45
|
-
const DIRECT_CLI_DEFAULTS = {
|
|
46
|
-
rootDir: ".",
|
|
47
|
-
artifactsDir: ".artifacts",
|
|
48
|
-
maxRuns: 1000,
|
|
49
|
-
agentBatchSize: 6,
|
|
50
|
-
parallelWorkers: 1,
|
|
51
|
-
timeoutMs: 30 * 60 * 1000, // 30 minutes
|
|
52
|
-
uiMode: "headless",
|
|
53
|
-
};
|
|
54
|
-
// Keep the sample-run payload explicit so the demo command is deterministic.
|
|
55
44
|
const SAMPLE_REPO_FILES = [
|
|
56
45
|
{ path: "src/api/auth.ts", size_bytes: 1240, hash: "abc123" },
|
|
57
46
|
{ path: "src/lib/session.ts", size_bytes: 980, hash: "def456" },
|
|
58
47
|
{ path: "infra/deploy.yml", size_bytes: 420, hash: "ghi789" },
|
|
59
48
|
{ path: "docs/notes.md", size_bytes: 300, hash: "doc111" },
|
|
60
49
|
];
|
|
61
|
-
function isLongFlagToken(value) {
|
|
62
|
-
return typeof value === "string" && value.startsWith("--");
|
|
63
|
-
}
|
|
64
|
-
// Read a long-form CLI flag while treating a following flag token as "missing".
|
|
65
|
-
function getFlag(argv, name, fallback) {
|
|
66
|
-
const index = argv.indexOf(name);
|
|
67
|
-
if (index < 0)
|
|
68
|
-
return fallback;
|
|
69
|
-
const candidate = argv[index + 1];
|
|
70
|
-
if (!candidate || isLongFlagToken(candidate))
|
|
71
|
-
return fallback;
|
|
72
|
-
return candidate;
|
|
73
|
-
}
|
|
74
|
-
// Boolean flags only care whether the token is present at all.
|
|
75
|
-
function hasFlag(argv, name) {
|
|
76
|
-
return argv.includes(name);
|
|
77
|
-
}
|
|
78
|
-
function getOptionalBooleanFlag(argv, name) {
|
|
79
|
-
const raw = getFlag(argv, name);
|
|
80
|
-
if (raw === undefined) {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
if (raw === "true") {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
if (raw === "false") {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
throw new Error(`${name} must be either true or false.`);
|
|
90
|
-
}
|
|
91
|
-
function optionalBooleanEnv(value) {
|
|
92
|
-
if (value === "true")
|
|
93
|
-
return true;
|
|
94
|
-
if (value === "false")
|
|
95
|
-
return false;
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
export function resolveHostDispatchCapability(options) {
|
|
99
|
-
if (options.explicit !== undefined) {
|
|
100
|
-
return options.explicit;
|
|
101
|
-
}
|
|
102
|
-
if (options.sessionConfig.host_can_dispatch_subagents !== undefined) {
|
|
103
|
-
return options.sessionConfig.host_can_dispatch_subagents;
|
|
104
|
-
}
|
|
105
|
-
return optionalBooleanEnv((options.env ?? process.env).AUDIT_CODE_HOST_CAN_DISPATCH) ?? true;
|
|
106
|
-
}
|
|
107
|
-
function toBase64Url(value) {
|
|
108
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
109
|
-
}
|
|
110
|
-
function fromBase64Url(value) {
|
|
111
|
-
return Buffer.from(value, "base64url").toString("utf8");
|
|
112
|
-
}
|
|
113
|
-
function digestId(value) {
|
|
114
|
-
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
115
|
-
}
|
|
116
|
-
function safeArtifactStem(value) {
|
|
117
|
-
const sanitized = value
|
|
118
|
-
.replace(/[^a-zA-Z0-9_-]+/g, "_")
|
|
119
|
-
.replace(/^_+|_+$/g, "")
|
|
120
|
-
.slice(0, 80);
|
|
121
|
-
return sanitized.length > 0 ? sanitized : "artifact";
|
|
122
|
-
}
|
|
123
|
-
function artifactNameForId(value, extension) {
|
|
124
|
-
return `${safeArtifactStem(value)}_${digestId(value)}.${extension}`;
|
|
125
|
-
}
|
|
126
|
-
function quoteCommandArg(value) {
|
|
127
|
-
return /[\s"]/u.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
128
|
-
}
|
|
129
|
-
function renderCommand(argv) {
|
|
130
|
-
return argv.map((item) => quoteCommandArg(item)).join(" ");
|
|
131
|
-
}
|
|
132
|
-
function summarizeLaunchExit(result) {
|
|
133
|
-
if (result.accepted !== false && !result.error) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
const parts = [
|
|
137
|
-
result.signal
|
|
138
|
-
? `signal ${result.signal}`
|
|
139
|
-
: `exit code ${result.exitCode ?? "unknown"}`,
|
|
140
|
-
result.command ? `command: ${result.command}` : null,
|
|
141
|
-
result.stdoutPath ? `stdout: ${result.stdoutPath}` : null,
|
|
142
|
-
result.stderrPath ? `stderr: ${result.stderrPath}` : null,
|
|
143
|
-
result.error ?? null,
|
|
144
|
-
].filter((part) => Boolean(part));
|
|
145
|
-
return parts.join("; ");
|
|
146
|
-
}
|
|
147
|
-
function taskResultPath(taskResultsDir, taskId) {
|
|
148
|
-
return join(taskResultsDir, artifactNameForId(taskId, "json"));
|
|
149
|
-
}
|
|
150
|
-
function packetPromptPath(taskResultsDir, packetId) {
|
|
151
|
-
return join(taskResultsDir, artifactNameForId(packetId, "prompt.md"));
|
|
152
|
-
}
|
|
153
|
-
async function readStdinText() {
|
|
154
|
-
if (process.stdin.isTTY) {
|
|
155
|
-
return "";
|
|
156
|
-
}
|
|
157
|
-
return await new Promise((resolveInput, reject) => {
|
|
158
|
-
let input = "";
|
|
159
|
-
process.stdin.setEncoding("utf8");
|
|
160
|
-
process.stdin.on("data", (chunk) => {
|
|
161
|
-
input += chunk;
|
|
162
|
-
});
|
|
163
|
-
process.stdin.on("end", () => resolveInput(input));
|
|
164
|
-
process.stdin.on("error", reject);
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
function resolveFlagPath(argv, name, fallback) {
|
|
168
|
-
return resolve(getFlag(argv, name, fallback));
|
|
169
|
-
}
|
|
170
|
-
function normalizePositiveInteger(value) {
|
|
171
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
return Math.floor(value);
|
|
175
|
-
}
|
|
176
|
-
function parsePositiveIntegerFlag(argv, name) {
|
|
177
|
-
const raw = getFlag(argv, name);
|
|
178
|
-
if (raw === undefined) {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
return normalizePositiveInteger(Number(raw));
|
|
182
|
-
}
|
|
183
|
-
function getArtifactsDir(argv) {
|
|
184
|
-
return resolveFlagPath(argv, "--artifacts-dir", DIRECT_CLI_DEFAULTS.artifactsDir);
|
|
185
|
-
}
|
|
186
|
-
function getRootDir(argv) {
|
|
187
|
-
return resolveFlagPath(argv, "--root", DIRECT_CLI_DEFAULTS.rootDir);
|
|
188
|
-
}
|
|
189
|
-
function warnIfNotGitRepo(root) {
|
|
190
|
-
const gitEntry = join(root, ".git");
|
|
191
|
-
if (!existsSync(gitEntry)) {
|
|
192
|
-
console.warn(`Warning: target directory '${root}' does not appear to be a git repository. Diff-based signals will be unavailable.`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
function getBatchResultsDir(argv) {
|
|
196
|
-
const value = getFlag(argv, "--batch-results");
|
|
197
|
-
return value ? resolve(value) : undefined;
|
|
198
|
-
}
|
|
199
|
-
function getMaxRuns(argv) {
|
|
200
|
-
return parsePositiveIntegerFlag(argv, "--max-runs") ?? DIRECT_CLI_DEFAULTS.maxRuns;
|
|
201
|
-
}
|
|
202
|
-
function getAgentBatchSize(argv, sessionConfig) {
|
|
203
|
-
return (parsePositiveIntegerFlag(argv, "--agent-batch-size") ??
|
|
204
|
-
normalizePositiveInteger(sessionConfig.agent_task_batch_size) ??
|
|
205
|
-
DIRECT_CLI_DEFAULTS.agentBatchSize);
|
|
206
|
-
}
|
|
207
|
-
function getParallelWorkers(argv, sessionConfig) {
|
|
208
|
-
return (parsePositiveIntegerFlag(argv, "--parallel") ??
|
|
209
|
-
normalizePositiveInteger(sessionConfig.parallel_workers) ??
|
|
210
|
-
DIRECT_CLI_DEFAULTS.parallelWorkers);
|
|
211
|
-
}
|
|
212
|
-
function getTimeoutMs(argv, sessionConfig) {
|
|
213
|
-
return (parsePositiveIntegerFlag(argv, "--timeout") ??
|
|
214
|
-
normalizePositiveInteger(sessionConfig.timeout_ms) ??
|
|
215
|
-
DIRECT_CLI_DEFAULTS.timeoutMs);
|
|
216
|
-
}
|
|
217
|
-
function getExplicitProvider(argv) {
|
|
218
|
-
return getFlag(argv, "--provider");
|
|
219
|
-
}
|
|
220
|
-
function getHostModel(argv) {
|
|
221
|
-
return getFlag(argv, "--host-model") ?? null;
|
|
222
|
-
}
|
|
223
|
-
function getHostMaxActiveSubagents(argv) {
|
|
224
|
-
return parsePositiveIntegerFlag(argv, "--host-max-active-subagents") ?? null;
|
|
225
|
-
}
|
|
226
|
-
function getQuotaProbeMode(argv, sessionConfig) {
|
|
227
|
-
const raw = getFlag(argv, "--quota-probe") ?? sessionConfig.quota?.probe ?? "auto";
|
|
228
|
-
if (raw === "auto" || raw === "never" || raw === "force")
|
|
229
|
-
return raw;
|
|
230
|
-
return "auto";
|
|
231
|
-
}
|
|
232
|
-
function resolveRunProviderName(argv, sessionConfig) {
|
|
233
|
-
return resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig);
|
|
234
|
-
}
|
|
235
|
-
function chunkArray(arr, size) {
|
|
236
|
-
const chunkSize = normalizePositiveInteger(size);
|
|
237
|
-
if (chunkSize === undefined) {
|
|
238
|
-
throw new Error("chunkArray size must be a positive integer.");
|
|
239
|
-
}
|
|
240
|
-
const chunks = [];
|
|
241
|
-
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
242
|
-
chunks.push(arr.slice(i, i + chunkSize));
|
|
243
|
-
}
|
|
244
|
-
return chunks;
|
|
245
|
-
}
|
|
246
|
-
function getUiMode(argv, fallback = DIRECT_CLI_DEFAULTS.uiMode) {
|
|
247
|
-
const raw = getFlag(argv, "--ui");
|
|
248
|
-
if (raw === "visible")
|
|
249
|
-
return "visible";
|
|
250
|
-
if (raw === "headless")
|
|
251
|
-
return "headless";
|
|
252
|
-
return fallback;
|
|
253
|
-
}
|
|
254
50
|
function buildEnvelope(params) {
|
|
255
51
|
return {
|
|
256
52
|
contract_version: ADVANCE_AUDIT_CONTRACT_VERSION,
|
|
@@ -312,30 +108,6 @@ function buildBlockedAuditState(params) {
|
|
|
312
108
|
: item),
|
|
313
109
|
};
|
|
314
110
|
}
|
|
315
|
-
async function countLines(path) {
|
|
316
|
-
return new Promise((resolve, reject) => {
|
|
317
|
-
let lines = 0;
|
|
318
|
-
let byteCount = 0;
|
|
319
|
-
let lastByte = -1;
|
|
320
|
-
const stream = createReadStream(path);
|
|
321
|
-
stream.on("data", (chunk) => {
|
|
322
|
-
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
323
|
-
byteCount += buffer.length;
|
|
324
|
-
for (let i = 0; i < buffer.length; ++i) {
|
|
325
|
-
if (buffer[i] === 10)
|
|
326
|
-
lines++;
|
|
327
|
-
lastByte = buffer[i];
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
stream.on("end", () => {
|
|
331
|
-
if (byteCount === 0)
|
|
332
|
-
return resolve(0);
|
|
333
|
-
// Files not ending with \n have one final line not counted above
|
|
334
|
-
resolve(lastByte !== 10 ? lines + 1 : lines);
|
|
335
|
-
});
|
|
336
|
-
stream.on("error", reject);
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
111
|
async function buildLineIndex(root, repoManifest) {
|
|
340
112
|
const entries = [];
|
|
341
113
|
const batchSize = 25;
|
|
@@ -368,26 +140,6 @@ async function buildLineIndexForPaths(root, paths) {
|
|
|
368
140
|
}));
|
|
369
141
|
return Object.fromEntries(entries);
|
|
370
142
|
}
|
|
371
|
-
async function listBatchResultFiles(batchDir) {
|
|
372
|
-
const entries = await readdir(batchDir, { withFileTypes: true });
|
|
373
|
-
const files = entries
|
|
374
|
-
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
|
|
375
|
-
.map((entry) => join(batchDir, entry.name))
|
|
376
|
-
.sort((a, b) => a.localeCompare(b));
|
|
377
|
-
if (files.length === 0) {
|
|
378
|
-
throw new Error(`No JSON audit result files found in ${batchDir}.`);
|
|
379
|
-
}
|
|
380
|
-
return files;
|
|
381
|
-
}
|
|
382
|
-
function buildPendingAuditTasks(bundle) {
|
|
383
|
-
const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
|
|
384
|
-
const pendingTasks = (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
|
|
385
|
-
const lineIndex = Object.fromEntries(pendingTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
|
|
386
|
-
return orderTasksForPacketReview(pendingTasks, {
|
|
387
|
-
graphBundle: bundle.graph_bundle,
|
|
388
|
-
lineIndex,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
143
|
async function addFileLineCountHints(root, tasks) {
|
|
392
144
|
const lineIndex = await buildLineIndexForPaths(root, tasks.flatMap((task) => task.file_paths));
|
|
393
145
|
return tasks.map((task) => ({
|
|
@@ -477,6 +229,11 @@ async function ensureSemanticReviewRun(params) {
|
|
|
477
229
|
const pendingTasks = await addFileLineCountHints(params.root, buildPendingAuditTasks(params.bundle));
|
|
478
230
|
const pendingTasksPath = join(paths.runDir, "pending-audit-tasks.json");
|
|
479
231
|
const auditResultsPath = join(paths.runDir, "audit-results.json");
|
|
232
|
+
const taskReadPaths = new Set();
|
|
233
|
+
for (const pt of pendingTasks) {
|
|
234
|
+
for (const fp of pt.file_paths)
|
|
235
|
+
taskReadPaths.add(fp);
|
|
236
|
+
}
|
|
480
237
|
const task = {
|
|
481
238
|
contract_version: "audit-code-worker/v1alpha1",
|
|
482
239
|
run_id: runId,
|
|
@@ -496,6 +253,10 @@ async function ensureSemanticReviewRun(params) {
|
|
|
496
253
|
pending_audit_tasks_path: pendingTasksPath,
|
|
497
254
|
timeout_ms: params.timeoutMs,
|
|
498
255
|
max_retries: 0,
|
|
256
|
+
access: {
|
|
257
|
+
read_paths: [...taskReadPaths],
|
|
258
|
+
write_paths: [auditResultsPath, paths.resultPath],
|
|
259
|
+
},
|
|
499
260
|
};
|
|
500
261
|
const prompt = renderWorkerPrompt(task);
|
|
501
262
|
await writeWorkerTaskFiles(task, prompt, paths, params.artifactsDir, pendingTasks);
|
|
@@ -519,174 +280,6 @@ async function ensureSemanticReviewRun(params) {
|
|
|
519
280
|
});
|
|
520
281
|
return { state: blockedState, bundle: blockedBundle, activeReviewRun };
|
|
521
282
|
}
|
|
522
|
-
function nextStepCommand(root, artifactsDir, extraArgs = []) {
|
|
523
|
-
return renderCommand([
|
|
524
|
-
"audit-code",
|
|
525
|
-
"next-step",
|
|
526
|
-
"--root",
|
|
527
|
-
root,
|
|
528
|
-
"--artifacts-dir",
|
|
529
|
-
artifactsDir,
|
|
530
|
-
...extraArgs,
|
|
531
|
-
]);
|
|
532
|
-
}
|
|
533
|
-
function mergeAndIngestCommand(artifactsDir, runId) {
|
|
534
|
-
return renderCommand([
|
|
535
|
-
"audit-code",
|
|
536
|
-
"merge-and-ingest",
|
|
537
|
-
"--run-id",
|
|
538
|
-
runId,
|
|
539
|
-
"--artifacts-dir",
|
|
540
|
-
artifactsDir,
|
|
541
|
-
]);
|
|
542
|
-
}
|
|
543
|
-
function renderDispatchReviewPrompt(params) {
|
|
544
|
-
const mergeCommand = mergeAndIngestCommand(params.artifactsDir, params.activeReviewRun.run_id);
|
|
545
|
-
const continueCommand = nextStepCommand(params.root, params.artifactsDir);
|
|
546
|
-
const modelLine = params.hostCanSelectSubagentModel
|
|
547
|
-
? "When launching each subagent, map `entry.model_hint.tier` (`small`, `standard`, `deep`) to an available host model without asking the user for model names."
|
|
548
|
-
: "Ignore `entry.model_hint`; this host did not report per-subagent model selection.";
|
|
549
|
-
const toolsLine = params.hostCanRestrictSubagentTools
|
|
550
|
-
? "Restrict review subagents to read/search plus the packet submit command named in their prompt. Do not give them source edit/write tools."
|
|
551
|
-
: "Do not ask the user about per-subagent tool restrictions; this host did not report a callable restriction facility.";
|
|
552
|
-
const dispatchDataLines = params.dispatchQuotaPath
|
|
553
|
-
? [
|
|
554
|
-
"Read these generated files unless the current tool response already included equivalent `dispatch_plan_entries` and `dispatch_quota` fields:",
|
|
555
|
-
"",
|
|
556
|
-
` Dispatch plan: ${params.dispatchPlanPath}`,
|
|
557
|
-
` Dispatch quota: ${params.dispatchQuotaPath}`,
|
|
558
|
-
"",
|
|
559
|
-
"Use the `wave_size` from the quota data. If `cooldown_until` is non-null, wait until that timestamp before starting the first wave.",
|
|
560
|
-
"",
|
|
561
|
-
"`host_concurrency_limit` records any detected hard host cap that contributed to `wave_size`.",
|
|
562
|
-
"",
|
|
563
|
-
"For each wave: use the `task` tool (or equivalent subagent dispatch) to launch up to `wave_size` subagents in parallel (one per entry), wait for all to finish, then start the next wave.",
|
|
564
|
-
]
|
|
565
|
-
: [
|
|
566
|
-
"Read this generated dispatch plan unless the current tool response already included equivalent `dispatch_plan_entries`:",
|
|
567
|
-
"",
|
|
568
|
-
` ${params.dispatchPlanPath}`,
|
|
569
|
-
"",
|
|
570
|
-
"Launch one subagent for each entry in the plan.",
|
|
571
|
-
];
|
|
572
|
-
return [
|
|
573
|
-
"# audit-code dispatch review",
|
|
574
|
-
"",
|
|
575
|
-
...dispatchDataLines,
|
|
576
|
-
"",
|
|
577
|
-
"Pass each `entry.prompt_path` literally to its subagent; do not load packet prompt files into this orchestrator context.",
|
|
578
|
-
"",
|
|
579
|
-
"Subagent prompt shape:",
|
|
580
|
-
"",
|
|
581
|
-
' Read and follow the audit instructions in: <entry.prompt_path>',
|
|
582
|
-
"",
|
|
583
|
-
modelLine,
|
|
584
|
-
toolsLine,
|
|
585
|
-
"",
|
|
586
|
-
"Each subagent must submit its packet through the submit command printed in its packet prompt and stop after successful submission.",
|
|
587
|
-
"",
|
|
588
|
-
"**After all waves complete:**",
|
|
589
|
-
"",
|
|
590
|
-
"Run exactly:",
|
|
591
|
-
"",
|
|
592
|
-
` ${mergeCommand}`,
|
|
593
|
-
"",
|
|
594
|
-
"If merge-and-ingest fails, stop and report the exact command and error output. Do not manually merge results or edit audit state.",
|
|
595
|
-
"",
|
|
596
|
-
"If merge-and-ingest succeeds, run:",
|
|
597
|
-
"",
|
|
598
|
-
` ${continueCommand}`,
|
|
599
|
-
"",
|
|
600
|
-
"Read and follow only the new step prompt path returned by that command.",
|
|
601
|
-
"",
|
|
602
|
-
].join("\n");
|
|
603
|
-
}
|
|
604
|
-
function renderSingleTaskFallbackStepPrompt(params) {
|
|
605
|
-
return [
|
|
606
|
-
"# audit-code single-task fallback step",
|
|
607
|
-
"",
|
|
608
|
-
"Use this step only because the host reported no callable subagent facility.",
|
|
609
|
-
"",
|
|
610
|
-
"Read and follow exactly this generated single-task prompt:",
|
|
611
|
-
"",
|
|
612
|
-
` ${params.singleTaskPromptPath}`,
|
|
613
|
-
"",
|
|
614
|
-
"Complete exactly one AuditResult for the task named there, write the JSON array to the prompt's audit_results_path, run the exact worker_command from that prompt, then stop.",
|
|
615
|
-
"",
|
|
616
|
-
"Do not run dispatch commands, do not prepare packets, do not run next-step again in this turn, and do not read a report after the worker command.",
|
|
617
|
-
"",
|
|
618
|
-
"The only backend command allowed after writing the result is:",
|
|
619
|
-
"",
|
|
620
|
-
` ${renderCommand(params.activeReviewRun.worker_command)}`,
|
|
621
|
-
"",
|
|
622
|
-
].join("\n");
|
|
623
|
-
}
|
|
624
|
-
function renderPresentReportPrompt(finalReportPath) {
|
|
625
|
-
return [
|
|
626
|
-
"# audit-code present report",
|
|
627
|
-
"",
|
|
628
|
-
"The deterministic audit is complete.",
|
|
629
|
-
"",
|
|
630
|
-
"Read the final audit report from the `audit-code://report/current` MCP resource (or from the file at:",
|
|
631
|
-
` ${finalReportPath}`,
|
|
632
|
-
"if a Read tool is available).",
|
|
633
|
-
"",
|
|
634
|
-
"Present the completed audit with work blocks first.",
|
|
635
|
-
"",
|
|
636
|
-
"Do not run the orchestrator again for this completed audit.",
|
|
637
|
-
"",
|
|
638
|
-
].join("\n");
|
|
639
|
-
}
|
|
640
|
-
function renderBlockedStepPrompt(reason) {
|
|
641
|
-
return [
|
|
642
|
-
"# audit-code blocked",
|
|
643
|
-
"",
|
|
644
|
-
"The audit cannot continue automatically from this step.",
|
|
645
|
-
"",
|
|
646
|
-
"Report this blocker verbatim and stop:",
|
|
647
|
-
"",
|
|
648
|
-
reason,
|
|
649
|
-
"",
|
|
650
|
-
].join("\n");
|
|
651
|
-
}
|
|
652
|
-
async function writeCurrentStep(params) {
|
|
653
|
-
const stepsDir = join(params.artifactsDir, "steps");
|
|
654
|
-
await mkdir(stepsDir, { recursive: true });
|
|
655
|
-
const promptPath = join(stepsDir, "current-prompt.md");
|
|
656
|
-
const stepPath = join(stepsDir, "current-step.json");
|
|
657
|
-
await writeFile(promptPath, params.prompt, "utf8");
|
|
658
|
-
const step = {
|
|
659
|
-
contract_version: STEP_CONTRACT_VERSION,
|
|
660
|
-
step_kind: params.stepKind,
|
|
661
|
-
prompt_path: promptPath,
|
|
662
|
-
status: params.status,
|
|
663
|
-
run_id: params.runId,
|
|
664
|
-
allowed_commands: params.allowedCommands,
|
|
665
|
-
stop_condition: params.stopCondition,
|
|
666
|
-
repo_root: params.repoRoot,
|
|
667
|
-
artifacts_dir: params.artifactsDir,
|
|
668
|
-
artifact_paths: {
|
|
669
|
-
current_step: stepPath,
|
|
670
|
-
current_prompt: promptPath,
|
|
671
|
-
...params.artifactPaths,
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
await writeJsonFile(stepPath, step);
|
|
675
|
-
return step;
|
|
676
|
-
}
|
|
677
|
-
function formatAuditResultValidationError(issues) {
|
|
678
|
-
return (`audit-results validation failed with ${issues.length} error(s):\n` +
|
|
679
|
-
formatAuditResultIssues(issues));
|
|
680
|
-
}
|
|
681
|
-
function buildWorkerFailureBlocker(workerResult) {
|
|
682
|
-
const details = workerResult.errors.filter((error) => error.trim().length > 0);
|
|
683
|
-
return details.length > 0
|
|
684
|
-
? `${workerResult.summary} ${details.join(" ")}`
|
|
685
|
-
: workerResult.summary;
|
|
686
|
-
}
|
|
687
|
-
function looksLikeCliFlag(value) {
|
|
688
|
-
return isLongFlagToken(value);
|
|
689
|
-
}
|
|
690
283
|
export const cliTestUtils = {
|
|
691
284
|
defaults: DIRECT_CLI_DEFAULTS,
|
|
692
285
|
getFlag,
|
|
@@ -757,6 +350,7 @@ async function runAuditStep(options) {
|
|
|
757
350
|
runtimeValidationUpdates,
|
|
758
351
|
externalAnalyzerResults,
|
|
759
352
|
preferredExecutor: options.preferredExecutor,
|
|
353
|
+
opentoken: options.opentoken,
|
|
760
354
|
});
|
|
761
355
|
await writeCoreArtifacts(options.artifactsDir, result.updated_bundle);
|
|
762
356
|
const archivedPendingResults = await maybeArchiveLegacyPendingResults(options.auditResultsPath);
|
|
@@ -805,29 +399,6 @@ async function ingestBatchAuditResults(options) {
|
|
|
805
399
|
next_likely_step: state.status === "complete" ? null : decision.selected_obligation,
|
|
806
400
|
};
|
|
807
401
|
}
|
|
808
|
-
function buildWorkerResult(params) {
|
|
809
|
-
return {
|
|
810
|
-
contract_version: WORKER_RESULT_CONTRACT_VERSION,
|
|
811
|
-
run_id: params.runId,
|
|
812
|
-
obligation_id: params.obligationId,
|
|
813
|
-
status: params.status,
|
|
814
|
-
progress_made: params.progressMade,
|
|
815
|
-
selected_executor: params.selectedExecutor,
|
|
816
|
-
artifacts_written: params.artifactsWritten,
|
|
817
|
-
summary: params.summary,
|
|
818
|
-
next_likely_step: params.nextLikelyStep,
|
|
819
|
-
errors: params.errors,
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
async function persistWorkerRunArtifacts(paths, workerResult, executionMode) {
|
|
823
|
-
await writeJsonFile(paths.resultPath, workerResult);
|
|
824
|
-
await writeJsonFile(paths.statusPath, {
|
|
825
|
-
run_id: workerResult.run_id,
|
|
826
|
-
status: workerResult.status,
|
|
827
|
-
execution_mode: executionMode,
|
|
828
|
-
result_path: paths.resultPath,
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
402
|
async function persistConfigErrorHandoff(params) {
|
|
832
403
|
const bundle = await loadArtifactBundle(params.artifactsDir);
|
|
833
404
|
const blockedState = buildBlockedAuditState({
|
|
@@ -850,12 +421,6 @@ async function persistConfigErrorHandoff(params) {
|
|
|
850
421
|
});
|
|
851
422
|
await writeAuditCodeHandoffArtifacts(handoff);
|
|
852
423
|
}
|
|
853
|
-
function isWorkerResult(value) {
|
|
854
|
-
return (typeof value === "object" &&
|
|
855
|
-
value !== null &&
|
|
856
|
-
value.contract_version ===
|
|
857
|
-
WORKER_RESULT_CONTRACT_VERSION);
|
|
858
|
-
}
|
|
859
424
|
export async function runSample(argv = process.argv) {
|
|
860
425
|
const repoManifest = buildRepoManifest("sample-repo", SAMPLE_REPO_FILES);
|
|
861
426
|
const disposition = buildFileDisposition(repoManifest);
|
|
@@ -990,6 +555,7 @@ async function cmdAdvanceAudit(argv) {
|
|
|
990
555
|
auditResultsPath: getFlag(argv, "--results"),
|
|
991
556
|
runtimeUpdatesPath: getFlag(argv, "--updates"),
|
|
992
557
|
externalAnalyzerPath,
|
|
558
|
+
opentoken: sessionConfig.opentoken?.enabled,
|
|
993
559
|
});
|
|
994
560
|
if (result.selected_executor !== "agent") {
|
|
995
561
|
await clearDispatchFiles(artifactsDir);
|
|
@@ -1039,6 +605,22 @@ async function runDeterministicForNextStep(params) {
|
|
|
1039
605
|
: join(params.artifactsDir, "audit-report.md"),
|
|
1040
606
|
};
|
|
1041
607
|
}
|
|
608
|
+
if (index === 0 && bundle.repo_manifest) {
|
|
609
|
+
const pendingTasks = buildPendingAuditTasks(bundle);
|
|
610
|
+
const taskFiles = new Set();
|
|
611
|
+
for (const task of pendingTasks) {
|
|
612
|
+
for (const fp of Object.keys(task.file_line_counts ?? {}))
|
|
613
|
+
taskFiles.add(fp);
|
|
614
|
+
}
|
|
615
|
+
if (taskFiles.size > 0) {
|
|
616
|
+
const integrity = await checkFileIntegrity(params.root, bundle.repo_manifest, [...taskFiles]);
|
|
617
|
+
if (!integrity.is_clean) {
|
|
618
|
+
console.log(`File integrity check: ${integrity.changed_files.length} changed, ${integrity.missing_files.length} missing — re-running intake.`);
|
|
619
|
+
await advanceAudit(bundle, { root: params.root, preferredExecutor: "intake_executor", opentoken: params.opentoken });
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
1042
624
|
if (decision.selected_executor === "design_review") {
|
|
1043
625
|
const findingsPath = join(params.artifactsDir, "incoming", "design-review-findings.json");
|
|
1044
626
|
let reviewFindings;
|
|
@@ -1095,11 +677,42 @@ async function runDeterministicForNextStep(params) {
|
|
|
1095
677
|
reason: lastSummary || decision.reason,
|
|
1096
678
|
};
|
|
1097
679
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
680
|
+
let result;
|
|
681
|
+
try {
|
|
682
|
+
result = await runAuditStep({
|
|
683
|
+
root: params.root,
|
|
684
|
+
artifactsDir: params.artifactsDir,
|
|
685
|
+
opentoken: params.opentoken,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
const current = await loadArtifactBundle(params.artifactsDir);
|
|
690
|
+
const currentState = deriveAuditState(current);
|
|
691
|
+
currentState.last_executor = decision.selected_executor ?? undefined;
|
|
692
|
+
currentState.last_obligation = decision.selected_obligation ?? undefined;
|
|
693
|
+
await writeCoreArtifacts(params.artifactsDir, { ...current, audit_state: currentState });
|
|
694
|
+
await writeJsonFile(join(params.artifactsDir, "steps", "deterministic-progress.json"), {
|
|
695
|
+
iteration: index + 1,
|
|
696
|
+
max_runs: params.maxRuns,
|
|
697
|
+
last_executor: decision.selected_executor,
|
|
698
|
+
last_obligation: decision.selected_obligation,
|
|
699
|
+
prior_summary: lastSummary || null,
|
|
700
|
+
error: error instanceof Error ? error.message : String(error),
|
|
701
|
+
timestamp: new Date().toISOString(),
|
|
702
|
+
});
|
|
703
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
704
|
+
throw new Error(`Deterministic executor ${decision.selected_executor} failed on obligation ${decision.selected_obligation} (iteration ${index + 1}/${params.maxRuns}, prior progress: ${lastSummary || "none"}): ${detail}`, { cause: error instanceof Error ? error : undefined });
|
|
705
|
+
}
|
|
1102
706
|
lastSummary = result.progress_summary;
|
|
707
|
+
await writeJsonFile(join(params.artifactsDir, "steps", "deterministic-progress.json"), {
|
|
708
|
+
iteration: index + 1,
|
|
709
|
+
max_runs: params.maxRuns,
|
|
710
|
+
last_executor: result.selected_executor,
|
|
711
|
+
last_obligation: decision.selected_obligation,
|
|
712
|
+
progress_made: result.progress_made,
|
|
713
|
+
summary: result.progress_summary,
|
|
714
|
+
timestamp: new Date().toISOString(),
|
|
715
|
+
});
|
|
1103
716
|
if (result.selected_executor !== "agent") {
|
|
1104
717
|
await clearDispatchFiles(params.artifactsDir);
|
|
1105
718
|
}
|
|
@@ -1169,6 +782,7 @@ async function cmdNextStep(argv) {
|
|
|
1169
782
|
selfCliPath: resolve(argv[1] ?? process.argv[1] ?? ""),
|
|
1170
783
|
timeoutMs: getTimeoutMs(argv, sessionConfig),
|
|
1171
784
|
maxRuns: getMaxRuns(argv),
|
|
785
|
+
opentoken: sessionConfig.opentoken?.enabled,
|
|
1172
786
|
});
|
|
1173
787
|
if (result.kind === "complete") {
|
|
1174
788
|
const step = await writeCurrentStep({
|
|
@@ -1258,11 +872,16 @@ async function cmdNextStep(argv) {
|
|
|
1258
872
|
singleTaskPromptPath,
|
|
1259
873
|
activeReviewRun: result.activeReviewRun,
|
|
1260
874
|
}),
|
|
875
|
+
access: {
|
|
876
|
+
read_paths: [singleTaskPromptPath],
|
|
877
|
+
write_paths: [result.activeReviewRun.audit_results_path],
|
|
878
|
+
},
|
|
1261
879
|
});
|
|
1262
880
|
console.log(JSON.stringify(step, null, 2));
|
|
1263
881
|
return;
|
|
1264
882
|
}
|
|
1265
883
|
const dispatch = await prepareDispatchArtifacts({
|
|
884
|
+
packageRoot,
|
|
1266
885
|
runId: result.activeReviewRun.run_id,
|
|
1267
886
|
artifactsDir,
|
|
1268
887
|
root,
|
|
@@ -1299,6 +918,13 @@ async function cmdNextStep(argv) {
|
|
|
1299
918
|
hostCanRestrictSubagentTools,
|
|
1300
919
|
hostCanSelectSubagentModel,
|
|
1301
920
|
}),
|
|
921
|
+
access: {
|
|
922
|
+
read_paths: [
|
|
923
|
+
dispatch.dispatch_plan_path,
|
|
924
|
+
...(dispatch.dispatch_quota_path ? [dispatch.dispatch_quota_path] : []),
|
|
925
|
+
],
|
|
926
|
+
write_paths: [],
|
|
927
|
+
},
|
|
1302
928
|
});
|
|
1303
929
|
console.log(JSON.stringify(step, null, 2));
|
|
1304
930
|
}
|
|
@@ -1349,6 +975,38 @@ async function cmdRunToCompletion(argv) {
|
|
|
1349
975
|
while (runCount < maxRuns) {
|
|
1350
976
|
const bundle = await loadArtifactBundle(artifactsDir);
|
|
1351
977
|
const decision = decideNextStep(bundle);
|
|
978
|
+
// Resume interrupted parallel wave: ingest any results that workers
|
|
979
|
+
// wrote before the previous process exited.
|
|
980
|
+
const priorWave = await readWaveManifest(artifactsDir);
|
|
981
|
+
if (priorWave) {
|
|
982
|
+
process.stderr.write(`[audit-code] Recovering interrupted wave (${priorWave.slots.length} slot(s), obligation ${priorWave.obligation_id}).\n`);
|
|
983
|
+
let recoveredProgress = false;
|
|
984
|
+
for (const entry of priorWave.slots) {
|
|
985
|
+
try {
|
|
986
|
+
const results = await readJsonFile(entry.audit_results_path);
|
|
987
|
+
if (!results || results.length === 0)
|
|
988
|
+
continue;
|
|
989
|
+
const stepResult = await runAuditStep({
|
|
990
|
+
root,
|
|
991
|
+
artifactsDir,
|
|
992
|
+
preferredExecutor: "result_ingestion_executor",
|
|
993
|
+
auditResultsPath: entry.audit_results_path,
|
|
994
|
+
});
|
|
995
|
+
if (stepResult.progress_made) {
|
|
996
|
+
recoveredProgress = true;
|
|
997
|
+
anyProgress = true;
|
|
998
|
+
for (const a of stepResult.artifacts_written)
|
|
999
|
+
artifactsWritten.add(a);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
process.stderr.write(`[audit-code] Skipping unreadable results for ${entry.run_id}.\n`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
await removeWaveManifest(artifactsDir);
|
|
1007
|
+
if (recoveredProgress)
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1352
1010
|
if (decision.selected_executor === "agent" &&
|
|
1353
1011
|
bundle.audit_tasks?.some((t) => t.tags?.includes("selective_deepening") &&
|
|
1354
1012
|
t.status !== "complete") &&
|
|
@@ -1402,6 +1060,11 @@ async function cmdRunToCompletion(argv) {
|
|
|
1402
1060
|
const blockPendingTasks = await addFileLineCountHints(root, buildPendingAuditTasks(bundle));
|
|
1403
1061
|
const blockPendingTasksPath = join(blockPaths.runDir, "pending-audit-tasks.json");
|
|
1404
1062
|
const blockAuditResultsPath = join(blockPaths.runDir, "audit-results.json");
|
|
1063
|
+
const blockReadPaths = new Set();
|
|
1064
|
+
for (const pt of blockPendingTasks) {
|
|
1065
|
+
for (const fp of pt.file_paths)
|
|
1066
|
+
blockReadPaths.add(fp);
|
|
1067
|
+
}
|
|
1405
1068
|
const blockTask = {
|
|
1406
1069
|
contract_version: "audit-code-worker/v1alpha1",
|
|
1407
1070
|
run_id: blockRunId,
|
|
@@ -1421,6 +1084,10 @@ async function cmdRunToCompletion(argv) {
|
|
|
1421
1084
|
pending_audit_tasks_path: blockPendingTasksPath,
|
|
1422
1085
|
timeout_ms: timeoutMs,
|
|
1423
1086
|
max_retries: 0,
|
|
1087
|
+
access: {
|
|
1088
|
+
read_paths: [...blockReadPaths],
|
|
1089
|
+
write_paths: [blockAuditResultsPath, blockPaths.resultPath],
|
|
1090
|
+
},
|
|
1424
1091
|
};
|
|
1425
1092
|
const blockPrompt = renderWorkerPrompt(blockTask);
|
|
1426
1093
|
await writeWorkerTaskFiles(blockTask, blockPrompt, blockPaths, artifactsDir, blockPendingTasks);
|
|
@@ -1526,6 +1193,11 @@ async function cmdRunToCompletion(argv) {
|
|
|
1526
1193
|
const slotPaths = getRunPaths(artifactsDir, slotRunId);
|
|
1527
1194
|
const slotAuditResultsPath = join(slotPaths.runDir, "audit-results.json");
|
|
1528
1195
|
const slotPendingTasksPath = join(slotPaths.runDir, "pending-audit-tasks.json");
|
|
1196
|
+
const slotReadPaths = new Set();
|
|
1197
|
+
for (const t of group) {
|
|
1198
|
+
for (const fp of t.file_paths)
|
|
1199
|
+
slotReadPaths.add(fp);
|
|
1200
|
+
}
|
|
1529
1201
|
const slotTask = {
|
|
1530
1202
|
contract_version: "audit-code-worker/v1alpha1",
|
|
1531
1203
|
run_id: slotRunId,
|
|
@@ -1540,6 +1212,10 @@ async function cmdRunToCompletion(argv) {
|
|
|
1540
1212
|
worker_command_mode: "deferred",
|
|
1541
1213
|
timeout_ms: timeoutMs,
|
|
1542
1214
|
max_retries: 0,
|
|
1215
|
+
access: {
|
|
1216
|
+
read_paths: [...slotReadPaths],
|
|
1217
|
+
write_paths: [slotAuditResultsPath, slotPaths.resultPath],
|
|
1218
|
+
},
|
|
1543
1219
|
};
|
|
1544
1220
|
const slotPrompt = renderWorkerPrompt(slotTask);
|
|
1545
1221
|
await writeWorkerTaskFiles(slotTask, slotPrompt, slotPaths, artifactsDir, group, { updateDispatch: false });
|
|
@@ -1556,6 +1232,12 @@ async function cmdRunToCompletion(argv) {
|
|
|
1556
1232
|
pending_audit_tasks_path: slot.pendingTasksPath,
|
|
1557
1233
|
})), workerSlots.flatMap((slot) => slot.group));
|
|
1558
1234
|
const parallelStartedAt = new Date().toISOString();
|
|
1235
|
+
await writeWaveManifest(artifactsDir, {
|
|
1236
|
+
obligation_id: obligationId ?? "unknown",
|
|
1237
|
+
started_at: parallelStartedAt,
|
|
1238
|
+
pid: process.pid,
|
|
1239
|
+
slots: workerSlots.map(buildWaveSlotEntry),
|
|
1240
|
+
});
|
|
1559
1241
|
const { results: launchResults } = await runSlidingWindow(workerSlots.map((slot) => () => provider.launch({
|
|
1560
1242
|
repoRoot: root,
|
|
1561
1243
|
runId: slot.runId,
|
|
@@ -1710,6 +1392,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
1710
1392
|
}
|
|
1711
1393
|
}
|
|
1712
1394
|
}
|
|
1395
|
+
await removeWaveManifest(artifactsDir);
|
|
1713
1396
|
if (batchErrors.length > 0) {
|
|
1714
1397
|
const bundleAfter = await loadArtifactBundle(artifactsDir);
|
|
1715
1398
|
const blockedState = buildBlockedAuditState({
|
|
@@ -1884,6 +1567,13 @@ async function cmdRunToCompletion(argv) {
|
|
|
1884
1567
|
const providerAuditResultsPath = preferredExecutor === "agent"
|
|
1885
1568
|
? join(paths.runDir, "audit-results.json")
|
|
1886
1569
|
: auditResultsPath;
|
|
1570
|
+
const providerReadPaths = new Set();
|
|
1571
|
+
if (pendingAuditTasks) {
|
|
1572
|
+
for (const pt of pendingAuditTasks) {
|
|
1573
|
+
for (const fp of pt.file_paths)
|
|
1574
|
+
providerReadPaths.add(fp);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1887
1577
|
const task = {
|
|
1888
1578
|
contract_version: "audit-code-worker/v1alpha1",
|
|
1889
1579
|
run_id: runId,
|
|
@@ -1905,6 +1595,10 @@ async function cmdRunToCompletion(argv) {
|
|
|
1905
1595
|
external_analyzer_results_path: externalAnalyzerPath,
|
|
1906
1596
|
timeout_ms: timeoutMs,
|
|
1907
1597
|
max_retries: 0,
|
|
1598
|
+
access: providerReadPaths.size > 0 ? {
|
|
1599
|
+
read_paths: [...providerReadPaths],
|
|
1600
|
+
write_paths: [providerAuditResultsPath ?? paths.resultPath, paths.resultPath],
|
|
1601
|
+
} : undefined,
|
|
1908
1602
|
};
|
|
1909
1603
|
const prompt = renderWorkerPrompt(task);
|
|
1910
1604
|
await writeWorkerTaskFiles(task, prompt, paths, artifactsDir, pendingAuditTasks);
|
|
@@ -2141,502 +1835,12 @@ async function cmdWorkerRun(argv) {
|
|
|
2141
1835
|
process.exitCode = 1;
|
|
2142
1836
|
}
|
|
2143
1837
|
}
|
|
2144
|
-
const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
|
|
2145
|
-
function dispatchResultMapPath(runDir) {
|
|
2146
|
-
return join(runDir, DISPATCH_RESULT_MAP_FILENAME);
|
|
2147
|
-
}
|
|
2148
|
-
function resolveRunScopedArg(argv, rawFlag, b64Flag) {
|
|
2149
|
-
const raw = getFlag(argv, rawFlag);
|
|
2150
|
-
const encoded = getFlag(argv, b64Flag);
|
|
2151
|
-
return raw ?? (encoded ? fromBase64Url(encoded) : undefined);
|
|
2152
|
-
}
|
|
2153
|
-
async function loadDispatchResultMap(runDir) {
|
|
2154
|
-
try {
|
|
2155
|
-
return await readJsonFile(dispatchResultMapPath(runDir));
|
|
2156
|
-
}
|
|
2157
|
-
catch (error) {
|
|
2158
|
-
if (!isFileMissingError(error)) {
|
|
2159
|
-
throw error;
|
|
2160
|
-
}
|
|
2161
|
-
return null;
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
function entriesByTaskId(entries) {
|
|
2165
|
-
return new Map(entries.map((entry) => [entry.task_id, entry]));
|
|
2166
|
-
}
|
|
2167
|
-
function isIsolatedLargeFilePacket(packet) {
|
|
2168
|
-
return (packet.file_paths.length === 1 &&
|
|
2169
|
-
packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES);
|
|
2170
|
-
}
|
|
2171
|
-
function buildDispatchComplexity(packet, largeFileMode) {
|
|
2172
|
-
return {
|
|
2173
|
-
priority: packet.priority,
|
|
2174
|
-
task_count: packet.task_ids.length,
|
|
2175
|
-
file_count: packet.file_paths.length,
|
|
2176
|
-
total_lines: packet.total_lines,
|
|
2177
|
-
estimated_tokens: packet.estimated_tokens,
|
|
2178
|
-
lenses: packet.lenses,
|
|
2179
|
-
tags: packet.tags ?? [],
|
|
2180
|
-
large_file_mode: largeFileMode,
|
|
2181
|
-
};
|
|
2182
|
-
}
|
|
2183
|
-
function buildDispatchModelHint(complexity) {
|
|
2184
|
-
const deepReasons = [];
|
|
2185
|
-
if (complexity.priority === "high")
|
|
2186
|
-
deepReasons.push("high_priority");
|
|
2187
|
-
if (complexity.large_file_mode)
|
|
2188
|
-
deepReasons.push("isolated_large_file");
|
|
2189
|
-
if (complexity.estimated_tokens >= DEEP_MODEL_HINT_MIN_ESTIMATED_TOKENS) {
|
|
2190
|
-
deepReasons.push("high_estimated_tokens");
|
|
2191
|
-
}
|
|
2192
|
-
if (complexity.tags.some((tag) => tag === "critical_flow" || tag.startsWith("critical_flow:"))) {
|
|
2193
|
-
deepReasons.push("critical_flow");
|
|
2194
|
-
}
|
|
2195
|
-
if (complexity.tags.some((tag) => tag === "external_analyzer_signal" || tag.startsWith("external_tool:"))) {
|
|
2196
|
-
deepReasons.push("external_analyzer_signal");
|
|
2197
|
-
}
|
|
2198
|
-
if (complexity.tags.includes("lens_verification")) {
|
|
2199
|
-
deepReasons.push("lens_verification");
|
|
2200
|
-
}
|
|
2201
|
-
if (deepReasons.length > 0) {
|
|
2202
|
-
return { tier: "deep", reasons: deepReasons };
|
|
2203
|
-
}
|
|
2204
|
-
const sensitiveLenses = new Set(["security", "data_integrity", "reliability"]);
|
|
2205
|
-
const hasSensitiveLens = complexity.lenses.some((lens) => sensitiveLenses.has(lens));
|
|
2206
|
-
if (complexity.priority === "low" &&
|
|
2207
|
-
complexity.total_lines <= SMALL_MODEL_HINT_MAX_LINES &&
|
|
2208
|
-
complexity.estimated_tokens <= SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS &&
|
|
2209
|
-
!hasSensitiveLens &&
|
|
2210
|
-
complexity.tags.length === 0) {
|
|
2211
|
-
return { tier: "small", reasons: ["small_low_priority_packet"] };
|
|
2212
|
-
}
|
|
2213
|
-
const reasons = [];
|
|
2214
|
-
if (complexity.priority === "medium")
|
|
2215
|
-
reasons.push("medium_priority");
|
|
2216
|
-
if (hasSensitiveLens)
|
|
2217
|
-
reasons.push("sensitive_lens");
|
|
2218
|
-
if (complexity.total_lines > SMALL_MODEL_HINT_MAX_LINES) {
|
|
2219
|
-
reasons.push("moderate_size");
|
|
2220
|
-
}
|
|
2221
|
-
return {
|
|
2222
|
-
tier: "standard",
|
|
2223
|
-
reasons: reasons.length > 0 ? reasons : ["default_review_packet"],
|
|
2224
|
-
};
|
|
2225
|
-
}
|
|
2226
|
-
function withinRoot(root, path) {
|
|
2227
|
-
const rootPath = resolve(root);
|
|
2228
|
-
const absolutePath = resolve(rootPath, path);
|
|
2229
|
-
const relativePath = relative(rootPath, absolutePath);
|
|
2230
|
-
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
2231
|
-
throw new Error(`Path '${path}' escapes repository root '${rootPath}'.`);
|
|
2232
|
-
}
|
|
2233
|
-
return absolutePath;
|
|
2234
|
-
}
|
|
2235
|
-
function renderAnchorPreview(summary, anchorPath) {
|
|
2236
|
-
const preview = summary.anchors.slice(0, 24).map((anchor) => {
|
|
2237
|
-
const location = anchor.line ? `${summary.path}:${anchor.line}` : summary.path;
|
|
2238
|
-
const detail = anchor.detail ? ` - ${anchor.detail}` : "";
|
|
2239
|
-
return `- ${location} [${anchor.kind}] ${anchor.name}${detail}`;
|
|
2240
|
-
});
|
|
2241
|
-
return [
|
|
2242
|
-
"## Large File Review Mode",
|
|
2243
|
-
"This packet is intentionally isolated because it covers one large file.",
|
|
2244
|
-
"Use targeted reads/searches within this file, guided by the mechanical anchors.",
|
|
2245
|
-
"Do not read unrelated files unless a finding cannot be evidenced without a direct boundary check.",
|
|
2246
|
-
`Anchor file: ${anchorPath}`,
|
|
2247
|
-
`Anchor counts: symbols=${summary.counts.symbols}, routes=${summary.counts.routes}, keywords=${summary.counts.keywords}, graph_edges=${summary.counts.graph_edges}, analyzer_signals=${summary.counts.analyzer_signals}, omitted=${summary.omitted_anchor_count}`,
|
|
2248
|
-
"Anchor preview:",
|
|
2249
|
-
...(preview.length > 0 ? preview : ["- no anchors extracted beyond file boundaries"]),
|
|
2250
|
-
"",
|
|
2251
|
-
];
|
|
2252
|
-
}
|
|
2253
|
-
function formatPacketConfidence(value) {
|
|
2254
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
2255
|
-
? value.toFixed(2)
|
|
2256
|
-
: "n/a";
|
|
2257
|
-
}
|
|
2258
|
-
function renderPacketGraphContext(packet) {
|
|
2259
|
-
const hasContext = (packet.entrypoints?.length ?? 0) > 0 ||
|
|
2260
|
-
(packet.key_edges?.length ?? 0) > 0 ||
|
|
2261
|
-
(packet.boundary_files?.length ?? 0) > 0 ||
|
|
2262
|
-
packet.quality !== undefined;
|
|
2263
|
-
if (!hasContext) {
|
|
2264
|
-
return [];
|
|
2265
|
-
}
|
|
2266
|
-
const lines = ["## Packet graph context"];
|
|
2267
|
-
if (packet.entrypoints?.length) {
|
|
2268
|
-
lines.push("Entrypoints:");
|
|
2269
|
-
lines.push(...packet.entrypoints.map((entrypoint) => `- ${entrypoint}`));
|
|
2270
|
-
}
|
|
2271
|
-
if (packet.key_edges?.length) {
|
|
2272
|
-
lines.push("Key internal edges:");
|
|
2273
|
-
lines.push(...packet.key_edges.map((edge) => {
|
|
2274
|
-
const kind = edge.kind ? ` [${edge.kind}]` : "";
|
|
2275
|
-
const reason = edge.reason ? ` - ${edge.reason}` : "";
|
|
2276
|
-
return `- ${edge.from} -> ${edge.to}${kind} confidence=${formatPacketConfidence(edge.confidence)}${reason}`;
|
|
2277
|
-
}));
|
|
2278
|
-
}
|
|
2279
|
-
if (packet.boundary_files?.length) {
|
|
2280
|
-
lines.push("Boundary files to check only when evidence crosses the packet:");
|
|
2281
|
-
lines.push(...packet.boundary_files.map((path) => `- ${path}`));
|
|
2282
|
-
}
|
|
2283
|
-
if (packet.quality) {
|
|
2284
|
-
lines.push(`Quality: cohesion=${packet.quality.cohesion_score}, internal_edges=${packet.quality.internal_edge_count}, boundary_edges=${packet.quality.boundary_edge_count}, unexplained_files=${packet.quality.unexplained_file_count}`);
|
|
2285
|
-
}
|
|
2286
|
-
lines.push("");
|
|
2287
|
-
return lines;
|
|
2288
|
-
}
|
|
2289
|
-
async function prepareDispatchArtifacts(params) {
|
|
2290
|
-
const runId = params.runId;
|
|
2291
|
-
const artifactsDir = params.artifactsDir;
|
|
2292
|
-
const runDir = join(artifactsDir, "runs", runId);
|
|
2293
|
-
const taskResultsDir = join(runDir, "task-results");
|
|
2294
|
-
const dispatchPlanPath = join(runDir, "dispatch-plan.json");
|
|
2295
|
-
let reviewRoot = params.root;
|
|
2296
|
-
try {
|
|
2297
|
-
const workerTask = await readJsonFile(join(runDir, "task.json"));
|
|
2298
|
-
reviewRoot ??= workerTask.repo_root;
|
|
2299
|
-
}
|
|
2300
|
-
catch (error) {
|
|
2301
|
-
if (!isFileMissingError(error)) {
|
|
2302
|
-
throw error;
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
const bundle = await loadArtifactBundle(artifactsDir);
|
|
2306
|
-
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
2307
|
-
const tasks = await readJsonFile(tasksPath).catch((error) => {
|
|
2308
|
-
if (isFileMissingError(error))
|
|
2309
|
-
return buildPendingAuditTasks(bundle);
|
|
2310
|
-
throw error;
|
|
2311
|
-
});
|
|
2312
|
-
const sessionConfig = params.sessionConfig ?? (await loadSessionConfig(artifactsDir).catch(() => ({})));
|
|
2313
|
-
const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
|
|
2314
|
-
const lensDefs = await readJsonFile(lensDefsPath);
|
|
2315
|
-
await mkdir(taskResultsDir, { recursive: true });
|
|
2316
|
-
// On resume: skip tasks whose result files already exist from a prior dispatch.
|
|
2317
|
-
const priorResultTaskIds = new Set();
|
|
2318
|
-
for (const task of tasks) {
|
|
2319
|
-
if (existsSync(taskResultPath(taskResultsDir, task.task_id))) {
|
|
2320
|
-
priorResultTaskIds.add(task.task_id);
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
const dispatchTasks = priorResultTaskIds.size > 0
|
|
2324
|
-
? tasks.filter((task) => !priorResultTaskIds.has(task.task_id))
|
|
2325
|
-
: tasks;
|
|
2326
|
-
const lineIndex = Object.fromEntries(dispatchTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
|
|
2327
|
-
const orderedTasks = orderTasksForPacketReview(dispatchTasks, {
|
|
2328
|
-
graphBundle: bundle.graph_bundle,
|
|
2329
|
-
lineIndex,
|
|
2330
|
-
});
|
|
2331
|
-
const packets = buildReviewPackets(orderedTasks, {
|
|
2332
|
-
graphBundle: bundle.graph_bundle,
|
|
2333
|
-
lineIndex,
|
|
2334
|
-
});
|
|
2335
|
-
const tasksById = new Map(orderedTasks.map((task) => [task.task_id, task]));
|
|
2336
|
-
const resultPathByTaskId = new Map(orderedTasks.map((task) => [
|
|
2337
|
-
task.task_id,
|
|
2338
|
-
taskResultPath(taskResultsDir, task.task_id),
|
|
2339
|
-
]));
|
|
2340
|
-
const resultPathSet = new Set(resultPathByTaskId.values());
|
|
2341
|
-
if (resultPathSet.size !== resultPathByTaskId.size) {
|
|
2342
|
-
throw new Error("prepare-dispatch generated duplicate result paths; task ids must be uniquely addressable.");
|
|
2343
|
-
}
|
|
2344
|
-
const plan = [];
|
|
2345
|
-
const resultMapEntries = [];
|
|
2346
|
-
for (const task of tasks) {
|
|
2347
|
-
if (priorResultTaskIds.has(task.task_id)) {
|
|
2348
|
-
resultMapEntries.push({
|
|
2349
|
-
packet_id: "__prior_dispatch__",
|
|
2350
|
-
task_id: task.task_id,
|
|
2351
|
-
result_path: taskResultPath(taskResultsDir, task.task_id),
|
|
2352
|
-
});
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
let largestPacketId = null;
|
|
2356
|
-
let largestLines = 0;
|
|
2357
|
-
let largestEstimatedTokens = 0;
|
|
2358
|
-
const warnings = [];
|
|
2359
|
-
for (const packet of packets) {
|
|
2360
|
-
const promptPath = packetPromptPath(taskResultsDir, packet.packet_id);
|
|
2361
|
-
const packetTasks = packet.task_ids
|
|
2362
|
-
.map((taskId) => tasksById.get(taskId))
|
|
2363
|
-
.filter((task) => task !== undefined);
|
|
2364
|
-
if (packet.total_lines > largestLines) {
|
|
2365
|
-
largestLines = packet.total_lines;
|
|
2366
|
-
largestEstimatedTokens = packet.estimated_tokens;
|
|
2367
|
-
largestPacketId = packet.packet_id;
|
|
2368
|
-
}
|
|
2369
|
-
const largeFileMode = isIsolatedLargeFilePacket(packet);
|
|
2370
|
-
if (packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES && !largeFileMode) {
|
|
2371
|
-
warnings.push({
|
|
2372
|
-
code: "large_packet",
|
|
2373
|
-
message: `large packet ${packet.packet_id} (~${packet.total_lines} lines) may hit quota limits`,
|
|
2374
|
-
});
|
|
2375
|
-
}
|
|
2376
|
-
for (const task of packetTasks) {
|
|
2377
|
-
if (!lensDefs[task.lens]) {
|
|
2378
|
-
warnings.push({
|
|
2379
|
-
code: "missing_lens_definition",
|
|
2380
|
-
message: `no lens definition for '${task.lens}' (task ${task.task_id})`,
|
|
2381
|
-
});
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
const fileList = packet.file_paths.map((path) => {
|
|
2385
|
-
const lines = packet.file_line_counts[path] ?? 0;
|
|
2386
|
-
return `- ${path} (${lines} lines)`;
|
|
2387
|
-
}).join("\n");
|
|
2388
|
-
let anchorPath = null;
|
|
2389
|
-
let anchorSummary = null;
|
|
2390
|
-
if (largeFileMode) {
|
|
2391
|
-
const filePath = packet.file_paths[0];
|
|
2392
|
-
if (!reviewRoot) {
|
|
2393
|
-
warnings.push({
|
|
2394
|
-
code: "large_file_anchor_unavailable",
|
|
2395
|
-
message: `large single-file packet ${packet.packet_id} has no repo root available for anchor extraction`,
|
|
2396
|
-
});
|
|
2397
|
-
}
|
|
2398
|
-
else {
|
|
2399
|
-
try {
|
|
2400
|
-
const totalLines = packet.file_line_counts[filePath] ?? packet.total_lines;
|
|
2401
|
-
const content = await readFile(withinRoot(reviewRoot, filePath), "utf8");
|
|
2402
|
-
anchorSummary = buildFileAnchorSummary({
|
|
2403
|
-
path: filePath,
|
|
2404
|
-
content,
|
|
2405
|
-
totalLines,
|
|
2406
|
-
graphBundle: bundle.graph_bundle,
|
|
2407
|
-
externalAnalyzerResults: bundle.external_analyzer_results,
|
|
2408
|
-
});
|
|
2409
|
-
anchorPath = join(taskResultsDir, artifactNameForId(packet.packet_id, "anchors.json"));
|
|
2410
|
-
await writeJsonFile(anchorPath, anchorSummary);
|
|
2411
|
-
}
|
|
2412
|
-
catch (error) {
|
|
2413
|
-
warnings.push({
|
|
2414
|
-
code: "large_file_anchor_failed",
|
|
2415
|
-
message: `large single-file packet ${packet.packet_id} could not be anchored mechanically: ` +
|
|
2416
|
-
(error instanceof Error ? error.message : String(error)),
|
|
2417
|
-
});
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
const largeFileSection = anchorSummary && anchorPath
|
|
2422
|
-
? renderAnchorPreview(anchorSummary, anchorPath)
|
|
2423
|
-
: largeFileMode
|
|
2424
|
-
? [
|
|
2425
|
-
"## Large File Review Mode",
|
|
2426
|
-
"This packet is intentionally isolated because it covers one large file.",
|
|
2427
|
-
"Use targeted reads/searches within this file only.",
|
|
2428
|
-
"No mechanical anchor file was available, so rely on targeted symbol and keyword searches before reading broad ranges.",
|
|
2429
|
-
"",
|
|
2430
|
-
]
|
|
2431
|
-
: [];
|
|
2432
|
-
const taskSections = packetTasks.flatMap((task) => {
|
|
2433
|
-
const lensDef = lensDefs[task.lens];
|
|
2434
|
-
const inputLines = task.inputs
|
|
2435
|
-
? Object.entries(task.inputs)
|
|
2436
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
2437
|
-
.map(([key, value]) => `input.${key}: ${value}`)
|
|
2438
|
-
: [];
|
|
2439
|
-
const isLensVerification = task.tags?.includes("lens_verification") ?? false;
|
|
2440
|
-
return [
|
|
2441
|
-
`### ${task.task_id}`,
|
|
2442
|
-
`unit_id: ${task.unit_id}`,
|
|
2443
|
-
`pass_id: ${task.pass_id}`,
|
|
2444
|
-
`lens: ${task.lens}`,
|
|
2445
|
-
...(task.tags?.length ? [`tags: ${task.tags.join(", ")}`] : []),
|
|
2446
|
-
...inputLines,
|
|
2447
|
-
`rationale: ${task.rationale}`,
|
|
2448
|
-
"",
|
|
2449
|
-
`Lens guidance: ${lensDef?.description ?? task.lens}`,
|
|
2450
|
-
`Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
|
|
2451
|
-
...(isLensVerification
|
|
2452
|
-
? [
|
|
2453
|
-
"",
|
|
2454
|
-
"Lens verification mode: review the prior result summary in the rationale and use only targeted source checks.",
|
|
2455
|
-
"Do not redo every packet and do not write direct findings for this task.",
|
|
2456
|
-
"Return findings: [] plus verification metadata. Include followup_tasks only for bounded, specific re-review packets.",
|
|
2457
|
-
]
|
|
2458
|
-
: []),
|
|
2459
|
-
"",
|
|
2460
|
-
];
|
|
2461
|
-
});
|
|
2462
|
-
const submitCommand = `"${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" submit-packet ` +
|
|
2463
|
-
`--run-id-b64 ${toBase64Url(runId)} ` +
|
|
2464
|
-
`--packet-id-b64 ${toBase64Url(packet.packet_id)} ` +
|
|
2465
|
-
`--artifacts-dir-b64 ${toBase64Url(artifactsDir)}`;
|
|
2466
|
-
const complexity = buildDispatchComplexity(packet, largeFileMode);
|
|
2467
|
-
for (const task of packetTasks) {
|
|
2468
|
-
resultMapEntries.push({
|
|
2469
|
-
packet_id: packet.packet_id,
|
|
2470
|
-
task_id: task.task_id,
|
|
2471
|
-
result_path: resultPathByTaskId.get(task.task_id),
|
|
2472
|
-
});
|
|
2473
|
-
}
|
|
2474
|
-
const prompt = [
|
|
2475
|
-
"You are a code auditor. Review this packet once, then submit exactly one result per listed task.",
|
|
2476
|
-
"",
|
|
2477
|
-
"## Packet",
|
|
2478
|
-
`packet_id: ${packet.packet_id}`,
|
|
2479
|
-
`task_count: ${packet.task_ids.length}`,
|
|
2480
|
-
`lenses: ${packet.lenses.join(", ")}`,
|
|
2481
|
-
`estimated_tokens: ${packet.estimated_tokens}`,
|
|
2482
|
-
"",
|
|
2483
|
-
"## Files to read",
|
|
2484
|
-
largeFileMode
|
|
2485
|
-
? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
|
|
2486
|
-
: "Use your Read tool. Paths are repo-relative from the current working directory.",
|
|
2487
|
-
"Prefer host Read/Grep tools. On native Windows, do not use Unix pipelines like `grep ... | head`; if shell search is unavoidable, use `Select-String` as a fallback.",
|
|
2488
|
-
fileList,
|
|
2489
|
-
"",
|
|
2490
|
-
...renderPacketGraphContext(packet),
|
|
2491
|
-
...largeFileSection,
|
|
2492
|
-
"## Tasks",
|
|
2493
|
-
...taskSections,
|
|
2494
|
-
"## Output",
|
|
2495
|
-
"Do not write files directly. Do not use a Write tool, create temp files, edit source files,",
|
|
2496
|
-
"remediate findings, create extra task results, or run unrelated audits.",
|
|
2497
|
-
"Produce one JSON array containing exactly one AuditResult object for each listed task.",
|
|
2498
|
-
"",
|
|
2499
|
-
"Required AuditResult fields:",
|
|
2500
|
-
" task_id copy from the task metadata",
|
|
2501
|
-
" unit_id copy from the task metadata",
|
|
2502
|
-
" pass_id copy from the task metadata",
|
|
2503
|
-
" lens copy from the task metadata",
|
|
2504
|
-
" file_coverage [{path, total_lines}] - one entry per assigned file; use the line counts listed above",
|
|
2505
|
-
" findings [] or array of finding objects",
|
|
2506
|
-
"",
|
|
2507
|
-
"Lens verification tasks:",
|
|
2508
|
-
" tasks tagged lens_verification must use findings: [] and include verification:",
|
|
2509
|
-
" {verified: boolean, needs_followup: boolean, concerns?: string[],",
|
|
2510
|
-
" coverage_concerns?: string[], confidence_concerns?: string[],",
|
|
2511
|
-
" followup_tasks?: AuditTask[]}.",
|
|
2512
|
-
" Follow-up AuditTask suggestions must stay bounded to files in this packet and use the same lens.",
|
|
2513
|
-
"",
|
|
2514
|
-
"Each finding object:",
|
|
2515
|
-
" id unique ID, e.g. \"COR-001\"",
|
|
2516
|
-
" title short title",
|
|
2517
|
-
" category specific finding category, such as missing-validation or command-execution",
|
|
2518
|
-
" severity critical|high|medium|low|info",
|
|
2519
|
-
" confidence high|medium|low",
|
|
2520
|
-
" lens must match the task lens exactly",
|
|
2521
|
-
" summary 1-2 sentence description",
|
|
2522
|
-
" affected_files [{path, line_start?, line_end?, symbol?}] - objects, not strings; min 1 entry",
|
|
2523
|
-
" evidence [\"path/to/file.ts:42 - description of what you see there\"] - min 1 entry",
|
|
2524
|
-
"",
|
|
2525
|
-
"Constraints:",
|
|
2526
|
-
"1. line_end must not exceed the file's actual line count.",
|
|
2527
|
-
"2. affected_files entries are objects with a path key, not plain strings.",
|
|
2528
|
-
"3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
|
|
2529
|
-
"4. findings: [] is correct when you find nothing genuine.",
|
|
2530
|
-
"",
|
|
2531
|
-
"## Submit",
|
|
2532
|
-
"Pipe the JSON array on stdin to this command:",
|
|
2533
|
-
` ${submitCommand}`,
|
|
2534
|
-
"",
|
|
2535
|
-
"The command validates and writes the packet-owned result files. Exit 0 means accepted.",
|
|
2536
|
-
"Non-zero: read the errors, fix the JSON, and run the same submit command again. Retry up to 3 times.",
|
|
2537
|
-
"",
|
|
2538
|
-
"## Final response",
|
|
2539
|
-
`After the submit command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
|
|
2540
|
-
].join("\n");
|
|
2541
|
-
await writeFile(promptPath, prompt, "utf8");
|
|
2542
|
-
plan.push({
|
|
2543
|
-
packet_id: packet.packet_id,
|
|
2544
|
-
description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)` +
|
|
2545
|
-
(largeFileMode ? " [isolated large-file mode]" : ""),
|
|
2546
|
-
prompt_path: promptPath,
|
|
2547
|
-
complexity,
|
|
2548
|
-
model_hint: buildDispatchModelHint(complexity),
|
|
2549
|
-
});
|
|
2550
|
-
}
|
|
2551
|
-
await writeJsonFile(dispatchPlanPath, plan);
|
|
2552
|
-
await writeJsonFile(dispatchResultMapPath(runDir), {
|
|
2553
|
-
contract_version: "audit-code-dispatch-results/v1alpha1",
|
|
2554
|
-
run_id: runId,
|
|
2555
|
-
entries: resultMapEntries,
|
|
2556
|
-
});
|
|
2557
|
-
// Compute and write dispatch-quota.json
|
|
2558
|
-
const hostModel = params.hostModel ?? null;
|
|
2559
|
-
const perPacketTokens = plan.map((p) => p.complexity.estimated_tokens);
|
|
2560
|
-
const quotaProviderName = resolveFreshSessionProviderName(undefined, sessionConfig);
|
|
2561
|
-
const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
|
|
2562
|
-
const quotaState = await readQuotaState().catch(() => ({ version: 2, entries: {} }));
|
|
2563
|
-
const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
|
|
2564
|
-
const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
|
|
2565
|
-
explicitLimit: params.hostActiveSubagentLimit,
|
|
2566
|
-
sessionConfig,
|
|
2567
|
-
});
|
|
2568
|
-
const dispatchCachedLimits = await lookupDiscoveredLimits(quotaProviderKey).catch(() => null);
|
|
2569
|
-
const waveSchedule = scheduleWave({
|
|
2570
|
-
providerName: quotaProviderName,
|
|
2571
|
-
sessionConfig,
|
|
2572
|
-
hostModel,
|
|
2573
|
-
requestedConcurrency: sessionConfig.parallel_workers ?? plan.length,
|
|
2574
|
-
estimatedSlotTokens: perPacketTokens,
|
|
2575
|
-
quotaStateEntry,
|
|
2576
|
-
hostConcurrencyLimit,
|
|
2577
|
-
discoveredLimits: dispatchCachedLimits,
|
|
2578
|
-
});
|
|
2579
|
-
const dispatchQuota = {
|
|
2580
|
-
contract_version: "audit-code-dispatch-quota/v1alpha2",
|
|
2581
|
-
run_id: runId,
|
|
2582
|
-
model: hostModel,
|
|
2583
|
-
resolved_limits: waveSchedule.resolved_limits,
|
|
2584
|
-
confidence: waveSchedule.confidence,
|
|
2585
|
-
source: waveSchedule.source,
|
|
2586
|
-
host_concurrency_limit: waveSchedule.host_concurrency_limit,
|
|
2587
|
-
wave_size: waveSchedule.wave_size,
|
|
2588
|
-
estimated_wave_tokens: waveSchedule.estimated_wave_tokens,
|
|
2589
|
-
cooldown_until: waveSchedule.cooldown_until,
|
|
2590
|
-
quota_source_snapshot: waveSchedule.quota_source_snapshot ?? null,
|
|
2591
|
-
backoff_state: null,
|
|
2592
|
-
};
|
|
2593
|
-
const dispatchQuotaPath = join(runDir, "dispatch-quota.json");
|
|
2594
|
-
await writeJsonFile(dispatchQuotaPath, dispatchQuota);
|
|
2595
|
-
// Warn about packets that exceed the context budget only when we have reliable limit
|
|
2596
|
-
// information (confidence medium/high). Low-confidence limits are conservative defaults
|
|
2597
|
-
// and would produce misleading warnings since the real context window is unknown.
|
|
2598
|
-
if (waveSchedule.confidence !== "low") {
|
|
2599
|
-
const contextBudget = waveSchedule.resolved_limits.context_tokens - waveSchedule.resolved_limits.output_tokens;
|
|
2600
|
-
for (const p of plan) {
|
|
2601
|
-
if (p.complexity.estimated_tokens > contextBudget) {
|
|
2602
|
-
warnings.push({
|
|
2603
|
-
code: "oversized_packet",
|
|
2604
|
-
message: `Packet ${p.packet_id} estimated tokens (${p.complexity.estimated_tokens}) exceed ` +
|
|
2605
|
-
`context budget (${contextBudget}). This packet may fail at dispatch. ` +
|
|
2606
|
-
`Set quota.default_context_tokens or quota.models in session-config.json to override.`,
|
|
2607
|
-
});
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
const warningsPath = warnings.length > 0
|
|
2612
|
-
? join(runDir, "dispatch-warnings.json")
|
|
2613
|
-
: null;
|
|
2614
|
-
if (warningsPath) {
|
|
2615
|
-
await writeJsonFile(warningsPath, warnings);
|
|
2616
|
-
}
|
|
2617
|
-
return {
|
|
2618
|
-
run_id: runId,
|
|
2619
|
-
dispatch_plan_path: dispatchPlanPath,
|
|
2620
|
-
dispatch_quota_path: dispatchQuotaPath,
|
|
2621
|
-
packet_count: plan.length,
|
|
2622
|
-
task_count: orderedTasks.length,
|
|
2623
|
-
skipped_task_count: priorResultTaskIds.size,
|
|
2624
|
-
largest_packet: largestPacketId
|
|
2625
|
-
? {
|
|
2626
|
-
packet_id: largestPacketId,
|
|
2627
|
-
total_lines: largestLines,
|
|
2628
|
-
estimated_tokens: largestEstimatedTokens,
|
|
2629
|
-
}
|
|
2630
|
-
: null,
|
|
2631
|
-
warning_count: warnings.length,
|
|
2632
|
-
dispatch_warnings_path: warningsPath,
|
|
2633
|
-
};
|
|
2634
|
-
}
|
|
2635
1838
|
async function cmdPrepareDispatch(argv) {
|
|
2636
1839
|
const runId = getFlag(argv, "--run-id");
|
|
2637
1840
|
if (!runId)
|
|
2638
1841
|
throw new Error("prepare-dispatch requires --run-id <run_id>");
|
|
2639
1842
|
const result = await prepareDispatchArtifacts({
|
|
1843
|
+
packageRoot,
|
|
2640
1844
|
runId,
|
|
2641
1845
|
artifactsDir: getArtifactsDir(argv),
|
|
2642
1846
|
root: getFlag(argv, "--root") ? getRootDir(argv) : undefined,
|
|
@@ -2661,12 +1865,23 @@ async function cmdSubmitPacket(argv) {
|
|
|
2661
1865
|
if (!resultMap) {
|
|
2662
1866
|
throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
|
|
2663
1867
|
}
|
|
2664
|
-
|
|
1868
|
+
let packetEntries = resultMap.entries.filter((entry) => entry.packet_id === packetId);
|
|
1869
|
+
let resolvedPacketId = packetId;
|
|
1870
|
+
if (packetEntries.length === 0) {
|
|
1871
|
+
const trimmed = packetId.trim();
|
|
1872
|
+
packetEntries = resultMap.entries.filter((entry) => entry.packet_id.trim().toLowerCase() === trimmed.toLowerCase());
|
|
1873
|
+
if (packetEntries.length > 0) {
|
|
1874
|
+
resolvedPacketId = packetEntries[0].packet_id;
|
|
1875
|
+
process.stderr.write(`[submit-packet] Resolved packet_id '${packetId}' → '${resolvedPacketId}' (case/whitespace normalization)\n`);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
2665
1878
|
if (packetEntries.length === 0) {
|
|
2666
|
-
|
|
1879
|
+
const knownIds = [...new Set(resultMap.entries.map((e) => e.packet_id))];
|
|
1880
|
+
throw new Error(`Unknown packet_id '${packetId}' for run ${runId}.\n` +
|
|
1881
|
+
`Valid packet IDs: ${knownIds.join(", ")}`);
|
|
2667
1882
|
}
|
|
2668
1883
|
if (entriesByTaskId(packetEntries).size !== packetEntries.length) {
|
|
2669
|
-
throw new Error(`Dispatch result map has duplicate task entries for packet '${
|
|
1884
|
+
throw new Error(`Dispatch result map has duplicate task entries for packet '${resolvedPacketId}'.`);
|
|
2670
1885
|
}
|
|
2671
1886
|
const allTasks = await readJsonFile(tasksPath);
|
|
2672
1887
|
const taskById = new Map(allTasks.map((task) => [task.task_id, task]));
|
|
@@ -2717,7 +1932,7 @@ async function cmdSubmitPacket(argv) {
|
|
|
2717
1932
|
}
|
|
2718
1933
|
seen.add(taskId);
|
|
2719
1934
|
if (!expectedTaskIds.has(taskId)) {
|
|
2720
|
-
resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${
|
|
1935
|
+
resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${resolvedPacketId}'.`);
|
|
2721
1936
|
}
|
|
2722
1937
|
}
|
|
2723
1938
|
for (const task of tasks) {
|
|
@@ -2727,7 +1942,44 @@ async function cmdSubmitPacket(argv) {
|
|
|
2727
1942
|
}
|
|
2728
1943
|
}
|
|
2729
1944
|
if (resultErrors.length > 0) {
|
|
2730
|
-
throw new Error(`submit-packet rejected ${
|
|
1945
|
+
throw new Error(`submit-packet rejected ${resolvedPacketId}:\n${resultErrors.join("\n")}`);
|
|
1946
|
+
}
|
|
1947
|
+
// Check for duplicate findings against already-submitted results in this run
|
|
1948
|
+
const existingFindingKeys = new Set();
|
|
1949
|
+
const otherEntries = resultMap.entries.filter((e) => e.packet_id !== resolvedPacketId);
|
|
1950
|
+
for (const other of otherEntries) {
|
|
1951
|
+
try {
|
|
1952
|
+
const existing = JSON.parse(await readFile(other.result_path, "utf8"));
|
|
1953
|
+
if (existing?.findings) {
|
|
1954
|
+
for (const f of existing.findings) {
|
|
1955
|
+
const key = [
|
|
1956
|
+
(f.lens ?? "").trim().toLowerCase(),
|
|
1957
|
+
(f.category ?? "").trim().toLowerCase(),
|
|
1958
|
+
(f.title ?? "").trim().toLowerCase(),
|
|
1959
|
+
f.affected_files?.[0]?.path ?? "",
|
|
1960
|
+
].join("|");
|
|
1961
|
+
existingFindingKeys.add(key);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
catch { /* file doesn't exist yet or invalid — skip */ }
|
|
1966
|
+
}
|
|
1967
|
+
let dupCount = 0;
|
|
1968
|
+
for (const result of payload) {
|
|
1969
|
+
for (const f of result.findings ?? []) {
|
|
1970
|
+
const key = [
|
|
1971
|
+
(f.lens ?? "").trim().toLowerCase(),
|
|
1972
|
+
(f.category ?? "").trim().toLowerCase(),
|
|
1973
|
+
(f.title ?? "").trim().toLowerCase(),
|
|
1974
|
+
f.affected_files?.[0]?.path ?? "",
|
|
1975
|
+
].join("|");
|
|
1976
|
+
if (existingFindingKeys.has(key)) {
|
|
1977
|
+
dupCount++;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (dupCount > 0) {
|
|
1982
|
+
process.stderr.write(`[submit-packet] Warning: ${dupCount} finding(s) appear to duplicate findings from other packets in this run.\n`);
|
|
2731
1983
|
}
|
|
2732
1984
|
const entryByTaskId = entriesByTaskId(packetEntries);
|
|
2733
1985
|
for (const result of payload) {
|
|
@@ -2740,9 +1992,10 @@ async function cmdSubmitPacket(argv) {
|
|
|
2740
1992
|
const findingCount = payload.reduce((sum, result) => sum + result.findings.length, 0);
|
|
2741
1993
|
console.log(JSON.stringify({
|
|
2742
1994
|
run_id: runId,
|
|
2743
|
-
packet_id:
|
|
1995
|
+
packet_id: resolvedPacketId,
|
|
2744
1996
|
accepted_count: payload.length,
|
|
2745
1997
|
finding_count: findingCount,
|
|
1998
|
+
...(dupCount > 0 ? { duplicate_warning_count: dupCount } : {}),
|
|
2746
1999
|
}, null, 2));
|
|
2747
2000
|
}
|
|
2748
2001
|
async function cmdMergeAndIngest(argv) {
|
|
@@ -2781,11 +2034,24 @@ async function cmdMergeAndIngest(argv) {
|
|
|
2781
2034
|
const failing = [];
|
|
2782
2035
|
const seenTaskIds = new Set();
|
|
2783
2036
|
let spuriousFileCount = 0;
|
|
2037
|
+
const fallbackByTaskId = new Map();
|
|
2784
2038
|
for (const filename of files) {
|
|
2785
2039
|
const filePath = resolve(join(taskResultsDir, filename));
|
|
2786
2040
|
if (!expectedPaths.has(filePath)) {
|
|
2787
2041
|
spuriousFileCount++;
|
|
2788
|
-
|
|
2042
|
+
try {
|
|
2043
|
+
const raw = await readFile(filePath, "utf8");
|
|
2044
|
+
const parsed = JSON.parse(raw);
|
|
2045
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2046
|
+
const tid = typeof parsed.task_id === "string"
|
|
2047
|
+
? String(parsed.task_id) : undefined;
|
|
2048
|
+
if (tid && !fallbackByTaskId.has(tid)) {
|
|
2049
|
+
fallbackByTaskId.set(tid, parsed);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
catch { /* not parseable — skip */ }
|
|
2054
|
+
process.stderr.write(`[merge-and-ingest] Warning: unexpected file in task-results/: ${filename}\n`);
|
|
2789
2055
|
}
|
|
2790
2056
|
}
|
|
2791
2057
|
for (const task of allTasks) {
|
|
@@ -2804,15 +2070,23 @@ async function cmdMergeAndIngest(argv) {
|
|
|
2804
2070
|
}
|
|
2805
2071
|
catch (e) {
|
|
2806
2072
|
if (isFileMissingError(e)) {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2073
|
+
const fallback = fallbackByTaskId.get(task.task_id);
|
|
2074
|
+
if (fallback) {
|
|
2075
|
+
process.stderr.write(`[merge-and-ingest] Recovered result for '${task.task_id}' from unexpected file (matched by task_id)\n`);
|
|
2076
|
+
obj = fallback;
|
|
2077
|
+
}
|
|
2078
|
+
else {
|
|
2079
|
+
failing.push({
|
|
2080
|
+
task_id: task.task_id,
|
|
2081
|
+
errors: ["Missing audit result for assigned task."],
|
|
2082
|
+
});
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2811
2085
|
}
|
|
2812
2086
|
else {
|
|
2813
2087
|
failing.push({ task_id: task.task_id, errors: [`Invalid JSON: ${e.message}`] });
|
|
2088
|
+
continue;
|
|
2814
2089
|
}
|
|
2815
|
-
continue;
|
|
2816
2090
|
}
|
|
2817
2091
|
const record = obj && typeof obj === "object" && !Array.isArray(obj)
|
|
2818
2092
|
? obj
|
|
@@ -2843,45 +2117,87 @@ async function cmdMergeAndIngest(argv) {
|
|
|
2843
2117
|
}
|
|
2844
2118
|
}
|
|
2845
2119
|
await writeJsonFile(auditResultsPath, passing);
|
|
2120
|
+
const failedTasksPath = join(runDir, "failed-tasks.json");
|
|
2846
2121
|
if (failing.length > 0) {
|
|
2847
|
-
const failedTasksPath = join(runDir, "failed-tasks.json");
|
|
2848
2122
|
await writeJsonFile(failedTasksPath, failing);
|
|
2849
|
-
|
|
2123
|
+
}
|
|
2124
|
+
if (passing.length === 0 && failing.length > 0) {
|
|
2125
|
+
throw new Error(`All ${failing.length} assigned task result(s) were missing or invalid; blocked before ingestion. See ${failedTasksPath}`);
|
|
2850
2126
|
}
|
|
2851
2127
|
const findingCount = passing.reduce((sum, result) => sum + result.findings.length, 0);
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2128
|
+
let result = null;
|
|
2129
|
+
if (passing.length > 0) {
|
|
2130
|
+
result = await runAuditStep({
|
|
2131
|
+
root: workerTask.repo_root,
|
|
2132
|
+
artifactsDir,
|
|
2133
|
+
preferredExecutor: "result_ingestion_executor",
|
|
2134
|
+
auditResultsPath,
|
|
2135
|
+
});
|
|
2136
|
+
const updatedPendingTasks = await addFileLineCountHints(workerTask.repo_root, buildPendingAuditTasks(result.updated_bundle));
|
|
2137
|
+
await writeJsonFile(tasksPath, updatedPendingTasks);
|
|
2138
|
+
}
|
|
2139
|
+
const activeDispatchPath = join(artifactsDir, ACTIVE_DISPATCH_FILENAME);
|
|
2140
|
+
try {
|
|
2141
|
+
const dispatch = await readJsonFile(activeDispatchPath);
|
|
2142
|
+
if (dispatch.run_id === runId) {
|
|
2143
|
+
dispatch.status = failing.length > 0 ? "active" : "merged";
|
|
2144
|
+
await writeJsonFile(activeDispatchPath, dispatch);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
catch { /* no active dispatch file — skip */ }
|
|
2148
|
+
let retryDispatchPath = null;
|
|
2149
|
+
if (failing.length > 0) {
|
|
2150
|
+
const failedTaskIds = new Set(failing.map((f) => f.task_id));
|
|
2151
|
+
const failedPacketIds = [
|
|
2152
|
+
...new Set(resultMap.entries
|
|
2153
|
+
.filter((e) => failedTaskIds.has(e.task_id))
|
|
2154
|
+
.map((e) => e.packet_id)),
|
|
2155
|
+
];
|
|
2156
|
+
const retryDispatch = {
|
|
2157
|
+
run_id: runId,
|
|
2158
|
+
retry_packet_ids: failedPacketIds,
|
|
2159
|
+
failed_task_count: failing.length,
|
|
2160
|
+
accepted_task_count: passing.length,
|
|
2161
|
+
};
|
|
2162
|
+
retryDispatchPath = join(runDir, "retry-dispatch.json");
|
|
2163
|
+
await writeJsonFile(retryDispatchPath, retryDispatch);
|
|
2164
|
+
process.stderr.write(`[merge-and-ingest] ${passing.length} accepted, ${failing.length} failed. ` +
|
|
2165
|
+
`Retry packets: ${failedPacketIds.join(", ")}\n`);
|
|
2166
|
+
}
|
|
2167
|
+
const status = failing.length > 0
|
|
2168
|
+
? "partial"
|
|
2169
|
+
: (result?.progress_made ? "completed" : "no_progress");
|
|
2860
2170
|
const workerResult = buildWorkerResult({
|
|
2861
2171
|
runId,
|
|
2862
2172
|
obligationId: workerTask.obligation_id,
|
|
2863
|
-
status: result
|
|
2864
|
-
progressMade: result
|
|
2865
|
-
selectedExecutor: result
|
|
2866
|
-
artifactsWritten: result
|
|
2867
|
-
summary: result.
|
|
2868
|
-
nextLikelyStep: result
|
|
2173
|
+
status: failing.length > 0 ? "no_progress" : (result?.progress_made ? "completed" : "no_progress"),
|
|
2174
|
+
progressMade: result?.progress_made ?? false,
|
|
2175
|
+
selectedExecutor: result?.selected_executor ?? null,
|
|
2176
|
+
artifactsWritten: result?.artifacts_written ?? [],
|
|
2177
|
+
summary: result?.progress_summary ?? `${failing.length} task(s) failed`,
|
|
2178
|
+
nextLikelyStep: result?.next_likely_step ?? null,
|
|
2869
2179
|
errors: [],
|
|
2870
2180
|
});
|
|
2871
2181
|
await writeJsonFile(workerTask.result_path, workerResult);
|
|
2872
2182
|
console.log(JSON.stringify({
|
|
2873
2183
|
run_id: runId,
|
|
2874
|
-
status
|
|
2184
|
+
status,
|
|
2875
2185
|
accepted_count: passing.length,
|
|
2876
|
-
rejected_count:
|
|
2186
|
+
rejected_count: failing.length,
|
|
2877
2187
|
spurious_file_count: spuriousFileCount,
|
|
2878
2188
|
finding_count: findingCount,
|
|
2879
2189
|
audit_results_path: auditResultsPath,
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2190
|
+
...(retryDispatchPath ? { retry_dispatch_path: retryDispatchPath } : {}),
|
|
2191
|
+
...(result ? {
|
|
2192
|
+
selected_executor: workerResult.selected_executor,
|
|
2193
|
+
progress_made: workerResult.progress_made,
|
|
2194
|
+
progress_summary: workerResult.summary,
|
|
2195
|
+
next_likely_step: workerResult.next_likely_step,
|
|
2196
|
+
} : {}),
|
|
2884
2197
|
}, null, 2));
|
|
2198
|
+
if (failing.length > 0) {
|
|
2199
|
+
process.exitCode = 2;
|
|
2200
|
+
}
|
|
2885
2201
|
}
|
|
2886
2202
|
async function cmdValidateResult(argv) {
|
|
2887
2203
|
const rawRunId = getFlag(argv, "--run-id");
|
|
@@ -3357,7 +2673,71 @@ async function cmdQuota(argv) {
|
|
|
3357
2673
|
quota_state_path: getQuotaStatePath(),
|
|
3358
2674
|
}, null, 2));
|
|
3359
2675
|
}
|
|
2676
|
+
async function cmdDispatchStatus(argv) {
|
|
2677
|
+
const artifactsDir = getArtifactsDir(argv);
|
|
2678
|
+
const activeDispatchPath = join(artifactsDir, ACTIVE_DISPATCH_FILENAME);
|
|
2679
|
+
let activeDispatch = null;
|
|
2680
|
+
try {
|
|
2681
|
+
activeDispatch = await readJsonFile(activeDispatchPath);
|
|
2682
|
+
}
|
|
2683
|
+
catch (e) {
|
|
2684
|
+
if (!isFileMissingError(e))
|
|
2685
|
+
throw e;
|
|
2686
|
+
}
|
|
2687
|
+
if (!activeDispatch) {
|
|
2688
|
+
console.log(JSON.stringify({ status: "no_active_dispatch" }, null, 2));
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
const runDir = join(artifactsDir, "runs", activeDispatch.run_id);
|
|
2692
|
+
const resultMap = await loadDispatchResultMap(runDir);
|
|
2693
|
+
if (!resultMap) {
|
|
2694
|
+
console.log(JSON.stringify({
|
|
2695
|
+
status: "missing_result_map",
|
|
2696
|
+
run_id: activeDispatch.run_id,
|
|
2697
|
+
}, null, 2));
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
const packetIds = [...new Set(resultMap.entries.map((e) => e.packet_id))];
|
|
2701
|
+
const packetStatus = [];
|
|
2702
|
+
for (const pid of packetIds) {
|
|
2703
|
+
if (pid === "__prior_dispatch__")
|
|
2704
|
+
continue;
|
|
2705
|
+
const entries = resultMap.entries.filter((e) => e.packet_id === pid);
|
|
2706
|
+
let completed = 0;
|
|
2707
|
+
const missing = [];
|
|
2708
|
+
for (const entry of entries) {
|
|
2709
|
+
try {
|
|
2710
|
+
await readFile(entry.result_path, "utf8");
|
|
2711
|
+
completed++;
|
|
2712
|
+
}
|
|
2713
|
+
catch {
|
|
2714
|
+
missing.push(entry.task_id);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
packetStatus.push({
|
|
2718
|
+
packet_id: pid,
|
|
2719
|
+
task_count: entries.length,
|
|
2720
|
+
completed_count: completed,
|
|
2721
|
+
missing_task_ids: missing,
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
const totalTasks = packetStatus.reduce((s, p) => s + p.task_count, 0);
|
|
2725
|
+
const completedTasks = packetStatus.reduce((s, p) => s + p.completed_count, 0);
|
|
2726
|
+
const completedPackets = packetStatus.filter((p) => p.missing_task_ids.length === 0).length;
|
|
2727
|
+
console.log(JSON.stringify({
|
|
2728
|
+
run_id: activeDispatch.run_id,
|
|
2729
|
+
dispatch_status: activeDispatch.status,
|
|
2730
|
+
created_at: activeDispatch.created_at,
|
|
2731
|
+
total_packets: packetStatus.length,
|
|
2732
|
+
completed_packets: completedPackets,
|
|
2733
|
+
total_tasks: totalTasks,
|
|
2734
|
+
completed_tasks: completedTasks,
|
|
2735
|
+
missing_tasks: totalTasks - completedTasks,
|
|
2736
|
+
packets: packetStatus,
|
|
2737
|
+
}, null, 2));
|
|
2738
|
+
}
|
|
3360
2739
|
async function main(argv) {
|
|
2740
|
+
setQuotaStateDir(join(homedir(), ".audit-code"));
|
|
3361
2741
|
const command = argv[2] ?? "sample-run";
|
|
3362
2742
|
switch (command) {
|
|
3363
2743
|
case "sample-run":
|
|
@@ -3429,9 +2809,12 @@ async function main(argv) {
|
|
|
3429
2809
|
case "status":
|
|
3430
2810
|
await cmdStatus(argv);
|
|
3431
2811
|
return;
|
|
2812
|
+
case "dispatch-status":
|
|
2813
|
+
await cmdDispatchStatus(argv);
|
|
2814
|
+
return;
|
|
3432
2815
|
default:
|
|
3433
2816
|
console.error(`Unknown command: ${command}`);
|
|
3434
|
-
console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status");
|
|
2817
|
+
console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status, dispatch-status");
|
|
3435
2818
|
process.exitCode = 1;
|
|
3436
2819
|
}
|
|
3437
2820
|
}
|