auditor-lambda 0.3.23 → 0.3.25

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() {
@@ -605,8 +602,25 @@ function renderOpenCodePermissionConfig() {
605
602
  };
606
603
  }
607
604
 
608
- function renderOpenCodeProjectConfig(root) {
609
- const launcher = replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)));
605
+ const OPENCODE_MCP_COMMAND_TEMPLATE = [
606
+ '# audit-code',
607
+ '',
608
+ 'Use the auditor MCP tools as the primary interface to the audit workflow.',
609
+ '',
610
+ '1. Call `auditor_start_audit` to initialize and receive the first step.',
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.',
614
+ '3. When a step completes (not blocked), call `auditor_continue_audit` to advance.',
615
+ '4. Stop when the step instructions say to stop.',
616
+ '',
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.',
620
+ ].join('\n');
621
+
622
+ function renderOpenCodeProjectConfig(_root) {
623
+ const launcher = `.audit-code/install/${MCP_LAUNCHER_FILENAME}`;
610
624
  const auditPermission = renderOpenCodePermissionConfig();
611
625
  return {
612
626
  $schema: 'https://opencode.ai/config.json',
@@ -623,12 +637,11 @@ function renderOpenCodeProjectConfig(root) {
623
637
  auditor: {
624
638
  description:
625
639
  'Read-heavy audit orchestration agent for the /audit-code workflow.',
626
- tools: {
627
- 'auditor*': true,
628
- },
629
640
  permission: {
630
641
  ...auditPermission,
642
+ 'auditor_*': 'allow',
631
643
  question: 'allow',
644
+ task: 'allow',
632
645
  },
633
646
  },
634
647
  },
@@ -727,12 +740,10 @@ function assertOpenCodeAuditPermissionConfig(permissionConfig, label) {
727
740
  }
728
741
  const externalDirectory = permissionConfig?.external_directory;
729
742
  if (!externalDirectory || typeof externalDirectory !== 'object' || Array.isArray(externalDirectory)) {
730
- 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".`);
731
744
  }
732
- for (const pattern of Object.keys(renderOpenCodeExternalDirectoryPermission())) {
733
- if (externalDirectory[pattern] !== 'allow') {
734
- throw new Error(`OpenCode ${label}.external_directory must allow ${pattern}. Run "audit-code install --host opencode".`);
735
- }
745
+ if (externalDirectory['*'] !== 'allow') {
746
+ throw new Error(`OpenCode ${label}.external_directory must set "*" to "allow". Run "audit-code install --host opencode".`);
736
747
  }
737
748
  const edit = permissionConfig?.edit;
738
749
  const bash = permissionConfig?.bash;
@@ -800,16 +811,22 @@ function buildMergedOpenCodeProjectConfig(existing, root) {
800
811
  ...objectValue(existing.mcp),
801
812
  auditor: generated.mcp.auditor,
802
813
  },
803
- permission: mergeOpenCodePermissionConfig(existing.permission, generated.permission),
814
+ permission: {
815
+ ...mergeOpenCodePermissionConfig(existing.permission, generated.permission),
816
+ external_directory: { '*': 'allow' },
817
+ },
804
818
  agent: {
805
819
  ...objectValue(existing.agent),
806
820
  auditor: {
807
821
  ...objectValue(objectValue(existing.agent).auditor),
808
822
  ...generated.agent.auditor,
809
- permission: mergeOpenCodePermissionConfig(
810
- objectValue(objectValue(existing.agent).auditor).permission,
811
- generated.agent.auditor.permission,
812
- ),
823
+ permission: {
824
+ ...mergeOpenCodePermissionConfig(
825
+ objectValue(objectValue(existing.agent).auditor).permission,
826
+ generated.agent.auditor.permission,
827
+ ),
828
+ external_directory: { '*': 'allow' },
829
+ },
813
830
  },
814
831
  },
815
832
  };
@@ -1992,8 +2009,8 @@ async function verifyInstalledBootstrap(argv) {
1992
2009
  if (!Array.isArray(mcpCommand) || mcpCommand[0] !== 'node') {
1993
2010
  throw new Error('OpenCode config must set mcp.auditor.command as a Node command array.');
1994
2011
  }
1995
- if (mcpCommand[1] !== '.audit-code/install/run-mcp-server.mjs') {
1996
- 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'}.`);
1997
2014
  }
1998
2015
  if (config?.mcp?.auditor?.type !== 'local') {
1999
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;
@@ -523,10 +529,21 @@ function renderCapabilityCheckPrompt(params) {
523
529
  return [
524
530
  "# audit-code capability check",
525
531
  "",
526
- "Decide one thing from the active toolset: does this host expose a callable subagent/delegation tool for source-code review, such as Agent, Task, or an equivalent built-in subagent call?",
532
+ "Decide one thing from the active toolset: does this host expose a callable subagent/delegation tool for source-code review, such as `task`, Agent, or an equivalent built-in subagent call?",
527
533
  "",
528
534
  "Do not run shell commands to answer this. Do not inspect packet prompts, schemas, or backend command catalogs.",
529
535
  "",
536
+ "**If auditor MCP tools are available** (preferred — no shell required):",
537
+ "",
538
+ "Call `auditor_report_capability` with:",
539
+ "- `can_dispatch_subagents: true` if the `task` tool or equivalent subagent dispatch is available",
540
+ "- `can_dispatch_subagents: false` if not",
541
+ "- Optionally `can_restrict_subagent_tools: true` and/or `can_select_subagent_model: true`",
542
+ "",
543
+ "Read the `prompt_content` field in the tool response and follow it.",
544
+ "",
545
+ "**Fallback — if auditor MCP tools are not available:**",
546
+ "",
530
547
  "If callable subagents are available, run:",
531
548
  "",
532
549
  ` ${yesCommand}`,
@@ -550,30 +567,41 @@ function renderDispatchReviewPrompt(params) {
550
567
  const toolsLine = params.hostCanRestrictSubagentTools
551
568
  ? "Restrict review subagents to read/search plus the packet submit command named in their prompt. Do not give them source edit/write tools."
552
569
  : "Do not ask the user about per-subagent tool restrictions; this host did not report a callable restriction facility.";
553
- const fileLines = params.dispatchQuotaPath
570
+ const runId = params.activeReviewRun.run_id;
571
+ const dispatchDataLines = params.dispatchQuotaPath
554
572
  ? [
555
- "Dispatch is prepared. Read both of these files:",
573
+ "**If auditor MCP tools are available** (preferred):",
574
+ "",
575
+ "The dispatch plan entries are in the `dispatch_plan_entries` field of the tool response that returned this step. The wave schedule is in the `dispatch_quota` field.",
576
+ "",
577
+ "Use the `wave_size` from `dispatch_quota`. If `cooldown_until` is non-null, wait until that timestamp before starting the first wave.",
578
+ "",
579
+ "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
+ "",
581
+ "**Fallback — if auditor MCP tools are not available:** Read both of these files:",
556
582
  "",
557
583
  ` Dispatch plan: ${params.dispatchPlanPath}`,
558
584
  ` Dispatch quota: ${params.dispatchQuotaPath}`,
559
585
  "",
560
- "The quota file contains a `wave_size` field. Dispatch at most `wave_size` subagents at a time. If `cooldown_until` is non-null, wait until that timestamp before starting the first wave.",
561
- "",
562
- "For each wave: launch up to `wave_size` subagents in parallel (one per plan entry), wait for all of them to finish, then start the next wave. Repeat until all entries are dispatched.",
586
+ "Apply the same wave logic from the quota file.",
563
587
  ]
564
588
  : [
565
- "Dispatch is prepared. Read only this dispatch plan JSON:",
589
+ "**If auditor MCP tools are available** (preferred):",
590
+ "",
591
+ "The dispatch plan entries are in the `dispatch_plan_entries` field of the tool response that returned this step.",
592
+ "",
593
+ "**Fallback — if auditor MCP tools are not available:** Read this dispatch plan JSON:",
566
594
  "",
567
595
  ` ${params.dispatchPlanPath}`,
568
596
  "",
569
- "Launch one host subagent for each entry in the plan.",
597
+ "Launch one subagent for each entry in the plan.",
570
598
  ];
571
599
  return [
572
600
  "# audit-code dispatch review",
573
601
  "",
574
- ...fileLines,
602
+ ...dispatchDataLines,
575
603
  "",
576
- "Pass each packet prompt path literally to its subagent; do not load packet prompt files into this orchestrator context.",
604
+ "Pass each `entry.prompt_path` literally to its subagent; do not load packet prompt files into this orchestrator context.",
577
605
  "",
578
606
  "Subagent prompt shape:",
579
607
  "",
@@ -584,7 +612,11 @@ function renderDispatchReviewPrompt(params) {
584
612
  "",
585
613
  "Each subagent must submit its packet through the submit command printed in its packet prompt and stop after successful submission.",
586
614
  "",
587
- "After all waves complete, run exactly:",
615
+ "**After all waves complete:**",
616
+ "",
617
+ "If auditor MCP tools are available, call `auditor_merge_and_ingest` with `{ run_id: \"" + runId + "\" }`, then call `auditor_continue_audit` and follow the `prompt_content` in the response.",
618
+ "",
619
+ "Fallback — if auditor MCP tools are not available, run exactly:",
588
620
  "",
589
621
  ` ${mergeCommand}`,
590
622
  "",
@@ -624,9 +656,11 @@ function renderPresentReportPrompt(finalReportPath) {
624
656
  "",
625
657
  "The deterministic audit is complete.",
626
658
  "",
627
- "Read this report and present the completed audit with work blocks first:",
628
- "",
659
+ "Read the final audit report from the `audit-code://report/current` MCP resource (or from the file at:",
629
660
  ` ${finalReportPath}`,
661
+ "if a Read tool is available).",
662
+ "",
663
+ "Present the completed audit with work blocks first.",
630
664
  "",
631
665
  "Do not run the orchestrator again for this completed audit.",
632
666
  "",
@@ -697,6 +731,7 @@ export const cliTestUtils = {
697
731
  getUiMode,
698
732
  looksLikeCliFlag,
699
733
  countLines,
734
+ warnIfNotGitRepo,
700
735
  };
701
736
  async function maybeArchiveLegacyPendingResults(auditResultsPath) {
702
737
  if (!auditResultsPath || basename(auditResultsPath) !== "worker_results_pending.json") {
@@ -926,6 +961,7 @@ export async function runSample(argv = process.argv) {
926
961
  }
927
962
  async function cmdAdvanceAudit(argv) {
928
963
  const root = getRootDir(argv);
964
+ warnIfNotGitRepo(root);
929
965
  const artifactsDir = getArtifactsDir(argv);
930
966
  await cleanupStaleArtifactsDir(artifactsDir);
931
967
  await mkdir(artifactsDir, { recursive: true });
@@ -1090,6 +1126,7 @@ async function runDeterministicForNextStep(params) {
1090
1126
  }
1091
1127
  async function cmdNextStep(argv) {
1092
1128
  const root = getRootDir(argv);
1129
+ warnIfNotGitRepo(root);
1093
1130
  const artifactsDir = getArtifactsDir(argv);
1094
1131
  await mkdir(artifactsDir, { recursive: true });
1095
1132
  await ensureSupervisorDirs(artifactsDir);
@@ -1231,8 +1268,13 @@ async function cmdNextStep(argv) {
1231
1268
  stepKind: "dispatch_review",
1232
1269
  status: "ready",
1233
1270
  runId: result.activeReviewRun.run_id,
1234
- allowedCommands: [mergeCommand, continueCommand],
1235
- stopCondition: "Dispatch every packet, merge-and-ingest once, then run next-step again.",
1271
+ allowedCommands: [
1272
+ "auditor_merge_and_ingest",
1273
+ "auditor_continue_audit",
1274
+ mergeCommand,
1275
+ continueCommand,
1276
+ ],
1277
+ stopCondition: "Dispatch every packet, call auditor_merge_and_ingest once, then call auditor_continue_audit.",
1236
1278
  repoRoot: root,
1237
1279
  artifactPaths: {
1238
1280
  dispatch_plan: dispatch.dispatch_plan_path,
@@ -1255,6 +1297,7 @@ async function cmdNextStep(argv) {
1255
1297
  }
1256
1298
  async function cmdRunToCompletion(argv) {
1257
1299
  const root = getRootDir(argv);
1300
+ warnIfNotGitRepo(root);
1258
1301
  const artifactsDir = getArtifactsDir(argv);
1259
1302
  await cleanupStaleArtifactsDir(artifactsDir);
1260
1303
  await mkdir(artifactsDir, { recursive: true });
@@ -2172,7 +2215,6 @@ async function prepareDispatchArtifacts(params) {
2172
2215
  const runId = params.runId;
2173
2216
  const artifactsDir = params.artifactsDir;
2174
2217
  const runDir = join(artifactsDir, "runs", runId);
2175
- const tasksPath = join(runDir, "pending-audit-tasks.json");
2176
2218
  const taskResultsDir = join(runDir, "task-results");
2177
2219
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
2178
2220
  let reviewRoot = params.root;
@@ -2185,8 +2227,13 @@ async function prepareDispatchArtifacts(params) {
2185
2227
  throw error;
2186
2228
  }
2187
2229
  }
2188
- const tasks = await readJsonFile(tasksPath);
2189
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
+ });
2190
2237
  const sessionConfig = params.sessionConfig ?? (await loadSessionConfig(artifactsDir).catch(() => ({})));
2191
2238
  const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
2192
2239
  const lensDefs = await readJsonFile(lensDefsPath);
@@ -2704,6 +2751,8 @@ async function cmdMergeAndIngest(argv) {
2704
2751
  preferredExecutor: "result_ingestion_executor",
2705
2752
  auditResultsPath,
2706
2753
  });
2754
+ const updatedPendingTasks = await addFileLineCountHints(workerTask.repo_root, buildPendingAuditTasks(result.updated_bundle));
2755
+ await writeJsonFile(tasksPath, updatedPendingTasks);
2707
2756
  const workerResult = buildWorkerResult({
2708
2757
  runId,
2709
2758
  obligationId: workerTask.obligation_id,
@@ -2805,9 +2854,11 @@ async function cmdImportExternalAnalyzer(argv) {
2805
2854
  }, null, 2));
2806
2855
  }
2807
2856
  async function cmdIntake(argv) {
2857
+ const root = getRootDir(argv);
2858
+ warnIfNotGitRepo(root);
2808
2859
  const artifactsDir = getArtifactsDir(argv);
2809
2860
  const result = await runAuditStep({
2810
- root: getRootDir(argv),
2861
+ root,
2811
2862
  artifactsDir,
2812
2863
  preferredExecutor: "intake_executor",
2813
2864
  });
@@ -3039,6 +3090,114 @@ async function cmdCleanup(argv) {
3039
3090
  dry_run: dryRun,
3040
3091
  }, null, 2));
3041
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
+ }
3042
3201
  async function cmdMcp(argv) {
3043
3202
  await runAuditCodeMcpServer(argv.slice(3));
3044
3203
  }
@@ -3150,9 +3309,12 @@ async function main(argv) {
3150
3309
  case "quota":
3151
3310
  await cmdQuota(argv);
3152
3311
  return;
3312
+ case "status":
3313
+ await cmdStatus(argv);
3314
+ return;
3153
3315
  default:
3154
3316
  console.error(`Unknown command: ${command}`);
3155
- 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");
3156
3318
  process.exitCode = 1;
3157
3319
  }
3158
3320
  }
@@ -85,6 +85,14 @@ function parseContentLength(headerBlock) {
85
85
  }
86
86
  return contentLength;
87
87
  }
88
+ async function readOptionalJson(path) {
89
+ try {
90
+ return JSON.parse(await readFile(path, "utf8"));
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
88
96
  async function runWrapperCommand(args, options) {
89
97
  return await new Promise((resolvePromise, rejectPromise) => {
90
98
  const child = spawn(process.execPath, [
@@ -266,13 +274,41 @@ function renderPrompt(name, args) {
266
274
  throw new Error(`Unknown prompt: ${name}`);
267
275
  }
268
276
  }
277
+ async function runContinueAudit(context, extraArgs = []) {
278
+ const step = await parseCliJson(extraArgs, context);
279
+ if (!step || typeof step !== "object" || Array.isArray(step))
280
+ return step;
281
+ const s = step;
282
+ if (hasValue(s.prompt_path)) {
283
+ try {
284
+ s.prompt_content = await readFile(s.prompt_path, "utf8");
285
+ }
286
+ catch {
287
+ // ignore — prompt_path is a fallback for hosts that can read files
288
+ }
289
+ }
290
+ if (s.step_kind === "dispatch_review") {
291
+ const paths = s.artifact_paths;
292
+ if (hasValue(paths?.dispatch_plan)) {
293
+ const plan = await readOptionalJson(paths.dispatch_plan);
294
+ if (plan !== undefined)
295
+ s.dispatch_plan_entries = plan;
296
+ }
297
+ if (hasValue(paths?.dispatch_quota)) {
298
+ const quota = await readOptionalJson(paths.dispatch_quota);
299
+ if (quota !== undefined)
300
+ s.dispatch_quota = quota;
301
+ }
302
+ }
303
+ return s;
304
+ }
269
305
  async function handleToolCall(name, params, defaults) {
270
306
  const context = getToolContext(params, defaults);
271
307
  switch (name) {
272
308
  case "start_audit":
273
- return toolResult(await parseCliJson([], context));
309
+ return toolResult(await runContinueAudit(context));
274
310
  case "continue_audit":
275
- return toolResult(await parseCliJson([], context));
311
+ return toolResult(await runContinueAudit(context));
276
312
  case "get_status":
277
313
  return toolResult(await getStatusPayload(context));
278
314
  case "explain_task": {
@@ -304,6 +340,32 @@ async function handleToolCall(name, params, defaults) {
304
340
  : params?.updatesPath;
305
341
  return toolResult(await parseCliJson(["--updates", resolve(updatesPath)], context));
306
342
  }
343
+ case "merge_and_ingest": {
344
+ const runId = hasValue(params?.run_id)
345
+ ? params.run_id
346
+ : hasValue(params?.runId)
347
+ ? params.runId
348
+ : undefined;
349
+ if (!runId)
350
+ throw new Error("merge_and_ingest requires run_id.");
351
+ return toolResult(await parseCliJson(["merge-and-ingest", "--run-id", runId], context, true));
352
+ }
353
+ case "report_capability": {
354
+ const extraArgs = [];
355
+ const canDispatch = params?.can_dispatch_subagents ?? params?.canDispatchSubagents;
356
+ if (canDispatch !== undefined) {
357
+ extraArgs.push("--host-can-dispatch-subagents", String(Boolean(canDispatch)));
358
+ }
359
+ const canRestrict = params?.can_restrict_subagent_tools ?? params?.canRestrictSubagentTools;
360
+ if (canRestrict !== undefined) {
361
+ extraArgs.push("--host-can-restrict-subagent-tools", String(Boolean(canRestrict)));
362
+ }
363
+ const canSelect = params?.can_select_subagent_model ?? params?.canSelectSubagentModel;
364
+ if (canSelect !== undefined) {
365
+ extraArgs.push("--host-can-select-subagent-model", String(Boolean(canSelect)));
366
+ }
367
+ return toolResult(await runContinueAudit(context, ["next-step", ...extraArgs]));
368
+ }
307
369
  default:
308
370
  throw new Error(`Unknown tool: ${name}`);
309
371
  }
@@ -423,6 +485,52 @@ function toolDefinitions() {
423
485
  required: ["updates_path"],
424
486
  },
425
487
  },
488
+ {
489
+ name: "merge_and_ingest",
490
+ description: "Merge completed packet submissions into the artifact bundle after all dispatch subagents finish.",
491
+ inputSchema: {
492
+ type: "object",
493
+ properties: {
494
+ run_id: {
495
+ type: "string",
496
+ description: "Review run ID from the dispatch_review step response.",
497
+ },
498
+ root: { type: "string", description: "Repository root override." },
499
+ artifacts_dir: {
500
+ type: "string",
501
+ description: "Artifacts directory override.",
502
+ },
503
+ },
504
+ required: ["run_id"],
505
+ },
506
+ },
507
+ {
508
+ name: "report_capability",
509
+ description: "Report host subagent dispatch capability and advance to the next step. Call this instead of running audit-code next-step from the shell during a capability_check step.",
510
+ inputSchema: {
511
+ type: "object",
512
+ properties: {
513
+ can_dispatch_subagents: {
514
+ type: "boolean",
515
+ description: "Whether this host can dispatch subagents (e.g. via the task tool).",
516
+ },
517
+ can_restrict_subagent_tools: {
518
+ type: "boolean",
519
+ description: "Whether this host can restrict tools per subagent.",
520
+ },
521
+ can_select_subagent_model: {
522
+ type: "boolean",
523
+ description: "Whether this host can select a model per subagent.",
524
+ },
525
+ root: { type: "string", description: "Repository root override." },
526
+ artifacts_dir: {
527
+ type: "string",
528
+ description: "Artifacts directory override.",
529
+ },
530
+ },
531
+ required: ["can_dispatch_subagents"],
532
+ },
533
+ },
426
534
  ];
427
535
  }
428
536
  export async function runAuditCodeMcpServer(argv) {
@@ -469,9 +577,15 @@ export async function runAuditCodeMcpServer(argv) {
469
577
  }
470
578
  try {
471
579
  switch (request.method) {
472
- 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;
473
587
  writeMessage(success(request.id ?? null, {
474
- protocolVersion: PROTOCOL_VERSION,
588
+ protocolVersion: negotiatedVersion,
475
589
  serverInfo: {
476
590
  name: "audit-code",
477
591
  version,
@@ -484,6 +598,7 @@ export async function runAuditCodeMcpServer(argv) {
484
598
  },
485
599
  }));
486
600
  break;
601
+ }
487
602
  case "notifications/initialized":
488
603
  break;
489
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.23",
3
+ "version": "0.3.25",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -35,13 +35,6 @@ function writeGeneratedFile(path, content) {
35
35
  return action;
36
36
  }
37
37
 
38
- function splitFrontmatter(text) {
39
- const normalized = text.replace(/\r\n/g, '\n');
40
- const match = normalized.match(/^---\n([\s\S]*?)\n---\n?/u);
41
- if (!match) return { body: normalized };
42
- return { body: normalized.slice(match[0].length) };
43
- }
44
-
45
38
  const OPENCODE_AUDIT_EDIT_PERMISSION = {
46
39
  '*': 'ask',
47
40
  '.audit-code/**': 'allow',
@@ -50,7 +43,7 @@ const OPENCODE_AUDIT_EDIT_PERMISSION = {
50
43
  };
51
44
 
52
45
  const OPENCODE_AUDIT_BASH_PERMISSION = {
53
- '*': 'ask',
46
+ '*': 'allow',
54
47
  'audit-code run-to-completion*': 'deny',
55
48
  'audit-code synthesize*': 'deny',
56
49
  'audit-code cleanup*': 'deny',
@@ -95,15 +88,94 @@ function replaceBackslashes(value) {
95
88
  return value.replace(/\\/g, '/');
96
89
  }
97
90
 
98
- function externalDirectoryPattern(path) {
99
- return `${replaceBackslashes(path).replace(/\/+$/u, '')}/**`;
91
+ function renderOpenCodeExternalDirectoryPermission() {
92
+ return { '*': 'allow' };
100
93
  }
101
94
 
102
- function renderOpenCodeExternalDirectoryPermission() {
103
- return {
104
- [externalDirectoryPattern(pkgRoot)]: 'allow',
105
- [externalDirectoryPattern(dirname(process.execPath))]: 'allow',
106
- };
95
+ function renderGlobalMcpLauncher(installedPkgRoot) {
96
+ return [
97
+ "import { access, readFile } from 'node:fs/promises';",
98
+ "import { constants } from 'node:fs';",
99
+ "import { spawn } from 'node:child_process';",
100
+ "import { join } from 'node:path';",
101
+ '',
102
+ 'const repoRoot = process.cwd();',
103
+ "const artifactsDir = join(repoRoot, '.audit-artifacts');",
104
+ `const globalPackageRoot = ${JSON.stringify(installedPkgRoot)};`,
105
+ '',
106
+ 'async function exists(path) {',
107
+ ' try {',
108
+ ' await access(path, constants.F_OK);',
109
+ ' return true;',
110
+ ' } catch {',
111
+ ' return false;',
112
+ ' }',
113
+ '}',
114
+ '',
115
+ 'function spawnForward(command, args) {',
116
+ ' return new Promise((resolvePromise, rejectPromise) => {',
117
+ ' const child = spawn(command, args, {',
118
+ ' cwd: repoRoot,',
119
+ ' env: process.env,',
120
+ " stdio: ['inherit', 'inherit', 'inherit'],",
121
+ ' });',
122
+ " child.on('error', rejectPromise);",
123
+ " child.on('exit', (code) => resolvePromise(code ?? 1));",
124
+ ' });',
125
+ '}',
126
+ '',
127
+ 'async function tryCandidates() {',
128
+ " const localPackageEntrypoint = join(repoRoot, 'node_modules', 'auditor-lambda', 'audit-code.mjs');",
129
+ " const localBin = process.platform === 'win32'",
130
+ " ? join(repoRoot, 'node_modules', '.bin', 'audit-code.cmd')",
131
+ " : join(repoRoot, 'node_modules', '.bin', 'audit-code');",
132
+ " const repoPackageJsonPath = join(repoRoot, 'package.json');",
133
+ " const globalPackageEntrypoint = globalPackageRoot ? join(globalPackageRoot, 'audit-code.mjs') : null;",
134
+ " const sharedArgs = ['mcp', '--root', repoRoot, '--artifacts-dir', artifactsDir];",
135
+ '',
136
+ ' if (await exists(localPackageEntrypoint)) {',
137
+ ' return await spawnForward(process.execPath, [localPackageEntrypoint, ...sharedArgs]);',
138
+ ' }',
139
+ '',
140
+ " if (await exists(repoPackageJsonPath) && await exists(join(repoRoot, 'audit-code.mjs'))) {",
141
+ ' try {',
142
+ " const packageJson = JSON.parse(await readFile(repoPackageJsonPath, 'utf8'));",
143
+ " if (packageJson?.name === 'auditor-lambda') {",
144
+ " return await spawnForward(process.execPath, [join(repoRoot, 'audit-code.mjs'), ...sharedArgs]);",
145
+ ' }',
146
+ ' } catch {',
147
+ ' // fall through to the next candidate',
148
+ ' }',
149
+ ' }',
150
+ '',
151
+ ' if (globalPackageEntrypoint && await exists(globalPackageEntrypoint)) {',
152
+ ' return await spawnForward(process.execPath, [globalPackageEntrypoint, ...sharedArgs]);',
153
+ ' }',
154
+ '',
155
+ ' if (await exists(localBin)) {',
156
+ ' return await spawnForward(localBin, sharedArgs);',
157
+ ' }',
158
+ '',
159
+ " const pathCandidate = process.platform === 'win32' ? 'audit-code.cmd' : 'audit-code';",
160
+ ' let exitCode = await spawnForward(pathCandidate, sharedArgs).catch(() => null);',
161
+ " if (typeof exitCode === 'number') {",
162
+ ' return exitCode;',
163
+ ' }',
164
+ '',
165
+ " exitCode = await spawnForward('npx', ['--no-install', 'audit-code', ...sharedArgs]).catch(() => null);",
166
+ " if (typeof exitCode === 'number') {",
167
+ ' return exitCode;',
168
+ ' }',
169
+ '',
170
+ ' throw new Error(',
171
+ " 'Unable to locate an audit-code executable. Install auditor-lambda globally or as a local dependency.',",
172
+ ' );',
173
+ '}',
174
+ '',
175
+ 'const code = await tryCandidates();',
176
+ 'process.exitCode = code;',
177
+ '',
178
+ ].join('\n');
107
179
  }
108
180
 
109
181
  function objectValue(value) {
@@ -183,10 +255,29 @@ function renderOpenCodePermissionConfig() {
183
255
  };
184
256
  }
185
257
 
186
- function mergeOpenCodeGlobalConfig(existing, promptBody) {
258
+ const OPENCODE_MCP_COMMAND_TEMPLATE = [
259
+ '# audit-code',
260
+ '',
261
+ 'Use the auditor MCP tools as the primary interface to the audit workflow.',
262
+ '',
263
+ '1. Call `auditor_start_audit` to initialize and receive the first step.',
264
+ '2. Check `step_kind` in the response:',
265
+ ' - 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.',
266
+ ' - Otherwise: read `prompt_content` and follow it.',
267
+ '3. When a step completes (not blocked), call `auditor_continue_audit` to advance.',
268
+ '4. Stop when the step instructions say to stop.',
269
+ '',
270
+ 'Do not run shell commands. Use only `auditor_*` MCP tools and the `task` tool for subagent dispatch.',
271
+ '',
272
+ '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.',
273
+ ].join('\n');
274
+
275
+ function mergeOpenCodeGlobalConfig(existing) {
187
276
  const parsed = existing ? JSON.parse(existing) : {};
188
277
  const auditPermission = renderOpenCodePermissionConfig();
189
278
  const existingAuditor = objectValue(objectValue(parsed.agent).auditor);
279
+ const globalLauncherPath = replaceBackslashes(join(homedir(), '.audit-code', 'run-mcp-server.mjs'));
280
+ const nodeExecPath = replaceBackslashes(process.execPath);
190
281
  return {
191
282
  ...parsed,
192
283
  command: {
@@ -194,13 +285,25 @@ function mergeOpenCodeGlobalConfig(existing, promptBody) {
194
285
  ? parsed.command
195
286
  : {}),
196
287
  'audit-code': {
197
- template: promptBody.trimStart(),
288
+ template: OPENCODE_MCP_COMMAND_TEMPLATE,
198
289
  description: 'Autonomous local loop code auditing',
199
290
  agent: 'auditor',
200
291
  subtask: false,
201
292
  },
202
293
  },
203
- permission: mergeOpenCodePermissionConfig(parsed.permission, auditPermission),
294
+ mcp: {
295
+ ...objectValue(parsed.mcp),
296
+ auditor: {
297
+ type: 'local',
298
+ command: [nodeExecPath, globalLauncherPath],
299
+ enabled: true,
300
+ timeout: 10000,
301
+ },
302
+ },
303
+ permission: {
304
+ ...mergeOpenCodePermissionConfig(parsed.permission, auditPermission),
305
+ external_directory: { '*': 'allow' },
306
+ },
204
307
  agent: {
205
308
  ...(parsed.agent && typeof parsed.agent === 'object' && !Array.isArray(parsed.agent)
206
309
  ? parsed.agent
@@ -208,10 +311,13 @@ function mergeOpenCodeGlobalConfig(existing, promptBody) {
208
311
  auditor: {
209
312
  ...existingAuditor,
210
313
  description: 'Read-heavy audit orchestration agent for the /audit-code workflow.',
211
- permission: mergeOpenCodePermissionConfig(
212
- existingAuditor.permission,
213
- auditPermission,
214
- ),
314
+ permission: {
315
+ ...mergeOpenCodePermissionConfig(existingAuditor.permission, auditPermission),
316
+ external_directory: { '*': 'allow' },
317
+ 'auditor_*': 'allow',
318
+ question: 'allow',
319
+ task: 'allow',
320
+ },
215
321
  },
216
322
  },
217
323
  };
@@ -233,7 +339,6 @@ if (!promptSource || !skillSource) {
233
339
  process.exit(0);
234
340
  }
235
341
 
236
- const promptBody = splitFrontmatter(promptSource.toString('utf8')).body;
237
342
  const codexOpenAiAgentSource = readOptionalSource(codexOpenAiAgentSourceFile, 'Codex skill UI metadata');
238
343
 
239
344
  const installs = [
@@ -280,15 +385,24 @@ for (const install of installs) {
280
385
  }
281
386
  }
282
387
 
283
- // Install OpenCode global command via merged config
388
+ // Install global MCP launcher for OpenCode (and other hosts that support global config)
389
+ const globalMcpLauncherPath = join(homedir(), '.audit-code', 'run-mcp-server.mjs');
390
+ try {
391
+ const action = writeGeneratedFile(globalMcpLauncherPath, Buffer.from(renderGlobalMcpLauncher(pkgRoot)));
392
+ console.log(`audit-code: ${action} global MCP launcher at ${globalMcpLauncherPath}`);
393
+ } catch (err) {
394
+ console.warn(`audit-code: could not install global MCP launcher (${err.message})`);
395
+ }
396
+
397
+ // Install OpenCode global command and MCP via merged config
284
398
  const opencodeGlobalConfig = join(homedir(), '.config', 'opencode', 'opencode.json');
285
399
  try {
286
400
  const action = installMergedJson(opencodeGlobalConfig, (existing) =>
287
- mergeOpenCodeGlobalConfig(existing, promptBody),
401
+ mergeOpenCodeGlobalConfig(existing),
288
402
  );
289
- console.log(`audit-code: ${action} global OpenCode command in ${opencodeGlobalConfig}`);
403
+ console.log(`audit-code: ${action} global OpenCode config in ${opencodeGlobalConfig}`);
290
404
  } catch (err) {
291
- console.warn(`audit-code: could not install global OpenCode command (${err.message})`);
292
- console.warn(` To install manually, add "command": { "audit-code": { "template": "...", "agent": "auditor" } } to:`);
405
+ console.warn(`audit-code: could not install global OpenCode config (${err.message})`);
406
+ console.warn(` To install manually, add the mcp.auditor and command["audit-code"] entries to:`);
293
407
  console.warn(` ${opencodeGlobalConfig}`);
294
408
  }