auditor-lambda 0.3.19 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,8 +30,9 @@ npm install -g auditor-lambda
30
30
 
31
31
  That makes `audit-code` available on `PATH`. During package install, the package
32
32
  also writes user-level command/skill assets for hosts we can seed safely, including
33
- the Claude command file, the global Codex skill bundle, and the global OpenCode
34
- slash command entry in `~/.config/opencode/opencode.json`.
33
+ the Claude command file, the global Codex skill bundle with `audit-code` display
34
+ metadata, and the global OpenCode slash command entry in
35
+ `~/.config/opencode/opencode.json`.
35
36
 
36
37
  After that, invoke `/audit-code` in a supported host. The prompt self-bootstraps
37
38
  the current repository by running:
@@ -533,8 +533,59 @@ function renderCodexAutomationRecipe() {
533
533
  ].join('\n');
534
534
  }
535
535
 
536
+ const OPENCODE_AUDIT_EDIT_PERMISSION = {
537
+ '*': 'ask',
538
+ '.audit-code/**': 'allow',
539
+ '.audit-artifacts/**': 'allow',
540
+ 'audit-report.md': 'allow',
541
+ };
542
+
543
+ const OPENCODE_AUDIT_BASH_PERMISSION = {
544
+ '*': 'ask',
545
+ 'audit-code run-to-completion*': 'deny',
546
+ 'audit-code synthesize*': 'deny',
547
+ 'audit-code cleanup*': 'deny',
548
+ 'audit-code requeue*': 'deny',
549
+ 'audit-code ingest-results*': 'deny',
550
+ '*audit-code.mjs* run-to-completion*': 'deny',
551
+ '*audit-code.mjs* synthesize*': 'deny',
552
+ '*audit-code.mjs* cleanup*': 'deny',
553
+ '*audit-code.mjs* requeue*': 'deny',
554
+ '*audit-code.mjs* ingest-results*': 'deny',
555
+ 'audit-code': 'allow',
556
+ 'audit-code ensure*': 'allow',
557
+ 'audit-code prepare-dispatch*': 'allow',
558
+ 'audit-code submit-packet*': 'allow',
559
+ 'audit-code merge-and-ingest*': 'allow',
560
+ 'audit-code validate*': 'allow',
561
+ '*audit-code.mjs': 'allow',
562
+ '*audit-code.mjs* ensure*': 'allow',
563
+ '*audit-code.mjs* prepare-dispatch*': 'allow',
564
+ '*audit-code.mjs* submit-packet*': 'allow',
565
+ '*audit-code.mjs* merge-and-ingest*': 'allow',
566
+ '*audit-code.mjs* worker-run*': 'allow',
567
+ '*audit-code.mjs* validate*': 'allow',
568
+ 'node* .audit-code/install/run-mcp-server.mjs*': 'allow',
569
+ 'node* ./.audit-code/install/run-mcp-server.mjs*': 'allow',
570
+ 'git status*': 'allow',
571
+ 'git diff*': 'allow',
572
+ 'grep *': 'allow',
573
+ 'rm *': 'deny',
574
+ };
575
+
576
+ function renderOpenCodePermissionConfig() {
577
+ return {
578
+ read: 'allow',
579
+ glob: 'allow',
580
+ grep: 'allow',
581
+ edit: { ...OPENCODE_AUDIT_EDIT_PERMISSION },
582
+ bash: { ...OPENCODE_AUDIT_BASH_PERMISSION },
583
+ };
584
+ }
585
+
536
586
  function renderOpenCodeProjectConfig(root, promptBody) {
537
587
  const launcher = replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)));
588
+ const auditPermission = renderOpenCodePermissionConfig();
538
589
  return {
539
590
  $schema: 'https://opencode.ai/config.json',
540
591
  command: {
@@ -553,19 +604,7 @@ function renderOpenCodeProjectConfig(root, promptBody) {
553
604
  timeout: 10000,
554
605
  },
555
606
  },
556
- permission: {
557
- read: 'allow',
558
- glob: 'allow',
559
- grep: 'allow',
560
- edit: 'ask',
561
- bash: {
562
- '*': 'ask',
563
- 'git status*': 'allow',
564
- 'git diff*': 'allow',
565
- 'grep *': 'allow',
566
- 'rm *': 'deny',
567
- },
568
- },
607
+ permission: auditPermission,
569
608
  agent: {
570
609
  auditor: {
571
610
  description:
@@ -574,14 +613,8 @@ function renderOpenCodeProjectConfig(root, promptBody) {
574
613
  'auditor*': true,
575
614
  },
576
615
  permission: {
577
- edit: 'ask',
578
- bash: {
579
- '*': 'ask',
580
- 'git status*': 'allow',
581
- 'git diff*': 'allow',
582
- 'grep *': 'allow',
583
- 'rm *': 'deny',
584
- },
616
+ edit: { ...OPENCODE_AUDIT_EDIT_PERMISSION },
617
+ bash: { ...OPENCODE_AUDIT_BASH_PERMISSION },
585
618
  question: 'allow',
586
619
  },
587
620
  },
@@ -607,6 +640,99 @@ function objectValue(value) {
607
640
  : {};
608
641
  }
609
642
 
643
+ function mergeOpenCodePermissionRule(existingRule, generatedRule, managedRules = {}) {
644
+ if (generatedRule && typeof generatedRule === 'object' && !Array.isArray(generatedRule)) {
645
+ const generatedObject = generatedRule;
646
+ const merged = {};
647
+ const existingObject =
648
+ existingRule && typeof existingRule === 'object' && !Array.isArray(existingRule)
649
+ ? existingRule
650
+ : {};
651
+
652
+ if (typeof existingRule === 'string') {
653
+ merged['*'] = existingRule;
654
+ } else {
655
+ merged['*'] = existingObject['*'] ?? generatedObject['*'] ?? 'ask';
656
+ }
657
+
658
+ for (const [key, value] of Object.entries(generatedObject)) {
659
+ if (key !== '*') merged[key] = value;
660
+ }
661
+ for (const [key, value] of Object.entries(existingObject)) {
662
+ if (key !== '*') merged[key] = value;
663
+ }
664
+ for (const [key, value] of Object.entries(managedRules)) {
665
+ merged[key] = value;
666
+ }
667
+
668
+ return merged;
669
+ }
670
+
671
+ return existingRule ?? generatedRule;
672
+ }
673
+
674
+ function mergeOpenCodePermissionConfig(existingPermission, generatedPermission) {
675
+ if (!existingPermission || typeof existingPermission !== 'object' || Array.isArray(existingPermission)) {
676
+ return generatedPermission;
677
+ }
678
+
679
+ return {
680
+ ...generatedPermission,
681
+ ...existingPermission,
682
+ edit: mergeOpenCodePermissionRule(
683
+ existingPermission.edit,
684
+ generatedPermission.edit,
685
+ OPENCODE_AUDIT_EDIT_PERMISSION,
686
+ ),
687
+ bash: mergeOpenCodePermissionRule(
688
+ existingPermission.bash,
689
+ generatedPermission.bash,
690
+ OPENCODE_AUDIT_BASH_PERMISSION,
691
+ ),
692
+ };
693
+ }
694
+
695
+ function assertOpenCodeAuditPermissionConfig(permissionConfig, label) {
696
+ const edit = permissionConfig?.edit;
697
+ const bash = permissionConfig?.bash;
698
+ if (!edit || typeof edit !== 'object' || Array.isArray(edit)) {
699
+ throw new Error(`OpenCode ${label}.edit must allow audit-owned file paths. Run "audit-code install --host opencode".`);
700
+ }
701
+ for (const pattern of ['.audit-code/**', '.audit-artifacts/**', 'audit-report.md']) {
702
+ if (edit[pattern] !== 'allow') {
703
+ throw new Error(`OpenCode ${label}.edit must allow ${pattern}. Run "audit-code install --host opencode".`);
704
+ }
705
+ }
706
+ if (!bash || typeof bash !== 'object' || Array.isArray(bash)) {
707
+ throw new Error(`OpenCode ${label}.bash must allow audit-code commands. Run "audit-code install --host opencode".`);
708
+ }
709
+ for (const pattern of [
710
+ 'audit-code',
711
+ 'audit-code ensure*',
712
+ 'audit-code prepare-dispatch*',
713
+ 'audit-code submit-packet*',
714
+ 'audit-code merge-and-ingest*',
715
+ '*audit-code.mjs',
716
+ '*audit-code.mjs* submit-packet*',
717
+ '*audit-code.mjs* merge-and-ingest*',
718
+ 'node* .audit-code/install/run-mcp-server.mjs*',
719
+ ]) {
720
+ if (bash[pattern] !== 'allow') {
721
+ throw new Error(`OpenCode ${label}.bash must allow ${pattern}. Run "audit-code install --host opencode".`);
722
+ }
723
+ }
724
+ for (const pattern of [
725
+ 'audit-code run-to-completion*',
726
+ 'audit-code synthesize*',
727
+ '*audit-code.mjs* run-to-completion*',
728
+ '*audit-code.mjs* synthesize*',
729
+ ]) {
730
+ if (bash[pattern] !== 'deny') {
731
+ throw new Error(`OpenCode ${label}.bash must deny ${pattern}. Run "audit-code install --host opencode".`);
732
+ }
733
+ }
734
+ }
735
+
610
736
  function buildMergedOpenCodeProjectConfig(existing, root, promptBody) {
611
737
  const generated = renderOpenCodeProjectConfig(root, promptBody);
612
738
  return {
@@ -620,13 +746,17 @@ function buildMergedOpenCodeProjectConfig(existing, root, promptBody) {
620
746
  ...objectValue(existing.mcp),
621
747
  auditor: generated.mcp.auditor,
622
748
  },
623
- permission:
624
- existing.permission && typeof existing.permission === 'object' && !Array.isArray(existing.permission)
625
- ? existing.permission
626
- : generated.permission,
749
+ permission: mergeOpenCodePermissionConfig(existing.permission, generated.permission),
627
750
  agent: {
628
751
  ...objectValue(existing.agent),
629
- auditor: generated.agent.auditor,
752
+ auditor: {
753
+ ...objectValue(objectValue(existing.agent).auditor),
754
+ ...generated.agent.auditor,
755
+ permission: mergeOpenCodePermissionConfig(
756
+ objectValue(objectValue(existing.agent).auditor).permission,
757
+ generated.agent.auditor.permission,
758
+ ),
759
+ },
630
760
  },
631
761
  };
632
762
  }
@@ -1822,8 +1952,10 @@ async function verifyInstalledBootstrap(argv) {
1822
1952
  if (commandConfig.template !== sourceBody.trimStart()) {
1823
1953
  throw new Error('OpenCode config command["audit-code"].template is out of sync with the source prompt. Run "audit-code install".');
1824
1954
  }
1955
+ assertOpenCodeAuditPermissionConfig(config?.permission, 'permission');
1956
+ assertOpenCodeAuditPermissionConfig(config?.agent?.auditor?.permission, 'agent.auditor.permission');
1825
1957
  return {
1826
- summary: 'OpenCode project config has MCP server and /audit-code slash command.',
1958
+ summary: 'OpenCode project config has MCP server, /audit-code slash command, and audit permissions.',
1827
1959
  path: assetPaths.opencodeConfigPath,
1828
1960
  };
1829
1961
  });
@@ -2001,6 +2133,12 @@ async function detectBootstrapRefreshReason(root, host) {
2001
2133
  if (opencodeConfig?.command?.['audit-code']?.template !== sourcePromptBody.trimStart()) {
2002
2134
  return 'stale_host_asset:opencode:config_command';
2003
2135
  }
2136
+ try {
2137
+ assertOpenCodeAuditPermissionConfig(opencodeConfig?.permission, 'permission');
2138
+ assertOpenCodeAuditPermissionConfig(opencodeConfig?.agent?.auditor?.permission, 'agent.auditor.permission');
2139
+ } catch {
2140
+ return 'stale_host_asset:opencode:permissions';
2141
+ }
2004
2142
  if (await fileExists(join(root, '.opencode', 'commands', 'audit-code.md'))) {
2005
2143
  return 'stale_host_asset:opencode:legacy_command_file';
2006
2144
  }
package/dist/cli.js CHANGED
@@ -218,8 +218,8 @@ async function emitEnvelope(params) {
218
218
  }
219
219
  function buildManualReviewBlocker(providerName) {
220
220
  return providerName === LOCAL_SUBPROCESS_PROVIDER_NAME
221
- ? "Ready for LLM review. Dispatched task files are in .audit-artifacts/dispatch/. " +
222
- "Review the code, write audit results to the specified path, then run the worker_command to continue."
221
+ ? "Ready for LLM semantic review. If the host exposes a callable subagent tool, prepare dispatch and fan out packets. " +
222
+ "If not, use single-task fallback: review only the first pending task, write one AuditResult to the run audit-results path, execute worker_command, then stop."
223
223
  : "Audit blocked: waiting for manual audit results or interactive provider configuration.";
224
224
  }
225
225
  function shouldRunInlineExecutor(selectedExecutor) {
@@ -10,6 +10,8 @@ const findingSchemaPath = join(packageRoot, "schemas", "finding.schema.json");
10
10
  const CURRENT_TASK_FILENAME = "current-task.json";
11
11
  const CURRENT_PROMPT_FILENAME = "current-prompt.md";
12
12
  const CURRENT_TASKS_FILENAME = "current-tasks.json";
13
+ const CURRENT_SINGLE_TASK_FILENAME = "current-single-task.json";
14
+ const CURRENT_SINGLE_TASK_PROMPT_FILENAME = "current-single-task-prompt.md";
13
15
  const CURRENT_SCHEMA_FILENAME = "audit-result.schema.json";
14
16
  const CURRENT_RESULTS_SCHEMA_FILENAME = "audit-results.schema.json";
15
17
  const CURRENT_FINDING_SCHEMA_FILENAME = "finding.schema.json";
@@ -63,6 +65,49 @@ async function writeDispatchSchemaFiles(artifactsDir) {
63
65
  await writeFile(join(dispatchDir, CURRENT_RESULTS_SCHEMA_FILENAME), await readFile(auditResultsSchemaPath, "utf8"), "utf8");
64
66
  await writeFile(join(dispatchDir, CURRENT_FINDING_SCHEMA_FILENAME), await readFile(findingSchemaPath, "utf8"), "utf8");
65
67
  }
68
+ function renderSingleTaskFallbackPrompt(task, auditTask) {
69
+ const commandArgv = JSON.stringify(task.worker_command);
70
+ const lineCounts = auditTask.file_paths
71
+ .map((path) => `- ${path}: ${auditTask.file_line_counts?.[path] ?? 0} lines`)
72
+ .join("\n");
73
+ return [
74
+ "# audit-code single-task fallback",
75
+ "",
76
+ "Use this file only when the conversation host cannot dispatch subagents.",
77
+ "This prompt is generated deterministically from the first pending task.",
78
+ "",
79
+ `run_id: ${task.run_id}`,
80
+ `task_id: ${auditTask.task_id}`,
81
+ `unit_id: ${auditTask.unit_id}`,
82
+ `pass_id: ${auditTask.pass_id}`,
83
+ `lens: ${auditTask.lens}`,
84
+ `rationale: ${auditTask.rationale}`,
85
+ "",
86
+ "Assigned files and line counts:",
87
+ lineCounts,
88
+ "",
89
+ "Instructions:",
90
+ "1. Read only the assigned files above.",
91
+ "2. Produce exactly one AuditResult object for task_id above, wrapped in a JSON array.",
92
+ "3. Write that JSON array to audit_results_path.",
93
+ "4. Run worker_command exactly, then stop without checking audit state or reading a report.",
94
+ "",
95
+ `audit_results_path: ${task.audit_results_path}`,
96
+ `worker_command: ${commandArgv}`,
97
+ "",
98
+ ].join("\n");
99
+ }
100
+ async function writeSingleTaskFallbackFiles(artifactsDir, task, currentTasks) {
101
+ if (task.preferred_executor !== "agent" ||
102
+ !task.audit_results_path ||
103
+ !currentTasks ||
104
+ currentTasks.length === 0) {
105
+ return;
106
+ }
107
+ const firstTask = currentTasks[0];
108
+ await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_SINGLE_TASK_FILENAME), firstTask);
109
+ await writeFile(join(artifactsDir, "dispatch", CURRENT_SINGLE_TASK_PROMPT_FILENAME), renderSingleTaskFallbackPrompt(task, firstTask), "utf8");
110
+ }
66
111
  export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, currentTasks, options = {}) {
67
112
  await mkdir(paths.runDir, { recursive: true });
68
113
  await writeJsonFile(paths.taskPath, task);
@@ -78,6 +123,7 @@ export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, cu
78
123
  await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASK_FILENAME), task);
79
124
  await writeFile(join(artifactsDir, "dispatch", CURRENT_PROMPT_FILENAME), prompt, "utf8");
80
125
  await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASKS_FILENAME), currentTasks ?? []);
126
+ await writeSingleTaskFallbackFiles(artifactsDir, task, currentTasks);
81
127
  await writeDispatchSchemaFiles(artifactsDir);
82
128
  }
83
129
  export async function writeDispatchBatchFiles(artifactsDir, runs, currentTasks) {
@@ -121,6 +167,8 @@ export async function clearDispatchFiles(artifactsDir) {
121
167
  CURRENT_TASK_FILENAME,
122
168
  CURRENT_PROMPT_FILENAME,
123
169
  CURRENT_TASKS_FILENAME,
170
+ CURRENT_SINGLE_TASK_FILENAME,
171
+ CURRENT_SINGLE_TASK_PROMPT_FILENAME,
124
172
  CURRENT_SCHEMA_FILENAME,
125
173
  CURRENT_RESULTS_SCHEMA_FILENAME,
126
174
  CURRENT_FINDING_SCHEMA_FILENAME,
@@ -11,6 +11,8 @@ const RUN_LEDGER_FILENAME = "run-ledger.json";
11
11
  const CURRENT_TASK_FILENAME = "current-task.json";
12
12
  const CURRENT_PROMPT_FILENAME = "current-prompt.md";
13
13
  const CURRENT_TASKS_FILENAME = "current-tasks.json";
14
+ const CURRENT_SINGLE_TASK_FILENAME = "current-single-task.json";
15
+ const CURRENT_SINGLE_TASK_PROMPT_FILENAME = "current-single-task-prompt.md";
14
16
  const AUDIT_TASKS_FILENAME = "audit_tasks.json";
15
17
  const RUNTIME_VALIDATION_TASKS_FILENAME = "runtime_validation_tasks.json";
16
18
  const BLOCKED_STATUS = "blocked";
@@ -121,7 +123,7 @@ function buildInteractiveProviderHint(status, providerName, sessionConfigPath, i
121
123
  return `Configuration error: Verify --root points to the intended repository root and that the tree contains auditable files.`;
122
124
  }
123
125
  const providerLabel = providerName ?? LOCAL_SUBPROCESS_PROVIDER_NAME;
124
- return `Provider: ${providerLabel}. For automatic LLM review, configure an interactive provider in ${sessionConfigPath}.`;
126
+ return `Provider: ${providerLabel}. This is a deterministic semantic-review handoff, not a failed audit. Use host subagents when the active toolset provides them; otherwise use the single-task fallback and stop after the worker command. For automatic LLM review, configure an interactive provider in ${sessionConfigPath}; that is only needed for backend-launched review.`;
125
127
  }
126
128
  function renderMarkdown(handoff) {
127
129
  const lines = [
@@ -167,6 +169,9 @@ function renderMarkdown(handoff) {
167
169
  for (const command of handoff.suggested_commands) {
168
170
  lines.push(`- ${command}`);
169
171
  }
172
+ if (handoff.active_review_run) {
173
+ lines.push("- Use packet dispatch commands only when the conversation host exposes a callable subagent tool; otherwise follow the single-task fallback.");
174
+ }
170
175
  }
171
176
  if (handoff.active_review_run) {
172
177
  lines.push("", "Active review run:");
@@ -237,6 +242,8 @@ export function buildAuditCodeHandoff(params) {
237
242
  handoff.file_map = {
238
243
  current_task: artifactPaths.current_task,
239
244
  current_prompt: artifactPaths.current_prompt,
245
+ single_task: join(params.artifactsDir, "dispatch", CURRENT_SINGLE_TASK_FILENAME),
246
+ single_task_prompt: join(params.artifactsDir, "dispatch", CURRENT_SINGLE_TASK_PROMPT_FILENAME),
240
247
  dispatch_plan: join(params.artifactsDir, "runs", params.activeReviewRun.run_id, "dispatch-plan.json"),
241
248
  audit_results: params.activeReviewRun.audit_results_path,
242
249
  final_report: join(params.root, "audit-report.md"),
@@ -57,7 +57,9 @@ ChatGPT-style project conversations are the intended product surface. Use
57
57
  default context.
58
58
 
59
59
  Codex should normally use the global skill seeded by the npm install plus
60
- repo-local `AGENTS.md` fallback guidance.
60
+ repo-local `AGENTS.md` fallback guidance. The installed skill includes
61
+ `agents/openai.yaml` metadata so Codex can keep the slash-list display aligned
62
+ with the canonical `/audit-code` spelling.
61
63
 
62
64
  Claude Desktop is treated as an MCP-first host. Use the generated project
63
65
  template and local bundle artifacts when installing the integration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.19",
3
+ "version": "0.3.20",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url';
7
7
  const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
8
  const promptSourceFile = join(pkgRoot, 'skills', 'audit-code', 'audit-code.prompt.md');
9
9
  const skillSourceFile = join(pkgRoot, 'skills', 'audit-code', 'SKILL.md');
10
+ const codexOpenAiAgentSourceFile = join(pkgRoot, 'skills', 'audit-code', 'agents', 'openai.yaml');
10
11
 
11
12
  function readRequiredSource(path, label) {
12
13
  if (!existsSync(path)) {
@@ -18,6 +19,15 @@ function readRequiredSource(path, label) {
18
19
  return readFileSync(path);
19
20
  }
20
21
 
22
+ function readOptionalSource(path, label) {
23
+ if (!existsSync(path)) {
24
+ console.warn(`audit-code: ${label} source not found at ${path} - skipping optional install`);
25
+ return null;
26
+ }
27
+
28
+ return readFileSync(path);
29
+ }
30
+
21
31
  function writeGeneratedFile(path, content) {
22
32
  const action = existsSync(path) ? 'updated' : 'installed';
23
33
  mkdirSync(dirname(path), { recursive: true });
@@ -32,8 +42,118 @@ function splitFrontmatter(text) {
32
42
  return { body: normalized.slice(match[0].length) };
33
43
  }
34
44
 
45
+ const OPENCODE_AUDIT_EDIT_PERMISSION = {
46
+ '*': 'ask',
47
+ '.audit-code/**': 'allow',
48
+ '.audit-artifacts/**': 'allow',
49
+ 'audit-report.md': 'allow',
50
+ };
51
+
52
+ const OPENCODE_AUDIT_BASH_PERMISSION = {
53
+ '*': 'ask',
54
+ 'audit-code run-to-completion*': 'deny',
55
+ 'audit-code synthesize*': 'deny',
56
+ 'audit-code cleanup*': 'deny',
57
+ 'audit-code requeue*': 'deny',
58
+ 'audit-code ingest-results*': 'deny',
59
+ '*audit-code.mjs* run-to-completion*': 'deny',
60
+ '*audit-code.mjs* synthesize*': 'deny',
61
+ '*audit-code.mjs* cleanup*': 'deny',
62
+ '*audit-code.mjs* requeue*': 'deny',
63
+ '*audit-code.mjs* ingest-results*': 'deny',
64
+ 'audit-code': 'allow',
65
+ 'audit-code ensure*': 'allow',
66
+ 'audit-code prepare-dispatch*': 'allow',
67
+ 'audit-code submit-packet*': 'allow',
68
+ 'audit-code merge-and-ingest*': 'allow',
69
+ 'audit-code validate*': 'allow',
70
+ '*audit-code.mjs': 'allow',
71
+ '*audit-code.mjs* ensure*': 'allow',
72
+ '*audit-code.mjs* prepare-dispatch*': 'allow',
73
+ '*audit-code.mjs* submit-packet*': 'allow',
74
+ '*audit-code.mjs* merge-and-ingest*': 'allow',
75
+ '*audit-code.mjs* worker-run*': 'allow',
76
+ '*audit-code.mjs* validate*': 'allow',
77
+ 'node* .audit-code/install/run-mcp-server.mjs*': 'allow',
78
+ 'node* ./.audit-code/install/run-mcp-server.mjs*': 'allow',
79
+ 'git status*': 'allow',
80
+ 'git diff*': 'allow',
81
+ 'grep *': 'allow',
82
+ 'rm *': 'deny',
83
+ };
84
+
85
+ function objectValue(value) {
86
+ return value && typeof value === 'object' && !Array.isArray(value)
87
+ ? value
88
+ : {};
89
+ }
90
+
91
+ function mergeOpenCodePermissionRule(existingRule, generatedRule, managedRules = {}) {
92
+ if (generatedRule && typeof generatedRule === 'object' && !Array.isArray(generatedRule)) {
93
+ const generatedObject = generatedRule;
94
+ const merged = {};
95
+ const existingObject =
96
+ existingRule && typeof existingRule === 'object' && !Array.isArray(existingRule)
97
+ ? existingRule
98
+ : {};
99
+
100
+ if (typeof existingRule === 'string') {
101
+ merged['*'] = existingRule;
102
+ } else {
103
+ merged['*'] = existingObject['*'] ?? generatedObject['*'] ?? 'ask';
104
+ }
105
+
106
+ for (const [key, value] of Object.entries(generatedObject)) {
107
+ if (key !== '*') merged[key] = value;
108
+ }
109
+ for (const [key, value] of Object.entries(existingObject)) {
110
+ if (key !== '*') merged[key] = value;
111
+ }
112
+ for (const [key, value] of Object.entries(managedRules)) {
113
+ merged[key] = value;
114
+ }
115
+
116
+ return merged;
117
+ }
118
+
119
+ return existingRule ?? generatedRule;
120
+ }
121
+
122
+ function mergeOpenCodePermissionConfig(existingPermission, generatedPermission) {
123
+ if (!existingPermission || typeof existingPermission !== 'object' || Array.isArray(existingPermission)) {
124
+ return generatedPermission;
125
+ }
126
+
127
+ return {
128
+ ...generatedPermission,
129
+ ...existingPermission,
130
+ edit: mergeOpenCodePermissionRule(
131
+ existingPermission.edit,
132
+ generatedPermission.edit,
133
+ OPENCODE_AUDIT_EDIT_PERMISSION,
134
+ ),
135
+ bash: mergeOpenCodePermissionRule(
136
+ existingPermission.bash,
137
+ generatedPermission.bash,
138
+ OPENCODE_AUDIT_BASH_PERMISSION,
139
+ ),
140
+ };
141
+ }
142
+
143
+ function renderOpenCodePermissionConfig() {
144
+ return {
145
+ read: 'allow',
146
+ glob: 'allow',
147
+ grep: 'allow',
148
+ edit: { ...OPENCODE_AUDIT_EDIT_PERMISSION },
149
+ bash: { ...OPENCODE_AUDIT_BASH_PERMISSION },
150
+ };
151
+ }
152
+
35
153
  function mergeOpenCodeGlobalConfig(existing, promptBody) {
36
154
  const parsed = existing ? JSON.parse(existing) : {};
155
+ const auditPermission = renderOpenCodePermissionConfig();
156
+ const existingAuditor = objectValue(objectValue(parsed.agent).auditor);
37
157
  return {
38
158
  ...parsed,
39
159
  command: {
@@ -47,12 +167,18 @@ function mergeOpenCodeGlobalConfig(existing, promptBody) {
47
167
  subtask: false,
48
168
  },
49
169
  },
170
+ permission: mergeOpenCodePermissionConfig(parsed.permission, auditPermission),
50
171
  agent: {
51
172
  ...(parsed.agent && typeof parsed.agent === 'object' && !Array.isArray(parsed.agent)
52
173
  ? parsed.agent
53
174
  : {}),
54
175
  auditor: {
176
+ ...existingAuditor,
55
177
  description: 'Read-heavy audit orchestration agent for the /audit-code workflow.',
178
+ permission: mergeOpenCodePermissionConfig(
179
+ existingAuditor.permission,
180
+ auditPermission,
181
+ ),
56
182
  },
57
183
  },
58
184
  };
@@ -75,23 +201,37 @@ if (!promptSource || !skillSource) {
75
201
  }
76
202
 
77
203
  const promptBody = splitFrontmatter(promptSource.toString('utf8')).body;
204
+ const codexOpenAiAgentSource = readOptionalSource(codexOpenAiAgentSourceFile, 'Codex skill UI metadata');
78
205
 
79
206
  const installs = [
80
207
  {
81
208
  label: 'Claude command',
82
209
  path: join(homedir(), '.claude', 'commands', 'audit-code.md'),
210
+ sourcePath: promptSourceFile,
83
211
  content: promptSource,
84
212
  },
85
213
  {
86
214
  label: 'Codex skill',
87
215
  path: join(homedir(), '.codex', 'skills', 'audit-code', 'SKILL.md'),
216
+ sourcePath: skillSourceFile,
88
217
  content: skillSource,
89
218
  },
90
219
  {
91
220
  label: 'Codex prompt',
92
221
  path: join(homedir(), '.codex', 'skills', 'audit-code', 'audit-code.prompt.md'),
222
+ sourcePath: promptSourceFile,
93
223
  content: promptSource,
94
224
  },
225
+ ...(codexOpenAiAgentSource
226
+ ? [
227
+ {
228
+ label: 'Codex skill UI metadata',
229
+ path: join(homedir(), '.codex', 'skills', 'audit-code', 'agents', 'openai.yaml'),
230
+ sourcePath: codexOpenAiAgentSourceFile,
231
+ content: codexOpenAiAgentSource,
232
+ },
233
+ ]
234
+ : []),
95
235
  ];
96
236
 
97
237
  for (const install of installs) {
@@ -101,7 +241,7 @@ for (const install of installs) {
101
241
  } catch (err) {
102
242
  console.warn(`audit-code: could not install global ${install.label} (${err.message})`);
103
243
  console.warn(` To install manually, copy from:`);
104
- console.warn(` ${install.label === 'Codex skill' ? skillSourceFile : promptSourceFile}`);
244
+ console.warn(` ${install.sourcePath}`);
105
245
  console.warn(` to:`);
106
246
  console.warn(` ${install.path}`);
107
247
  }
@@ -27,6 +27,11 @@ dispatch.
27
27
  If the host cannot delegate to subagents, the conversation orchestrator may
28
28
  complete exactly one assigned review task, ingest it through the provided backend
29
29
  command, then stop so the user can rerun `/audit-code` from fresh context.
30
+ In that fallback path it should not prepare packet dispatch, probe alternate
31
+ backend subcommands, synthesize reports, or choose a smaller task; the first
32
+ pending task and the exact worker command are the boundary.
33
+ The backend writes a deterministic single-task fallback prompt for that case so
34
+ the orchestrator does not need to infer the first task from a broad batch prompt.
30
35
 
31
36
  Subagent fan-out belongs to the host agent runtime rather than to repo-local
32
37
  backend provider settings.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "audit-code"
3
+ short_description: "Run the autonomous /audit-code repository audit workflow."
4
+ default_prompt: "Start /audit-code for this repository and continue until the audit completes or blocks for operator input."
@@ -25,12 +25,21 @@ and ingest results mechanically.
25
25
  a backend command fails and the error explicitly requires diagnosis.
26
26
  - Do not inspect individual subagent result files after dispatch. Validation
27
27
  and ingestion are backend responsibilities.
28
+ - Do not inspect the backend command catalog or try alternate subcommands to
29
+ bypass a blocked semantic-review handoff. In particular, do not run
30
+ `run-to-completion`, `synthesize`, `cleanup`, `requeue`, or direct
31
+ `ingest-results` while following this directive.
32
+ - A report under `.audit-artifacts/` is not a completion signal while
33
+ `audit_state.status` is `"blocked"`. Present a report only after Step 5.
28
34
  - CRITICAL: Do not use your `Read` tool to read `entry.prompt_path` or JSON schemas into your own context window. The subagent will read them. Pass the path literally.
29
35
  - Prefer subagent dispatch for semantic review whenever the host exposes an
30
36
  Agent/subagent tool.
31
37
  - Treat the user's `/audit-code` request as explicit authorization to launch
32
38
  review subagents in parallel. Do not ask for a separate delegation request
33
39
  before using available Agent/subagent tools.
40
+ - Decide subagent support from the active toolset, not from shell commands or
41
+ backend provider names. A shell command named `agent`, an MCP prompt, or a
42
+ `local-subprocess` provider is not a host subagent facility.
34
43
  - Do not use `browser_subagent` for semantic review of source code unless the
35
44
  task explicitly requires browser-based validation.
36
45
  - If the host cannot dispatch subagents, complete exactly one assigned review
@@ -86,7 +95,11 @@ If status is `"blocked"` for semantic review, continue to Step 2.
86
95
 
87
96
  ## Step 2 - Dispatch Review Work
88
97
 
89
- When the host supports subagents, prepare a dispatch plan by default:
98
+ Use this step only when the active toolset exposes a callable host subagent
99
+ facility such as `Agent`, `Task`, or an equivalent built-in delegation tool.
100
+ Do not try to discover subagent support by running shell commands.
101
+
102
+ When that callable subagent facility exists, prepare a dispatch plan by default:
90
103
 
91
104
  ```bash
92
105
  audit-code prepare-dispatch --run-id <run_id> --artifacts-dir <artifacts_dir>
@@ -132,21 +145,43 @@ error. Do not improvise manual merging or state edits.
132
145
 
133
146
  Loop back to Step 1.
134
147
 
148
+ If no callable host subagent facility exists, or a delegation attempt fails
149
+ because the host does not provide such a tool, go directly to Step 3. Do not run
150
+ `prepare-dispatch`, do not inspect generated packet prompts, and do not try
151
+ alternate backend commands.
152
+
135
153
  ## Step 3 - Single-Task Fallback
136
154
 
137
155
  Use this path only when the host cannot dispatch subagents.
138
156
 
139
- Read the current review prompt named by `handoff.active_review_run.prompt_path`
140
- or `.audit-artifacts/dispatch/current-prompt.md`, plus the matching task file
157
+ Allowed backend command in this step: the exact `worker_command` from the task
158
+ file, after you have written the single-task result. Do not run `audit-code`,
159
+ `run-to-completion`, `prepare-dispatch`, `merge-and-ingest`, `synthesize`,
160
+ `validate`, or any other backend command as a substitute for the fallback.
161
+
162
+ Read the generated single-task fallback prompt at
163
+ `handoff.file_map.single_task_prompt` when present, otherwise
164
+ `.audit-artifacts/dispatch/current-single-task-prompt.md`. That file is
165
+ deterministically narrowed to the first pending task. If it is unavailable, read
166
+ the current review prompt named by `handoff.active_review_run.prompt_path` or
167
+ `.audit-artifacts/dispatch/current-prompt.md`, plus the matching task file
141
168
  needed to find `audit_results_path` and `worker_command`.
142
169
 
143
170
  Complete exactly one assigned review task. If a batch file lists multiple tasks,
144
- choose the first pending task only. Read only that task's assigned files. Write
145
- one valid `AuditResult` object, wrapped in a JSON array, to `audit_results_path`.
171
+ choose the first pending task by array order only; do not substitute a smaller
172
+ or easier task. If that first task covers a large file, use targeted reads and
173
+ searches within its assigned files instead of abandoning it. Read only that
174
+ task's assigned files. Write one valid `AuditResult` object, wrapped in a JSON
175
+ array, to `audit_results_path`.
176
+
177
+ If the current review prompt says to produce results for every listed task, the
178
+ single-task fallback overrides that wording for the top-level orchestrator:
179
+ produce exactly one result for the first pending task only.
146
180
 
147
181
  Run the exact `worker_command` from the task file. Then stop and summarize that
148
182
  one bounded step. Do not loop into another semantic review task in the same
149
- conversation turn.
183
+ conversation turn. Do not re-check audit state or read an audit report after the
184
+ worker command.
150
185
 
151
186
  ## Step 4 - Backend Failure Handling
152
187