auditor-lambda 0.3.27 → 0.3.29

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.
@@ -996,6 +996,21 @@ function renderAntigravityPlanningGuide(root) {
996
996
  ].join('\n');
997
997
  }
998
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
+
999
1014
  function renderSharedMcpLauncher(sourcePackageRoot) {
1000
1015
  return [
1001
1016
  "import { access, readFile } from 'node:fs/promises';",
@@ -1428,19 +1443,22 @@ const INSTALL_HOST_DEFINITIONS = {
1428
1443
  antigravity: {
1429
1444
  host: 'antigravity',
1430
1445
  label: 'Antigravity',
1431
- support_level: 'guided',
1432
- setup_kind: 'planning-guide+mcp-ready',
1446
+ support_level: 'supported',
1447
+ setup_kind: 'agent-skill+gemini-command+planning-guide+mcp-ready',
1433
1448
  summary:
1434
- 'Start in Planning mode with the generated guide and AGENTS instructions, then use the shared MCP launcher once Antigravity is ready to call structured tools.',
1435
- primary_path_key: 'antigravityPlanningGuidePath',
1449
+ 'Uses the project-scoped .agent/skills/audit-code/SKILL.md skill, the .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: 'antigravitySkillPath',
1436
1451
  supporting_path_keys: [
1452
+ 'geminiCommandPath',
1453
+ 'antigravityPlanningGuidePath',
1437
1454
  'agentsInstructionsPath',
1438
1455
  'mcpLauncherPath',
1439
1456
  'installedPromptPath',
1440
1457
  ],
1441
1458
  steps: [
1442
- 'Open this repository in Antigravity Planning mode.',
1443
- 'Load the generated planning guide and AGENTS instructions before starting the audit.',
1459
+ 'Open this repository in Antigravity.',
1460
+ 'The audit-code skill is automatically discovered from .agent/skills/audit-code/SKILL.md.',
1461
+ 'The /audit-code slash command is also available from .gemini/commands/audit-code.toml.',
1444
1462
  'Use the shared auditor MCP server when Antigravity needs structured audit state instead of free-form shell guesses.',
1445
1463
  ],
1446
1464
  profile: {
@@ -2154,6 +2172,16 @@ async function verifyInstalledBootstrap(argv) {
2154
2172
  });
2155
2173
  break;
2156
2174
  case 'antigravity':
2175
+ await collectVerifyCheck(checks, 'antigravity_skill', async () => {
2176
+ const content = await readFile(assetPaths.antigravitySkillPath, 'utf8');
2177
+ if (!content.includes('name: audit-code')) {
2178
+ throw new Error('Antigravity skill SKILL.md must contain "name: audit-code" in frontmatter.');
2179
+ }
2180
+ return {
2181
+ summary: 'Antigravity .agent/skills/audit-code/SKILL.md is present and valid.',
2182
+ path: assetPaths.antigravitySkillPath,
2183
+ };
2184
+ });
2157
2185
  await collectVerifyCheck(checks, 'antigravity_guide', async () => {
2158
2186
  const content = await readFile(assetPaths.antigravityPlanningGuidePath, 'utf8');
2159
2187
  if (!content.includes(MCP_LAUNCHER_FILENAME) || !content.includes(INSTALLED_PROMPT_FILENAME)) {
@@ -2321,6 +2349,17 @@ async function detectBootstrapRefreshReason(root, host) {
2321
2349
  }
2322
2350
  break;
2323
2351
  }
2352
+ case 'antigravity': {
2353
+ const expectedSkillPath = join(root, '.agent', 'skills', 'audit-code', 'SKILL.md');
2354
+ if (!(await fileExists(expectedSkillPath))) {
2355
+ return 'missing_host_asset:antigravity:skill';
2356
+ }
2357
+ const antigravitySkill = await readTextIfExists(expectedSkillPath);
2358
+ if (antigravitySkill !== null && antigravitySkill.replace(/\r\n/g, '\n') !== sourceSkill) {
2359
+ return 'stale_host_asset:antigravity:skill';
2360
+ }
2361
+ break;
2362
+ }
2324
2363
  default:
2325
2364
  break;
2326
2365
  }
@@ -2451,6 +2490,12 @@ async function installBootstrap(argv, options = {}) {
2451
2490
  antigravityPlanningGuidePath: profile.writeAntigravity
2452
2491
  ? join(root, '.audit-code', 'install', 'antigravity', 'PLANNING-MODE.md')
2453
2492
  : null,
2493
+ geminiCommandPath: profile.writeAntigravity
2494
+ ? join(root, '.gemini', 'commands', 'audit-code.toml')
2495
+ : null,
2496
+ antigravitySkillPath: profile.writeAntigravity
2497
+ ? join(root, '.agent', 'skills', 'audit-code', 'SKILL.md')
2498
+ : null,
2454
2499
  };
2455
2500
 
2456
2501
  const results = [];
@@ -2578,6 +2623,18 @@ async function installBootstrap(argv, options = {}) {
2578
2623
  renderAntigravityPlanningGuide(root),
2579
2624
  ),
2580
2625
  );
2626
+ results.push(
2627
+ await writeGeneratedMarkdown(
2628
+ assetPaths.geminiCommandPath,
2629
+ renderGeminiCommandToml(promptBody),
2630
+ ),
2631
+ );
2632
+ results.push(
2633
+ await writeGeneratedMarkdown(
2634
+ assetPaths.antigravitySkillPath,
2635
+ skillSource,
2636
+ ),
2637
+ );
2581
2638
  }
2582
2639
 
2583
2640
  const hostGuidance = buildHostCatalog({
@@ -2639,6 +2696,8 @@ async function installBootstrap(argv, options = {}) {
2639
2696
  slash_command_surfaces: {
2640
2697
  vscode_prompt: assetPaths.vscodePromptPath,
2641
2698
  opencode_config: assetPaths.opencodeConfigPath,
2699
+ gemini_command: assetPaths.geminiCommandPath,
2700
+ antigravity_skill: assetPaths.antigravitySkillPath,
2642
2701
  },
2643
2702
  instruction_surfaces: {
2644
2703
  agents: assetPaths.agentsInstructionsPath,
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);
@@ -2488,6 +2498,10 @@ async function prepareDispatchArtifacts(params) {
2488
2498
  const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
2489
2499
  const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
2490
2500
  const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
2501
+ const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
2502
+ explicitLimit: params.hostActiveSubagentLimit,
2503
+ sessionConfig,
2504
+ });
2491
2505
  const waveSchedule = scheduleWave({
2492
2506
  providerName: quotaProviderName,
2493
2507
  sessionConfig,
@@ -2495,6 +2509,7 @@ async function prepareDispatchArtifacts(params) {
2495
2509
  requestedConcurrency: sessionConfig.parallel_workers ?? plan.length,
2496
2510
  estimatedPacketTokens: avgPacketTokens,
2497
2511
  quotaStateEntry,
2512
+ hostConcurrencyLimit,
2498
2513
  });
2499
2514
  const dispatchQuota = {
2500
2515
  contract_version: "audit-code-dispatch-quota/v1alpha1",
@@ -2503,6 +2518,7 @@ async function prepareDispatchArtifacts(params) {
2503
2518
  resolved_limits: waveSchedule.resolved_limits,
2504
2519
  confidence: waveSchedule.confidence,
2505
2520
  source: waveSchedule.source,
2521
+ host_concurrency_limit: waveSchedule.host_concurrency_limit,
2506
2522
  wave_size: waveSchedule.wave_size,
2507
2523
  estimated_wave_tokens: waveSchedule.estimated_wave_tokens,
2508
2524
  cooldown_until: waveSchedule.cooldown_until,
@@ -2558,6 +2574,7 @@ async function cmdPrepareDispatch(argv) {
2558
2574
  artifactsDir: getArtifactsDir(argv),
2559
2575
  root: getFlag(argv, "--root") ? getRootDir(argv) : undefined,
2560
2576
  hostModel: getHostModel(argv),
2577
+ hostActiveSubagentLimit: getHostMaxActiveSubagents(argv),
2561
2578
  });
2562
2579
  console.log(JSON.stringify(result, null, 2));
2563
2580
  }
@@ -3234,12 +3251,17 @@ async function cmdQuota(argv) {
3234
3251
  const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
3235
3252
  const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
3236
3253
  const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ?? 24;
3254
+ const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
3255
+ explicitLimit: getHostMaxActiveSubagents(argv),
3256
+ sessionConfig,
3257
+ });
3237
3258
  const waveSchedule = scheduleWave({
3238
3259
  providerName,
3239
3260
  sessionConfig,
3240
3261
  hostModel,
3241
3262
  requestedConcurrency: sessionConfig.parallel_workers ?? 1,
3242
3263
  quotaStateEntry,
3264
+ hostConcurrencyLimit,
3243
3265
  });
3244
3266
  console.log(JSON.stringify({
3245
3267
  provider: providerName,
@@ -3248,6 +3270,7 @@ async function cmdQuota(argv) {
3248
3270
  resolved_limits: limits,
3249
3271
  confidence,
3250
3272
  source,
3273
+ host_concurrency_limit: hostConcurrencyLimit,
3251
3274
  probe: probeResult,
3252
3275
  learned_caps: quotaStateEntry
3253
3276
  ? {
@@ -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
+ }
@@ -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";
@@ -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. */
@@ -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: requestedConcurrency,
16
- estimated_wave_tokens: requestedConcurrency * estimatedPacketTokens,
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -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,