auditor-lambda 0.3.18 → 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.
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { buildFileDisposition } from "../extractors/disposition.js";
2
+ import { buildFileDisposition, isAuditExcludedStatus, } from "../extractors/disposition.js";
3
3
  import { buildGraphBundle, buildGraphBundleFromFs, } from "../extractors/graph.js";
4
4
  import { buildCriticalFlowManifest } from "../extractors/flows.js";
5
5
  import { buildRiskRegister } from "../extractors/risk.js";
@@ -123,6 +123,10 @@ export async function runIntakeExecutor(bundle, root) {
123
123
  hash_files: false,
124
124
  });
125
125
  const disposition = buildFileDisposition(repoManifest);
126
+ const auditableCount = disposition.files.filter((file) => !isAuditExcludedStatus(file.status)).length;
127
+ if (auditableCount === 0) {
128
+ throw new Error(`No auditable files found in ${root}. The repository may be empty, generated-only, documentation-only, or filtered by .auditorignore.`);
129
+ }
126
130
  return {
127
131
  updated: {
128
132
  ...bundle,
@@ -140,7 +144,6 @@ export async function runStructureExecutor(bundle, root) {
140
144
  const externalAnalyzerResults = bundle.external_analyzer_results;
141
145
  const disposition = bundle.file_disposition ?? buildFileDisposition(bundle.repo_manifest);
142
146
  const unitManifest = buildUnitManifest(bundle.repo_manifest, disposition);
143
- const surfaceManifest = buildSurfaceManifest(bundle.repo_manifest, disposition);
144
147
  const graphBundle = root
145
148
  ? await buildGraphBundleFromFs(bundle.repo_manifest, root, disposition, {
146
149
  externalAnalyzerResults,
@@ -148,6 +151,7 @@ export async function runStructureExecutor(bundle, root) {
148
151
  : buildGraphBundle(bundle.repo_manifest, disposition, {
149
152
  externalAnalyzerResults,
150
153
  });
154
+ const surfaceManifest = buildSurfaceManifest(bundle.repo_manifest, disposition, { graphBundle });
151
155
  const criticalFlows = buildCriticalFlowManifest(bundle.repo_manifest, surfaceManifest, disposition);
152
156
  const riskRegister = buildRiskRegister(unitManifest, criticalFlows, externalAnalyzerResults);
153
157
  return {
@@ -1,5 +1,6 @@
1
1
  import { applyUnitCoverage, createCoverageMatrix, markExcludedPath, } from "../coverage.js";
2
2
  import { isAuditExcludedStatus } from "../extractors/disposition.js";
3
+ import { hasBrowserExtensionManifestFile } from "../extractors/browserExtension.js";
3
4
  import { deriveRequiredLensesForPath } from "./unitBuilder.js";
4
5
  const CATEGORY_LENS_TABLE = [
5
6
  [["security", "secret"], ["security", "correctness"]],
@@ -47,6 +48,7 @@ function applyAnalyzerCoverage(coverage, externalAnalyzerResults) {
47
48
  }
48
49
  export function initializeCoverageFromPlan(repoManifest, unitManifest, disposition, externalAnalyzerResults) {
49
50
  const coverage = createCoverageMatrix(repoManifest.files.map((file) => file.path));
51
+ const isBrowserExtensionProject = hasBrowserExtensionManifestFile(repoManifest);
50
52
  const dispositionMap = new Map(disposition.files.map((item) => [item.path, item.status]));
51
53
  for (const file of repoManifest.files) {
52
54
  const status = dispositionMap.get(file.path);
@@ -66,7 +68,9 @@ export function initializeCoverageFromPlan(repoManifest, unitManifest, dispositi
66
68
  }
67
69
  for (const file of repoManifest.files) {
68
70
  const unitIds = unitIdsByPath.get(file.path) ?? [];
69
- const requiredLenses = deriveRequiredLensesForPath(file.path);
71
+ const requiredLenses = deriveRequiredLensesForPath(file.path, {
72
+ isBrowserExtensionProject,
73
+ });
70
74
  for (const unitId of unitIds) {
71
75
  applyUnitCoverage(coverage, file.path, unitId, requiredLenses);
72
76
  }
@@ -15,6 +15,14 @@ const ESLINT_CONFIG_FILES = [
15
15
  ".eslintrc.yml",
16
16
  ".eslintrc.yaml",
17
17
  ];
18
+ const TSCONFIG_FILES = [
19
+ "tsconfig.json",
20
+ "tsconfig.build.json",
21
+ "jsconfig.json",
22
+ ];
23
+ function hasTypeScriptConfig(root) {
24
+ return TSCONFIG_FILES.some((file) => existsSync(join(root, file)));
25
+ }
18
26
  function hasEslintConfig(root) {
19
27
  if (ESLINT_CONFIG_FILES.some((file) => existsSync(join(root, file)))) {
20
28
  return true;
@@ -109,7 +117,8 @@ function runEslint(root) {
109
117
  }
110
118
  export function runSyntaxResolutionExecutor(bundle, root) {
111
119
  const items = [];
112
- if (bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts"))) {
120
+ if (hasTypeScriptConfig(root) &&
121
+ bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts"))) {
113
122
  items.push(...runTsc(root));
114
123
  }
115
124
  if (bundle.file_disposition?.files.some((f) => f.path.endsWith(".ts") || f.path.endsWith(".js"))) {
@@ -1,5 +1,7 @@
1
1
  import type { Lens, RepoManifest, UnitManifest } from "../types.js";
2
2
  import type { FileDisposition } from "../types/disposition.js";
3
3
  export declare const LENS_ORDER: Lens[];
4
- export declare function deriveRequiredLensesForPath(path: string): Lens[];
4
+ export declare function deriveRequiredLensesForPath(path: string, options?: {
5
+ isBrowserExtensionProject?: boolean;
6
+ }): Lens[];
5
7
  export declare function buildUnitManifest(repoManifest: RepoManifest, disposition?: FileDisposition): UnitManifest;
@@ -1,3 +1,4 @@
1
+ import { deriveBrowserExtensionLensesForPath, hasBrowserExtensionManifestFile, inferBrowserExtensionUnitKind, } from "../extractors/browserExtension.js";
1
2
  import { bucketFile } from "../extractors/bucketing.js";
2
3
  import { isAuditExcludedStatus } from "../extractors/disposition.js";
3
4
  import { pathTokens, normalizeExtractorPath } from "../extractors/pathPatterns.js";
@@ -14,7 +15,13 @@ const LENS_MAP = {
14
15
  generated_vendor: ["maintainability"],
15
16
  unknown: ["correctness"],
16
17
  };
17
- function inferUnitKind(path) {
18
+ function inferUnitKind(path, isBrowserExtensionProject = false) {
19
+ if (isBrowserExtensionProject) {
20
+ const extensionKind = inferBrowserExtensionUnitKind(path);
21
+ if (extensionKind) {
22
+ return extensionKind;
23
+ }
24
+ }
18
25
  const normalized = path.toLowerCase();
19
26
  if (normalized.startsWith("apps/") || normalized.startsWith("services/"))
20
27
  return "service";
@@ -94,7 +101,7 @@ function applyExtensionLensGuards(path, lenses) {
94
101
  }
95
102
  return lenses;
96
103
  }
97
- export function deriveRequiredLensesForPath(path) {
104
+ export function deriveRequiredLensesForPath(path, options = {}) {
98
105
  const assignment = bucketFile(path);
99
106
  const required = new Set();
100
107
  for (const bucket of assignment.buckets) {
@@ -102,6 +109,11 @@ export function deriveRequiredLensesForPath(path) {
102
109
  required.add(lens);
103
110
  }
104
111
  }
112
+ if (options.isBrowserExtensionProject) {
113
+ for (const lens of deriveBrowserExtensionLensesForPath(path)) {
114
+ required.add(lens);
115
+ }
116
+ }
105
117
  return applyExtensionLensGuards(path, sortLenses(required));
106
118
  }
107
119
  function inferCriticalFlows(files, requiredLenses) {
@@ -128,13 +140,14 @@ function inferCriticalFlows(files, requiredLenses) {
128
140
  }
129
141
  export function buildUnitManifest(repoManifest, disposition) {
130
142
  const units = new Map();
143
+ const isBrowserExtensionProject = hasBrowserExtensionManifestFile(repoManifest);
131
144
  const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
132
145
  for (const file of repoManifest.files) {
133
146
  const status = dispositionMap.get(file.path);
134
147
  if (file.excluded || (status && isAuditExcludedStatus(status))) {
135
148
  continue;
136
149
  }
137
- const kind = inferUnitKind(file.path);
150
+ const kind = inferUnitKind(file.path, isBrowserExtensionProject);
138
151
  const unitId = inferUnitId(file.path, kind);
139
152
  const existing = units.get(unitId) ?? {
140
153
  unit_id: unitId,
@@ -148,7 +161,9 @@ export function buildUnitManifest(repoManifest, disposition) {
148
161
  }
149
162
  const assignment = bucketFile(file.path);
150
163
  const required = new Set(existing.required_lenses);
151
- for (const lens of deriveRequiredLensesForPath(file.path)) {
164
+ for (const lens of deriveRequiredLensesForPath(file.path, {
165
+ isBrowserExtensionProject,
166
+ })) {
152
167
  required.add(lens);
153
168
  }
154
169
  existing.required_lenses = sortLenses(required);
@@ -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";
@@ -118,10 +120,10 @@ function buildInteractiveProviderHint(status, providerName, sessionConfigPath, i
118
120
  return null;
119
121
  }
120
122
  if (isConfigError) {
121
- return `Configuration error: Verify --root points to a repository root (with package.json, go.mod, etc.).`;
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.18",
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