auditor-lambda 0.3.19 → 0.3.21

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.
@@ -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,
@@ -12,6 +12,7 @@ export function renderWorkerPrompt(task) {
12
12
  `Read: ${tasksPath}`,
13
13
  "Scope: review only the tasks listed in the Read file. Do not add tasks,",
14
14
  "edit source files, remediate findings, run unrelated audits, or write result_path.",
15
+ "Prefer host Read and Grep tools for source inspection. On native Windows, do not use Unix pipelines like `grep ... | head`; if shell search is unavoidable, use `Select-String` as a fallback.",
15
16
  "For each listed task: read the assigned file_paths under the specified lens,",
16
17
  "using targeted reads/searches where they give complete enough evidence without loading unrelated context,",
17
18
  "and emit exactly one AuditResult object with:",
@@ -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";
@@ -103,9 +105,7 @@ function buildSuggestedCommands(artifactsDir, suggestedInputs, status, activeRev
103
105
  return [
104
106
  renderShellCommand([
105
107
  "audit-code",
106
- "prepare-dispatch",
107
- "--run-id",
108
- activeReviewRun.run_id,
108
+ "next-step",
109
109
  "--artifacts-dir",
110
110
  artifactsDir,
111
111
  ]),
@@ -121,7 +121,7 @@ function buildInteractiveProviderHint(status, providerName, sessionConfigPath, i
121
121
  return `Configuration error: Verify --root points to the intended repository root and that the tree contains auditable files.`;
122
122
  }
123
123
  const providerLabel = providerName ?? LOCAL_SUBPROCESS_PROVIDER_NAME;
124
- return `Provider: ${providerLabel}. For automatic LLM review, configure an interactive provider in ${sessionConfigPath}.`;
124
+ 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
125
  }
126
126
  function renderMarkdown(handoff) {
127
127
  const lines = [
@@ -167,6 +167,9 @@ function renderMarkdown(handoff) {
167
167
  for (const command of handoff.suggested_commands) {
168
168
  lines.push(`- ${command}`);
169
169
  }
170
+ if (handoff.active_review_run) {
171
+ lines.push("- Use next-step so the backend renders either packet dispatch or single-task fallback after the host reports capabilities.");
172
+ }
170
173
  }
171
174
  if (handoff.active_review_run) {
172
175
  lines.push("", "Active review run:");
@@ -228,15 +231,15 @@ export function buildAuditCodeHandoff(params) {
228
231
  if (params.state.status === BLOCKED_STATUS && params.activeReviewRun) {
229
232
  handoff.quick_start = renderShellCommand([
230
233
  "audit-code",
231
- "prepare-dispatch",
232
- "--run-id",
233
- params.activeReviewRun.run_id,
234
+ "next-step",
234
235
  "--artifacts-dir",
235
236
  params.artifactsDir,
236
237
  ]);
237
238
  handoff.file_map = {
238
239
  current_task: artifactPaths.current_task,
239
240
  current_prompt: artifactPaths.current_prompt,
241
+ single_task: join(params.artifactsDir, "dispatch", CURRENT_SINGLE_TASK_FILENAME),
242
+ single_task_prompt: join(params.artifactsDir, "dispatch", CURRENT_SINGLE_TASK_PROMPT_FILENAME),
240
243
  dispatch_plan: join(params.artifactsDir, "runs", params.activeReviewRun.run_id, "dispatch-plan.json"),
241
244
  audit_results: params.activeReviewRun.audit_results_path,
242
245
  final_report: join(params.root, "audit-report.md"),
package/docs/contracts.md CHANGED
@@ -77,6 +77,23 @@ The backend stores resumable artifacts under `.audit-artifacts/`, including:
77
77
  Consumers should treat these as versioned JSON artifacts and validate them with
78
78
  `audit-code validate` rather than inferring state from filenames alone.
79
79
 
80
+ ## Step artifacts
81
+
82
+ The conversation-first `/audit-code` prompt is a loader. It runs
83
+ `audit-code next-step` and then follows only the returned step prompt. The
84
+ backend writes the current step contract to:
85
+
86
+ - `<artifacts_dir>/steps/current-step.json`
87
+ - `<artifacts_dir>/steps/current-prompt.md`
88
+
89
+ `current-step.json` uses `contract_version: "audit-code-step/v1alpha1"` and
90
+ includes `step_kind`, `prompt_path`, `status`, `run_id`, `allowed_commands`,
91
+ `stop_condition`, `repo_root`, `artifacts_dir`, and relevant `artifact_paths`.
92
+
93
+ When semantic review is blocked, `next-step` first emits a `capability_check`.
94
+ After the host reports `--host-can-dispatch-subagents true|false`, the backend
95
+ renders exactly one review path: packet dispatch or the single-task fallback.
96
+
80
97
  ## Dispatch packets
81
98
 
82
99
  Packet dispatch preserves the existing `AuditTask` and `AuditResult`
@@ -92,13 +109,18 @@ Planning artifacts are shaped by:
92
109
  Normal packet flow:
93
110
 
94
111
  ```text
95
- audit-code prepare-dispatch --run-id <run_id> --artifacts-dir <artifacts_dir>
112
+ audit-code next-step --host-can-dispatch-subagents true
113
+ backend prepares dispatch-plan.json
96
114
  conversation launches one worker per dispatch-plan entry
97
115
  worker reads entry.prompt_path
98
116
  worker submits AuditResult[] through submit-packet
99
117
  audit-code merge-and-ingest --run-id <run_id> --artifacts-dir <artifacts_dir>
100
118
  ```
101
119
 
120
+ `audit-code prepare-dispatch --run-id <run_id> --artifacts-dir
121
+ <artifacts_dir>` remains available for compatibility and tests, but generic
122
+ handoff fields point users and prompts to `next-step`.
123
+
102
124
  Packet artifacts:
103
125
 
104
126
  - `<artifacts_dir>/runs/<run_id>/dispatch-plan.json`
@@ -43,7 +43,7 @@ Host-specific files may include:
43
43
 
44
44
  - Codex: managed `AGENTS.md` fallback guidance
45
45
  - Claude Desktop: project template, remote MCP connector, local MCP bundle
46
- - OpenCode: `opencode.json` with `/audit-code` slash command and auditor MCP server
46
+ - OpenCode: `opencode.json` with auditor MCP server and permission wiring; the `/audit-code` command is global npm-installed state
47
47
  - VS Code/Copilot: prompt, custom agent, instructions, and `.vscode/mcp.json`
48
48
  - Antigravity: planning-mode and MCP-oriented guidance
49
49
 
@@ -57,14 +57,17 @@ 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.
64
66
 
65
- OpenCode uses `opencode.json` (generated by `audit-code ensure` or `audit-code
66
- install`) which registers the `/audit-code` slash command and the auditor MCP
67
- server together. VS Code uses repo-local prompt and MCP configuration files.
67
+ OpenCode uses the global command seeded by `npm install -g auditor-lambda`.
68
+ The generated project `opencode.json` should not define `command["audit-code"]`;
69
+ it only wires the auditor MCP server and project permissions. VS Code uses
70
+ repo-local prompt and MCP configuration files.
68
71
 
69
72
  Antigravity should be treated as a workflow-and-artifacts host until it has a
70
73
  stable project-local config surface. Use generated planning-mode guidance,
@@ -98,6 +101,7 @@ The wrapper:
98
101
  Useful fallback commands:
99
102
 
100
103
  ```bash
104
+ audit-code next-step
101
105
  audit-code --single-step
102
106
  audit-code --results /path/to/audit_results.json
103
107
  audit-code --batch-results /path/to/results-dir
@@ -109,6 +113,11 @@ audit-code cleanup
109
113
  audit-code mcp
110
114
  ```
111
115
 
116
+ `audit-code next-step` is the backend-rendered step engine used by the
117
+ conversation prompt. It writes `.audit-artifacts/steps/current-step.json` and
118
+ `.audit-artifacts/steps/current-prompt.md`, then the host should follow only
119
+ that prompt.
120
+
112
121
  `audit-code validate` checks artifact shape, cross-artifact consistency,
113
122
  session config, and explicit provider readiness.
114
123
 
package/docs/product.md CHANGED
@@ -148,9 +148,10 @@ Readiness should be judged through three checks:
148
148
  - field-trial quality: run real repositories through planning, validate
149
149
  artifacts, and use `audit_plan_metrics.json` to track packet count, weak
150
150
  packet count, average cohesion, merge edge kinds, and weak-packet samples
151
- - full-loop behavior: prove `prepare-dispatch`, worker review,
152
- `submit-packet`, `merge-and-ingest`, selective deepening, runtime validation,
153
- and final `audit-report.md` promotion in at least one real host flow
151
+ - full-loop behavior: prove `next-step` capability routing, packet dispatch,
152
+ worker review, `submit-packet`, `merge-and-ingest`, selective deepening,
153
+ runtime validation, and final `audit-report.md` promotion in at least one
154
+ real host flow
154
155
  - release hygiene: keep `npm run verify:release`, linked smoke, packaged
155
156
  smoke, tarball preview, and Trusted Publishing green from a clean checkout
156
157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
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,151 @@ 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
+ '*dist*index.js* run-to-completion*': 'deny',
60
+ '*dist*index.js* synthesize*': 'deny',
61
+ '*dist*index.js* cleanup*': 'deny',
62
+ '*dist*index.js* requeue*': 'deny',
63
+ '*dist*index.js* ingest-results*': 'deny',
64
+ '*audit-code.mjs* run-to-completion*': 'deny',
65
+ '*audit-code.mjs* synthesize*': 'deny',
66
+ '*audit-code.mjs* cleanup*': 'deny',
67
+ '*audit-code.mjs* requeue*': 'deny',
68
+ '*audit-code.mjs* ingest-results*': 'deny',
69
+ 'audit-code': 'allow',
70
+ 'audit-code ensure*': 'allow',
71
+ 'audit-code next-step*': 'allow',
72
+ 'audit-code prepare-dispatch*': 'allow',
73
+ 'audit-code submit-packet*': 'allow',
74
+ 'audit-code merge-and-ingest*': 'allow',
75
+ 'audit-code validate*': 'allow',
76
+ '*audit-code.mjs': 'allow',
77
+ '*audit-code.mjs* ensure*': 'allow',
78
+ '*audit-code.mjs* next-step*': 'allow',
79
+ '*audit-code.mjs* prepare-dispatch*': 'allow',
80
+ '*audit-code.mjs* submit-packet*': 'allow',
81
+ '*audit-code.mjs* merge-and-ingest*': 'allow',
82
+ '*audit-code.mjs* worker-run*': 'allow',
83
+ '*audit-code.mjs* validate*': 'allow',
84
+ '*node* *auditor-lambda*dist*index.js* worker-run*': 'allow',
85
+ 'node* .audit-code/install/run-mcp-server.mjs*': 'allow',
86
+ 'node* ./.audit-code/install/run-mcp-server.mjs*': 'allow',
87
+ 'git status*': 'allow',
88
+ 'git diff*': 'allow',
89
+ 'grep *': 'allow',
90
+ 'Select-String *': 'allow',
91
+ 'rm *': 'deny',
92
+ };
93
+
94
+ function replaceBackslashes(value) {
95
+ return value.replace(/\\/g, '/');
96
+ }
97
+
98
+ function externalDirectoryPattern(path) {
99
+ return `${replaceBackslashes(path).replace(/\/+$/u, '')}/**`;
100
+ }
101
+
102
+ function renderOpenCodeExternalDirectoryPermission() {
103
+ return {
104
+ [externalDirectoryPattern(pkgRoot)]: 'allow',
105
+ [externalDirectoryPattern(dirname(process.execPath))]: 'allow',
106
+ };
107
+ }
108
+
109
+ function objectValue(value) {
110
+ return value && typeof value === 'object' && !Array.isArray(value)
111
+ ? value
112
+ : {};
113
+ }
114
+
115
+ function mergeOpenCodePermissionRule(existingRule, generatedRule, managedRules = {}) {
116
+ if (generatedRule && typeof generatedRule === 'object' && !Array.isArray(generatedRule)) {
117
+ const generatedObject = generatedRule;
118
+ const merged = {};
119
+ const existingObject =
120
+ existingRule && typeof existingRule === 'object' && !Array.isArray(existingRule)
121
+ ? existingRule
122
+ : {};
123
+
124
+ if (typeof existingRule === 'string') {
125
+ merged['*'] = existingRule;
126
+ } else {
127
+ merged['*'] = existingObject['*'] ?? generatedObject['*'] ?? 'ask';
128
+ }
129
+
130
+ for (const [key, value] of Object.entries(generatedObject)) {
131
+ if (key !== '*') merged[key] = value;
132
+ }
133
+ for (const [key, value] of Object.entries(existingObject)) {
134
+ if (key !== '*') merged[key] = value;
135
+ }
136
+ for (const [key, value] of Object.entries(managedRules)) {
137
+ merged[key] = value;
138
+ }
139
+
140
+ return merged;
141
+ }
142
+
143
+ return existingRule ?? generatedRule;
144
+ }
145
+
146
+ function mergeOpenCodePermissionConfig(existingPermission, generatedPermission) {
147
+ if (!existingPermission || typeof existingPermission !== 'object' || Array.isArray(existingPermission)) {
148
+ return generatedPermission;
149
+ }
150
+
151
+ return {
152
+ ...generatedPermission,
153
+ ...existingPermission,
154
+ read: generatedPermission.read,
155
+ glob: generatedPermission.glob,
156
+ grep: generatedPermission.grep,
157
+ external_directory: mergeOpenCodePermissionRule(
158
+ existingPermission.external_directory,
159
+ generatedPermission.external_directory,
160
+ generatedPermission.external_directory,
161
+ ),
162
+ edit: mergeOpenCodePermissionRule(
163
+ existingPermission.edit,
164
+ generatedPermission.edit,
165
+ OPENCODE_AUDIT_EDIT_PERMISSION,
166
+ ),
167
+ bash: mergeOpenCodePermissionRule(
168
+ existingPermission.bash,
169
+ generatedPermission.bash,
170
+ OPENCODE_AUDIT_BASH_PERMISSION,
171
+ ),
172
+ };
173
+ }
174
+
175
+ function renderOpenCodePermissionConfig() {
176
+ return {
177
+ read: 'allow',
178
+ glob: 'allow',
179
+ grep: 'allow',
180
+ external_directory: renderOpenCodeExternalDirectoryPermission(),
181
+ edit: { ...OPENCODE_AUDIT_EDIT_PERMISSION },
182
+ bash: { ...OPENCODE_AUDIT_BASH_PERMISSION },
183
+ };
184
+ }
185
+
35
186
  function mergeOpenCodeGlobalConfig(existing, promptBody) {
36
187
  const parsed = existing ? JSON.parse(existing) : {};
188
+ const auditPermission = renderOpenCodePermissionConfig();
189
+ const existingAuditor = objectValue(objectValue(parsed.agent).auditor);
37
190
  return {
38
191
  ...parsed,
39
192
  command: {
@@ -47,12 +200,18 @@ function mergeOpenCodeGlobalConfig(existing, promptBody) {
47
200
  subtask: false,
48
201
  },
49
202
  },
203
+ permission: mergeOpenCodePermissionConfig(parsed.permission, auditPermission),
50
204
  agent: {
51
205
  ...(parsed.agent && typeof parsed.agent === 'object' && !Array.isArray(parsed.agent)
52
206
  ? parsed.agent
53
207
  : {}),
54
208
  auditor: {
209
+ ...existingAuditor,
55
210
  description: 'Read-heavy audit orchestration agent for the /audit-code workflow.',
211
+ permission: mergeOpenCodePermissionConfig(
212
+ existingAuditor.permission,
213
+ auditPermission,
214
+ ),
56
215
  },
57
216
  },
58
217
  };
@@ -75,23 +234,37 @@ if (!promptSource || !skillSource) {
75
234
  }
76
235
 
77
236
  const promptBody = splitFrontmatter(promptSource.toString('utf8')).body;
237
+ const codexOpenAiAgentSource = readOptionalSource(codexOpenAiAgentSourceFile, 'Codex skill UI metadata');
78
238
 
79
239
  const installs = [
80
240
  {
81
241
  label: 'Claude command',
82
242
  path: join(homedir(), '.claude', 'commands', 'audit-code.md'),
243
+ sourcePath: promptSourceFile,
83
244
  content: promptSource,
84
245
  },
85
246
  {
86
247
  label: 'Codex skill',
87
248
  path: join(homedir(), '.codex', 'skills', 'audit-code', 'SKILL.md'),
249
+ sourcePath: skillSourceFile,
88
250
  content: skillSource,
89
251
  },
90
252
  {
91
253
  label: 'Codex prompt',
92
254
  path: join(homedir(), '.codex', 'skills', 'audit-code', 'audit-code.prompt.md'),
255
+ sourcePath: promptSourceFile,
93
256
  content: promptSource,
94
257
  },
258
+ ...(codexOpenAiAgentSource
259
+ ? [
260
+ {
261
+ label: 'Codex skill UI metadata',
262
+ path: join(homedir(), '.codex', 'skills', 'audit-code', 'agents', 'openai.yaml'),
263
+ sourcePath: codexOpenAiAgentSourceFile,
264
+ content: codexOpenAiAgentSource,
265
+ },
266
+ ]
267
+ : []),
95
268
  ];
96
269
 
97
270
  for (const install of installs) {
@@ -101,7 +274,7 @@ for (const install of installs) {
101
274
  } catch (err) {
102
275
  console.warn(`audit-code: could not install global ${install.label} (${err.message})`);
103
276
  console.warn(` To install manually, copy from:`);
104
- console.warn(` ${install.label === 'Codex skill' ? skillSourceFile : promptSourceFile}`);
277
+ console.warn(` ${install.sourcePath}`);
105
278
  console.warn(` to:`);
106
279
  console.warn(` ${install.path}`);
107
280
  }
@@ -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."