@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.
- package/HOW_TO_USE.md +42 -2
- package/docs/user-guide.html +45 -3
- package/package.json +1 -1
- package/prompts/README.md +6 -1
- package/prompts/api-scenarios.md +21 -10
- package/prompts/api-tests.md +55 -25
- package/prompts/brain.md +296 -135
- package/prompts/browser-capture.md +8 -1
- package/prompts/browser-case-report.md +12 -1
- package/prompts/browser-checks.md +22 -8
- package/prompts/browser-efficiency.md +1 -1
- package/prompts/browser-error-recovery.md +18 -6
- package/prompts/browser-goal-verification.md +9 -8
- package/prompts/browser-skill-select.md +1 -1
- package/prompts/domain-rule.md +16 -8
- package/prompts/eval-judge.md +15 -8
- package/prompts/explore-charters.md +19 -15
- package/prompts/find-tests.md +26 -16
- package/prompts/heal.md +32 -24
- package/prompts/pr-review.md +21 -13
- package/prompts/refine.md +30 -10
- package/prompts/skill-rule.md +1 -1
- package/prompts/test-agent.md +43 -18
- package/prompts/test-reviewer.md +37 -15
- package/prompts/test-rules.md +55 -19
- package/src/brain/index.ts +4 -3
- package/src/brain/prompt.ts +34 -2
- package/src/capabilities/registry.ts +12 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +21 -3
- package/src/mcp/reporttools.ts +101 -0
- package/src/mcp/tools.ts +4 -0
- package/src/refine.ts +53 -7
- package/src/ste.ts +184 -0
- package/src/tui/App.tsx +60 -41
- package/src/tui/components.tsx +49 -0
- package/src/tui/inspector.tsx +40 -6
- package/src/tui/next.ts +12 -1
- package/src/tui/palette.tsx +6 -2
- package/src/tui/refine-panel.tsx +14 -1
- package/src/tui/transcript.tsx +78 -2
package/src/tui/inspector.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/tui/palette.tsx
CHANGED
|
@@ -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")
|
package/src/tui/refine-panel.tsx
CHANGED
|
@@ -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
|
-
|
|
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)}
|
package/src/tui/transcript.tsx
CHANGED
|
@@ -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
|
-
<
|
|
417
|
+
<ReplyBody text={line.text} />
|
|
342
418
|
</box>
|
|
343
419
|
)
|
|
344
420
|
}
|