@agentbridge1/cli 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +4 -4
- package/dist/commands/connect.js +58 -122
- package/dist/commands/doctor.js +46 -8
- package/dist/commands/setup-mcp.js +54 -44
- package/dist/commands/start.js +86 -22
- package/dist/commands/watch.js +662 -92
- package/dist/contract-intelligence.js +597 -0
- package/dist/contract-verdict.js +239 -0
- package/dist/diff-reader.js +200 -0
- package/dist/error-catalog.js +29 -0
- package/dist/git-status.js +6 -2
- package/dist/index.js +11 -5
- package/dist/intent-parser.js +178 -0
- package/dist/intent-validation.js +37 -0
- package/dist/local-proof.js +12 -4
- package/dist/mcp/agentbridge-mcp.js +1174 -37
- package/dist/mcp/agentbridge-mcp.js.map +4 -4
- package/dist/mcp-config.js +64 -0
- package/dist/proof-parser.js +118 -0
- package/dist/session-state.js +15 -0
- package/dist/session.js +10 -0
- package/dist/supervision.js +191 -48
- package/dist/test-runner.js +201 -15
- package/package.json +1 -1
package/dist/test-runner.js
CHANGED
|
@@ -1,44 +1,152 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectTestCommand = detectTestCommand;
|
|
4
|
+
exports.buildScopedCommand = buildScopedCommand;
|
|
5
|
+
exports.extractFailedTestNames = extractFailedTestNames;
|
|
4
6
|
exports.runDetectedTests = runDetectedTests;
|
|
7
|
+
exports.extractIntentKeywords = extractIntentKeywords;
|
|
8
|
+
exports.tokenizePath = tokenizePath;
|
|
9
|
+
exports.analyzeCoherence = analyzeCoherence;
|
|
5
10
|
const node_fs_1 = require("node:fs");
|
|
6
11
|
const node_path_1 = require("node:path");
|
|
7
12
|
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
/**
|
|
14
|
+
* Detect the appropriate test command for the project rooted at `cwd`.
|
|
15
|
+
* Checks config override first, then language-specific conventions.
|
|
16
|
+
*/
|
|
8
17
|
function detectTestCommand(cwd = process.cwd()) {
|
|
18
|
+
// 1. Check .agentbridge.json for an explicit verify.command override
|
|
19
|
+
const abConfigPath = (0, node_path_1.resolve)(cwd, ".agentbridge.json");
|
|
20
|
+
if ((0, node_fs_1.existsSync)(abConfigPath)) {
|
|
21
|
+
try {
|
|
22
|
+
const cfg = JSON.parse((0, node_fs_1.readFileSync)(abConfigPath, "utf8"));
|
|
23
|
+
if (cfg.verify?.command)
|
|
24
|
+
return cfg.verify.command;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// malformed config — fall through
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// 2. Node / npm — prefer test:unit for speed
|
|
9
31
|
const packageJsonPath = (0, node_path_1.resolve)(cwd, "package.json");
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
if ((0, node_fs_1.existsSync)(packageJsonPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, "utf8"));
|
|
35
|
+
if (pkg.scripts?.["test:unit"])
|
|
36
|
+
return "npm run test:unit";
|
|
37
|
+
if (pkg.scripts?.["test"])
|
|
38
|
+
return "npm run test";
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// malformed package.json — fall through
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 3. Python
|
|
45
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "pytest.ini")) || (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "pyproject.toml"))) {
|
|
46
|
+
return "pytest";
|
|
47
|
+
}
|
|
48
|
+
// 4. Rust
|
|
49
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "Cargo.toml"))) {
|
|
50
|
+
return "cargo test";
|
|
51
|
+
}
|
|
52
|
+
// 5. Go
|
|
53
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "go.mod"))) {
|
|
54
|
+
return "go test ./...";
|
|
55
|
+
}
|
|
56
|
+
// 6. Makefile with a `test` target
|
|
57
|
+
const makefilePath = (0, node_path_1.resolve)(cwd, "Makefile");
|
|
58
|
+
if ((0, node_fs_1.existsSync)(makefilePath)) {
|
|
59
|
+
try {
|
|
60
|
+
const contents = (0, node_fs_1.readFileSync)(makefilePath, "utf8");
|
|
61
|
+
if (/^test:/m.test(contents))
|
|
62
|
+
return "make test";
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// unreadable Makefile — fall through
|
|
66
|
+
}
|
|
67
|
+
}
|
|
19
68
|
return null;
|
|
20
69
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Build a scoped version of the base command that only runs tests related to
|
|
72
|
+
* the provided files. Falls back to the full command if scoping is not
|
|
73
|
+
* supported for the detected runner.
|
|
74
|
+
*/
|
|
75
|
+
function buildScopedCommand(base, relatedFiles) {
|
|
76
|
+
if (!relatedFiles.length)
|
|
77
|
+
return base;
|
|
78
|
+
// vitest / jest both support --testPathPattern (regex)
|
|
79
|
+
if (/vitest|jest/.test(base)) {
|
|
80
|
+
// Build a pattern that matches the test files corresponding to the changed source files.
|
|
81
|
+
// e.g. "src/foo.ts" → look for "src/foo" anywhere in test paths.
|
|
82
|
+
const stems = relatedFiles.map((f) => f.replace(/\.[^/.]+$/, "").replace(/\//g, "\\/"));
|
|
83
|
+
const pattern = stems.join("|");
|
|
84
|
+
return `${base} --testPathPattern="${pattern}"`;
|
|
85
|
+
}
|
|
86
|
+
// pytest supports positional file/dir args (best effort)
|
|
87
|
+
if (base.startsWith("pytest")) {
|
|
88
|
+
const pyFiles = relatedFiles
|
|
89
|
+
.map((f) => f.replace(/\.ts$/, ".py"))
|
|
90
|
+
.filter((f) => (0, node_fs_1.existsSync)(f));
|
|
91
|
+
if (pyFiles.length)
|
|
92
|
+
return `${base} ${pyFiles.join(" ")}`;
|
|
93
|
+
}
|
|
94
|
+
// Everything else: full run
|
|
95
|
+
return base;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Extract failed test names from stdout/stderr output of vitest, jest, or pytest.
|
|
99
|
+
*/
|
|
100
|
+
function extractFailedTestNames(output) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
for (const line of output.split("\n")) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
// vitest / jest: lines starting with × or ✗ or containing "● " (jest describe block)
|
|
105
|
+
if (/^[×✗]/.test(trimmed) || /^\s*●\s+/.test(line)) {
|
|
106
|
+
lines.push(trimmed.replace(/^[×✗●\s]+/, "").trim());
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// pytest: lines like "FAILED src/foo_test.py::test_name"
|
|
110
|
+
if (/^FAILED\s+/.test(trimmed)) {
|
|
111
|
+
lines.push(trimmed.replace(/^FAILED\s+/, "").trim());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...new Set(lines)].filter(Boolean).slice(0, 10);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Run the detected (or configured) test suite and return the result.
|
|
118
|
+
* Returns null if no test command could be detected.
|
|
119
|
+
*/
|
|
120
|
+
async function runDetectedTests(optionsOrTimeout = {}) {
|
|
121
|
+
// Accept the legacy `timeoutMs` number signature for backwards compatibility.
|
|
122
|
+
const options = typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout;
|
|
123
|
+
const cwd = options.cwd ?? process.cwd();
|
|
124
|
+
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
|
|
125
|
+
const baseCommand = detectTestCommand(cwd);
|
|
126
|
+
if (!baseCommand)
|
|
24
127
|
return null;
|
|
128
|
+
const command = options.relatedFiles?.length
|
|
129
|
+
? buildScopedCommand(baseCommand, options.relatedFiles)
|
|
130
|
+
: baseCommand;
|
|
25
131
|
const started = Date.now();
|
|
26
132
|
return new Promise((resolveResult) => {
|
|
27
133
|
const child = (0, node_child_process_1.spawn)(command, {
|
|
28
134
|
shell: true,
|
|
29
|
-
cwd
|
|
135
|
+
cwd,
|
|
30
136
|
env: process.env,
|
|
31
137
|
stdio: ["ignore", "pipe", "pipe"],
|
|
32
138
|
});
|
|
33
139
|
let stdout = "";
|
|
34
140
|
let stderr = "";
|
|
141
|
+
let didTimeout = false;
|
|
35
142
|
child.stdout?.on("data", (chunk) => {
|
|
36
143
|
stdout += chunk.toString();
|
|
37
144
|
});
|
|
38
145
|
child.stderr?.on("data", (chunk) => {
|
|
39
146
|
stderr += chunk.toString();
|
|
40
147
|
});
|
|
41
|
-
const
|
|
148
|
+
const timer = setTimeout(() => {
|
|
149
|
+
didTimeout = true;
|
|
42
150
|
child.kill("SIGTERM");
|
|
43
151
|
resolveResult({
|
|
44
152
|
command,
|
|
@@ -46,17 +154,95 @@ async function runDetectedTests(timeoutMs = 5 * 60 * 1000) {
|
|
|
46
154
|
stdout,
|
|
47
155
|
stderr: `${stderr}\nTimed out after ${timeoutMs}ms`,
|
|
48
156
|
durationMs: Date.now() - started,
|
|
157
|
+
timedOut: true,
|
|
49
158
|
});
|
|
50
159
|
}, timeoutMs);
|
|
51
160
|
child.on("close", (code) => {
|
|
52
|
-
clearTimeout(
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
if (didTimeout)
|
|
163
|
+
return; // already resolved
|
|
53
164
|
resolveResult({
|
|
54
165
|
command,
|
|
55
166
|
passed: code === 0,
|
|
56
167
|
stdout,
|
|
57
168
|
stderr,
|
|
58
169
|
durationMs: Date.now() - started,
|
|
170
|
+
timedOut: false,
|
|
59
171
|
});
|
|
60
172
|
});
|
|
61
173
|
});
|
|
62
174
|
}
|
|
175
|
+
/** Common English stop words to strip from intent before keyword extraction. */
|
|
176
|
+
const STOP_WORDS = new Set([
|
|
177
|
+
"a", "an", "the", "and", "or", "of", "to", "in", "for", "on", "with",
|
|
178
|
+
"at", "by", "from", "as", "is", "was", "are", "were", "be", "been",
|
|
179
|
+
"it", "this", "that", "these", "those", "we", "i", "you", "they",
|
|
180
|
+
"add", "fix", "update", "change", "make", "get", "set", "use",
|
|
181
|
+
"all", "some", "any", "new", "old", "via", "into", "also", "should",
|
|
182
|
+
]);
|
|
183
|
+
/**
|
|
184
|
+
* Extract meaningful tokens from an intent string.
|
|
185
|
+
* Splits on non-alphanumeric characters and removes stop words.
|
|
186
|
+
*/
|
|
187
|
+
function extractIntentKeywords(intent) {
|
|
188
|
+
return intent
|
|
189
|
+
.toLowerCase()
|
|
190
|
+
.split(/[^a-z0-9]+/)
|
|
191
|
+
.filter((t) => t.length >= 3 && !STOP_WORDS.has(t));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Extract tokens from a file path (useful for overlap scoring).
|
|
195
|
+
* e.g. "src/middleware/userAuth.ts" → ["src","middleware","userauth","ts"]
|
|
196
|
+
*/
|
|
197
|
+
function tokenizePath(filePath) {
|
|
198
|
+
return filePath
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.split(/[/\\._\-]+/)
|
|
201
|
+
.filter((t) => t.length >= 2);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Analyse whether the changed files are coherent with the declared intent.
|
|
205
|
+
* Returns a CoherenceResult with a verdict and list of suspicious files.
|
|
206
|
+
*
|
|
207
|
+
* Does NOT require an LLM — purely keyword/token overlap heuristics.
|
|
208
|
+
*/
|
|
209
|
+
function analyzeCoherence(ctx) {
|
|
210
|
+
const { intent, changedFiles } = ctx;
|
|
211
|
+
if (!intent || intent.trim().length === 0) {
|
|
212
|
+
return {
|
|
213
|
+
score: 0,
|
|
214
|
+
suspiciousFiles: [],
|
|
215
|
+
intentKeywords: [],
|
|
216
|
+
verdict: "unknown",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const intentKeywords = extractIntentKeywords(intent);
|
|
220
|
+
if (intentKeywords.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
score: 0,
|
|
223
|
+
suspiciousFiles: [],
|
|
224
|
+
intentKeywords: [],
|
|
225
|
+
verdict: "unknown",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (changedFiles.length === 0) {
|
|
229
|
+
return {
|
|
230
|
+
score: 1,
|
|
231
|
+
suspiciousFiles: [],
|
|
232
|
+
intentKeywords,
|
|
233
|
+
verdict: "coherent",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const suspiciousFiles = [];
|
|
237
|
+
for (const file of changedFiles) {
|
|
238
|
+
const pathTokens = tokenizePath(file);
|
|
239
|
+
const hasOverlap = intentKeywords.some((kw) => pathTokens.some((pt) => pt.includes(kw) || kw.includes(pt)));
|
|
240
|
+
if (!hasOverlap) {
|
|
241
|
+
suspiciousFiles.push(file);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const score = (changedFiles.length - suspiciousFiles.length) / changedFiles.length;
|
|
245
|
+
const driftThreshold = 0.4;
|
|
246
|
+
const verdict = suspiciousFiles.length / changedFiles.length > driftThreshold ? "drift" : "coherent";
|
|
247
|
+
return { score, suspiciousFiles, intentKeywords, verdict };
|
|
248
|
+
}
|