agent-qa 0.0.1 → 0.1.2
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/LICENSE.md +105 -0
- package/NOTICE.md +11 -0
- package/dist/cli.cjs +5605 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +5600 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +349 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +300 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -8
- package/skills/agent-qa-authoring/SKILL.md +49 -0
- package/skills/agent-qa-authoring/agents/openai.yaml +15 -0
- package/skills/agent-qa-authoring/references/agent-qa-contracts.json +31 -0
- package/skills/agent-qa-debug-fix/SKILL.md +31 -0
- package/skills/agent-qa-debug-fix/agents/openai.yaml +15 -0
- package/skills/agent-qa-result-triage/SKILL.md +41 -0
- package/skills/agent-qa-result-triage/agents/openai.yaml +15 -0
- package/skills/agent-qa-result-triage/references/triage-categories.md +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { getAgentQaVersion } from "@vostride/agent-qa-core";
|
|
5
|
+
export * from "@vostride/agent-qa-core";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
|
+
import { dirname, resolve as resolvePath } from "path";
|
|
10
|
+
import { parse as parseYaml } from "yaml";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
import { AgentQaConfigSchema, loadLLMAuthPlugins } from "@vostride/agent-qa-core";
|
|
13
|
+
var ENV_MAPPING = {
|
|
14
|
+
AGENT_QA_DASHBOARD_PORT: "services.dashboard.port",
|
|
15
|
+
AGENT_QA_MCP_PORT: "services.mcp.port",
|
|
16
|
+
AGENT_QA_CACHE_DIR: "services.cache.dir",
|
|
17
|
+
AGENT_QA_CACHE_TTL: "services.cache.ttl",
|
|
18
|
+
AGENT_QA_LOG_LEVEL: "services.logging.level",
|
|
19
|
+
AGENT_QA_HEADLESS: "use.browser.headless"
|
|
20
|
+
};
|
|
21
|
+
function setNestedValue(obj, path, value) {
|
|
22
|
+
const parts = path.split(".");
|
|
23
|
+
let current = obj;
|
|
24
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
25
|
+
const key = parts[i];
|
|
26
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
|
|
27
|
+
current[key] = {};
|
|
28
|
+
}
|
|
29
|
+
current = current[key];
|
|
30
|
+
}
|
|
31
|
+
current[parts[parts.length - 1]] = value;
|
|
32
|
+
}
|
|
33
|
+
function parseEnvValue(key, value) {
|
|
34
|
+
if (key === "AGENT_QA_DASHBOARD_PORT" || key === "AGENT_QA_MCP_PORT") {
|
|
35
|
+
const parsed = Number.parseInt(value, 10);
|
|
36
|
+
if (Number.isInteger(parsed)) return parsed;
|
|
37
|
+
}
|
|
38
|
+
if (key === "AGENT_QA_HEADLESS") {
|
|
39
|
+
const normalized = value.trim().toLowerCase();
|
|
40
|
+
if (normalized === "true") return true;
|
|
41
|
+
if (normalized === "false") return false;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
async function loadConfigFile(configPath) {
|
|
46
|
+
try {
|
|
47
|
+
const content = await readFile(configPath, "utf-8");
|
|
48
|
+
return parseYaml(content) ?? {};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err.code === "ENOENT") return {};
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function loadRequiredConfigFile(configPath) {
|
|
55
|
+
try {
|
|
56
|
+
const content = await readFile(configPath, "utf-8");
|
|
57
|
+
return parseYaml(content) ?? {};
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err.code === "ENOENT") {
|
|
60
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function loadEnvOverrides() {
|
|
66
|
+
const result = {};
|
|
67
|
+
for (const [envKey, configPath] of Object.entries(ENV_MAPPING)) {
|
|
68
|
+
const value = process.env[envKey];
|
|
69
|
+
if (value !== void 0) {
|
|
70
|
+
setNestedValue(result, configPath, parseEnvValue(envKey, value));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function isPlainObject(val) {
|
|
76
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
77
|
+
}
|
|
78
|
+
function mergeConfigs(file, env, flags) {
|
|
79
|
+
return deepMerge(deepMerge(file ?? {}, env), flags);
|
|
80
|
+
}
|
|
81
|
+
function deepMerge(base, override) {
|
|
82
|
+
if (!isPlainObject(base) || !isPlainObject(override)) return override;
|
|
83
|
+
const result = { ...base };
|
|
84
|
+
for (const key of Object.keys(override)) {
|
|
85
|
+
if (key in result && isPlainObject(result[key]) && isPlainObject(override[key])) {
|
|
86
|
+
result[key] = deepMerge(result[key], override[key]);
|
|
87
|
+
} else {
|
|
88
|
+
result[key] = override[key];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
function mergeWithTestConfig(globalConfig, testConfig, flagOverrides) {
|
|
94
|
+
return deepMerge(deepMerge(globalConfig ?? {}, testConfig ?? {}), flagOverrides);
|
|
95
|
+
}
|
|
96
|
+
function normalizeConfigForValidation(config) {
|
|
97
|
+
if (!isPlainObject(config)) return config;
|
|
98
|
+
const normalized = deepMerge({}, config);
|
|
99
|
+
const use = normalized.use;
|
|
100
|
+
if (isPlainObject(use)) {
|
|
101
|
+
delete use.headless;
|
|
102
|
+
}
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
function mergeUseBlocks(globalUse, suiteUse, testUse, cliFlags) {
|
|
106
|
+
return deepMerge(
|
|
107
|
+
deepMerge(
|
|
108
|
+
deepMerge(globalUse ?? {}, suiteUse ?? {}),
|
|
109
|
+
testUse ?? {}
|
|
110
|
+
),
|
|
111
|
+
cliFlags
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
function flattenObject(obj, prefix = "") {
|
|
115
|
+
const result = {};
|
|
116
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
117
|
+
if (prefix) result[prefix] = obj;
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
121
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
122
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
123
|
+
Object.assign(result, flattenObject(value, path));
|
|
124
|
+
} else {
|
|
125
|
+
result[path] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
function formatConfigDebug(globalConfig, testConfig, flagOverrides) {
|
|
131
|
+
const layers = [
|
|
132
|
+
{ flat: flattenObject(globalConfig), label: "global config" },
|
|
133
|
+
{ flat: flattenObject(testConfig), label: "test yaml" },
|
|
134
|
+
{ flat: flattenObject(flagOverrides), label: "cli flag" }
|
|
135
|
+
];
|
|
136
|
+
const merged = mergeWithTestConfig(globalConfig, testConfig, flagOverrides);
|
|
137
|
+
const mergedFlat = flattenObject(merged);
|
|
138
|
+
const colorize = {
|
|
139
|
+
"global config": pc.dim,
|
|
140
|
+
"test yaml": pc.cyan,
|
|
141
|
+
"cli flag": pc.green
|
|
142
|
+
};
|
|
143
|
+
const rows = [];
|
|
144
|
+
for (const key of Object.keys(mergedFlat).sort()) {
|
|
145
|
+
let source = "global config";
|
|
146
|
+
for (const layer of layers) {
|
|
147
|
+
if (key in layer.flat) {
|
|
148
|
+
source = layer.label;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const valStr = typeof mergedFlat[key] === "string" ? mergedFlat[key] : JSON.stringify(mergedFlat[key]);
|
|
152
|
+
rows.push({ key, value: valStr, source });
|
|
153
|
+
}
|
|
154
|
+
if (rows.length === 0) return pc.dim(" (no config values)");
|
|
155
|
+
const maxKey = Math.max(...rows.map((r) => r.key.length), 3);
|
|
156
|
+
const maxVal = Math.max(...rows.map((r) => r.value.length), 5);
|
|
157
|
+
const header = ` ${"Key".padEnd(maxKey)} ${"Value".padEnd(maxVal)} Source`;
|
|
158
|
+
const sep = ` ${"\u2500".repeat(maxKey)} ${"\u2500".repeat(maxVal)} ${"\u2500".repeat(13)}`;
|
|
159
|
+
const lines = rows.map((r) => {
|
|
160
|
+
const color = colorize[r.source];
|
|
161
|
+
return ` ${r.key.padEnd(maxKey)} ${r.value.padEnd(maxVal)} ${color(r.source)}`;
|
|
162
|
+
});
|
|
163
|
+
return [header, sep, ...lines].join("\n");
|
|
164
|
+
}
|
|
165
|
+
async function resolveConfig(options) {
|
|
166
|
+
const configPath = options.configPath ?? "agent-qa.config.yaml";
|
|
167
|
+
const fileConfig = await loadRequiredConfigFile(configPath);
|
|
168
|
+
const envOverrides = loadEnvOverrides();
|
|
169
|
+
const merged = mergeConfigs(fileConfig, envOverrides, options.flags ?? {});
|
|
170
|
+
const normalized = normalizeConfigForValidation(merged);
|
|
171
|
+
const result = AgentQaConfigSchema.safeParse(normalized);
|
|
172
|
+
if (!result.success) {
|
|
173
|
+
const messages = result.error.issues.map((issue) => {
|
|
174
|
+
const path = issue.path.map(String).join(".");
|
|
175
|
+
return ` ${path}: ${issue.message}`;
|
|
176
|
+
});
|
|
177
|
+
const legacyDashboardEnabled = typeof merged === "object" && merged !== null && !Array.isArray(merged) && typeof merged.services === "object" && merged.services !== null && !Array.isArray(merged.services) && typeof merged.services.dashboard === "object" && merged.services.dashboard !== null && !Array.isArray(merged.services.dashboard) && "enabled" in merged.services.dashboard;
|
|
178
|
+
const dashboardHint = legacyDashboardEnabled ? `
|
|
179
|
+
|
|
180
|
+
Hint: services.dashboard.enabled was removed. Dashboard behavior is enabled by the presence of the services.dashboard block itself. Remove the enabled key and keep fields like port, dbPath, or artifactsDir.` : "";
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Config validation failed:
|
|
183
|
+
${messages.join("\n")}
|
|
184
|
+
|
|
185
|
+
Run 'agent-qa init' to generate a complete config file.` + dashboardHint
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (options.loadAuthPlugins !== false) {
|
|
189
|
+
await loadLLMAuthPlugins(result.data.plugins?.auth, {
|
|
190
|
+
baseDir: dirname(resolvePath(configPath))
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return result.data;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/targets.ts
|
|
197
|
+
function resolveTarget(config, targetName) {
|
|
198
|
+
const targets = config.registry?.targets;
|
|
199
|
+
if (!targets || !(targetName in targets)) {
|
|
200
|
+
throw new Error(`Target "${targetName}" not found in config. Available targets: ${targets ? Object.keys(targets).join(", ") : "none"}`);
|
|
201
|
+
}
|
|
202
|
+
const target = targets[targetName];
|
|
203
|
+
return {
|
|
204
|
+
name: targetName,
|
|
205
|
+
product: target.product ?? targetName,
|
|
206
|
+
platform: target.platform,
|
|
207
|
+
url: target.url,
|
|
208
|
+
bundleId: target.bundleId,
|
|
209
|
+
appPackage: target.appPackage,
|
|
210
|
+
appActivity: target.appActivity,
|
|
211
|
+
device: target.device
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function listTargets(config) {
|
|
215
|
+
const targets = config.registry?.targets;
|
|
216
|
+
if (!targets) return [];
|
|
217
|
+
return Object.entries(targets).map(([name, entry]) => {
|
|
218
|
+
const typedEntry = entry;
|
|
219
|
+
return {
|
|
220
|
+
name,
|
|
221
|
+
platform: typedEntry.platform
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/commands/cache.ts
|
|
227
|
+
import { Command } from "commander";
|
|
228
|
+
import pc2 from "picocolors";
|
|
229
|
+
import { readFile as readFile2, readdir, rm, stat } from "fs/promises";
|
|
230
|
+
import { parse } from "yaml";
|
|
231
|
+
import { join, resolve, basename } from "path";
|
|
232
|
+
import { createInterface } from "readline";
|
|
233
|
+
import { DEFAULT_AGENT_QA_CACHE_DIR, hashStepInstruction } from "@vostride/agent-qa-core";
|
|
234
|
+
async function purgeTest(testPath, cacheDir, configContent = "") {
|
|
235
|
+
const content = await readFile2(testPath, "utf-8");
|
|
236
|
+
const doc = parse(content);
|
|
237
|
+
const platform = doc.platform ?? "web";
|
|
238
|
+
const steps = (doc.steps ?? []).map(
|
|
239
|
+
(s) => typeof s === "string" ? s : s.step
|
|
240
|
+
);
|
|
241
|
+
const resolvedCacheDir = resolve(cacheDir);
|
|
242
|
+
let purged = 0;
|
|
243
|
+
for (let i = 0; i < steps.length; i++) {
|
|
244
|
+
const stepHash = hashStepInstruction(steps[i], platform, configContent, content, i);
|
|
245
|
+
const dirPath = join(resolvedCacheDir, stepHash);
|
|
246
|
+
try {
|
|
247
|
+
await stat(dirPath);
|
|
248
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
249
|
+
purged++;
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return purged;
|
|
254
|
+
}
|
|
255
|
+
async function purgeAll(cacheDir, force) {
|
|
256
|
+
const resolvedDir = resolve(cacheDir);
|
|
257
|
+
let entries;
|
|
258
|
+
try {
|
|
259
|
+
entries = await readdir(resolvedDir);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err.code === "ENOENT") return 0;
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
if (entries.length === 0) return 0;
|
|
265
|
+
if (!force) {
|
|
266
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
267
|
+
const answer = await new Promise((res) => {
|
|
268
|
+
rl.question(`About to delete ${entries.length} cached plans. Continue? (y/N) `, res);
|
|
269
|
+
});
|
|
270
|
+
rl.close();
|
|
271
|
+
if (answer.toLowerCase() !== "y") {
|
|
272
|
+
console.log("Aborted.");
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
await rm(join(resolvedDir, entry), { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
return entries.length;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/index.ts
|
|
283
|
+
var VERSION = getAgentQaVersion();
|
|
284
|
+
export {
|
|
285
|
+
VERSION,
|
|
286
|
+
deepMerge,
|
|
287
|
+
formatConfigDebug,
|
|
288
|
+
listTargets,
|
|
289
|
+
loadConfigFile,
|
|
290
|
+
loadEnvOverrides,
|
|
291
|
+
loadRequiredConfigFile,
|
|
292
|
+
mergeConfigs,
|
|
293
|
+
mergeUseBlocks,
|
|
294
|
+
mergeWithTestConfig,
|
|
295
|
+
purgeAll,
|
|
296
|
+
purgeTest,
|
|
297
|
+
resolveConfig,
|
|
298
|
+
resolveTarget
|
|
299
|
+
};
|
|
300
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/targets.ts","../src/commands/cache.ts"],"sourcesContent":["import { getAgentQaVersion } from '@vostride/agent-qa-core'\n\nexport * from '@vostride/agent-qa-core'\nexport const VERSION = getAgentQaVersion()\nexport * from './config.js'\nexport * from './targets.js'\nexport { purgeTest, purgeAll } from './commands/cache.js'\n","import { readFile } from 'node:fs/promises'\nimport { dirname, resolve as resolvePath } from 'node:path'\nimport { parse as parseYaml } from 'yaml'\nimport pc from 'picocolors'\nimport { AgentQaConfigSchema, loadLLMAuthPlugins } from '@vostride/agent-qa-core'\nimport type { AgentQaConfig } from '@vostride/agent-qa-core'\n\nconst ENV_MAPPING: Record<string, string> = {\n AGENT_QA_DASHBOARD_PORT: 'services.dashboard.port',\n AGENT_QA_MCP_PORT: 'services.mcp.port',\n AGENT_QA_CACHE_DIR: 'services.cache.dir',\n AGENT_QA_CACHE_TTL: 'services.cache.ttl',\n AGENT_QA_LOG_LEVEL: 'services.logging.level',\n AGENT_QA_HEADLESS: 'use.browser.headless',\n}\n\nfunction setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.')\n let current: Record<string, unknown> = obj\n for (let i = 0; i < parts.length - 1; i++) {\n const key = parts[i]\n if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {\n current[key] = {}\n }\n current = current[key] as Record<string, unknown>\n }\n current[parts[parts.length - 1]] = value\n}\n\nfunction parseEnvValue(key: string, value: string): unknown {\n if (key === 'AGENT_QA_DASHBOARD_PORT' || key === 'AGENT_QA_MCP_PORT') {\n const parsed = Number.parseInt(value, 10)\n if (Number.isInteger(parsed)) return parsed\n }\n if (key === 'AGENT_QA_HEADLESS') {\n const normalized = value.trim().toLowerCase()\n if (normalized === 'true') return true\n if (normalized === 'false') return false\n }\n return value\n}\n\nexport async function loadConfigFile(configPath: string): Promise<unknown> {\n try {\n const content = await readFile(configPath, 'utf-8')\n return parseYaml(content) ?? {}\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {}\n throw err\n }\n}\n\nexport async function loadRequiredConfigFile(configPath: string): Promise<unknown> {\n try {\n const content = await readFile(configPath, 'utf-8')\n return parseYaml(content) ?? {}\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n throw new Error(`Config file not found: ${configPath}`)\n }\n throw err\n }\n}\n\nexport function loadEnvOverrides(): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const [envKey, configPath] of Object.entries(ENV_MAPPING)) {\n const value = process.env[envKey]\n if (value !== undefined) {\n setNestedValue(result, configPath, parseEnvValue(envKey, value))\n }\n }\n return result\n}\n\nfunction isPlainObject(val: unknown): val is Record<string, unknown> {\n return typeof val === 'object' && val !== null && !Array.isArray(val)\n}\n\nexport function mergeConfigs(\n file: unknown,\n env: Record<string, unknown>,\n flags: Record<string, unknown>,\n): unknown {\n return deepMerge(deepMerge(file ?? {}, env), flags)\n}\n\nexport function deepMerge(base: unknown, override: unknown): unknown {\n if (!isPlainObject(base) || !isPlainObject(override)) return override\n const result: Record<string, unknown> = { ...base }\n for (const key of Object.keys(override)) {\n if (key in result && isPlainObject(result[key]) && isPlainObject(override[key])) {\n result[key] = deepMerge(result[key], override[key])\n } else {\n result[key] = override[key]\n }\n }\n return result\n}\n\nexport function mergeWithTestConfig(\n globalConfig: unknown,\n testConfig: unknown,\n flagOverrides: Record<string, unknown>,\n): unknown {\n return deepMerge(deepMerge(globalConfig ?? {}, testConfig ?? {}), flagOverrides)\n}\n\nfunction normalizeConfigForValidation(config: unknown): unknown {\n if (!isPlainObject(config)) return config\n const normalized = deepMerge({}, config) as Record<string, unknown>\n const use = normalized.use\n if (isPlainObject(use)) {\n delete use.headless\n }\n return normalized\n}\n\nexport function mergeUseBlocks(\n globalUse: Record<string, unknown> | undefined,\n suiteUse: Record<string, unknown> | undefined,\n testUse: Record<string, unknown> | undefined,\n cliFlags: Record<string, unknown>,\n): Record<string, unknown> {\n return deepMerge(\n deepMerge(\n deepMerge(globalUse ?? {}, suiteUse ?? {}),\n testUse ?? {},\n ),\n cliFlags,\n ) as Record<string, unknown>\n}\n\nfunction flattenObject(obj: unknown, prefix = ''): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {\n if (prefix) result[prefix] = obj\n return result\n }\n for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n const path = prefix ? `${prefix}.${key}` : key\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n Object.assign(result, flattenObject(value, path))\n } else {\n result[path] = value\n }\n }\n return result\n}\n\ntype ConfigSource = 'global config' | 'test yaml' | 'cli flag'\n\nexport function formatConfigDebug(\n globalConfig: unknown,\n testConfig: unknown,\n flagOverrides: Record<string, unknown>,\n): string {\n const layers: { flat: Record<string, unknown>; label: ConfigSource }[] = [\n { flat: flattenObject(globalConfig), label: 'global config' },\n { flat: flattenObject(testConfig), label: 'test yaml' },\n { flat: flattenObject(flagOverrides), label: 'cli flag' },\n ]\n\n const merged = mergeWithTestConfig(globalConfig, testConfig, flagOverrides)\n const mergedFlat = flattenObject(merged)\n\n const colorize: Record<ConfigSource, (s: string) => string> = {\n 'global config': pc.dim,\n 'test yaml': pc.cyan,\n 'cli flag': pc.green,\n }\n\n const rows: { key: string; value: string; source: ConfigSource }[] = []\n for (const key of Object.keys(mergedFlat).sort()) {\n let source: ConfigSource = 'global config'\n // Check layers in reverse priority order — last match wins\n for (const layer of layers) {\n if (key in layer.flat) {\n source = layer.label\n }\n }\n const valStr = typeof mergedFlat[key] === 'string' ? mergedFlat[key] : JSON.stringify(mergedFlat[key])\n rows.push({ key, value: valStr, source })\n }\n\n if (rows.length === 0) return pc.dim(' (no config values)')\n\n const maxKey = Math.max(...rows.map(r => r.key.length), 3)\n const maxVal = Math.max(...rows.map(r => r.value.length), 5)\n\n const header = ` ${'Key'.padEnd(maxKey)} ${'Value'.padEnd(maxVal)} Source`\n const sep = ` ${'─'.repeat(maxKey)} ${'─'.repeat(maxVal)} ${'─'.repeat(13)}`\n const lines = rows.map(r => {\n const color = colorize[r.source]\n return ` ${r.key.padEnd(maxKey)} ${r.value.padEnd(maxVal)} ${color(r.source)}`\n })\n\n return [header, sep, ...lines].join('\\n')\n}\n\nexport async function resolveConfig(options: {\n configPath?: string\n flags?: Record<string, unknown>\n loadAuthPlugins?: boolean\n}): Promise<AgentQaConfig> {\n const configPath = options.configPath ?? 'agent-qa.config.yaml'\n const fileConfig = await loadRequiredConfigFile(configPath)\n const envOverrides = loadEnvOverrides()\n const merged = mergeConfigs(fileConfig, envOverrides, options.flags ?? {})\n const normalized = normalizeConfigForValidation(merged)\n\n const result = AgentQaConfigSchema.safeParse(normalized)\n if (!result.success) {\n const messages = result.error.issues.map((issue: { path: PropertyKey[]; message: string }) => {\n const path = issue.path.map(String).join('.')\n return ` ${path}: ${issue.message}`\n })\n const legacyDashboardEnabled =\n typeof merged === 'object'\n && merged !== null\n && !Array.isArray(merged)\n && typeof (merged as Record<string, unknown>).services === 'object'\n && (merged as Record<string, unknown>).services !== null\n && !Array.isArray((merged as Record<string, unknown>).services)\n && typeof ((merged as Record<string, unknown>).services as Record<string, unknown>).dashboard === 'object'\n && ((merged as Record<string, unknown>).services as Record<string, unknown>).dashboard !== null\n && !Array.isArray(((merged as Record<string, unknown>).services as Record<string, unknown>).dashboard)\n && 'enabled' in (((merged as Record<string, unknown>).services as Record<string, unknown>).dashboard as Record<string, unknown>)\n\n const dashboardHint = legacyDashboardEnabled\n ? `\\n\\nHint: services.dashboard.enabled was removed. Dashboard behavior is enabled by the presence of the services.dashboard block itself. Remove the enabled key and keep fields like port, dbPath, or artifactsDir.`\n : ''\n throw new Error(\n `Config validation failed:\\n${messages.join('\\n')}\\n\\n` +\n `Run 'agent-qa init' to generate a complete config file.` +\n dashboardHint\n )\n }\n if (options.loadAuthPlugins !== false) {\n await loadLLMAuthPlugins(result.data.plugins?.auth, {\n baseDir: dirname(resolvePath(configPath)),\n })\n }\n return result.data\n}\n","import type { AgentQaConfig } from '@vostride/agent-qa-core'\n\nexport interface ResolvedTarget {\n name: string\n product: string\n platform: 'web' | 'android' | 'ios'\n url?: string\n bundleId?: string\n appPackage?: string\n appActivity?: string\n device?: string\n}\n\nexport interface TargetSummary {\n name: string\n platform: 'web' | 'android' | 'ios'\n}\n\ninterface TargetEntry {\n product?: string\n platform: 'web' | 'android' | 'ios'\n bundleId?: string\n appPackage?: string\n appActivity?: string\n url?: string\n device?: string\n}\n\nexport function resolveTarget(\n config: AgentQaConfig,\n targetName: string,\n): ResolvedTarget {\n const targets = (config as any).registry?.targets\n if (!targets || !(targetName in targets)) {\n throw new Error(`Target \"${targetName}\" not found in config. Available targets: ${targets ? Object.keys(targets).join(', ') : 'none'}`)\n }\n\n const target = targets[targetName] as TargetEntry\n\n return {\n name: targetName,\n product: target.product ?? targetName,\n platform: target.platform,\n url: target.url,\n bundleId: target.bundleId,\n appPackage: target.appPackage,\n appActivity: target.appActivity,\n device: target.device,\n }\n}\n\nexport function listTargets(config: AgentQaConfig): TargetSummary[] {\n const targets = (config as any).registry?.targets\n if (!targets) return []\n\n return Object.entries(targets).map(([name, entry]) => {\n const typedEntry = entry as TargetEntry\n return {\n name,\n platform: typedEntry.platform,\n }\n })\n}\n","import { Command } from 'commander'\nimport pc from 'picocolors'\nimport { readFile, readdir, rm, stat } from 'node:fs/promises'\nimport { parse } from 'yaml'\nimport { join, resolve, basename } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport { DEFAULT_AGENT_QA_CACHE_DIR, hashStepInstruction } from '@vostride/agent-qa-core'\nimport { resolveConfig } from '../config.js'\n\nconst CACHE_DIR_FALLBACK = DEFAULT_AGENT_QA_CACHE_DIR || '.agent-qa/cache'\n\nexport async function purgeTest(testPath: string, cacheDir: string, configContent = ''): Promise<number> {\n const content = await readFile(testPath, 'utf-8')\n const doc = parse(content)\n const platform: string = doc.platform ?? 'web'\n const steps: string[] = (doc.steps ?? []).map((s: unknown) =>\n typeof s === 'string' ? s : (s as Record<string, string>).step,\n )\n\n const resolvedCacheDir = resolve(cacheDir)\n let purged = 0\n\n for (let i = 0; i < steps.length; i++) {\n const stepHash = hashStepInstruction(steps[i], platform, configContent, content, i)\n const dirPath = join(resolvedCacheDir, stepHash)\n try {\n await stat(dirPath)\n await rm(dirPath, { recursive: true, force: true })\n purged++\n } catch {\n // directory doesn't exist — skip\n }\n }\n\n return purged\n}\n\nexport async function purgeAll(cacheDir: string, force: boolean): Promise<number> {\n const resolvedDir = resolve(cacheDir)\n let entries: string[]\n try {\n entries = await readdir(resolvedDir)\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return 0\n throw err\n }\n\n if (entries.length === 0) return 0\n\n if (!force) {\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n const answer = await new Promise<string>((res) => {\n rl.question(`About to delete ${entries.length} cached plans. Continue? (y/N) `, res)\n })\n rl.close()\n if (answer.toLowerCase() !== 'y') {\n console.log('Aborted.')\n return 0\n }\n }\n\n for (const entry of entries) {\n await rm(join(resolvedDir, entry), { recursive: true, force: true })\n }\n\n return entries.length\n}\n\nexport function createCacheCommand(): Command {\n const cmd = new Command('cache').description('Manage action cache')\n\n cmd\n .command('purge')\n .description('Clear cached action plans')\n .option('--test <path>', 'purge cache for a specific test file')\n .option('--all', 'purge all cached plans')\n .option('--force', 'skip confirmation prompt')\n .action(async (opts) => {\n if (!opts.test && !opts.all) {\n console.error(pc.red('Specify --test <path> or --all'))\n process.exitCode = 1\n return\n }\n\n const parentOpts = cmd.parent?.opts() ?? {}\n const config = await resolveConfig({\n configPath: parentOpts.config as string | undefined,\n })\n const cacheDir = config.services?.cache?.dir ?? CACHE_DIR_FALLBACK\n\n // Read raw config file content for cache key scoping\n const configPath = (parentOpts.config as string | undefined) ?? 'agent-qa.config.yaml'\n let configContent = ''\n try {\n configContent = await readFile(configPath, 'utf-8')\n } catch { /* config file may not exist */ }\n\n if (opts.test) {\n const count = await purgeTest(opts.test, cacheDir, configContent)\n if (count === 0) {\n console.log(pc.dim('No cached plans found'))\n } else {\n console.log(pc.green(`Purged ${count} cached plans for ${basename(opts.test)}`))\n }\n } else if (opts.all) {\n const count = await purgeAll(cacheDir, !!opts.force)\n if (count === 0) {\n console.log(pc.dim('No cached plans found'))\n } else {\n console.log(pc.green(`Purged ${count} cached plans (all)`))\n }\n }\n })\n\n return cmd\n}\n"],"mappings":"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","names":["pc","readFile","readFile"]}
|
package/package.json
CHANGED
|
@@ -1,11 +1,82 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-qa",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "agent-qa command-line interface for AI-powered web and mobile testing.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-qa",
|
|
8
|
+
"testing",
|
|
9
|
+
"automation",
|
|
10
|
+
"cli",
|
|
11
|
+
"web",
|
|
12
|
+
"mobile"
|
|
13
|
+
],
|
|
14
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
15
|
+
"homepage": "https://github.com/vostride/agent-qa#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/vostride/agent-qa/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/vostride/agent-qa.git",
|
|
22
|
+
"directory": "packages/cli"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"bin": {
|
|
29
|
+
"agent-qa": "./dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js",
|
|
35
|
+
"require": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"skills",
|
|
45
|
+
"LICENSE.md",
|
|
46
|
+
"NOTICE.md"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"clean": "rm -rf dist .turbo"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@inquirer/checkbox": "5.1.4",
|
|
57
|
+
"@inquirer/confirm": "6.0.12",
|
|
58
|
+
"@inquirer/input": "5.0.12",
|
|
59
|
+
"@inquirer/password": "5.0.12",
|
|
60
|
+
"@inquirer/select": "5.1.4",
|
|
61
|
+
"@vostride/agent-qa-android": "0.1.2",
|
|
62
|
+
"@vostride/agent-qa-core": "0.1.2",
|
|
63
|
+
"@vostride/agent-qa-dashboard": "0.1.2",
|
|
64
|
+
"@vostride/agent-qa-dashboard-ui": "0.1.2",
|
|
65
|
+
"@vostride/agent-qa-ids": "0.1.2",
|
|
66
|
+
"@vostride/agent-qa-ios": "0.1.2",
|
|
67
|
+
"@vostride/agent-qa-mcp": "0.1.2",
|
|
68
|
+
"@vostride/agent-qa-web": "0.1.2",
|
|
69
|
+
"ai": "6.0.174",
|
|
70
|
+
"commander": "14.0.3",
|
|
71
|
+
"glob": "13.0.6",
|
|
72
|
+
"picocolors": "1.1.1",
|
|
73
|
+
"yaml": "2.8.4",
|
|
74
|
+
"zod": "4.4.2"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@types/node": "~24.12.2",
|
|
78
|
+
"tsup": "~8.5.1",
|
|
79
|
+
"typescript": "~6.0.3",
|
|
80
|
+
"vitest": "~4.1.5"
|
|
81
|
+
}
|
|
11
82
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-qa-authoring
|
|
3
|
+
description: Use when creating, editing, validating, or running agent-qa tests, suites, or hooks. Prefer agent-qa MCP tools, enforce canonical agent-qa IDs, and use the bundled schema reference to avoid hallucinated config keys or YAML fields.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Author agent-qa tests safely
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# agent-qa Authoring
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Discover the local surface with `agent_qa_discover`.
|
|
13
|
+
2. Inspect active config with `agent_qa_get_config`, especially targets, devices, providers, and `services.mcp`.
|
|
14
|
+
3. Load `references/agent-qa-contracts.json` when you need exact schema fields or ID contracts.
|
|
15
|
+
4. Generate every new ID with agent-qa tooling:
|
|
16
|
+
- MCP: `agent_qa_generate_id`
|
|
17
|
+
- CLI fallback: `agent-qa ids generate <test|suite|hook|run|observation>`
|
|
18
|
+
- npx fallback: `npx --yes agent-qa ids generate <type>`
|
|
19
|
+
5. Never hand-write IDs. Validate existing IDs with `agent_qa_validate_id` or `agent-qa ids validate <type> <id> --json`.
|
|
20
|
+
6. Validate definitions before saving:
|
|
21
|
+
- Tests: `agent_qa_validate_test` or `agent_qa_validate_definition` with `kind: "test"`
|
|
22
|
+
- Suites: `agent_qa_validate_suite` or `agent_qa_validate_definition` with `kind: "suite"`
|
|
23
|
+
- Hooks: `agent_qa_validate_definition` with `kind: "hooks"`
|
|
24
|
+
7. Prefer MCP authoring mutations:
|
|
25
|
+
- Tests: `agent_qa_create_test`, `agent_qa_update_test`, `agent_qa_delete_test`
|
|
26
|
+
- Suites: `agent_qa_create_suite`, `agent_qa_update_suite`, `agent_qa_delete_suite`
|
|
27
|
+
- Hooks: `agent_qa_create_hook`, `agent_qa_update_hook`, `agent_qa_delete_hook`
|
|
28
|
+
8. Use CLI/YAML fallback only when MCP is unavailable. Keep file paths matched by `workspace.testMatch` or `workspace.suiteMatch`.
|
|
29
|
+
|
|
30
|
+
## Required ID Contracts
|
|
31
|
+
|
|
32
|
+
- Test IDs: `t_` + 10 id-agent words.
|
|
33
|
+
- Suite IDs: `s_` + 10 id-agent words.
|
|
34
|
+
- Hook IDs: `h_` + 10 id-agent words.
|
|
35
|
+
- Run IDs: `r_` + 10 id-agent words.
|
|
36
|
+
- Observation IDs: `obs_` + 10 id-agent words.
|
|
37
|
+
|
|
38
|
+
## Before Running
|
|
39
|
+
|
|
40
|
+
- Validate YAML first.
|
|
41
|
+
- Prefer `agent_qa_enqueue_test_run` and `agent_qa_enqueue_suite_run` over shelling out.
|
|
42
|
+
- If using CLI fallback, run only after validation succeeds.
|
|
43
|
+
|
|
44
|
+
## Do Not
|
|
45
|
+
|
|
46
|
+
- Do not invent config keys.
|
|
47
|
+
- Do not use legacy root config buckets.
|
|
48
|
+
- Do not hand-write IDs.
|
|
49
|
+
- Do not mutate files outside the configured workspace patterns.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "agent-qa Authoring"
|
|
3
|
+
short_description: "Author tests with real agent-qa contracts"
|
|
4
|
+
default_prompt: "Use $agent-qa-authoring to create and validate an agent-qa test with canonical IDs."
|
|
5
|
+
|
|
6
|
+
dependencies:
|
|
7
|
+
tools:
|
|
8
|
+
- type: "mcp"
|
|
9
|
+
value: "agent-qa"
|
|
10
|
+
description: "agent-qa local MCP server for schema, ID, authoring, and run tools."
|
|
11
|
+
transport: "streamable_http"
|
|
12
|
+
url: "http://127.0.0.1:3471/mcp"
|
|
13
|
+
|
|
14
|
+
policy:
|
|
15
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"source": "packages/mcp/references/agent-qa-contracts.json",
|
|
4
|
+
"ids": {
|
|
5
|
+
"instruction": "Generate IDs with agent_qa_generate_id or `agent-qa ids generate <type>`; never hand-write IDs.",
|
|
6
|
+
"contracts": [
|
|
7
|
+
{ "type": "test", "prefix": "t", "prefixWithSeparator": "t_", "words": 10 },
|
|
8
|
+
{ "type": "suite", "prefix": "s", "prefixWithSeparator": "s_", "words": 10 },
|
|
9
|
+
{ "type": "hook", "prefix": "h", "prefixWithSeparator": "h_", "words": 10 },
|
|
10
|
+
{ "type": "observation", "prefix": "obs", "prefixWithSeparator": "obs_", "words": 10 },
|
|
11
|
+
{ "type": "run", "prefix": "r", "prefixWithSeparator": "r_", "words": 10 }
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"config": {
|
|
15
|
+
"rootKeys": ["workspace", "services", "registry", "use", "analytics"],
|
|
16
|
+
"mcpFields": ["enabled", "transport", "host", "port", "path"]
|
|
17
|
+
},
|
|
18
|
+
"test": {
|
|
19
|
+
"requiredKeys": ["test-id", "name", "target", "steps"],
|
|
20
|
+
"optionalKeys": ["context", "use", "meta", "setup", "teardown"]
|
|
21
|
+
},
|
|
22
|
+
"suite": {
|
|
23
|
+
"requiredKeys": ["name", "target", "tests"],
|
|
24
|
+
"optionalKeys": ["suite-id", "context", "setup", "teardown", "use"],
|
|
25
|
+
"testEntryKeys": ["test", "id"]
|
|
26
|
+
},
|
|
27
|
+
"hooks": {
|
|
28
|
+
"rootKeys": ["hooks"],
|
|
29
|
+
"hookKeys": ["id", "name", "runtime", "file", "deps", "packageFile", "timeout", "network"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-qa-debug-fix
|
|
3
|
+
description: Use after an agent-qa run has failed and you need to debug, patch, and verify the issue using MCP evidence, logs, artifacts, and local code changes instead of generated fix suggestions.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Debug and fix failed agent-qa runs
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# agent-qa Debug Fix
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Start with evidence collection:
|
|
13
|
+
- `agent_qa_get_run`
|
|
14
|
+
- `agent_qa_get_run_steps`
|
|
15
|
+
- `agent_qa_get_run_artifact`
|
|
16
|
+
- `agent_qa_get_run_logs`
|
|
17
|
+
- `agent_qa_get_run_execution_logs`
|
|
18
|
+
2. Call `agent_qa_classify_failure` and treat its category as a hypothesis, not a verdict.
|
|
19
|
+
3. Identify the failing surface: test definition, hook, application under test, runtime infrastructure, or agent behavior.
|
|
20
|
+
4. Inspect the relevant local files directly. Do not infer patches from artifacts alone.
|
|
21
|
+
5. Apply the smallest code or YAML change that explains the evidence.
|
|
22
|
+
6. Re-run the narrowest affected agent-qa test, suite, hook, or unit test.
|
|
23
|
+
7. Report the root cause, changed files, verification command, and any remaining risk.
|
|
24
|
+
|
|
25
|
+
## Fix Rules
|
|
26
|
+
|
|
27
|
+
- Do not invent selectors, screen states, screenshots, logs, or source files.
|
|
28
|
+
- Do not rewrite a test just to make it pass if the artifact shows a product or runtime defect.
|
|
29
|
+
- Preserve canonical agent-qa IDs when editing tests, suites, hooks, or memory files.
|
|
30
|
+
- Prefer `agent_qa_validate_test`, `agent_qa_validate_suite`, and `agent_qa_validate_definition` before re-running edited YAML.
|
|
31
|
+
- When MCP is unavailable, use dashboard REST APIs or local `.agent-qa` artifacts and state that MCP evidence was unavailable.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "agent-qa Debug Fix"
|
|
3
|
+
short_description: "Patch failed runs from evidence"
|
|
4
|
+
default_prompt: "Use $agent-qa-debug-fix to debug this failed agent-qa run, patch the root cause, and verify it."
|
|
5
|
+
|
|
6
|
+
dependencies:
|
|
7
|
+
tools:
|
|
8
|
+
- type: "mcp"
|
|
9
|
+
value: "agent-qa"
|
|
10
|
+
description: "agent-qa local MCP server for run, artifact, log, validation, and triage tools."
|
|
11
|
+
transport: "streamable_http"
|
|
12
|
+
url: "http://127.0.0.1:3471/mcp"
|
|
13
|
+
|
|
14
|
+
policy:
|
|
15
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-qa-result-triage
|
|
3
|
+
description: Use when investigating failed agent-qa runs, inspecting artifacts, classifying failures, or comparing recent runs. Prefer agent-qa MCP run/artifact tools and produce evidence-backed triage.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Triage agent-qa run failures
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# agent-qa Result Triage
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Start with `agent_qa_get_run` for run status, suite child context, steps, and attempts.
|
|
13
|
+
2. Fetch evidence before deciding:
|
|
14
|
+
- `agent_qa_get_run_artifact`
|
|
15
|
+
- `agent_qa_get_run_steps`
|
|
16
|
+
- `agent_qa_get_run_logs`
|
|
17
|
+
- `agent_qa_get_run_execution_logs`
|
|
18
|
+
3. Call `agent_qa_classify_failure` and use its category as the default classification unless stronger evidence contradicts it.
|
|
19
|
+
4. Compare recent related runs when available in the classifier output.
|
|
20
|
+
5. Return a concise triage result: category, confidence, evidence, likely fix area, and next action.
|
|
21
|
+
6. For code changes, switch to `agent-qa-debug-fix` after triage is complete.
|
|
22
|
+
|
|
23
|
+
## Categories
|
|
24
|
+
|
|
25
|
+
Use one of the fixed categories from `references/triage-categories.md`:
|
|
26
|
+
|
|
27
|
+
- `timeout`
|
|
28
|
+
- `appium_startup`
|
|
29
|
+
- `browser_disconnect`
|
|
30
|
+
- `element_not_found`
|
|
31
|
+
- `assertion_failure`
|
|
32
|
+
- `hook_failure`
|
|
33
|
+
- `infrastructure`
|
|
34
|
+
- `unknown_failure`
|
|
35
|
+
|
|
36
|
+
## Evidence Rules
|
|
37
|
+
|
|
38
|
+
- Quote or summarize concrete artifact/log/step evidence.
|
|
39
|
+
- Mention missing artifact sections if they limit confidence.
|
|
40
|
+
- Do not invent screenshots, videos, logs, or memory context that was not returned by MCP.
|
|
41
|
+
- If MCP is unavailable, use dashboard REST APIs or `agent-qa` CLI output as fallback and say which evidence was unavailable.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "agent-qa Result Triage"
|
|
3
|
+
short_description: "Classify failures from run artifacts"
|
|
4
|
+
default_prompt: "Use $agent-qa-result-triage to classify this failed agent-qa run with evidence."
|
|
5
|
+
|
|
6
|
+
dependencies:
|
|
7
|
+
tools:
|
|
8
|
+
- type: "mcp"
|
|
9
|
+
value: "agent-qa"
|
|
10
|
+
description: "agent-qa local MCP server for run, artifact, log, and triage tools."
|
|
11
|
+
transport: "streamable_http"
|
|
12
|
+
url: "http://127.0.0.1:3471/mcp"
|
|
13
|
+
|
|
14
|
+
policy:
|
|
15
|
+
allow_implicit_invocation: true
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# agent-qa Triage Categories
|
|
2
|
+
|
|
3
|
+
Use exactly one category.
|
|
4
|
+
|
|
5
|
+
| Category | Use When | First Checks |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `timeout` | The run or step exceeded timeout. | Run failure summary, step duration, logs. |
|
|
8
|
+
| `appium_startup` | Appium failed to start or acquire a mobile session. | Failure summary, execution logs, artifact runtime errors. |
|
|
9
|
+
| `browser_disconnect` | Browser/page/context closed unexpectedly. | Error logs containing browser closed or target closed. |
|
|
10
|
+
| `element_not_found` | Locator, element, selector, or UI description was unavailable. | Failed step error, observation, screenshot, DOM/accessibility context. |
|
|
11
|
+
| `assertion_failure` | App was reachable but expected content/state did not match. | Failed assert/verify step, observation, screenshot. |
|
|
12
|
+
| `hook_failure` | Setup/teardown/hook execution blocked the run. | Hook logs, hook artifact sections, hook registry errors. |
|
|
13
|
+
| `infrastructure` | Network, Docker, farm, device, filesystem, or service dependency failed. | Execution logs, stderr, artifact runtime errors. |
|
|
14
|
+
| `unknown_failure` | Evidence is insufficient for a stronger category. | Missing sections and next evidence to collect. |
|
|
15
|
+
|
|
16
|
+
Always include evidence and a next action.
|