auditor-lambda 0.3.24 → 0.3.26

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.
@@ -542,7 +542,7 @@ const OPENCODE_AUDIT_EDIT_PERMISSION = {
542
542
  };
543
543
 
544
544
  const OPENCODE_AUDIT_BASH_PERMISSION = {
545
- '*': 'ask',
545
+ '*': 'allow',
546
546
  'audit-code run-to-completion*': 'deny',
547
547
  'audit-code synthesize*': 'deny',
548
548
  'audit-code cleanup*': 'deny',
@@ -588,10 +588,7 @@ function externalDirectoryPattern(path) {
588
588
  }
589
589
 
590
590
  function renderOpenCodeExternalDirectoryPermission() {
591
- return {
592
- [externalDirectoryPattern(repoRoot)]: 'allow',
593
- [externalDirectoryPattern(dirname(process.execPath))]: 'allow',
594
- };
591
+ return { '*': 'allow' };
595
592
  }
596
593
 
597
594
  function renderOpenCodePermissionConfig() {
@@ -611,15 +608,19 @@ const OPENCODE_MCP_COMMAND_TEMPLATE = [
611
608
  'Use the auditor MCP tools as the primary interface to the audit workflow.',
612
609
  '',
613
610
  '1. Call `auditor_start_audit` to initialize and receive the first step.',
614
- '2. Read the `prompt_content` field in the response and follow the instructions there.',
611
+ '2. Check `step_kind` in the response:',
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.',
615
614
  '3. When a step completes (not blocked), call `auditor_continue_audit` to advance.',
616
615
  '4. Stop when the step instructions say to stop.',
617
616
  '',
618
617
  'Do not run shell commands. Use only `auditor_*` MCP tools and the `task` tool for subagent dispatch.',
618
+ '',
619
+ 'If `auditor_start_audit` is not listed in your available tools, stop immediately and tell the user the auditor MCP server is not connected. Do not read local files as a fallback.',
619
620
  ].join('\n');
620
621
 
621
- function renderOpenCodeProjectConfig(root) {
622
- const launcher = replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)));
622
+ function renderOpenCodeProjectConfig(_root) {
623
+ const launcher = `.audit-code/install/${MCP_LAUNCHER_FILENAME}`;
623
624
  const auditPermission = renderOpenCodePermissionConfig();
624
625
  return {
625
626
  $schema: 'https://opencode.ai/config.json',
@@ -636,12 +637,9 @@ function renderOpenCodeProjectConfig(root) {
636
637
  auditor: {
637
638
  description:
638
639
  'Read-heavy audit orchestration agent for the /audit-code workflow.',
639
- tools: {
640
- 'auditor*': true,
641
- task: true,
642
- },
643
640
  permission: {
644
641
  ...auditPermission,
642
+ 'auditor_*': 'allow',
645
643
  question: 'allow',
646
644
  task: 'allow',
647
645
  },
@@ -742,12 +740,10 @@ function assertOpenCodeAuditPermissionConfig(permissionConfig, label) {
742
740
  }
743
741
  const externalDirectory = permissionConfig?.external_directory;
744
742
  if (!externalDirectory || typeof externalDirectory !== 'object' || Array.isArray(externalDirectory)) {
745
- throw new Error(`OpenCode ${label}.external_directory must allow audit package paths. Run "audit-code install --host opencode".`);
743
+ throw new Error(`OpenCode ${label}.external_directory must set "*" to "allow". Run "audit-code install --host opencode".`);
746
744
  }
747
- for (const pattern of Object.keys(renderOpenCodeExternalDirectoryPermission())) {
748
- if (externalDirectory[pattern] !== 'allow') {
749
- throw new Error(`OpenCode ${label}.external_directory must allow ${pattern}. Run "audit-code install --host opencode".`);
750
- }
745
+ if (externalDirectory['*'] !== 'allow') {
746
+ throw new Error(`OpenCode ${label}.external_directory must set "*" to "allow". Run "audit-code install --host opencode".`);
751
747
  }
752
748
  const edit = permissionConfig?.edit;
753
749
  const bash = permissionConfig?.bash;
@@ -815,16 +811,22 @@ function buildMergedOpenCodeProjectConfig(existing, root) {
815
811
  ...objectValue(existing.mcp),
816
812
  auditor: generated.mcp.auditor,
817
813
  },
818
- permission: mergeOpenCodePermissionConfig(existing.permission, generated.permission),
814
+ permission: {
815
+ ...mergeOpenCodePermissionConfig(existing.permission, generated.permission),
816
+ external_directory: { '*': 'allow' },
817
+ },
819
818
  agent: {
820
819
  ...objectValue(existing.agent),
821
820
  auditor: {
822
821
  ...objectValue(objectValue(existing.agent).auditor),
823
822
  ...generated.agent.auditor,
824
- permission: mergeOpenCodePermissionConfig(
825
- objectValue(objectValue(existing.agent).auditor).permission,
826
- generated.agent.auditor.permission,
827
- ),
823
+ permission: {
824
+ ...mergeOpenCodePermissionConfig(
825
+ objectValue(objectValue(existing.agent).auditor).permission,
826
+ generated.agent.auditor.permission,
827
+ ),
828
+ external_directory: { '*': 'allow' },
829
+ },
828
830
  },
829
831
  },
830
832
  };
@@ -2007,8 +2009,8 @@ async function verifyInstalledBootstrap(argv) {
2007
2009
  if (!Array.isArray(mcpCommand) || mcpCommand[0] !== 'node') {
2008
2010
  throw new Error('OpenCode config must set mcp.auditor.command as a Node command array.');
2009
2011
  }
2010
- if (mcpCommand[1] !== '.audit-code/install/run-mcp-server.mjs') {
2011
- throw new Error(`OpenCode config must point at .audit-code/install/${MCP_LAUNCHER_FILENAME}, got ${mcpCommand[1] ?? 'missing'}.`);
2012
+ if (!mcpCommand[1]?.includes(MCP_LAUNCHER_FILENAME)) {
2013
+ throw new Error(`OpenCode config must reference ${MCP_LAUNCHER_FILENAME}, got ${mcpCommand[1] ?? 'missing'}.`);
2012
2014
  }
2013
2015
  if (config?.mcp?.auditor?.type !== 'local') {
2014
2016
  throw new Error(`OpenCode config must set mcp.auditor.type to "local", got ${config?.mcp?.auditor?.type ?? 'missing'}.`);
package/dist/cli.d.ts CHANGED
@@ -4,6 +4,7 @@ declare function getFlag(argv: string[], name: string, fallback?: string): strin
4
4
  declare function hasFlag(argv: string[], name: string): boolean;
5
5
  declare function getArtifactsDir(argv: string[]): string;
6
6
  declare function getRootDir(argv: string[]): string;
7
+ declare function warnIfNotGitRepo(root: string): void;
7
8
  declare function getBatchResultsDir(argv: string[]): string | undefined;
8
9
  declare function getMaxRuns(argv: string[]): number;
9
10
  declare function getAgentBatchSize(argv: string[], sessionConfig: SessionConfig): number;
@@ -36,6 +37,7 @@ export declare const cliTestUtils: {
36
37
  getUiMode: typeof getUiMode;
37
38
  looksLikeCliFlag: typeof looksLikeCliFlag;
38
39
  countLines: typeof countLines;
40
+ warnIfNotGitRepo: typeof warnIfNotGitRepo;
39
41
  };
40
42
  export declare function runSample(argv?: string[]): Promise<void>;
41
43
  export declare function runCli(argv: string[]): Promise<void>;
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
- import { createReadStream } from "node:fs";
2
+ import { createReadStream, existsSync } from "node:fs";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { createHash } from "node:crypto";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -23,7 +23,7 @@ import { deriveAuditState } from "./orchestrator/state.js";
23
23
  import { advanceAudit } from "./orchestrator/advance.js";
24
24
  import { decideNextStep } from "./orchestrator/nextStep.js";
25
25
  import { createFreshSessionProvider, resolveFreshSessionProviderName, } from "./providers/index.js";
26
- import { appendRunLedgerEntry } from "./supervisor/runLedger.js";
26
+ import { appendRunLedgerEntry, loadRunLedger } from "./supervisor/runLedger.js";
27
27
  import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./supervisor/operatorHandoff.js";
28
28
  import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
29
29
  import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
@@ -154,6 +154,12 @@ function getArtifactsDir(argv) {
154
154
  function getRootDir(argv) {
155
155
  return resolveFlagPath(argv, "--root", DIRECT_CLI_DEFAULTS.rootDir);
156
156
  }
157
+ function warnIfNotGitRepo(root) {
158
+ const gitEntry = join(root, ".git");
159
+ if (!existsSync(gitEntry)) {
160
+ console.warn(`Warning: target directory '${root}' does not appear to be a git repository. Diff-based signals will be unavailable.`);
161
+ }
162
+ }
157
163
  function getBatchResultsDir(argv) {
158
164
  const value = getFlag(argv, "--batch-results");
159
165
  return value ? resolve(value) : undefined;
@@ -725,6 +731,7 @@ export const cliTestUtils = {
725
731
  getUiMode,
726
732
  looksLikeCliFlag,
727
733
  countLines,
734
+ warnIfNotGitRepo,
728
735
  };
729
736
  async function maybeArchiveLegacyPendingResults(auditResultsPath) {
730
737
  if (!auditResultsPath || basename(auditResultsPath) !== "worker_results_pending.json") {
@@ -954,6 +961,7 @@ export async function runSample(argv = process.argv) {
954
961
  }
955
962
  async function cmdAdvanceAudit(argv) {
956
963
  const root = getRootDir(argv);
964
+ warnIfNotGitRepo(root);
957
965
  const artifactsDir = getArtifactsDir(argv);
958
966
  await cleanupStaleArtifactsDir(artifactsDir);
959
967
  await mkdir(artifactsDir, { recursive: true });
@@ -1118,6 +1126,7 @@ async function runDeterministicForNextStep(params) {
1118
1126
  }
1119
1127
  async function cmdNextStep(argv) {
1120
1128
  const root = getRootDir(argv);
1129
+ warnIfNotGitRepo(root);
1121
1130
  const artifactsDir = getArtifactsDir(argv);
1122
1131
  await mkdir(artifactsDir, { recursive: true });
1123
1132
  await ensureSupervisorDirs(artifactsDir);
@@ -1288,6 +1297,7 @@ async function cmdNextStep(argv) {
1288
1297
  }
1289
1298
  async function cmdRunToCompletion(argv) {
1290
1299
  const root = getRootDir(argv);
1300
+ warnIfNotGitRepo(root);
1291
1301
  const artifactsDir = getArtifactsDir(argv);
1292
1302
  await cleanupStaleArtifactsDir(artifactsDir);
1293
1303
  await mkdir(artifactsDir, { recursive: true });
@@ -2205,7 +2215,6 @@ async function prepareDispatchArtifacts(params) {
2205
2215
  const runId = params.runId;
2206
2216
  const artifactsDir = params.artifactsDir;
2207
2217
  const runDir = join(artifactsDir, "runs", runId);
2208
- const tasksPath = join(runDir, "pending-audit-tasks.json");
2209
2218
  const taskResultsDir = join(runDir, "task-results");
2210
2219
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
2211
2220
  let reviewRoot = params.root;
@@ -2218,8 +2227,13 @@ async function prepareDispatchArtifacts(params) {
2218
2227
  throw error;
2219
2228
  }
2220
2229
  }
2221
- const tasks = await readJsonFile(tasksPath);
2222
2230
  const bundle = await loadArtifactBundle(artifactsDir);
2231
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
2232
+ const tasks = await readJsonFile(tasksPath).catch((error) => {
2233
+ if (isFileMissingError(error))
2234
+ return buildPendingAuditTasks(bundle);
2235
+ throw error;
2236
+ });
2223
2237
  const sessionConfig = params.sessionConfig ?? (await loadSessionConfig(artifactsDir).catch(() => ({})));
2224
2238
  const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
2225
2239
  const lensDefs = await readJsonFile(lensDefsPath);
@@ -2737,6 +2751,8 @@ async function cmdMergeAndIngest(argv) {
2737
2751
  preferredExecutor: "result_ingestion_executor",
2738
2752
  auditResultsPath,
2739
2753
  });
2754
+ const updatedPendingTasks = await addFileLineCountHints(workerTask.repo_root, buildPendingAuditTasks(result.updated_bundle));
2755
+ await writeJsonFile(tasksPath, updatedPendingTasks);
2740
2756
  const workerResult = buildWorkerResult({
2741
2757
  runId,
2742
2758
  obligationId: workerTask.obligation_id,
@@ -2838,9 +2854,11 @@ async function cmdImportExternalAnalyzer(argv) {
2838
2854
  }, null, 2));
2839
2855
  }
2840
2856
  async function cmdIntake(argv) {
2857
+ const root = getRootDir(argv);
2858
+ warnIfNotGitRepo(root);
2841
2859
  const artifactsDir = getArtifactsDir(argv);
2842
2860
  const result = await runAuditStep({
2843
- root: getRootDir(argv),
2861
+ root,
2844
2862
  artifactsDir,
2845
2863
  preferredExecutor: "intake_executor",
2846
2864
  });
@@ -3072,6 +3090,114 @@ async function cmdCleanup(argv) {
3072
3090
  dry_run: dryRun,
3073
3091
  }, null, 2));
3074
3092
  }
3093
+ async function cmdStatus(argv) {
3094
+ const artifactsDir = getArtifactsDir(argv);
3095
+ const auditStatePath = join(artifactsDir, "audit_state.json");
3096
+ // 1. Read audit_state.json
3097
+ let auditState = null;
3098
+ try {
3099
+ auditState = await readJsonFile(auditStatePath);
3100
+ }
3101
+ catch (error) {
3102
+ if (!isFileMissingError(error)) {
3103
+ throw error;
3104
+ }
3105
+ }
3106
+ if (!auditState) {
3107
+ console.error("No audit_state.json found; no active audit in this artifacts directory.");
3108
+ process.exitCode = 1;
3109
+ return;
3110
+ }
3111
+ // Build obligations summary: count by state
3112
+ const obligationStates = {
3113
+ missing: 0,
3114
+ present: 0,
3115
+ stale: 0,
3116
+ blocked: 0,
3117
+ satisfied: 0,
3118
+ };
3119
+ for (const obligation of auditState.obligations ?? []) {
3120
+ const state = obligation.state;
3121
+ if (state in obligationStates) {
3122
+ obligationStates[state]++;
3123
+ }
3124
+ }
3125
+ // 2. Read run ledger for last N entries
3126
+ const ledger = await loadRunLedger(artifactsDir);
3127
+ const RECENT_RUN_LIMIT = 5;
3128
+ const recentRuns = ledger.runs
3129
+ .slice(-RECENT_RUN_LIMIT)
3130
+ .reverse()
3131
+ .map((entry) => ({
3132
+ run_id: entry.run_id,
3133
+ obligation_id: entry.obligation_id,
3134
+ status: entry.status,
3135
+ started_at: entry.started_at,
3136
+ }));
3137
+ // 3. Find the most recent run directory and read pending-audit-tasks.json
3138
+ let pendingTasksSummary = null;
3139
+ const runsDir = join(artifactsDir, "runs");
3140
+ let runDirs = [];
3141
+ try {
3142
+ const entries = await readdir(runsDir, { withFileTypes: true });
3143
+ runDirs = entries
3144
+ .filter((e) => e.isDirectory())
3145
+ .map((e) => e.name)
3146
+ .sort()
3147
+ .reverse();
3148
+ }
3149
+ catch {
3150
+ // runs directory may not exist yet
3151
+ }
3152
+ for (const runDirName of runDirs) {
3153
+ const runDir = join(runsDir, runDirName);
3154
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
3155
+ let tasks = null;
3156
+ try {
3157
+ tasks = await readJsonFile(tasksPath);
3158
+ }
3159
+ catch {
3160
+ continue; // no pending-audit-tasks.json in this run dir — try previous
3161
+ }
3162
+ if (!Array.isArray(tasks))
3163
+ continue;
3164
+ // Count remaining: tasks without status "complete"
3165
+ const total = tasks.length;
3166
+ const remaining = tasks.filter((t) => t.status !== "complete").length;
3167
+ pendingTasksSummary = {
3168
+ run_id: runDirName,
3169
+ total,
3170
+ remaining,
3171
+ };
3172
+ break;
3173
+ }
3174
+ // 4. Surface failed-tasks.json from the most recent run that has one
3175
+ let failedTasks = null;
3176
+ for (const runDirName of runDirs) {
3177
+ const failedTasksPath = join(runsDir, runDirName, "failed-tasks.json");
3178
+ try {
3179
+ const raw = await readJsonFile(failedTasksPath);
3180
+ if (Array.isArray(raw) && raw.length > 0) {
3181
+ failedTasks = raw;
3182
+ break;
3183
+ }
3184
+ }
3185
+ catch {
3186
+ // Not present in this run dir — keep looking
3187
+ }
3188
+ }
3189
+ console.log(JSON.stringify({
3190
+ artifacts_dir: artifactsDir,
3191
+ status: auditState.status,
3192
+ last_obligation: auditState.last_obligation ?? null,
3193
+ last_executor: auditState.last_executor ?? null,
3194
+ blockers: auditState.blockers ?? [],
3195
+ obligations_summary: obligationStates,
3196
+ recent_runs: recentRuns,
3197
+ pending_tasks: pendingTasksSummary,
3198
+ failed_tasks: failedTasks,
3199
+ }, null, 2));
3200
+ }
3075
3201
  async function cmdMcp(argv) {
3076
3202
  await runAuditCodeMcpServer(argv.slice(3));
3077
3203
  }
@@ -3183,9 +3309,12 @@ async function main(argv) {
3183
3309
  case "quota":
3184
3310
  await cmdQuota(argv);
3185
3311
  return;
3312
+ case "status":
3313
+ await cmdStatus(argv);
3314
+ return;
3186
3315
  default:
3187
3316
  console.error(`Unknown command: ${command}`);
3188
- console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota");
3317
+ console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status");
3189
3318
  process.exitCode = 1;
3190
3319
  }
3191
3320
  }
@@ -577,9 +577,15 @@ export async function runAuditCodeMcpServer(argv) {
577
577
  }
578
578
  try {
579
579
  switch (request.method) {
580
- case "initialize":
580
+ case "initialize": {
581
+ const requestedVersion = typeof request.params?.protocolVersion === "string"
582
+ ? request.params.protocolVersion
583
+ : PROTOCOL_VERSION;
584
+ const negotiatedVersion = requestedVersion <= PROTOCOL_VERSION
585
+ ? requestedVersion
586
+ : PROTOCOL_VERSION;
581
587
  writeMessage(success(request.id ?? null, {
582
- protocolVersion: PROTOCOL_VERSION,
588
+ protocolVersion: negotiatedVersion,
583
589
  serverInfo: {
584
590
  name: "audit-code",
585
591
  version,
@@ -592,6 +598,7 @@ export async function runAuditCodeMcpServer(argv) {
592
598
  },
593
599
  }));
594
600
  break;
601
+ }
595
602
  case "notifications/initialized":
596
603
  break;
597
604
  case "ping":
@@ -272,7 +272,6 @@ export function runResultIngestionExecutor(bundle, results) {
272
272
  const runtimeValidationReport = runtimeValidationTasks
273
273
  ? mergeRuntimeValidationReport(runtimeValidationTasks, bundle.runtime_validation_report)
274
274
  : bundle.runtime_validation_report;
275
- const requeuePayload = buildRequeuePayload(updatedCoverageMatrix, bundle.critical_flows, flowCoverage, bundle.external_analyzer_results);
276
275
  const mergedResults = [...(bundle.audit_results ?? []), ...results];
277
276
  const completedAuditTasks = updateAuditTaskStatuses(bundle.audit_tasks, mergedResults);
278
277
  const baseUpdatedBundle = {
@@ -283,7 +282,6 @@ export function runResultIngestionExecutor(bundle, results) {
283
282
  runtime_validation_report: runtimeValidationReport,
284
283
  audit_results: mergedResults,
285
284
  audit_tasks: completedAuditTasks,
286
- requeue_tasks: requeuePayload.tasks,
287
285
  audit_report: undefined,
288
286
  };
289
287
  const selectiveDeepening = appendSelectiveDeepeningTasks({
@@ -291,8 +289,13 @@ export function runResultIngestionExecutor(bundle, results) {
291
289
  results: mergedResults,
292
290
  runtimeValidationReport,
293
291
  });
292
+ const requeuePayload = buildRequeuePayload(updatedCoverageMatrix, selectiveDeepening.bundle.critical_flows, selectiveDeepening.bundle.flow_coverage, selectiveDeepening.bundle.external_analyzer_results);
293
+ const finalBundle = {
294
+ ...selectiveDeepening.bundle,
295
+ requeue_tasks: requeuePayload.tasks,
296
+ };
294
297
  return {
295
- updated: selectiveDeepening.bundle,
298
+ updated: finalBundle,
296
299
  artifacts_written: [
297
300
  "coverage_matrix.json",
298
301
  "flow_coverage.json",
@@ -13,8 +13,8 @@ const KNOWN_MODEL_LIMITS = {
13
13
  export function classifyProvider(providerName) {
14
14
  switch (providerName) {
15
15
  case "claude-code":
16
- case "opencode":
17
16
  return "hosted";
17
+ case "opencode":
18
18
  case "local-subprocess":
19
19
  return "local";
20
20
  case "subprocess-template":
@@ -1,4 +1,4 @@
1
- import { classifyProvider, resolveLimits } from "./limits.js";
1
+ import { resolveLimits } from "./limits.js";
2
2
  import { computeMaxSafeConcurrency } from "./state.js";
3
3
  export function scheduleWave(options) {
4
4
  const { providerName, sessionConfig, hostModel, requestedConcurrency, estimatedPacketTokens = 0, quotaStateEntry = null, } = options;
@@ -23,7 +23,6 @@ export function scheduleWave(options) {
23
23
  }
24
24
  const safetyMargin = quota.safety_margin ?? 0.8;
25
25
  const halfLifeHours = quota.empirical_half_life_hours ?? 24;
26
- const providerType = classifyProvider(providerName);
27
26
  const { limits, source, confidence } = resolveLimits({ providerName, sessionConfig, hostModel });
28
27
  let waveSize = requestedConcurrency;
29
28
  let cooldownUntil = null;
@@ -50,14 +49,7 @@ export function scheduleWave(options) {
50
49
  const learnedCap = computeMaxSafeConcurrency(quotaStateEntry, halfLifeHours);
51
50
  waveSize = Math.min(waveSize, learnedCap);
52
51
  }
53
- else if (providerType === "hosted" && source === "default") {
54
- // Unknown hosted provider with no learned data and no model-specific limits —
55
- // be conservative. If the caller supplied RPM/TPM caps those already govern rate;
56
- // this guard only triggers when we have no rate information at all.
57
- const conservativeDefault = quota.unknown_hosted_concurrency ?? 1;
58
- waveSize = Math.min(waveSize, conservativeDefault);
59
- }
60
- // Local providers with no learned data: use requestedConcurrency (no rate pressure)
52
+ // No learned data: use requestedConcurrency and let 429 outcomes train the cap
61
53
  }
62
54
  waveSize = Math.max(1, waveSize);
63
55
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -43,7 +43,7 @@ const OPENCODE_AUDIT_EDIT_PERMISSION = {
43
43
  };
44
44
 
45
45
  const OPENCODE_AUDIT_BASH_PERMISSION = {
46
- '*': 'ask',
46
+ '*': 'allow',
47
47
  'audit-code run-to-completion*': 'deny',
48
48
  'audit-code synthesize*': 'deny',
49
49
  'audit-code cleanup*': 'deny',
@@ -88,15 +88,116 @@ function replaceBackslashes(value) {
88
88
  return value.replace(/\\/g, '/');
89
89
  }
90
90
 
91
- function externalDirectoryPattern(path) {
92
- return `${replaceBackslashes(path).replace(/\/+$/u, '')}/**`;
91
+ function renderOpenCodeExternalDirectoryPermission() {
92
+ return { '*': 'allow' };
93
93
  }
94
94
 
95
- function renderOpenCodeExternalDirectoryPermission() {
96
- return {
97
- [externalDirectoryPattern(pkgRoot)]: 'allow',
98
- [externalDirectoryPattern(dirname(process.execPath))]: 'allow',
99
- };
95
+ function renderGlobalMcpLauncher(installedPkgRoot) {
96
+ return [
97
+ "import { access, readFile, appendFile } from 'node:fs/promises';",
98
+ "import { constants } from 'node:fs';",
99
+ "import { spawn } from 'node:child_process';",
100
+ "import { join } from 'node:path';",
101
+ "import { homedir } from 'node:os';",
102
+ '',
103
+ 'const repoRoot = process.cwd();',
104
+ "const artifactsDir = join(repoRoot, '.audit-artifacts');",
105
+ `const globalPackageRoot = ${JSON.stringify(installedPkgRoot)};`,
106
+ "const logPath = join(homedir(), '.audit-code', 'mcp-server.log');",
107
+ '',
108
+ 'async function log(msg) {',
109
+ ' try {',
110
+ ' const ts = new Date().toISOString();',
111
+ " await appendFile(logPath, `${ts} ${msg}\\n`, 'utf8');",
112
+ ' } catch {',
113
+ ' // ignore log failures',
114
+ ' }',
115
+ '}',
116
+ '',
117
+ 'async function exists(path) {',
118
+ ' try {',
119
+ ' await access(path, constants.F_OK);',
120
+ ' return true;',
121
+ ' } catch {',
122
+ ' return false;',
123
+ ' }',
124
+ '}',
125
+ '',
126
+ 'function spawnForward(command, args) {',
127
+ ' return new Promise((resolvePromise, rejectPromise) => {',
128
+ ' const child = spawn(command, args, {',
129
+ ' cwd: repoRoot,',
130
+ ' env: process.env,',
131
+ " stdio: ['inherit', 'inherit', 'inherit'],",
132
+ ' });',
133
+ " child.on('error', rejectPromise);",
134
+ " child.on('exit', (code) => resolvePromise(code ?? 1));",
135
+ ' });',
136
+ '}',
137
+ '',
138
+ 'async function tryCandidates() {',
139
+ " const localPackageEntrypoint = join(repoRoot, 'node_modules', 'auditor-lambda', 'audit-code.mjs');",
140
+ " const localBin = process.platform === 'win32'",
141
+ " ? join(repoRoot, 'node_modules', '.bin', 'audit-code.cmd')",
142
+ " : join(repoRoot, 'node_modules', '.bin', 'audit-code');",
143
+ " const repoPackageJsonPath = join(repoRoot, 'package.json');",
144
+ " const globalPackageEntrypoint = globalPackageRoot ? join(globalPackageRoot, 'audit-code.mjs') : null;",
145
+ " const sharedArgs = ['mcp', '--root', repoRoot, '--artifacts-dir', artifactsDir];",
146
+ '',
147
+ ' if (await exists(localPackageEntrypoint)) {',
148
+ " await log(`launching local node_modules candidate: ${localPackageEntrypoint}`);",
149
+ ' return await spawnForward(process.execPath, [localPackageEntrypoint, ...sharedArgs]);',
150
+ ' }',
151
+ '',
152
+ " if (await exists(repoPackageJsonPath) && await exists(join(repoRoot, 'audit-code.mjs'))) {",
153
+ ' try {',
154
+ " const packageJson = JSON.parse(await readFile(repoPackageJsonPath, 'utf8'));",
155
+ " if (packageJson?.name === 'auditor-lambda') {",
156
+ " await log(`launching repo-root candidate: ${join(repoRoot, 'audit-code.mjs')}`);",
157
+ " return await spawnForward(process.execPath, [join(repoRoot, 'audit-code.mjs'), ...sharedArgs]);",
158
+ ' }',
159
+ ' } catch {',
160
+ ' // fall through to the next candidate',
161
+ ' }',
162
+ ' }',
163
+ '',
164
+ ' if (globalPackageEntrypoint && await exists(globalPackageEntrypoint)) {',
165
+ " await log(`launching global candidate: ${globalPackageEntrypoint}`);",
166
+ ' return await spawnForward(process.execPath, [globalPackageEntrypoint, ...sharedArgs]);',
167
+ ' }',
168
+ '',
169
+ ' if (await exists(localBin)) {',
170
+ " await log(`launching local bin candidate: ${localBin}`);",
171
+ ' return await spawnForward(localBin, sharedArgs);',
172
+ ' }',
173
+ '',
174
+ " const pathCandidate = process.platform === 'win32' ? 'audit-code.cmd' : 'audit-code';",
175
+ " await log(`trying PATH candidate: ${pathCandidate}`);",
176
+ ' let exitCode = await spawnForward(pathCandidate, sharedArgs).catch(() => null);',
177
+ " if (typeof exitCode === 'number') {",
178
+ ' return exitCode;',
179
+ ' }',
180
+ '',
181
+ " exitCode = await spawnForward('npx', ['--no-install', 'audit-code', ...sharedArgs]).catch(() => null);",
182
+ " if (typeof exitCode === 'number') {",
183
+ ' return exitCode;',
184
+ ' }',
185
+ '',
186
+ " await log('ERROR: no candidate found');",
187
+ ' throw new Error(',
188
+ " 'Unable to locate an audit-code executable. Install auditor-lambda globally or as a local dependency.',",
189
+ ' );',
190
+ '}',
191
+ '',
192
+ "log(`run-mcp-server.mjs started: node=${process.execPath} cwd=${repoRoot} globalPkg=${globalPackageRoot}`).catch(() => {});",
193
+ 'const code = await tryCandidates().catch(async (err) => {',
194
+ " await log(`FATAL: ${err.message}`);",
195
+ ' process.stderr.write(err.message + "\\n");',
196
+ ' return 1;',
197
+ '});',
198
+ 'process.exitCode = code;',
199
+ '',
200
+ ].join('\n');
100
201
  }
101
202
 
102
203
  function objectValue(value) {
@@ -182,17 +283,23 @@ const OPENCODE_MCP_COMMAND_TEMPLATE = [
182
283
  'Use the auditor MCP tools as the primary interface to the audit workflow.',
183
284
  '',
184
285
  '1. Call `auditor_start_audit` to initialize and receive the first step.',
185
- '2. Read the `prompt_content` field in the response and follow the instructions there.',
286
+ '2. Check `step_kind` in the response:',
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.',
186
289
  '3. When a step completes (not blocked), call `auditor_continue_audit` to advance.',
187
290
  '4. Stop when the step instructions say to stop.',
188
291
  '',
189
292
  'Do not run shell commands. Use only `auditor_*` MCP tools and the `task` tool for subagent dispatch.',
293
+ '',
294
+ 'If `auditor_start_audit` is not listed in your available tools, stop immediately and tell the user the auditor MCP server is not connected. Do not read local files as a fallback.',
190
295
  ].join('\n');
191
296
 
192
297
  function mergeOpenCodeGlobalConfig(existing) {
193
298
  const parsed = existing ? JSON.parse(existing) : {};
194
299
  const auditPermission = renderOpenCodePermissionConfig();
195
300
  const existingAuditor = objectValue(objectValue(parsed.agent).auditor);
301
+ const nodeExecPath = replaceBackslashes(process.execPath);
302
+ const pkgEntrypoint = replaceBackslashes(join(pkgRoot, 'audit-code.mjs'));
196
303
  return {
197
304
  ...parsed,
198
305
  command: {
@@ -206,7 +313,19 @@ function mergeOpenCodeGlobalConfig(existing) {
206
313
  subtask: false,
207
314
  },
208
315
  },
209
- permission: mergeOpenCodePermissionConfig(parsed.permission, auditPermission),
316
+ mcp: {
317
+ ...objectValue(parsed.mcp),
318
+ auditor: {
319
+ type: 'local',
320
+ command: [nodeExecPath, pkgEntrypoint, 'mcp'],
321
+ enabled: true,
322
+ timeout: 10000,
323
+ },
324
+ },
325
+ permission: {
326
+ ...mergeOpenCodePermissionConfig(parsed.permission, auditPermission),
327
+ external_directory: { '*': 'allow' },
328
+ },
210
329
  agent: {
211
330
  ...(parsed.agent && typeof parsed.agent === 'object' && !Array.isArray(parsed.agent)
212
331
  ? parsed.agent
@@ -214,12 +333,10 @@ function mergeOpenCodeGlobalConfig(existing) {
214
333
  auditor: {
215
334
  ...existingAuditor,
216
335
  description: 'Read-heavy audit orchestration agent for the /audit-code workflow.',
217
- tools: {
218
- 'auditor*': true,
219
- task: true,
220
- },
221
336
  permission: {
222
337
  ...mergeOpenCodePermissionConfig(existingAuditor.permission, auditPermission),
338
+ external_directory: { '*': 'allow' },
339
+ 'auditor_*': 'allow',
223
340
  question: 'allow',
224
341
  task: 'allow',
225
342
  },
@@ -290,15 +407,24 @@ for (const install of installs) {
290
407
  }
291
408
  }
292
409
 
293
- // Install OpenCode global command via merged config
410
+ // Install global MCP launcher for OpenCode (and other hosts that support global config)
411
+ const globalMcpLauncherPath = join(homedir(), '.audit-code', 'run-mcp-server.mjs');
412
+ try {
413
+ const action = writeGeneratedFile(globalMcpLauncherPath, Buffer.from(renderGlobalMcpLauncher(pkgRoot)));
414
+ console.log(`audit-code: ${action} global MCP launcher at ${globalMcpLauncherPath}`);
415
+ } catch (err) {
416
+ console.warn(`audit-code: could not install global MCP launcher (${err.message})`);
417
+ }
418
+
419
+ // Install OpenCode global command and MCP via merged config
294
420
  const opencodeGlobalConfig = join(homedir(), '.config', 'opencode', 'opencode.json');
295
421
  try {
296
422
  const action = installMergedJson(opencodeGlobalConfig, (existing) =>
297
423
  mergeOpenCodeGlobalConfig(existing),
298
424
  );
299
- console.log(`audit-code: ${action} global OpenCode command in ${opencodeGlobalConfig}`);
425
+ console.log(`audit-code: ${action} global OpenCode config in ${opencodeGlobalConfig}`);
300
426
  } catch (err) {
301
- console.warn(`audit-code: could not install global OpenCode command (${err.message})`);
302
- console.warn(` To install manually, add "command": { "audit-code": { "template": "...", "agent": "auditor" } } to:`);
427
+ console.warn(`audit-code: could not install global OpenCode config (${err.message})`);
428
+ console.warn(` To install manually, add the mcp.auditor and command["audit-code"] entries to:`);
303
429
  console.warn(` ${opencodeGlobalConfig}`);
304
430
  }