auditor-lambda 0.2.5 → 0.2.8
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 +35 -7
- package/audit-code-wrapper-lib.mjs +1612 -331
- package/dist/cli.js +397 -38
- package/dist/coverage.d.ts +2 -2
- package/dist/coverage.js +5 -5
- package/dist/extractors/disposition.js +10 -1
- package/dist/extractors/flows.js +7 -1
- package/dist/extractors/pathPatterns.d.ts +3 -0
- package/dist/extractors/pathPatterns.js +15 -0
- package/dist/extractors/risk.js +7 -1
- package/dist/io/artifacts.d.ts +6 -6
- package/dist/io/artifacts.js +14 -17
- package/dist/io/json.d.ts +2 -0
- package/dist/io/json.js +15 -0
- package/dist/io/runArtifacts.d.ts +3 -1
- package/dist/io/runArtifacts.js +20 -5
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +579 -0
- package/dist/orchestrator/advance.js +9 -2
- package/dist/orchestrator/dependencyMap.js +9 -13
- package/dist/orchestrator/executors.js +7 -2
- package/dist/orchestrator/flowRequeue.d.ts +2 -2
- package/dist/orchestrator/flowRequeue.js +16 -3
- package/dist/orchestrator/internalExecutors.d.ts +2 -1
- package/dist/orchestrator/internalExecutors.js +129 -48
- package/dist/orchestrator/requeue.js +10 -4
- package/dist/orchestrator/requeueCommand.js +15 -2
- package/dist/orchestrator/resultIngestion.d.ts +2 -1
- package/dist/orchestrator/resultIngestion.js +26 -6
- package/dist/orchestrator/runtimeValidation.d.ts +7 -2
- package/dist/orchestrator/runtimeValidation.js +61 -49
- package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
- package/dist/orchestrator/state.js +28 -14
- package/dist/orchestrator/taskBuilder.js +4 -2
- package/dist/orchestrator/trivialAudit.d.ts +4 -0
- package/dist/orchestrator/trivialAudit.js +49 -0
- package/dist/prompts/renderWorkerPrompt.js +6 -2
- package/dist/providers/spawnLoggedCommand.js +17 -0
- package/dist/reporting/mergeFindings.js +3 -11
- package/dist/reporting/rootCause.js +92 -9
- package/dist/reporting/synthesis.d.ts +25 -22
- package/dist/reporting/synthesis.js +92 -59
- package/dist/reporting/workBlocks.d.ts +12 -3
- package/dist/reporting/workBlocks.js +124 -70
- package/dist/supervisor/sessionConfig.js +4 -2
- package/dist/types/flows.d.ts +2 -0
- package/dist/types/runtimeValidation.d.ts +2 -1
- package/dist/types.d.ts +8 -6
- package/dist/validation/auditResults.d.ts +5 -2
- package/dist/validation/auditResults.js +335 -43
- package/docs/agent-integrations.md +38 -29
- package/docs/artifacts.md +18 -51
- package/docs/bootstrap-install.md +60 -30
- package/docs/contract.md +25 -117
- package/docs/field-trial-bug-report.md +237 -0
- package/docs/next-steps.md +59 -44
- package/docs/packaging.md +13 -3
- package/docs/production-launch-bar.md +2 -2
- package/docs/production-readiness.md +9 -5
- package/docs/releasing.md +81 -0
- package/docs/session-config.md +20 -1
- package/docs/usage.md +22 -0
- package/package.json +4 -1
- package/schemas/audit_result.schema.json +4 -5
- package/schemas/audit_task.schema.json +10 -0
- package/schemas/runtime_validation_report.schema.json +1 -1
- package/skills/audit-code/SKILL.md +11 -2
- package/skills/audit-code/audit-code.prompt.md +11 -10
- package/schemas/merged_findings.schema.json +0 -19
- package/schemas/root_cause_clusters.schema.json +0 -28
- 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);
|
|
@@ -234,13 +238,17 @@ async function ensureBuilt() {
|
|
|
234
238
|
|
|
235
239
|
function printHelp({ usageName, preferredEntrypoint }) {
|
|
236
240
|
const lines = [
|
|
237
|
-
`Usage: node ${usageName} [--single-step] [--root PATH] [--artifacts-dir PATH] [--results FILE] [--updates FILE] [--external-analyzer-results FILE]`,
|
|
241
|
+
`Usage: node ${usageName} [--single-step] [--root PATH] [--artifacts-dir PATH] [--results FILE] [--batch-results DIR] [--updates FILE] [--external-analyzer-results FILE] [--timeout MS]`,
|
|
238
242
|
'',
|
|
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',
|
|
251
|
+
'- explain-task <task_id> prints the resolved file coverage and current status for a task id',
|
|
244
252
|
'',
|
|
245
253
|
'Primary usage:',
|
|
246
254
|
'- from the repository root, run the wrapper with no arguments',
|
|
@@ -333,132 +341,698 @@ function buildInstallDirective(relativePromptPath) {
|
|
|
333
341
|
].join('\n');
|
|
334
342
|
}
|
|
335
343
|
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
);
|
|
342
350
|
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
host: 'vscode',
|
|
346
|
-
label: 'VS Code',
|
|
347
|
-
support_level: 'supported',
|
|
348
|
-
setup_kind: 'repo-local-slash-command',
|
|
349
|
-
summary:
|
|
350
|
-
'Use the generated VS Code / Copilot prompt surface, then invoke `/audit-code` in chat.',
|
|
351
|
-
primary_path: slashCommandSurfaces.vscode_prompt,
|
|
352
|
-
supporting_paths: [
|
|
353
|
-
instructionSurfaces.copilot_instructions,
|
|
354
|
-
instructionSurfaces.agents,
|
|
355
|
-
].filter(Boolean),
|
|
356
|
-
steps: [
|
|
357
|
-
'Open this repository in VS Code or GitHub Copilot Chat.',
|
|
358
|
-
'Invoke `/audit-code` in chat.',
|
|
359
|
-
'Use the integrated terminal and run `audit-code` only when you intentionally need the repo-local backend fallback.',
|
|
360
|
-
],
|
|
361
|
-
});
|
|
351
|
+
if (blockPattern.test(normalized)) {
|
|
352
|
+
return normalized.replace(blockPattern, blockContent);
|
|
362
353
|
}
|
|
363
354
|
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
host: 'opencode',
|
|
367
|
-
label: 'OpenCode',
|
|
368
|
-
support_level: 'supported',
|
|
369
|
-
setup_kind: 'repo-local-slash-command',
|
|
370
|
-
summary:
|
|
371
|
-
'Use the generated OpenCode command surface so `/audit-code` is available without extra provider flags.',
|
|
372
|
-
primary_path: slashCommandSurfaces.opencode_command,
|
|
373
|
-
supporting_paths: [instructionSurfaces.agents].filter(Boolean),
|
|
374
|
-
steps: [
|
|
375
|
-
'Open this repository in OpenCode.',
|
|
376
|
-
'Invoke `/audit-code` from the OpenCode command surface.',
|
|
377
|
-
'Use the repo-local backend wrapper only when you intentionally need the fallback automation path.',
|
|
378
|
-
],
|
|
379
|
-
});
|
|
355
|
+
if (normalized.trim().length === 0) {
|
|
356
|
+
return `${blockContent}\n`;
|
|
380
357
|
}
|
|
381
358
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
host: 'claude-code',
|
|
385
|
-
label: 'Claude Code',
|
|
386
|
-
support_level: 'supported',
|
|
387
|
-
setup_kind: 'repo-local-slash-command',
|
|
388
|
-
summary:
|
|
389
|
-
'Use the generated Claude Code command surface so `/audit-code` is available inside the repository without extra provider wiring.',
|
|
390
|
-
primary_path: slashCommandSurfaces.claude_code_command,
|
|
391
|
-
supporting_paths: [instructionSurfaces.claude].filter(Boolean),
|
|
392
|
-
steps: [
|
|
393
|
-
'Open this repository in Claude Code.',
|
|
394
|
-
'Invoke `/audit-code` from the Claude Code project command surface.',
|
|
395
|
-
'Use the terminal fallback and run `audit-code` only when you intentionally need the repo-local backend wrapper.',
|
|
396
|
-
],
|
|
397
|
-
});
|
|
398
|
-
}
|
|
359
|
+
return `${normalized.replace(/\s+$/u, '')}\n\n${blockContent}\n`;
|
|
360
|
+
}
|
|
399
361
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
'Invoke `/audit-code` conversationally inside Claude Desktop after the prompt is available.',
|
|
412
|
-
'If you intentionally need the repo-local backend fallback instead, run `audit-code` from the repository root.',
|
|
413
|
-
],
|
|
414
|
-
});
|
|
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
|
+
}
|
|
415
373
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
steps: [
|
|
426
|
-
'Import the installed prompt asset into Antigravity\'s prompt or instruction surface when that surface is available.',
|
|
427
|
-
'Invoke `/audit-code` conversationally inside Antigravity.',
|
|
428
|
-
'If you prefer the backend fallback, run `audit-code` from an Antigravity-managed terminal with `local-subprocess` first.',
|
|
429
|
-
],
|
|
430
|
-
});
|
|
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
|
+
}
|
|
431
383
|
|
|
432
|
-
|
|
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
|
+
};
|
|
433
392
|
}
|
|
434
393
|
|
|
435
|
-
function
|
|
436
|
-
const
|
|
437
|
-
|
|
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
|
+
'---',
|
|
438
413
|
'',
|
|
439
|
-
|
|
440
|
-
`Setup kind: ${guide.setup_kind}`,
|
|
414
|
+
'# Auditor Agent',
|
|
441
415
|
'',
|
|
442
|
-
|
|
416
|
+
'Use the installed auditor MCP server as the primary integration surface for the audit workflow.',
|
|
443
417
|
'',
|
|
444
|
-
'
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
}
|
|
447
465
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
+
}
|
|
697
|
+
|
|
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);
|
|
452
704
|
}
|
|
705
|
+
table[index] = value >>> 0;
|
|
453
706
|
}
|
|
707
|
+
return table;
|
|
708
|
+
}
|
|
454
709
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
710
|
+
const CRC32_TABLE = createCrc32Table();
|
|
711
|
+
|
|
712
|
+
function crc32(buffer) {
|
|
713
|
+
let crc = 0xffffffff;
|
|
714
|
+
for (const byte of buffer) {
|
|
715
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
458
716
|
}
|
|
459
|
-
|
|
717
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
718
|
+
}
|
|
460
719
|
|
|
461
|
-
|
|
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
|
+
}));
|
|
462
1036
|
}
|
|
463
1037
|
|
|
464
1038
|
function renderInstallGuide({
|
|
@@ -466,180 +1040,753 @@ function renderInstallGuide({
|
|
|
466
1040
|
host,
|
|
467
1041
|
installedPromptPath,
|
|
468
1042
|
installedSkillPath,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
unsupportedHosts,
|
|
1043
|
+
installManifestPath,
|
|
1044
|
+
mcpLauncherPath,
|
|
472
1045
|
hostGuidance,
|
|
473
1046
|
}) {
|
|
474
|
-
const slashCommandPaths = Object.values(slashCommandSurfaces).filter(Boolean);
|
|
475
|
-
const instructionPaths = Object.values(instructionSurfaces).filter(Boolean);
|
|
476
1047
|
const lines = [
|
|
477
1048
|
'# audit-code bootstrap guide',
|
|
478
1049
|
'',
|
|
479
1050
|
'The canonical product route is `/audit-code` in conversation.',
|
|
480
1051
|
'',
|
|
481
|
-
'
|
|
1052
|
+
'Shared repo-local assets:',
|
|
482
1053
|
`- prompt asset: \`${toRepoRelativePath(root, installedPromptPath)}\``,
|
|
483
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:',
|
|
484
1059
|
];
|
|
485
1060
|
|
|
486
|
-
|
|
487
|
-
lines.push(
|
|
488
|
-
|
|
489
|
-
|
|
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}`);
|
|
490
1083
|
}
|
|
491
1084
|
}
|
|
492
1085
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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);
|
|
497
1214
|
}
|
|
1215
|
+
pending.clear();
|
|
498
1216
|
}
|
|
499
1217
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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;
|
|
1300
|
+
}
|
|
1301
|
+
child.stdin.write(
|
|
1302
|
+
encodeMcpMessage({
|
|
1303
|
+
jsonrpc: '2.0',
|
|
1304
|
+
method,
|
|
1305
|
+
params,
|
|
1306
|
+
}),
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async function close() {
|
|
1311
|
+
if (!exitError) {
|
|
1312
|
+
await request('shutdown', 'shutdown');
|
|
1313
|
+
notify('exit');
|
|
1314
|
+
child.stdin.end();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
await new Promise((resolvePromise) => {
|
|
1318
|
+
child.on('exit', () => resolvePromise());
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
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
|
+
);
|
|
503
1428
|
}
|
|
1429
|
+
}
|
|
504
1430
|
|
|
505
|
-
|
|
506
|
-
|
|
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}`);
|
|
507
1435
|
}
|
|
1436
|
+
return content.length;
|
|
1437
|
+
}
|
|
508
1438
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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,
|
|
1447
|
+
);
|
|
1448
|
+
const installGuidePath = join(
|
|
1449
|
+
root,
|
|
1450
|
+
'.audit-code',
|
|
1451
|
+
'install',
|
|
1452
|
+
INSTALL_GUIDE_FILENAME,
|
|
513
1453
|
);
|
|
514
1454
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1455
|
+
await assertDirectoryExists(root, 'Target repository root');
|
|
1456
|
+
|
|
1457
|
+
const generalChecks = [];
|
|
1458
|
+
const hostResults = [];
|
|
1459
|
+
let installManifest;
|
|
1460
|
+
|
|
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
|
+
);
|
|
519
1468
|
}
|
|
520
|
-
|
|
1469
|
+
return {
|
|
1470
|
+
summary: 'Install manifest parsed successfully.',
|
|
1471
|
+
path: installManifestPath,
|
|
1472
|
+
};
|
|
1473
|
+
});
|
|
521
1474
|
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
+
),
|
|
526
1489
|
);
|
|
1490
|
+
process.exitCode = 1;
|
|
1491
|
+
return;
|
|
527
1492
|
}
|
|
528
1493
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
function upsertManagedBlock(existingContent, blockContent) {
|
|
534
|
-
const normalized = normalizeNewlines(existingContent);
|
|
535
|
-
const blockPattern = new RegExp(
|
|
536
|
-
`${INSTALL_MARKER_START}[\\s\\S]*?${INSTALL_MARKER_END}`,
|
|
537
|
-
'u',
|
|
1494
|
+
const assetPaths = installManifest.asset_paths ?? {};
|
|
1495
|
+
const hostCatalog = new Map(
|
|
1496
|
+
(installManifest.hosts ?? []).map((entry) => [entry.host, entry]),
|
|
538
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
|
+
});
|
|
539
1512
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
});
|
|
543
1520
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
});
|
|
547
1528
|
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
});
|
|
550
1542
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
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
|
+
});
|
|
562
1561
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
await writeFile(targetPath, content, 'utf8');
|
|
567
|
-
return {
|
|
568
|
-
path: targetPath,
|
|
569
|
-
mode: existed ? 'updated' : 'created',
|
|
570
|
-
};
|
|
571
|
-
}
|
|
1562
|
+
for (const hostKey of selectedHosts) {
|
|
1563
|
+
const checks = [];
|
|
1564
|
+
const hostEntry = hostCatalog.get(hostKey);
|
|
572
1565
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
writeClaudeCommand: true,
|
|
583
|
-
writeCompatibilitySkills: true,
|
|
584
|
-
};
|
|
585
|
-
case 'copilot':
|
|
586
|
-
return {
|
|
587
|
-
writeVSCodePrompt: true,
|
|
588
|
-
writeCopilotInstructions: true,
|
|
589
|
-
writeAgents: false,
|
|
590
|
-
writeClaudeMemory: false,
|
|
591
|
-
writeOpenCodeCommand: false,
|
|
592
|
-
writeClaudeCommand: false,
|
|
593
|
-
writeCompatibilitySkills: false,
|
|
594
|
-
};
|
|
595
|
-
case 'vscode':
|
|
596
|
-
return {
|
|
597
|
-
writeVSCodePrompt: true,
|
|
598
|
-
writeCopilotInstructions: true,
|
|
599
|
-
writeAgents: true,
|
|
600
|
-
writeClaudeMemory: true,
|
|
601
|
-
writeOpenCodeCommand: false,
|
|
602
|
-
writeClaudeCommand: false,
|
|
603
|
-
writeCompatibilitySkills: false,
|
|
604
|
-
};
|
|
605
|
-
case 'opencode':
|
|
606
|
-
return {
|
|
607
|
-
writeVSCodePrompt: false,
|
|
608
|
-
writeCopilotInstructions: false,
|
|
609
|
-
writeAgents: true,
|
|
610
|
-
writeClaudeMemory: false,
|
|
611
|
-
writeOpenCodeCommand: true,
|
|
612
|
-
writeClaudeCommand: false,
|
|
613
|
-
writeCompatibilitySkills: true,
|
|
614
|
-
};
|
|
615
|
-
case 'claude-code':
|
|
616
|
-
return {
|
|
617
|
-
writeVSCodePrompt: false,
|
|
618
|
-
writeCopilotInstructions: false,
|
|
619
|
-
writeAgents: false,
|
|
620
|
-
writeClaudeMemory: true,
|
|
621
|
-
writeOpenCodeCommand: false,
|
|
622
|
-
writeClaudeCommand: true,
|
|
623
|
-
writeCompatibilitySkills: true,
|
|
624
|
-
};
|
|
625
|
-
default:
|
|
626
|
-
throw new Error(
|
|
627
|
-
`Unsupported host "${host}". Supported hosts: all, copilot, vscode, opencode, claude-code.`,
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
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
|
+
}
|
|
631
1575
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
+
}
|
|
639
1758
|
|
|
640
|
-
|
|
641
|
-
|
|
1759
|
+
hostResults.push({
|
|
1760
|
+
host: hostKey,
|
|
1761
|
+
status: checks.some((check) => check.status === 'error') ? 'error' : 'ok',
|
|
1762
|
+
checks,
|
|
1763
|
+
});
|
|
642
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;
|
|
643
1790
|
}
|
|
644
1791
|
|
|
645
1792
|
async function installBootstrap(argv) {
|
|
@@ -654,43 +1801,67 @@ async function installBootstrap(argv) {
|
|
|
654
1801
|
const legacyInstalledPromptPath = join(root, '.audit-code', 'install', 'audit-code.prompt.md');
|
|
655
1802
|
const installedSkillPath = join(root, '.audit-code', 'install', 'SKILL.md');
|
|
656
1803
|
const installGuidePath = join(root, '.audit-code', 'install', INSTALL_GUIDE_FILENAME);
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
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')
|
|
1818
|
+
: null,
|
|
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')
|
|
660
1827
|
: null,
|
|
661
|
-
|
|
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
|
|
662
1841
|
? join(root, '.opencode', 'commands', 'audit-code.md')
|
|
663
1842
|
: null,
|
|
664
|
-
|
|
665
|
-
? join(root, '.
|
|
1843
|
+
opencodeConfigPath: profile.writeOpenCode
|
|
1844
|
+
? join(root, 'opencode.json')
|
|
666
1845
|
: null,
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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')
|
|
671
1863
|
: null,
|
|
672
|
-
agents: profile.writeAgents ? join(root, 'AGENTS.md') : null,
|
|
673
|
-
claude: profile.writeClaudeMemory ? join(root, 'CLAUDE.md') : null,
|
|
674
1864
|
};
|
|
675
|
-
const unsupportedHosts = host === 'all'
|
|
676
|
-
? [
|
|
677
|
-
{
|
|
678
|
-
host: 'claude-desktop',
|
|
679
|
-
reason:
|
|
680
|
-
'No verified project-local slash-command installation surface is currently shipped for Claude Desktop.',
|
|
681
|
-
},
|
|
682
|
-
{
|
|
683
|
-
host: 'antigravity',
|
|
684
|
-
reason:
|
|
685
|
-
'No verified repo-local slash-command installation surface is currently shipped for Antigravity.',
|
|
686
|
-
},
|
|
687
|
-
]
|
|
688
|
-
: [];
|
|
689
|
-
const hostGuidance = buildInstallHostGuidance({
|
|
690
|
-
installedPromptPath,
|
|
691
|
-
slashCommandSurfaces,
|
|
692
|
-
instructionSurfaces,
|
|
693
|
-
});
|
|
694
1865
|
|
|
695
1866
|
const results = [];
|
|
696
1867
|
if (await fileExists(legacyInstalledPromptPath)) {
|
|
@@ -704,84 +1875,161 @@ async function installBootstrap(argv) {
|
|
|
704
1875
|
);
|
|
705
1876
|
results.push(await writeGeneratedMarkdown(installedSkillPath, skillSource));
|
|
706
1877
|
|
|
707
|
-
|
|
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) {
|
|
708
1888
|
results.push(
|
|
709
|
-
await
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
{
|
|
713
|
-
name: 'audit-code',
|
|
714
|
-
description: 'Autonomous local loop code auditing',
|
|
715
|
-
agent: 'agent',
|
|
716
|
-
},
|
|
717
|
-
promptBody,
|
|
1889
|
+
await writeManagedMarkdown(
|
|
1890
|
+
targetPath,
|
|
1891
|
+
buildInstallDirective(
|
|
1892
|
+
relative(dirname(targetPath), installedPromptPath) || `./.audit-code/install/${INSTALLED_PROMPT_FILENAME}`,
|
|
718
1893
|
),
|
|
719
1894
|
),
|
|
720
1895
|
);
|
|
721
1896
|
}
|
|
722
1897
|
|
|
723
|
-
if (profile.
|
|
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) {
|
|
724
1944
|
results.push(
|
|
725
1945
|
await writeGeneratedMarkdown(
|
|
726
|
-
|
|
1946
|
+
assetPaths.opencodeCommandPath,
|
|
727
1947
|
renderPromptFile(
|
|
728
1948
|
{
|
|
729
1949
|
description: 'Autonomous local loop code auditing',
|
|
730
|
-
agent: '
|
|
1950
|
+
agent: 'auditor',
|
|
731
1951
|
subtask: false,
|
|
732
1952
|
},
|
|
733
1953
|
promptBody,
|
|
734
1954
|
),
|
|
735
1955
|
),
|
|
736
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
|
+
);
|
|
737
1975
|
}
|
|
738
1976
|
|
|
739
|
-
if (profile.
|
|
1977
|
+
if (profile.writeVSCode) {
|
|
740
1978
|
results.push(
|
|
741
1979
|
await writeGeneratedMarkdown(
|
|
742
|
-
|
|
1980
|
+
assetPaths.vscodePromptPath,
|
|
743
1981
|
renderPromptFile(
|
|
744
1982
|
{
|
|
1983
|
+
name: 'audit-code',
|
|
745
1984
|
description: 'Autonomous local loop code auditing',
|
|
1985
|
+
agent: 'auditor',
|
|
746
1986
|
},
|
|
747
1987
|
promptBody,
|
|
748
1988
|
),
|
|
749
1989
|
),
|
|
750
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
|
+
);
|
|
751
2003
|
}
|
|
752
2004
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
for (const targetPath of compatibilityBlockTargets) {
|
|
2005
|
+
if (profile.writeAntigravity) {
|
|
756
2006
|
results.push(
|
|
757
|
-
await
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
relative(dirname(targetPath), installedPromptPath) || `./.audit-code/install/${INSTALLED_PROMPT_FILENAME}`,
|
|
761
|
-
),
|
|
2007
|
+
await writeGeneratedMarkdown(
|
|
2008
|
+
assetPaths.antigravityPlanningGuidePath,
|
|
2009
|
+
renderAntigravityPlanningGuide(root),
|
|
762
2010
|
),
|
|
763
2011
|
);
|
|
764
2012
|
}
|
|
765
2013
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
];
|
|
2014
|
+
const hostGuidance = buildHostCatalog({
|
|
2015
|
+
root,
|
|
2016
|
+
host,
|
|
2017
|
+
assets: assetPaths,
|
|
2018
|
+
});
|
|
772
2019
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
+
};
|
|
785
2033
|
|
|
786
2034
|
results.push(
|
|
787
2035
|
await writeGeneratedMarkdown(
|
|
@@ -791,25 +2039,20 @@ async function installBootstrap(argv) {
|
|
|
791
2039
|
host,
|
|
792
2040
|
installedPromptPath,
|
|
793
2041
|
installedSkillPath,
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
unsupportedHosts,
|
|
2042
|
+
installManifestPath,
|
|
2043
|
+
mcpLauncherPath,
|
|
797
2044
|
hostGuidance,
|
|
798
2045
|
}),
|
|
799
2046
|
),
|
|
800
2047
|
);
|
|
2048
|
+
results.push(await writeGeneratedJson(installManifestPath, installManifest));
|
|
801
2049
|
|
|
802
2050
|
const sessionConfigPath = join(root, '.audit-artifacts', 'session-config.json');
|
|
803
|
-
let sessionConfigWritten = false;
|
|
804
2051
|
if (!(await fileExists(sessionConfigPath))) {
|
|
805
|
-
const
|
|
806
|
-
const defaultConfig = insideClaudeCode
|
|
807
|
-
? { provider: 'local-subprocess' }
|
|
808
|
-
: { provider: 'auto' };
|
|
2052
|
+
const defaultConfig = { provider: 'auto' };
|
|
809
2053
|
await mkdir(dirname(sessionConfigPath), { recursive: true });
|
|
810
2054
|
await writeFile(sessionConfigPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
|
|
811
2055
|
results.push({ path: sessionConfigPath, mode: 'created' });
|
|
812
|
-
sessionConfigWritten = true;
|
|
813
2056
|
}
|
|
814
2057
|
|
|
815
2058
|
console.log(
|
|
@@ -820,16 +2063,34 @@ async function installBootstrap(argv) {
|
|
|
820
2063
|
installed_prompt_path: installedPromptPath,
|
|
821
2064
|
installed_skill_path: installedSkillPath,
|
|
822
2065
|
install_guide_path: installGuidePath,
|
|
2066
|
+
install_manifest_path: installManifestPath,
|
|
2067
|
+
mcp_server_launcher_path: mcpLauncherPath,
|
|
823
2068
|
source_prompt_path: resolve(promptAssetPath),
|
|
824
2069
|
files: results,
|
|
825
|
-
slash_command_surfaces:
|
|
826
|
-
|
|
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
|
+
},
|
|
827
2087
|
host_guidance: hostGuidance,
|
|
828
|
-
unsupported_hosts:
|
|
2088
|
+
unsupported_hosts: [],
|
|
829
2089
|
next_steps: [
|
|
830
2090
|
'Open the repository in your preferred host and follow the matching host_guidance entry.',
|
|
831
|
-
`Open ${installGuidePath} for repo-local quick-start steps for
|
|
832
|
-
'
|
|
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.',
|
|
833
2094
|
],
|
|
834
2095
|
},
|
|
835
2096
|
null,
|
|
@@ -898,11 +2159,31 @@ export async function runAuditCodeWrapper({
|
|
|
898
2159
|
return;
|
|
899
2160
|
}
|
|
900
2161
|
|
|
2162
|
+
if (argv[0] === 'verify-install') {
|
|
2163
|
+
await verifyInstalledBootstrap(argv.slice(1));
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
901
2167
|
if (argv[0] === 'validate') {
|
|
902
2168
|
await runDistCommand('validate', argv.slice(1));
|
|
903
2169
|
return;
|
|
904
2170
|
}
|
|
905
2171
|
|
|
2172
|
+
if (argv[0] === 'validate-results') {
|
|
2173
|
+
await runDistCommand('validate-results', argv.slice(1));
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
if (argv[0] === 'explain-task') {
|
|
2178
|
+
await runDistCommand('explain-task', argv.slice(1));
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
if (argv[0] === 'mcp') {
|
|
2183
|
+
await runDistCommand('mcp', argv.slice(1), { ensureArtifactsDir: true });
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
906
2187
|
const wrapperArgs = [...argv];
|
|
907
2188
|
if (defaultSingleStep && !hasFlag(wrapperArgs, '--single-step')) {
|
|
908
2189
|
wrapperArgs.push('--single-step');
|