auditor-lambda 0.3.26 → 0.3.28
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 +190 -75
- package/dist/cli.js +47 -4
- package/dist/mcp/server.js +9 -0
- package/dist/quota/hostLimits.d.ts +8 -0
- package/dist/quota/hostLimits.js +50 -0
- package/dist/quota/index.d.ts +2 -1
- package/dist/quota/index.js +1 -0
- package/dist/quota/scheduler.d.ts +2 -1
- package/dist/quota/scheduler.js +12 -3
- package/dist/quota/types.d.ts +8 -0
- package/dist/reporting/mergeFindings.js +86 -1
- package/dist/types/sessionConfig.d.ts +2 -0
- package/package.json +1 -1
- package/schemas/dispatch_quota.schema.json +25 -0
- package/scripts/postinstall.mjs +1 -3
|
@@ -399,21 +399,122 @@ async function writeGeneratedMarkdown(targetPath, content) {
|
|
|
399
399
|
};
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
function looksLikeAuditCodeSkill(content) {
|
|
403
|
+
const normalized = normalizeNewlines(content);
|
|
404
|
+
return (
|
|
405
|
+
/^name:\s*audit-code\b/mu.test(normalized)
|
|
406
|
+
|| normalized.includes('Conversation-first autonomous code auditing workflow for the /audit-code command.')
|
|
407
|
+
|| normalized.includes('The canonical entrypoint is `/audit-code` in conversation.')
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function looksLikeAuditCodePrompt(content) {
|
|
412
|
+
const normalized = normalizeNewlines(content);
|
|
413
|
+
return (
|
|
414
|
+
normalized.includes('# `/audit-code`')
|
|
415
|
+
&& (
|
|
416
|
+
normalized.includes('audit-code orchestrator')
|
|
417
|
+
|| normalized.includes('Autonomous local loop code auditing')
|
|
418
|
+
|| normalized.includes('Conversation-first autonomous code auditing workflow')
|
|
419
|
+
)
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function looksLikeAuditCodeInterfaceMetadata(content) {
|
|
424
|
+
const normalized = normalizeNewlines(content);
|
|
425
|
+
return (
|
|
426
|
+
normalized.includes('audit-code')
|
|
427
|
+
&& (
|
|
428
|
+
normalized.includes('display_name:')
|
|
429
|
+
|| normalized.includes('short_description:')
|
|
430
|
+
|| normalized.includes('default_prompt:')
|
|
431
|
+
)
|
|
432
|
+
&& (
|
|
433
|
+
normalized.includes('/audit-code')
|
|
434
|
+
|| normalized.includes('Start /audit-code')
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function buildLegacyAuditCodeSurfaceTargets(root) {
|
|
440
|
+
const targets = [
|
|
441
|
+
{
|
|
442
|
+
host: 'codex',
|
|
443
|
+
surface: 'skill',
|
|
444
|
+
path: join(root, '.codex', 'skills', 'audit-code', 'SKILL.md'),
|
|
445
|
+
matches: looksLikeAuditCodeSkill,
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
host: 'codex',
|
|
449
|
+
surface: 'prompt',
|
|
450
|
+
path: join(root, '.codex', 'skills', 'audit-code', 'audit-code.prompt.md'),
|
|
451
|
+
matches: looksLikeAuditCodePrompt,
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
host: 'opencode',
|
|
455
|
+
surface: 'command',
|
|
456
|
+
path: join(root, '.opencode', 'commands', 'audit-code.md'),
|
|
457
|
+
matches: looksLikeAuditCodePrompt,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
host: 'opencode',
|
|
461
|
+
surface: 'skill',
|
|
462
|
+
path: join(root, '.opencode', 'skills', 'audit-code', 'SKILL.md'),
|
|
463
|
+
matches: looksLikeAuditCodeSkill,
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
host: 'opencode',
|
|
467
|
+
surface: 'prompt',
|
|
468
|
+
path: join(root, '.opencode', 'skills', 'audit-code', 'audit-code.prompt.md'),
|
|
469
|
+
matches: looksLikeAuditCodePrompt,
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
host: 'claude',
|
|
473
|
+
surface: 'command',
|
|
474
|
+
path: join(root, '.claude', 'commands', 'audit-code.md'),
|
|
475
|
+
matches: looksLikeAuditCodePrompt,
|
|
476
|
+
},
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
const codexAgentDir = join(root, '.codex', 'skills', 'audit-code', 'agents');
|
|
480
|
+
const codexAgentEntries = await readdir(codexAgentDir).catch(() => []);
|
|
481
|
+
for (const entry of codexAgentEntries) {
|
|
482
|
+
targets.push({
|
|
483
|
+
host: 'codex',
|
|
484
|
+
surface: 'interface-metadata',
|
|
485
|
+
path: join(codexAgentDir, entry),
|
|
486
|
+
matches: looksLikeAuditCodeInterfaceMetadata,
|
|
487
|
+
});
|
|
406
488
|
}
|
|
407
489
|
|
|
408
|
-
|
|
409
|
-
|
|
490
|
+
return targets;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function findLegacyAuditCodeSurfaceFiles(root) {
|
|
494
|
+
const matches = [];
|
|
495
|
+
for (const target of await buildLegacyAuditCodeSurfaceTargets(root)) {
|
|
496
|
+
const existing = await readTextIfExists(target.path);
|
|
497
|
+
if (existing !== null && target.matches(existing)) {
|
|
498
|
+
matches.push(target.path);
|
|
499
|
+
}
|
|
410
500
|
}
|
|
501
|
+
return matches;
|
|
502
|
+
}
|
|
411
503
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
504
|
+
async function removeLegacyAuditCodeSurfaceFiles(root) {
|
|
505
|
+
const removed = [];
|
|
506
|
+
for (const target of await buildLegacyAuditCodeSurfaceTargets(root)) {
|
|
507
|
+
const existing = await readTextIfExists(target.path);
|
|
508
|
+
if (existing === null || !target.matches(existing)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
await unlink(target.path);
|
|
512
|
+
removed.push({
|
|
513
|
+
path: target.path,
|
|
514
|
+
mode: 'removed',
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return removed;
|
|
417
518
|
}
|
|
418
519
|
|
|
419
520
|
async function writeGeneratedJson(targetPath, value) {
|
|
@@ -608,9 +709,7 @@ const OPENCODE_MCP_COMMAND_TEMPLATE = [
|
|
|
608
709
|
'Use the auditor MCP tools as the primary interface to the audit workflow.',
|
|
609
710
|
'',
|
|
610
711
|
'1. Call `auditor_start_audit` to initialize and receive the first step.',
|
|
611
|
-
'2.
|
|
612
|
-
' - If `step_kind` is `"capability_check"`: immediately call `auditor_report_capability` with `can_dispatch_subagents: true` and `can_select_subagent_model: true`. Do not run shell commands or inspect prompt_content for this step.',
|
|
613
|
-
' - Otherwise: read `prompt_content` and follow it.',
|
|
712
|
+
'2. Read `prompt_content` in the response and follow it.',
|
|
614
713
|
'3. When a step completes (not blocked), call `auditor_continue_audit` to advance.',
|
|
615
714
|
'4. Stop when the step instructions say to stop.',
|
|
616
715
|
'',
|
|
@@ -620,18 +719,9 @@ const OPENCODE_MCP_COMMAND_TEMPLATE = [
|
|
|
620
719
|
].join('\n');
|
|
621
720
|
|
|
622
721
|
function renderOpenCodeProjectConfig(_root) {
|
|
623
|
-
const launcher = `.audit-code/install/${MCP_LAUNCHER_FILENAME}`;
|
|
624
722
|
const auditPermission = renderOpenCodePermissionConfig();
|
|
625
723
|
return {
|
|
626
724
|
$schema: 'https://opencode.ai/config.json',
|
|
627
|
-
mcp: {
|
|
628
|
-
auditor: {
|
|
629
|
-
type: 'local',
|
|
630
|
-
command: ['node', launcher],
|
|
631
|
-
enabled: true,
|
|
632
|
-
timeout: 10000,
|
|
633
|
-
},
|
|
634
|
-
},
|
|
635
725
|
permission: auditPermission,
|
|
636
726
|
agent: {
|
|
637
727
|
auditor: {
|
|
@@ -803,14 +893,13 @@ function assertOpenCodeAuditPermissionConfig(permissionConfig, label) {
|
|
|
803
893
|
|
|
804
894
|
function buildMergedOpenCodeProjectConfig(existing, root) {
|
|
805
895
|
const generated = renderOpenCodeProjectConfig(root);
|
|
896
|
+
const mergedMcp = objectValue(existing.mcp);
|
|
897
|
+
delete mergedMcp.auditor;
|
|
806
898
|
return {
|
|
807
899
|
...existing,
|
|
808
900
|
$schema: existing.$schema ?? generated.$schema,
|
|
809
901
|
command: removeManagedOpenCodeCommand(existing.command),
|
|
810
|
-
mcp:
|
|
811
|
-
...objectValue(existing.mcp),
|
|
812
|
-
auditor: generated.mcp.auditor,
|
|
813
|
-
},
|
|
902
|
+
mcp: mergedMcp,
|
|
814
903
|
permission: {
|
|
815
904
|
...mergeOpenCodePermissionConfig(existing.permission, generated.permission),
|
|
816
905
|
external_directory: { '*': 'allow' },
|
|
@@ -907,6 +996,21 @@ function renderAntigravityPlanningGuide(root) {
|
|
|
907
996
|
].join('\n');
|
|
908
997
|
}
|
|
909
998
|
|
|
999
|
+
function renderGeminiCommandToml(promptBody) {
|
|
1000
|
+
const escapedBody = promptBody.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1001
|
+
return [
|
|
1002
|
+
'# /audit-code \u2014 Autonomous local-loop code auditing',
|
|
1003
|
+
'# Registered as a Gemini/Antigravity slash command.',
|
|
1004
|
+
'',
|
|
1005
|
+
'description = "Autonomous local-loop code auditing \u2014 loads one backend-rendered audit step at a time"',
|
|
1006
|
+
'',
|
|
1007
|
+
'prompt = """',
|
|
1008
|
+
promptBody.trimEnd(),
|
|
1009
|
+
'"""',
|
|
1010
|
+
'',
|
|
1011
|
+
].join('\n');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
910
1014
|
function renderSharedMcpLauncher(sourcePackageRoot) {
|
|
911
1015
|
return [
|
|
912
1016
|
"import { access, readFile } from 'node:fs/promises';",
|
|
@@ -1339,19 +1443,20 @@ const INSTALL_HOST_DEFINITIONS = {
|
|
|
1339
1443
|
antigravity: {
|
|
1340
1444
|
host: 'antigravity',
|
|
1341
1445
|
label: 'Antigravity',
|
|
1342
|
-
support_level: '
|
|
1343
|
-
setup_kind: 'planning-guide+mcp-ready',
|
|
1446
|
+
support_level: 'supported',
|
|
1447
|
+
setup_kind: 'gemini-command+planning-guide+mcp-ready',
|
|
1344
1448
|
summary:
|
|
1345
|
-
'
|
|
1346
|
-
primary_path_key: '
|
|
1449
|
+
'Use the repo-local .gemini/commands/audit-code.toml slash command, the planning guide, and AGENTS instructions. The shared MCP launcher is available for structured tool calls.',
|
|
1450
|
+
primary_path_key: 'geminiCommandPath',
|
|
1347
1451
|
supporting_path_keys: [
|
|
1452
|
+
'antigravityPlanningGuidePath',
|
|
1348
1453
|
'agentsInstructionsPath',
|
|
1349
1454
|
'mcpLauncherPath',
|
|
1350
1455
|
'installedPromptPath',
|
|
1351
1456
|
],
|
|
1352
1457
|
steps: [
|
|
1353
|
-
'Open this repository in Antigravity
|
|
1354
|
-
'
|
|
1458
|
+
'Open this repository in Antigravity.',
|
|
1459
|
+
'The /audit-code slash command is automatically discovered from .gemini/commands/audit-code.toml.',
|
|
1355
1460
|
'Use the shared auditor MCP server when Antigravity needs structured audit state instead of free-form shell guesses.',
|
|
1356
1461
|
],
|
|
1357
1462
|
profile: {
|
|
@@ -1857,6 +1962,18 @@ async function verifyInstalledBootstrap(argv) {
|
|
|
1857
1962
|
};
|
|
1858
1963
|
});
|
|
1859
1964
|
|
|
1965
|
+
await collectVerifyCheck(generalChecks, 'legacy_local_surfaces', async () => {
|
|
1966
|
+
const legacySurfaces = await findLegacyAuditCodeSurfaceFiles(root);
|
|
1967
|
+
if (legacySurfaces.length > 0) {
|
|
1968
|
+
throw new Error(
|
|
1969
|
+
`Legacy local /audit-code surfaces are still present: ${legacySurfaces.join(', ')}. Run "audit-code install" from ${root}.`,
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
summary: 'No legacy local /audit-code command or skill surfaces were found.',
|
|
1974
|
+
};
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1860
1977
|
await collectVerifyCheck(generalChecks, 'shared_launcher_file', async () => {
|
|
1861
1978
|
const launcher = await readFile(assetPaths.mcpLauncherPath, 'utf8');
|
|
1862
1979
|
if (!launcher.includes('Unable to locate an audit-code executable')) {
|
|
@@ -2005,23 +2122,16 @@ async function verifyInstalledBootstrap(argv) {
|
|
|
2005
2122
|
case 'opencode':
|
|
2006
2123
|
await collectVerifyCheck(checks, 'opencode_config', async () => {
|
|
2007
2124
|
const config = await readJson(assetPaths.opencodeConfigPath, 'OpenCode project config');
|
|
2008
|
-
const mcpCommand = config?.mcp?.auditor?.command;
|
|
2009
|
-
if (!Array.isArray(mcpCommand) || mcpCommand[0] !== 'node') {
|
|
2010
|
-
throw new Error('OpenCode config must set mcp.auditor.command as a Node command array.');
|
|
2011
|
-
}
|
|
2012
|
-
if (!mcpCommand[1]?.includes(MCP_LAUNCHER_FILENAME)) {
|
|
2013
|
-
throw new Error(`OpenCode config must reference ${MCP_LAUNCHER_FILENAME}, got ${mcpCommand[1] ?? 'missing'}.`);
|
|
2014
|
-
}
|
|
2015
|
-
if (config?.mcp?.auditor?.type !== 'local') {
|
|
2016
|
-
throw new Error(`OpenCode config must set mcp.auditor.type to "local", got ${config?.mcp?.auditor?.type ?? 'missing'}.`);
|
|
2017
|
-
}
|
|
2018
2125
|
if (config?.command?.['audit-code']) {
|
|
2019
2126
|
throw new Error('OpenCode project config must not define command["audit-code"]; the slash command is global npm-installed state. Run "audit-code install --host opencode" to remove the stale local command.');
|
|
2020
2127
|
}
|
|
2128
|
+
if (config?.mcp?.auditor) {
|
|
2129
|
+
throw new Error('OpenCode project config must not define mcp.auditor; the MCP server is supplied by the global npm-installed config. Run "audit-code install --host opencode" to remove the stale project-level MCP entry.');
|
|
2130
|
+
}
|
|
2021
2131
|
assertOpenCodeAuditPermissionConfig(config?.permission, 'permission');
|
|
2022
2132
|
assertOpenCodeAuditPermissionConfig(config?.agent?.auditor?.permission, 'agent.auditor.permission');
|
|
2023
2133
|
return {
|
|
2024
|
-
summary: 'OpenCode project config has MCP server and
|
|
2134
|
+
summary: 'OpenCode project config has audit permissions; MCP server and /audit-code command are supplied by the global npm-installed config.',
|
|
2025
2135
|
path: assetPaths.opencodeConfigPath,
|
|
2026
2136
|
};
|
|
2027
2137
|
});
|
|
@@ -2149,7 +2259,11 @@ async function detectBootstrapRefreshReason(root, host) {
|
|
|
2149
2259
|
);
|
|
2150
2260
|
|
|
2151
2261
|
if (hostCatalog.has('codex') && (assetPaths.codexSkillPath || assetPaths.codexPromptPath)) {
|
|
2152
|
-
return '
|
|
2262
|
+
return 'legacy_local_audit_code_surface';
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
if ((await findLegacyAuditCodeSurfaceFiles(root)).length > 0) {
|
|
2266
|
+
return 'legacy_local_audit_code_surface';
|
|
2153
2267
|
}
|
|
2154
2268
|
|
|
2155
2269
|
for (const hostKey of getInstallHostKeys(host)) {
|
|
@@ -2199,6 +2313,9 @@ async function detectBootstrapRefreshReason(root, host) {
|
|
|
2199
2313
|
if (opencodeConfig?.command?.['audit-code']) {
|
|
2200
2314
|
return 'stale_host_asset:opencode:local_command';
|
|
2201
2315
|
}
|
|
2316
|
+
if (opencodeConfig?.mcp?.auditor) {
|
|
2317
|
+
return 'stale_host_asset:opencode:project_mcp';
|
|
2318
|
+
}
|
|
2202
2319
|
try {
|
|
2203
2320
|
assertOpenCodeAuditPermissionConfig(opencodeConfig?.permission, 'permission');
|
|
2204
2321
|
assertOpenCodeAuditPermissionConfig(opencodeConfig?.agent?.auditor?.permission, 'agent.auditor.permission');
|
|
@@ -2350,6 +2467,9 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2350
2467
|
antigravityPlanningGuidePath: profile.writeAntigravity
|
|
2351
2468
|
? join(root, '.audit-code', 'install', 'antigravity', 'PLANNING-MODE.md')
|
|
2352
2469
|
: null,
|
|
2470
|
+
geminiCommandPath: profile.writeAntigravity
|
|
2471
|
+
? join(root, '.gemini', 'commands', 'audit-code.toml')
|
|
2472
|
+
: null,
|
|
2353
2473
|
};
|
|
2354
2474
|
|
|
2355
2475
|
const results = [];
|
|
@@ -2384,22 +2504,7 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2384
2504
|
);
|
|
2385
2505
|
}
|
|
2386
2506
|
|
|
2387
|
-
|
|
2388
|
-
const legacyCodexSkillRemoval = await removeGeneratedMarkdownIfMatches(
|
|
2389
|
-
join(root, '.codex', 'skills', 'audit-code', 'SKILL.md'),
|
|
2390
|
-
skillSource,
|
|
2391
|
-
);
|
|
2392
|
-
if (legacyCodexSkillRemoval) {
|
|
2393
|
-
results.push(legacyCodexSkillRemoval);
|
|
2394
|
-
}
|
|
2395
|
-
const legacyCodexPromptRemoval = await removeGeneratedMarkdownIfMatches(
|
|
2396
|
-
join(root, '.codex', 'skills', 'audit-code', 'audit-code.prompt.md'),
|
|
2397
|
-
promptSource,
|
|
2398
|
-
);
|
|
2399
|
-
if (legacyCodexPromptRemoval) {
|
|
2400
|
-
results.push(legacyCodexPromptRemoval);
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2507
|
+
results.push(...await removeLegacyAuditCodeSurfaceFiles(root));
|
|
2403
2508
|
|
|
2404
2509
|
if (profile.writeCodex) {
|
|
2405
2510
|
results.push(
|
|
@@ -2447,19 +2552,6 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2447
2552
|
}
|
|
2448
2553
|
|
|
2449
2554
|
if (profile.writeOpenCode) {
|
|
2450
|
-
// Remove legacy command/skill/prompt files unconditionally — these paths are exclusively
|
|
2451
|
-
// owned by the auditor-lambda installer and keeping any version of them causes OpenCode to
|
|
2452
|
-
// load the wrong slash command regardless of prompt content.
|
|
2453
|
-
for (const legacyPath of [
|
|
2454
|
-
join(root, '.opencode', 'commands', 'audit-code.md'),
|
|
2455
|
-
join(root, '.opencode', 'skills', 'audit-code', 'SKILL.md'),
|
|
2456
|
-
join(root, '.opencode', 'skills', 'audit-code', 'audit-code.prompt.md'),
|
|
2457
|
-
]) {
|
|
2458
|
-
if (await fileExists(legacyPath)) {
|
|
2459
|
-
await unlink(legacyPath);
|
|
2460
|
-
results.push({ path: legacyPath, mode: 'removed' });
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
2555
|
results.push(
|
|
2464
2556
|
await writeMergedGeneratedJson(
|
|
2465
2557
|
assetPaths.opencodeConfigPath,
|
|
@@ -2505,6 +2597,12 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2505
2597
|
renderAntigravityPlanningGuide(root),
|
|
2506
2598
|
),
|
|
2507
2599
|
);
|
|
2600
|
+
results.push(
|
|
2601
|
+
await writeGeneratedMarkdown(
|
|
2602
|
+
assetPaths.geminiCommandPath,
|
|
2603
|
+
renderGeminiCommandToml(promptBody),
|
|
2604
|
+
),
|
|
2605
|
+
);
|
|
2508
2606
|
}
|
|
2509
2607
|
|
|
2510
2608
|
const hostGuidance = buildHostCatalog({
|
|
@@ -2566,6 +2664,7 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2566
2664
|
slash_command_surfaces: {
|
|
2567
2665
|
vscode_prompt: assetPaths.vscodePromptPath,
|
|
2568
2666
|
opencode_config: assetPaths.opencodeConfigPath,
|
|
2667
|
+
gemini_command: assetPaths.geminiCommandPath,
|
|
2569
2668
|
},
|
|
2570
2669
|
instruction_surfaces: {
|
|
2571
2670
|
agents: assetPaths.agentsInstructionsPath,
|
|
@@ -2625,6 +2724,22 @@ async function runDistCommand(commandName, argv, { ensureArtifactsDir = false }
|
|
|
2625
2724
|
await run(nodeExecutable(), [distEntry, commandName, ...commandArgs]);
|
|
2626
2725
|
}
|
|
2627
2726
|
|
|
2727
|
+
async function runDistCommandInline(commandName, argv) {
|
|
2728
|
+
const commandArgs = [...argv];
|
|
2729
|
+
const rootValue = resolve(getFlag(commandArgs, '--root') ?? '.');
|
|
2730
|
+
const artifactsDir = resolve(getFlag(commandArgs, '--artifacts-dir') ?? join(rootValue, '.audit-artifacts'));
|
|
2731
|
+
|
|
2732
|
+
setDefaultFlag(commandArgs, '--root', rootValue);
|
|
2733
|
+
setDefaultFlag(commandArgs, '--artifacts-dir', artifactsDir);
|
|
2734
|
+
|
|
2735
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
2736
|
+
await ensureBuilt();
|
|
2737
|
+
|
|
2738
|
+
const distUrl = new URL(`file:///${distEntry.replace(/\\/g, '/')}`);
|
|
2739
|
+
const cli = await import(distUrl.href);
|
|
2740
|
+
await cli.runCli([process.execPath, distEntry, commandName, ...commandArgs]);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2628
2743
|
export async function runAuditCodeWrapper({
|
|
2629
2744
|
usageName,
|
|
2630
2745
|
argv = process.argv.slice(2),
|
|
@@ -2683,7 +2798,7 @@ export async function runAuditCodeWrapper({
|
|
|
2683
2798
|
}
|
|
2684
2799
|
|
|
2685
2800
|
if (argv[0] === 'mcp') {
|
|
2686
|
-
await
|
|
2801
|
+
await runDistCommandInline('mcp', argv.slice(1));
|
|
2687
2802
|
return;
|
|
2688
2803
|
}
|
|
2689
2804
|
|
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ import { buildReviewPackets, orderTasksForPacketReview, } from "./orchestrator/r
|
|
|
32
32
|
import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
|
|
33
33
|
import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
|
|
34
34
|
import { runAuditCodeMcpServer } from "./mcp/server.js";
|
|
35
|
-
import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, } from "./quota/index.js";
|
|
35
|
+
import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, } from "./quota/index.js";
|
|
36
36
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
37
37
|
const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
|
|
38
38
|
const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
|
|
@@ -188,6 +188,9 @@ function getExplicitProvider(argv) {
|
|
|
188
188
|
function getHostModel(argv) {
|
|
189
189
|
return getFlag(argv, "--host-model") ?? null;
|
|
190
190
|
}
|
|
191
|
+
function getHostMaxActiveSubagents(argv) {
|
|
192
|
+
return parsePositiveIntegerFlag(argv, "--host-max-active-subagents") ?? null;
|
|
193
|
+
}
|
|
191
194
|
function getQuotaProbeMode(argv, sessionConfig) {
|
|
192
195
|
const raw = getFlag(argv, "--quota-probe") ?? sessionConfig.quota?.probe ?? "auto";
|
|
193
196
|
if (raw === "auto" || raw === "never" || raw === "force")
|
|
@@ -539,6 +542,7 @@ function renderCapabilityCheckPrompt(params) {
|
|
|
539
542
|
"- `can_dispatch_subagents: true` if the `task` tool or equivalent subagent dispatch is available",
|
|
540
543
|
"- `can_dispatch_subagents: false` if not",
|
|
541
544
|
"- Optionally `can_restrict_subagent_tools: true` and/or `can_select_subagent_model: true`",
|
|
545
|
+
"- If the host documents or exposes a hard cap on simultaneously active subagents, include `max_active_subagents`.",
|
|
542
546
|
"",
|
|
543
547
|
"Read the `prompt_content` field in the tool response and follow it.",
|
|
544
548
|
"",
|
|
@@ -554,6 +558,8 @@ function renderCapabilityCheckPrompt(params) {
|
|
|
554
558
|
"",
|
|
555
559
|
"If the host can also restrict tools per subagent or select models per subagent, add the matching `--host-can-restrict-subagent-tools true` or `--host-can-select-subagent-model true` flags to the same command. Omit those flags when unsure.",
|
|
556
560
|
"",
|
|
561
|
+
"If the host has a known active-subagent ceiling, add `--host-max-active-subagents <n>` to the same command. For Codex Desktop, use 6.",
|
|
562
|
+
"",
|
|
557
563
|
"After the command writes the next step, read and follow only its `prompt_path`.",
|
|
558
564
|
"",
|
|
559
565
|
].join("\n");
|
|
@@ -576,6 +582,8 @@ function renderDispatchReviewPrompt(params) {
|
|
|
576
582
|
"",
|
|
577
583
|
"Use the `wave_size` from `dispatch_quota`. If `cooldown_until` is non-null, wait until that timestamp before starting the first wave.",
|
|
578
584
|
"",
|
|
585
|
+
"`dispatch_quota.host_concurrency_limit` records any detected hard host cap that contributed to `wave_size`.",
|
|
586
|
+
"",
|
|
579
587
|
"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.",
|
|
580
588
|
"",
|
|
581
589
|
"**Fallback — if auditor MCP tools are not available:** Read both of these files:",
|
|
@@ -1134,6 +1142,7 @@ async function cmdNextStep(argv) {
|
|
|
1134
1142
|
const hostCanRestrictSubagentTools = getOptionalBooleanFlag(argv, "--host-can-restrict-subagent-tools") ??
|
|
1135
1143
|
false;
|
|
1136
1144
|
const hostCanSelectSubagentModel = getOptionalBooleanFlag(argv, "--host-can-select-subagent-model") ?? false;
|
|
1145
|
+
const hostMaxActiveSubagents = getHostMaxActiveSubagents(argv);
|
|
1137
1146
|
let sessionConfig;
|
|
1138
1147
|
try {
|
|
1139
1148
|
sessionConfig = await loadSessionConfig(artifactsDir);
|
|
@@ -1260,6 +1269,7 @@ async function cmdNextStep(argv) {
|
|
|
1260
1269
|
runId: result.activeReviewRun.run_id,
|
|
1261
1270
|
artifactsDir,
|
|
1262
1271
|
root,
|
|
1272
|
+
hostActiveSubagentLimit: hostMaxActiveSubagents,
|
|
1263
1273
|
});
|
|
1264
1274
|
const mergeCommand = mergeAndIngestCommand(artifactsDir, result.activeReviewRun.run_id);
|
|
1265
1275
|
const continueCommand = nextStepCommand(root, artifactsDir);
|
|
@@ -2238,8 +2248,18 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2238
2248
|
const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
|
|
2239
2249
|
const lensDefs = await readJsonFile(lensDefsPath);
|
|
2240
2250
|
await mkdir(taskResultsDir, { recursive: true });
|
|
2241
|
-
|
|
2242
|
-
const
|
|
2251
|
+
// On resume: skip tasks whose result files already exist from a prior dispatch.
|
|
2252
|
+
const priorResultTaskIds = new Set();
|
|
2253
|
+
for (const task of tasks) {
|
|
2254
|
+
if (existsSync(taskResultPath(taskResultsDir, task.task_id))) {
|
|
2255
|
+
priorResultTaskIds.add(task.task_id);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
const dispatchTasks = priorResultTaskIds.size > 0
|
|
2259
|
+
? tasks.filter((task) => !priorResultTaskIds.has(task.task_id))
|
|
2260
|
+
: tasks;
|
|
2261
|
+
const lineIndex = Object.fromEntries(dispatchTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
|
|
2262
|
+
const orderedTasks = orderTasksForPacketReview(dispatchTasks, {
|
|
2243
2263
|
graphBundle: bundle.graph_bundle,
|
|
2244
2264
|
lineIndex,
|
|
2245
2265
|
});
|
|
@@ -2258,6 +2278,15 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2258
2278
|
}
|
|
2259
2279
|
const plan = [];
|
|
2260
2280
|
const resultMapEntries = [];
|
|
2281
|
+
for (const task of tasks) {
|
|
2282
|
+
if (priorResultTaskIds.has(task.task_id)) {
|
|
2283
|
+
resultMapEntries.push({
|
|
2284
|
+
packet_id: "__prior_dispatch__",
|
|
2285
|
+
task_id: task.task_id,
|
|
2286
|
+
result_path: taskResultPath(taskResultsDir, task.task_id),
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2261
2290
|
let largestPacketId = null;
|
|
2262
2291
|
let largestLines = 0;
|
|
2263
2292
|
let largestEstimatedTokens = 0;
|
|
@@ -2469,13 +2498,18 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2469
2498
|
const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
|
|
2470
2499
|
const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
|
|
2471
2500
|
const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
|
|
2501
|
+
const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
|
|
2502
|
+
explicitLimit: params.hostActiveSubagentLimit,
|
|
2503
|
+
sessionConfig,
|
|
2504
|
+
});
|
|
2472
2505
|
const waveSchedule = scheduleWave({
|
|
2473
2506
|
providerName: quotaProviderName,
|
|
2474
2507
|
sessionConfig,
|
|
2475
2508
|
hostModel,
|
|
2476
|
-
requestedConcurrency: sessionConfig.parallel_workers ??
|
|
2509
|
+
requestedConcurrency: sessionConfig.parallel_workers ?? plan.length,
|
|
2477
2510
|
estimatedPacketTokens: avgPacketTokens,
|
|
2478
2511
|
quotaStateEntry,
|
|
2512
|
+
hostConcurrencyLimit,
|
|
2479
2513
|
});
|
|
2480
2514
|
const dispatchQuota = {
|
|
2481
2515
|
contract_version: "audit-code-dispatch-quota/v1alpha1",
|
|
@@ -2484,6 +2518,7 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2484
2518
|
resolved_limits: waveSchedule.resolved_limits,
|
|
2485
2519
|
confidence: waveSchedule.confidence,
|
|
2486
2520
|
source: waveSchedule.source,
|
|
2521
|
+
host_concurrency_limit: waveSchedule.host_concurrency_limit,
|
|
2487
2522
|
wave_size: waveSchedule.wave_size,
|
|
2488
2523
|
estimated_wave_tokens: waveSchedule.estimated_wave_tokens,
|
|
2489
2524
|
cooldown_until: waveSchedule.cooldown_until,
|
|
@@ -2518,6 +2553,7 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2518
2553
|
dispatch_quota_path: dispatchQuotaPath,
|
|
2519
2554
|
packet_count: plan.length,
|
|
2520
2555
|
task_count: orderedTasks.length,
|
|
2556
|
+
skipped_task_count: priorResultTaskIds.size,
|
|
2521
2557
|
largest_packet: largestPacketId
|
|
2522
2558
|
? {
|
|
2523
2559
|
packet_id: largestPacketId,
|
|
@@ -2538,6 +2574,7 @@ async function cmdPrepareDispatch(argv) {
|
|
|
2538
2574
|
artifactsDir: getArtifactsDir(argv),
|
|
2539
2575
|
root: getFlag(argv, "--root") ? getRootDir(argv) : undefined,
|
|
2540
2576
|
hostModel: getHostModel(argv),
|
|
2577
|
+
hostActiveSubagentLimit: getHostMaxActiveSubagents(argv),
|
|
2541
2578
|
});
|
|
2542
2579
|
console.log(JSON.stringify(result, null, 2));
|
|
2543
2580
|
}
|
|
@@ -3214,12 +3251,17 @@ async function cmdQuota(argv) {
|
|
|
3214
3251
|
const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
|
|
3215
3252
|
const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
|
|
3216
3253
|
const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ?? 24;
|
|
3254
|
+
const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
|
|
3255
|
+
explicitLimit: getHostMaxActiveSubagents(argv),
|
|
3256
|
+
sessionConfig,
|
|
3257
|
+
});
|
|
3217
3258
|
const waveSchedule = scheduleWave({
|
|
3218
3259
|
providerName,
|
|
3219
3260
|
sessionConfig,
|
|
3220
3261
|
hostModel,
|
|
3221
3262
|
requestedConcurrency: sessionConfig.parallel_workers ?? 1,
|
|
3222
3263
|
quotaStateEntry,
|
|
3264
|
+
hostConcurrencyLimit,
|
|
3223
3265
|
});
|
|
3224
3266
|
console.log(JSON.stringify({
|
|
3225
3267
|
provider: providerName,
|
|
@@ -3228,6 +3270,7 @@ async function cmdQuota(argv) {
|
|
|
3228
3270
|
resolved_limits: limits,
|
|
3229
3271
|
confidence,
|
|
3230
3272
|
source,
|
|
3273
|
+
host_concurrency_limit: hostConcurrencyLimit,
|
|
3231
3274
|
probe: probeResult,
|
|
3232
3275
|
learned_caps: quotaStateEntry
|
|
3233
3276
|
? {
|
package/dist/mcp/server.js
CHANGED
|
@@ -364,6 +364,10 @@ async function handleToolCall(name, params, defaults) {
|
|
|
364
364
|
if (canSelect !== undefined) {
|
|
365
365
|
extraArgs.push("--host-can-select-subagent-model", String(Boolean(canSelect)));
|
|
366
366
|
}
|
|
367
|
+
const maxActiveSubagents = params?.max_active_subagents ?? params?.maxActiveSubagents;
|
|
368
|
+
if (maxActiveSubagents !== undefined) {
|
|
369
|
+
extraArgs.push("--host-max-active-subagents", String(maxActiveSubagents));
|
|
370
|
+
}
|
|
367
371
|
return toolResult(await runContinueAudit(context, ["next-step", ...extraArgs]));
|
|
368
372
|
}
|
|
369
373
|
default:
|
|
@@ -522,6 +526,11 @@ function toolDefinitions() {
|
|
|
522
526
|
type: "boolean",
|
|
523
527
|
description: "Whether this host can select a model per subagent.",
|
|
524
528
|
},
|
|
529
|
+
max_active_subagents: {
|
|
530
|
+
type: "integer",
|
|
531
|
+
minimum: 1,
|
|
532
|
+
description: "Known hard cap on simultaneously active subagents for this host, if available.",
|
|
533
|
+
},
|
|
525
534
|
root: { type: "string", description: "Repository root override." },
|
|
526
535
|
artifacts_dir: {
|
|
527
536
|
type: "string",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SessionConfig } from "../types/sessionConfig.js";
|
|
2
|
+
import type { HostConcurrencyLimit } from "./types.js";
|
|
3
|
+
export declare function detectHostActiveSubagentLimit(env?: NodeJS.ProcessEnv): HostConcurrencyLimit | null;
|
|
4
|
+
export declare function resolveHostActiveSubagentLimit(options: {
|
|
5
|
+
explicitLimit?: number | null;
|
|
6
|
+
sessionConfig: SessionConfig;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
}): HostConcurrencyLimit | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const CODEX_DESKTOP_ACTIVE_SUBAGENT_LIMIT = 6;
|
|
2
|
+
function parsePositiveInteger(value) {
|
|
3
|
+
if (typeof value === "number") {
|
|
4
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
5
|
+
}
|
|
6
|
+
if (typeof value !== "string")
|
|
7
|
+
return null;
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!/^\d+$/.test(trimmed))
|
|
10
|
+
return null;
|
|
11
|
+
const parsed = Number(trimmed);
|
|
12
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
13
|
+
}
|
|
14
|
+
export function detectHostActiveSubagentLimit(env = process.env) {
|
|
15
|
+
const explicitEnvLimit = parsePositiveInteger(env.AUDIT_CODE_HOST_MAX_ACTIVE_SUBAGENTS ??
|
|
16
|
+
env.CODEX_MAX_ACTIVE_SUBAGENTS);
|
|
17
|
+
if (explicitEnvLimit !== null) {
|
|
18
|
+
return {
|
|
19
|
+
active_subagents: explicitEnvLimit,
|
|
20
|
+
source: "environment",
|
|
21
|
+
description: "Host active subagent limit from environment.",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop") {
|
|
25
|
+
return {
|
|
26
|
+
active_subagents: CODEX_DESKTOP_ACTIVE_SUBAGENT_LIMIT,
|
|
27
|
+
source: "environment",
|
|
28
|
+
description: "Codex Desktop active subagent limit.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export function resolveHostActiveSubagentLimit(options) {
|
|
34
|
+
if (options.explicitLimit !== undefined && options.explicitLimit !== null) {
|
|
35
|
+
return {
|
|
36
|
+
active_subagents: options.explicitLimit,
|
|
37
|
+
source: "cli_flags",
|
|
38
|
+
description: "Host active subagent limit reported by the conversation host.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const configuredLimit = parsePositiveInteger(options.sessionConfig.quota?.host_active_subagent_limit);
|
|
42
|
+
if (configuredLimit !== null) {
|
|
43
|
+
return {
|
|
44
|
+
active_subagents: configuredLimit,
|
|
45
|
+
source: "session_config",
|
|
46
|
+
description: "Host active subagent limit from session-config quota settings.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return detectHostActiveSubagentLimit(options.env);
|
|
50
|
+
}
|
package/dist/quota/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { resolveLimits, lookupKnownModel, classifyProvider } from "./limits.js";
|
|
2
2
|
export type { LimitResolutionResult, ResolveLimitsOptions, ProviderType } from "./limits.js";
|
|
3
|
+
export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "./hostLimits.js";
|
|
3
4
|
export { readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, } from "./state.js";
|
|
4
5
|
export { scheduleWave, buildProviderModelKey } from "./scheduler.js";
|
|
5
6
|
export type { ScheduleWaveOptions } from "./scheduler.js";
|
|
6
7
|
export { probeProvider } from "./probe.js";
|
|
7
8
|
export type { ProbeResult } from "./probe.js";
|
|
8
|
-
export type { ResolvedLimits, LimitSource, LimitConfidence, QuotaState, QuotaStateEntry, ConcurrencyBucket, WaveSchedule, DispatchQuota, ObservedWaveOutcome, } from "./types.js";
|
|
9
|
+
export type { ResolvedLimits, LimitSource, LimitConfidence, HostConcurrencyLimit, HostConcurrencyLimitSource, QuotaState, QuotaStateEntry, ConcurrencyBucket, WaveSchedule, DispatchQuota, ObservedWaveOutcome, } from "./types.js";
|
package/dist/quota/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { resolveLimits, lookupKnownModel, classifyProvider } from "./limits.js";
|
|
2
|
+
export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "./hostLimits.js";
|
|
2
3
|
export { readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, } from "./state.js";
|
|
3
4
|
export { scheduleWave, buildProviderModelKey } from "./scheduler.js";
|
|
4
5
|
export { probeProvider } from "./probe.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResolvedProviderName, SessionConfig } from "../types/sessionConfig.js";
|
|
2
|
-
import type { QuotaStateEntry, WaveSchedule } from "./types.js";
|
|
2
|
+
import type { HostConcurrencyLimit, QuotaStateEntry, WaveSchedule } from "./types.js";
|
|
3
3
|
export interface ScheduleWaveOptions {
|
|
4
4
|
providerName: ResolvedProviderName;
|
|
5
5
|
sessionConfig: SessionConfig;
|
|
@@ -8,6 +8,7 @@ export interface ScheduleWaveOptions {
|
|
|
8
8
|
/** Average estimated tokens per packet/worker. Used for TPM budget. */
|
|
9
9
|
estimatedPacketTokens?: number;
|
|
10
10
|
quotaStateEntry?: QuotaStateEntry | null;
|
|
11
|
+
hostConcurrencyLimit?: HostConcurrencyLimit | null;
|
|
11
12
|
}
|
|
12
13
|
export declare function scheduleWave(options: ScheduleWaveOptions): WaveSchedule;
|
|
13
14
|
/** Build the state key used for indexing quota-state.json entries. */
|
package/dist/quota/scheduler.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { resolveLimits } from "./limits.js";
|
|
2
2
|
import { computeMaxSafeConcurrency } from "./state.js";
|
|
3
3
|
export function scheduleWave(options) {
|
|
4
|
-
const { providerName, sessionConfig, hostModel, requestedConcurrency, estimatedPacketTokens = 0, quotaStateEntry = null, } = options;
|
|
4
|
+
const { providerName, sessionConfig, hostModel, requestedConcurrency, estimatedPacketTokens = 0, quotaStateEntry = null, hostConcurrencyLimit = null, } = options;
|
|
5
5
|
const quota = sessionConfig.quota ?? {};
|
|
6
|
+
const applyHostConcurrencyLimit = (waveSize) => {
|
|
7
|
+
if (hostConcurrencyLimit === null)
|
|
8
|
+
return waveSize;
|
|
9
|
+
return Math.min(waveSize, hostConcurrencyLimit.active_subagents);
|
|
10
|
+
};
|
|
6
11
|
if (quota.enabled === false) {
|
|
12
|
+
const waveSize = Math.max(1, applyHostConcurrencyLimit(requestedConcurrency));
|
|
7
13
|
const limits = {
|
|
8
14
|
context_tokens: quota.default_context_tokens ?? 32_000,
|
|
9
15
|
output_tokens: quota.reserved_output_tokens ?? 4_096,
|
|
@@ -12,12 +18,13 @@ export function scheduleWave(options) {
|
|
|
12
18
|
output_tokens_per_minute: null,
|
|
13
19
|
};
|
|
14
20
|
return {
|
|
15
|
-
wave_size:
|
|
16
|
-
estimated_wave_tokens:
|
|
21
|
+
wave_size: waveSize,
|
|
22
|
+
estimated_wave_tokens: waveSize * estimatedPacketTokens,
|
|
17
23
|
cooldown_until: null,
|
|
18
24
|
confidence: "high",
|
|
19
25
|
source: "default",
|
|
20
26
|
resolved_limits: limits,
|
|
27
|
+
host_concurrency_limit: hostConcurrencyLimit,
|
|
21
28
|
model: hostModel,
|
|
22
29
|
};
|
|
23
30
|
}
|
|
@@ -51,6 +58,7 @@ export function scheduleWave(options) {
|
|
|
51
58
|
}
|
|
52
59
|
// No learned data: use requestedConcurrency and let 429 outcomes train the cap
|
|
53
60
|
}
|
|
61
|
+
waveSize = applyHostConcurrencyLimit(waveSize);
|
|
54
62
|
waveSize = Math.max(1, waveSize);
|
|
55
63
|
return {
|
|
56
64
|
wave_size: waveSize,
|
|
@@ -59,6 +67,7 @@ export function scheduleWave(options) {
|
|
|
59
67
|
confidence,
|
|
60
68
|
source,
|
|
61
69
|
resolved_limits: limits,
|
|
70
|
+
host_concurrency_limit: hostConcurrencyLimit,
|
|
62
71
|
model: hostModel,
|
|
63
72
|
};
|
|
64
73
|
}
|
package/dist/quota/types.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type LimitSource = "explicit_config" | "cli_flags" | "known_metadata" | "learned" | "default";
|
|
2
2
|
export type LimitConfidence = "high" | "medium" | "low";
|
|
3
|
+
export type HostConcurrencyLimitSource = "cli_flags" | "session_config" | "environment";
|
|
4
|
+
export interface HostConcurrencyLimit {
|
|
5
|
+
active_subagents: number;
|
|
6
|
+
source: HostConcurrencyLimitSource;
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
3
9
|
export interface ResolvedLimits {
|
|
4
10
|
context_tokens: number;
|
|
5
11
|
output_tokens: number;
|
|
@@ -28,6 +34,7 @@ export interface WaveSchedule {
|
|
|
28
34
|
confidence: LimitConfidence;
|
|
29
35
|
source: LimitSource;
|
|
30
36
|
resolved_limits: ResolvedLimits;
|
|
37
|
+
host_concurrency_limit: HostConcurrencyLimit | null;
|
|
31
38
|
model: string | null;
|
|
32
39
|
}
|
|
33
40
|
export interface DispatchQuota {
|
|
@@ -37,6 +44,7 @@ export interface DispatchQuota {
|
|
|
37
44
|
resolved_limits: ResolvedLimits;
|
|
38
45
|
confidence: LimitConfidence;
|
|
39
46
|
source: LimitSource;
|
|
47
|
+
host_concurrency_limit: HostConcurrencyLimit | null;
|
|
40
48
|
wave_size: number;
|
|
41
49
|
estimated_wave_tokens: number;
|
|
42
50
|
cooldown_until: string | null;
|
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
function normalizeText(value) {
|
|
2
2
|
return (value ?? "").trim().toLowerCase();
|
|
3
3
|
}
|
|
4
|
+
function wordSet(text) {
|
|
5
|
+
return new Set(text
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
8
|
+
.split(/\s+/)
|
|
9
|
+
.filter(Boolean));
|
|
10
|
+
}
|
|
11
|
+
function wordJaccard(a, b) {
|
|
12
|
+
const sa = wordSet(a);
|
|
13
|
+
const sb = wordSet(b);
|
|
14
|
+
let intersection = 0;
|
|
15
|
+
for (const w of sa) {
|
|
16
|
+
if (sb.has(w))
|
|
17
|
+
intersection++;
|
|
18
|
+
}
|
|
19
|
+
const union = sa.size + sb.size - intersection;
|
|
20
|
+
return union === 0 ? 0 : intersection / union;
|
|
21
|
+
}
|
|
22
|
+
function filePathOverlap(a, b) {
|
|
23
|
+
const setA = new Set(a.affected_files.map((f) => f.path));
|
|
24
|
+
const setB = new Set(b.affected_files.map((f) => f.path));
|
|
25
|
+
let intersection = 0;
|
|
26
|
+
for (const path of setA) {
|
|
27
|
+
if (setB.has(path))
|
|
28
|
+
intersection++;
|
|
29
|
+
}
|
|
30
|
+
const union = setA.size + setB.size - intersection;
|
|
31
|
+
return union === 0 ? 0 : intersection / union;
|
|
32
|
+
}
|
|
4
33
|
function primaryPath(finding) {
|
|
5
34
|
return finding.affected_files[0]?.path ?? "";
|
|
6
35
|
}
|
|
@@ -63,6 +92,62 @@ function mergeAffectedFiles(existing, incoming) {
|
|
|
63
92
|
}
|
|
64
93
|
existing.affected_files.sort((a, b) => a.path.localeCompare(b.path) || (a.line_start ?? 0) - (b.line_start ?? 0));
|
|
65
94
|
}
|
|
95
|
+
function deduplicateCrossLens(findings) {
|
|
96
|
+
const groups = new Map();
|
|
97
|
+
for (const finding of findings) {
|
|
98
|
+
const key = primaryPath(finding);
|
|
99
|
+
const group = groups.get(key);
|
|
100
|
+
if (group) {
|
|
101
|
+
group.push(finding);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
groups.set(key, [finding]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const removed = new Set();
|
|
108
|
+
for (const group of groups.values()) {
|
|
109
|
+
if (group.length < 2)
|
|
110
|
+
continue;
|
|
111
|
+
for (let i = 0; i < group.length; i++) {
|
|
112
|
+
if (removed.has(group[i]))
|
|
113
|
+
continue;
|
|
114
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
115
|
+
if (removed.has(group[j]))
|
|
116
|
+
continue;
|
|
117
|
+
const a = group[i];
|
|
118
|
+
const b = group[j];
|
|
119
|
+
if (normalizeText(a.lens) === normalizeText(b.lens))
|
|
120
|
+
continue;
|
|
121
|
+
const titleSim = wordJaccard(a.title, b.title);
|
|
122
|
+
const catMatch = normalizeText(a.category) === normalizeText(b.category);
|
|
123
|
+
const threshold = catMatch ? 0.4 : 0.5;
|
|
124
|
+
if (titleSim < threshold)
|
|
125
|
+
continue;
|
|
126
|
+
if (filePathOverlap(a, b) < 0.5)
|
|
127
|
+
continue;
|
|
128
|
+
const aSev = severityRank(a.severity);
|
|
129
|
+
const bSev = severityRank(b.severity);
|
|
130
|
+
const aConf = confidenceRank(a.confidence);
|
|
131
|
+
const bConf = confidenceRank(b.confidence);
|
|
132
|
+
const keepA = aSev > bSev || (aSev === bSev && aConf >= bConf);
|
|
133
|
+
const [survivor, absorbed] = keepA ? [a, b] : [b, a];
|
|
134
|
+
mergeAffectedFiles(survivor, absorbed);
|
|
135
|
+
survivor.evidence = [
|
|
136
|
+
...new Set([
|
|
137
|
+
...(survivor.evidence ?? []),
|
|
138
|
+
...(absorbed.evidence ?? []),
|
|
139
|
+
]),
|
|
140
|
+
];
|
|
141
|
+
survivor.systemic = Boolean(survivor.systemic || absorbed.systemic);
|
|
142
|
+
if (absorbed.summary.length > survivor.summary.length) {
|
|
143
|
+
survivor.summary = absorbed.summary;
|
|
144
|
+
}
|
|
145
|
+
removed.add(absorbed);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return findings.filter((f) => !removed.has(f));
|
|
150
|
+
}
|
|
66
151
|
export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
67
152
|
const merged = new Map();
|
|
68
153
|
const runtimeEvidence = runtimeSummary(runtimeReport);
|
|
@@ -109,7 +194,7 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
109
194
|
];
|
|
110
195
|
}
|
|
111
196
|
}
|
|
112
|
-
return [...merged.values()].sort((a, b) => {
|
|
197
|
+
return deduplicateCrossLens([...merged.values()]).sort((a, b) => {
|
|
113
198
|
const severityDelta = severityRank(b.severity) - severityRank(a.severity);
|
|
114
199
|
if (severityDelta !== 0)
|
|
115
200
|
return severityDelta;
|
|
@@ -44,6 +44,8 @@ export interface QuotaConfig {
|
|
|
44
44
|
reserved_output_tokens?: number;
|
|
45
45
|
/** Half-life of empirical success/failure evidence in hours (default: 24). */
|
|
46
46
|
empirical_half_life_hours?: number;
|
|
47
|
+
/** Hard host ceiling for simultaneously active conversation subagents. */
|
|
48
|
+
host_active_subagent_limit?: number;
|
|
47
49
|
/** Per-model overrides keyed by "provider/model". */
|
|
48
50
|
models?: Record<string, QuotaModelLimits>;
|
|
49
51
|
}
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"resolved_limits",
|
|
12
12
|
"confidence",
|
|
13
13
|
"source",
|
|
14
|
+
"host_concurrency_limit",
|
|
14
15
|
"wave_size",
|
|
15
16
|
"estimated_wave_tokens",
|
|
16
17
|
"cooldown_until"
|
|
@@ -58,6 +59,30 @@
|
|
|
58
59
|
"enum": ["explicit_config", "cli_flags", "known_metadata", "learned", "default"],
|
|
59
60
|
"description": "Where the resolved limits came from."
|
|
60
61
|
},
|
|
62
|
+
"host_concurrency_limit": {
|
|
63
|
+
"type": ["object", "null"],
|
|
64
|
+
"description": "A hard host ceiling on simultaneously active subagents, if detected or reported.",
|
|
65
|
+
"required": [
|
|
66
|
+
"active_subagents",
|
|
67
|
+
"source",
|
|
68
|
+
"description"
|
|
69
|
+
],
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"properties": {
|
|
72
|
+
"active_subagents": {
|
|
73
|
+
"type": "integer",
|
|
74
|
+
"minimum": 1
|
|
75
|
+
},
|
|
76
|
+
"source": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": ["cli_flags", "session_config", "environment"]
|
|
79
|
+
},
|
|
80
|
+
"description": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"minLength": 1
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
61
86
|
"wave_size": {
|
|
62
87
|
"type": "integer",
|
|
63
88
|
"minimum": 1,
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -283,9 +283,7 @@ const OPENCODE_MCP_COMMAND_TEMPLATE = [
|
|
|
283
283
|
'Use the auditor MCP tools as the primary interface to the audit workflow.',
|
|
284
284
|
'',
|
|
285
285
|
'1. Call `auditor_start_audit` to initialize and receive the first step.',
|
|
286
|
-
'2.
|
|
287
|
-
' - If `step_kind` is `"capability_check"`: immediately call `auditor_report_capability` with `can_dispatch_subagents: true` and `can_select_subagent_model: true`. Do not run shell commands or inspect prompt_content for this step.',
|
|
288
|
-
' - Otherwise: read `prompt_content` and follow it.',
|
|
286
|
+
'2. Read `prompt_content` in the response and follow it.',
|
|
289
287
|
'3. When a step completes (not blocked), call `auditor_continue_audit` to advance.',
|
|
290
288
|
'4. Stop when the step instructions say to stop.',
|
|
291
289
|
'',
|