@capagents/forgeqe 1.1.8 → 1.2.0
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 +31 -1
- package/docs/user-guide.html +30 -5
- package/package.json +1 -1
- package/prompts/brain.md +6 -0
- package/prompts/test-rules.md +2 -0
- package/src/code/coverage.ts +117 -0
- package/src/code/exchange.ts +74 -0
- package/src/code/graph/graph.ts +30 -3
- package/src/code/loop.ts +70 -7
- package/src/code/notes.ts +76 -0
- package/src/code/prompt.ts +52 -3
- package/src/code/review.ts +13 -0
- package/src/detail.ts +2 -1
- package/src/mcp/commands.ts +76 -0
- package/src/mcp/tools.ts +69 -2
- package/src/print.ts +9 -0
- package/src/tui/App.tsx +23 -6
- package/src/tui/components.tsx +4 -3
- package/src/tui/text.ts +16 -0
- package/src/tui/theme.ts +25 -26
- package/src/tui/transcript.tsx +22 -9
- package/src/types.ts +8 -0
package/src/tui/theme.ts
CHANGED
|
@@ -35,35 +35,34 @@ export function formatDuration(ms: number, precise = false): string {
|
|
|
35
35
|
return `${m}m ${String(Math.floor(s % 60)).padStart(2, "0")}s`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
c: [" ", "█▀▀▀", "█ ", "▀▀▀▀"],
|
|
50
|
-
h: ["▄ ", "█▀▀█", "█ █", "▀ ▀"],
|
|
51
|
-
s: [" ", "█▀▀▀", "▀▀▀█", "▀▀▀▀"],
|
|
52
|
-
a: [" ", "▀▀▀█", "█▀▀█", "▀▀▀▀"],
|
|
53
|
-
f: [" ▄▀", "▀█▀", " █ ", " ▀ "],
|
|
54
|
-
g: [" ", "█▀▀█", "█ █", "▀▀▀█"],
|
|
55
|
-
Q: ["█▀▀█ ", "█ █ ", "█ █ ", "▀▀▀█▄"],
|
|
56
|
-
E: ["█▀▀▀", "█▄▄ ", "█ ", "▀▀▀▀"],
|
|
57
|
-
".": [" ", " ", " ", "▀"],
|
|
38
|
+
/**
|
|
39
|
+
* The wordmark's capitals as pixel maps, five pixels tall: every letter has the same weight, height and baseline. A
|
|
40
|
+
* sixth row, left blank, is the Q's tail. Two pixel rows go in one text row as half blocks, so it is three rows high.
|
|
41
|
+
*/
|
|
42
|
+
const LETTERS: Record<string, string[]> = {
|
|
43
|
+
F: ["####", "#...", "###.", "#...", "#..."],
|
|
44
|
+
O: ["####", "#..#", "#..#", "#..#", "####"],
|
|
45
|
+
R: ["####", "#..#", "###.", "#..#", "#..#"],
|
|
46
|
+
G: ["####", "#...", "#.##", "#..#", "####"],
|
|
47
|
+
E: ["####", "#...", "###.", "#...", "####"],
|
|
48
|
+
Q: ["####.", "#..#.", "#..#.", "#..#.", "####.", "...##"],
|
|
58
49
|
}
|
|
59
50
|
|
|
51
|
+
/** The letters of `word` (capitals; others are left out) as three rows of half blocks, one space between letters. */
|
|
60
52
|
export function logoRows(word: string): string[] {
|
|
61
|
-
const rows = ["", "", ""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
const rows = ["", "", ""]
|
|
54
|
+
;[...word.toUpperCase()].filter((ch) => LETTERS[ch]).forEach((ch, index) => {
|
|
55
|
+
const px = LETTERS[ch]!
|
|
56
|
+
const at = (r: number, c: number) => px[r]?.[c] === "#"
|
|
57
|
+
for (let r = 0; r < 3; r++) {
|
|
58
|
+
let cells = ""
|
|
59
|
+
for (let c = 0; c < px[0]!.length; c++) {
|
|
60
|
+
const top = at(r * 2, c)
|
|
61
|
+
const bottom = at(r * 2 + 1, c)
|
|
62
|
+
cells += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " "
|
|
63
|
+
}
|
|
64
|
+
rows[r] += (index ? " " : "") + cells
|
|
65
|
+
}
|
|
67
66
|
})
|
|
68
67
|
return rows
|
|
69
68
|
}
|
package/src/tui/transcript.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs"
|
|
|
2
2
|
import os from "node:os"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { SyntaxStyle } from "@opentui/core"
|
|
5
|
-
import type
|
|
5
|
+
import { createContext, useContext, type ReactNode } from "react"
|
|
6
6
|
|
|
7
7
|
import type { CodeResult, RunStatus } from "../types.ts"
|
|
8
8
|
import { CARD_MARK, type Card } from "../card.ts"
|
|
@@ -11,7 +11,7 @@ import { suiteExitCode, type SuiteResult } from "../suite/run.ts"
|
|
|
11
11
|
import { STATUS_MARK, shortPath, type RunSummary } from "../summary.ts"
|
|
12
12
|
import { BAR } from "./components.tsx"
|
|
13
13
|
import { formatDuration, formatTokens, theme } from "./theme.ts"
|
|
14
|
-
import { clip } from "./text.ts"
|
|
14
|
+
import { clip, shortLink } from "./text.ts"
|
|
15
15
|
|
|
16
16
|
/** The transcript: its lines, and the cards for a run, a suite, a tool result, and what a request produced. */
|
|
17
17
|
|
|
@@ -195,7 +195,7 @@ export function SummaryCard({ summary }: { summary: RunSummary }) {
|
|
|
195
195
|
{summary.savedDir ? (
|
|
196
196
|
<text>
|
|
197
197
|
<span fg={theme.faint}>{"Saved "}</span>
|
|
198
|
-
<Linked text={`${summary.savedDir}/ `} color={theme.muted} />
|
|
198
|
+
<Linked text={`${summary.savedDir}/ `} color={theme.muted} lead={UNDER + 10} />
|
|
199
199
|
<span fg={theme.faint}>{summary.savedFiles.join(" · ")}</span>
|
|
200
200
|
</text>
|
|
201
201
|
) : null}
|
|
@@ -218,14 +218,27 @@ export function linkTarget(token: string, cwd = process.cwd()): string | null {
|
|
|
218
218
|
* Text whose web addresses and existing file paths can be opened from the terminal (OSC 8 links: cmd/ctrl+click in
|
|
219
219
|
* VS Code and most terminals). A path that does not exist stays plain text, so no link leads nowhere.
|
|
220
220
|
*/
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
/** The columns a line of the conversation has, for links to fit in; 0 when not known (no shortening). */
|
|
222
|
+
export const LineRoom = createContext(0)
|
|
223
|
+
|
|
224
|
+
/** Columns before a card's line of text: the "⎿" gutter under the card's title. */
|
|
225
|
+
const UNDER = 4
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* `lead`: the columns before this text on its row (a gutter, a label in another span). A link longer than what is left
|
|
229
|
+
* of the row is drawn shortened, its target whole, so it moves to the next row in one piece instead of splitting.
|
|
230
|
+
*/
|
|
231
|
+
export function Linked({ text, color, lead = UNDER }: { text: string; color: string; lead?: number }) {
|
|
232
|
+
const room = useContext(LineRoom)
|
|
233
|
+
const parts: { text: string; href?: string; max?: number }[] = []
|
|
223
234
|
let at = 0
|
|
224
235
|
for (const m of text.matchAll(LINKABLE)) {
|
|
225
236
|
const href = linkTarget(m[0])
|
|
226
237
|
if (!href) continue
|
|
227
238
|
if (m.index! > at) parts.push({ text: text.slice(at, m.index) })
|
|
228
|
-
|
|
239
|
+
// Beside its label when a useful part fits there ("Page .forgeqe/…/report.html"), else alone on the next row.
|
|
240
|
+
const beside = room - lead - m.index! - 1
|
|
241
|
+
parts.push({ text: m[0], href, ...(room ? { max: beside >= 30 ? beside : room - lead - 1 } : {}) })
|
|
229
242
|
at = m.index! + m[0].length
|
|
230
243
|
}
|
|
231
244
|
if (at < text.length) parts.push({ text: text.slice(at) })
|
|
@@ -234,7 +247,7 @@ export function Linked({ text, color }: { text: string; color: string }) {
|
|
|
234
247
|
{parts.map((p, i) =>
|
|
235
248
|
p.href ? (
|
|
236
249
|
<a key={i} href={p.href} fg={theme.accent}>
|
|
237
|
-
{p.text}
|
|
250
|
+
{p.max ? shortLink(p.text, p.max) : p.text}
|
|
238
251
|
</a>
|
|
239
252
|
) : (
|
|
240
253
|
<span key={i} fg={color}>
|
|
@@ -251,7 +264,7 @@ export function Row({ label, value, color = theme.muted }: { label: string; valu
|
|
|
251
264
|
return (
|
|
252
265
|
<text>
|
|
253
266
|
<span fg={theme.faint}>{label.padEnd(10)}</span>
|
|
254
|
-
<Linked text={value} color={color} />
|
|
267
|
+
<Linked text={value} color={color} lead={UNDER + 10} />
|
|
255
268
|
</text>
|
|
256
269
|
)
|
|
257
270
|
}
|
|
@@ -307,7 +320,7 @@ export const CARD_COLOR: Record<Card["status"], string> = { ok: theme.ok, fail:
|
|
|
307
320
|
|
|
308
321
|
export function ResultCard({ card }: { card: Card }) {
|
|
309
322
|
const color = CARD_COLOR[card.status]
|
|
310
|
-
const lineColor = (l: string) => (/^\s*✓/.test(l) ? theme.ok : /^\s*[✗!]/.test(l) ? theme.err : /^\s
|
|
323
|
+
const lineColor = (l: string) => (/^\s*✓/.test(l) ? theme.ok : /^\s*[✗!]/.test(l) ? theme.err : /^\s*[?~]/.test(l) ? theme.warn : /^\s*(Fix:|→)/.test(l) ? theme.text : theme.muted)
|
|
311
324
|
// What a run used is in the footer and the reports; price talk stays out of the conversation.
|
|
312
325
|
const body = card.body.filter((l) => !/^\s*Usage\s{2,}/.test(l)).filter((l, i, all) => l || (i > 0 && i < all.length - 1))
|
|
313
326
|
return (
|
package/src/types.ts
CHANGED
|
@@ -116,6 +116,8 @@ export type CodeResult = {
|
|
|
116
116
|
knownDefect?: boolean
|
|
117
117
|
/** The job did not finish: what it wrote was moved to the needs-attention folder (see src/code/attention.ts). */
|
|
118
118
|
attention?: { dir: string; files: string[]; patch?: string; readme: string }
|
|
119
|
+
/** The job worked from an existing test: ran it as it was, extended it, or updated it. */
|
|
120
|
+
existing?: "reused" | "extended" | "updated"
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
/** What an agent did to a file, for the shell's file list. */
|
|
@@ -136,6 +138,12 @@ export type CodeEvent =
|
|
|
136
138
|
| { type: "code"; kind: "usage"; usage: Usage; context?: number }
|
|
137
139
|
/** The agent's to-do list, each time it changes. */
|
|
138
140
|
| { type: "code"; kind: "plan"; items: PlanItem[] }
|
|
141
|
+
/** The reviewer's verdict on a test that passed: approved or not, why, and each issue with its file and fix. */
|
|
142
|
+
| { type: "code"; kind: "review"; attempt: number; max: number; approved: boolean; summary: string; issues: CodeReview["issues"] }
|
|
143
|
+
/** Scribe starts another attempt: why, and what it was asked to fix (the reviewer's issues, the test's error). */
|
|
144
|
+
| { type: "code"; kind: "attempt"; attempt: number; max: number; why: string; fixing: string[] }
|
|
145
|
+
/** What Scribe said it did in an attempt: its notes, and the files it changed. */
|
|
146
|
+
| { type: "code"; kind: "answer"; attempt: number; notes?: string; changed: string[] }
|
|
139
147
|
|
|
140
148
|
export type AgentEvent =
|
|
141
149
|
| { type: "log"; level: "info" | "warn" | "error"; text: string }
|