@pcamarajr/scout 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cli.js +126 -19
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +73 -0
- package/dist/credentials.js +196 -0
- package/dist/credentials.js.map +1 -0
- package/dist/engine.d.ts +29 -0
- package/dist/engine.js +81 -41
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +26 -0
- package/dist/init.js +60 -0
- package/dist/init.js.map +1 -0
- package/dist/runner/agent-tools.d.ts +45 -0
- package/dist/runner/agent-tools.js +151 -0
- package/dist/runner/agent-tools.js.map +1 -0
- package/dist/runner/ai-runner.d.ts +11 -35
- package/dist/runner/ai-runner.js +36 -229
- package/dist/runner/ai-runner.js.map +1 -1
- package/dist/runner/engines/ai-sdk.d.ts +58 -0
- package/dist/runner/engines/ai-sdk.js +144 -0
- package/dist/runner/engines/ai-sdk.js.map +1 -0
- package/dist/runner/engines/claude-agent-sdk.d.ts +11 -0
- package/dist/runner/engines/claude-agent-sdk.js +117 -0
- package/dist/runner/engines/claude-agent-sdk.js.map +1 -0
- package/dist/runner/engines/index.d.ts +20 -0
- package/dist/runner/engines/index.js +28 -0
- package/dist/runner/engines/index.js.map +1 -0
- package/dist/runner/engines/types.d.ts +68 -0
- package/dist/runner/engines/types.js +37 -0
- package/dist/runner/engines/types.js.map +1 -0
- package/dist/scaffold.d.ts +31 -0
- package/dist/scaffold.js +82 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +9 -2
- package/templates/AGENTS.md +100 -0
- package/templates/SKILL.md +25 -0
- package/templates/scout.mdc +21 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* The original, trusted engine: runs the verification through the
|
|
4
|
+
* `@anthropic-ai/claude-agent-sdk` subprocess. Behavior here is intentionally
|
|
5
|
+
* IDENTICAL to the pre-refactor ai-runner: bypassPermissions, empty
|
|
6
|
+
* settingSources, allowedTools scoped to the in-process MCP browser server, the
|
|
7
|
+
* query()+drain() loop, and an abort-in-finally so no subprocess outlives a run.
|
|
8
|
+
*/
|
|
9
|
+
export class ClaudeAgentSdkEngine {
|
|
10
|
+
async run(spec) {
|
|
11
|
+
const browserServer = createSdkMcpServer({
|
|
12
|
+
name: "browser",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
tools: spec.tools.map(toAgentSdkTool),
|
|
15
|
+
});
|
|
16
|
+
const baseOptions = {
|
|
17
|
+
model: spec.model,
|
|
18
|
+
systemPrompt: spec.systemPrompt,
|
|
19
|
+
mcpServers: { browser: browserServer },
|
|
20
|
+
allowedTools: ["mcp__browser__*"],
|
|
21
|
+
tools: [],
|
|
22
|
+
settingSources: [],
|
|
23
|
+
permissionMode: "bypassPermissions",
|
|
24
|
+
};
|
|
25
|
+
const transcript = [];
|
|
26
|
+
let sessionId;
|
|
27
|
+
let endInfo;
|
|
28
|
+
// Drains a query, collecting transcript/session/end info. Always aborts at
|
|
29
|
+
// the end so no claude-agent-sdk subprocess outlives the run.
|
|
30
|
+
const drain = async (q, controller) => {
|
|
31
|
+
try {
|
|
32
|
+
for await (const message of q) {
|
|
33
|
+
if ("session_id" in message && message.session_id)
|
|
34
|
+
sessionId = message.session_id;
|
|
35
|
+
if (message.type === "assistant") {
|
|
36
|
+
for (const block of message.message.content) {
|
|
37
|
+
if (block.type === "text" && block.text.trim())
|
|
38
|
+
transcript.push(block.text.trim());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (message.type === "result") {
|
|
42
|
+
endInfo = {
|
|
43
|
+
subtype: message.subtype,
|
|
44
|
+
numTurns: message.num_turns,
|
|
45
|
+
errors: "errors" in message ? message.errors : undefined,
|
|
46
|
+
};
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
controller.abort();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const mainController = new AbortController();
|
|
56
|
+
try {
|
|
57
|
+
await drain(query({
|
|
58
|
+
prompt: spec.userPrompt,
|
|
59
|
+
options: { ...baseOptions, maxTurns: spec.maxTurns, abortController: mainController },
|
|
60
|
+
}), mainController);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
endInfo = {
|
|
64
|
+
subtype: "sdk_error",
|
|
65
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const resume = async (prompt, maxTurns) => {
|
|
69
|
+
const resumeTranscript = [];
|
|
70
|
+
let resumeEnd;
|
|
71
|
+
if (!sessionId) {
|
|
72
|
+
return { end: { subtype: "no_session" }, transcript: resumeTranscript };
|
|
73
|
+
}
|
|
74
|
+
const rescueController = new AbortController();
|
|
75
|
+
// Reuse drain() so transcript/session/end semantics stay identical, then
|
|
76
|
+
// split off only what the rescue produced for the resume result.
|
|
77
|
+
const beforeLen = transcript.length;
|
|
78
|
+
try {
|
|
79
|
+
await drain(query({
|
|
80
|
+
prompt,
|
|
81
|
+
options: { ...baseOptions, maxTurns, resume: sessionId, abortController: rescueController },
|
|
82
|
+
}), rescueController);
|
|
83
|
+
resumeEnd = endInfo;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
resumeEnd = {
|
|
87
|
+
subtype: "sdk_error",
|
|
88
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
resumeTranscript.push(...transcript.slice(beforeLen));
|
|
92
|
+
return { end: resumeEnd ?? { subtype: "no_session" }, transcript: resumeTranscript };
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
get end() {
|
|
96
|
+
return endInfo ?? { subtype: "sdk_error" };
|
|
97
|
+
},
|
|
98
|
+
transcript,
|
|
99
|
+
resume,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Adapts an engine-neutral ScoutTool into the Agent SDK `tool()` shape. The
|
|
105
|
+
* Agent SDK's tool() wants a raw Zod shape, so we hand it `schema.shape`, and we
|
|
106
|
+
* map our normalized {text, isError} result back into a CallToolResult.
|
|
107
|
+
*/
|
|
108
|
+
function toAgentSdkTool(scoutTool) {
|
|
109
|
+
return tool(scoutTool.name, scoutTool.description, scoutTool.schema.shape, async (args) => {
|
|
110
|
+
const result = await scoutTool.handler(args);
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: result.text }],
|
|
113
|
+
...(result.isError ? { isError: true } : {}),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=claude-agent-sdk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-agent-sdk.js","sourceRoot":"","sources":["../../../src/runner/engines/claude-agent-sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAUjF;;;;;;GAMG;AACH,MAAM,OAAO,oBAAoB;IAC/B,KAAK,CAAC,GAAG,CAAC,IAAmB;QAC3B,MAAM,aAAa,GAAG,kBAAkB,CAAC;YACvC,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,OAAO;YAChB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC;SACtC,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG;YAClB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,UAAU,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE;YACtC,YAAY,EAAE,CAAC,iBAAiB,CAAC;YACjC,KAAK,EAAE,EAAQ;YACf,cAAc,EAAE,EAAQ;YACxB,cAAc,EAAE,mBAA4B;SAC7C,CAAC;QAEF,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,IAAI,SAA6B,CAAC;QAClC,IAAI,OAAiC,CAAC;QAEtC,2EAA2E;QAC3E,8DAA8D;QAC9D,MAAM,KAAK,GAAG,KAAK,EACjB,CAA2B,EAC3B,UAA2B,EACZ,EAAE;YACjB,IAAI,CAAC;gBACH,IAAI,KAAK,EAAE,MAAM,OAAO,IAAI,CAAC,EAAE,CAAC;oBAC9B,IAAI,YAAY,IAAI,OAAO,IAAI,OAAO,CAAC,UAAU;wBAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;oBAClF,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;wBACjC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;4BAC5C,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;gCAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;wBACrF,CAAC;oBACH,CAAC;oBACD,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;wBAC9B,OAAO,GAAG;4BACR,OAAO,EAAE,OAAO,CAAC,OAAO;4BACxB,QAAQ,EAAE,OAAO,CAAC,SAAS;4BAC3B,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;yBACzD,CAAC;wBACF,MAAM;oBACR,CAAC;gBACH,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,UAAU,CAAC,KAAK,EAAE,CAAC;YACrB,CAAC;QACH,CAAC,CAAC;QAEF,MAAM,cAAc,GAAG,IAAI,eAAe,EAAE,CAAC;QAC7C,IAAI,CAAC;YACH,MAAM,KAAK,CACT,KAAK,CAAC;gBACJ,MAAM,EAAE,IAAI,CAAC,UAAU;gBACvB,OAAO,EAAE,EAAE,GAAG,WAAW,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE;aACtF,CAAC,EACF,cAAc,CACf,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,GAAG;gBACR,OAAO,EAAE,WAAW;gBACpB,MAAM,EAAE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;aACjE,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,EAAE,MAAc,EAAE,QAAgB,EAA+B,EAAE;YACrF,MAAM,gBAAgB,GAAa,EAAE,CAAC;YACtC,IAAI,SAAmC,CAAC;YACxC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,OAAO,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC;YAC1E,CAAC;YACD,MAAM,gBAAgB,GAAG,IAAI,eAAe,EAAE,CAAC;YAC/C,yEAAyE;YACzE,iEAAiE;YACjE,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC;YACpC,IAAI,CAAC;gBACH,MAAM,KAAK,CACT,KAAK,CAAC;oBACJ,MAAM;oBACN,OAAO,EAAE,EAAE,GAAG,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE;iBAC5F,CAAC,EACF,gBAAgB,CACjB,CAAC;gBACF,SAAS,GAAG,OAAO,CAAC;YACtB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,SAAS,GAAG;oBACV,OAAO,EAAE,WAAW;oBACpB,MAAM,EAAE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;iBACjE,CAAC;YACJ,CAAC;YACD,gBAAgB,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YACtD,OAAO,EAAE,GAAG,EAAE,SAAS,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC;QACvF,CAAC,CAAC;QAEF,OAAO;YACL,IAAI,GAAG;gBACL,OAAO,OAAO,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;YAC7C,CAAC;YACD,UAAU;YACV,MAAM;SACP,CAAC;IACJ,CAAC;CACF;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,SAAoB;IAC1C,OAAO,IAAI,CACT,SAAS,CAAC,IAAI,EACd,SAAS,CAAC,WAAW,EACrB,SAAS,CAAC,MAAM,CAAC,KAAK,EACtB,KAAK,EAAE,IAAa,EAAE,EAAE;QACtB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7C,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;YACvD,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7C,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AiProvider } from "../../credentials.js";
|
|
2
|
+
import { AiSdkEngine } from "./ai-sdk.js";
|
|
3
|
+
import { ClaudeAgentSdkEngine } from "./claude-agent-sdk.js";
|
|
4
|
+
import type { AgentEngine } from "./types.js";
|
|
5
|
+
/** The two engines Scout can run an AI verification through. */
|
|
6
|
+
export type EngineKind = "agent-sdk" | "ai-sdk";
|
|
7
|
+
/**
|
|
8
|
+
* Resolves which engine to use. Explicit preference (SCOUT_ENGINE / config
|
|
9
|
+
* `engine`) wins. Otherwise the default is forward-looking: the trusted Agent
|
|
10
|
+
* SDK for Anthropic, the AI SDK for non-Anthropic providers (which is the only
|
|
11
|
+
* engine that can reach them — still gated by credential detection until those
|
|
12
|
+
* providers are wired up).
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveEngineKind(provider: AiProvider, enginePref: EngineKind | undefined): EngineKind;
|
|
15
|
+
/** Instantiates the engine for a provider + explicit preference. */
|
|
16
|
+
export declare function selectEngine(provider: AiProvider, enginePref: EngineKind | undefined): AgentEngine;
|
|
17
|
+
/** Parses a raw SCOUT_ENGINE / config value, ignoring unknown strings. */
|
|
18
|
+
export declare function parseEngineKind(value: string | undefined): EngineKind | undefined;
|
|
19
|
+
export { AiSdkEngine, ClaudeAgentSdkEngine };
|
|
20
|
+
export type { AgentEngine };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AiSdkEngine } from "./ai-sdk.js";
|
|
2
|
+
import { ClaudeAgentSdkEngine } from "./claude-agent-sdk.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves which engine to use. Explicit preference (SCOUT_ENGINE / config
|
|
5
|
+
* `engine`) wins. Otherwise the default is forward-looking: the trusted Agent
|
|
6
|
+
* SDK for Anthropic, the AI SDK for non-Anthropic providers (which is the only
|
|
7
|
+
* engine that can reach them — still gated by credential detection until those
|
|
8
|
+
* providers are wired up).
|
|
9
|
+
*/
|
|
10
|
+
export function resolveEngineKind(provider, enginePref) {
|
|
11
|
+
if (enginePref)
|
|
12
|
+
return enginePref;
|
|
13
|
+
return provider === "anthropic" ? "agent-sdk" : "ai-sdk";
|
|
14
|
+
}
|
|
15
|
+
/** Instantiates the engine for a provider + explicit preference. */
|
|
16
|
+
export function selectEngine(provider, enginePref) {
|
|
17
|
+
return resolveEngineKind(provider, enginePref) === "ai-sdk"
|
|
18
|
+
? new AiSdkEngine()
|
|
19
|
+
: new ClaudeAgentSdkEngine();
|
|
20
|
+
}
|
|
21
|
+
/** Parses a raw SCOUT_ENGINE / config value, ignoring unknown strings. */
|
|
22
|
+
export function parseEngineKind(value) {
|
|
23
|
+
if (value === "agent-sdk" || value === "ai-sdk")
|
|
24
|
+
return value;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
export { AiSdkEngine, ClaudeAgentSdkEngine };
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/runner/engines/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAM7D;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAoB,EACpB,UAAkC;IAElC,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAClC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3D,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAC1B,QAAoB,EACpB,UAAkC;IAElC,OAAO,iBAAiB,CAAC,QAAQ,EAAE,UAAU,CAAC,KAAK,QAAQ;QACzD,CAAC,CAAC,IAAI,WAAW,EAAE;QACnB,CAAC,CAAC,IAAI,oBAAoB,EAAE,CAAC;AACjC,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,eAAe,CAAC,KAAyB;IACvD,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AiProvider } from "../../credentials.js";
|
|
2
|
+
import type { Step, Verdict } from "../../types.js";
|
|
3
|
+
import type { ScoutTool } from "../agent-tools.js";
|
|
4
|
+
export interface AiRunOutcome {
|
|
5
|
+
verdict: Verdict;
|
|
6
|
+
reason: string;
|
|
7
|
+
steps: Step[];
|
|
8
|
+
transcript: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Set when the runner itself failed to produce a verdict (agent never called
|
|
11
|
+
* scout_verdict) — NOT a UI judgment. Callers may retry and must report it
|
|
12
|
+
* as an infrastructure failure, never as "the scenario is blocked by the UI".
|
|
13
|
+
*/
|
|
14
|
+
runnerFailure?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How an engine run ended, for no-verdict diagnostics. The Agent SDK fills this
|
|
18
|
+
* from the SDK `result` message; the AI SDK engine maps its `finishReason` into
|
|
19
|
+
* the same `subtype` vocabulary so {@link describeNoVerdict} works unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export interface QueryEndInfo {
|
|
22
|
+
subtype: string;
|
|
23
|
+
numTurns?: number;
|
|
24
|
+
errors?: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Human-readable cause for a run that ended without scout_verdict.
|
|
28
|
+
* Distinguishes runner-infrastructure causes (turn budget, SDK errors) from
|
|
29
|
+
* an agent that simply stopped talking.
|
|
30
|
+
*/
|
|
31
|
+
export declare function describeNoVerdict(end: QueryEndInfo | undefined, maxTurns: number): string;
|
|
32
|
+
/**
|
|
33
|
+
* Records navigation relative to the configured baseUrl so cached scripts
|
|
34
|
+
* survive --base-url / SCOUT_BASE_URL pointing at another server. URLs that
|
|
35
|
+
* merely share the prefix (http://localhost:3000x) or live on other hosts
|
|
36
|
+
* stay absolute.
|
|
37
|
+
*/
|
|
38
|
+
export declare function relativizeUrl(url: string, baseUrl: string): string;
|
|
39
|
+
/** Everything an engine needs to drive one agent run. */
|
|
40
|
+
export interface EngineRunSpec {
|
|
41
|
+
/** Inferred from the model id; selects the AI SDK provider in the AI SDK engine. */
|
|
42
|
+
provider: AiProvider;
|
|
43
|
+
model: string;
|
|
44
|
+
systemPrompt: string;
|
|
45
|
+
userPrompt: string;
|
|
46
|
+
tools: ScoutTool[];
|
|
47
|
+
maxTurns: number;
|
|
48
|
+
}
|
|
49
|
+
/** The result of resuming a session (the forced-verdict rescue). */
|
|
50
|
+
export interface EngineResumeResult {
|
|
51
|
+
end: QueryEndInfo;
|
|
52
|
+
transcript: string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A live (or just-finished) engine session. The shared orchestrator reads
|
|
56
|
+
* `end`/`transcript`, and — if no verdict was captured — calls `resume()` to
|
|
57
|
+
* run the forced-verdict rescue with a small turn budget.
|
|
58
|
+
*/
|
|
59
|
+
export interface EngineSession {
|
|
60
|
+
end: QueryEndInfo;
|
|
61
|
+
/** Assistant text collected during the run, in order. */
|
|
62
|
+
transcript: string[];
|
|
63
|
+
resume(prompt: string, maxTurns: number): Promise<EngineResumeResult>;
|
|
64
|
+
}
|
|
65
|
+
/** A pluggable agent engine. `run` performs the initial verification pass. */
|
|
66
|
+
export interface AgentEngine {
|
|
67
|
+
run(spec: EngineRunSpec): Promise<EngineSession>;
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable cause for a run that ended without scout_verdict.
|
|
3
|
+
* Distinguishes runner-infrastructure causes (turn budget, SDK errors) from
|
|
4
|
+
* an agent that simply stopped talking.
|
|
5
|
+
*/
|
|
6
|
+
export function describeNoVerdict(end, maxTurns) {
|
|
7
|
+
if (!end)
|
|
8
|
+
return "a sessão do agente terminou sem emitir resultado (subprocesso morreu?)";
|
|
9
|
+
switch (end.subtype) {
|
|
10
|
+
case "error_max_turns":
|
|
11
|
+
return `o agente estourou o limite de ${maxTurns} turns sem chamar scout_verdict`;
|
|
12
|
+
case "error_during_execution":
|
|
13
|
+
return `erro durante a execução do agente${end.errors?.length ? `: ${end.errors.join("; ")}` : ""}`;
|
|
14
|
+
case "success":
|
|
15
|
+
return "o agente encerrou normalmente sem chamar scout_verdict";
|
|
16
|
+
default:
|
|
17
|
+
return `a sessão do agente terminou com "${end.subtype}"${end.errors?.length ? `: ${end.errors.join("; ")}` : ""}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Records navigation relative to the configured baseUrl so cached scripts
|
|
22
|
+
* survive --base-url / SCOUT_BASE_URL pointing at another server. URLs that
|
|
23
|
+
* merely share the prefix (http://localhost:3000x) or live on other hosts
|
|
24
|
+
* stay absolute.
|
|
25
|
+
*/
|
|
26
|
+
export function relativizeUrl(url, baseUrl) {
|
|
27
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
28
|
+
if (url === base || url === `${base}/`)
|
|
29
|
+
return "/";
|
|
30
|
+
if (url.startsWith(base)) {
|
|
31
|
+
const rest = url.slice(base.length);
|
|
32
|
+
if (rest.startsWith("/") || rest.startsWith("?") || rest.startsWith("#"))
|
|
33
|
+
return rest;
|
|
34
|
+
}
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/runner/engines/types.ts"],"names":[],"mappings":"AA4BA;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAA6B,EAAE,QAAgB;IAC/E,IAAI,CAAC,GAAG;QAAE,OAAO,wEAAwE,CAAC;IAC1F,QAAQ,GAAG,CAAC,OAAO,EAAE,CAAC;QACpB,KAAK,iBAAiB;YACpB,OAAO,iCAAiC,QAAQ,iCAAiC,CAAC;QACpF,KAAK,wBAAwB;YAC3B,OAAO,oCAAoC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACtG,KAAK,SAAS;YACZ,OAAO,wDAAwD,CAAC;QAClE;YACE,OAAO,oCAAoC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACvH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,OAAe;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACzC,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IACnD,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACxF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding artifacts written by `scout init` so any AI agent in the consuming
|
|
3
|
+
* repo can help a QA team configure Scout and author `.scout.md` scenarios.
|
|
4
|
+
*
|
|
5
|
+
* Three files, two ownership models:
|
|
6
|
+
* - AGENTS.md (repo root) — SHARED with the user and other tools. Scout owns
|
|
7
|
+
* only a managed block delimited by the markers below; everything outside
|
|
8
|
+
* it is left untouched.
|
|
9
|
+
* - .claude/skills/scout/SKILL.md and .cursor/rules/scout.mdc — Scout-OWNED,
|
|
10
|
+
* overwritten on every init (init is the upgrade path).
|
|
11
|
+
*
|
|
12
|
+
* Re-running init refreshes all three. `scout.config.json` is never touched here.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SCOUT_BLOCK_START = "<!-- scout:start -->";
|
|
15
|
+
export declare const SCOUT_BLOCK_END = "<!-- scout:end -->";
|
|
16
|
+
/** Resolve the bundled `templates/` dir, whether running from `src/` or `dist/`. */
|
|
17
|
+
export declare function templatesDir(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Embed `body` inside the scout managed block in `existing` content.
|
|
20
|
+
*
|
|
21
|
+
* - no existing content → just the block.
|
|
22
|
+
* - existing WITH a block → replace ONLY the block's contents.
|
|
23
|
+
* - existing WITHOUT a block → append the block, preserving everything else.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyManagedBlock(existing: string | undefined, body: string): string;
|
|
26
|
+
export interface ScaffoldDeps {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
log?: (message: string) => void;
|
|
29
|
+
}
|
|
30
|
+
/** Write/refresh AGENTS.md (managed block), the Claude skill, and the Cursor rule. */
|
|
31
|
+
export declare function scaffoldAgentOnboarding(deps?: ScaffoldDeps): void;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Onboarding artifacts written by `scout init` so any AI agent in the consuming
|
|
6
|
+
* repo can help a QA team configure Scout and author `.scout.md` scenarios.
|
|
7
|
+
*
|
|
8
|
+
* Three files, two ownership models:
|
|
9
|
+
* - AGENTS.md (repo root) — SHARED with the user and other tools. Scout owns
|
|
10
|
+
* only a managed block delimited by the markers below; everything outside
|
|
11
|
+
* it is left untouched.
|
|
12
|
+
* - .claude/skills/scout/SKILL.md and .cursor/rules/scout.mdc — Scout-OWNED,
|
|
13
|
+
* overwritten on every init (init is the upgrade path).
|
|
14
|
+
*
|
|
15
|
+
* Re-running init refreshes all three. `scout.config.json` is never touched here.
|
|
16
|
+
*/
|
|
17
|
+
export const SCOUT_BLOCK_START = "<!-- scout:start -->";
|
|
18
|
+
export const SCOUT_BLOCK_END = "<!-- scout:end -->";
|
|
19
|
+
const AGENTS_FILE = "AGENTS.md";
|
|
20
|
+
const SKILL_FILE = path.join(".claude", "skills", "scout", "SKILL.md");
|
|
21
|
+
const CURSOR_RULE_FILE = path.join(".cursor", "rules", "scout.mdc");
|
|
22
|
+
/** Resolve the bundled `templates/` dir, whether running from `src/` or `dist/`. */
|
|
23
|
+
export function templatesDir() {
|
|
24
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
// here is <pkg>/src or <pkg>/dist → templates sits at <pkg>/templates
|
|
26
|
+
return path.join(here, "..", "templates");
|
|
27
|
+
}
|
|
28
|
+
function readTemplate(name) {
|
|
29
|
+
return fs.readFileSync(path.join(templatesDir(), name), "utf8");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Embed `body` inside the scout managed block in `existing` content.
|
|
33
|
+
*
|
|
34
|
+
* - no existing content → just the block.
|
|
35
|
+
* - existing WITH a block → replace ONLY the block's contents.
|
|
36
|
+
* - existing WITHOUT a block → append the block, preserving everything else.
|
|
37
|
+
*/
|
|
38
|
+
export function applyManagedBlock(existing, body) {
|
|
39
|
+
const block = `${SCOUT_BLOCK_START}\n${body.trim()}\n${SCOUT_BLOCK_END}\n`;
|
|
40
|
+
if (existing === undefined || existing.trim() === "") {
|
|
41
|
+
return block;
|
|
42
|
+
}
|
|
43
|
+
const startIdx = existing.indexOf(SCOUT_BLOCK_START);
|
|
44
|
+
const endIdx = existing.indexOf(SCOUT_BLOCK_END);
|
|
45
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
46
|
+
const before = existing.slice(0, startIdx);
|
|
47
|
+
const after = existing.slice(endIdx + SCOUT_BLOCK_END.length);
|
|
48
|
+
// Trim a leading newline off `after` so we don't accumulate blank lines.
|
|
49
|
+
return `${before}${block}${after.replace(/^\n/, "")}`;
|
|
50
|
+
}
|
|
51
|
+
// No block yet — append, keeping the user's content and a clean separator.
|
|
52
|
+
const sep = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
53
|
+
return `${existing}${sep}${block}`;
|
|
54
|
+
}
|
|
55
|
+
/** Write/refresh AGENTS.md (managed block), the Claude skill, and the Cursor rule. */
|
|
56
|
+
export function scaffoldAgentOnboarding(deps = {}) {
|
|
57
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
58
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
59
|
+
// 1. AGENTS.md — managed block, never clobbers surrounding content.
|
|
60
|
+
const agentsPath = path.join(cwd, AGENTS_FILE);
|
|
61
|
+
const body = readTemplate("AGENTS.md");
|
|
62
|
+
const existing = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, "utf8") : undefined;
|
|
63
|
+
const had = existing !== undefined;
|
|
64
|
+
const hadBlock = had && existing.includes(SCOUT_BLOCK_START);
|
|
65
|
+
fs.writeFileSync(agentsPath, applyManagedBlock(existing, body));
|
|
66
|
+
log(hadBlock
|
|
67
|
+
? `✓ ${AGENTS_FILE} — refreshed the Scout block.`
|
|
68
|
+
: had
|
|
69
|
+
? `✓ ${AGENTS_FILE} — appended the Scout block (your content kept).`
|
|
70
|
+
: `✓ ${AGENTS_FILE} created.`);
|
|
71
|
+
// 2. Claude Code skill — Scout-owned, overwrite.
|
|
72
|
+
const skillPath = path.join(cwd, SKILL_FILE);
|
|
73
|
+
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
74
|
+
fs.writeFileSync(skillPath, readTemplate("SKILL.md"));
|
|
75
|
+
log(`✓ ${SKILL_FILE}`);
|
|
76
|
+
// 3. Cursor rule — Scout-owned, overwrite.
|
|
77
|
+
const cursorPath = path.join(cwd, CURSOR_RULE_FILE);
|
|
78
|
+
fs.mkdirSync(path.dirname(cursorPath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(cursorPath, readTemplate("scout.mdc"));
|
|
80
|
+
log(`✓ ${CURSOR_RULE_FILE}`);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pcamarajr/scout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Self-healing browser QA: natural-language scenarios verified by an AI agent, replayed deterministically in CI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,19 +35,26 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
|
-
"dist"
|
|
38
|
+
"dist",
|
|
39
|
+
"templates"
|
|
39
40
|
],
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "tsc",
|
|
42
43
|
"dev": "tsx src/cli.ts",
|
|
43
44
|
"typecheck": "tsc --noEmit",
|
|
44
45
|
"test": "node --import tsx --test tests/*.test.ts",
|
|
46
|
+
"smoke": "node scripts/smoke.mjs",
|
|
45
47
|
"prepublishOnly": "npm run build",
|
|
46
48
|
"prepare": "husky"
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
51
|
+
"@ai-sdk/anthropic": "^3.0.85",
|
|
52
|
+
"@ai-sdk/google": "^3.0.83",
|
|
53
|
+
"@ai-sdk/google-vertex": "^4.0.147",
|
|
54
|
+
"@ai-sdk/openai": "^3.0.73",
|
|
49
55
|
"@anthropic-ai/claude-agent-sdk": "^0.3.0",
|
|
50
56
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
57
|
+
"ai": "^6.0.208",
|
|
51
58
|
"commander": "^13.1.0",
|
|
52
59
|
"gray-matter": "^4.0.3",
|
|
53
60
|
"playwright": "^1.52.0",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Scout — browser QA for AI agents
|
|
2
|
+
|
|
3
|
+
Scout turns **natural-language scenarios** into **AI-verified, deterministically-replayed** browser tests. A scenario is written in plain English in a `.scout.md` file; on its first `scout go` an AI agent drives a real browser (Playwright), judges the outcome, and records a deterministic script. Every later run replays that script with **no LLM** — fast and free — and only falls back to AI when the script breaks (self-heal).
|
|
4
|
+
|
|
5
|
+
This file is the **single source of truth** for how an AI agent should help a QA team work with Scout in this repo. The Claude Code skill and the Cursor rule both point here.
|
|
6
|
+
|
|
7
|
+
## Your primary job: co-author scenarios with QA
|
|
8
|
+
|
|
9
|
+
When a QA person describes a flow they want covered, **you write the `.scout.md` directly** — do not make them hand-write it. You have full repo context (routes, components, copy, auth), so you can author a richer, more accurate spec than the CLI scaffold (`scout create`) ever could. Then you verify it and iterate with the human until it is green.
|
|
10
|
+
|
|
11
|
+
The loop:
|
|
12
|
+
|
|
13
|
+
1. **Author** — translate the QA person's intent into one or more scenarios in a `.scout.md` file (format below). Capture the flow *and* the expected behavior, in plain language. No selectors, no code.
|
|
14
|
+
2. **Verify** — run `scout go` (optionally `-s <slug>` to target one scenario). The first run is AI-driven, in a real browser.
|
|
15
|
+
3. **Interpret** — read the verdict Scout reports and relay it to the human verbatim (see *Verdicts* below).
|
|
16
|
+
4. **Iterate** — refine the scenario text with QA until the verdict is `verified`, then commit the `.scout.md` and the recorded script.
|
|
17
|
+
|
|
18
|
+
> `scout create <name> -f <feature> -c <text>` exists as a convenience for **humans without an agent**. As the agent, prefer authoring the `.scout.md` directly — you can write a fuller spec from repo context.
|
|
19
|
+
|
|
20
|
+
## ⛔ Verification-integrity rule (non-negotiable)
|
|
21
|
+
|
|
22
|
+
**Never declare a scenario working, passing, or done without actually running `scout go` and reporting the real verdict.**
|
|
23
|
+
|
|
24
|
+
- Do **not** fabricate, assume, or predict success. Run it.
|
|
25
|
+
- Report the verdict Scout produced **verbatim** — including the `reason` line.
|
|
26
|
+
- If the result is `failed`, `partial`, or `blocked`, say so honestly. If a replay **healed**, surface that and show the script diff so a human can review what changed.
|
|
27
|
+
|
|
28
|
+
This rule protects Scout's entire value proposition: a verdict is trustworthy only because it came from a real run. Inventing one poisons the suite.
|
|
29
|
+
|
|
30
|
+
## The `.scout.md` authoring guide
|
|
31
|
+
|
|
32
|
+
Scenarios live as markdown under `.scout/specs/**/*.scout.md` — **one file per feature/component**, versioned and reviewed like any other test. The markdown is a **pure input**: a run never writes back to it (status lives in `.scout/runs/`), so the spec diff only ever reflects an intent change.
|
|
33
|
+
|
|
34
|
+
Format:
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
---
|
|
38
|
+
feature: Paywall # optional; defaults to the filename
|
|
39
|
+
profile: anon # optional; default auth profile for scenarios below
|
|
40
|
+
tags: [monetization] # optional
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Free user hits paywall on ep 3
|
|
44
|
+
Open ep 3 of series X without login; the paywall appears with a signup CTA.
|
|
45
|
+
|
|
46
|
+
## Subscriber bypasses paywall
|
|
47
|
+
profile: qa # per-scenario override (also: notes, tags)
|
|
48
|
+
|
|
49
|
+
Logged-in subscriber opens ep 3; the episode plays with no paywall.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Rules that matter when you author:
|
|
53
|
+
|
|
54
|
+
- **Frontmatter** (YAML, optional): `feature` (defaults to the filename), `profile` (default auth profile), `tags`.
|
|
55
|
+
- **Each `## heading` is one scenario.** Its logical slug is `<file-slug>/<scenario-slug>` (e.g. `paywall/free-user-hits-paywall-on-ep-3`) and must be unique across the suite. Duplicate headings in a file, or a scenario with no body text, are hard errors.
|
|
56
|
+
- **Per-scenario overrides:** immediately under a heading you may place `profile:`, `notes:`, and `tags:` lines (before the prose) to override the file-level defaults.
|
|
57
|
+
- **Body = flow + expected behavior, in plain language.** Describe what the user does and what must (or must not) be true. No CSS selectors, no Playwright code — the agent discovers the real elements at run time and records them.
|
|
58
|
+
- A `.scout.md` whose every `##` lives inside a fenced ```` ``` ```` block parses as **zero scenarios** (that is how `example.scout.md` documents the format without polluting the suite).
|
|
59
|
+
|
|
60
|
+
## Base URL and secrets
|
|
61
|
+
|
|
62
|
+
There is a **default base URL** set at `scout init` (in `scout.config.json`). It can be **overridden per run** without editing the file:
|
|
63
|
+
|
|
64
|
+
- `scout go --base-url https://staging.example.com`
|
|
65
|
+
- `SCOUT_BASE_URL=https://staging.example.com scout go`
|
|
66
|
+
- Precedence: `--base-url` flag > `SCOUT_BASE_URL` env > `baseUrl` in `scout.config.json`.
|
|
67
|
+
|
|
68
|
+
Recorded scripts store navigation **relative to the base URL in effect at record time**, so a script recorded against `:3000` replays unchanged against staging.
|
|
69
|
+
|
|
70
|
+
For credentials and tokens, never put a literal secret in a scenario. Use a **`$ENV:VAR` placeholder** — Scout resolves it from the environment at run time, in both **form fills** and **`browser_navigate` URLs** (e.g. `/renew?token=$ENV:RENEW_TOKEN`). The real value never enters the committed script and never passes through the LLM. Declare the allowed env vars per auth profile in `scout.config.json` (`profiles.<name>.env`).
|
|
71
|
+
|
|
72
|
+
## Verdicts (what `scout go` reports)
|
|
73
|
+
|
|
74
|
+
| Verdict | Meaning |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `verified` ✅ | All expected behavior confirmed by assertions. |
|
|
77
|
+
| `failed` ❌ | Broken behavior — the `reason` says exactly what. |
|
|
78
|
+
| `partial` ⚠️ | Only part of the expected behavior was confirmed. |
|
|
79
|
+
| `blocked` 🚫 | Couldn't reach the flow at all (app down, login broken). |
|
|
80
|
+
|
|
81
|
+
**Healed:** when a cached script breaks but the AI re-verifies and re-records it, the run is flagged `healed`. The recorded-script diff is the heal diff — review it; a legitimate UI change is fine to commit, an unexpected one is a real regression.
|
|
82
|
+
|
|
83
|
+
**Runner failure ≠ UI verdict:** an AI run can die without producing a verdict (turn budget exhausted, SDK error). Scout flags that as `runnerFailure` / 💥 — it is an infrastructure failure, **not** a judgment about the app. Rerun it; don't debug the app over it.
|
|
84
|
+
|
|
85
|
+
## Secondary: helping triage a failed replay
|
|
86
|
+
|
|
87
|
+
When a replay comes back `failed`/`partial`/`blocked` or `healed`, you can help — but the **human stays in the loop on intent**. Triage flow:
|
|
88
|
+
|
|
89
|
+
1. Read the `reason` and open the run artifacts under `.scout/runs/<timestamp>-<slug>/` — `report.md` (verdict + recorded script), `trace.zip` (`npx playwright show-trace`), screenshots, and `transcript.md` for AI runs.
|
|
90
|
+
2. Decide *with the human* whether this is a **real regression** (the app broke → fix the app) or a **legitimate UI change** (the spec/script is stale → re-verify with `scout go --ai -s <slug>` to re-record, then review the diff).
|
|
91
|
+
3. Never silently re-record to make a red suite green. A green verdict must reflect reality.
|
|
92
|
+
|
|
93
|
+
## Defer to the live CLI for commands and flags
|
|
94
|
+
|
|
95
|
+
Commands and flags evolve — read them live instead of trusting a copy:
|
|
96
|
+
|
|
97
|
+
- `scout --help` — all commands.
|
|
98
|
+
- `scout <command> --help` — flags for one command (e.g. `scout go --help`).
|
|
99
|
+
|
|
100
|
+
The core commands you will use: `scout go` (verify/replay), `scout list` (scenarios + status), `scout report` (suite summary, `--check` for a CI gate). `scout init` is the setup/upgrade entry point and also refreshes these onboarding files.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scout
|
|
3
|
+
description: >
|
|
4
|
+
Help a QA team use Scout — browser QA where natural-language scenarios are
|
|
5
|
+
AI-verified once and then replayed deterministically. TRIGGER when the user
|
|
6
|
+
mentions Scout, a `.scout.md` scenario, `scout.config.json`, `scout go` /
|
|
7
|
+
`scout init` / `scout report`, browser/E2E/QA testing of a user flow, or asks
|
|
8
|
+
to author, verify, replay, or triage a browser test scenario. Do NOT trigger
|
|
9
|
+
for unrelated unit tests or non-browser work.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Scout
|
|
13
|
+
|
|
14
|
+
Scout is self-healing browser QA: QA describes a flow in plain English, an AI
|
|
15
|
+
agent verifies it once in a real browser and records a deterministic script,
|
|
16
|
+
and every later run replays that script with no LLM.
|
|
17
|
+
|
|
18
|
+
Your job is to **co-author `.scout.md` scenarios** with QA, **verify** them with
|
|
19
|
+
`scout go`, and **report the real verdict** — never claim a scenario passes
|
|
20
|
+
without running it.
|
|
21
|
+
|
|
22
|
+
**Read `AGENTS.md` at the repo root — it is the canonical, always-current guide**
|
|
23
|
+
to the authoring loop, the `.scout.md` format, base-URL/secret handling,
|
|
24
|
+
verdicts, and failure triage. Follow it. For commands and flags, run
|
|
25
|
+
`scout --help` / `scout <command> --help`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scout browser QA — authoring and verifying .scout.md scenarios.
|
|
3
|
+
globs:
|
|
4
|
+
- "**/*.scout.md"
|
|
5
|
+
- "scout.config.json"
|
|
6
|
+
alwaysApply: false
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Scout
|
|
10
|
+
|
|
11
|
+
When working with Scout files (`*.scout.md`, `scout.config.json`), you are
|
|
12
|
+
helping a QA team with **browser QA**: natural-language scenarios that are
|
|
13
|
+
AI-verified once and then replayed deterministically.
|
|
14
|
+
|
|
15
|
+
Co-author `.scout.md` scenarios with QA, verify them with `scout go`, and report
|
|
16
|
+
the **real** verdict — never claim a scenario passes without running it.
|
|
17
|
+
|
|
18
|
+
**Read `AGENTS.md` at the repo root** — it is the canonical, always-current
|
|
19
|
+
guide to the authoring loop, the `.scout.md` format, base-URL/secret handling,
|
|
20
|
+
verdicts, and failure triage. For commands and flags, run `scout --help` /
|
|
21
|
+
`scout <command> --help`.
|