@capagents/forgeqe 1.0.7 → 1.0.9
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 +49 -2
- package/docs/user-guide.html +44 -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 -138
- 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/cli/help.ts +1 -0
- package/src/cli/index.ts +21 -3
- package/src/refine.ts +53 -7
- package/src/ste.ts +184 -0
- package/src/tui/App.tsx +169 -32
- package/src/tui/components.tsx +49 -7
- package/src/tui/landing.ts +128 -0
- package/src/tui/palette.tsx +2 -1
- package/src/tui/refine-panel.tsx +14 -1
- package/src/tui/shortcuts.tsx +66 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import type { AppConfig } from "../config.ts"
|
|
5
|
+
import { CAPABILITIES } from "../capabilities/registry.ts"
|
|
6
|
+
import { allStatuses, configuredSystems, SYSTEM_LABEL, type Probes, MACHINE } from "../capabilities/status.ts"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What the start screen shows before the first request: the project it works on, the actions that work in this setup,
|
|
10
|
+
* and the reports and runs to pick up again. Everything comes from the config, the capability registry, and the files
|
|
11
|
+
* in the folder, so it changes as the setup changes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** "C# · .NET solution · Playwright Test": what the folder's own files say it is built with. */
|
|
15
|
+
export function projectStack(cwd: string): string[] {
|
|
16
|
+
let names: string[] = []
|
|
17
|
+
try {
|
|
18
|
+
names = readdirSync(cwd)
|
|
19
|
+
} catch {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
const has = (re: RegExp) => names.some((n) => re.test(n))
|
|
23
|
+
const out: string[] = []
|
|
24
|
+
if (has(/^playwright\.config\.(ts|js|mjs|cjs)$/)) out.push("Playwright Test")
|
|
25
|
+
if (has(/^cypress\.config\.(ts|js|mjs|cjs)$/)) out.push("Cypress")
|
|
26
|
+
if (has(/\.sln$/) || has(/\.csproj$/)) out.push(".NET")
|
|
27
|
+
if (has(/^pom\.xml$/)) out.push("Maven")
|
|
28
|
+
if (has(/^build\.gradle(\.kts)?$/)) out.push("Gradle")
|
|
29
|
+
if (has(/^(pytest\.ini|conftest\.py)$/) || (has(/^pyproject\.toml$/) && !out.length)) out.push("Python")
|
|
30
|
+
if (has(/^package\.json$/) && !out.some((s) => s === "Playwright Test" || s === "Cypress")) out.push("Node")
|
|
31
|
+
return out.slice(0, 2)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The project line: folder, stack, the systems ForgeQE works with here, and the model. */
|
|
35
|
+
export function projectLine(cfg: AppConfig | null, cwd: string, model: string): string[] {
|
|
36
|
+
const parts = [path.basename(cwd), ...projectStack(cwd)]
|
|
37
|
+
if (cfg) parts.push(...[...configuredSystems(cfg)].filter((s) => s !== "xray" || !configuredSystems(cfg).has("jira")).map((s) => SYSTEM_LABEL[s]))
|
|
38
|
+
if (model) parts.push(model)
|
|
39
|
+
return parts.filter(Boolean)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ALL_SYSTEMS = Object.values(SYSTEM_LABEL)
|
|
43
|
+
|
|
44
|
+
export type StartAction = { id: string; label: string; detail: string; request: string }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The first things worth asking, in this order, among the capabilities that work in this setup: work from the systems
|
|
48
|
+
* first, then what a team asks for most. Each puts its example request in the prompt to adjust.
|
|
49
|
+
*/
|
|
50
|
+
const START_ORDER = ["run-test-cases", "release-readiness", "reports", "exploratory-testing", "write-test-cases", "pr-review", "api-testing", "find-run-tests", "browser-goals"]
|
|
51
|
+
|
|
52
|
+
const START_LABEL: Record<string, string> = {
|
|
53
|
+
"run-test-cases": "Run test cases",
|
|
54
|
+
"release-readiness": "Check release readiness",
|
|
55
|
+
reports: "Write a report to share",
|
|
56
|
+
"exploratory-testing": "Explore a story for bugs",
|
|
57
|
+
"write-test-cases": "Write test cases from a story",
|
|
58
|
+
"pr-review": "Review a pull request",
|
|
59
|
+
"api-testing": "Test an API",
|
|
60
|
+
"find-run-tests": "Run the repository's tests",
|
|
61
|
+
"browser-goals": "Try a flow in the browser",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function startActions(cfg: AppConfig | null, limit = 4, probes: Probes = MACHINE): StartAction[] {
|
|
65
|
+
if (!cfg) return []
|
|
66
|
+
const statuses = allStatuses(cfg, probes)
|
|
67
|
+
const systems = [...configuredSystems(cfg)].map((x) => SYSTEM_LABEL[x])
|
|
68
|
+
const out: StartAction[] = []
|
|
69
|
+
for (const id of START_ORDER) {
|
|
70
|
+
const c = CAPABILITIES.find((x) => x.id === id)
|
|
71
|
+
if (!c || statuses.get(id)?.state === "needs_setup") continue
|
|
72
|
+
// An example in the words of this setup: one that names a system that is set up, else one that names none.
|
|
73
|
+
const named = c.examples.find((e) => systems.some((n) => e.includes(n)))
|
|
74
|
+
const neutral = c.examples.find((e) => !ALL_SYSTEMS.some((n) => e.includes(n)))
|
|
75
|
+
const example = named || neutral || c.examples[0] || c.title
|
|
76
|
+
out.push({ id, label: START_LABEL[id] || c.title, detail: example, request: example })
|
|
77
|
+
if (out.length === limit) break
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type RecentItem = { kind: "report" | "run"; title: string; dir: string; open: string; when: Date; status?: string }
|
|
83
|
+
|
|
84
|
+
/** The newest reports and runs in the reports folder: what a returning user picks up again. */
|
|
85
|
+
export function recentWork(outDir: string, limit = 3): RecentItem[] {
|
|
86
|
+
if (!existsSync(outDir)) return []
|
|
87
|
+
const items: RecentItem[] = []
|
|
88
|
+
for (const name of readdirSync(outDir)) {
|
|
89
|
+
const dir = path.join(outDir, name)
|
|
90
|
+
try {
|
|
91
|
+
if (!statSync(dir).isDirectory()) continue
|
|
92
|
+
const when = statSync(dir).mtime
|
|
93
|
+
if (existsSync(path.join(dir, "run.json"))) {
|
|
94
|
+
const run = JSON.parse(readFileSync(path.join(dir, "run.json"), "utf8")) as { goal?: string; status?: string }
|
|
95
|
+
const report = path.join(dir, "report.html")
|
|
96
|
+
items.push({ kind: "run", title: String(run.goal || name), dir, open: existsSync(report) ? report : dir, when, status: run.status })
|
|
97
|
+
} else if (existsSync(path.join(dir, "report.md"))) {
|
|
98
|
+
const first = readFileSync(path.join(dir, "report.md"), "utf8").split("\n")[0] || ""
|
|
99
|
+
const html = path.join(dir, "report.html")
|
|
100
|
+
items.push({ kind: "report", title: first.replace(/^#\s*/, "").trim() || name, dir, open: existsSync(html) ? html : path.join(dir, "report.md"), when })
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
return items.sort((a, b) => b.when.getTime() - a.when.getTime()).slice(0, limit)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** "just now", "12 min ago", "3 h ago", "yesterday", "4 days ago", then the date. */
|
|
108
|
+
export function ago(when: Date, now = new Date()): string {
|
|
109
|
+
const s = Math.max(0, (now.getTime() - when.getTime()) / 1000)
|
|
110
|
+
if (s < 60) return "just now"
|
|
111
|
+
if (s < 3600) return `${Math.floor(s / 60)} min ago`
|
|
112
|
+
if (s < 86400) return `${Math.floor(s / 3600)} h ago`
|
|
113
|
+
if (s < 2 * 86400) return "yesterday"
|
|
114
|
+
if (s < 7 * 86400) return `${Math.floor(s / 86400)} days ago`
|
|
115
|
+
return when.toISOString().slice(0, 10)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** "Domain", "Domain and Notifications", "Model, Domain, and Notifications". */
|
|
119
|
+
export function listWords(words: string[]): string {
|
|
120
|
+
if (words.length < 3) return words.join(" and ")
|
|
121
|
+
return `${words.slice(0, -1).join(", ")}, and ${words.at(-1)}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** "Domain and Notifications are not set up", or, when the names do not fit in `room`, "5 items are not set up". */
|
|
125
|
+
export function setupSentence(missing: string[], room: number): string {
|
|
126
|
+
const named = `${listWords(missing)} ${missing.length === 1 ? "is" : "are"} not set up`
|
|
127
|
+
return named.length <= room ? named : `${missing.length} ${missing.length === 1 ? "item is" : "items are"} not set up`
|
|
128
|
+
}
|
package/src/tui/palette.tsx
CHANGED
|
@@ -15,7 +15,7 @@ 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" | "wide" | "panels" }
|
|
18
|
+
| { kind: "action"; id: "refine" | "stop" | "details" | "chat" | "wide" | "panels" | "shortcuts" }
|
|
19
19
|
|
|
20
20
|
export type PaletteItem = { id: string; group: PaletteGroup; title: string; detail?: string; keys?: string; run: PaletteRun }
|
|
21
21
|
|
|
@@ -31,6 +31,7 @@ export function paletteItems(opts: { running: boolean; headed: boolean; detailsF
|
|
|
31
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
32
|
}
|
|
33
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" } })
|
|
34
|
+
items.push({ id: "shortcuts", group: "Action", title: "Keyboard shortcuts", keys: "?", run: { kind: "action", id: "shortcuts" } })
|
|
34
35
|
command("features", "What ForgeQE can do here", "/features", "and what each feature still needs")
|
|
35
36
|
command("auth", "Sign in to a system", "/auth", "Azure DevOps, GitHub, Jira, Confluence, TestRail, Zephyr, qTest, Xray")
|
|
36
37
|
command("auth-status", "What is signed in", "/auth status")
|
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)}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { theme } from "./theme.ts"
|
|
2
|
+
|
|
3
|
+
/** Every key the shell answers to, in one panel (? on an empty prompt); esc or ? closes it. */
|
|
4
|
+
export const SHORTCUTS: { group: string; keys: [string, string][] }[] = [
|
|
5
|
+
{
|
|
6
|
+
group: "Prompt",
|
|
7
|
+
keys: [
|
|
8
|
+
["enter", "send the request"],
|
|
9
|
+
["shift+enter, ctrl+j", "new line"],
|
|
10
|
+
["ctrl+r", "refine the request before it is sent"],
|
|
11
|
+
["/ @", "commands; runs, files, and work items to reference"],
|
|
12
|
+
["tab", "complete a suggestion, or the next model profile"],
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
group: "Find and move",
|
|
17
|
+
keys: [
|
|
18
|
+
["ctrl+p", "search: actions, commands, profiles, examples"],
|
|
19
|
+
["shift+tab", "move between the chat and the plan, activity, and files panels"],
|
|
20
|
+
["ctrl+o", "chat or details, on a narrow terminal"],
|
|
21
|
+
["alt+1 to alt+4", "a start action, or a next step"],
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
group: "Run",
|
|
26
|
+
keys: [
|
|
27
|
+
["esc", "stop the request (asks first)"],
|
|
28
|
+
["ctrl+c", "close ForgeQE (asks first; twice closes)"],
|
|
29
|
+
["drag", "copy the selected text"],
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export function ShortcutsPanel(props: { width: number; wide: boolean; onClose: () => void }) {
|
|
35
|
+
const boxWidth = Math.min(84, props.width - 8)
|
|
36
|
+
const keyWidth = 22
|
|
37
|
+
return (
|
|
38
|
+
<box
|
|
39
|
+
position="absolute"
|
|
40
|
+
zIndex={100}
|
|
41
|
+
border
|
|
42
|
+
borderStyle="rounded"
|
|
43
|
+
borderColor={theme.accent}
|
|
44
|
+
title=" Keyboard shortcuts "
|
|
45
|
+
titleColor={theme.accent}
|
|
46
|
+
bottomTitle=" esc close "
|
|
47
|
+
bottomTitleAlignment="right"
|
|
48
|
+
onMouseDown={props.onClose}
|
|
49
|
+
style={{ top: 2, left: Math.floor((props.width - boxWidth) / 2), width: boxWidth, flexDirection: "column", backgroundColor: theme.panel, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }}
|
|
50
|
+
>
|
|
51
|
+
{SHORTCUTS.map((g) => (
|
|
52
|
+
<box key={g.group} style={{ flexDirection: "column", marginBottom: 1, flexShrink: 0 }}>
|
|
53
|
+
<text fg={theme.muted}>{g.group}</text>
|
|
54
|
+
{g.keys
|
|
55
|
+
.filter(([k]) => props.wide || k !== "shift+tab")
|
|
56
|
+
.map(([k, what]) => (
|
|
57
|
+
<text key={k}>
|
|
58
|
+
<span fg={theme.text}>{` ${k.padEnd(keyWidth)}`}</span>
|
|
59
|
+
<span fg={theme.muted}>{what}</span>
|
|
60
|
+
</text>
|
|
61
|
+
))}
|
|
62
|
+
</box>
|
|
63
|
+
))}
|
|
64
|
+
</box>
|
|
65
|
+
)
|
|
66
|
+
}
|