@capagents/forgeqe 1.1.0 → 1.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/HOW_TO_USE.md +30 -13
- package/README.md +27 -1
- package/docs/user-guide.html +33 -13
- 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 +39 -0
- package/package.json +1 -1
- package/prompts/README.md +28 -7
- package/prompts/api-scenarios.md +56 -24
- package/prompts/api-tests.md +67 -51
- package/prompts/brain.md +283 -266
- 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 -36
- 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 -28
- package/py/forgeqe_engine/skills.py +31 -7
- package/src/auth/signin.ts +30 -1
- package/src/bi.ts +899 -0
- package/src/childEnv.ts +4 -0
- package/src/cli/help.ts +10 -2
- package/src/cli/index.ts +114 -10
- package/src/config.ts +149 -0
- package/src/engine/bridge.ts +32 -2
- package/src/evals/prompts.ts +288 -0
- package/src/mcp/server.ts +19 -1
- package/src/mcp/tools.ts +27 -0
- package/src/prompts.ts +6 -3
- package/src/promptstyle.ts +159 -0
- package/src/refine.ts +30 -14
- package/src/run.ts +1 -0
- package/src/ste.ts +8 -2
- package/src/tui/App.tsx +16 -2
- package/src/tui/refine-panel.tsx +9 -5
package/src/refine.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { CAPABILITIES } from "./capabilities/registry.ts"
|
|
|
10
10
|
import { allStatuses } from "./capabilities/status.ts"
|
|
11
11
|
import { domainNotes } from "./domain.ts"
|
|
12
12
|
import type { CodeEvent } from "./types.ts"
|
|
13
|
+
import { steScore } from "./ste.ts"
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* The shell's Refine (ctrl+r): what the user typed becomes a precise request for ForgeQE's agent, checked against what
|
|
@@ -20,13 +21,18 @@ import type { CodeEvent } from "./types.ts"
|
|
|
20
21
|
/** The only tools the refiner may call: each reads, none changes anything or starts a run. */
|
|
21
22
|
export const REFINE_TOOLS = ["capabilities", "query_api", "find_test_cases", "list_sprints", "run_details"]
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The refined request as a task specification, laid out as a system prompt: the role the agent takes, the objective,
|
|
26
|
+
* the context, the steps, the rules, the output format, and the objective again in one sentence at the end.
|
|
27
|
+
*/
|
|
28
|
+
export type RefinedSpec = { role: string; objective: string; context: string[]; steps: string[]; rules: string[]; output: string; summary: string }
|
|
25
29
|
|
|
26
30
|
export type Refined = {
|
|
27
|
-
/** The request that is sent: the spec written out (
|
|
31
|
+
/** The request that is sent: the spec written out (Role, Objective, Context, Steps, Rules, Output format, Summary). */
|
|
28
32
|
refined: string
|
|
29
33
|
spec?: RefinedSpec
|
|
34
|
+
/** The share of the request's sentences that keep the ASD-STE100 rules, 0 to 1. */
|
|
35
|
+
ste?: number
|
|
30
36
|
grounded: { fact: string; checked: boolean }[]
|
|
31
37
|
assumed: string[]
|
|
32
38
|
missing: string[]
|
|
@@ -37,11 +43,13 @@ const lines = { type: "array", items: { type: "string" } } as const
|
|
|
37
43
|
export const REFINE_SCHEMA = {
|
|
38
44
|
type: "object",
|
|
39
45
|
properties: {
|
|
40
|
-
|
|
46
|
+
role: { type: "string", description: "The role the agent takes for this task, as one description, e.g. A release analyst who reads Azure DevOps deployments" },
|
|
47
|
+
objective: { type: "string", description: "One instruction that starts with a verb: what the agent must do" },
|
|
41
48
|
context: { ...lines, description: "Facts the agent needs, one per line: system, project, work items, files, runs, with exact names and ids" },
|
|
42
49
|
steps: { ...lines, description: "The actions in sequence, one per step; empty for a request with one action" },
|
|
43
50
|
rules: { ...lines, description: "Limits the user gave or the setup sets, one per line" },
|
|
44
|
-
|
|
51
|
+
output: { type: "string", description: "The output format: what the agent gives back and how it is laid out (sections, a table, a file)" },
|
|
52
|
+
summary: { type: "string", description: "The objective again, in one short sentence, as the last line" },
|
|
45
53
|
grounded: {
|
|
46
54
|
type: "array",
|
|
47
55
|
items: { type: "object", properties: { fact: { type: "string" }, checked: { type: "boolean" } }, required: ["fact", "checked"] },
|
|
@@ -49,7 +57,7 @@ export const REFINE_SCHEMA = {
|
|
|
49
57
|
assumed: { type: "array", items: { type: "string" } },
|
|
50
58
|
missing: { type: "array", items: { type: "string" } },
|
|
51
59
|
},
|
|
52
|
-
required: ["
|
|
60
|
+
required: ["role", "objective", "context", "steps", "rules", "output", "summary", "grounded", "assumed", "missing"],
|
|
53
61
|
} as const
|
|
54
62
|
|
|
55
63
|
const REFINE_AGENT = {
|
|
@@ -64,11 +72,14 @@ const REFINE_AGENT = {
|
|
|
64
72
|
|
|
65
73
|
/** The spec as the request ForgeQE's agent gets: labelled parts, empty ones left out. */
|
|
66
74
|
export function composeRefined(spec: RefinedSpec): string {
|
|
67
|
-
const parts = [
|
|
75
|
+
const parts: string[] = []
|
|
76
|
+
if (spec.role) parts.push(`Role: ${spec.role}`)
|
|
77
|
+
parts.push(`Objective: ${spec.objective}`)
|
|
68
78
|
if (spec.context.length) parts.push(["Context:", ...spec.context.map((c) => `- ${c}`)].join("\n"))
|
|
69
79
|
if (spec.steps.length) parts.push(["Steps:", ...spec.steps.map((st, i) => `${i + 1}. ${st}`)].join("\n"))
|
|
70
80
|
if (spec.rules.length) parts.push(["Rules:", ...spec.rules.map((r) => `- ${r}`)].join("\n"))
|
|
71
|
-
if (spec.
|
|
81
|
+
if (spec.output) parts.push(`Output format: ${spec.output}`)
|
|
82
|
+
if (spec.summary) parts.push(`Summary: ${spec.summary}`)
|
|
72
83
|
return parts.join("\n\n")
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -97,13 +108,18 @@ export function refineRequest(text: string, recent: { user: string; reply: strin
|
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
/** Keeps only what the schema allows, so the shell never shows a malformed answer. */
|
|
100
|
-
export function cleanRefined(r: (Partial<Refined> & Partial<RefinedSpec>) | null | undefined, original: string): Refined {
|
|
111
|
+
export function cleanRefined(r: (Partial<Refined> & Partial<RefinedSpec> & { goal?: string; result?: string }) | null | undefined, original: string): Refined {
|
|
101
112
|
const strings = (v: unknown) => (Array.isArray(v) ? v.map(String).map((s) => s.trim()).filter(Boolean) : [])
|
|
102
|
-
const
|
|
103
|
-
|
|
113
|
+
const text = (v: unknown) => String(v || "").trim()
|
|
114
|
+
// goal and result: the names an older refine prompt (a project's own copy) gives the objective and the output.
|
|
115
|
+
const objective = text(r?.objective) || text(r?.goal)
|
|
116
|
+
const spec: RefinedSpec | undefined = objective
|
|
117
|
+
? { role: text(r?.role), objective, context: strings(r?.context), steps: strings(r?.steps), rules: strings(r?.rules), output: text(r?.output) || text(r?.result), summary: text(r?.summary) }
|
|
118
|
+
: undefined
|
|
119
|
+
const refined = spec ? composeRefined(spec) : text(r?.refined) || original
|
|
104
120
|
return {
|
|
105
|
-
refined
|
|
106
|
-
...(spec ? { spec } : {}),
|
|
121
|
+
refined,
|
|
122
|
+
...(spec ? { spec, ste: steScore(refined).score } : {}),
|
|
107
123
|
grounded: Array.isArray(r?.grounded) ? r!.grounded.filter((g) => g && String(g.fact || "").trim()).map((g) => ({ fact: String(g.fact).trim(), checked: g.checked === true })) : [],
|
|
108
124
|
assumed: strings(r?.assumed),
|
|
109
125
|
missing: strings(r?.missing),
|
|
@@ -150,7 +166,7 @@ export async function refine(opts: {
|
|
|
150
166
|
})
|
|
151
167
|
try {
|
|
152
168
|
let out = await session.prompt(refineRequest(opts.text, opts.recent))
|
|
153
|
-
if (!out.report && !out.error && !opts.signal?.aborted) out = await session.prompt("Give the structured result:
|
|
169
|
+
if (!out.report && !out.error && !opts.signal?.aborted) out = await session.prompt("Give the structured result: role, objective, context, steps, rules, output, summary, grounded, assumed, and missing.")
|
|
154
170
|
if (!out.report) throw new Error(out.error ? `Refine failed: ${out.error}` : "Refine gave no result.")
|
|
155
171
|
return cleanRefined(out.report, opts.text)
|
|
156
172
|
} finally {
|
package/src/run.ts
CHANGED
|
@@ -216,6 +216,7 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
|
|
|
216
216
|
headed: Boolean(opts.headed) || !opts.config.browser.headless,
|
|
217
217
|
maxSteps,
|
|
218
218
|
allowedHosts: allowed,
|
|
219
|
+
browser: { provider: opts.config.browser.provider, remote: opts.config.browser.remote },
|
|
219
220
|
extendSystemMessage: extend || undefined,
|
|
220
221
|
memory,
|
|
221
222
|
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/tui/App.tsx
CHANGED
|
@@ -519,6 +519,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
/** Refines what is in the prompt; the user then decides what is sent. */
|
|
522
|
+
const refineCount = useRef(0)
|
|
522
523
|
const startRefine = () => {
|
|
523
524
|
const text = promptTextRef.current.trim()
|
|
524
525
|
const cfg = configRef.current
|
|
@@ -534,10 +535,19 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
534
535
|
const controller = new AbortController()
|
|
535
536
|
refineAbort.current = controller
|
|
536
537
|
setRefine({ phase: "working", original: text })
|
|
537
|
-
|
|
538
|
+
// Refine is a model session of its own: its tokens count in the session total, under a key per refine.
|
|
539
|
+
const key = `refine-${++refineCount.current}`
|
|
540
|
+
const started = Date.now()
|
|
541
|
+
let used = 0
|
|
542
|
+
const onEvent = (e: CodeEvent) => {
|
|
543
|
+
if (e.kind !== "usage") return
|
|
544
|
+
used = e.usage.input + e.usage.output
|
|
545
|
+
useTokens(key, used)
|
|
546
|
+
}
|
|
547
|
+
void (options.refine || refine)({ config: cfg, cwd: sessionSetup(cfg, options.repo).cwd, text, recent: convoRef.current, signal: controller.signal, onEvent })
|
|
538
548
|
.then((result) => {
|
|
539
549
|
if (controller.signal.aborted || refiningRef.current?.phase !== "working") return
|
|
540
|
-
setRefine({ phase: "done", original: text, result, choice: initialChoice(result.missing) })
|
|
550
|
+
setRefine({ phase: "done", original: text, result, choice: initialChoice(result.missing), ...(used ? { tokens: used } : {}), ms: Date.now() - started })
|
|
541
551
|
})
|
|
542
552
|
.catch((err) => {
|
|
543
553
|
if (controller.signal.aborted) return
|
|
@@ -914,6 +924,10 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
914
924
|
case "info":
|
|
915
925
|
push(e.ok === undefined ? "code" : e.ok ? "ok" : "err", e.text)
|
|
916
926
|
return
|
|
927
|
+
case "call":
|
|
928
|
+
pushTool(e.text, e.ok)
|
|
929
|
+
if (!aborted()) setActivity(`Agent: ${clip(e.text, 60)}`)
|
|
930
|
+
return
|
|
917
931
|
case "agent": {
|
|
918
932
|
const ev = e.event
|
|
919
933
|
inspect(ev)
|
package/src/tui/refine-panel.tsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Refined } from "../refine.ts"
|
|
2
2
|
import { Spinner } from "./components.tsx"
|
|
3
|
-
import { theme } from "./theme.ts"
|
|
3
|
+
import { formatDuration, formatTokens, theme } from "./theme.ts"
|
|
4
4
|
|
|
5
5
|
/** Refine's panel, in the prompt's place: working, the result with what was checked, or why it could not refine. */
|
|
6
6
|
export type RefineState =
|
|
7
7
|
| { phase: "working"; original: string }
|
|
8
|
-
|
|
8
|
+
/** `tokens` and `ms`: what Refine's own model session cost, shown in the title. */
|
|
9
|
+
| { phase: "done"; original: string; result: Refined; choice: number; tokens?: number; ms?: number }
|
|
9
10
|
| { phase: "error"; original: string; message: string }
|
|
10
11
|
|
|
11
12
|
export const REFINE_CHOICES = [
|
|
@@ -28,7 +29,10 @@ export function initialChoice(missing: string[]): number {
|
|
|
28
29
|
|
|
29
30
|
export function RefinePanel(props: { state: RefineState; width: number; onChoose: (id: RefineChoice) => void; onHover: (i: number) => void }) {
|
|
30
31
|
const { state, width } = props
|
|
31
|
-
const
|
|
32
|
+
const cost = state.phase === "done" && state.tokens ? ` · ${formatTokens(state.tokens)} tokens${state.ms ? ` · ${formatDuration(state.ms)}` : ""}` : ""
|
|
33
|
+
// How much of the request keeps the ASD-STE100 rules, as Refine's prompt asks.
|
|
34
|
+
const ste = state.phase === "done" && state.result.ste !== undefined ? ` · STE ${Math.round(state.result.ste * 100)}%` : ""
|
|
35
|
+
const title = state.phase === "working" ? " ✦ Refining " : state.phase === "error" ? " ✦ Could not refine " : ` ✦ Refined request${ste}${cost} `
|
|
32
36
|
// Facts Refine checked that are also context lines are marked there, not repeated under Checked.
|
|
33
37
|
const contextLines = state.phase === "done" ? state.result.refined.split("\n").filter((l) => l.startsWith("- ")).map((l) => norm(l.slice(2))) : []
|
|
34
38
|
const inContext = new Set(contextLines)
|
|
@@ -61,9 +65,9 @@ export function RefinePanel(props: { state: RefineState; width: number; onChoose
|
|
|
61
65
|
<text fg={theme.err}>{state.message}</text>
|
|
62
66
|
) : (
|
|
63
67
|
<>
|
|
64
|
-
{/* The request as it is sent: its labelled parts (
|
|
68
|
+
{/* The request as it is sent: its labelled parts (Role, Objective, Context, Steps, Rules, Output format, Summary) stand out. */}
|
|
65
69
|
{state.result.refined.split("\n").map((line, i) => {
|
|
66
|
-
const label = /^(
|
|
70
|
+
const label = /^(Role|Objective|Context|Steps|Rules|Output format|Summary|Goal|Result):/.exec(line)
|
|
67
71
|
// A context line Refine checked against the systems: ✓ in place of its dash, so it is not listed twice.
|
|
68
72
|
if (line.startsWith("- ") && checked.has(norm(line.slice(2)))) {
|
|
69
73
|
return (
|