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.
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:
@@ -50,11 +51,11 @@ The explicit repair and compatibility setup path remains:
50
51
  audit-code install
51
52
  ```
52
53
 
53
- That bootstraps repo-local `/audit-code` surfaces for the hosts we can automate today, including:
54
+ That bootstraps repo-local supporting surfaces for the hosts we can automate today, including:
54
55
 
55
56
  - Codex `AGENTS.md` fallback guidance for the global skill surface
56
57
  - Claude Desktop local MCP bundle artifacts and project template guidance
57
- - OpenCode `opencode.json` with the `/audit-code` slash command and auditor MCP server
58
+ - OpenCode `opencode.json` with auditor MCP server and permission wiring; the `/audit-code` command stays in the global npm-installed OpenCode config
58
59
  - VS Code prompt, custom agent, Copilot instructions, and `.vscode/mcp.json`
59
60
  - Antigravity planning-mode guidance plus the shared repo-local MCP launcher
60
61
 
@@ -129,6 +130,16 @@ For one bounded debug step instead of run-to-completion:
129
130
  audit-code --single-step
130
131
  ```
131
132
 
133
+ For the conversation step engine used by `/audit-code`:
134
+
135
+ ```bash
136
+ audit-code next-step
137
+ ```
138
+
139
+ This writes `.audit-artifacts/steps/current-step.json` and
140
+ `.audit-artifacts/steps/current-prompt.md`; hosts should follow only the
141
+ returned step prompt.
142
+
132
143
  For an operator-side artifact consistency check:
133
144
 
134
145
  ```bash
@@ -258,6 +258,7 @@ function printHelp({ usageName, preferredEntrypoint }) {
258
258
  '- verify-install smoke-tests the generated host assets and repo-local MCP launchers after install',
259
259
  '- mcp starts the local stdio MCP server for repo-local IDE integrations',
260
260
  '- install-host --host copilot keeps the narrower Copilot-focused install path available',
261
+ '- next-step advances deterministic audit state and writes .audit-artifacts/steps/current-step.json plus current-prompt.md',
261
262
  '- validate checks the current artifact bundle plus session-config/provider readiness and exits non-zero when issues exist',
262
263
  '- validate-results --results FILE validates AuditResult payloads against the active task manifest without ingesting them',
263
264
  '- explain-task <task_id> prints the resolved file coverage and current status for a task id',
@@ -533,18 +534,82 @@ function renderCodexAutomationRecipe() {
533
534
  ].join('\n');
534
535
  }
535
536
 
536
- function renderOpenCodeProjectConfig(root, promptBody) {
537
+ const OPENCODE_AUDIT_EDIT_PERMISSION = {
538
+ '*': 'ask',
539
+ '.audit-code/**': 'allow',
540
+ '.audit-artifacts/**': 'allow',
541
+ 'audit-report.md': 'allow',
542
+ };
543
+
544
+ const OPENCODE_AUDIT_BASH_PERMISSION = {
545
+ '*': 'ask',
546
+ 'audit-code run-to-completion*': 'deny',
547
+ 'audit-code synthesize*': 'deny',
548
+ 'audit-code cleanup*': 'deny',
549
+ 'audit-code requeue*': 'deny',
550
+ 'audit-code ingest-results*': 'deny',
551
+ '*dist*index.js* run-to-completion*': 'deny',
552
+ '*dist*index.js* synthesize*': 'deny',
553
+ '*dist*index.js* cleanup*': 'deny',
554
+ '*dist*index.js* requeue*': 'deny',
555
+ '*dist*index.js* ingest-results*': 'deny',
556
+ '*audit-code.mjs* run-to-completion*': 'deny',
557
+ '*audit-code.mjs* synthesize*': 'deny',
558
+ '*audit-code.mjs* cleanup*': 'deny',
559
+ '*audit-code.mjs* requeue*': 'deny',
560
+ '*audit-code.mjs* ingest-results*': 'deny',
561
+ 'audit-code': 'allow',
562
+ 'audit-code ensure*': 'allow',
563
+ 'audit-code next-step*': 'allow',
564
+ 'audit-code prepare-dispatch*': 'allow',
565
+ 'audit-code submit-packet*': 'allow',
566
+ 'audit-code merge-and-ingest*': 'allow',
567
+ 'audit-code validate*': 'allow',
568
+ '*audit-code.mjs': 'allow',
569
+ '*audit-code.mjs* ensure*': 'allow',
570
+ '*audit-code.mjs* next-step*': 'allow',
571
+ '*audit-code.mjs* prepare-dispatch*': 'allow',
572
+ '*audit-code.mjs* submit-packet*': 'allow',
573
+ '*audit-code.mjs* merge-and-ingest*': 'allow',
574
+ '*audit-code.mjs* worker-run*': 'allow',
575
+ '*audit-code.mjs* validate*': 'allow',
576
+ '*node* *auditor-lambda*dist*index.js* worker-run*': 'allow',
577
+ 'node* .audit-code/install/run-mcp-server.mjs*': 'allow',
578
+ 'node* ./.audit-code/install/run-mcp-server.mjs*': 'allow',
579
+ 'git status*': 'allow',
580
+ 'git diff*': 'allow',
581
+ 'grep *': 'allow',
582
+ 'Select-String *': 'allow',
583
+ 'rm *': 'deny',
584
+ };
585
+
586
+ function externalDirectoryPattern(path) {
587
+ return `${replaceBackslashes(path).replace(/\/+$/u, '')}/**`;
588
+ }
589
+
590
+ function renderOpenCodeExternalDirectoryPermission() {
591
+ return {
592
+ [externalDirectoryPattern(repoRoot)]: 'allow',
593
+ [externalDirectoryPattern(dirname(process.execPath))]: 'allow',
594
+ };
595
+ }
596
+
597
+ function renderOpenCodePermissionConfig() {
598
+ return {
599
+ read: 'allow',
600
+ glob: 'allow',
601
+ grep: 'allow',
602
+ external_directory: renderOpenCodeExternalDirectoryPermission(),
603
+ edit: { ...OPENCODE_AUDIT_EDIT_PERMISSION },
604
+ bash: { ...OPENCODE_AUDIT_BASH_PERMISSION },
605
+ };
606
+ }
607
+
608
+ function renderOpenCodeProjectConfig(root) {
537
609
  const launcher = replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)));
610
+ const auditPermission = renderOpenCodePermissionConfig();
538
611
  return {
539
612
  $schema: 'https://opencode.ai/config.json',
540
- command: {
541
- 'audit-code': {
542
- template: promptBody.trimStart(),
543
- description: 'Autonomous local loop code auditing',
544
- agent: 'auditor',
545
- subtask: false,
546
- },
547
- },
548
613
  mcp: {
549
614
  auditor: {
550
615
  type: 'local',
@@ -553,19 +618,7 @@ function renderOpenCodeProjectConfig(root, promptBody) {
553
618
  timeout: 10000,
554
619
  },
555
620
  },
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
- },
621
+ permission: auditPermission,
569
622
  agent: {
570
623
  auditor: {
571
624
  description:
@@ -574,14 +627,7 @@ function renderOpenCodeProjectConfig(root, promptBody) {
574
627
  'auditor*': true,
575
628
  },
576
629
  permission: {
577
- edit: 'ask',
578
- bash: {
579
- '*': 'ask',
580
- 'git status*': 'allow',
581
- 'git diff*': 'allow',
582
- 'grep *': 'allow',
583
- 'rm *': 'deny',
584
- },
630
+ ...auditPermission,
585
631
  question: 'allow',
586
632
  },
587
633
  },
@@ -607,26 +653,164 @@ function objectValue(value) {
607
653
  : {};
608
654
  }
609
655
 
610
- function buildMergedOpenCodeProjectConfig(existing, root, promptBody) {
611
- const generated = renderOpenCodeProjectConfig(root, promptBody);
656
+ function mergeOpenCodePermissionRule(existingRule, generatedRule, managedRules = {}) {
657
+ if (generatedRule && typeof generatedRule === 'object' && !Array.isArray(generatedRule)) {
658
+ const generatedObject = generatedRule;
659
+ const merged = {};
660
+ const existingObject =
661
+ existingRule && typeof existingRule === 'object' && !Array.isArray(existingRule)
662
+ ? existingRule
663
+ : {};
664
+
665
+ if (typeof existingRule === 'string') {
666
+ merged['*'] = existingRule;
667
+ } else {
668
+ merged['*'] = existingObject['*'] ?? generatedObject['*'] ?? 'ask';
669
+ }
670
+
671
+ for (const [key, value] of Object.entries(generatedObject)) {
672
+ if (key !== '*') merged[key] = value;
673
+ }
674
+ for (const [key, value] of Object.entries(existingObject)) {
675
+ if (key !== '*') merged[key] = value;
676
+ }
677
+ for (const [key, value] of Object.entries(managedRules)) {
678
+ merged[key] = value;
679
+ }
680
+
681
+ return merged;
682
+ }
683
+
684
+ return existingRule ?? generatedRule;
685
+ }
686
+
687
+ function mergeOpenCodePermissionConfig(existingPermission, generatedPermission) {
688
+ if (!existingPermission || typeof existingPermission !== 'object' || Array.isArray(existingPermission)) {
689
+ return generatedPermission;
690
+ }
691
+
692
+ return {
693
+ ...generatedPermission,
694
+ ...existingPermission,
695
+ read: generatedPermission.read,
696
+ glob: generatedPermission.glob,
697
+ grep: generatedPermission.grep,
698
+ external_directory: mergeOpenCodePermissionRule(
699
+ existingPermission.external_directory,
700
+ generatedPermission.external_directory,
701
+ generatedPermission.external_directory,
702
+ ),
703
+ edit: mergeOpenCodePermissionRule(
704
+ existingPermission.edit,
705
+ generatedPermission.edit,
706
+ OPENCODE_AUDIT_EDIT_PERMISSION,
707
+ ),
708
+ bash: mergeOpenCodePermissionRule(
709
+ existingPermission.bash,
710
+ generatedPermission.bash,
711
+ OPENCODE_AUDIT_BASH_PERMISSION,
712
+ ),
713
+ };
714
+ }
715
+
716
+ function removeManagedOpenCodeCommand(commandConfig) {
717
+ const command = objectValue(commandConfig);
718
+ const { 'audit-code': _managedAuditCodeCommand, ...remaining } = command;
719
+ return remaining;
720
+ }
721
+
722
+ function assertOpenCodeAuditPermissionConfig(permissionConfig, label) {
723
+ for (const tool of ['read', 'glob', 'grep']) {
724
+ if (permissionConfig?.[tool] !== 'allow') {
725
+ throw new Error(`OpenCode ${label}.${tool} must be allow. Run "audit-code install --host opencode".`);
726
+ }
727
+ }
728
+ const externalDirectory = permissionConfig?.external_directory;
729
+ if (!externalDirectory || typeof externalDirectory !== 'object' || Array.isArray(externalDirectory)) {
730
+ throw new Error(`OpenCode ${label}.external_directory must allow audit package paths. Run "audit-code install --host opencode".`);
731
+ }
732
+ for (const pattern of Object.keys(renderOpenCodeExternalDirectoryPermission())) {
733
+ if (externalDirectory[pattern] !== 'allow') {
734
+ throw new Error(`OpenCode ${label}.external_directory must allow ${pattern}. Run "audit-code install --host opencode".`);
735
+ }
736
+ }
737
+ const edit = permissionConfig?.edit;
738
+ const bash = permissionConfig?.bash;
739
+ if (!edit || typeof edit !== 'object' || Array.isArray(edit)) {
740
+ throw new Error(`OpenCode ${label}.edit must allow audit-owned file paths. Run "audit-code install --host opencode".`);
741
+ }
742
+ for (const pattern of ['.audit-code/**', '.audit-artifacts/**', 'audit-report.md']) {
743
+ if (edit[pattern] !== 'allow') {
744
+ throw new Error(`OpenCode ${label}.edit must allow ${pattern}. Run "audit-code install --host opencode".`);
745
+ }
746
+ }
747
+ if (!bash || typeof bash !== 'object' || Array.isArray(bash)) {
748
+ throw new Error(`OpenCode ${label}.bash must allow audit-code commands. Run "audit-code install --host opencode".`);
749
+ }
750
+ for (const pattern of [
751
+ 'audit-code',
752
+ 'audit-code ensure*',
753
+ 'audit-code next-step*',
754
+ 'audit-code prepare-dispatch*',
755
+ 'audit-code submit-packet*',
756
+ 'audit-code merge-and-ingest*',
757
+ '*audit-code.mjs',
758
+ '*audit-code.mjs* next-step*',
759
+ '*audit-code.mjs* submit-packet*',
760
+ '*audit-code.mjs* merge-and-ingest*',
761
+ '*audit-code.mjs* worker-run*',
762
+ '*node* *auditor-lambda*dist*index.js* worker-run*',
763
+ 'node* .audit-code/install/run-mcp-server.mjs*',
764
+ 'Select-String *',
765
+ ]) {
766
+ if (bash[pattern] !== 'allow') {
767
+ throw new Error(`OpenCode ${label}.bash must allow ${pattern}. Run "audit-code install --host opencode".`);
768
+ }
769
+ }
770
+ for (const pattern of [
771
+ 'audit-code run-to-completion*',
772
+ 'audit-code synthesize*',
773
+ 'audit-code cleanup*',
774
+ 'audit-code requeue*',
775
+ 'audit-code ingest-results*',
776
+ '*dist*index.js* run-to-completion*',
777
+ '*dist*index.js* synthesize*',
778
+ '*dist*index.js* cleanup*',
779
+ '*dist*index.js* requeue*',
780
+ '*dist*index.js* ingest-results*',
781
+ '*audit-code.mjs* run-to-completion*',
782
+ '*audit-code.mjs* synthesize*',
783
+ '*audit-code.mjs* cleanup*',
784
+ '*audit-code.mjs* requeue*',
785
+ '*audit-code.mjs* ingest-results*',
786
+ ]) {
787
+ if (bash[pattern] !== 'deny') {
788
+ throw new Error(`OpenCode ${label}.bash must deny ${pattern}. Run "audit-code install --host opencode".`);
789
+ }
790
+ }
791
+ }
792
+
793
+ function buildMergedOpenCodeProjectConfig(existing, root) {
794
+ const generated = renderOpenCodeProjectConfig(root);
612
795
  return {
613
796
  ...existing,
614
797
  $schema: existing.$schema ?? generated.$schema,
615
- command: {
616
- ...objectValue(existing.command),
617
- 'audit-code': generated.command['audit-code'],
618
- },
798
+ command: removeManagedOpenCodeCommand(existing.command),
619
799
  mcp: {
620
800
  ...objectValue(existing.mcp),
621
801
  auditor: generated.mcp.auditor,
622
802
  },
623
- permission:
624
- existing.permission && typeof existing.permission === 'object' && !Array.isArray(existing.permission)
625
- ? existing.permission
626
- : generated.permission,
803
+ permission: mergeOpenCodePermissionConfig(existing.permission, generated.permission),
627
804
  agent: {
628
805
  ...objectValue(existing.agent),
629
- auditor: generated.agent.auditor,
806
+ auditor: {
807
+ ...objectValue(objectValue(existing.agent).auditor),
808
+ ...generated.agent.auditor,
809
+ permission: mergeOpenCodePermissionConfig(
810
+ objectValue(objectValue(existing.agent).auditor).permission,
811
+ generated.agent.auditor.permission,
812
+ ),
813
+ },
630
814
  },
631
815
  };
632
816
  }
@@ -1094,9 +1278,9 @@ const INSTALL_HOST_DEFINITIONS = {
1094
1278
  host: 'opencode',
1095
1279
  label: 'OpenCode',
1096
1280
  support_level: 'supported',
1097
- setup_kind: 'command+agent+mcp',
1281
+ setup_kind: 'global-command+project-mcp',
1098
1282
  summary:
1099
- 'Use the generated `opencode.json` so the `/audit-code` slash command and the local auditor MCP server are both available.',
1283
+ 'Use the global OpenCode `/audit-code` command installed by npm plus generated project MCP and permission wiring.',
1100
1284
  primary_path_key: 'opencodeConfigPath',
1101
1285
  supporting_path_keys: [
1102
1286
  'agentsInstructionsPath',
@@ -1104,8 +1288,8 @@ const INSTALL_HOST_DEFINITIONS = {
1104
1288
  ],
1105
1289
  steps: [
1106
1290
  'Open this repository in OpenCode.',
1107
- 'Let OpenCode load the generated `opencode.json` — it registers the `/audit-code` slash command and the auditor MCP server.',
1108
- 'Invoke `/audit-code` and keep the audit loop on the auditor MCP tools.',
1291
+ 'Use the global `/audit-code` command installed by `npm install -g auditor-lambda`.',
1292
+ 'Let OpenCode load the generated `opencode.json` for the auditor MCP server and project permissions only.',
1109
1293
  ],
1110
1294
  profile: {
1111
1295
  writeOpenCode: true,
@@ -1814,16 +1998,13 @@ async function verifyInstalledBootstrap(argv) {
1814
1998
  if (config?.mcp?.auditor?.type !== 'local') {
1815
1999
  throw new Error(`OpenCode config must set mcp.auditor.type to "local", got ${config?.mcp?.auditor?.type ?? 'missing'}.`);
1816
2000
  }
1817
- const commandConfig = config?.command?.['audit-code'];
1818
- if (!commandConfig?.template) {
1819
- throw new Error('OpenCode config is missing command["audit-code"].template — the /audit-code slash command will not surface. Run "audit-code install".');
1820
- }
1821
- const { body: sourceBody } = splitFrontmatter(await readFile(promptAssetPath, 'utf8'));
1822
- if (commandConfig.template !== sourceBody.trimStart()) {
1823
- throw new Error('OpenCode config command["audit-code"].template is out of sync with the source prompt. Run "audit-code install".');
2001
+ if (config?.command?.['audit-code']) {
2002
+ throw new Error('OpenCode project config must not define command["audit-code"]; the slash command is global npm-installed state. Run "audit-code install --host opencode" to remove the stale local command.');
1824
2003
  }
2004
+ assertOpenCodeAuditPermissionConfig(config?.permission, 'permission');
2005
+ assertOpenCodeAuditPermissionConfig(config?.agent?.auditor?.permission, 'agent.auditor.permission');
1825
2006
  return {
1826
- summary: 'OpenCode project config has MCP server and /audit-code slash command.',
2007
+ summary: 'OpenCode project config has MCP server and audit permissions; /audit-code is supplied by the global npm-installed OpenCode command.',
1827
2008
  path: assetPaths.opencodeConfigPath,
1828
2009
  };
1829
2010
  });
@@ -1998,8 +2179,14 @@ async function detectBootstrapRefreshReason(root, host) {
1998
2179
  }
1999
2180
  case 'opencode': {
2000
2181
  const opencodeConfig = await readJson(assetPaths.opencodeConfigPath, 'OpenCode config').catch(() => null);
2001
- if (opencodeConfig?.command?.['audit-code']?.template !== sourcePromptBody.trimStart()) {
2002
- return 'stale_host_asset:opencode:config_command';
2182
+ if (opencodeConfig?.command?.['audit-code']) {
2183
+ return 'stale_host_asset:opencode:local_command';
2184
+ }
2185
+ try {
2186
+ assertOpenCodeAuditPermissionConfig(opencodeConfig?.permission, 'permission');
2187
+ assertOpenCodeAuditPermissionConfig(opencodeConfig?.agent?.auditor?.permission, 'agent.auditor.permission');
2188
+ } catch {
2189
+ return 'stale_host_asset:opencode:permissions';
2003
2190
  }
2004
2191
  if (await fileExists(join(root, '.opencode', 'commands', 'audit-code.md'))) {
2005
2192
  return 'stale_host_asset:opencode:legacy_command_file';
@@ -2260,7 +2447,7 @@ async function installBootstrap(argv, options = {}) {
2260
2447
  await writeMergedGeneratedJson(
2261
2448
  assetPaths.opencodeConfigPath,
2262
2449
  'OpenCode project config',
2263
- (existing) => buildMergedOpenCodeProjectConfig(existing, root, promptBody),
2450
+ (existing) => buildMergedOpenCodeProjectConfig(existing, root),
2264
2451
  ),
2265
2452
  );
2266
2453
  }
@@ -2483,6 +2670,11 @@ export async function runAuditCodeWrapper({
2483
2670
  return;
2484
2671
  }
2485
2672
 
2673
+ if (argv[0] === 'next-step') {
2674
+ await runDistCommand('next-step', argv.slice(1), { ensureArtifactsDir: true });
2675
+ return;
2676
+ }
2677
+
2486
2678
  if (argv[0] === 'prepare-dispatch') {
2487
2679
  await runDistCommand('prepare-dispatch', argv.slice(1));
2488
2680
  return;