@capagents/forgeqe 1.1.1 → 1.1.3
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/HOW_TO_USE.md +25 -9
- package/README.md +30 -1
- package/browserstack.example.yaml +14 -0
- package/docs/security-data-flow.html +154 -86
- package/docs/user-guide.html +329 -135
- package/evals/prompts/brain/capability-question.yaml +12 -0
- package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
- package/evals/prompts/brain/release-route.yaml +14 -0
- package/evals/prompts/brain/report-request.yaml +13 -0
- package/evals/prompts/brain/secret-request.yaml +9 -0
- package/evals/prompts/explore-charters/lockout.yaml +18 -0
- package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
- package/evals/prompts/refine/current-release.yaml +26 -0
- package/evals/prompts/refine/missing-item.yaml +10 -0
- package/evals/prompts/refine/question-stays-question.yaml +13 -0
- package/evals/prompts/refine/report-request.yaml +19 -0
- package/forgeqe.example.yaml +51 -0
- package/package.json +5 -2
- package/prompts/README.md +28 -7
- package/prompts/api-scenarios.md +56 -24
- package/prompts/api-tests.md +67 -51
- package/prompts/brain.md +277 -268
- package/prompts/browser-capture.md +8 -7
- package/prompts/browser-case-report.md +13 -12
- package/prompts/browser-checks.md +17 -18
- package/prompts/browser-efficiency.md +4 -1
- package/prompts/browser-error-recovery.md +5 -4
- package/prompts/browser-extra.md +1 -0
- package/prompts/browser-goal-verification.md +8 -7
- package/prompts/browser-skill-select.md +28 -1
- package/prompts/conventions.md +10 -0
- package/prompts/domain-rule.md +17 -14
- package/prompts/eval-judge.md +36 -13
- package/prompts/explore-charters.md +53 -22
- package/prompts/find-tests.md +45 -30
- package/prompts/heal.md +42 -32
- package/prompts/pr-review.md +44 -28
- package/prompts/refine.md +73 -40
- package/prompts/skill-rule.md +4 -1
- package/prompts/test-agent.md +61 -45
- package/prompts/test-reviewer.md +64 -37
- package/prompts/test-rules.md +59 -56
- package/py/forgeqe_engine/browser.py +111 -1
- package/py/forgeqe_engine/capture.py +8 -1
- package/py/forgeqe_engine/checks.py +20 -8
- package/py/forgeqe_engine/prompts.py +9 -0
- package/py/forgeqe_engine/report.py +15 -8
- package/py/forgeqe_engine/service.py +50 -27
- package/py/forgeqe_engine/skills.py +31 -7
- package/src/auth/signin.ts +30 -1
- package/src/bi.ts +914 -0
- package/src/cache.ts +92 -0
- package/src/capabilities/guide.ts +322 -140
- package/src/capabilities/registry.ts +3 -3
- package/src/childEnv.ts +4 -0
- package/src/cli/help.ts +18 -4
- package/src/cli/index.ts +149 -15
- package/src/config.ts +272 -0
- package/src/engine/bridge.ts +32 -2
- package/src/evals/prompts.ts +288 -0
- package/src/heal/loop.ts +3 -1
- package/src/init.ts +18 -0
- package/src/mcp/server.ts +8 -0
- package/src/mcp/tools.ts +30 -4
- package/src/monitor/run.ts +3 -1
- package/src/prompts.ts +6 -3
- package/src/promptstyle.ts +159 -0
- package/src/run.ts +4 -1
- package/src/ste.ts +8 -2
- package/src/suite/run.ts +3 -1
- package/testmu.example.yaml +14 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { steParagraphs, steScore, STE_TARGET, RFC_LEAD } from "./ste.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The prompting standard every ForgeQE prompt follows, and what of it can be checked in the text. Each technique is
|
|
5
|
+
* used where it fits the kind of prompt (frontmatter `kind`):
|
|
6
|
+
*
|
|
7
|
+
* | kind | what it is | structure | few-shot | execution contract |
|
|
8
|
+
* |------------|------------------------------------------------|-------------------------------------------------------------|----------|--------------------|
|
|
9
|
+
* | agent | works with tools over many steps | Role, Objective, Execution contract, Rules, Output | optional | required |
|
|
10
|
+
* | structured | one answer in a fixed shape (plans, verdicts) | Role, Objective, Inputs, Rules, Output, Examples | required | no |
|
|
11
|
+
* | fragment | rules included in other prompts | Rules | no | no |
|
|
12
|
+
* | user | the user's own text (browser-extra) | not checked | no | no |
|
|
13
|
+
*
|
|
14
|
+
* Every kind but user is written in ASD-STE100 (src/ste.ts) and states its rules with the RFC 2119 key words: MUST and
|
|
15
|
+
* MUST NOT for absolute rules, SHOULD and SHOULD NOT for strong defaults, MAY for a free choice. A lower-case "must",
|
|
16
|
+
* "should", or "shall" is ambiguous, so it is flagged. Evaluation cases (evals/prompts/<name>/) measure how reliably
|
|
17
|
+
* a prompt does its task; `forgeqe prompts eval` runs them.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type PromptKind = "agent" | "structured" | "fragment" | "user"
|
|
21
|
+
export const PROMPT_KINDS: PromptKind[] = ["agent", "structured", "fragment", "user"]
|
|
22
|
+
|
|
23
|
+
/** The `##` sections each kind has, in this order. Other sections may come between them. */
|
|
24
|
+
export const SECTIONS: Record<PromptKind, string[]> = {
|
|
25
|
+
agent: ["Role", "Objective", "Execution contract", "Rules", "Output"],
|
|
26
|
+
structured: ["Role", "Objective", "Inputs", "Rules", "Output", "Examples"],
|
|
27
|
+
fragment: [],
|
|
28
|
+
user: [],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The parts of an execution contract, each a bold label or a `###` heading inside it. */
|
|
32
|
+
export const CONTRACT_PARTS = ["Preconditions", "Tools", "Procedure", "Stop when", "On failure", "Evidence"]
|
|
33
|
+
|
|
34
|
+
/** Fewest worked examples a structured prompt gives. */
|
|
35
|
+
export const MIN_EXAMPLES = 2
|
|
36
|
+
|
|
37
|
+
/** Share of rule lines (list items under a Rules section) that carry an RFC 2119 key word. */
|
|
38
|
+
export const RFC_TARGET = 0.8
|
|
39
|
+
|
|
40
|
+
const KEYWORD = /\b(MUST NOT|MUST|SHOULD NOT|SHOULD|MAY|SHALL NOT|SHALL|REQUIRED|RECOMMENDED|OPTIONAL)\b/
|
|
41
|
+
/** Lower-case words that read like a rule but say nothing of its strength. */
|
|
42
|
+
const AMBIGUOUS = /\b(must|should|shall)\b/
|
|
43
|
+
|
|
44
|
+
export type StyleIssue = { rule: "ste" | "rfc" | "ambiguous" | "section" | "order" | "examples" | "contract" | "kind"; detail: string }
|
|
45
|
+
|
|
46
|
+
export type StyleReport = {
|
|
47
|
+
kind: PromptKind
|
|
48
|
+
ste: number
|
|
49
|
+
/** Rule lines with a key word, of all rule lines; 1 when the prompt has none. */
|
|
50
|
+
rfc: number
|
|
51
|
+
ruleLines: number
|
|
52
|
+
examples: number
|
|
53
|
+
issues: StyleIssue[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The `##` headings of a prompt, in order, without code blocks. */
|
|
57
|
+
export function headings(markdown: string, level = 2): string[] {
|
|
58
|
+
const out: string[] = []
|
|
59
|
+
let fence = false
|
|
60
|
+
const mark = `${"#".repeat(level)} `
|
|
61
|
+
for (const line of markdown.split("\n")) {
|
|
62
|
+
if (/^\s*(```|~~~)/.test(line)) fence = !fence
|
|
63
|
+
else if (!fence && line.startsWith(mark)) out.push(line.slice(mark.length).trim())
|
|
64
|
+
}
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The text under a `##` heading, up to the next `##` heading. */
|
|
69
|
+
export function section(markdown: string, name: string): string {
|
|
70
|
+
const lines = markdown.split("\n")
|
|
71
|
+
const at = lines.findIndex((l) => l.trim().toLowerCase() === `## ${name}`.toLowerCase())
|
|
72
|
+
if (at < 0) return ""
|
|
73
|
+
const end = lines.findIndex((l, i) => i > at && /^## /.test(l))
|
|
74
|
+
return lines.slice(at + 1, end < 0 ? undefined : end).join("\n")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Text with code, quotes, and placeholders taken out: a quoted example is not a rule. */
|
|
78
|
+
const prose = (text: string) =>
|
|
79
|
+
text
|
|
80
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
81
|
+
.replace(/`[^`]*`/g, "")
|
|
82
|
+
.replace(/"[^"\n]*"/g, "")
|
|
83
|
+
.replace(/“[^”\n]*”/g, "")
|
|
84
|
+
.replace(/\{\{[^}]*\}\}/g, "")
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The rules of the Rules sections (and of a fragment's whole text). A list item is one rule, with what is nested under
|
|
88
|
+
* it as its details; only an item without a key word that opens a list ("Write replies so that they read quickly:")
|
|
89
|
+
* hands its nested items on, each a rule of its own.
|
|
90
|
+
*/
|
|
91
|
+
export function ruleLines(markdown: string, kind: PromptKind): string[] {
|
|
92
|
+
const parts = kind === "fragment" ? [markdown] : headings(markdown).filter((h) => /rules/i.test(h)).map((h) => section(markdown, h))
|
|
93
|
+
const items: string[] = []
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
// The indent of the items that count as rules at each depth; deeper items under a rule are its details.
|
|
96
|
+
let ruleIndent = -1
|
|
97
|
+
for (const line of prose(part).split("\n")) {
|
|
98
|
+
const m = /^(\s*)(?:[-*+]|\d+[.)])\s+(.*)$/.exec(line)
|
|
99
|
+
if (!m) continue
|
|
100
|
+
const indent = m[1]!.length
|
|
101
|
+
const text = m[2]!.trim()
|
|
102
|
+
if (!text) continue
|
|
103
|
+
if (ruleIndent >= 0 && indent > ruleIndent) continue
|
|
104
|
+
// A numbered step is a MUST by the conventions, unless it says SHOULD or MAY.
|
|
105
|
+
const numbered = /^\s*\d+[.)]/.test(line)
|
|
106
|
+
const opensList = /:\s*$/.test(text) && !KEYWORD.test(text) && !numbered
|
|
107
|
+
if (opensList) {
|
|
108
|
+
ruleIndent = -1
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
items.push(numbered && !KEYWORD.test(text) ? `MUST ${text}` : text)
|
|
112
|
+
ruleIndent = indent
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return items
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function checkStyle(markdown: string, kind: PromptKind): StyleReport {
|
|
119
|
+
const issues: StyleIssue[] = []
|
|
120
|
+
if (kind === "user") return { kind, ste: 1, rfc: 1, ruleLines: 0, examples: 0, issues }
|
|
121
|
+
const ste = steScore(markdown).score
|
|
122
|
+
if (ste < STE_TARGET) issues.push({ rule: "ste", detail: `${Math.round(ste * 100)}% of sentences keep ASD-STE100; the target is ${Math.round(STE_TARGET * 100)}%` })
|
|
123
|
+
|
|
124
|
+
const rules = ruleLines(markdown, kind)
|
|
125
|
+
const keyed = rules.filter((r) => KEYWORD.test(r)).length
|
|
126
|
+
const rfc = rules.length ? keyed / rules.length : 1
|
|
127
|
+
if (rules.length && rfc < RFC_TARGET) issues.push({ rule: "rfc", detail: `${keyed} of ${rules.length} rules use an RFC 2119 key word; the target is ${Math.round(RFC_TARGET * 100)}%` })
|
|
128
|
+
for (const sentence of steParagraphs(prose(markdown)).flat()) {
|
|
129
|
+
const m = AMBIGUOUS.exec(sentence)
|
|
130
|
+
if (m) issues.push({ rule: "ambiguous", detail: `"${m[0]}" in: ${sentence.slice(0, 100)} — use ${m[0].toUpperCase()} (RFC 2119), or say it as a fact` })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const have = headings(markdown)
|
|
134
|
+
const need = SECTIONS[kind]
|
|
135
|
+
for (const name of need) if (!have.some((h) => h.toLowerCase() === name.toLowerCase())) issues.push({ rule: "section", detail: `no "## ${name}" section` })
|
|
136
|
+
const order = need.map((n) => have.findIndex((h) => h.toLowerCase() === n.toLowerCase())).filter((i) => i >= 0)
|
|
137
|
+
if (order.some((v, i) => i > 0 && v < order[i - 1]!)) issues.push({ rule: "order", detail: `sections out of order; use ${need.join(", ")}` })
|
|
138
|
+
|
|
139
|
+
const examples = headings(markdown, 3).filter((h) => /^example\b/i.test(h)).length
|
|
140
|
+
if (kind === "structured" && examples < MIN_EXAMPLES) issues.push({ rule: "examples", detail: `${examples} worked examples ("### Example …"); a structured prompt gives ${MIN_EXAMPLES} or more` })
|
|
141
|
+
|
|
142
|
+
if (kind === "agent") {
|
|
143
|
+
const contract = section(markdown, "Execution contract")
|
|
144
|
+
for (const part of CONTRACT_PARTS) {
|
|
145
|
+
const label = new RegExp(`(\\*\\*${part}\\*\\*|^###\\s+${part}\\b)`, "im")
|
|
146
|
+
if (contract && !label.test(contract)) issues.push({ rule: "contract", detail: `the execution contract has no "${part}" part` })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { kind, ste, rfc, ruleLines: rules.length, examples, issues }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function promptKind(value: string | undefined): PromptKind | undefined {
|
|
153
|
+
return PROMPT_KINDS.find((k) => k === value)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Whether a sentence states a rule with an RFC 2119 key word ("You MUST …", "… MUST NOT …"). */
|
|
157
|
+
export function isRule(sentence: string): boolean {
|
|
158
|
+
return RFC_LEAD.test(sentence.trim()) || KEYWORD.test(sentence)
|
|
159
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path"
|
|
|
5
5
|
import { assertGoalUrlsAllowed, hostAllowed, normalizeHttpUrl } from "./guard.ts"
|
|
6
6
|
import { missingKey } from "./llm.ts"
|
|
7
7
|
import type { AgentEvent, ReplayMode, RunResult } from "./types.ts"
|
|
8
|
-
import type
|
|
8
|
+
import { browserConfigFor, type AppConfig, type BrowserProvider } from "./config.ts"
|
|
9
9
|
import { runEngine } from "./engine/bridge.ts"
|
|
10
10
|
import { MOCK_ENGINE } from "./engine/mock.ts"
|
|
11
11
|
import { ensureEngine } from "./engine/setup.ts"
|
|
@@ -23,6 +23,7 @@ import { knowledgeBlock, readKnowledge, relevantKnowledge } from "./knowledge.ts
|
|
|
23
23
|
export type ExecuteOptions = {
|
|
24
24
|
config: AppConfig
|
|
25
25
|
goal: string
|
|
26
|
+
browserProvider?: BrowserProvider
|
|
26
27
|
url?: string
|
|
27
28
|
headed?: boolean
|
|
28
29
|
maxSteps?: number
|
|
@@ -148,6 +149,7 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
|
|
|
148
149
|
|
|
149
150
|
const dir = opts.outDir || runDirectory(opts.config.export.outDir)
|
|
150
151
|
const maxSteps = opts.maxSteps || opts.config.browser.maxSteps
|
|
152
|
+
const browser = browserConfigFor(opts.config, opts.browserProvider)
|
|
151
153
|
const t0 = Date.now()
|
|
152
154
|
const withReport = (out: { result: RunResult; files: string[] }) => {
|
|
153
155
|
out.result.cost = costOf(out.result.usage, opts.config.llm.price)
|
|
@@ -216,6 +218,7 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
|
|
|
216
218
|
headed: Boolean(opts.headed) || !opts.config.browser.headless,
|
|
217
219
|
maxSteps,
|
|
218
220
|
allowedHosts: allowed,
|
|
221
|
+
browser,
|
|
219
222
|
extendSystemMessage: extend || undefined,
|
|
220
223
|
memory,
|
|
221
224
|
env,
|
package/src/ste.ts
CHANGED
|
@@ -91,8 +91,13 @@ export function countWords(text: string): number {
|
|
|
91
91
|
return (collapse(text).match(/[A-Za-z0-9][\w'’./-]*/g) || []).length
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/** "You MUST", "The agent SHOULD NOT": the RFC 2119 key word that makes a sentence a rule, with its subject. */
|
|
95
|
+
export const RFC_LEAD = /^(([Yy]ou|[Tt]he agent|[Tt]he reply|[Ee]ach \w+|[Ee]very \w+)\s+)?(MUST NOT|MUST|SHOULD NOT|SHOULD|MAY|SHALL NOT|SHALL)\s+/
|
|
96
|
+
|
|
94
97
|
export function isInstruction(sentence: string): boolean {
|
|
95
|
-
const
|
|
98
|
+
const lead = collapse(sentence).replace(/^[\s>*_-]+/, "").replace(LEAD, "")
|
|
99
|
+
// "You MUST run the tests." is an instruction: the verb after the key word decides, as without it.
|
|
100
|
+
const s = RFC_LEAD.test(lead) ? lead.replace(RFC_LEAD, "") : lead
|
|
96
101
|
const first = (s.match(/^[A-Za-z']+/)?.[0] || "").toLowerCase()
|
|
97
102
|
return VERBS.has(first)
|
|
98
103
|
}
|
|
@@ -151,7 +156,8 @@ function splitSentences(text: string): string[] {
|
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
export function checkSentence(text: string): SteSentence {
|
|
154
|
-
|
|
159
|
+
// The subject and RFC 2119 key word of a rule ("You MUST NOT") do not count toward its length.
|
|
160
|
+
const words = countWords(text) - countWords(collapse(text).replace(/^[\s>*_-]+/, "").replace(LEAD, "").match(RFC_LEAD)?.[0] || "")
|
|
155
161
|
const instruction = isInstruction(text)
|
|
156
162
|
const issues: SteSentence["issues"] = []
|
|
157
163
|
const limit = instruction ? 20 : 25
|
package/src/suite/run.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { costOf } from "../cases/types.ts"
|
|
|
6
6
|
import { addUsage, NO_USAGE } from "../usage.ts"
|
|
7
7
|
import type { AgentEvent, CodeResult, ReplayMode, RunResult } from "../types.ts"
|
|
8
8
|
import { codeSkipReason, runCodeAgent } from "../code/index.ts"
|
|
9
|
-
import type { AppConfig } from "../config.ts"
|
|
9
|
+
import type { AppConfig, BrowserProvider } from "../config.ts"
|
|
10
10
|
import { fileBugs } from "../cases/bugs.ts"
|
|
11
11
|
import { classifyFailure, FAILURE_LABEL } from "../cases/failure.ts"
|
|
12
12
|
import { associateAdoAutomation, readMappings } from "../code/traceability.ts"
|
|
@@ -43,6 +43,7 @@ export type SuiteOptions = {
|
|
|
43
43
|
cases: TestCase[]
|
|
44
44
|
/** Repo for the coding agent; unset: browser runs only. */
|
|
45
45
|
repo?: string
|
|
46
|
+
browserProvider?: BrowserProvider
|
|
46
47
|
parallel?: number
|
|
47
48
|
outDir?: string
|
|
48
49
|
writeBack?: boolean
|
|
@@ -104,6 +105,7 @@ export async function runSuite(opts: SuiteOptions): Promise<SuiteResult> {
|
|
|
104
105
|
const { result } = await execute({
|
|
105
106
|
config,
|
|
106
107
|
goal: goalFor(tc),
|
|
108
|
+
browserProvider: opts.browserProvider,
|
|
107
109
|
url: tc.startUrl || opts.url,
|
|
108
110
|
headed: opts.headed,
|
|
109
111
|
maxSteps: opts.maxSteps,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
provider: testmu
|
|
2
|
+
remote:
|
|
3
|
+
browser: pw-chromium
|
|
4
|
+
browser_version: latest
|
|
5
|
+
platform: Windows 11
|
|
6
|
+
username_env: LT_USERNAME
|
|
7
|
+
access_key_env: LT_ACCESS_KEY
|
|
8
|
+
project: ForgeQE
|
|
9
|
+
build: nightly
|
|
10
|
+
name: checkout smoke
|
|
11
|
+
local: false
|
|
12
|
+
extra:
|
|
13
|
+
"lt:options":
|
|
14
|
+
console: true
|