@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.
Files changed (64) hide show
  1. package/HOW_TO_USE.md +30 -13
  2. package/README.md +27 -1
  3. package/docs/user-guide.html +33 -13
  4. package/evals/prompts/brain/capability-question.yaml +12 -0
  5. package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
  6. package/evals/prompts/brain/release-route.yaml +14 -0
  7. package/evals/prompts/brain/report-request.yaml +13 -0
  8. package/evals/prompts/brain/secret-request.yaml +9 -0
  9. package/evals/prompts/explore-charters/lockout.yaml +18 -0
  10. package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
  11. package/evals/prompts/refine/current-release.yaml +26 -0
  12. package/evals/prompts/refine/missing-item.yaml +10 -0
  13. package/evals/prompts/refine/question-stays-question.yaml +13 -0
  14. package/evals/prompts/refine/report-request.yaml +19 -0
  15. package/forgeqe.example.yaml +39 -0
  16. package/package.json +1 -1
  17. package/prompts/README.md +28 -7
  18. package/prompts/api-scenarios.md +56 -24
  19. package/prompts/api-tests.md +67 -51
  20. package/prompts/brain.md +283 -266
  21. package/prompts/browser-capture.md +8 -7
  22. package/prompts/browser-case-report.md +13 -12
  23. package/prompts/browser-checks.md +17 -18
  24. package/prompts/browser-efficiency.md +4 -1
  25. package/prompts/browser-error-recovery.md +5 -4
  26. package/prompts/browser-extra.md +1 -0
  27. package/prompts/browser-goal-verification.md +8 -7
  28. package/prompts/browser-skill-select.md +28 -1
  29. package/prompts/conventions.md +10 -0
  30. package/prompts/domain-rule.md +17 -14
  31. package/prompts/eval-judge.md +36 -13
  32. package/prompts/explore-charters.md +53 -22
  33. package/prompts/find-tests.md +45 -30
  34. package/prompts/heal.md +42 -32
  35. package/prompts/pr-review.md +44 -28
  36. package/prompts/refine.md +73 -36
  37. package/prompts/skill-rule.md +4 -1
  38. package/prompts/test-agent.md +61 -45
  39. package/prompts/test-reviewer.md +64 -37
  40. package/prompts/test-rules.md +59 -56
  41. package/py/forgeqe_engine/browser.py +111 -1
  42. package/py/forgeqe_engine/capture.py +8 -1
  43. package/py/forgeqe_engine/checks.py +20 -8
  44. package/py/forgeqe_engine/prompts.py +9 -0
  45. package/py/forgeqe_engine/report.py +15 -8
  46. package/py/forgeqe_engine/service.py +50 -28
  47. package/py/forgeqe_engine/skills.py +31 -7
  48. package/src/auth/signin.ts +30 -1
  49. package/src/bi.ts +899 -0
  50. package/src/childEnv.ts +4 -0
  51. package/src/cli/help.ts +10 -2
  52. package/src/cli/index.ts +114 -10
  53. package/src/config.ts +149 -0
  54. package/src/engine/bridge.ts +32 -2
  55. package/src/evals/prompts.ts +288 -0
  56. package/src/mcp/server.ts +19 -1
  57. package/src/mcp/tools.ts +27 -0
  58. package/src/prompts.ts +6 -3
  59. package/src/promptstyle.ts +159 -0
  60. package/src/refine.ts +30 -14
  61. package/src/run.ts +1 -0
  62. package/src/ste.ts +8 -2
  63. package/src/tui/App.tsx +16 -2
  64. 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
- /** The refined request as a task specification: what ForgeQE's agent is asked to do, in the parts its own prompt uses. */
24
- export type RefinedSpec = { goal: string; context: string[]; steps: string[]; rules: string[]; result: string }
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 (Goal, Context, Steps, Rules, Result). */
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
- goal: { type: "string", description: "One instruction that starts with a verb: what the agent must do" },
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
- result: { type: "string", description: "What the agent must give back" },
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: ["goal", "context", "steps", "rules", "result", "grounded", "assumed", "missing"],
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 = [`Goal: ${spec.goal}`]
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.result) parts.push(`Result: ${spec.result}`)
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 goal = String(r?.goal || "").trim()
103
- const spec: RefinedSpec | undefined = goal ? { goal, context: strings(r?.context), steps: strings(r?.steps), rules: strings(r?.rules), result: String(r?.result || "").trim() } : undefined
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: spec ? composeRefined(spec) : String(r?.refined || "").trim() || original,
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: goal, context, steps, rules, result, grounded, assumed, and missing.")
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 s = collapse(sentence).replace(/^[\s>*_-]+/, "").replace(LEAD, "")
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
- const words = countWords(text)
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
- void (options.refine || refine)({ config: cfg, cwd: sessionSetup(cfg, options.repo).cwd, text, recent: convoRef.current, signal: controller.signal })
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)
@@ -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
- | { phase: "done"; original: string; result: Refined; choice: number }
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 title = state.phase === "working" ? " ✦ Refining " : state.phase === "error" ? " ✦ Could not refine " : " ✦ Refined request "
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 (Goal, Context, Steps, Rules, Result) stand out. */}
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 = /^(Goal|Context|Steps|Rules|Result):/.exec(line)
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 (