argusqa-os 9.6.6 → 9.7.4

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/glama.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "$schema": "https://glama.ai/mcp/schemas/server.json",
3
3
  "name": "argus",
4
- "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 9 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types), argus_visual_diff (screenshot baseline comparison, updateBaseline flag), argus_pr_validate (PR diff → affected routes → targeted audit → blocked flag). 137 test blocks, 634 hard assertions, 63 detection categories.",
4
+ "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 9 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types), argus_visual_diff (screenshot baseline comparison, updateBaseline flag), argus_pr_validate (PR diff → affected routes → targeted audit → blocked flag). Every finding is post-processed with intelligent baseline filtering (cross-run noise classifier) and root cause linking (recent git commits mapped to new findings). 142 test blocks, 688 hard assertions, 67 detection categories.",
5
5
  "maintainers": ["ironclawdevs27"],
6
6
  "tools": [
7
7
  {
8
8
  "name": "argus_audit",
9
- "description": "Fast QA audit — JS errors, network failures (4xx/5xx), API frequency loops, CSS cascade issues, SEO violations, security headers, accessibility, and content. Returns { findings, summary }. Supports cache: true to skip re-crawl on repeat calls."
9
+ "description": "Fast QA audit — JS errors, network failures (4xx/5xx), CORS, API frequency loops, slow/blocking third-party requests, API contract violations, SEO violations, security headers, content quality, DevTools Issues panel, and HTTPS enforcement. Returns { findings, summary }. Supports cache: true to skip re-crawl on repeat calls."
10
10
  },
11
11
  {
12
12
  "name": "argus_audit_full",
package/package.json CHANGED
@@ -1,71 +1,77 @@
1
- {
2
- "name": "argusqa-os",
3
- "version": "9.6.6",
4
- "mcpName": "io.github.ironclawdevs27/argus",
5
- "description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
6
- "keywords": [
7
- "mcp",
8
- "mcp-server",
9
- "claude",
10
- "qa",
11
- "testing",
12
- "accessibility",
13
- "automation",
14
- "chrome-devtools",
15
- "web-testing"
16
- ],
17
- "author": "ironclawdevs",
18
- "license": "MIT",
19
- "homepage": "https://argus-qa.com",
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/ironclawdevs27/Argus.git"
23
- },
24
- "files": [
25
- "src/",
26
- ".mcp.json",
27
- "glama.json"
28
- ],
29
- "type": "module",
30
- "engines": {
31
- "node": ">=20.19.0"
32
- },
33
- "bin": {
34
- "argus": "src/cli/init.js",
35
- "argus-mcp": "src/mcp-server.js",
36
- "argusqa-os": "src/mcp-server.js",
37
- "argus-pr-validate": "src/cli/pr-validate.js"
38
- },
39
- "scripts": {
40
- "setup": "node -e \"import('fs').then(fs => fs.default.mkdirSync('./reports', { recursive: true }))\"",
41
- "init": "node src/cli/init.js",
42
- "crawl": "node src/orchestration/crawl-and-report.js",
43
- "compare": "node src/orchestration/env-comparison.js",
44
- "watch": "node src/orchestration/watch-mode.js",
45
- "server": "node src/server/index.js",
46
- "harness": "node test-harness/server.js",
47
- "harness:staging": "PORT=3101 node test-harness/server.js",
48
- "test:harness": "node test-harness/validate.js",
49
- "test:unit": "vitest run test/unit",
50
- "test": "npm run test:unit && npm run test:harness",
51
- "report:html": "node src/utils/html-reporter.js",
52
- "mcp-server": "node src/mcp-server.js"
53
- },
54
- "dependencies": {
55
- "@modelcontextprotocol/sdk": "^1.29.0",
56
- "@opentelemetry/api": "^1.9.1",
57
- "@opentelemetry/sdk-node": "^0.218.0",
58
- "@slack/web-api": "^7.16.0",
59
- "axe-core": "^4.12.0",
60
- "dotenv": "^17.4.2",
61
- "express": "^5.2.1",
62
- "pino": "^10.3.1",
63
- "pino-pretty": "^13.1.3",
64
- "pixelmatch": "^7.2.0",
65
- "pngjs": "^7.0.0",
66
- "zod": "^4.4.3"
67
- },
68
- "devDependencies": {
69
- "vitest": "^4.1.8"
70
- }
71
- }
1
+ {
2
+ "name": "argusqa-os",
3
+ "version": "9.7.4",
4
+ "mcpName": "io.github.ironclawdevs27/argus",
5
+ "description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
6
+ "keywords": [
7
+ "mcp",
8
+ "mcp-server",
9
+ "claude",
10
+ "qa",
11
+ "testing",
12
+ "accessibility",
13
+ "automation",
14
+ "chrome-devtools",
15
+ "web-testing"
16
+ ],
17
+ "author": "ironclawdevs",
18
+ "license": "MIT",
19
+ "homepage": "https://argus-qa.com",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ironclawdevs27/Argus.git"
23
+ },
24
+ "files": [
25
+ "src/",
26
+ ".mcp.json",
27
+ "glama.json"
28
+ ],
29
+ "type": "module",
30
+ "engines": {
31
+ "node": ">=20.19.0"
32
+ },
33
+ "bin": {
34
+ "argus": "src/cli/init.js",
35
+ "argus-mcp": "src/mcp-server.js",
36
+ "argusqa-os": "src/mcp-server.js",
37
+ "argus-pr-validate": "src/cli/pr-validate.js",
38
+ "argus-chrome": "src/cli/chrome-launcher.js",
39
+ "argus-doctor": "src/cli/doctor.js"
40
+ },
41
+ "scripts": {
42
+ "setup": "node -e \"import('fs').then(fs => fs.default.mkdirSync('./reports', { recursive: true }))\"",
43
+ "init": "node src/cli/init.js",
44
+ "chrome": "node src/cli/chrome-launcher.js",
45
+ "doctor": "node src/cli/doctor.js",
46
+ "crawl": "node src/orchestration/crawl-and-report.js",
47
+ "compare": "node src/orchestration/env-comparison.js",
48
+ "watch": "node src/orchestration/watch-mode.js",
49
+ "server": "node src/server/index.js",
50
+ "harness": "node test-harness/server.js",
51
+ "harness:staging": "node test-harness/server.js --port=3101 --staging",
52
+ "test:harness": "node --env-file=test-harness/.env.harness test-harness/validate.js",
53
+ "test:harness:log": "node test-harness/run-with-log.mjs",
54
+ "test:unit": "vitest run test/unit",
55
+ "test": "npm run test:unit && npm run test:harness",
56
+ "report:html": "node src/utils/html-reporter.js",
57
+ "report:pdf": "node src/utils/pdf-exporter.js",
58
+ "mcp-server": "node src/mcp-server.js"
59
+ },
60
+ "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.29.0",
62
+ "@opentelemetry/api": "^1.9.1",
63
+ "@opentelemetry/sdk-node": "^0.218.0",
64
+ "@slack/web-api": "^7.16.0",
65
+ "axe-core": "^4.12.0",
66
+ "dotenv": "^17.4.2",
67
+ "express": "^5.2.1",
68
+ "pino": "^10.3.1",
69
+ "pino-pretty": "^13.1.3",
70
+ "pixelmatch": "^7.2.0",
71
+ "pngjs": "^7.0.0",
72
+ "zod": "^4.4.3"
73
+ },
74
+ "devDependencies": {
75
+ "vitest": "^4.1.8"
76
+ }
77
+ }
@@ -52,15 +52,22 @@ export class CdpBrowserAdapter {
52
52
  resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
53
53
 
54
54
  // ── Network & performance ───────────────────────────────────────────────────
55
- getNetworkRequest(reqId) { return this._mcp.get_network_request({ requestId: reqId }); }
55
+ // chrome-devtools-mcp expects the wire parameter "reqid" (sending "requestId"
56
+ // is rejected with an Unknown-argument error). Callers still pass the numeric
57
+ // requestId parsed from list_network_requests.
58
+ getNetworkRequest(reqId) { return this._mcp.get_network_request({ reqid: reqId }); }
56
59
  lighthouse(url, opts = {}) { return this._mcp.lighthouse_audit({ url, ...opts }); }
57
60
  startTrace() { return this._mcp.performance_start_trace({}); }
58
61
  stopTrace() { return this._mcp.performance_stop_trace({}); }
59
62
  analyzeInsight(opts) { return this._mcp.performance_analyze_insight(opts); }
60
63
 
61
64
  // ── Tab management ─────────────────────────────────────────────────────────
65
+ // list_pages returns markdown text ("## Pages\n1: <url> [selected]") like all
66
+ // MCP responses — callers parse with parseListPagesResponse (mcp-parsers.js).
62
67
  listPages() { return this._mcp.list_pages({}); }
63
- selectPage(tabId) { return this._mcp.select_page({ pageId: tabId }); }
68
+ // select_page validates pageId as a number — coerce so callers may pass the
69
+ // string tabId they received from an MCP tool argument.
70
+ selectPage(tabId) { return this._mcp.select_page({ pageId: Number(tabId) }); }
64
71
 
65
72
  // ── Lifecycle ───────────────────────────────────────────────────────────────
66
73
  close() { return this._mcp.close(); }
@@ -82,7 +89,8 @@ export class CdpBrowserAdapter {
82
89
  /**
83
90
  * Raw pass-through to list_console_messages with custom args.
84
91
  * Used by issues-analyzer.js for the DevTools Issues panel
85
- * (types: ['issue']) which returns structured data, not text.
92
+ * (types: ['issue']). Like all MCP responses, the result is markdown
93
+ * text ("msgid=N [issue] text") — parse with parseConsoleMsgResponse.
86
94
  */
87
95
  listConsoleRaw(args = {}) { return this._mcp.list_console_messages(args); }
88
96
  }
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Argus Chrome Launcher — npm run chrome / argus-chrome
4
+ *
5
+ * Finds system Chrome / Chromium on Windows, macOS, and Linux,
6
+ * then launches it with the remote debugging port Argus needs.
7
+ *
8
+ * Exported for testing:
9
+ * findChrome() — returns path string or null
10
+ * launchChrome(opts) — returns { chromePath, process }
11
+ *
12
+ * CLI flags:
13
+ * --port=N debugging port (default: 9222)
14
+ * --headless launch headless (default: visible)
15
+ * --url=URL initial URL (default: about:blank)
16
+ */
17
+
18
+ import { execFileSync, spawn } from 'child_process';
19
+ import fs from 'fs';
20
+ import os from 'os';
21
+ import path from 'path';
22
+ import { fileURLToPath } from 'url';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+
26
+ const CHROME_PATHS = {
27
+ win32: [
28
+ process.env.LOCALAPPDATA
29
+ ? path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome', 'Application', 'chrome.exe')
30
+ : null,
31
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
32
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
33
+ process.env.LOCALAPPDATA
34
+ ? path.join(process.env.LOCALAPPDATA, 'Chromium', 'Application', 'chrome.exe')
35
+ : null,
36
+ ].filter(Boolean),
37
+ darwin: [
38
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
39
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
40
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
41
+ ],
42
+ linux: [
43
+ '/usr/bin/google-chrome',
44
+ '/usr/bin/google-chrome-stable',
45
+ '/usr/bin/chromium-browser',
46
+ '/usr/bin/chromium',
47
+ '/snap/bin/chromium',
48
+ ],
49
+ };
50
+
51
+ /**
52
+ * Find the first Chrome/Chromium binary available on this system.
53
+ * Returns the absolute path string or null if none found.
54
+ *
55
+ * Resolution order:
56
+ * 1. ARGUS_CHROME_PATH env var
57
+ * 2. Known platform-specific paths
58
+ * 3. `which` / `where` fallback
59
+ *
60
+ * @returns {string|null}
61
+ */
62
+ export function findChrome() {
63
+ if (process.env.ARGUS_CHROME_PATH && fs.existsSync(process.env.ARGUS_CHROME_PATH)) {
64
+ return process.env.ARGUS_CHROME_PATH;
65
+ }
66
+
67
+ const platform = process.platform;
68
+ const candidates = CHROME_PATHS[platform] ?? CHROME_PATHS.linux;
69
+ for (const p of candidates) {
70
+ if (p && fs.existsSync(p)) return p;
71
+ }
72
+
73
+ try {
74
+ const cmd = platform === 'win32' ? 'where' : 'which';
75
+ const names = platform === 'win32'
76
+ ? ['chrome', 'chromium']
77
+ : ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium'];
78
+ for (const name of names) {
79
+ try {
80
+ const found = execFileSync(cmd, [name], {
81
+ encoding: 'utf8',
82
+ stdio: ['pipe', 'pipe', 'ignore'],
83
+ }).trim().split('\n')[0].trim();
84
+ if (found && fs.existsSync(found)) return found;
85
+ } catch { /* binary not in PATH */ }
86
+ }
87
+ } catch { /* which/where not available */ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Launch Chrome with remote debugging enabled and return the spawned process.
94
+ *
95
+ * @param {{ port?: number, headless?: boolean, userDataDir?: string, url?: string }} [options]
96
+ * @returns {{ chromePath: string, process: import('child_process').ChildProcess }}
97
+ * @throws {Error} if Chrome binary cannot be found
98
+ */
99
+ export function launchChrome(options = {}) {
100
+ const {
101
+ port = 9222,
102
+ headless = false,
103
+ userDataDir = path.join(os.tmpdir(), 'argus-chrome'),
104
+ url = 'about:blank',
105
+ } = options;
106
+
107
+ const chromePath = findChrome();
108
+ if (!chromePath) {
109
+ throw new Error(
110
+ 'Chrome not found. Install Google Chrome, then retry.\n' +
111
+ ' Download: https://www.google.com/chrome/\n' +
112
+ ' Or set ARGUS_CHROME_PATH=/path/to/chrome.'
113
+ );
114
+ }
115
+
116
+ const flags = [
117
+ `--remote-debugging-port=${port}`,
118
+ `--user-data-dir=${userDataDir}`,
119
+ '--no-first-run',
120
+ '--no-default-browser-check',
121
+ '--disable-background-networking',
122
+ '--disable-sync',
123
+ '--metrics-recording-only',
124
+ '--no-sandbox',
125
+ '--disable-gpu',
126
+ ];
127
+ if (headless) flags.push('--headless=new');
128
+ flags.push(url);
129
+
130
+ const child = spawn(chromePath, flags, { stdio: 'ignore', detached: true });
131
+ child.unref();
132
+ return { chromePath, process: child };
133
+ }
134
+
135
+ function parseCliArgs() {
136
+ const args = process.argv.slice(2);
137
+ const opts = { port: 9222, headless: false, url: 'about:blank' };
138
+ for (const arg of args) {
139
+ if (arg === '--headless' || arg === '--headless=new') { opts.headless = true; continue; }
140
+ const m = arg.match(/^--(\w[\w-]*)(?:=(.*))?$/);
141
+ if (!m) continue;
142
+ const [, key, val] = m;
143
+ if (key === 'port') { opts.port = parseInt(val, 10) || 9222; continue; }
144
+ if (key === 'url') { opts.url = val ?? opts.url; continue; }
145
+ }
146
+ return opts;
147
+ }
148
+
149
+ if (process.argv[1] === __filename) {
150
+ const opts = parseCliArgs();
151
+ const ok = s => process.stdout.write(` ✓ ${s}\n`);
152
+ const fail = s => process.stderr.write(` ✗ ${s}\n`);
153
+
154
+ process.stdout.write('\n');
155
+ process.stdout.write(' ╬ Argus Chrome Launcher\n\n');
156
+
157
+ const chromePath = findChrome();
158
+ if (!chromePath) {
159
+ fail('Chrome not found. Install Google Chrome or set ARGUS_CHROME_PATH.');
160
+ process.stdout.write('\n Download: https://www.google.com/chrome/\n\n');
161
+ process.exit(1);
162
+ }
163
+
164
+ ok(`Found: ${chromePath}`);
165
+
166
+ try {
167
+ const { process: child } = launchChrome(opts);
168
+ ok(`Launched (PID ${child.pid}) — remote debugging on port ${opts.port}`);
169
+ process.stdout.write(`\n CDP endpoint: http://localhost:${opts.port}\n`);
170
+ process.stdout.write(` Run \`npm run doctor\` to verify Argus can reach Chrome.\n\n`);
171
+ } catch (err) {
172
+ fail(err.message);
173
+ process.exit(1);
174
+ }
175
+ }
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Argus Doctor — npm run doctor / argus-doctor
4
+ *
5
+ * Pre-flight check before running any Argus audit. Runs three checks:
6
+ * 1. Chrome CDP reachable at localhost:9222 (or ARGUS_CHROME_PORT)
7
+ * 2. .mcp.json exists, is valid JSON, has a chrome-devtools server entry
8
+ * 3. TARGET_DEV_URL is set (in .env or already in process.env)
9
+ *
10
+ * All three check functions are exported as pure async functions so the
11
+ * test harness can unit-test them without a real Chrome instance or disk setup.
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+
20
+ /**
21
+ * Check whether Chrome's CDP endpoint is reachable on the given port.
22
+ *
23
+ * @param {number} [port=9222]
24
+ * @returns {Promise<{ ok: boolean, detail: string }>}
25
+ */
26
+ export async function checkChrome(port = 9222) {
27
+ try {
28
+ const res = await fetch(`http://localhost:${port}/json/version`, {
29
+ signal: AbortSignal.timeout(3000),
30
+ });
31
+ if (!res.ok) return { ok: false, detail: `HTTP ${res.status}` };
32
+ const data = await res.json();
33
+ return { ok: true, detail: data.Browser ?? 'reachable' };
34
+ } catch (err) {
35
+ return { ok: false, detail: err.message ?? String(err) };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Check that .mcp.json exists, is valid JSON, and contains a chrome-devtools
41
+ * MCP server entry in mcpServers.
42
+ *
43
+ * @param {string} [filePath='.mcp.json']
44
+ * @returns {{ ok: boolean, detail: string }}
45
+ */
46
+ export function checkMcpConfig(filePath = '.mcp.json') {
47
+ if (!fs.existsSync(filePath)) {
48
+ return {
49
+ ok: false,
50
+ detail: `${path.basename(filePath)} not found — create it with: {"mcpServers":{"chrome-devtools":{"command":"npx","args":["-y","chrome-devtools-mcp@1.1.1"]}}}`,
51
+ };
52
+ }
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
56
+ } catch (err) {
57
+ return { ok: false, detail: `Invalid JSON in ${path.basename(filePath)}: ${err.message}` };
58
+ }
59
+ const servers = parsed?.mcpServers ?? parsed?.servers ?? {};
60
+ const hasCdp = Object.values(servers).some(s => {
61
+ const cmd = String(s.command ?? '');
62
+ const args = Array.isArray(s.args) ? s.args.map(String) : [];
63
+ return cmd.includes('chrome-devtools') || args.some(a => a.includes('chrome-devtools'));
64
+ });
65
+ if (!hasCdp) {
66
+ return { ok: false, detail: 'No chrome-devtools entry in mcpServers — add: "chrome-devtools": {"command":"npx","args":["-y","chrome-devtools-mcp@1.1.1"]}' };
67
+ }
68
+ return { ok: true, detail: `${Object.keys(servers).length} server(s) configured` };
69
+ }
70
+
71
+ /**
72
+ * Check that TARGET_DEV_URL is set — either already in process.env or present
73
+ * and non-empty in the .env file on disk.
74
+ *
75
+ * @param {string} [envFile='.env']
76
+ * @returns {{ ok: boolean, detail: string }}
77
+ */
78
+ export function checkEnvKeys(envFile = '.env') {
79
+ if (process.env.TARGET_DEV_URL) {
80
+ return { ok: true, detail: `TARGET_DEV_URL=${process.env.TARGET_DEV_URL}` };
81
+ }
82
+ if (!fs.existsSync(envFile)) {
83
+ return {
84
+ ok: false,
85
+ detail: `.env not found — run \`argus init\` to generate one`,
86
+ };
87
+ }
88
+ const content = fs.readFileSync(envFile, 'utf8');
89
+ const match = content.match(/^TARGET_DEV_URL\s*=\s*(.+)$/m);
90
+ if (!match || !match[1].trim()) {
91
+ return {
92
+ ok: false,
93
+ detail: 'TARGET_DEV_URL not set in .env — add TARGET_DEV_URL=http://localhost:3000',
94
+ };
95
+ }
96
+ return { ok: true, detail: `TARGET_DEV_URL=${match[1].trim()}` };
97
+ }
98
+
99
+ // ── CLI entry ─────────────────────────────────────────────────────────────────
100
+
101
+ const PAD = 42;
102
+
103
+ function row(label, result) {
104
+ const icon = result.ok ? '✓' : '✗';
105
+ const status = result.ok ? 'OK ' : 'FAIL';
106
+ process.stdout.write(` ${icon} ${label.padEnd(PAD)} ${status} ${result.detail}\n`);
107
+ }
108
+
109
+ if (process.argv[1] === __filename) {
110
+ const port = parseInt(process.env.ARGUS_CHROME_PORT ?? '9222', 10);
111
+
112
+ process.stdout.write('\n ╬ Argus Doctor — pre-flight check\n\n');
113
+
114
+ const [chromeRes, mcpRes, envRes] = await Promise.all([
115
+ checkChrome(port),
116
+ Promise.resolve(checkMcpConfig()),
117
+ Promise.resolve(checkEnvKeys()),
118
+ ]);
119
+
120
+ row(`Chrome CDP reachable (port ${port})`, chromeRes);
121
+ row('.mcp.json valid + chrome-devtools entry', mcpRes);
122
+ row('TARGET_DEV_URL configured', envRes);
123
+
124
+ const allOk = chromeRes.ok && mcpRes.ok && envRes.ok;
125
+ process.stdout.write('\n');
126
+ if (allOk) {
127
+ process.stdout.write(' All checks passed — run `npm run crawl` to start auditing.\n\n');
128
+ process.exit(0);
129
+ } else {
130
+ process.stdout.write(' One or more checks failed — fix the issues above and retry.\n\n');
131
+ process.exit(1);
132
+ }
133
+ }
@@ -26,7 +26,7 @@ import path from 'path';
26
26
  import { fileURLToPath } from 'url';
27
27
  import { createMcpClient } from '../utils/mcp-client.js';
28
28
  import { crawlRouteCheap } from '../orchestration/crawl-and-report.js';
29
- import { parsePrUrl, fetchPrFiles, mapFilesToRoutes } from '../utils/pr-diff-analyzer.js';
29
+ import { fetchPrFiles, mapFilesToRoutes } from '../utils/pr-diff-analyzer.js';
30
30
 
31
31
  // ── Exported helpers (testable without Chrome) ────────────────────────────────
32
32
 
@@ -125,7 +125,7 @@ export function writeStepSummary(markdown) {
125
125
 
126
126
  // ── Preflight reachability check ──────────────────────────────────────────────
127
127
 
128
- async function checkTargetReachable(url) {
128
+ export async function checkTargetReachable(url) {
129
129
  try {
130
130
  // fetch throws only on network errors (ECONNREFUSED, ETIMEDOUT, DNS failure).
131
131
  // HTTP error status codes (4xx/5xx) still mean the server is up — Argus should
@@ -137,6 +137,21 @@ async function checkTargetReachable(url) {
137
137
  }
138
138
  }
139
139
 
140
+ /**
141
+ * Normalize route paths — crawlRouteCheap builds URLs via string concatenation
142
+ * (baseUrl + route.path), so paths without a leading slash produce malformed URLs.
143
+ * @param {Array<{path: string, name?: string}>} routes
144
+ * @returns {Array<{path: string, name?: string}>}
145
+ */
146
+ export function normalizeRoutePaths(routes) {
147
+ return routes.map(r => {
148
+ if (!r.path.startsWith('/')) {
149
+ return { ...r, path: `/${r.path}` };
150
+ }
151
+ return r;
152
+ });
153
+ }
154
+
140
155
  // ── Route loader ──────────────────────────────────────────────────────────────
141
156
 
142
157
  async function loadRoutes() {
@@ -215,6 +230,11 @@ async function main() {
215
230
  const files = await fetchPrFiles(prUrl, token);
216
231
  changedFiles.push(...files);
217
232
  console.log(`[argus] ${files.length} changed file(s)`);
233
+ if (files.length >= 300) {
234
+ // CI annotation lives here (CLI owns stdout) — fetchPrFiles itself only
235
+ // logs to stderr so the MCP server's JSON-RPC stdout stays clean.
236
+ console.log('::warning::PR has 300+ changed files — Argus analyzed the first 300. Routes affected by later files may be missed.');
237
+ }
218
238
 
219
239
  // Step 2: Map changed files to affected routes
220
240
  const routes = await loadRoutes();
@@ -249,10 +269,9 @@ async function main() {
249
269
 
250
270
  // Normalize route paths — crawlRouteCheap builds URLs via string concat (baseUrl + route.path)
251
271
  // so paths without a leading slash produce malformed URLs like https://example.comlogin
252
- const normalizedAffected = affected.map(r => {
253
- if (!r.path.startsWith('/')) {
254
- console.log(`::warning::Route path "${r.path}" has no leading slash — normalizing to "/${r.path}". Update argus.routes.json to use a leading slash.`);
255
- return { ...r, path: `/${r.path}` };
272
+ const normalizedAffected = normalizeRoutePaths(affected).map((r, i) => {
273
+ if (r.path !== affected[i].path) {
274
+ console.log(`::warning::Route path "${affected[i].path}" has no leading slash — normalizing. Update argus.routes.json to use a leading slash.`);
256
275
  }
257
276
  return r;
258
277
  });