auditor-lambda 0.2.6 → 0.2.9

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.
Files changed (125) hide show
  1. package/README.md +29 -7
  2. package/audit-code-wrapper-lib.mjs +1605 -330
  3. package/dist/adapters/eslint.js +9 -5
  4. package/dist/cli.d.ts +42 -1
  5. package/dist/cli.js +192 -80
  6. package/dist/coverage.d.ts +2 -2
  7. package/dist/coverage.js +5 -5
  8. package/dist/extractors/bucketing.d.ts +4 -0
  9. package/dist/extractors/bucketing.js +6 -2
  10. package/dist/extractors/disposition.d.ts +4 -0
  11. package/dist/extractors/disposition.js +15 -2
  12. package/dist/extractors/fileInventory.js +24 -28
  13. package/dist/extractors/flows.d.ts +5 -0
  14. package/dist/extractors/flows.js +25 -39
  15. package/dist/extractors/pathPatterns.d.ts +13 -3
  16. package/dist/extractors/pathPatterns.js +116 -53
  17. package/dist/extractors/risk.js +7 -1
  18. package/dist/extractors/surfaces.d.ts +4 -0
  19. package/dist/extractors/surfaces.js +11 -11
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/io/artifacts.d.ts +59 -44
  23. package/dist/io/artifacts.js +80 -120
  24. package/dist/io/json.d.ts +2 -0
  25. package/dist/io/json.js +65 -19
  26. package/dist/io/runArtifacts.d.ts +2 -1
  27. package/dist/io/runArtifacts.js +44 -7
  28. package/dist/mcp/server.d.ts +1 -0
  29. package/dist/mcp/server.js +579 -0
  30. package/dist/orchestrator/advance.js +84 -56
  31. package/dist/orchestrator/dependencyMap.js +9 -13
  32. package/dist/orchestrator/executors.js +7 -2
  33. package/dist/orchestrator/flowCoverage.js +11 -5
  34. package/dist/orchestrator/flowPlanning.d.ts +7 -2
  35. package/dist/orchestrator/flowPlanning.js +46 -21
  36. package/dist/orchestrator/flowRequeue.js +29 -9
  37. package/dist/orchestrator/internalExecutors.d.ts +2 -1
  38. package/dist/orchestrator/internalExecutors.js +130 -69
  39. package/dist/orchestrator/planning.js +25 -3
  40. package/dist/orchestrator/requeue.js +20 -5
  41. package/dist/orchestrator/resultIngestion.js +5 -6
  42. package/dist/orchestrator/runtimeValidation.d.ts +7 -2
  43. package/dist/orchestrator/runtimeValidation.js +61 -49
  44. package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
  45. package/dist/orchestrator/state.js +18 -13
  46. package/dist/orchestrator/taskBuilder.d.ts +4 -2
  47. package/dist/orchestrator/taskBuilder.js +153 -52
  48. package/dist/orchestrator/trivialAudit.js +8 -5
  49. package/dist/orchestrator/unitBuilder.d.ts +3 -1
  50. package/dist/orchestrator/unitBuilder.js +24 -16
  51. package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
  52. package/dist/prompts/renderWorkerPrompt.js +19 -10
  53. package/dist/providers/claudeCodeProvider.d.ts +4 -1
  54. package/dist/providers/claudeCodeProvider.js +8 -5
  55. package/dist/providers/localSubprocessProvider.d.ts +4 -0
  56. package/dist/providers/localSubprocessProvider.js +7 -2
  57. package/dist/providers/spawnLoggedCommand.d.ts +9 -1
  58. package/dist/providers/spawnLoggedCommand.js +77 -29
  59. package/dist/reporting/mergeFindings.js +0 -11
  60. package/dist/reporting/synthesis.d.ts +26 -21
  61. package/dist/reporting/synthesis.js +97 -61
  62. package/dist/reporting/workBlocks.d.ts +12 -3
  63. package/dist/reporting/workBlocks.js +124 -70
  64. package/dist/supervisor/operatorHandoff.js +48 -18
  65. package/dist/supervisor/runLedger.d.ts +1 -1
  66. package/dist/supervisor/runLedger.js +112 -5
  67. package/dist/supervisor/sessionConfig.js +10 -10
  68. package/dist/types/externalAnalyzer.d.ts +3 -0
  69. package/dist/types/flowCoverage.d.ts +5 -1
  70. package/dist/types/flowCoverage.js +5 -1
  71. package/dist/types/flows.d.ts +6 -0
  72. package/dist/types/flows.js +1 -1
  73. package/dist/types/runLedger.d.ts +5 -1
  74. package/dist/types/runLedger.js +6 -1
  75. package/dist/types/runtimeValidation.d.ts +13 -3
  76. package/dist/types/runtimeValidation.js +16 -1
  77. package/dist/types/sessionConfig.d.ts +15 -2
  78. package/dist/types/sessionConfig.js +15 -1
  79. package/dist/types/surfaces.d.ts +4 -1
  80. package/dist/types/surfaces.js +1 -1
  81. package/dist/types/workerSession.d.ts +9 -0
  82. package/dist/types/workerSession.js +5 -1
  83. package/dist/types.d.ts +4 -7
  84. package/dist/validation/artifacts.d.ts +1 -1
  85. package/dist/validation/artifacts.js +33 -20
  86. package/dist/validation/auditResults.d.ts +2 -2
  87. package/dist/validation/auditResults.js +71 -114
  88. package/dist/validation/basic.d.ts +9 -1
  89. package/dist/validation/basic.js +40 -3
  90. package/dist/validation/sessionConfig.d.ts +4 -2
  91. package/dist/validation/sessionConfig.js +62 -15
  92. package/docs/agent-integrations.md +67 -38
  93. package/docs/artifacts.md +16 -56
  94. package/docs/bootstrap-install.md +60 -30
  95. package/docs/contract.md +22 -205
  96. package/docs/next-steps.md +76 -44
  97. package/docs/packaging.md +27 -3
  98. package/docs/product-direction.md +22 -0
  99. package/docs/production-launch-bar.md +4 -2
  100. package/docs/production-readiness.md +9 -5
  101. package/docs/releasing.md +98 -0
  102. package/docs/remediation-baseline.md +75 -0
  103. package/docs/run-flow.md +23 -11
  104. package/docs/session-config.md +50 -5
  105. package/docs/supervisor.md +7 -0
  106. package/docs/workflow-refactor-brief.md +177 -0
  107. package/package.json +4 -1
  108. package/schemas/audit_result.schema.json +8 -7
  109. package/schemas/audit_task.schema.json +3 -1
  110. package/schemas/coverage_matrix.schema.json +3 -3
  111. package/schemas/critical_flows.schema.json +6 -2
  112. package/schemas/file_disposition.schema.json +2 -2
  113. package/schemas/finding.schema.json +9 -4
  114. package/schemas/flow_coverage.schema.json +2 -2
  115. package/schemas/repo_manifest.schema.json +4 -4
  116. package/schemas/risk_register.schema.json +2 -2
  117. package/schemas/runtime_validation_report.schema.json +3 -3
  118. package/schemas/runtime_validation_tasks.schema.json +8 -2
  119. package/schemas/surface_manifest.schema.json +6 -3
  120. package/schemas/unit_manifest.schema.json +3 -2
  121. package/skills/audit-code/SKILL.md +16 -2
  122. package/skills/audit-code/audit-code.prompt.md +5 -8
  123. package/schemas/merged_findings.schema.json +0 -19
  124. package/schemas/root_cause_clusters.schema.json +0 -28
  125. package/schemas/synthesis_report.schema.json +0 -61
@@ -1,4 +1,4 @@
1
- import { access, mkdir, open, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
1
+ import { access, cp, mkdir, open, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { dirname, join, relative, resolve } from 'node:path';
@@ -18,9 +18,13 @@ const BUILD_LOCK_WAIT_INTERVAL_MS = 200;
18
18
  const INSTALL_MARKER_START = '<!-- audit-code:begin -->';
19
19
  const INSTALL_MARKER_END = '<!-- audit-code:end -->';
20
20
  const INSTALL_GUIDE_FILENAME = 'GETTING-STARTED.md';
21
+ const INSTALL_MANIFEST_FILENAME = 'manifest.json';
21
22
  const DEFAULT_INSTALL_HOST = 'all';
22
23
  const INSTALLED_PROMPT_FILENAME = 'audit-code.import.md';
24
+ const MCP_LAUNCHER_FILENAME = 'run-mcp-server.mjs';
23
25
  const packageVersion = JSON.parse(await readFile(packageJsonPath, 'utf8')).version;
26
+ const MCP_PROTOCOL_VERSION = '2025-06-18';
27
+ const INSTALL_VERIFY_TIMEOUT_MS = 10_000;
24
28
 
25
29
  function hasFlag(argv, name) {
26
30
  return argv.includes(name);
@@ -239,8 +243,11 @@ function printHelp({ usageName, preferredEntrypoint }) {
239
243
  'Helper commands:',
240
244
  '- prompt-path prints the absolute path to the canonical /audit-code prompt asset',
241
245
  '- install bootstraps /audit-code into supported repo-local host surfaces',
246
+ '- verify-install smoke-tests the generated host assets and repo-local MCP launchers after install',
247
+ '- mcp starts the local stdio MCP server for repo-local IDE integrations',
242
248
  '- install-host --host copilot keeps the narrower Copilot-focused install path available',
243
249
  '- validate checks the current artifact bundle plus session-config/provider readiness and exits non-zero when issues exist',
250
+ '- validate-results --results FILE validates AuditResult payloads against the active task manifest without ingesting them',
244
251
  '- explain-task <task_id> prints the resolved file coverage and current status for a task id',
245
252
  '',
246
253
  'Primary usage:',
@@ -334,132 +341,698 @@ function buildInstallDirective(relativePromptPath) {
334
341
  ].join('\n');
335
342
  }
336
343
 
337
- function buildInstallHostGuidance({
338
- installedPromptPath,
339
- slashCommandSurfaces,
340
- instructionSurfaces,
341
- }) {
342
- const guidance = [];
344
+ function upsertManagedBlock(existingContent, blockContent) {
345
+ const normalized = normalizeNewlines(existingContent);
346
+ const blockPattern = new RegExp(
347
+ `${INSTALL_MARKER_START}[\\s\\S]*?${INSTALL_MARKER_END}`,
348
+ 'u',
349
+ );
343
350
 
344
- if (slashCommandSurfaces.vscode_prompt) {
345
- guidance.push({
346
- host: 'vscode',
347
- label: 'VS Code',
348
- support_level: 'supported',
349
- setup_kind: 'repo-local-slash-command',
350
- summary:
351
- 'Use the generated VS Code / Copilot prompt surface, then invoke `/audit-code` in chat.',
352
- primary_path: slashCommandSurfaces.vscode_prompt,
353
- supporting_paths: [
354
- instructionSurfaces.copilot_instructions,
355
- instructionSurfaces.agents,
356
- ].filter(Boolean),
357
- steps: [
358
- 'Open this repository in VS Code or GitHub Copilot Chat.',
359
- 'Invoke `/audit-code` in chat.',
360
- 'Use the integrated terminal and run `audit-code` only when you intentionally need the repo-local backend fallback.',
361
- ],
362
- });
351
+ if (blockPattern.test(normalized)) {
352
+ return normalized.replace(blockPattern, blockContent);
363
353
  }
364
354
 
365
- if (slashCommandSurfaces.opencode_command) {
366
- guidance.push({
367
- host: 'opencode',
368
- label: 'OpenCode',
369
- support_level: 'supported',
370
- setup_kind: 'repo-local-slash-command',
371
- summary:
372
- 'Use the generated OpenCode command surface so `/audit-code` is available without extra provider flags.',
373
- primary_path: slashCommandSurfaces.opencode_command,
374
- supporting_paths: [instructionSurfaces.agents].filter(Boolean),
375
- steps: [
376
- 'Open this repository in OpenCode.',
377
- 'Invoke `/audit-code` from the OpenCode command surface.',
378
- 'Use the repo-local backend wrapper only when you intentionally need the fallback automation path.',
379
- ],
380
- });
355
+ if (normalized.trim().length === 0) {
356
+ return `${blockContent}\n`;
381
357
  }
382
358
 
383
- if (slashCommandSurfaces.claude_code_command) {
384
- guidance.push({
385
- host: 'claude-code',
386
- label: 'Claude Code',
387
- support_level: 'supported',
388
- setup_kind: 'repo-local-slash-command',
389
- summary:
390
- 'Use the generated Claude Code command surface so `/audit-code` is available inside the repository without extra provider wiring.',
391
- primary_path: slashCommandSurfaces.claude_code_command,
392
- supporting_paths: [instructionSurfaces.claude].filter(Boolean),
393
- steps: [
394
- 'Open this repository in Claude Code.',
395
- 'Invoke `/audit-code` from the Claude Code project command surface.',
396
- 'Use the terminal fallback and run `audit-code` only when you intentionally need the repo-local backend wrapper.',
397
- ],
398
- });
399
- }
359
+ return `${normalized.replace(/\s+$/u, '')}\n\n${blockContent}\n`;
360
+ }
400
361
 
401
- guidance.push({
402
- host: 'claude-desktop',
403
- label: 'Claude Desktop',
404
- support_level: 'manual-import',
405
- setup_kind: 'prompt-import',
406
- summary:
407
- 'No verified project-local slash-command surface is shipped for Claude Desktop, so use the installed prompt asset as the primary path.',
408
- primary_path: installedPromptPath,
409
- supporting_paths: [instructionSurfaces.claude].filter(Boolean),
410
- steps: [
411
- 'Import the installed prompt asset into Claude Desktop\'s prompt or instruction surface.',
412
- 'Invoke `/audit-code` conversationally inside Claude Desktop after the prompt is available.',
413
- 'If you intentionally need the repo-local backend fallback instead, run `audit-code` from the repository root.',
414
- ],
415
- });
362
+ async function writeManagedMarkdown(targetPath, blockContent) {
363
+ const existed = await fileExists(targetPath);
364
+ const existingContent = existed ? await readFile(targetPath, 'utf8') : '';
365
+ const nextContent = upsertManagedBlock(existingContent, blockContent);
366
+ await mkdir(dirname(targetPath), { recursive: true });
367
+ await writeFile(targetPath, nextContent, 'utf8');
368
+ return {
369
+ path: targetPath,
370
+ mode: existed ? 'updated' : 'created',
371
+ };
372
+ }
416
373
 
417
- guidance.push({
418
- host: 'antigravity',
419
- label: 'Antigravity',
420
- support_level: 'manual-import',
421
- setup_kind: 'prompt-import-or-terminal',
422
- summary:
423
- 'No verified repo-local slash-command surface is shipped for Antigravity, so start from the installed prompt asset or an Antigravity-managed terminal.',
424
- primary_path: installedPromptPath,
425
- supporting_paths: [instructionSurfaces.agents].filter(Boolean),
426
- steps: [
427
- 'Import the installed prompt asset into Antigravity\'s prompt or instruction surface when that surface is available.',
428
- 'Invoke `/audit-code` conversationally inside Antigravity.',
429
- 'If you prefer the backend fallback, run `audit-code` from an Antigravity-managed terminal with `local-subprocess` first.',
430
- ],
431
- });
374
+ async function writeGeneratedMarkdown(targetPath, content) {
375
+ const existed = await fileExists(targetPath);
376
+ await mkdir(dirname(targetPath), { recursive: true });
377
+ await writeFile(targetPath, content, 'utf8');
378
+ return {
379
+ path: targetPath,
380
+ mode: existed ? 'updated' : 'created',
381
+ };
382
+ }
432
383
 
433
- return guidance;
384
+ async function writeGeneratedJson(targetPath, value) {
385
+ const existed = await fileExists(targetPath);
386
+ await mkdir(dirname(targetPath), { recursive: true });
387
+ await writeFile(targetPath, JSON.stringify(value, null, 2) + '\n', 'utf8');
388
+ return {
389
+ path: targetPath,
390
+ mode: existed ? 'updated' : 'created',
391
+ };
434
392
  }
435
393
 
436
- function renderInstallHostSection(root, guide) {
437
- const lines = [
438
- `## ${guide.label}`,
394
+ async function writeGeneratedBinary(targetPath, content) {
395
+ const existed = await fileExists(targetPath);
396
+ await mkdir(dirname(targetPath), { recursive: true });
397
+ await writeFile(targetPath, content);
398
+ return {
399
+ path: targetPath,
400
+ mode: existed ? 'updated' : 'created',
401
+ };
402
+ }
403
+
404
+ function replaceBackslashes(value) {
405
+ return value.replace(/\\/g, '/');
406
+ }
407
+
408
+ function renderVSCodeAgentFile() {
409
+ return [
410
+ '---',
411
+ 'description: Plan and orchestrate /audit-code with the installed auditor MCP server before making code changes.',
412
+ '---',
439
413
  '',
440
- `Support level: ${guide.support_level}`,
441
- `Setup kind: ${guide.setup_kind}`,
414
+ '# Auditor Agent',
442
415
  '',
443
- guide.summary,
416
+ 'Use the installed auditor MCP server as the primary integration surface for the audit workflow.',
444
417
  '',
445
- 'Primary repo-local path:',
446
- `- \`${toRepoRelativePath(root, guide.primary_path)}\``,
447
- ];
418
+ 'When the user asks to run or continue `/audit-code`:',
419
+ '',
420
+ '- call the `start_audit`, `get_status`, and `continue_audit` MCP tools instead of reconstructing backend state manually',
421
+ '- read `audit-code://handoff/current` and `audit-code://artifacts/current` when the audit blocks or you need current context',
422
+ '- prefer imported audit results and runtime updates over ad hoc manual state edits',
423
+ '- treat the deterministic audit report as the final source of truth once the audit completes',
424
+ '',
425
+ ].join('\n');
426
+ }
427
+
428
+ function renderCodexMcpSetupGuide(root) {
429
+ return [
430
+ '# Codex MCP setup',
431
+ '',
432
+ 'Codex shares MCP configuration between the app, CLI, and IDE extension. Register a local stdio MCP server named `auditor` that launches the repo-local wrapper below.',
433
+ '',
434
+ 'Recommended command:',
435
+ `- command: \`node\``,
436
+ `- args: \`${toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME))}\``,
437
+ '',
438
+ 'Equivalent config shape:',
439
+ '',
440
+ '```toml',
441
+ '[mcp_servers.auditor]',
442
+ 'command = "node"',
443
+ `args = ["${replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)))}"]`,
444
+ '```',
445
+ '',
446
+ 'Once the server is registered, ask Codex to use the `auditor` MCP tools to start or continue the audit.',
447
+ '',
448
+ ].join('\n');
449
+ }
450
+
451
+ function renderCodexAutomationRecipe() {
452
+ return [
453
+ '# Codex re-audit automation recipe',
454
+ '',
455
+ 'Suggested recurring task:',
456
+ '',
457
+ '- Prompt: Re-run the autonomous audit workflow for this repository. Use the installed auditor MCP tools, summarize only new or regressed findings, and stop once the deterministic report is current.',
458
+ '- Cadence: daily on active branches or before release cut-offs',
459
+ '- Inputs: repository root plus the installed `auditor` MCP server',
460
+ '',
461
+ 'Use this recipe as a starting point for a Codex automation once the local workflow is stable in your environment.',
462
+ '',
463
+ ].join('\n');
464
+ }
465
+
466
+ function renderOpenCodeProjectConfig(root) {
467
+ const launcher = replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)));
468
+ return JSON.stringify(
469
+ {
470
+ $schema: 'https://opencode.ai/config.json',
471
+ mcp: {
472
+ auditor: {
473
+ type: 'local',
474
+ command: ['node', launcher],
475
+ enabled: true,
476
+ timeout: 10000,
477
+ },
478
+ },
479
+ permission: {
480
+ read: 'allow',
481
+ glob: 'allow',
482
+ grep: 'allow',
483
+ edit: 'ask',
484
+ bash: {
485
+ '*': 'ask',
486
+ 'git status*': 'allow',
487
+ 'git diff*': 'allow',
488
+ 'grep *': 'allow',
489
+ 'rm *': 'deny',
490
+ },
491
+ },
492
+ agent: {
493
+ auditor: {
494
+ description:
495
+ 'Read-heavy audit orchestration agent for the /audit-code workflow.',
496
+ tools: {
497
+ 'auditor*': true,
498
+ },
499
+ permission: {
500
+ edit: 'ask',
501
+ bash: {
502
+ '*': 'ask',
503
+ 'git status*': 'allow',
504
+ 'git diff*': 'allow',
505
+ 'grep *': 'allow',
506
+ 'rm *': 'deny',
507
+ },
508
+ question: 'allow',
509
+ },
510
+ },
511
+ },
512
+ },
513
+ null,
514
+ 2,
515
+ ) + '\n';
516
+ }
517
+
518
+ function renderVSCodeMcpConfig() {
519
+ return JSON.stringify(
520
+ {
521
+ servers: {
522
+ auditor: {
523
+ type: 'stdio',
524
+ command: 'node',
525
+ args: ['${workspaceFolder}/.audit-code/install/run-mcp-server.mjs'],
526
+ },
527
+ },
528
+ },
529
+ null,
530
+ 2,
531
+ ) + '\n';
532
+ }
533
+
534
+ function renderClaudeDesktopProjectTemplate() {
535
+ return [
536
+ '# Claude Desktop project template',
537
+ '',
538
+ 'Suggested project instructions:',
539
+ '',
540
+ '- Treat `/audit-code` as the canonical autonomous audit workflow for this repository.',
541
+ '- Prefer the installed auditor MCP tools over recreating state manually.',
542
+ '- Read the operator handoff and artifact resources before asking for more context.',
543
+ '- Present the final deterministic audit report as work blocks first.',
544
+ '',
545
+ 'Suggested project knowledge uploads:',
546
+ '',
547
+ '- `.audit-code/install/audit-code.import.md`',
548
+ '- `.audit-code/install/GETTING-STARTED.md`',
549
+ '- `docs/agent-integrations.md` when you want host-specific operator context',
550
+ '',
551
+ 'Starter prompt:',
552
+ '',
553
+ '> Start `/audit-code` for this repository using the installed auditor MCP tools. Continue until the audit is complete or blocked for operator input, and summarize the current handoff status before you stop.',
554
+ '',
555
+ ].join('\n');
556
+ }
557
+
558
+ function renderClaudeDesktopRemoteConnectorTemplate() {
559
+ return JSON.stringify(
560
+ {
561
+ name: 'auditor-lambda',
562
+ transport: 'remote-mcp',
563
+ url: 'https://example.com/auditor-lambda/mcp',
564
+ notes: [
565
+ 'Replace the URL with your hosted auditor MCP endpoint.',
566
+ 'Expose the same tool names as the local bundle: start_audit, get_status, continue_audit, explain_task, validate_artifacts, import_results, import_runtime_updates.',
567
+ 'Use OAuth or host-level auth when deploying this connector for Team or Enterprise rollouts.',
568
+ ],
569
+ },
570
+ null,
571
+ 2,
572
+ ) + '\n';
573
+ }
574
+
575
+ function renderAntigravityPlanningGuide(root) {
576
+ return [
577
+ '# Antigravity planning-mode guide',
578
+ '',
579
+ 'Recommended workflow:',
580
+ '',
581
+ '1. Open Antigravity in Planning mode.',
582
+ '2. Load the repo-local prompt asset or the AGENTS instructions before starting the audit conversation.',
583
+ '3. Ask Antigravity to use the installed auditor MCP server for structured state inspection and continuation.',
584
+ '4. Review Antigravity artifacts before accepting major code changes or imported evidence.',
585
+ '',
586
+ 'Recommended repo-local paths:',
587
+ `- prompt asset: \`${toRepoRelativePath(root, join(root, '.audit-code', 'install', INSTALLED_PROMPT_FILENAME))}\``,
588
+ `- MCP launcher: \`${toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME))}\``,
589
+ '',
590
+ 'Artifact round-tripping policy:',
591
+ '',
592
+ '- Browser walkthroughs and validation artifacts should be converted into runtime validation updates before import.',
593
+ '- Task-specific review artifacts should be normalized into `AuditResult` payloads before using `import_results`.',
594
+ '',
595
+ ].join('\n');
596
+ }
597
+
598
+ function renderSharedMcpLauncher(sourcePackageRoot) {
599
+ return [
600
+ "import { access, readFile } from 'node:fs/promises';",
601
+ "import { constants } from 'node:fs';",
602
+ "import { spawn } from 'node:child_process';",
603
+ "import { dirname, join, resolve } from 'node:path';",
604
+ "import { fileURLToPath } from 'node:url';",
605
+ '',
606
+ 'const scriptDir = dirname(fileURLToPath(import.meta.url));',
607
+ "const repoRoot = resolve(scriptDir, '..', '..');",
608
+ "const artifactsDir = join(repoRoot, '.audit-artifacts');",
609
+ `const sourcePackageRoot = ${JSON.stringify(sourcePackageRoot)};`,
610
+ '',
611
+ 'async function exists(path) {',
612
+ ' try {',
613
+ ' await access(path, constants.F_OK);',
614
+ ' return true;',
615
+ ' } catch {',
616
+ ' return false;',
617
+ ' }',
618
+ '}',
619
+ '',
620
+ 'function spawnForward(command, args) {',
621
+ ' return new Promise((resolvePromise, rejectPromise) => {',
622
+ ' const child = spawn(command, args, {',
623
+ ' cwd: repoRoot,',
624
+ ' env: process.env,',
625
+ " stdio: ['inherit', 'inherit', 'inherit'],",
626
+ ' });',
627
+ " child.on('error', rejectPromise);",
628
+ " child.on('exit', (code) => resolvePromise(code ?? 1));",
629
+ ' });',
630
+ '}',
631
+ '',
632
+ 'async function tryCandidates() {',
633
+ " const localPackageEntrypoint = join(repoRoot, 'node_modules', 'auditor-lambda', 'audit-code.mjs');",
634
+ " const localBin = process.platform === 'win32'",
635
+ " ? join(repoRoot, 'node_modules', '.bin', 'audit-code.cmd')",
636
+ " : join(repoRoot, 'node_modules', '.bin', 'audit-code');",
637
+ " const repoPackageJsonPath = join(repoRoot, 'package.json');",
638
+ " const sourcePackageEntrypoint = sourcePackageRoot",
639
+ " ? join(sourcePackageRoot, 'audit-code.mjs')",
640
+ " : null;",
641
+ " const sourcePackageJsonPath = sourcePackageRoot",
642
+ " ? join(sourcePackageRoot, 'package.json')",
643
+ " : null;",
644
+ ' const sharedArgs = [\'mcp\', \'--root\', repoRoot, \'--artifacts-dir\', artifactsDir];',
645
+ '',
646
+ ' if (await exists(localPackageEntrypoint)) {',
647
+ ' return await spawnForward(process.execPath, [localPackageEntrypoint, ...sharedArgs]);',
648
+ ' }',
649
+ '',
650
+ ' if (await exists(repoPackageJsonPath) && await exists(join(repoRoot, \'audit-code.mjs\'))) {',
651
+ ' try {',
652
+ " const packageJson = JSON.parse(await readFile(repoPackageJsonPath, 'utf8'));",
653
+ " if (packageJson?.name === 'auditor-lambda') {",
654
+ " return await spawnForward(process.execPath, [join(repoRoot, 'audit-code.mjs'), ...sharedArgs]);",
655
+ ' }',
656
+ ' } catch {',
657
+ ' // fall through to the next candidate',
658
+ ' }',
659
+ ' }',
660
+ '',
661
+ ' if (sourcePackageEntrypoint && sourcePackageJsonPath && await exists(sourcePackageEntrypoint) && await exists(sourcePackageJsonPath)) {',
662
+ ' try {',
663
+ " const packageJson = JSON.parse(await readFile(sourcePackageJsonPath, 'utf8'));",
664
+ " if (packageJson?.name === 'auditor-lambda') {",
665
+ ' return await spawnForward(process.execPath, [sourcePackageEntrypoint, ...sharedArgs]);',
666
+ ' }',
667
+ ' } catch {',
668
+ ' // fall through to the next candidate',
669
+ ' }',
670
+ ' }',
671
+ '',
672
+ ' if (await exists(localBin)) {',
673
+ ' return await spawnForward(localBin, sharedArgs);',
674
+ ' }',
675
+ '',
676
+ " const pathCandidate = process.platform === 'win32' ? 'audit-code.cmd' : 'audit-code';",
677
+ ' let exitCode = await spawnForward(pathCandidate, sharedArgs).catch(() => null);',
678
+ ' if (typeof exitCode === \'number\') {',
679
+ ' return exitCode;',
680
+ ' }',
681
+ '',
682
+ " exitCode = await spawnForward('npx', ['--no-install', 'audit-code', ...sharedArgs]).catch(() => null);",
683
+ " if (typeof exitCode === 'number') {",
684
+ ' return exitCode;',
685
+ ' }',
686
+ '',
687
+ ' throw new Error(',
688
+ " 'Unable to locate an audit-code executable. Install auditor-lambda as a dependency or make the audit-code binary available on PATH.',",
689
+ ' );',
690
+ '}',
691
+ '',
692
+ 'const code = await tryCandidates();',
693
+ 'process.exitCode = code;',
694
+ '',
695
+ ].join('\n');
696
+ }
448
697
 
449
- if (guide.supporting_paths.length > 0) {
450
- lines.push('', 'Supporting repo-local paths:');
451
- for (const targetPath of guide.supporting_paths) {
452
- lines.push(`- \`${toRepoRelativePath(root, targetPath)}\``);
698
+ function createCrc32Table() {
699
+ const table = new Uint32Array(256);
700
+ for (let index = 0; index < 256; index += 1) {
701
+ let value = index;
702
+ for (let bit = 0; bit < 8; bit += 1) {
703
+ value = (value & 1) !== 0 ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
453
704
  }
705
+ table[index] = value >>> 0;
454
706
  }
707
+ return table;
708
+ }
709
+
710
+ const CRC32_TABLE = createCrc32Table();
455
711
 
456
- lines.push('', 'Recommended steps:');
457
- for (const step of guide.steps) {
458
- lines.push(`- ${step}`);
712
+ function crc32(buffer) {
713
+ let crc = 0xffffffff;
714
+ for (const byte of buffer) {
715
+ crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
459
716
  }
460
- lines.push('');
717
+ return (crc ^ 0xffffffff) >>> 0;
718
+ }
461
719
 
462
- return lines.join('\n');
720
+ async function listFilesRecursive(root) {
721
+ const files = [];
722
+ const entries = await readdir(root, { withFileTypes: true });
723
+ for (const entry of entries) {
724
+ const absolutePath = join(root, entry.name);
725
+ if (entry.isDirectory()) {
726
+ files.push(...(await listFilesRecursive(absolutePath)));
727
+ continue;
728
+ }
729
+ if (entry.isFile()) {
730
+ files.push(absolutePath);
731
+ }
732
+ }
733
+ return files.sort((a, b) => a.localeCompare(b));
734
+ }
735
+
736
+ async function createStoredZipBuffer(sourceDir) {
737
+ const absoluteFiles = await listFilesRecursive(sourceDir);
738
+ const localParts = [];
739
+ const centralParts = [];
740
+ let offset = 0;
741
+
742
+ for (const absolutePath of absoluteFiles) {
743
+ const relativePath = replaceBackslashes(relative(sourceDir, absolutePath));
744
+ const fileName = Buffer.from(relativePath, 'utf8');
745
+ const content = await readFile(absolutePath);
746
+ const checksum = crc32(content);
747
+
748
+ const localHeader = Buffer.alloc(30);
749
+ localHeader.writeUInt32LE(0x04034b50, 0);
750
+ localHeader.writeUInt16LE(20, 4);
751
+ localHeader.writeUInt16LE(0, 6);
752
+ localHeader.writeUInt16LE(0, 8);
753
+ localHeader.writeUInt16LE(0, 10);
754
+ localHeader.writeUInt16LE(0, 12);
755
+ localHeader.writeUInt32LE(checksum, 14);
756
+ localHeader.writeUInt32LE(content.length, 18);
757
+ localHeader.writeUInt32LE(content.length, 22);
758
+ localHeader.writeUInt16LE(fileName.length, 26);
759
+ localHeader.writeUInt16LE(0, 28);
760
+
761
+ localParts.push(localHeader, fileName, content);
762
+
763
+ const centralHeader = Buffer.alloc(46);
764
+ centralHeader.writeUInt32LE(0x02014b50, 0);
765
+ centralHeader.writeUInt16LE(20, 4);
766
+ centralHeader.writeUInt16LE(20, 6);
767
+ centralHeader.writeUInt16LE(0, 8);
768
+ centralHeader.writeUInt16LE(0, 10);
769
+ centralHeader.writeUInt16LE(0, 12);
770
+ centralHeader.writeUInt16LE(0, 14);
771
+ centralHeader.writeUInt32LE(checksum, 16);
772
+ centralHeader.writeUInt32LE(content.length, 20);
773
+ centralHeader.writeUInt32LE(content.length, 24);
774
+ centralHeader.writeUInt16LE(fileName.length, 28);
775
+ centralHeader.writeUInt16LE(0, 30);
776
+ centralHeader.writeUInt16LE(0, 32);
777
+ centralHeader.writeUInt16LE(0, 34);
778
+ centralHeader.writeUInt16LE(0, 36);
779
+ centralHeader.writeUInt32LE(0, 38);
780
+ centralHeader.writeUInt32LE(offset, 42);
781
+
782
+ centralParts.push(centralHeader, fileName);
783
+ offset += localHeader.length + fileName.length + content.length;
784
+ }
785
+
786
+ const centralDirectory = Buffer.concat(centralParts);
787
+ const endOfCentralDirectory = Buffer.alloc(22);
788
+ endOfCentralDirectory.writeUInt32LE(0x06054b50, 0);
789
+ endOfCentralDirectory.writeUInt16LE(0, 4);
790
+ endOfCentralDirectory.writeUInt16LE(0, 6);
791
+ endOfCentralDirectory.writeUInt16LE(absoluteFiles.length, 8);
792
+ endOfCentralDirectory.writeUInt16LE(absoluteFiles.length, 10);
793
+ endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12);
794
+ endOfCentralDirectory.writeUInt32LE(offset, 16);
795
+ endOfCentralDirectory.writeUInt16LE(0, 20);
796
+
797
+ return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
798
+ }
799
+
800
+ async function buildClaudeDesktopBundle(root, results) {
801
+ const bundleRoot = join(root, '.audit-code', 'install', 'claude-desktop', 'bundle');
802
+ const serverRoot = join(bundleRoot, 'server');
803
+ const manifestPath = join(bundleRoot, 'manifest.json');
804
+ const serverEntrypointPath = join(serverRoot, 'index.js');
805
+ const dxtPath = join(root, '.audit-code', 'install', 'claude-desktop', 'auditor-lambda.dxt');
806
+ const mcpbPath = join(root, '.audit-code', 'install', 'claude-desktop', 'auditor-lambda.mcpb');
807
+
808
+ const bundleExisted = await fileExists(bundleRoot);
809
+ await mkdir(serverRoot, { recursive: true });
810
+ await cp(distEntry.replace(/dist[\\/]index\.js$/, 'dist'), join(bundleRoot, 'dist'), { recursive: true, force: true });
811
+ await cp(join(repoRoot, 'schemas'), join(bundleRoot, 'schemas'), { recursive: true, force: true });
812
+ await cp(join(repoRoot, 'skills', 'audit-code'), join(bundleRoot, 'skills', 'audit-code'), { recursive: true, force: true });
813
+ await writeFile(join(bundleRoot, 'audit-code.mjs'), await readFile(join(repoRoot, 'audit-code.mjs')));
814
+ await writeFile(join(bundleRoot, 'audit-code-wrapper-lib.mjs'), await readFile(join(repoRoot, 'audit-code-wrapper-lib.mjs')));
815
+ await writeFile(join(bundleRoot, 'package.json'), await readFile(join(repoRoot, 'package.json')));
816
+
817
+ results.push({ path: bundleRoot, mode: bundleExisted ? 'updated' : 'created' });
818
+
819
+ const serverEntry = [
820
+ "import { spawn } from 'node:child_process';",
821
+ "import { dirname, join } from 'node:path';",
822
+ "import { fileURLToPath } from 'node:url';",
823
+ '',
824
+ 'const serverDir = dirname(fileURLToPath(import.meta.url));',
825
+ "const bundleRoot = join(serverDir, '..');",
826
+ "const repoRoot = process.env.AUDIT_CODE_REPO_ROOT;",
827
+ "const artifactsDir = process.env.AUDIT_CODE_ARTIFACTS_DIR || (repoRoot ? join(repoRoot, '.audit-artifacts') : undefined);",
828
+ '',
829
+ 'if (!repoRoot) {',
830
+ " console.error('AUDIT_CODE_REPO_ROOT must be configured before launching the auditor MCP bundle.');",
831
+ ' process.exit(1);',
832
+ '}',
833
+ '',
834
+ "const child = spawn(process.execPath, [join(bundleRoot, 'audit-code.mjs'), 'mcp', '--root', repoRoot, '--artifacts-dir', artifactsDir], {",
835
+ " cwd: repoRoot,",
836
+ ' env: process.env,',
837
+ " stdio: ['inherit', 'inherit', 'inherit'],",
838
+ '});',
839
+ '',
840
+ "child.on('exit', (code) => {",
841
+ ' process.exitCode = code ?? 1;',
842
+ '});',
843
+ '',
844
+ ].join('\n');
845
+
846
+ results.push(await writeGeneratedMarkdown(serverEntrypointPath, serverEntry));
847
+
848
+ const manifest = {
849
+ manifest_version: '0.3',
850
+ name: 'auditor-lambda',
851
+ display_name: 'Auditor Lambda',
852
+ version: packageVersion,
853
+ description: 'Local MCP bundle for the /audit-code autonomous audit workflow.',
854
+ long_description:
855
+ 'Runs the auditor-lambda MCP server locally so Claude Desktop can start, inspect, and continue repository audits with structured tools, resources, and prompts.',
856
+ author: {
857
+ name: 'auditor-lambda',
858
+ url: 'https://github.com/OhOkThisIsFine/auditor-lambda',
859
+ },
860
+ repository: {
861
+ type: 'git',
862
+ url: 'https://github.com/OhOkThisIsFine/auditor-lambda.git',
863
+ },
864
+ documentation: 'https://github.com/OhOkThisIsFine/auditor-lambda/tree/main/docs',
865
+ support: 'https://github.com/OhOkThisIsFine/auditor-lambda/issues',
866
+ license: 'MIT',
867
+ compatibility: {
868
+ claude_desktop: '>=1.0.0',
869
+ platforms: ['darwin', 'win32', 'linux'],
870
+ runtimes: {
871
+ node: '>=20.0.0',
872
+ },
873
+ },
874
+ tools_generated: true,
875
+ prompts_generated: true,
876
+ server: {
877
+ type: 'node',
878
+ entry_point: 'server/index.js',
879
+ mcp_config: {
880
+ command: 'node',
881
+ args: ['${__dirname}/server/index.js'],
882
+ env: {
883
+ AUDIT_CODE_REPO_ROOT: '${user_config.repo_root}',
884
+ AUDIT_CODE_ARTIFACTS_DIR: '${user_config.artifacts_dir}',
885
+ },
886
+ },
887
+ },
888
+ user_config: {
889
+ repo_root: {
890
+ type: 'directory',
891
+ title: 'Repository Root',
892
+ description: 'Repository to audit with auditor-lambda.',
893
+ required: true,
894
+ },
895
+ artifacts_dir: {
896
+ type: 'directory',
897
+ title: 'Artifacts Directory',
898
+ description: 'Optional override for the audit artifacts directory.',
899
+ required: false,
900
+ },
901
+ },
902
+ };
903
+
904
+ results.push(await writeGeneratedJson(manifestPath, manifest));
905
+
906
+ const archive = await createStoredZipBuffer(bundleRoot);
907
+ results.push(await writeGeneratedBinary(dxtPath, archive));
908
+ results.push(await writeGeneratedBinary(mcpbPath, archive));
909
+
910
+ return {
911
+ bundleRoot,
912
+ manifestPath,
913
+ dxtPath,
914
+ mcpbPath,
915
+ serverEntrypointPath,
916
+ };
917
+ }
918
+
919
+ function buildHostCatalog({ root, host, assets }) {
920
+ const entries = {
921
+ codex: {
922
+ host: 'codex',
923
+ label: 'Codex',
924
+ support_level: 'supported',
925
+ setup_kind: 'skills+mcp+instructions',
926
+ summary:
927
+ 'Use the generated Codex skill bundle, AGENTS instructions, and shared MCP launcher so Codex can drive the backend through native tools instead of raw shell commands.',
928
+ primary_path: assets.codexSkillPath,
929
+ supporting_paths: [
930
+ assets.agentsInstructionsPath,
931
+ assets.codexMcpSetupPath,
932
+ assets.codexAutomationRecipePath,
933
+ ].filter(Boolean),
934
+ steps: [
935
+ 'Open this repository in Codex.',
936
+ 'Ensure Codex can access the repo-local auditor MCP server using the generated setup guide.',
937
+ 'Ask Codex to use the auditor MCP tools to start or continue `/audit-code`.',
938
+ ],
939
+ },
940
+ 'claude-desktop': {
941
+ host: 'claude-desktop',
942
+ label: 'Claude Desktop',
943
+ support_level: 'supported',
944
+ setup_kind: 'local-mcp-bundle',
945
+ summary:
946
+ 'Install the generated local MCP bundle in Claude Desktop, then use the project template and prompt asset as supporting context.',
947
+ primary_path: assets.claudeDesktopDxtPath,
948
+ supporting_paths: [
949
+ assets.claudeDesktopMcpbPath,
950
+ assets.claudeDesktopProjectTemplatePath,
951
+ assets.claudeDesktopRemoteConnectorPath,
952
+ assets.installedPromptPath,
953
+ ].filter(Boolean),
954
+ steps: [
955
+ 'Open Claude Desktop Settings and install the generated `.dxt` bundle.',
956
+ 'Configure the repository root when prompted so the bundle can launch the local auditor MCP server.',
957
+ 'Use the project template and prompt asset to kick off `/audit-code` in conversation.',
958
+ ],
959
+ },
960
+ opencode: {
961
+ host: 'opencode',
962
+ label: 'OpenCode',
963
+ support_level: 'supported',
964
+ setup_kind: 'command+agent+mcp',
965
+ summary:
966
+ 'Use the generated OpenCode command and project config so `/audit-code` and the local auditor MCP server are available together.',
967
+ primary_path: assets.opencodeCommandPath,
968
+ supporting_paths: [
969
+ assets.opencodeConfigPath,
970
+ assets.opencodeSkillPath,
971
+ assets.agentsInstructionsPath,
972
+ assets.mcpLauncherPath,
973
+ ].filter(Boolean),
974
+ steps: [
975
+ 'Open this repository in OpenCode.',
976
+ 'Let OpenCode load the generated `opencode.json` so the auditor MCP server is available.',
977
+ 'Invoke `/audit-code` and keep the audit loop on the auditor MCP tools.',
978
+ ],
979
+ },
980
+ vscode: {
981
+ host: 'vscode',
982
+ label: 'VS Code',
983
+ support_level: 'supported',
984
+ setup_kind: 'prompt+agent+mcp',
985
+ summary:
986
+ 'Use the generated prompt file, custom agent, and workspace MCP configuration for the cleanest VS Code integration.',
987
+ primary_path: assets.vscodePromptPath,
988
+ supporting_paths: [
989
+ assets.vscodeAgentPath,
990
+ assets.vscodeMcpConfigPath,
991
+ assets.copilotInstructionsPath,
992
+ ].filter(Boolean),
993
+ steps: [
994
+ 'Open this repository in VS Code with Copilot.',
995
+ 'Allow VS Code to discover the workspace MCP server from `.vscode/mcp.json`.',
996
+ 'Invoke `/audit-code` in chat and use the custom auditor agent when you want the dedicated orchestration persona.',
997
+ ],
998
+ },
999
+ antigravity: {
1000
+ host: 'antigravity',
1001
+ label: 'Antigravity',
1002
+ support_level: 'guided',
1003
+ setup_kind: 'planning-guide+mcp-ready',
1004
+ summary:
1005
+ 'Start in Planning mode with the generated guide and AGENTS instructions, then use the shared MCP launcher once Antigravity is ready to call structured tools.',
1006
+ primary_path: assets.antigravityPlanningGuidePath,
1007
+ supporting_paths: [
1008
+ assets.agentsInstructionsPath,
1009
+ assets.mcpLauncherPath,
1010
+ assets.installedPromptPath,
1011
+ ].filter(Boolean),
1012
+ steps: [
1013
+ 'Open this repository in Antigravity Planning mode.',
1014
+ 'Load the generated planning guide and AGENTS instructions before starting the audit.',
1015
+ 'Use the shared auditor MCP server when Antigravity needs structured audit state instead of free-form shell guesses.',
1016
+ ],
1017
+ },
1018
+ };
1019
+
1020
+ const hostOrder = host === 'all'
1021
+ ? ['codex', 'claude-desktop', 'opencode', 'vscode', 'antigravity']
1022
+ : host === 'copilot'
1023
+ ? ['vscode']
1024
+ : [host];
1025
+
1026
+ return hostOrder
1027
+ .map((hostKey) => entries[hostKey])
1028
+ .filter((entry) => entry?.primary_path)
1029
+ .map((entry) => ({
1030
+ ...entry,
1031
+ primary_path: entry.primary_path,
1032
+ supporting_paths: entry.supporting_paths,
1033
+ repo_relative_primary_path: toRepoRelativePath(root, entry.primary_path),
1034
+ repo_relative_supporting_paths: entry.supporting_paths.map((path) => toRepoRelativePath(root, path)),
1035
+ }));
463
1036
  }
464
1037
 
465
1038
  function renderInstallGuide({
@@ -467,180 +1040,753 @@ function renderInstallGuide({
467
1040
  host,
468
1041
  installedPromptPath,
469
1042
  installedSkillPath,
470
- slashCommandSurfaces,
471
- instructionSurfaces,
472
- unsupportedHosts,
1043
+ installManifestPath,
1044
+ mcpLauncherPath,
473
1045
  hostGuidance,
474
1046
  }) {
475
- const slashCommandPaths = Object.values(slashCommandSurfaces).filter(Boolean);
476
- const instructionPaths = Object.values(instructionSurfaces).filter(Boolean);
477
1047
  const lines = [
478
1048
  '# audit-code bootstrap guide',
479
1049
  '',
480
1050
  'The canonical product route is `/audit-code` in conversation.',
481
1051
  '',
482
- 'Canonical installed assets:',
1052
+ 'Shared repo-local assets:',
483
1053
  `- prompt asset: \`${toRepoRelativePath(root, installedPromptPath)}\``,
484
1054
  `- skill asset: \`${toRepoRelativePath(root, installedSkillPath)}\``,
1055
+ `- host manifest: \`${toRepoRelativePath(root, installManifestPath)}\``,
1056
+ `- shared MCP launcher: \`${toRepoRelativePath(root, mcpLauncherPath)}\``,
1057
+ '',
1058
+ 'Host-specific quick starts:',
485
1059
  ];
486
1060
 
487
- if (slashCommandPaths.length > 0) {
488
- lines.push('', 'Repo-local slash-command surfaces:');
489
- for (const targetPath of slashCommandPaths) {
490
- lines.push(`- \`${toRepoRelativePath(root, targetPath)}\``);
1061
+ for (const guide of hostGuidance) {
1062
+ lines.push(`- ${guide.label}: ${guide.summary}`);
1063
+ }
1064
+
1065
+ for (const guide of hostGuidance) {
1066
+ lines.push('', `## ${guide.label}`, '');
1067
+ lines.push(`Support level: ${guide.support_level}`);
1068
+ lines.push(`Setup kind: ${guide.setup_kind}`);
1069
+ lines.push('');
1070
+ lines.push(guide.summary);
1071
+ lines.push('');
1072
+ lines.push('Primary repo-local path:');
1073
+ lines.push(`- \`${toRepoRelativePath(root, guide.primary_path)}\``);
1074
+ if (guide.supporting_paths.length > 0) {
1075
+ lines.push('', 'Supporting repo-local paths:');
1076
+ for (const path of guide.supporting_paths) {
1077
+ lines.push(`- \`${toRepoRelativePath(root, path)}\``);
1078
+ }
1079
+ }
1080
+ lines.push('', 'Recommended steps:');
1081
+ for (const step of guide.steps) {
1082
+ lines.push(`- ${step}`);
1083
+ }
1084
+ }
1085
+
1086
+ lines.push('', 'Backend fallback:');
1087
+ lines.push('- from the repository root, run `audit-code` only when you intentionally need the repo-local backend wrapper');
1088
+ lines.push('- run `audit-code verify-install` after bootstrap when you want to smoke-test the generated launchers and host configs');
1089
+
1090
+ if (host !== 'all') {
1091
+ lines.push('');
1092
+ lines.push(`This install was scoped to \`${host}\`, so assets for other hosts may be intentionally omitted.`);
1093
+ }
1094
+
1095
+ lines.push('');
1096
+ return lines.join('\n');
1097
+ }
1098
+
1099
+ function getInstallProfile(host) {
1100
+ switch (host) {
1101
+ case 'all':
1102
+ return {
1103
+ writeVSCode: true,
1104
+ writeCopilotInstructions: true,
1105
+ writeOpenCode: true,
1106
+ writeCodex: true,
1107
+ writeClaudeDesktop: true,
1108
+ writeAntigravity: true,
1109
+ writeAgents: true,
1110
+ };
1111
+ case 'copilot':
1112
+ return {
1113
+ writeVSCode: true,
1114
+ writeCopilotInstructions: true,
1115
+ writeAgents: false,
1116
+ writeOpenCode: false,
1117
+ writeCodex: false,
1118
+ writeClaudeDesktop: false,
1119
+ writeAntigravity: false,
1120
+ };
1121
+ case 'vscode':
1122
+ return {
1123
+ writeVSCode: true,
1124
+ writeCopilotInstructions: true,
1125
+ writeAgents: false,
1126
+ writeOpenCode: false,
1127
+ writeCodex: false,
1128
+ writeClaudeDesktop: false,
1129
+ writeAntigravity: false,
1130
+ };
1131
+ case 'opencode':
1132
+ return {
1133
+ writeVSCode: false,
1134
+ writeCopilotInstructions: false,
1135
+ writeAgents: true,
1136
+ writeOpenCode: true,
1137
+ writeCodex: false,
1138
+ writeClaudeDesktop: false,
1139
+ writeAntigravity: false,
1140
+ };
1141
+ case 'codex':
1142
+ return {
1143
+ writeVSCode: false,
1144
+ writeCopilotInstructions: false,
1145
+ writeAgents: true,
1146
+ writeOpenCode: false,
1147
+ writeCodex: true,
1148
+ writeClaudeDesktop: false,
1149
+ writeAntigravity: false,
1150
+ };
1151
+ case 'claude-desktop':
1152
+ return {
1153
+ writeVSCode: false,
1154
+ writeCopilotInstructions: false,
1155
+ writeAgents: false,
1156
+ writeOpenCode: false,
1157
+ writeCodex: false,
1158
+ writeClaudeDesktop: true,
1159
+ writeAntigravity: false,
1160
+ };
1161
+ case 'antigravity':
1162
+ return {
1163
+ writeVSCode: false,
1164
+ writeCopilotInstructions: false,
1165
+ writeAgents: true,
1166
+ writeOpenCode: false,
1167
+ writeCodex: false,
1168
+ writeClaudeDesktop: false,
1169
+ writeAntigravity: true,
1170
+ };
1171
+ default:
1172
+ throw new Error(
1173
+ `Unsupported host "${host}". Supported hosts: all, copilot, vscode, opencode, codex, claude-desktop, antigravity.`,
1174
+ );
1175
+ }
1176
+ }
1177
+
1178
+ async function assertDirectoryExists(path, description) {
1179
+ let stats;
1180
+ try {
1181
+ stats = await stat(path);
1182
+ } catch {
1183
+ throw new Error(`${description} does not exist: ${path}`);
1184
+ }
1185
+
1186
+ if (!stats.isDirectory()) {
1187
+ throw new Error(`${description} is not a directory: ${path}`);
1188
+ }
1189
+ }
1190
+
1191
+ function encodeMcpMessage(payload) {
1192
+ const body = Buffer.from(JSON.stringify(payload), 'utf8');
1193
+ return Buffer.concat([
1194
+ Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8'),
1195
+ body,
1196
+ ]);
1197
+ }
1198
+
1199
+ function createInstallMcpClient(command, args, options = {}) {
1200
+ const child = spawn(command, args, {
1201
+ cwd: options.cwd,
1202
+ env: { ...process.env, ...(options.env ?? {}) },
1203
+ stdio: ['pipe', 'pipe', 'pipe'],
1204
+ });
1205
+
1206
+ let buffer = Buffer.alloc(0);
1207
+ let stderr = '';
1208
+ let exitError = null;
1209
+ const pending = new Map();
1210
+
1211
+ function failPending(error) {
1212
+ for (const { reject } of pending.values()) {
1213
+ reject(error);
1214
+ }
1215
+ pending.clear();
1216
+ }
1217
+
1218
+ child.stdout.on('data', (chunk) => {
1219
+ buffer = Buffer.concat([buffer, chunk]);
1220
+
1221
+ while (true) {
1222
+ const separator = buffer.indexOf('\r\n\r\n');
1223
+ if (separator < 0) {
1224
+ return;
1225
+ }
1226
+
1227
+ const headerBlock = buffer.slice(0, separator).toString('utf8');
1228
+ const contentLengthHeader = headerBlock
1229
+ .split('\r\n')
1230
+ .find((header) => header.toLowerCase().startsWith('content-length:'));
1231
+ if (!contentLengthHeader) {
1232
+ return;
1233
+ }
1234
+
1235
+ const contentLength = Number(contentLengthHeader.split(':')[1]?.trim());
1236
+ const frameLength = separator + 4 + contentLength;
1237
+ if (buffer.length < frameLength) {
1238
+ return;
1239
+ }
1240
+
1241
+ const payload = JSON.parse(
1242
+ buffer.slice(separator + 4, frameLength).toString('utf8'),
1243
+ );
1244
+ buffer = buffer.slice(frameLength);
1245
+
1246
+ if (pending.has(payload.id)) {
1247
+ const { resolve, reject } = pending.get(payload.id);
1248
+ pending.delete(payload.id);
1249
+ if (payload.error) {
1250
+ reject(
1251
+ new Error(payload.error.message ?? JSON.stringify(payload.error)),
1252
+ );
1253
+ continue;
1254
+ }
1255
+ resolve(payload.result);
1256
+ }
1257
+ }
1258
+ });
1259
+
1260
+ child.stderr.on('data', (chunk) => {
1261
+ stderr += String(chunk);
1262
+ });
1263
+
1264
+ child.on('error', (error) => {
1265
+ exitError = error;
1266
+ failPending(error);
1267
+ });
1268
+
1269
+ child.on('exit', (code, signal) => {
1270
+ if (exitError) {
1271
+ return;
1272
+ }
1273
+ exitError = new Error(
1274
+ `MCP process exited early with code ${code ?? 'null'}${signal ? ` and signal ${signal}` : ''}.${stderr.trim().length > 0 ? ` ${stderr.trim()}` : ''}`,
1275
+ );
1276
+ failPending(exitError);
1277
+ });
1278
+
1279
+ function request(id, method, params = {}) {
1280
+ return new Promise((resolve, reject) => {
1281
+ if (exitError) {
1282
+ reject(exitError);
1283
+ return;
1284
+ }
1285
+ pending.set(id, { resolve, reject });
1286
+ child.stdin.write(
1287
+ encodeMcpMessage({
1288
+ jsonrpc: '2.0',
1289
+ id,
1290
+ method,
1291
+ params,
1292
+ }),
1293
+ );
1294
+ });
1295
+ }
1296
+
1297
+ function notify(method, params = {}) {
1298
+ if (exitError) {
1299
+ return;
491
1300
  }
1301
+ child.stdin.write(
1302
+ encodeMcpMessage({
1303
+ jsonrpc: '2.0',
1304
+ method,
1305
+ params,
1306
+ }),
1307
+ );
492
1308
  }
493
1309
 
494
- if (instructionPaths.length > 0) {
495
- lines.push('', 'Compatibility instruction surfaces:');
496
- for (const targetPath of instructionPaths) {
497
- lines.push(`- \`${toRepoRelativePath(root, targetPath)}\``);
1310
+ async function close() {
1311
+ if (!exitError) {
1312
+ await request('shutdown', 'shutdown');
1313
+ notify('exit');
1314
+ child.stdin.end();
498
1315
  }
1316
+
1317
+ await new Promise((resolvePromise) => {
1318
+ child.on('exit', () => resolvePromise());
1319
+ });
499
1320
  }
500
1321
 
501
- lines.push('', 'Host-specific quick starts:');
502
- for (const guide of hostGuidance) {
503
- lines.push(`- ${guide.label}: ${guide.summary}`);
1322
+ return {
1323
+ request,
1324
+ notify,
1325
+ close,
1326
+ readStderr() {
1327
+ return stderr.trim();
1328
+ },
1329
+ };
1330
+ }
1331
+
1332
+ async function probeMcpServer(params) {
1333
+ const client = createInstallMcpClient(params.command, params.args, {
1334
+ cwd: params.cwd,
1335
+ env: params.env,
1336
+ });
1337
+
1338
+ let timerId;
1339
+ const timeout = new Promise((_, reject) => {
1340
+ timerId = setTimeout(() => {
1341
+ reject(
1342
+ new Error(
1343
+ `${params.label} did not complete an MCP handshake within ${INSTALL_VERIFY_TIMEOUT_MS}ms.`,
1344
+ ),
1345
+ );
1346
+ }, INSTALL_VERIFY_TIMEOUT_MS);
1347
+ });
1348
+
1349
+ try {
1350
+ const result = await Promise.race([
1351
+ (async () => {
1352
+ const initialize = await client.request('init', 'initialize', {
1353
+ protocolVersion: MCP_PROTOCOL_VERSION,
1354
+ capabilities: {},
1355
+ clientInfo: {
1356
+ name: 'audit-code-verify-install',
1357
+ version: packageVersion,
1358
+ },
1359
+ });
1360
+ client.notify('notifications/initialized');
1361
+ const tools = await client.request('tools', 'tools/list');
1362
+ const resources = await client.request('resources', 'resources/list');
1363
+ await client.close();
1364
+ return {
1365
+ initialize,
1366
+ tools,
1367
+ resources,
1368
+ stderr: client.readStderr(),
1369
+ };
1370
+ })(),
1371
+ timeout,
1372
+ ]);
1373
+ clearTimeout(timerId);
1374
+ return result;
1375
+ } catch (error) {
1376
+ clearTimeout(timerId);
1377
+ const stderr = client.readStderr();
1378
+ try {
1379
+ await client.close();
1380
+ } catch {
1381
+ // already failed or exited
1382
+ }
1383
+ const suffix = stderr.length > 0 ? ` ${stderr}` : '';
1384
+ throw new Error(`${error instanceof Error ? error.message : String(error)}${suffix}`);
1385
+ }
1386
+ }
1387
+
1388
+ async function collectVerifyCheck(target, id, fn) {
1389
+ try {
1390
+ const details = await fn();
1391
+ target.push({
1392
+ id,
1393
+ status: 'ok',
1394
+ ...(details ?? {}),
1395
+ });
1396
+ } catch (error) {
1397
+ target.push({
1398
+ id,
1399
+ status: 'error',
1400
+ summary: error instanceof Error ? error.message : String(error),
1401
+ });
1402
+ }
1403
+ }
1404
+
1405
+ async function ensureFile(path, description) {
1406
+ let stats;
1407
+ try {
1408
+ stats = await stat(path);
1409
+ } catch {
1410
+ throw new Error(`${description} does not exist: ${path}`);
1411
+ }
1412
+
1413
+ if (!stats.isFile()) {
1414
+ throw new Error(`${description} is not a file: ${path}`);
1415
+ }
1416
+
1417
+ return stats;
1418
+ }
1419
+
1420
+ async function readJson(path, description) {
1421
+ const content = await readFile(path, 'utf8');
1422
+ try {
1423
+ return JSON.parse(content);
1424
+ } catch (error) {
1425
+ throw new Error(
1426
+ `${description} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
1427
+ );
504
1428
  }
1429
+ }
505
1430
 
506
- for (const guide of hostGuidance) {
507
- lines.push('', renderInstallHostSection(root, guide).trimEnd());
1431
+ async function verifyZipFile(path, description) {
1432
+ const content = await readFile(path);
1433
+ if (content.length < 4 || content[0] !== 0x50 || content[1] !== 0x4b) {
1434
+ throw new Error(`${description} is not a valid ZIP-like archive: ${path}`);
508
1435
  }
1436
+ return content.length;
1437
+ }
509
1438
 
510
- lines.push(
511
- '',
512
- 'Backend fallback:',
513
- '- from the repository root, run `audit-code` only when you intentionally need the repo-local backend wrapper',
1439
+ async function verifyInstalledBootstrap(argv) {
1440
+ const root = resolve(getFlag(argv, '--root') ?? '.');
1441
+ const requestedHost = getFlag(argv, '--host')?.toLowerCase() ?? null;
1442
+ const installManifestPath = join(
1443
+ root,
1444
+ '.audit-code',
1445
+ 'install',
1446
+ INSTALL_MANIFEST_FILENAME,
514
1447
  );
1448
+ const installGuidePath = join(
1449
+ root,
1450
+ '.audit-code',
1451
+ 'install',
1452
+ INSTALL_GUIDE_FILENAME,
1453
+ );
1454
+
1455
+ await assertDirectoryExists(root, 'Target repository root');
1456
+
1457
+ const generalChecks = [];
1458
+ const hostResults = [];
1459
+ let installManifest;
515
1460
 
516
- if (unsupportedHosts.length > 0) {
517
- lines.push('', 'Hosts still requiring extra handling today:');
518
- for (const item of unsupportedHosts) {
519
- lines.push(`- ${item.host}: ${item.reason}`);
1461
+ await collectVerifyCheck(generalChecks, 'install_manifest', async () => {
1462
+ await ensureFile(installManifestPath, 'Install manifest');
1463
+ installManifest = await readJson(installManifestPath, 'Install manifest');
1464
+ if (installManifest?.contract_version !== 'audit-code-install/v1alpha1') {
1465
+ throw new Error(
1466
+ `Unexpected install manifest contract version: ${installManifest?.contract_version ?? 'missing'}.`,
1467
+ );
520
1468
  }
521
- }
1469
+ return {
1470
+ summary: 'Install manifest parsed successfully.',
1471
+ path: installManifestPath,
1472
+ };
1473
+ });
522
1474
 
523
- if (host !== 'all') {
524
- lines.push(
525
- '',
526
- `This install was scoped to \`${host}\`, so some repo-local surfaces may be intentionally omitted.`,
1475
+ if (!installManifest) {
1476
+ console.log(
1477
+ JSON.stringify(
1478
+ {
1479
+ root,
1480
+ requested_host: requestedHost ?? 'all',
1481
+ status: 'error',
1482
+ issue_count: generalChecks.filter((check) => check.status === 'error').length,
1483
+ checks: generalChecks,
1484
+ hosts: [],
1485
+ },
1486
+ null,
1487
+ 2,
1488
+ ),
527
1489
  );
1490
+ process.exitCode = 1;
1491
+ return;
528
1492
  }
529
1493
 
530
- lines.push('');
531
- return lines.join('\n');
532
- }
533
-
534
- function upsertManagedBlock(existingContent, blockContent) {
535
- const normalized = normalizeNewlines(existingContent);
536
- const blockPattern = new RegExp(
537
- `${INSTALL_MARKER_START}[\\s\\S]*?${INSTALL_MARKER_END}`,
538
- 'u',
1494
+ const assetPaths = installManifest.asset_paths ?? {};
1495
+ const hostCatalog = new Map(
1496
+ (installManifest.hosts ?? []).map((entry) => [entry.host, entry]),
539
1497
  );
1498
+ const selectedHosts = requestedHost && requestedHost !== 'all'
1499
+ ? [requestedHost]
1500
+ : [...hostCatalog.keys()];
1501
+
1502
+ await collectVerifyCheck(generalChecks, 'install_guide', async () => {
1503
+ const guide = await readFile(installGuidePath, 'utf8');
1504
+ if (!guide.includes('# audit-code bootstrap guide')) {
1505
+ throw new Error(`Install guide does not look like an audit-code bootstrap guide: ${installGuidePath}`);
1506
+ }
1507
+ return {
1508
+ summary: 'Install guide is present and readable.',
1509
+ path: installGuidePath,
1510
+ };
1511
+ });
540
1512
 
541
- if (blockPattern.test(normalized)) {
542
- return normalized.replace(blockPattern, blockContent);
543
- }
1513
+ await collectVerifyCheck(generalChecks, 'installed_prompt', async () => {
1514
+ await ensureFile(assetPaths.installedPromptPath, 'Installed prompt asset');
1515
+ return {
1516
+ summary: 'Installed prompt asset is present.',
1517
+ path: assetPaths.installedPromptPath,
1518
+ };
1519
+ });
544
1520
 
545
- if (normalized.trim().length === 0) {
546
- return `${blockContent}\n`;
547
- }
1521
+ await collectVerifyCheck(generalChecks, 'installed_skill', async () => {
1522
+ await ensureFile(assetPaths.installedSkillPath, 'Installed skill asset');
1523
+ return {
1524
+ summary: 'Installed skill asset is present.',
1525
+ path: assetPaths.installedSkillPath,
1526
+ };
1527
+ });
548
1528
 
549
- return `${normalized.replace(/\s+$/u, '')}\n\n${blockContent}\n`;
550
- }
1529
+ await collectVerifyCheck(generalChecks, 'shared_launcher_file', async () => {
1530
+ const launcher = await readFile(assetPaths.mcpLauncherPath, 'utf8');
1531
+ if (!launcher.includes('Unable to locate an audit-code executable')) {
1532
+ throw new Error(`Shared MCP launcher is missing the executable resolution fallback message: ${assetPaths.mcpLauncherPath}`);
1533
+ }
1534
+ if (!launcher.includes('sourcePackageRoot')) {
1535
+ throw new Error(`Shared MCP launcher is missing the package-source fallback hint: ${assetPaths.mcpLauncherPath}`);
1536
+ }
1537
+ return {
1538
+ summary: 'Shared MCP launcher is present and includes resolver fallbacks.',
1539
+ path: assetPaths.mcpLauncherPath,
1540
+ };
1541
+ });
551
1542
 
552
- async function writeManagedMarkdown(targetPath, blockContent) {
553
- const existed = await fileExists(targetPath);
554
- const existingContent = existed ? await readFile(targetPath, 'utf8') : '';
555
- const nextContent = upsertManagedBlock(existingContent, blockContent);
556
- await mkdir(dirname(targetPath), { recursive: true });
557
- await writeFile(targetPath, nextContent, 'utf8');
558
- return {
559
- path: targetPath,
560
- mode: existed ? 'updated' : 'created',
561
- };
562
- }
1543
+ await collectVerifyCheck(generalChecks, 'shared_launcher_mcp', async () => {
1544
+ const probe = await probeMcpServer({
1545
+ label: 'Shared MCP launcher',
1546
+ command: process.execPath,
1547
+ args: [assetPaths.mcpLauncherPath],
1548
+ cwd: root,
1549
+ });
1550
+ const toolNames = (probe.tools?.tools ?? []).map((tool) => tool.name);
1551
+ if (!toolNames.includes('start_audit')) {
1552
+ throw new Error('Shared MCP launcher did not expose the start_audit tool.');
1553
+ }
1554
+ return {
1555
+ summary: 'Shared MCP launcher completed an MCP handshake.',
1556
+ server_name: probe.initialize?.serverInfo?.name ?? null,
1557
+ tool_count: toolNames.length,
1558
+ resource_count: (probe.resources?.resources ?? []).length,
1559
+ };
1560
+ });
563
1561
 
564
- async function writeGeneratedMarkdown(targetPath, content) {
565
- const existed = await fileExists(targetPath);
566
- await mkdir(dirname(targetPath), { recursive: true });
567
- await writeFile(targetPath, content, 'utf8');
568
- return {
569
- path: targetPath,
570
- mode: existed ? 'updated' : 'created',
571
- };
572
- }
1562
+ for (const hostKey of selectedHosts) {
1563
+ const checks = [];
1564
+ const hostEntry = hostCatalog.get(hostKey);
573
1565
 
574
- function getInstallProfile(host) {
575
- switch (host) {
576
- case 'all':
577
- return {
578
- writeVSCodePrompt: true,
579
- writeCopilotInstructions: true,
580
- writeAgents: true,
581
- writeClaudeMemory: true,
582
- writeOpenCodeCommand: true,
583
- writeClaudeCommand: true,
584
- writeCompatibilitySkills: true,
585
- };
586
- case 'copilot':
587
- return {
588
- writeVSCodePrompt: true,
589
- writeCopilotInstructions: true,
590
- writeAgents: false,
591
- writeClaudeMemory: false,
592
- writeOpenCodeCommand: false,
593
- writeClaudeCommand: false,
594
- writeCompatibilitySkills: false,
595
- };
596
- case 'vscode':
597
- return {
598
- writeVSCodePrompt: true,
599
- writeCopilotInstructions: true,
600
- writeAgents: true,
601
- writeClaudeMemory: true,
602
- writeOpenCodeCommand: false,
603
- writeClaudeCommand: false,
604
- writeCompatibilitySkills: false,
605
- };
606
- case 'opencode':
607
- return {
608
- writeVSCodePrompt: false,
609
- writeCopilotInstructions: false,
610
- writeAgents: true,
611
- writeClaudeMemory: false,
612
- writeOpenCodeCommand: true,
613
- writeClaudeCommand: false,
614
- writeCompatibilitySkills: true,
615
- };
616
- case 'claude-code':
617
- return {
618
- writeVSCodePrompt: false,
619
- writeCopilotInstructions: false,
620
- writeAgents: false,
621
- writeClaudeMemory: true,
622
- writeOpenCodeCommand: false,
623
- writeClaudeCommand: true,
624
- writeCompatibilitySkills: true,
625
- };
626
- default:
627
- throw new Error(
628
- `Unsupported host "${host}". Supported hosts: all, copilot, vscode, opencode, claude-code.`,
629
- );
630
- }
631
- }
1566
+ if (!hostEntry) {
1567
+ checks.push({
1568
+ id: 'host_manifest_entry',
1569
+ status: 'error',
1570
+ summary: `Install manifest does not contain host guidance for "${hostKey}".`,
1571
+ });
1572
+ hostResults.push({ host: hostKey, status: 'error', checks });
1573
+ continue;
1574
+ }
632
1575
 
633
- async function assertDirectoryExists(path, description) {
634
- let stats;
635
- try {
636
- stats = await stat(path);
637
- } catch {
638
- throw new Error(`${description} does not exist: ${path}`);
639
- }
1576
+ await collectVerifyCheck(checks, 'host_manifest_entry', async () => ({
1577
+ summary: `Host guidance exists for ${hostEntry.label}.`,
1578
+ primary_path: hostEntry.primary_path,
1579
+ }));
1580
+
1581
+ switch (hostKey) {
1582
+ case 'codex':
1583
+ await collectVerifyCheck(checks, 'codex_skill', async () => {
1584
+ const content = await readFile(assetPaths.codexSkillPath, 'utf8');
1585
+ if (!content.includes('# audit-code skill')) {
1586
+ throw new Error(`Codex skill file is missing the expected heading: ${assetPaths.codexSkillPath}`);
1587
+ }
1588
+ return {
1589
+ summary: 'Codex skill bundle is present.',
1590
+ path: assetPaths.codexSkillPath,
1591
+ };
1592
+ });
1593
+ await collectVerifyCheck(checks, 'codex_mcp_setup', async () => {
1594
+ const content = await readFile(assetPaths.codexMcpSetupPath, 'utf8');
1595
+ if (!content.includes(MCP_LAUNCHER_FILENAME)) {
1596
+ throw new Error(`Codex MCP setup guide does not reference ${MCP_LAUNCHER_FILENAME}.`);
1597
+ }
1598
+ return {
1599
+ summary: 'Codex MCP setup guide references the shared launcher.',
1600
+ path: assetPaths.codexMcpSetupPath,
1601
+ };
1602
+ });
1603
+ break;
1604
+ case 'claude-desktop': {
1605
+ const bundleManifestPath = join(
1606
+ root,
1607
+ '.audit-code',
1608
+ 'install',
1609
+ 'claude-desktop',
1610
+ 'bundle',
1611
+ 'manifest.json',
1612
+ );
1613
+ const bundleServerPath = join(
1614
+ root,
1615
+ '.audit-code',
1616
+ 'install',
1617
+ 'claude-desktop',
1618
+ 'bundle',
1619
+ 'server',
1620
+ 'index.js',
1621
+ );
1622
+
1623
+ await collectVerifyCheck(checks, 'claude_bundle_manifest', async () => {
1624
+ const manifest = await readJson(bundleManifestPath, 'Claude Desktop bundle manifest');
1625
+ if (manifest?.server?.entry_point !== 'server/index.js') {
1626
+ throw new Error(`Claude Desktop bundle manifest has an unexpected entry_point: ${manifest?.server?.entry_point ?? 'missing'}.`);
1627
+ }
1628
+ return {
1629
+ summary: 'Claude Desktop bundle manifest parsed successfully.',
1630
+ path: bundleManifestPath,
1631
+ };
1632
+ });
1633
+ await collectVerifyCheck(checks, 'claude_connector_template', async () => {
1634
+ const connector = await readJson(
1635
+ assetPaths.claudeDesktopRemoteConnectorPath,
1636
+ 'Claude Desktop remote connector template',
1637
+ );
1638
+ if (connector?.transport !== 'remote-mcp') {
1639
+ throw new Error(`Claude Desktop remote connector transport must be "remote-mcp", got ${connector?.transport ?? 'missing'}.`);
1640
+ }
1641
+ return {
1642
+ summary: 'Claude Desktop remote connector template parsed successfully.',
1643
+ path: assetPaths.claudeDesktopRemoteConnectorPath,
1644
+ };
1645
+ });
1646
+ await collectVerifyCheck(checks, 'claude_dxt_archive', async () => ({
1647
+ summary: 'Claude Desktop .dxt bundle is present.',
1648
+ path: assetPaths.claudeDesktopDxtPath,
1649
+ size_bytes: await verifyZipFile(
1650
+ assetPaths.claudeDesktopDxtPath,
1651
+ 'Claude Desktop .dxt bundle',
1652
+ ),
1653
+ }));
1654
+ await collectVerifyCheck(checks, 'claude_mcpb_archive', async () => ({
1655
+ summary: 'Claude Desktop .mcpb bundle is present.',
1656
+ path: assetPaths.claudeDesktopMcpbPath,
1657
+ size_bytes: await verifyZipFile(
1658
+ assetPaths.claudeDesktopMcpbPath,
1659
+ 'Claude Desktop .mcpb bundle',
1660
+ ),
1661
+ }));
1662
+ await collectVerifyCheck(checks, 'claude_bundle_mcp', async () => {
1663
+ const probe = await probeMcpServer({
1664
+ label: 'Claude Desktop bundle server',
1665
+ command: process.execPath,
1666
+ args: [bundleServerPath],
1667
+ cwd: root,
1668
+ env: {
1669
+ AUDIT_CODE_REPO_ROOT: root,
1670
+ AUDIT_CODE_ARTIFACTS_DIR: join(root, '.audit-artifacts'),
1671
+ },
1672
+ });
1673
+ const toolNames = (probe.tools?.tools ?? []).map((tool) => tool.name);
1674
+ if (!toolNames.includes('start_audit')) {
1675
+ throw new Error('Claude Desktop bundle server did not expose the start_audit tool.');
1676
+ }
1677
+ return {
1678
+ summary: 'Claude Desktop bundle completed an MCP handshake.',
1679
+ tool_count: toolNames.length,
1680
+ };
1681
+ });
1682
+ break;
1683
+ }
1684
+ case 'opencode':
1685
+ await collectVerifyCheck(checks, 'opencode_command', async () => {
1686
+ const content = await readFile(assetPaths.opencodeCommandPath, 'utf8');
1687
+ if (!content.includes('agent: auditor')) {
1688
+ throw new Error(`OpenCode command file is missing the auditor agent frontmatter: ${assetPaths.opencodeCommandPath}`);
1689
+ }
1690
+ return {
1691
+ summary: 'OpenCode command file is present.',
1692
+ path: assetPaths.opencodeCommandPath,
1693
+ };
1694
+ });
1695
+ await collectVerifyCheck(checks, 'opencode_config', async () => {
1696
+ const config = await readJson(assetPaths.opencodeConfigPath, 'OpenCode project config');
1697
+ const command = config?.mcp?.auditor?.command;
1698
+ if (!Array.isArray(command) || command[0] !== 'node') {
1699
+ throw new Error('OpenCode config must set mcp.auditor.command as a Node command array.');
1700
+ }
1701
+ if (command[1] !== '.audit-code/install/run-mcp-server.mjs') {
1702
+ throw new Error(`OpenCode config must point at .audit-code/install/${MCP_LAUNCHER_FILENAME}, got ${command[1] ?? 'missing'}.`);
1703
+ }
1704
+ if (config?.mcp?.auditor?.type !== 'local') {
1705
+ throw new Error(`OpenCode config must set mcp.auditor.type to "local", got ${config?.mcp?.auditor?.type ?? 'missing'}.`);
1706
+ }
1707
+ return {
1708
+ summary: 'OpenCode project config parsed successfully.',
1709
+ path: assetPaths.opencodeConfigPath,
1710
+ };
1711
+ });
1712
+ break;
1713
+ case 'vscode':
1714
+ await collectVerifyCheck(checks, 'vscode_prompt', async () => {
1715
+ const content = await readFile(assetPaths.vscodePromptPath, 'utf8');
1716
+ if (!content.includes('name: audit-code')) {
1717
+ throw new Error(`VS Code prompt file is missing the expected frontmatter name: ${assetPaths.vscodePromptPath}`);
1718
+ }
1719
+ return {
1720
+ summary: 'VS Code prompt file is present.',
1721
+ path: assetPaths.vscodePromptPath,
1722
+ };
1723
+ });
1724
+ await collectVerifyCheck(checks, 'vscode_mcp_config', async () => {
1725
+ const config = await readJson(assetPaths.vscodeMcpConfigPath, 'VS Code MCP config');
1726
+ const args = config?.servers?.auditor?.args;
1727
+ if (config?.servers?.auditor?.command !== 'node') {
1728
+ throw new Error(`VS Code MCP config must use node as the command, got ${config?.servers?.auditor?.command ?? 'missing'}.`);
1729
+ }
1730
+ if (!Array.isArray(args) || args[0] !== '${workspaceFolder}/.audit-code/install/run-mcp-server.mjs') {
1731
+ throw new Error(`VS Code MCP config must point at \${workspaceFolder}/.audit-code/install/${MCP_LAUNCHER_FILENAME}.`);
1732
+ }
1733
+ return {
1734
+ summary: 'VS Code MCP config parsed successfully.',
1735
+ path: assetPaths.vscodeMcpConfigPath,
1736
+ };
1737
+ });
1738
+ break;
1739
+ case 'antigravity':
1740
+ await collectVerifyCheck(checks, 'antigravity_guide', async () => {
1741
+ const content = await readFile(assetPaths.antigravityPlanningGuidePath, 'utf8');
1742
+ if (!content.includes(MCP_LAUNCHER_FILENAME) || !content.includes(INSTALLED_PROMPT_FILENAME)) {
1743
+ throw new Error(`Antigravity guide must reference both ${MCP_LAUNCHER_FILENAME} and ${INSTALLED_PROMPT_FILENAME}.`);
1744
+ }
1745
+ return {
1746
+ summary: 'Antigravity planning guide references the repo-local prompt asset and MCP launcher.',
1747
+ path: assetPaths.antigravityPlanningGuidePath,
1748
+ };
1749
+ });
1750
+ break;
1751
+ default:
1752
+ checks.push({
1753
+ id: 'host_handler',
1754
+ status: 'error',
1755
+ summary: `No verification handler is implemented for host "${hostKey}".`,
1756
+ });
1757
+ }
640
1758
 
641
- if (!stats.isDirectory()) {
642
- throw new Error(`${description} is not a directory: ${path}`);
1759
+ hostResults.push({
1760
+ host: hostKey,
1761
+ status: checks.some((check) => check.status === 'error') ? 'error' : 'ok',
1762
+ checks,
1763
+ });
643
1764
  }
1765
+
1766
+ const issueCount =
1767
+ generalChecks.filter((check) => check.status === 'error').length +
1768
+ hostResults.reduce(
1769
+ (sum, host) => sum + host.checks.filter((check) => check.status === 'error').length,
1770
+ 0,
1771
+ );
1772
+
1773
+ console.log(
1774
+ JSON.stringify(
1775
+ {
1776
+ root,
1777
+ requested_host: requestedHost ?? 'all',
1778
+ manifest_path: installManifestPath,
1779
+ status: issueCount > 0 ? 'error' : 'ok',
1780
+ issue_count: issueCount,
1781
+ checks: generalChecks,
1782
+ hosts: hostResults,
1783
+ },
1784
+ null,
1785
+ 2,
1786
+ ),
1787
+ );
1788
+
1789
+ process.exitCode = issueCount > 0 ? 1 : 0;
644
1790
  }
645
1791
 
646
1792
  async function installBootstrap(argv) {
@@ -655,43 +1801,67 @@ async function installBootstrap(argv) {
655
1801
  const legacyInstalledPromptPath = join(root, '.audit-code', 'install', 'audit-code.prompt.md');
656
1802
  const installedSkillPath = join(root, '.audit-code', 'install', 'SKILL.md');
657
1803
  const installGuidePath = join(root, '.audit-code', 'install', INSTALL_GUIDE_FILENAME);
658
- const slashCommandSurfaces = {
659
- vscode_prompt: profile.writeVSCodePrompt
660
- ? join(root, '.github', 'prompts', 'audit-code.prompt.md')
1804
+ const installManifestPath = join(root, '.audit-code', 'install', INSTALL_MANIFEST_FILENAME);
1805
+ const mcpLauncherPath = join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME);
1806
+ const assetPaths = {
1807
+ installedPromptPath,
1808
+ installedSkillPath,
1809
+ installGuidePath,
1810
+ installManifestPath,
1811
+ mcpLauncherPath,
1812
+ agentsInstructionsPath: profile.writeAgents ? join(root, 'AGENTS.md') : null,
1813
+ copilotInstructionsPath: profile.writeCopilotInstructions
1814
+ ? join(root, '.github', 'copilot-instructions.md')
1815
+ : null,
1816
+ codexSkillPath: profile.writeCodex
1817
+ ? join(root, '.codex', 'skills', 'audit-code', 'SKILL.md')
661
1818
  : null,
662
- opencode_command: profile.writeOpenCodeCommand
1819
+ codexPromptPath: profile.writeCodex
1820
+ ? join(root, '.codex', 'skills', 'audit-code', 'audit-code.prompt.md')
1821
+ : null,
1822
+ codexMcpSetupPath: profile.writeCodex
1823
+ ? join(root, '.audit-code', 'install', 'codex', 'MCP-SETUP.md')
1824
+ : null,
1825
+ codexAutomationRecipePath: profile.writeCodex
1826
+ ? join(root, '.audit-code', 'install', 'codex', 'RE-AUDIT-AUTOMATION.md')
1827
+ : null,
1828
+ claudeDesktopProjectTemplatePath: profile.writeClaudeDesktop
1829
+ ? join(root, '.audit-code', 'install', 'claude-desktop', 'PROJECT-TEMPLATE.md')
1830
+ : null,
1831
+ claudeDesktopRemoteConnectorPath: profile.writeClaudeDesktop
1832
+ ? join(root, '.audit-code', 'install', 'claude-desktop', 'remote-mcp-connector.json')
1833
+ : null,
1834
+ claudeDesktopDxtPath: profile.writeClaudeDesktop
1835
+ ? join(root, '.audit-code', 'install', 'claude-desktop', 'auditor-lambda.dxt')
1836
+ : null,
1837
+ claudeDesktopMcpbPath: profile.writeClaudeDesktop
1838
+ ? join(root, '.audit-code', 'install', 'claude-desktop', 'auditor-lambda.mcpb')
1839
+ : null,
1840
+ opencodeCommandPath: profile.writeOpenCode
663
1841
  ? join(root, '.opencode', 'commands', 'audit-code.md')
664
1842
  : null,
665
- claude_code_command: profile.writeClaudeCommand
666
- ? join(root, '.claude', 'commands', 'audit-code.md')
1843
+ opencodeConfigPath: profile.writeOpenCode
1844
+ ? join(root, 'opencode.json')
667
1845
  : null,
668
- };
669
- const instructionSurfaces = {
670
- copilot_instructions: profile.writeCopilotInstructions
671
- ? join(root, '.github', 'copilot-instructions.md')
1846
+ opencodeSkillPath: profile.writeOpenCode
1847
+ ? join(root, '.opencode', 'skills', 'audit-code', 'SKILL.md')
1848
+ : null,
1849
+ opencodePromptPath: profile.writeOpenCode
1850
+ ? join(root, '.opencode', 'skills', 'audit-code', 'audit-code.prompt.md')
1851
+ : null,
1852
+ vscodePromptPath: profile.writeVSCode
1853
+ ? join(root, '.github', 'prompts', 'audit-code.prompt.md')
1854
+ : null,
1855
+ vscodeAgentPath: profile.writeVSCode
1856
+ ? join(root, '.github', 'agents', 'auditor.agent.md')
1857
+ : null,
1858
+ vscodeMcpConfigPath: profile.writeVSCode
1859
+ ? join(root, '.vscode', 'mcp.json')
1860
+ : null,
1861
+ antigravityPlanningGuidePath: profile.writeAntigravity
1862
+ ? join(root, '.audit-code', 'install', 'antigravity', 'PLANNING-MODE.md')
672
1863
  : null,
673
- agents: profile.writeAgents ? join(root, 'AGENTS.md') : null,
674
- claude: profile.writeClaudeMemory ? join(root, 'CLAUDE.md') : null,
675
1864
  };
676
- const unsupportedHosts = host === 'all'
677
- ? [
678
- {
679
- host: 'claude-desktop',
680
- reason:
681
- 'No verified project-local slash-command installation surface is currently shipped for Claude Desktop.',
682
- },
683
- {
684
- host: 'antigravity',
685
- reason:
686
- 'No verified repo-local slash-command installation surface is currently shipped for Antigravity.',
687
- },
688
- ]
689
- : [];
690
- const hostGuidance = buildInstallHostGuidance({
691
- installedPromptPath,
692
- slashCommandSurfaces,
693
- instructionSurfaces,
694
- });
695
1865
 
696
1866
  const results = [];
697
1867
  if (await fileExists(legacyInstalledPromptPath)) {
@@ -705,84 +1875,161 @@ async function installBootstrap(argv) {
705
1875
  );
706
1876
  results.push(await writeGeneratedMarkdown(installedSkillPath, skillSource));
707
1877
 
708
- if (profile.writeVSCodePrompt) {
1878
+ results.push(
1879
+ await writeGeneratedMarkdown(mcpLauncherPath, renderSharedMcpLauncher(repoRoot)),
1880
+ );
1881
+
1882
+ const compatibilityBlockTargets = [
1883
+ assetPaths.agentsInstructionsPath,
1884
+ assetPaths.copilotInstructionsPath,
1885
+ ].filter(Boolean);
1886
+
1887
+ for (const targetPath of compatibilityBlockTargets) {
709
1888
  results.push(
710
- await writeGeneratedMarkdown(
711
- join(root, '.github', 'prompts', 'audit-code.prompt.md'),
712
- renderPromptFile(
713
- {
714
- name: 'audit-code',
715
- description: 'Autonomous local loop code auditing',
716
- agent: 'agent',
717
- },
718
- promptBody,
1889
+ await writeManagedMarkdown(
1890
+ targetPath,
1891
+ buildInstallDirective(
1892
+ relative(dirname(targetPath), installedPromptPath) || `./.audit-code/install/${INSTALLED_PROMPT_FILENAME}`,
719
1893
  ),
720
1894
  ),
721
1895
  );
722
1896
  }
723
1897
 
724
- if (profile.writeOpenCodeCommand) {
1898
+ if (profile.writeCodex) {
1899
+ results.push(
1900
+ await writeGeneratedMarkdown(
1901
+ assetPaths.codexSkillPath,
1902
+ skillSource,
1903
+ ),
1904
+ );
1905
+ results.push(
1906
+ await writeGeneratedMarkdown(
1907
+ assetPaths.codexPromptPath,
1908
+ promptSource,
1909
+ ),
1910
+ );
1911
+ results.push(
1912
+ await writeGeneratedMarkdown(
1913
+ assetPaths.codexMcpSetupPath,
1914
+ renderCodexMcpSetupGuide(root),
1915
+ ),
1916
+ );
1917
+ results.push(
1918
+ await writeGeneratedMarkdown(
1919
+ assetPaths.codexAutomationRecipePath,
1920
+ renderCodexAutomationRecipe(),
1921
+ ),
1922
+ );
1923
+ }
1924
+
1925
+ if (profile.writeClaudeDesktop) {
1926
+ const claudeDesktopBundle = await buildClaudeDesktopBundle(root, results);
1927
+ assetPaths.claudeDesktopDxtPath = claudeDesktopBundle.dxtPath;
1928
+ assetPaths.claudeDesktopMcpbPath = claudeDesktopBundle.mcpbPath;
1929
+ results.push(
1930
+ await writeGeneratedMarkdown(
1931
+ assetPaths.claudeDesktopProjectTemplatePath,
1932
+ renderClaudeDesktopProjectTemplate(),
1933
+ ),
1934
+ );
1935
+ results.push(
1936
+ await writeGeneratedJson(
1937
+ assetPaths.claudeDesktopRemoteConnectorPath,
1938
+ JSON.parse(renderClaudeDesktopRemoteConnectorTemplate()),
1939
+ ),
1940
+ );
1941
+ }
1942
+
1943
+ if (profile.writeOpenCode) {
725
1944
  results.push(
726
1945
  await writeGeneratedMarkdown(
727
- join(root, '.opencode', 'commands', 'audit-code.md'),
1946
+ assetPaths.opencodeCommandPath,
728
1947
  renderPromptFile(
729
1948
  {
730
1949
  description: 'Autonomous local loop code auditing',
731
- agent: 'build',
1950
+ agent: 'auditor',
732
1951
  subtask: false,
733
1952
  },
734
1953
  promptBody,
735
1954
  ),
736
1955
  ),
737
1956
  );
1957
+ results.push(
1958
+ await writeGeneratedMarkdown(
1959
+ assetPaths.opencodeSkillPath,
1960
+ skillSource,
1961
+ ),
1962
+ );
1963
+ results.push(
1964
+ await writeGeneratedMarkdown(
1965
+ assetPaths.opencodePromptPath,
1966
+ promptSource,
1967
+ ),
1968
+ );
1969
+ results.push(
1970
+ await writeGeneratedMarkdown(
1971
+ assetPaths.opencodeConfigPath,
1972
+ renderOpenCodeProjectConfig(root),
1973
+ ),
1974
+ );
738
1975
  }
739
1976
 
740
- if (profile.writeClaudeCommand) {
1977
+ if (profile.writeVSCode) {
741
1978
  results.push(
742
1979
  await writeGeneratedMarkdown(
743
- join(root, '.claude', 'commands', 'audit-code.md'),
1980
+ assetPaths.vscodePromptPath,
744
1981
  renderPromptFile(
745
1982
  {
1983
+ name: 'audit-code',
746
1984
  description: 'Autonomous local loop code auditing',
1985
+ agent: 'auditor',
747
1986
  },
748
1987
  promptBody,
749
1988
  ),
750
1989
  ),
751
1990
  );
1991
+ results.push(
1992
+ await writeGeneratedMarkdown(
1993
+ assetPaths.vscodeAgentPath,
1994
+ renderVSCodeAgentFile(),
1995
+ ),
1996
+ );
1997
+ results.push(
1998
+ await writeGeneratedMarkdown(
1999
+ assetPaths.vscodeMcpConfigPath,
2000
+ renderVSCodeMcpConfig(),
2001
+ ),
2002
+ );
752
2003
  }
753
2004
 
754
- const compatibilityBlockTargets = Object.values(instructionSurfaces).filter(Boolean);
755
-
756
- for (const targetPath of compatibilityBlockTargets) {
2005
+ if (profile.writeAntigravity) {
757
2006
  results.push(
758
- await writeManagedMarkdown(
759
- targetPath,
760
- buildInstallDirective(
761
- relative(dirname(targetPath), installedPromptPath) || `./.audit-code/install/${INSTALLED_PROMPT_FILENAME}`,
762
- ),
2007
+ await writeGeneratedMarkdown(
2008
+ assetPaths.antigravityPlanningGuidePath,
2009
+ renderAntigravityPlanningGuide(root),
763
2010
  ),
764
2011
  );
765
2012
  }
766
2013
 
767
- if (profile.writeCompatibilitySkills) {
768
- const skillTargets = [
769
- join(root, '.opencode', 'skills', 'audit-code'),
770
- join(root, '.claude', 'skills', 'audit-code'),
771
- join(root, '.agents', 'skills', 'audit-code'),
772
- ];
2014
+ const hostGuidance = buildHostCatalog({
2015
+ root,
2016
+ host,
2017
+ assets: assetPaths,
2018
+ });
773
2019
 
774
- for (const targetDir of skillTargets) {
775
- results.push(
776
- await writeGeneratedMarkdown(join(targetDir, 'SKILL.md'), skillSource),
777
- );
778
- results.push(
779
- await writeGeneratedMarkdown(
780
- join(targetDir, 'audit-code.prompt.md'),
781
- promptSource,
782
- ),
783
- );
784
- }
785
- }
2020
+ const installManifest = {
2021
+ contract_version: 'audit-code-install/v1alpha1',
2022
+ host,
2023
+ repo_root: root,
2024
+ installed_prompt_path: installedPromptPath,
2025
+ installed_skill_path: installedSkillPath,
2026
+ install_guide_path: installGuidePath,
2027
+ install_manifest_path: installManifestPath,
2028
+ mcp_server_launcher_path: mcpLauncherPath,
2029
+ source_prompt_path: resolve(promptAssetPath),
2030
+ asset_paths: assetPaths,
2031
+ hosts: hostGuidance,
2032
+ };
786
2033
 
787
2034
  results.push(
788
2035
  await writeGeneratedMarkdown(
@@ -792,25 +2039,20 @@ async function installBootstrap(argv) {
792
2039
  host,
793
2040
  installedPromptPath,
794
2041
  installedSkillPath,
795
- slashCommandSurfaces,
796
- instructionSurfaces,
797
- unsupportedHosts,
2042
+ installManifestPath,
2043
+ mcpLauncherPath,
798
2044
  hostGuidance,
799
2045
  }),
800
2046
  ),
801
2047
  );
2048
+ results.push(await writeGeneratedJson(installManifestPath, installManifest));
802
2049
 
803
2050
  const sessionConfigPath = join(root, '.audit-artifacts', 'session-config.json');
804
- let sessionConfigWritten = false;
805
2051
  if (!(await fileExists(sessionConfigPath))) {
806
- const insideClaudeCode = Boolean(process.env.CLAUDECODE);
807
- const defaultConfig = insideClaudeCode
808
- ? { provider: 'local-subprocess' }
809
- : { provider: 'auto' };
2052
+ const defaultConfig = { provider: 'local-subprocess' };
810
2053
  await mkdir(dirname(sessionConfigPath), { recursive: true });
811
2054
  await writeFile(sessionConfigPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
812
2055
  results.push({ path: sessionConfigPath, mode: 'created' });
813
- sessionConfigWritten = true;
814
2056
  }
815
2057
 
816
2058
  console.log(
@@ -821,16 +2063,34 @@ async function installBootstrap(argv) {
821
2063
  installed_prompt_path: installedPromptPath,
822
2064
  installed_skill_path: installedSkillPath,
823
2065
  install_guide_path: installGuidePath,
2066
+ install_manifest_path: installManifestPath,
2067
+ mcp_server_launcher_path: mcpLauncherPath,
824
2068
  source_prompt_path: resolve(promptAssetPath),
825
2069
  files: results,
826
- slash_command_surfaces: slashCommandSurfaces,
827
- instruction_surfaces: instructionSurfaces,
2070
+ slash_command_surfaces: {
2071
+ vscode_prompt: assetPaths.vscodePromptPath,
2072
+ opencode_command: assetPaths.opencodeCommandPath,
2073
+ },
2074
+ instruction_surfaces: {
2075
+ agents: assetPaths.agentsInstructionsPath,
2076
+ copilot_instructions: assetPaths.copilotInstructionsPath,
2077
+ },
2078
+ mcp_surfaces: {
2079
+ vscode_workspace: assetPaths.vscodeMcpConfigPath,
2080
+ opencode_project: assetPaths.opencodeConfigPath,
2081
+ codex_setup: assetPaths.codexMcpSetupPath,
2082
+ shared_launcher: mcpLauncherPath,
2083
+ claude_desktop_dxt: assetPaths.claudeDesktopDxtPath,
2084
+ claude_desktop_mcpb: assetPaths.claudeDesktopMcpbPath,
2085
+ antigravity_planning_guide: assetPaths.antigravityPlanningGuidePath,
2086
+ },
828
2087
  host_guidance: hostGuidance,
829
- unsupported_hosts: unsupportedHosts,
2088
+ unsupported_hosts: [],
830
2089
  next_steps: [
831
2090
  'Open the repository in your preferred host and follow the matching host_guidance entry.',
832
- `Open ${installGuidePath} for repo-local quick-start steps for VS Code, OpenCode, Claude Code, Claude Desktop, and Antigravity.`,
833
- 'If a host does not auto-discover slash commands, use the installed prompt asset or the listed compatibility instruction surfaces.',
2091
+ `Open ${installGuidePath} for repo-local quick-start steps for Codex, Claude Desktop, OpenCode, VS Code, and Antigravity.`,
2092
+ 'Run `audit-code verify-install` from the repository root to smoke-test the generated launchers and host configs.',
2093
+ 'Use the shared MCP launcher as the source of truth for local stdio MCP registration across hosts.',
834
2094
  ],
835
2095
  },
836
2096
  null,
@@ -899,16 +2159,31 @@ export async function runAuditCodeWrapper({
899
2159
  return;
900
2160
  }
901
2161
 
2162
+ if (argv[0] === 'verify-install') {
2163
+ await verifyInstalledBootstrap(argv.slice(1));
2164
+ return;
2165
+ }
2166
+
902
2167
  if (argv[0] === 'validate') {
903
2168
  await runDistCommand('validate', argv.slice(1));
904
2169
  return;
905
2170
  }
906
2171
 
2172
+ if (argv[0] === 'validate-results') {
2173
+ await runDistCommand('validate-results', argv.slice(1));
2174
+ return;
2175
+ }
2176
+
907
2177
  if (argv[0] === 'explain-task') {
908
2178
  await runDistCommand('explain-task', argv.slice(1));
909
2179
  return;
910
2180
  }
911
2181
 
2182
+ if (argv[0] === 'mcp') {
2183
+ await runDistCommand('mcp', argv.slice(1), { ensureArtifactsDir: true });
2184
+ return;
2185
+ }
2186
+
912
2187
  const wrapperArgs = [...argv];
913
2188
  if (defaultSingleStep && !hasFlag(wrapperArgs, '--single-step')) {
914
2189
  wrapperArgs.push('--single-step');