Skip to content

Commit fd7f8f6

Browse files
authored
chore: release v5.8.6 (#344)
* chore: release v5.8.6 * fix(repo-intel/queries): wrap errors with query context, add validation, add tests Addresses six reviewer comments on PR #344: 1. Wrap runQuery's `JSON.parse` and `binary.runAnalyzer` exceptions so the failing query name + an output preview surface in the error message instead of bubbling raw `SyntaxError: Unexpected token`. 2. Pre-flight check for the cached repo-intel artifact via `fs.existsSync(mapFilePath)`. Throws a typed `RepoIntelMissingError` (with `.code = 'REPO_INTEL_MISSING'`) so callers can branch instead of parsing a string. 3. `communityHealth`: validate `id` is a non-negative integer. The old `String(undefined)` -> "undefined" would have made the binary complain about an invalid arg, with no useful error context. 4. `diffRisk`: validate the `files` argument shape and refuse calls where the joined `--files` argument exceeds 30 KB (the platform-safe cap, tightest on Windows). Better than letting `execFileSync` fail with E2BIG/ENAMETOOLONG that looks like a binary crash. 5. Export `RepoIntelMissingError` from the queries module so consumer plugins can write `if (err.code === 'REPO_INTEL_MISSING') ...`. 6. New test file `__tests__/repo-intel-queries.test.js` (20 tests): verifies argv construction for every flag-bearing query (existing + the 4 graph), the input-validation paths, and the three error wrappers (missing artifact, binary failure, non-JSON output incl. preview truncation). The binary itself is mocked via jest.mock so tests run hermetically. * fix(@agentsys/lib): add ./repo-intel to package exports map Cursor Bugbot caught: the new lib/repo-intel module wasn't in the `exports` map of lib/package.json, so direct subpath imports `require('@agentsys/lib/repo-intel')` would fail with ERR_PACKAGE_PATH_NOT_EXPORTED. Top-level `require('@agentsys/lib').repoIntel` worked but subpath imports (the pattern used elsewhere in the codebase for cross-platform, collectors, enhance, etc.) didn't. Adds two entries: - ./repo-intel -> ./repo-intel/index.js - ./repo-intel/queries -> ./repo-intel/queries.js Verified both subpath imports resolve cleanly.
1 parent 90b7051 commit fd7f8f6

12 files changed

Lines changed: 741 additions & 7 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "agentsys",
33
"description": "19 specialized plugins for AI workflow automation - task orchestration, PR workflow, slop detection, code review, drift detection, enhancement analysis, documentation sync, unified static analysis, perf investigations, topic research, agent config linting, cross-tool AI consultation, structured AI debate, workflow pattern learning, codebase onboarding, and contributor guidance",
4-
"version": "5.8.5",
4+
"version": "5.8.6",
55
"owner": {
66
"name": "Avi Fenesh",
77
"url": "https://github.com/avifenesh"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agentsys",
3-
"version": "5.8.5",
3+
"version": "5.8.6",
44
"description": "Professional-grade slash commands for Claude Code with cross-platform support",
55
"keywords": [
66
"workflow",

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
## [5.8.6] - 2026-04-23
13+
14+
### Added
15+
- **`@agentsys/lib`'s `repoIntel.queries` module** - typed wrappers over every `agent-analyzer repo-intel query <type>` subcommand (28 functions). Consumer plugins can now call `require('@agentsys/lib').repoIntel.queries.hotspots(cwd, { limit: 20 })` instead of constructing raw CLI argv themselves. Functions returned in JSON match the binary's output shape per query.
16+
- **4 new graph-derived query wrappers** for the analyzer-graph crate landed in agent-analyzer v0.4.0:
17+
- `communities(cwd)` - lists Louvain-discovered file clusters (the natural feature areas, independent of directory layout)
18+
- `boundaries(cwd, { limit })` - files bridging multiple communities by betweenness centrality (architectural seams - highest-leverage files for refactoring)
19+
- `areaOf(cwd, file)` - which community a file belongs to
20+
- `communityHealth(cwd, id)` - composite per-community roll-up (size, total/recent changes, bug-fix rate, AI ratio, stale-owner count)
21+
22+
### Changed
23+
- **`ANALYZER_MIN_VERSION` bumped 0.3.0 -> 0.4.0** to match agent-analyzer v0.4.0 which adds the graph subcommands. Older binaries get auto-upgraded on first call by `lib/binary.ensureBinary()`.
24+
1225
## [5.8.5] - 2026-04-23
1326

1427
### Fixed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/**
2+
* Tests for lib/repo-intel/queries - typed wrappers over agent-analyzer
3+
* `repo-intel query <type>` subcommands.
4+
*
5+
* Verifies argv construction (the contract with the binary), error wrapping
6+
* (parse failures, missing artifact), and input validation. The binary
7+
* itself is mocked so these run hermetically in CI.
8+
*/
9+
10+
'use strict';
11+
12+
const fs = require('fs');
13+
const path = require('path');
14+
const os = require('os');
15+
16+
// Mock the binary module so runAnalyzer doesn't actually shell out.
17+
jest.mock('../lib/binary', () => ({
18+
runAnalyzer: jest.fn(),
19+
}));
20+
21+
const binary = require('../lib/binary');
22+
const queries = require('../lib/repo-intel/queries');
23+
24+
describe('lib/repo-intel/queries', () => {
25+
let tempDir;
26+
let mapFilePath;
27+
28+
beforeEach(() => {
29+
binary.runAnalyzer.mockReset();
30+
// Build a fake repo with the state dir + map file the queries look for.
31+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'repo-intel-queries-test-'));
32+
fs.mkdirSync(path.join(tempDir, '.claude'), { recursive: true });
33+
mapFilePath = path.join(tempDir, '.claude', 'repo-intel.json');
34+
fs.writeFileSync(mapFilePath, '{}');
35+
});
36+
37+
afterEach(() => {
38+
if (fs.existsSync(tempDir)) {
39+
fs.rmSync(tempDir, { recursive: true, force: true });
40+
}
41+
});
42+
43+
// ─── Argv construction ────────────────────────────────────────────────────
44+
// For each query, we mock the binary to return a known JSON value, call
45+
// the wrapper, then assert the exact argv passed through. This is the
46+
// load-bearing contract with the agent-analyzer CLI.
47+
48+
describe('argv construction', () => {
49+
test('hotspots without limit', () => {
50+
binary.runAnalyzer.mockReturnValue('[]');
51+
queries.hotspots(tempDir);
52+
const args = binary.runAnalyzer.mock.calls[0][0];
53+
expect(args.slice(0, 3)).toEqual(['repo-intel', 'query', 'hotspots']);
54+
expect(args).toContain('--map-file');
55+
expect(args).toContain(mapFilePath);
56+
expect(args).toContain(tempDir);
57+
expect(args).not.toContain('--top');
58+
});
59+
60+
test('hotspots with limit adds --top', () => {
61+
binary.runAnalyzer.mockReturnValue('[]');
62+
queries.hotspots(tempDir, { limit: 25 });
63+
const args = binary.runAnalyzer.mock.calls[0][0];
64+
expect(args).toContain('--top');
65+
expect(args[args.indexOf('--top') + 1]).toBe('25');
66+
});
67+
68+
test('coupling forwards file argument', () => {
69+
binary.runAnalyzer.mockReturnValue('[]');
70+
queries.coupling(tempDir, 'src/auth.js');
71+
const args = binary.runAnalyzer.mock.calls[0][0];
72+
expect(args.slice(0, 4)).toEqual(['repo-intel', 'query', 'coupling', 'src/auth.js']);
73+
});
74+
75+
test('busFactor with adjustForAi adds --adjust-for-ai', () => {
76+
binary.runAnalyzer.mockReturnValue('{}');
77+
queries.busFactor(tempDir, { adjustForAi: true });
78+
const args = binary.runAnalyzer.mock.calls[0][0];
79+
expect(args).toContain('--adjust-for-ai');
80+
});
81+
82+
test('testGaps with limit + minChanges adds both flags', () => {
83+
binary.runAnalyzer.mockReturnValue('[]');
84+
queries.testGaps(tempDir, { limit: 5, minChanges: 3 });
85+
const args = binary.runAnalyzer.mock.calls[0][0];
86+
expect(args[args.indexOf('--top') + 1]).toBe('5');
87+
expect(args[args.indexOf('--min-changes') + 1]).toBe('3');
88+
});
89+
90+
test('aiRatio with pathFilter adds --path-filter', () => {
91+
binary.runAnalyzer.mockReturnValue('{}');
92+
queries.aiRatio(tempDir, { pathFilter: 'src/' });
93+
const args = binary.runAnalyzer.mock.calls[0][0];
94+
expect(args[args.indexOf('--path-filter') + 1]).toBe('src/');
95+
});
96+
97+
test('diffRisk joins files with comma', () => {
98+
binary.runAnalyzer.mockReturnValue('[]');
99+
queries.diffRisk(tempDir, ['a.js', 'b.js', 'c.js']);
100+
const args = binary.runAnalyzer.mock.calls[0][0];
101+
expect(args[args.indexOf('--files') + 1]).toBe('a.js,b.js,c.js');
102+
});
103+
104+
test('dependents with file scope adds --file', () => {
105+
binary.runAnalyzer.mockReturnValue('{}');
106+
queries.dependents(tempDir, 'createUser', 'src/users.js');
107+
const args = binary.runAnalyzer.mock.calls[0][0];
108+
expect(args).toContain('createUser');
109+
expect(args[args.indexOf('--file') + 1]).toBe('src/users.js');
110+
});
111+
});
112+
113+
// ─── Phase 5 graph queries ────────────────────────────────────────────────
114+
115+
describe('graph queries (Phase 5.1)', () => {
116+
test('communities sends "communities" subcommand', () => {
117+
binary.runAnalyzer.mockReturnValue('[]');
118+
queries.communities(tempDir);
119+
const args = binary.runAnalyzer.mock.calls[0][0];
120+
expect(args.slice(0, 3)).toEqual(['repo-intel', 'query', 'communities']);
121+
});
122+
123+
test('boundaries with limit adds --top', () => {
124+
binary.runAnalyzer.mockReturnValue('[]');
125+
queries.boundaries(tempDir, { limit: 8 });
126+
const args = binary.runAnalyzer.mock.calls[0][0];
127+
expect(args.slice(0, 3)).toEqual(['repo-intel', 'query', 'boundaries']);
128+
expect(args[args.indexOf('--top') + 1]).toBe('8');
129+
});
130+
131+
test('areaOf forwards file argument', () => {
132+
binary.runAnalyzer.mockReturnValue('{}');
133+
queries.areaOf(tempDir, 'src/foo.js');
134+
const args = binary.runAnalyzer.mock.calls[0][0];
135+
expect(args.slice(0, 4)).toEqual(['repo-intel', 'query', 'area-of', 'src/foo.js']);
136+
});
137+
138+
test('communityHealth forwards id as string', () => {
139+
binary.runAnalyzer.mockReturnValue('{}');
140+
queries.communityHealth(tempDir, 7);
141+
const args = binary.runAnalyzer.mock.calls[0][0];
142+
expect(args.slice(0, 4)).toEqual(['repo-intel', 'query', 'community-health', '7']);
143+
});
144+
});
145+
146+
// ─── Input validation ─────────────────────────────────────────────────────
147+
148+
describe('input validation', () => {
149+
test('communityHealth rejects non-integer id', () => {
150+
expect(() => queries.communityHealth(tempDir, 'foo')).toThrow(TypeError);
151+
expect(() => queries.communityHealth(tempDir, null)).toThrow(TypeError);
152+
expect(() => queries.communityHealth(tempDir, 1.5)).toThrow(TypeError);
153+
expect(() => queries.communityHealth(tempDir, -1)).toThrow(TypeError);
154+
expect(binary.runAnalyzer).not.toHaveBeenCalled();
155+
});
156+
157+
test('diffRisk rejects non-array files', () => {
158+
expect(() => queries.diffRisk(tempDir, 'a.js')).toThrow(TypeError);
159+
expect(() => queries.diffRisk(tempDir, [1, 2, 3])).toThrow(TypeError);
160+
expect(binary.runAnalyzer).not.toHaveBeenCalled();
161+
});
162+
163+
test('diffRisk rejects oversized file argument', () => {
164+
// 1000 paths × 50 chars each = 50000 chars, exceeds 30000 cap.
165+
const huge = Array.from({ length: 1000 }, (_, i) => `path/that/is/about/fifty-chars-long/file${i}.js`);
166+
expect(() => queries.diffRisk(tempDir, huge)).toThrow(RangeError);
167+
expect(binary.runAnalyzer).not.toHaveBeenCalled();
168+
});
169+
170+
test('diffRisk accepts a normal-sized file argument', () => {
171+
binary.runAnalyzer.mockReturnValue('[]');
172+
queries.diffRisk(tempDir, ['a.js', 'b.js']);
173+
expect(binary.runAnalyzer).toHaveBeenCalledTimes(1);
174+
});
175+
});
176+
177+
// ─── Error wrapping ───────────────────────────────────────────────────────
178+
179+
describe('error wrapping', () => {
180+
test('throws RepoIntelMissingError when artifact is absent', () => {
181+
// Use a fresh dir with no .claude/repo-intel.json.
182+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'repo-intel-empty-'));
183+
try {
184+
let caught;
185+
try {
186+
queries.hotspots(emptyDir);
187+
} catch (err) {
188+
caught = err;
189+
}
190+
expect(caught).toBeInstanceOf(queries.RepoIntelMissingError);
191+
expect(caught.code).toBe('REPO_INTEL_MISSING');
192+
expect(caught.mapFile).toContain('repo-intel.json');
193+
expect(binary.runAnalyzer).not.toHaveBeenCalled();
194+
} finally {
195+
fs.rmSync(emptyDir, { recursive: true, force: true });
196+
}
197+
});
198+
199+
test('wraps binary failure with query context', () => {
200+
binary.runAnalyzer.mockImplementation(() => {
201+
throw new Error('binary blew up');
202+
});
203+
let caught;
204+
try {
205+
queries.bugspots(tempDir, { limit: 5 });
206+
} catch (err) {
207+
caught = err;
208+
}
209+
expect(caught).toBeInstanceOf(Error);
210+
expect(caught.message).toContain('repo-intel query failed');
211+
expect(caught.message).toContain('bugspots');
212+
expect(caught.message).toContain('binary blew up');
213+
expect(caught.cause).toBeDefined();
214+
});
215+
216+
test('wraps non-JSON output with preview', () => {
217+
binary.runAnalyzer.mockReturnValue('not actually json');
218+
let caught;
219+
try {
220+
queries.health(tempDir);
221+
} catch (err) {
222+
caught = err;
223+
}
224+
expect(caught).toBeInstanceOf(Error);
225+
expect(caught.message).toContain('non-JSON output');
226+
expect(caught.message).toContain('health');
227+
expect(caught.message).toContain('not actually json');
228+
});
229+
230+
test('non-JSON output preview is truncated', () => {
231+
// 500 chars - should be truncated to 200 in the message.
232+
const big = 'x'.repeat(500);
233+
binary.runAnalyzer.mockReturnValue(big);
234+
let caught;
235+
try {
236+
queries.health(tempDir);
237+
} catch (err) {
238+
caught = err;
239+
}
240+
// The message contains exactly 200 x's, not 500.
241+
const xCount = (caught.message.match(/x/g) || []).length;
242+
expect(xCount).toBe(200);
243+
});
244+
});
245+
});

lib/binary/version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
// Minimum binary version required by this version of agent-core
4-
const ANALYZER_MIN_VERSION = '0.3.0';
4+
const ANALYZER_MIN_VERSION = '0.4.0';
55

66
// Binary name
77
const BINARY_NAME = 'agent-analyzer';

lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const perf = require('./perf');
3030
const collectors = require('./collectors');
3131
const discoveryModule = require('./discovery');
3232
const binary = require('./binary');
33+
const repoIntel = require('./repo-intel');
3334

3435
/**
3536
* Platform detection and verification utilities
@@ -255,6 +256,7 @@ module.exports = {
255256
collectors,
256257
discovery,
257258
binary,
259+
repoIntel,
258260

259261
// Direct module access for backward compatibility
260262
detectPlatform,

lib/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"./discovery": "./discovery/index.js",
1414
"./enhance": "./enhance/index.js",
1515
"./perf": "./perf/index.js",
16+
"./repo-intel": "./repo-intel/index.js",
17+
"./repo-intel/queries": "./repo-intel/queries.js",
1618
"./repo-map": "./repo-map/index.js",
1719
"./collectors/codebase": "./collectors/codebase.js",
1820
"./collectors/docs-patterns": "./collectors/docs-patterns.js",

lib/repo-intel/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Repo intel module - typed wrappers over agent-analyzer's repo-intel
3+
* subcommands. Consumers can import via:
4+
*
5+
* const { repoIntel } = require('@agentsys/lib');
6+
* const hot = repoIntel.queries.hotspots(cwd, { limit: 20 });
7+
* const communities = repoIntel.queries.communities(cwd);
8+
*
9+
* The Rust binary is downloaded lazily on first call by lib/binary; this
10+
* module just constructs the right argv and parses the JSON output.
11+
*
12+
* @module lib/repo-intel
13+
*/
14+
15+
'use strict';
16+
17+
const queries = require('./queries');
18+
19+
module.exports = {
20+
queries,
21+
};

0 commit comments

Comments
 (0)