@capagents/forgeqe 1.0.5 → 1.0.8

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 (41) hide show
  1. package/HOW_TO_USE.md +42 -2
  2. package/docs/user-guide.html +45 -3
  3. package/package.json +1 -1
  4. package/prompts/README.md +6 -1
  5. package/prompts/api-scenarios.md +21 -10
  6. package/prompts/api-tests.md +55 -25
  7. package/prompts/brain.md +296 -135
  8. package/prompts/browser-capture.md +8 -1
  9. package/prompts/browser-case-report.md +12 -1
  10. package/prompts/browser-checks.md +22 -8
  11. package/prompts/browser-efficiency.md +1 -1
  12. package/prompts/browser-error-recovery.md +18 -6
  13. package/prompts/browser-goal-verification.md +9 -8
  14. package/prompts/browser-skill-select.md +1 -1
  15. package/prompts/domain-rule.md +16 -8
  16. package/prompts/eval-judge.md +15 -8
  17. package/prompts/explore-charters.md +19 -15
  18. package/prompts/find-tests.md +26 -16
  19. package/prompts/heal.md +32 -24
  20. package/prompts/pr-review.md +21 -13
  21. package/prompts/refine.md +30 -10
  22. package/prompts/skill-rule.md +1 -1
  23. package/prompts/test-agent.md +43 -18
  24. package/prompts/test-reviewer.md +37 -15
  25. package/prompts/test-rules.md +55 -19
  26. package/src/brain/index.ts +4 -3
  27. package/src/brain/prompt.ts +34 -2
  28. package/src/capabilities/registry.ts +12 -0
  29. package/src/cli/help.ts +1 -0
  30. package/src/cli/index.ts +21 -3
  31. package/src/mcp/reporttools.ts +101 -0
  32. package/src/mcp/tools.ts +4 -0
  33. package/src/refine.ts +53 -7
  34. package/src/ste.ts +184 -0
  35. package/src/tui/App.tsx +60 -41
  36. package/src/tui/components.tsx +49 -0
  37. package/src/tui/inspector.tsx +40 -6
  38. package/src/tui/next.ts +12 -1
  39. package/src/tui/palette.tsx +6 -2
  40. package/src/tui/refine-panel.tsx +14 -1
  41. package/src/tui/transcript.tsx +78 -2
@@ -2,7 +2,7 @@ import type { ScrollBoxRenderable } from "@opentui/core"
2
2
  import type { ReactNode, RefObject } from "react"
3
3
 
4
4
  import type { Usage } from "../types.ts"
5
- import { Spinner } from "./components.tsx"
5
+ import { fitButtons, Spinner, type Button } from "./components.tsx"
6
6
  import { filesByUse, type FileUse, type Inspector, type Progress } from "./session.ts"
7
7
  import { clip, tailCut } from "./text.ts"
8
8
  import { formatTokens, theme } from "./theme.ts"
@@ -102,6 +102,11 @@ export function SessionPanel({ info, width }: { info: SessionInfo; width: number
102
102
  }
103
103
 
104
104
  /** The right pane's first line: who is working on what and for how long, and Stop while something runs. */
105
+ /** How far the agent's own plan has got, while it has one of two steps or more. */
106
+ export function planProgress(s: Inspector): { done: number; total: number } | null {
107
+ return s.plan.length > 1 ? { done: s.plan.filter((p) => p.status === "completed").length, total: s.plan.length } : null
108
+ }
109
+
105
110
  /** "███████░░░░░ 5/8": how far a suite has got, in `cells` cells. */
106
111
  export function progressBar(done: number, total: number, cells: number): [string, string] {
107
112
  const filled = total ? Math.round((Math.min(done, total) / total) * cells) : 0
@@ -118,6 +123,8 @@ export function StatusBar(props: {
118
123
  onStop: () => void
119
124
  /** A suite's cases: done so far, of how many. */
120
125
  progress?: { done: number; total: number } | null
126
+ /** Buttons for the view (Panels, Wide chat), before Stop; the first to give way when the line is short. */
127
+ actions?: Button[]
121
128
  }) {
122
129
  const done = props.status === "done"
123
130
  const failed = props.status === "fail"
@@ -134,7 +141,9 @@ export function StatusBar(props: {
134
141
  const bar = props.running && props.progress && props.progress.total > 1 ? props.progress : null
135
142
  const cells = 12
136
143
  const barText = bar ? ` ${bar.done}/${bar.total} ` : ""
137
- const room = Math.max(10, props.width - props.who.length - (props.running ? 30 : 6) - (bar ? cells + barText.length : 0))
144
+ const fixed = props.who.length + (props.running ? 30 : 6) + (bar ? cells + barText.length : 0)
145
+ const actions = fitButtons(props.actions || [], Math.max(0, props.width - fixed - 28))
146
+ const room = Math.max(10, props.width - fixed - actions.reduce((n, b) => n + b.icon.length + b.label.length + (b.keys ? b.keys.length + 1 : 0) + 4, 0))
138
147
  const [full, empty] = bar ? progressBar(bar.done, bar.total, cells) : ["", ""]
139
148
  return (
140
149
  <box style={{ flexDirection: "row", justifyContent: "space-between", height: 1, flexShrink: 0, paddingLeft: 1, paddingRight: 1 }}>
@@ -145,8 +154,18 @@ export function StatusBar(props: {
145
154
  <span fg={props.running ? theme.muted : color}>{`${props.running ? " " : " "}${clip(what, room)}`}</span>
146
155
  </text>
147
156
  </box>
157
+ <box style={{ flexDirection: "row" }}>
158
+ {actions.map((b) => (
159
+ <box key={b.label} onMouseDown={b.onPress} style={{ flexDirection: "row", paddingLeft: 1, paddingRight: 1, marginLeft: 1, backgroundColor: theme.button }}>
160
+ <text>
161
+ <span fg={b.color}>{`${b.icon} `}</span>
162
+ <span fg={theme.text}>{b.label}</span>
163
+ {b.keys ? <span fg={theme.faint}>{` ${b.keys}`}</span> : null}
164
+ </text>
165
+ </box>
166
+ ))}
148
167
  {props.running ? (
149
- <box style={{ flexDirection: "row" }}>
168
+ <box style={{ flexDirection: "row", marginLeft: 2 }}>
150
169
  {bar ? (
151
170
  <text>
152
171
  <span fg={theme.ok}>{full}</span>
@@ -160,6 +179,7 @@ export function StatusBar(props: {
160
179
  </box>
161
180
  </box>
162
181
  ) : null}
182
+ </box>
163
183
  </box>
164
184
  )
165
185
  }
@@ -242,6 +262,12 @@ const ACTIVITY_STYLE: Record<string, [string, string, string]> = {
242
262
  err: ["✗", theme.err, theme.text],
243
263
  }
244
264
 
265
+ /** A tool line's call and its arguments in brackets, or null when it has none. */
266
+ export function toolArgs(text: string): [string, string] | null {
267
+ const at = text.indexOf(" (")
268
+ return at > 0 && text.endsWith(")") ? [text.slice(0, at), text.slice(at + 1)] : null
269
+ }
270
+
245
271
  /** One activity line, its mark in a column of its own so wrapped text stays under the text. */
246
272
  export function ActivityRow({ line }: { line: Line }) {
247
273
  const [mark, markColor, textColor] = ACTIVITY_STYLE[line.kind] || ["·", theme.faint, theme.muted]
@@ -250,9 +276,17 @@ export function ActivityRow({ line }: { line: Line }) {
250
276
  <text fg={markColor} style={{ width: 2, flexShrink: 0 }}>
251
277
  {mark}
252
278
  </text>
253
- <text fg={textColor} style={{ flexGrow: 1, flexShrink: 1 }}>
254
- {line.text || " "}
255
- </text>
279
+ {line.kind === "tool" && toolArgs(line.text) ? (
280
+ // "Azure DevOps · pipelines build (action list, project QI-Claims)": the call readable, its arguments quiet.
281
+ <text style={{ flexGrow: 1, flexShrink: 1 }}>
282
+ <span fg={theme.text}>{toolArgs(line.text)![0]}</span>
283
+ <span fg={theme.faint}>{` ${toolArgs(line.text)![1]}`}</span>
284
+ </text>
285
+ ) : (
286
+ <text fg={textColor} style={{ flexGrow: 1, flexShrink: 1 }}>
287
+ {line.text || " "}
288
+ </text>
289
+ )}
256
290
  </box>
257
291
  )
258
292
  }
package/src/tui/next.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import path from "node:path"
3
3
 
4
+ import type { FollowUp } from "../brain/prompt.ts"
4
5
  import type { Produced } from "./transcript.tsx"
5
6
 
6
7
  /**
@@ -10,8 +11,14 @@ import type { Produced } from "./transcript.tsx"
10
11
 
11
12
  export type NextStep = { label: string; run: { kind: "insert"; text: string } | { kind: "open"; path: string } }
12
13
 
13
- export function nextSteps(p: Produced, opts: { testsOn: boolean }): NextStep[] {
14
+ /**
15
+ * `followUps`: what ForgeQE's agent suggests from its answer ("Write a release report"), after what the request
16
+ * produced; a follow-up that repeats a step already offered is left out.
17
+ */
18
+ export function nextSteps(p: Produced, opts: { testsOn: boolean; followUps?: FollowUp[] }): NextStep[] {
14
19
  const steps: NextStep[] = []
20
+ const report = p.reports.at(-1)
21
+ if (report && existsSync(report.html)) steps.push({ label: "Open the report", run: { kind: "open", path: report.html } })
15
22
  const run = p.runs.at(-1)
16
23
  const suite = p.suites.at(-1)
17
24
  if (suite) {
@@ -28,5 +35,9 @@ export function nextSteps(p: Produced, opts: { testsOn: boolean }): NextStep[] {
28
35
  }
29
36
  const test = p.tests.at(-1)
30
37
  if (test?.result?.testFile && test.result.status === "passed") steps.unshift({ label: "Open the new test", run: { kind: "open", path: test.result.testFile } })
38
+ for (const f of opts.followUps || []) {
39
+ if (steps.some((s) => s.label.toLowerCase() === f.label.toLowerCase())) continue
40
+ steps.push({ label: f.label, run: { kind: "insert", text: f.request } })
41
+ }
31
42
  return steps.slice(0, 3)
32
43
  }
@@ -15,17 +15,21 @@ export type PaletteRun =
15
15
  /** Put in the prompt to edit and send: an example request. */
16
16
  | { kind: "insert"; text: string }
17
17
  /** Something the shell does itself. */
18
- | { kind: "action"; id: "refine" | "stop" | "details" | "chat" }
18
+ | { kind: "action"; id: "refine" | "stop" | "details" | "chat" | "wide" | "panels" }
19
19
 
20
20
  export type PaletteItem = { id: string; group: PaletteGroup; title: string; detail?: string; keys?: string; run: PaletteRun }
21
21
 
22
22
  export const GROUP_COLOR: Record<PaletteGroup, string> = { Action: theme.warn, Command: theme.accent, Profile: theme.memory, Ask: theme.ok }
23
23
 
24
- export function paletteItems(opts: { running: boolean; headed: boolean; detailsFull: boolean; profiles: string[]; active: string; narrow: boolean; view: "chat" | "details" }): PaletteItem[] {
24
+ export function paletteItems(opts: { running: boolean; headed: boolean; detailsFull: boolean; profiles: string[]; active: string; narrow: boolean; view: "chat" | "details"; chatWide?: boolean }): PaletteItem[] {
25
25
  const items: PaletteItem[] = []
26
26
  const command = (id: string, title: string, text: string, detail?: string, keys?: string) => items.push({ id, group: "Command", title, detail, keys, run: { kind: "command", text } })
27
27
  if (opts.running) items.push({ id: "stop", group: "Action", title: "Stop the running request", keys: "esc", run: { kind: "action", id: "stop" } })
28
28
  items.push({ id: "refine", group: "Action", title: "Refine my request", detail: "check it against what ForgeQE knows before sending", keys: "ctrl+r", run: { kind: "action", id: "refine" } })
29
+ if (!opts.narrow) {
30
+ items.push({ id: "wide", group: "Action", title: opts.chatWide ? "Split view: chat and panels" : "Wide chat: the conversation across the screen", run: { kind: "action", id: "wide" } })
31
+ if (!opts.chatWide) items.push({ id: "panels", group: "Action", title: "Move to the panels: plan, activity, files", keys: "shift+tab", run: { kind: "action", id: "panels" } })
32
+ }
29
33
  if (opts.narrow) items.push({ id: "view", group: "Action", title: opts.view === "chat" ? "Show the details view" : "Back to the chat", keys: "ctrl+o", run: { kind: "action", id: opts.view === "chat" ? "details" : "chat" } })
30
34
  command("features", "What ForgeQE can do here", "/features", "and what each feature still needs")
31
35
  command("auth", "Sign in to a system", "/auth", "Azure DevOps, GitHub, Jira, Confluence, TestRail, Zephyr, qTest, Xray")
@@ -49,7 +49,20 @@ export function RefinePanel(props: { state: RefineState; width: number; onChoose
49
49
  <text fg={theme.err}>{state.message}</text>
50
50
  ) : (
51
51
  <>
52
- <text fg={theme.text}>{state.result.refined}</text>
52
+ {/* The request as it is sent: its labelled parts (Goal, Context, Steps, Rules, Result) stand out. */}
53
+ {state.result.refined.split("\n").map((line, i) => {
54
+ const label = /^(Goal|Context|Steps|Rules|Result):/.exec(line)
55
+ return label ? (
56
+ <text key={i}>
57
+ <span fg={COLOR}>{label[0]}</span>
58
+ <span fg={theme.text}>{line.slice(label[0].length)}</span>
59
+ </text>
60
+ ) : (
61
+ <text key={i} fg={line.trim() ? theme.text : undefined}>
62
+ {line || " "}
63
+ </text>
64
+ )
65
+ })}
53
66
  {section("Checked", theme.ok, "✓", state.result.grounded.filter((g) => g.checked).map((g) => g.fact))}
54
67
  {section("Assumed", theme.warn, "~", [...state.result.grounded.filter((g) => !g.checked).map((g) => g.fact), ...state.result.assumed])}
55
68
  {section("Missing — answer these in your request", theme.err, "?", state.result.missing)}
@@ -31,9 +31,11 @@ export type Produced = {
31
31
  runs: { runDir: string; status: RunStatus; files: string[] }[]
32
32
  suites: { html: string; junit: string }[]
33
33
  tests: { runDir: string; result?: CodeResult; error?: string }[]
34
+ /** Reports write_report saved: the page to open. */
35
+ reports: { title: string; html: string }[]
34
36
  }
35
37
 
36
- export const emptyProduced = (): Produced => ({ runs: [], suites: [], tests: [] })
38
+ export const emptyProduced = (): Produced => ({ runs: [], suites: [], tests: [], reports: [] })
37
39
 
38
40
  /** Whether passed runs become tests this session, and why not when they don't. */
39
41
  export type TestsSetting = { on: boolean; why: string }
@@ -325,6 +327,80 @@ function replyStyle(): SyntaxStyle {
325
327
  return style
326
328
  }
327
329
 
330
+ /** A reply cut into what reads better laid out than wrapped: headings, list items with their depth, and the rest. */
331
+ export type ReplyBlock =
332
+ | { kind: "heading"; text: string }
333
+ | { kind: "item"; depth: number; mark: string; text: string }
334
+ | { kind: "markdown"; text: string }
335
+
336
+ const ITEM = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/
337
+
338
+ export function replyBlocks(md: string): ReplyBlock[] {
339
+ const blocks: ReplyBlock[] = []
340
+ let rest: string[] = []
341
+ let fence = false
342
+ const flush = () => {
343
+ const text = rest.join("\n").trim()
344
+ if (text) blocks.push({ kind: "markdown", text })
345
+ rest = []
346
+ }
347
+ for (const line of md.replace(/\r/g, "").split("\n")) {
348
+ if (/^\s*(```|~~~)/.test(line)) fence = !fence
349
+ const last = blocks.at(-1)
350
+ if (fence || /^\s*(```|~~~)/.test(line) || /^\s*\|/.test(line)) {
351
+ rest.push(line)
352
+ continue
353
+ }
354
+ const heading = /^#{1,6}\s+(.*)$/.exec(line)
355
+ const item = ITEM.exec(line)
356
+ if (heading) {
357
+ flush()
358
+ blocks.push({ kind: "heading", text: heading[1]!.replace(/\*\*/g, "").trim() })
359
+ } else if (item) {
360
+ flush()
361
+ const depth = Math.min(3, Math.floor(item[1]!.replace(/\t/g, " ").length / 2))
362
+ const ordered = /\d/.test(item[2]!)
363
+ blocks.push({ kind: "item", depth, mark: ordered ? item[2]!.replace(")", ".") : depth ? "◦" : "•", text: item[3]! })
364
+ } else if (line.trim() && /^\s+\S/.test(line) && last?.kind === "item" && !rest.length) {
365
+ // A list item's continuation line: it belongs to the item, under its text.
366
+ last.text += ` ${line.trim()}`
367
+ } else if (!line.trim()) {
368
+ rest.push(line)
369
+ } else rest.push(line)
370
+ }
371
+ flush()
372
+ return blocks
373
+ }
374
+
375
+ /** The agent's reply: headings with room above, list items with their marks in a column so wrapped lines hang under the text. */
376
+ function ReplyBody({ text }: { text: string }) {
377
+ const blocks = replyBlocks(text)
378
+ return (
379
+ <box style={{ flexDirection: "column", flexShrink: 0 }}>
380
+ {blocks.map((b, i) =>
381
+ b.kind === "heading" ? (
382
+ <text key={i} fg={theme.accent} style={{ marginTop: i ? 1 : 0, flexShrink: 0 }}>
383
+ <strong>{b.text}</strong>
384
+ </text>
385
+ ) : b.kind === "item" ? (
386
+ <box key={i} style={{ flexDirection: "row", flexShrink: 0, paddingLeft: b.depth * 2, marginTop: blocks[i - 1]?.kind === "markdown" ? 1 : 0 }}>
387
+ <text fg={theme.accent} style={{ width: b.mark.length + 1, flexShrink: 0 }}>
388
+ {b.mark}
389
+ </text>
390
+ <box style={{ flexGrow: 1, flexShrink: 1 }}>
391
+ <markdown content={b.text} syntaxStyle={replyStyle()} fg={theme.text} conceal />
392
+ </box>
393
+ </box>
394
+ ) : (
395
+ <box key={i} style={{ marginTop: i ? 1 : 0, flexShrink: 0 }}>
396
+ <markdown content={b.text} syntaxStyle={replyStyle()} fg={theme.text} conceal tableOptions={{ widthMode: "content", wrapMode: "word" }} />
397
+ </box>
398
+ ),
399
+ )}
400
+ </box>
401
+ )
402
+ }
403
+
328
404
  export function TranscriptLine({ line }: { line: Line }) {
329
405
  if (line.kind === "card" && line.card) return <ResultCard card={line.card} />
330
406
  if (line.kind === "summary" && line.summary) return <SummaryCard summary={line.summary} />
@@ -338,7 +414,7 @@ export function TranscriptLine({ line }: { line: Line }) {
338
414
  customBorderChars={BAR}
339
415
  style={{ backgroundColor: theme.panel, paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, marginTop: 1, marginBottom: 1, flexShrink: 0 }}
340
416
  >
341
- <markdown content={line.text} syntaxStyle={replyStyle()} fg={theme.text} conceal tableOptions={{ widthMode: "content", wrapMode: "word" }} />
417
+ <ReplyBody text={line.text} />
342
418
  </box>
343
419
  )
344
420
  }