auditor-lambda 0.9.1 → 0.10.0
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 +2 -9
- package/audit-code-wrapper-lib.mjs +19 -915
- package/dispatch/merge-results.mjs +1 -1
- package/dist/cli/auditStep.d.ts +1 -33
- package/dist/cli/dispatch.d.ts +47 -0
- package/dist/cli/dispatch.js +116 -1
- package/dist/cli/mergeAndIngestCommand.js +55 -8
- package/dist/cli/nextStepCommand.js +43 -15
- package/dist/cli/prompts.d.ts +2 -0
- package/dist/cli/prompts.js +9 -0
- package/dist/cli/reviewRun.js +1 -1
- package/dist/cli/runToCompletion.js +21 -8
- package/dist/cli/semanticReviewStep.js +12 -1
- package/dist/cli/steps.d.ts +15 -0
- package/dist/cli.js +1 -8
- package/dist/io/artifacts.d.ts +9 -1
- package/dist/io/artifacts.js +7 -0
- package/dist/io/runArtifacts.d.ts +14 -0
- package/dist/io/runArtifacts.js +23 -0
- package/dist/orchestrator/designReviewPrompt.d.ts +4 -1
- package/dist/orchestrator/designReviewPrompt.js +43 -2
- package/dist/orchestrator/executorResult.d.ts +25 -0
- package/dist/orchestrator/intakeExecutors.d.ts +19 -1
- package/dist/orchestrator/intakeExecutors.js +89 -3
- package/dist/orchestrator/nextStep.d.ts +1 -0
- package/dist/orchestrator/nextStep.js +1 -1
- package/dist/orchestrator/state.js +8 -1
- package/dist/providers/constants.d.ts +1 -1
- package/dist/providers/constants.js +1 -1
- package/dist/reporting/synthesis.d.ts +8 -0
- package/dist/reporting/synthesis.js +16 -1
- package/dist/supervisor/operatorHandoff.js +8 -1
- package/dist/types/auditScope.d.ts +16 -2
- package/dist/validation/sessionConfig.js +35 -0
- package/docs/contracts.md +0 -16
- package/docs/operator-guide.md +6 -8
- package/package.json +1 -1
- package/schemas/audit_findings.schema.json +1 -0
- package/scripts/postinstall.mjs +0 -174
- package/skills/audit-code/SKILL.md +17 -1
- package/skills/audit-code/audit-code.prompt.md +25 -0
- package/dist/mcp/server.d.ts +0 -72
- package/dist/mcp/server.js +0 -765
|
@@ -64,7 +64,10 @@ export async function renderSemanticReviewStep(params) {
|
|
|
64
64
|
allowedCommands: [mergeCommand, continueCommand],
|
|
65
65
|
allowedMcpTools: ["auditor_merge_and_ingest", "auditor_continue_audit"],
|
|
66
66
|
progress: {
|
|
67
|
-
summary:
|
|
67
|
+
summary: (dispatch.phase === "canary"
|
|
68
|
+
? `Canary: dispatching only the top-priority packet (${dispatch.canary_packet_id}) before fan-out. `
|
|
69
|
+
: "") +
|
|
70
|
+
`Dispatching ${dispatch.packet_count} review packet(s) covering ` +
|
|
68
71
|
`${dispatch.task_count} task(s) in waves of ${dispatch.wave_size}` +
|
|
69
72
|
(dispatch.skipped_task_count > 0
|
|
70
73
|
? `; ${dispatch.skipped_task_count} task(s) already completed.`
|
|
@@ -73,6 +76,12 @@ export async function renderSemanticReviewStep(params) {
|
|
|
73
76
|
pending_tasks: dispatch.task_count,
|
|
74
77
|
completed_tasks: dispatch.skipped_task_count,
|
|
75
78
|
wave_size: dispatch.wave_size,
|
|
79
|
+
phase: dispatch.phase,
|
|
80
|
+
canary_packet_id: dispatch.canary_packet_id,
|
|
81
|
+
agent_count: dispatch.agent_count,
|
|
82
|
+
wave_count: dispatch.wave_count,
|
|
83
|
+
confirmation_recommended: dispatch.confirmation_recommended,
|
|
84
|
+
dispatch_summary: dispatch.dispatch_summary,
|
|
76
85
|
},
|
|
77
86
|
stopCondition: "Dispatch every packet, run merge-and-ingest once, then run next-step.",
|
|
78
87
|
repoRoot: root,
|
|
@@ -91,6 +100,8 @@ export async function renderSemanticReviewStep(params) {
|
|
|
91
100
|
dispatchQuotaPath: dispatch.dispatch_quota_path,
|
|
92
101
|
hostCanRestrictSubagentTools: params.hostCanRestrictSubagentTools,
|
|
93
102
|
hostCanSelectSubagentModel: params.hostCanSelectSubagentModel,
|
|
103
|
+
phase: dispatch.phase,
|
|
104
|
+
canaryPacketId: dispatch.canary_packet_id,
|
|
94
105
|
}),
|
|
95
106
|
access: {
|
|
96
107
|
read_paths: [
|
package/dist/cli/steps.d.ts
CHANGED
|
@@ -17,6 +17,21 @@ export interface StepProgress {
|
|
|
17
17
|
completed_tasks?: number;
|
|
18
18
|
/** Subagent parallelism resolved for this dispatch run. */
|
|
19
19
|
wave_size?: number;
|
|
20
|
+
/** "canary" when only the top packet was emitted this round; "fan_out" otherwise. */
|
|
21
|
+
phase?: "canary" | "fan_out";
|
|
22
|
+
/** packet_id of the emitted canary packet when `phase === "canary"`. */
|
|
23
|
+
canary_packet_id?: string | null;
|
|
24
|
+
/** Total agents (packets) that will be launched this run. */
|
|
25
|
+
agent_count?: number;
|
|
26
|
+
/** Number of dispatch waves for this run (`ceil(agent_count / wave_size)`). */
|
|
27
|
+
wave_count?: number;
|
|
28
|
+
/**
|
|
29
|
+
* True when `agent_count` exceeds the configured confirm threshold and the
|
|
30
|
+
* loader should pause for user confirmation before fan-out (FINDING-012).
|
|
31
|
+
*/
|
|
32
|
+
confirmation_recommended?: boolean;
|
|
33
|
+
/** Human-readable fan-out summary, e.g. "12 agents across 3 waves (wave_size=4)". */
|
|
34
|
+
dispatch_summary?: string;
|
|
20
35
|
}
|
|
21
36
|
export interface StepArtifact {
|
|
22
37
|
contract_version: typeof STEP_CONTRACT_VERSION;
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,6 @@ import { deriveAuditState } from "./orchestrator/state.js";
|
|
|
21
21
|
import { createFreshSessionProvider, resolveFreshSessionProviderName, } from "./providers/index.js";
|
|
22
22
|
import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
|
|
23
23
|
import { clearDispatchFiles, ensureSupervisorDirs, } from "./io/runArtifacts.js";
|
|
24
|
-
import { runAuditCodeMcpServer } from "./mcp/server.js";
|
|
25
24
|
import { scheduleWave, buildProviderModelKey, readQuotaState, resolveLimits, resolveHostActiveSubagentLimit, computeMaxSafeConcurrency, getQuotaStatePath, lookupDiscoveredLimits, setQuotaStateDir, } from "./quota/index.js";
|
|
26
25
|
import { DIRECT_CLI_DEFAULTS, getFlag, hasFlag, fromBase64Url, taskResultPath, getArtifactsDir, getRootDir, warnIfNotGitRepo, getBatchResultsDir, getMaxRuns, getAgentBatchSize, getParallelWorkers, getTimeoutMs, getExplicitProvider, getHostModel, getHostMaxActiveSubagents, resolveRunProviderName, chunkArray, getUiMode, looksLikeCliFlag, countLines, } from "./cli/args.js";
|
|
27
26
|
import { ACTIVE_DISPATCH_FILENAME, loadDispatchResultMap, prepareDispatchArtifacts, } from "./cli/dispatch.js";
|
|
@@ -539,9 +538,6 @@ async function cmdCleanup(argv) {
|
|
|
539
538
|
dry_run: dryRun,
|
|
540
539
|
}, null, 2));
|
|
541
540
|
}
|
|
542
|
-
async function cmdMcp(argv) {
|
|
543
|
-
await runAuditCodeMcpServer(argv.slice(3));
|
|
544
|
-
}
|
|
545
541
|
async function cmdQuota(argv) {
|
|
546
542
|
const artifactsDir = getArtifactsDir(argv);
|
|
547
543
|
const sessionConfig = await loadSessionConfig(artifactsDir).catch(() => ({}));
|
|
@@ -707,9 +703,6 @@ async function main(argv) {
|
|
|
707
703
|
case "cleanup":
|
|
708
704
|
await cmdCleanup(argv);
|
|
709
705
|
return;
|
|
710
|
-
case "mcp":
|
|
711
|
-
await cmdMcp(argv);
|
|
712
|
-
return;
|
|
713
706
|
case "prepare-dispatch":
|
|
714
707
|
await cmdPrepareDispatch(argv);
|
|
715
708
|
return;
|
|
@@ -733,7 +726,7 @@ async function main(argv) {
|
|
|
733
726
|
return;
|
|
734
727
|
default:
|
|
735
728
|
console.error(`Unknown command: ${command}`);
|
|
736
|
-
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,
|
|
729
|
+
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, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status, dispatch-status");
|
|
737
730
|
process.exitCode = 1;
|
|
738
731
|
}
|
|
739
732
|
}
|
package/dist/io/artifacts.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { DesignAssessment } from "../types/designAssessment.js";
|
|
|
12
12
|
import type { AnalyzerCapabilityRecord } from "../types/analyzerCapability.js";
|
|
13
13
|
import type { AuditScopeManifest } from "../types/auditScope.js";
|
|
14
14
|
import type { ToolingManifest } from "../types/toolingManifest.js";
|
|
15
|
+
import type { ActiveDispatchState } from "../cli/dispatch.js";
|
|
15
16
|
type ArtifactPayloadMap = {
|
|
16
17
|
repo_manifest: RepoManifest;
|
|
17
18
|
file_disposition: FileDisposition;
|
|
@@ -45,8 +46,15 @@ type ArtifactPayloadMap = {
|
|
|
45
46
|
/**
|
|
46
47
|
* Audit artifacts accumulate phase-by-phase as the orchestrator advances.
|
|
47
48
|
* Missing keys mean the corresponding artifact has not been produced yet.
|
|
49
|
+
*
|
|
50
|
+
* `active_dispatch` is loaded specially (like `tooling_manifest`): it lives at
|
|
51
|
+
* the artifacts root rather than as a standard pruned artifact, and carries the
|
|
52
|
+
* in-flight dispatch phase plus any budget-deferred task ids the completion
|
|
53
|
+
* obligation must exclude.
|
|
48
54
|
*/
|
|
49
|
-
export type ArtifactBundle = Partial<ArtifactPayloadMap
|
|
55
|
+
export type ArtifactBundle = Partial<ArtifactPayloadMap> & {
|
|
56
|
+
active_dispatch?: ActiveDispatchState;
|
|
57
|
+
};
|
|
50
58
|
export type ArtifactBundleKey = keyof ArtifactPayloadMap;
|
|
51
59
|
type ArtifactPhase = "intake" | "analysis" | "execution" | "reporting" | "supervisor";
|
|
52
60
|
interface ArtifactDefinition<K extends ArtifactBundleKey = ArtifactBundleKey> {
|
package/dist/io/artifacts.js
CHANGED
|
@@ -77,6 +77,13 @@ export async function loadArtifactBundle(root) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
bundle.tooling_manifest = await buildToolingManifest();
|
|
80
|
+
// active-dispatch.json is written by prepare-dispatch at the artifacts root
|
|
81
|
+
// (not a standard ARTIFACT_DEFINITIONS entry). Load it so the completion
|
|
82
|
+
// obligation can exclude budget-deferred tasks. Absent on a fresh run.
|
|
83
|
+
const activeDispatch = await readOptionalJsonFile(join(root, "active-dispatch.json"));
|
|
84
|
+
if (activeDispatch !== undefined) {
|
|
85
|
+
bundle.active_dispatch = activeDispatch;
|
|
86
|
+
}
|
|
80
87
|
return bundle;
|
|
81
88
|
}
|
|
82
89
|
export async function writeCoreArtifacts(root, bundle, options = {}) {
|
|
@@ -2,6 +2,20 @@ import type { AuditTask } from "../types.js";
|
|
|
2
2
|
import type { WorkerTask } from "../types/workerSession.js";
|
|
3
3
|
import type { RunPaths, DispatchBatchRun } from "./runArtifactTypes.js";
|
|
4
4
|
export type { RunPaths, DispatchBatchRun } from "./runArtifactTypes.js";
|
|
5
|
+
/**
|
|
6
|
+
* Schema files copied into a dispatch run's `task-results/` directory so packet
|
|
7
|
+
* workers can optionally self-validate before submit. `audit_result.schema.json`
|
|
8
|
+
* `$ref`s the other two by relative filename, so all three must sit side-by-side
|
|
9
|
+
* for a validator to resolve them. Exported so merge-and-ingest can recognize
|
|
10
|
+
* them as legitimate (not stray) files in `task-results/`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PACKET_SCHEMA_FILENAMES: readonly ["audit_result.schema.json", "finding.schema.json", "audit_task.schema.json"];
|
|
13
|
+
/**
|
|
14
|
+
* Copy {@link PACKET_SCHEMA_FILENAMES} into `targetDir` under their canonical
|
|
15
|
+
* filenames, making the AuditResult schema reachable from a dispatch run's
|
|
16
|
+
* `task-results/` directory.
|
|
17
|
+
*/
|
|
18
|
+
export declare function writePacketSchemaFiles(targetDir: string, pkgRoot: string): Promise<void>;
|
|
5
19
|
export declare function buildRunId(obligationId: string | null, index: number, now?: Date): string;
|
|
6
20
|
export declare function getRunPaths(artifactsDir: string, runId: string): RunPaths;
|
|
7
21
|
export declare function ensureSupervisorDirs(artifactsDir: string): Promise<void>;
|
package/dist/io/runArtifacts.js
CHANGED
|
@@ -7,6 +7,29 @@ const packageRoot = resolve(moduleDir, "..", "..");
|
|
|
7
7
|
const auditResultSchemaPath = join(packageRoot, "schemas", "audit_result.schema.json");
|
|
8
8
|
const auditResultsSchemaPath = join(packageRoot, "schemas", "audit_results.schema.json");
|
|
9
9
|
const findingSchemaPath = join(packageRoot, "schemas", "finding.schema.json");
|
|
10
|
+
/**
|
|
11
|
+
* Schema files copied into a dispatch run's `task-results/` directory so packet
|
|
12
|
+
* workers can optionally self-validate before submit. `audit_result.schema.json`
|
|
13
|
+
* `$ref`s the other two by relative filename, so all three must sit side-by-side
|
|
14
|
+
* for a validator to resolve them. Exported so merge-and-ingest can recognize
|
|
15
|
+
* them as legitimate (not stray) files in `task-results/`.
|
|
16
|
+
*/
|
|
17
|
+
export const PACKET_SCHEMA_FILENAMES = [
|
|
18
|
+
"audit_result.schema.json",
|
|
19
|
+
"finding.schema.json",
|
|
20
|
+
"audit_task.schema.json",
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Copy {@link PACKET_SCHEMA_FILENAMES} into `targetDir` under their canonical
|
|
24
|
+
* filenames, making the AuditResult schema reachable from a dispatch run's
|
|
25
|
+
* `task-results/` directory.
|
|
26
|
+
*/
|
|
27
|
+
export async function writePacketSchemaFiles(targetDir, pkgRoot) {
|
|
28
|
+
await mkdir(targetDir, { recursive: true });
|
|
29
|
+
for (const name of PACKET_SCHEMA_FILENAMES) {
|
|
30
|
+
await writeFile(join(targetDir, name), await readFile(join(pkgRoot, "schemas", name), "utf8"), "utf8");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
10
33
|
const CURRENT_TASK_FILENAME = "current-task.json";
|
|
11
34
|
const CURRENT_PROMPT_FILENAME = "current-prompt.md";
|
|
12
35
|
const CURRENT_TASKS_FILENAME = "current-tasks.json";
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { ArtifactBundle } from "../io/artifacts.js";
|
|
2
|
-
export
|
|
2
|
+
export interface DesignReviewOptions {
|
|
3
|
+
max_units?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function renderDesignReviewPrompt(bundle: ArtifactBundle, options?: DesignReviewOptions): string;
|
|
@@ -45,6 +45,39 @@ function summarizeRisk(bundle) {
|
|
|
45
45
|
...lines,
|
|
46
46
|
].join("\n");
|
|
47
47
|
}
|
|
48
|
+
function buildPrioritizedReadingList(bundle, maxUnits) {
|
|
49
|
+
const items = bundle.risk_register?.items ?? [];
|
|
50
|
+
const units = bundle.unit_manifest?.units ?? [];
|
|
51
|
+
if (items.length === 0 && units.length === 0) {
|
|
52
|
+
return "No risk or unit data available; read the repository root files to orient yourself.";
|
|
53
|
+
}
|
|
54
|
+
// Build a map from unit_id → file list for fast lookup
|
|
55
|
+
const unitFiles = new Map();
|
|
56
|
+
for (const unit of units) {
|
|
57
|
+
unitFiles.set(unit.unit_id, unit.files);
|
|
58
|
+
}
|
|
59
|
+
// Sort risk items by score descending, then take the top-N
|
|
60
|
+
const sorted = [...items].sort((a, b) => b.risk_score - a.risk_score);
|
|
61
|
+
const top = sorted.slice(0, maxUnits);
|
|
62
|
+
if (top.length === 0) {
|
|
63
|
+
// Fall back to listing all units if no risk data
|
|
64
|
+
const allUnits = units.slice(0, maxUnits);
|
|
65
|
+
const lines = allUnits.map((u) => `- **${u.unit_id}** — ${u.files.join(", ")}`);
|
|
66
|
+
return [
|
|
67
|
+
`Top ${allUnits.length} unit(s) (no risk scores available):`,
|
|
68
|
+
...lines,
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
const lines = top.map((item) => {
|
|
72
|
+
const files = unitFiles.get(item.unit_id);
|
|
73
|
+
const fileList = files && files.length > 0 ? files.join(", ") : "(files unknown)";
|
|
74
|
+
return `- **${item.unit_id}** (risk score: ${item.risk_score}) — ${fileList}`;
|
|
75
|
+
});
|
|
76
|
+
return [
|
|
77
|
+
`Top ${top.length} highest-risk unit(s) by risk score (out of ${items.length} total):`,
|
|
78
|
+
...lines,
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
48
81
|
function summarizeSurfaces(bundle) {
|
|
49
82
|
const surfaces = bundle.surface_manifest?.surfaces ?? [];
|
|
50
83
|
if (surfaces.length === 0)
|
|
@@ -76,8 +109,12 @@ function formatDeterministicFindings(findings) {
|
|
|
76
109
|
...lines,
|
|
77
110
|
].join("\n");
|
|
78
111
|
}
|
|
79
|
-
export function renderDesignReviewPrompt(bundle) {
|
|
112
|
+
export function renderDesignReviewPrompt(bundle, options = {}) {
|
|
80
113
|
const deterministicFindings = bundle.design_assessment?.findings ?? [];
|
|
114
|
+
const unitCount = bundle.unit_manifest?.units.length ?? 0;
|
|
115
|
+
const defaultMaxUnits = Math.max(5, Math.min(20, Math.ceil(unitCount / 5)));
|
|
116
|
+
const maxUnits = options.max_units ?? defaultMaxUnits;
|
|
117
|
+
const prioritizedReadingList = buildPrioritizedReadingList(bundle, maxUnits);
|
|
81
118
|
return [
|
|
82
119
|
"# Project design review",
|
|
83
120
|
"",
|
|
@@ -117,7 +154,11 @@ export function renderDesignReviewPrompt(bundle) {
|
|
|
117
154
|
"",
|
|
118
155
|
"## What to assess",
|
|
119
156
|
"",
|
|
120
|
-
|
|
157
|
+
`Focus on the ${maxUnits} highest-risk units listed below; you need not read the entire repository, though you may follow any thread that demands more context. Produce findings about:`,
|
|
158
|
+
"",
|
|
159
|
+
"### Prioritised reading list",
|
|
160
|
+
"",
|
|
161
|
+
prioritizedReadingList,
|
|
121
162
|
"",
|
|
122
163
|
"- **Tool and library opportunities**: third-party tools, libraries, or frameworks that would improve the project. Concrete suggestions with rationale, not generic advice.",
|
|
123
164
|
"- **Architecture pattern improvements**: structural changes that would improve extensibility, testability, or maintainability. Consider whether the current abstractions match the problem domain.",
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import type { ArtifactBundle } from "../io/artifacts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolved audit scope, emitted by the intake executor so the conversation-first
|
|
4
|
+
* loader can echo what is about to be audited (and gate on confirmation when a
|
|
5
|
+
* mis-scope smell suggests the user targeted the wrong directory).
|
|
6
|
+
*/
|
|
7
|
+
export interface ScopeSummary {
|
|
8
|
+
/** Absolute path of the resolved repository root being audited. */
|
|
9
|
+
repo_root: string;
|
|
10
|
+
/** Count of files that will actually be audited (after disposition filtering). */
|
|
11
|
+
auditable_file_count: number;
|
|
12
|
+
/** Whether `repo_root` sits inside a git working tree. */
|
|
13
|
+
git_available: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Zero or more human-readable warnings that the resolved root may be the wrong
|
|
16
|
+
* target (e.g. a non-git subdirectory whose ancestor is a repo, or a workspace
|
|
17
|
+
* member of a parent monorepo). Empty when the scope looks correct.
|
|
18
|
+
*/
|
|
19
|
+
mis_scope_smells: string[];
|
|
20
|
+
}
|
|
2
21
|
/**
|
|
3
22
|
* Uniform result of running one audit executor: the updated artifact bundle, the
|
|
4
23
|
* artifact filenames it wrote (which drive metadata/staleness bookkeeping in
|
|
@@ -9,4 +28,10 @@ export interface ExecutorRunResult {
|
|
|
9
28
|
updated: ArtifactBundle;
|
|
10
29
|
artifacts_written: string[];
|
|
11
30
|
progress_summary: string;
|
|
31
|
+
/**
|
|
32
|
+
* Optional resolved-scope summary. Only the intake executor sets this; it lets
|
|
33
|
+
* the loader echo the audit target and gate on confirmation when
|
|
34
|
+
* `mis_scope_smells` is non-empty.
|
|
35
|
+
*/
|
|
36
|
+
scope_summary?: ScopeSummary;
|
|
12
37
|
}
|
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
import type { ArtifactBundle } from "../io/artifacts.js";
|
|
2
2
|
import type { ExecutorRunResult } from "./executorResult.js";
|
|
3
|
-
|
|
3
|
+
/** Prefix used to carry the scope summary inside `progress_summary` for hosts
|
|
4
|
+
* that read the step's progress text rather than the `scope_summary.json`
|
|
5
|
+
* artifact. The loader extracts everything after this marker as JSON. */
|
|
6
|
+
export declare const SCOPE_SUMMARY_PREFIX = "SCOPE_SUMMARY:";
|
|
7
|
+
/**
|
|
8
|
+
* Detect signals that the resolved audit root may be the *wrong* directory.
|
|
9
|
+
* Two heuristics, returned as zero or more human-readable warnings:
|
|
10
|
+
*
|
|
11
|
+
* a. No-git-but-ancestor-is-repo — `root` is not a git repo but some ancestor
|
|
12
|
+
* directory is (you probably targeted a subdirectory instead of the repo
|
|
13
|
+
* root).
|
|
14
|
+
* b. Workspace-member — `root` has a `package.json` with a `name`, and its
|
|
15
|
+
* parent has a `package.json` declaring `workspaces` (you probably want to
|
|
16
|
+
* audit from the monorepo root).
|
|
17
|
+
*
|
|
18
|
+
* Returns an empty array when the scope looks correct. Never throws.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectMisScopeSmells(root: string): string[];
|
|
21
|
+
export declare function runIntakeExecutor(bundle: ArtifactBundle, root: string, artifactsDir?: string): Promise<ExecutorRunResult>;
|
|
@@ -1,7 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { isGitRepo, writeJsonFile } from "@audit-tools/shared";
|
|
1
4
|
import { buildFileDisposition, isAuditExcludedStatus, } from "../extractors/disposition.js";
|
|
2
5
|
import { buildRepoManifestFromFs } from "../extractors/fsIntake.js";
|
|
3
6
|
import { loadIgnoreFile } from "../extractors/ignore.js";
|
|
4
|
-
|
|
7
|
+
/** Prefix used to carry the scope summary inside `progress_summary` for hosts
|
|
8
|
+
* that read the step's progress text rather than the `scope_summary.json`
|
|
9
|
+
* artifact. The loader extracts everything after this marker as JSON. */
|
|
10
|
+
export const SCOPE_SUMMARY_PREFIX = "SCOPE_SUMMARY:";
|
|
11
|
+
/**
|
|
12
|
+
* Detect signals that the resolved audit root may be the *wrong* directory.
|
|
13
|
+
* Two heuristics, returned as zero or more human-readable warnings:
|
|
14
|
+
*
|
|
15
|
+
* a. No-git-but-ancestor-is-repo — `root` is not a git repo but some ancestor
|
|
16
|
+
* directory is (you probably targeted a subdirectory instead of the repo
|
|
17
|
+
* root).
|
|
18
|
+
* b. Workspace-member — `root` has a `package.json` with a `name`, and its
|
|
19
|
+
* parent has a `package.json` declaring `workspaces` (you probably want to
|
|
20
|
+
* audit from the monorepo root).
|
|
21
|
+
*
|
|
22
|
+
* Returns an empty array when the scope looks correct. Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export function detectMisScopeSmells(root) {
|
|
25
|
+
const smells = [];
|
|
26
|
+
// (a) No .git here, but an ancestor is a git repository.
|
|
27
|
+
if (!isGitRepo(root)) {
|
|
28
|
+
let current = dirname(root);
|
|
29
|
+
let previous = root;
|
|
30
|
+
while (current && current !== previous) {
|
|
31
|
+
if (existsSync(join(current, ".git"))) {
|
|
32
|
+
smells.push(`root has no .git but ancestor '${current}' is a git repository — you may have targeted a subdirectory instead of the repo root`);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
previous = current;
|
|
36
|
+
current = dirname(current);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// (b) Workspace member of a parent monorepo.
|
|
40
|
+
const rootPkg = readPackageJson(root);
|
|
41
|
+
if (rootPkg && rootPkg.name !== undefined) {
|
|
42
|
+
const parent = dirname(root);
|
|
43
|
+
if (parent && parent !== root) {
|
|
44
|
+
const parentPkg = readPackageJson(parent);
|
|
45
|
+
if (parentPkg && parentPkg.workspaces !== undefined) {
|
|
46
|
+
smells.push(`root appears to be a workspace member of a parent monorepo at '${parent}' — consider auditing from the monorepo root instead`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return smells;
|
|
51
|
+
}
|
|
52
|
+
function readPackageJson(dir) {
|
|
53
|
+
const path = join(dir, "package.json");
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return undefined;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
58
|
+
return parsed && typeof parsed === "object"
|
|
59
|
+
? parsed
|
|
60
|
+
: undefined;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Missing or malformed package.json — treat as absent for smell purposes.
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function runIntakeExecutor(bundle, root, artifactsDir) {
|
|
5
68
|
const ignore = await loadIgnoreFile(root);
|
|
6
69
|
const repoManifest = await buildRepoManifestFromFs({
|
|
7
70
|
root,
|
|
@@ -13,13 +76,36 @@ export async function runIntakeExecutor(bundle, root) {
|
|
|
13
76
|
if (auditableCount === 0) {
|
|
14
77
|
throw new Error(`No auditable files found in ${root}. The repository may be empty, generated-only, documentation-only, or filtered by .auditorignore.`);
|
|
15
78
|
}
|
|
79
|
+
const scopeSummary = {
|
|
80
|
+
repo_root: root,
|
|
81
|
+
auditable_file_count: auditableCount,
|
|
82
|
+
git_available: isGitRepo(root),
|
|
83
|
+
mis_scope_smells: detectMisScopeSmells(root),
|
|
84
|
+
};
|
|
85
|
+
const artifactsWritten = ["repo_manifest.json", "file_disposition.json"];
|
|
86
|
+
// Persist the scope summary alongside the other intake artifacts when we know
|
|
87
|
+
// where the artifacts directory is. The typed `scope_summary` field and the
|
|
88
|
+
// progress_summary marker below carry the same data for hosts that don't read
|
|
89
|
+
// the file directly.
|
|
90
|
+
if (artifactsDir) {
|
|
91
|
+
await writeJsonFile(join(artifactsDir, "scope_summary.json"), scopeSummary);
|
|
92
|
+
artifactsWritten.push("scope_summary.json");
|
|
93
|
+
}
|
|
94
|
+
const progressSummary = `${SCOPE_SUMMARY_PREFIX}${JSON.stringify(scopeSummary)}\n` +
|
|
95
|
+
`Created intake artifacts for ${repoManifest.files.length} files ` +
|
|
96
|
+
`(${auditableCount} auditable). Scope: ${root}, git: ${scopeSummary.git_available ? "yes" : "no"}` +
|
|
97
|
+
(scopeSummary.mis_scope_smells.length > 0
|
|
98
|
+
? `; ${scopeSummary.mis_scope_smells.length} mis-scope warning(s)`
|
|
99
|
+
: "") +
|
|
100
|
+
".";
|
|
16
101
|
return {
|
|
17
102
|
updated: {
|
|
18
103
|
...bundle,
|
|
19
104
|
repo_manifest: repoManifest,
|
|
20
105
|
file_disposition: disposition,
|
|
21
106
|
},
|
|
22
|
-
artifacts_written:
|
|
23
|
-
progress_summary:
|
|
107
|
+
artifacts_written: artifactsWritten,
|
|
108
|
+
progress_summary: progressSummary,
|
|
109
|
+
scope_summary: scopeSummary,
|
|
24
110
|
};
|
|
25
111
|
}
|
|
@@ -6,5 +6,6 @@ export interface NextStepDecision {
|
|
|
6
6
|
selected_executor: string | null;
|
|
7
7
|
reason: string;
|
|
8
8
|
}
|
|
9
|
+
export declare const PRIORITY: string[];
|
|
9
10
|
export declare function findObligation(obligations: AuditObligation[]): AuditObligation | undefined;
|
|
10
11
|
export declare function decideNextStep(bundle: ArtifactBundle): NextStepDecision;
|
|
@@ -46,7 +46,14 @@ export function deriveAuditState(bundle) {
|
|
|
46
46
|
"requeue_tasks.json",
|
|
47
47
|
], planningReady)));
|
|
48
48
|
const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
|
|
49
|
-
|
|
49
|
+
// Tasks deferred by a budget cap (FINDING-013) will never have results, so
|
|
50
|
+
// they must be excluded from the completion check — otherwise the obligation
|
|
51
|
+
// loops forever under a budget. Absent active_dispatch => empty set => the
|
|
52
|
+
// logic is unchanged (all tasks must be complete).
|
|
53
|
+
const deferredTaskIds = new Set(bundle.active_dispatch?.deferred_task_ids ?? []);
|
|
54
|
+
const hasPendingAuditTasks = bundle.audit_tasks?.some((task) => task.status !== "complete" &&
|
|
55
|
+
!completedTaskIds.has(task.task_id) &&
|
|
56
|
+
!deferredTaskIds.has(task.task_id)) ?? false;
|
|
50
57
|
if (hasPendingAuditTasks) {
|
|
51
58
|
obligations.push(obligation("audit_tasks_completed", "missing"));
|
|
52
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { LOCAL_SUBPROCESS_PROVIDER_NAME } from "@audit-tools/shared";
|
|
1
|
+
export { LOCAL_SUBPROCESS_PROVIDER_NAME, CODEX_PROVIDER_NAME, ANTIGRAVITY_PROVIDER_NAME, } from "@audit-tools/shared";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { LOCAL_SUBPROCESS_PROVIDER_NAME } from "@audit-tools/shared";
|
|
1
|
+
export { LOCAL_SUBPROCESS_PROVIDER_NAME, CODEX_PROVIDER_NAME, ANTIGRAVITY_PROVIDER_NAME, } from "@audit-tools/shared";
|
|
@@ -28,6 +28,14 @@ export interface AuditReportSummary {
|
|
|
28
28
|
audited_file_count: number;
|
|
29
29
|
excluded_file_count: number;
|
|
30
30
|
runtime_validation_status_breakdown: Record<string, number>;
|
|
31
|
+
/**
|
|
32
|
+
* Distinct count of tasks/files NOT audited because a packet budget cap
|
|
33
|
+
* (FINDING-013) deferred them — kept separate from `excluded_file_count`
|
|
34
|
+
* (non-auditable files), since a budget skip is an honest partial-coverage
|
|
35
|
+
* signal, not an exclusion. Optional so the shared `AuditFindingsSummary`
|
|
36
|
+
* (which omits it) stays assignable to this render shape; defaults to 0.
|
|
37
|
+
*/
|
|
38
|
+
budget_deferred_task_count?: number;
|
|
31
39
|
}
|
|
32
40
|
export interface AuditReportModel {
|
|
33
41
|
summary: AuditReportSummary;
|
|
@@ -25,6 +25,8 @@ function coverageSummary(coverage) {
|
|
|
25
25
|
return {
|
|
26
26
|
audited_file_count: files.filter((file) => file.audit_status === "complete").length,
|
|
27
27
|
excluded_file_count: files.filter((file) => file.audit_status === "excluded").length,
|
|
28
|
+
// Distinct from excluded: files a budget cap deferred (status set by scope).
|
|
29
|
+
budget_deferred_task_count: files.filter((file) => file.audit_status === "budget_deferred").length,
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
function formatSeverityList(summary) {
|
|
@@ -50,6 +52,7 @@ export function buildAuditReportModel(params) {
|
|
|
50
52
|
severity_breakdown: severityBreakdown(findings),
|
|
51
53
|
audited_file_count: coverage.audited_file_count,
|
|
52
54
|
excluded_file_count: coverage.excluded_file_count,
|
|
55
|
+
budget_deferred_task_count: coverage.budget_deferred_task_count,
|
|
53
56
|
runtime_validation_status_breakdown: runtimeStatusBreakdown(params.runtimeValidationReport),
|
|
54
57
|
},
|
|
55
58
|
findings,
|
|
@@ -117,7 +120,11 @@ export function renderAuditReportMarkdown(report, options = {}) {
|
|
|
117
120
|
if (report.executive_summary && report.executive_summary.trim().length > 0) {
|
|
118
121
|
lines.push("## Executive Summary", "", report.executive_summary.trim(), "");
|
|
119
122
|
}
|
|
120
|
-
lines.push("## Summary", "", `- Findings: ${report.summary.finding_count}`, `- Work blocks: ${report.summary.work_block_count}`, `- Severity breakdown: ${formatSeverityList(report.summary.severity_breakdown)}`, `- Fully audited files: ${report.summary.audited_file_count}`, `- Excluded non-auditable files: ${report.summary.excluded_file_count}`,
|
|
123
|
+
lines.push("## Summary", "", `- Findings: ${report.summary.finding_count}`, `- Work blocks: ${report.summary.work_block_count}`, `- Severity breakdown: ${formatSeverityList(report.summary.severity_breakdown)}`, `- Fully audited files: ${report.summary.audited_file_count}`, `- Excluded non-auditable files: ${report.summary.excluded_file_count}`, ...((report.summary.budget_deferred_task_count ?? 0) > 0
|
|
124
|
+
? [
|
|
125
|
+
`- Not audited (budget): ${report.summary.budget_deferred_task_count} task(s) skipped by packet budget cap`,
|
|
126
|
+
]
|
|
127
|
+
: []), "");
|
|
121
128
|
if (report.top_risks && report.top_risks.length > 0) {
|
|
122
129
|
lines.push("## Top Risks", "");
|
|
123
130
|
for (const risk of report.top_risks) {
|
|
@@ -186,6 +193,14 @@ export function renderAuditReportMarkdown(report, options = {}) {
|
|
|
186
193
|
lines.push("", scope.dropped_note);
|
|
187
194
|
}
|
|
188
195
|
}
|
|
196
|
+
else if (scope && scope.mode === "budget") {
|
|
197
|
+
lines.push(`**Partial audit (budget cap).** This run dispatched only the top-${scope.budget?.max_files ?? "K"} packet(s); ` +
|
|
198
|
+
`${scope.deferred_packet_count ?? 0} packet(s) covering ${scope.deferred_task_ids?.length ?? 0} task(s) were deferred and NOT audited. ` +
|
|
199
|
+
`Findings above reflect only the audited subset. **A full audit is advised before release.**`);
|
|
200
|
+
if (scope.dropped_note) {
|
|
201
|
+
lines.push("", scope.dropped_note);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
189
204
|
else {
|
|
190
205
|
lines.push("This report is deterministic output from the completed audit. Non-auditable files were excluded from scope before task generation.");
|
|
191
206
|
}
|
|
@@ -24,9 +24,11 @@ const NON_PENDING_OBLIGATION_STATES = new Set([
|
|
|
24
24
|
const INTERACTIVE_PROVIDER_OPTIONS = [
|
|
25
25
|
"auto",
|
|
26
26
|
"claude-code",
|
|
27
|
+
"codex",
|
|
27
28
|
"opencode",
|
|
28
29
|
"subprocess-template",
|
|
29
30
|
"vscode-task",
|
|
31
|
+
"antigravity",
|
|
30
32
|
];
|
|
31
33
|
function quoteShellPath(filePath) {
|
|
32
34
|
// The handoff renders a single shell argument, so the snippet only needs
|
|
@@ -252,7 +254,12 @@ export function buildAuditCodeHandoff(params) {
|
|
|
252
254
|
current_task: artifactPaths.current_task,
|
|
253
255
|
current_prompt: artifactPaths.current_prompt,
|
|
254
256
|
audit_results: params.activeReviewRun.audit_results_path,
|
|
255
|
-
|
|
257
|
+
// Synthesis writes the report into the artifacts dir; it is only promoted
|
|
258
|
+
// to <repo-root>/audit-report.md at completion (which then removes the
|
|
259
|
+
// artifacts dir). A blocked-for-review handoff happens before that, so the
|
|
260
|
+
// advertised deliverable must point at its real mid-run location, not the
|
|
261
|
+
// repo-root path that does not exist yet.
|
|
262
|
+
final_report: join(params.artifactsDir, AUDIT_REPORT_FILENAME),
|
|
256
263
|
};
|
|
257
264
|
}
|
|
258
265
|
return handoff;
|
|
@@ -19,8 +19,12 @@ export interface AuditScopeBudget {
|
|
|
19
19
|
max_files: number;
|
|
20
20
|
}
|
|
21
21
|
export interface AuditScopeManifest {
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* `full` audits every auditable file; `delta` scopes to a changed
|
|
24
|
+
* neighbourhood; `budget` dispatches only the top-K review packets under a
|
|
25
|
+
* `max_packets` cap and defers the rest.
|
|
26
|
+
*/
|
|
27
|
+
mode: "full" | "delta" | "budget";
|
|
24
28
|
/** Git ref/SHA the delta was measured against; `null` in full mode. */
|
|
25
29
|
since: string | null;
|
|
26
30
|
/**
|
|
@@ -40,4 +44,14 @@ export interface AuditScopeManifest {
|
|
|
40
44
|
* requested `--since` could not be honoured and the run fell back to full.
|
|
41
45
|
*/
|
|
42
46
|
dropped_note?: string;
|
|
47
|
+
/**
|
|
48
|
+
* When `mode === 'budget'`: the number of review packets that were NOT
|
|
49
|
+
* dispatched due to the `max_packets` cap. Present only in budget mode.
|
|
50
|
+
*/
|
|
51
|
+
deferred_packet_count?: number;
|
|
52
|
+
/**
|
|
53
|
+
* When `mode === 'budget'`: the task_ids skipped due to the budget cap.
|
|
54
|
+
* Present only in budget mode.
|
|
55
|
+
*/
|
|
56
|
+
deferred_task_ids?: string[];
|
|
43
57
|
}
|