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 +3 -2
- package/audit-code-wrapper-lib.mjs +165 -27
- package/dist/cli.js +2 -2
- package/dist/io/runArtifacts.js +48 -0
- package/dist/supervisor/operatorHandoff.js +8 -1
- package/docs/operator-guide.md +3 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +141 -1
- package/skills/audit-code/SKILL.md +5 -0
- package/skills/audit-code/agents/openai.yaml +4 -0
- package/skills/audit-code/audit-code.prompt.md +41 -6
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
|
|
34
|
-
slash command entry in
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
222
|
-
"
|
|
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) {
|
package/dist/io/runArtifacts.js
CHANGED
|
@@ -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"),
|
package/docs/operator-guide.md
CHANGED
|
@@ -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
package/scripts/postinstall.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
145
|
-
|
|
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
|
|