@capagents/forgeqe 1.1.7 → 1.1.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 +32 -7
- package/docs/user-guide.html +32 -13
- package/package.json +1 -1
- package/prompts/brain.md +4 -2
- package/src/brain/index.ts +4 -0
- package/src/capabilities/registry.ts +6 -2
- package/src/cases/api.ts +75 -22
- package/src/code/exchange.ts +53 -0
- package/src/code/loop.ts +15 -3
- package/src/code/runtime.ts +3 -0
- package/src/detail.ts +2 -1
- package/src/mcp/reporttools.ts +205 -14
- package/src/mcp/server.ts +8 -3
- package/src/mcp/tools.ts +4 -1
- package/src/print.ts +9 -0
- package/src/report/evidence.ts +313 -0
- package/src/report/html.ts +7 -3
- package/src/report/template.html +46 -45
- 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 +6 -0
package/src/tui/App.tsx
CHANGED
|
@@ -30,10 +30,11 @@ import { initWorkspace } from "../init.ts"
|
|
|
30
30
|
import { ShortcutsPanel } from "./shortcuts.tsx"
|
|
31
31
|
import { ago, getStarted, projectLine, setupSentence, type Fact, type GetStarted, recentWork, startActions, type RecentItem, type StartAction } from "./landing.ts"
|
|
32
32
|
import { TIPS, formatDuration, formatTokens, packageVersion, shortCwd, theme } from "./theme.ts"
|
|
33
|
-
import { emptyProduced, Line, Produced, producedCard, settleCall, sinceRequest, TestsSetting, TranscriptLine, trimLines } from "./transcript.tsx"
|
|
33
|
+
import { emptyProduced, Line, LineRoom, Produced, producedCard, settleCall, sinceRequest, TestsSetting, TranscriptLine, trimLines } from "./transcript.tsx"
|
|
34
34
|
import { Dialog, dialogSpec } from "./dialogs.ts"
|
|
35
35
|
import { activityFromLog, clip, modelLabel } from "./text.ts"
|
|
36
36
|
import { featuresText } from "../capabilities/render.ts"
|
|
37
|
+
import { answerText, attemptCard, reviewCard } from "../code/exchange.ts"
|
|
37
38
|
import { Palette, paletteItems, searchPalette, SuggestList, type PaletteItem } from "./palette.tsx"
|
|
38
39
|
import { commandSuggestions, complete, lookupWorkItem, recentRuns, referenceSuggestions, repoFiles, triggerAt, type Suggestion, type WorkItem } from "./suggest.ts"
|
|
39
40
|
import { initialChoice, REFINE_CHOICES, RefinePanel, type RefineChoice, type RefineState } from "./refine-panel.tsx"
|
|
@@ -988,6 +989,17 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
988
989
|
else if (ev.kind === "say") record("thought", ev.text, shown)
|
|
989
990
|
else if (ev.kind === "tool") pushTool(ev.text, ev.ok, ev.id, shown)
|
|
990
991
|
else if (ev.kind === "test") record(ev.ok ? "ok" : "err", ev.text, shown)
|
|
992
|
+
// The exchange with the reviewer: its verdict and Scribe's retries as cards in the conversation, each with a
|
|
993
|
+
// line in Activity, so it is clear why a test was rejected and what is being fixed.
|
|
994
|
+
else if (ev.kind === "review") {
|
|
995
|
+
const card = reviewCard(ev)
|
|
996
|
+
pushLine({ kind: "card", text: card.title, card })
|
|
997
|
+
record(ev.approved ? "ok" : "err", `Review, attempt ${ev.attempt}/${ev.max}: ${ev.approved ? "approved" : `changes needed (${ev.issues.length})`} · ${clip(ev.summary, 140)}`, false)
|
|
998
|
+
} else if (ev.kind === "attempt") {
|
|
999
|
+
const card = attemptCard(ev)
|
|
1000
|
+
pushLine({ kind: "card", text: card.title, card })
|
|
1001
|
+
record("code", `Attempt ${ev.attempt}/${ev.max}: ${ev.why}${ev.fixing.length ? ` · fixing ${ev.fixing.length}` : ""}`, false)
|
|
1002
|
+
} else if (ev.kind === "answer") record("code", answerText(ev), true)
|
|
991
1003
|
}
|
|
992
1004
|
if (aborted()) return
|
|
993
1005
|
const who = codingRef.current ? "Coding agent" : "Agent"
|
|
@@ -995,6 +1007,8 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
995
1007
|
else if (ev.kind === "tool") setActivity(`${who}: ${clip(ev.text, 60)}`)
|
|
996
1008
|
else if (ev.kind === "info" && ev.text.startsWith("ForgeQE runs the test")) setActivity("ForgeQE is running the new test…")
|
|
997
1009
|
else if (ev.kind === "say") setActivity(`${who} is working…`)
|
|
1010
|
+
else if (ev.kind === "review") setActivity(ev.approved ? "The reviewer approved the test" : `Scribe is fixing ${ev.issues.length} review ${ev.issues.length === 1 ? "issue" : "issues"}…`)
|
|
1011
|
+
else if (ev.kind === "attempt") setActivity(`Scribe, attempt ${ev.attempt} of ${ev.max}: ${clip(ev.why, 50)}…`)
|
|
998
1012
|
return
|
|
999
1013
|
}
|
|
1000
1014
|
case "code-start": {
|
|
@@ -1626,7 +1640,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1626
1640
|
const startRows = loading ? 1 + 1 + 4 : offer.length ? 1 + 1 + offer.length : 0
|
|
1627
1641
|
const recentRows = recent.length ? 1 + 1 + recent.length : 0
|
|
1628
1642
|
const setupRows = loading || open.length ? 2 : 0
|
|
1629
|
-
const fixed =
|
|
1643
|
+
const fixed = 4 + 2 + 1 + 3 + 1 + 1 + 1 // logo and byline, project line, gap, prompt border, buttons, footer, one row to spare
|
|
1630
1644
|
const room = height - fixed
|
|
1631
1645
|
const want = Math.max(5, Math.min(promptLines, room - 1))
|
|
1632
1646
|
// Refine or a question in the prompt's place takes the room the sections would: they are for starting, not deciding.
|
|
@@ -1661,7 +1675,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1661
1675
|
</box>
|
|
1662
1676
|
<box style={{ height: 1, flexShrink: 0 }} />
|
|
1663
1677
|
<box style={{ flexDirection: "column", width: promptWidth, flexShrink: 0 }}>
|
|
1664
|
-
{renderControls(promptWidth, "Describe what you want to test…", [5, maxRows], height -
|
|
1678
|
+
{renderControls(promptWidth, "Describe what you want to test…", [5, maxRows], height - 4 - 2 - 1 - 1 - 1)}
|
|
1665
1679
|
{showStart ? (
|
|
1666
1680
|
<box style={{ flexDirection: "column", marginTop: 1, height: startRows - 1, flexShrink: 0 }}>
|
|
1667
1681
|
{heading(offerTitle)}
|
|
@@ -1813,10 +1827,12 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1813
1827
|
)
|
|
1814
1828
|
}
|
|
1815
1829
|
|
|
1816
|
-
|
|
1830
|
+
/** `room`: the columns a line of the conversation has (less the scrollbar), so long links are drawn to fit. */
|
|
1831
|
+
const transcript = (every: Line[], style: Record<string, unknown>, room: number) => {
|
|
1817
1832
|
// Quiet lines (reads, searches, calls in progress) are the Activity panel's; the conversation stays readable.
|
|
1818
1833
|
const all = every.filter((l) => !l.quiet)
|
|
1819
1834
|
return (
|
|
1835
|
+
<LineRoom.Provider value={Math.max(20, room - 1)}>
|
|
1820
1836
|
<scrollbox ref={scrollRef} stickyScroll stickyStart="bottom" style={{ flexGrow: 1, ...style }} focused={false}>
|
|
1821
1837
|
{all.map((line, i) => (
|
|
1822
1838
|
<TranscriptLine key={line.id} line={line} prev={all[i - 1]} />
|
|
@@ -1832,6 +1848,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1832
1848
|
</box>
|
|
1833
1849
|
) : null}
|
|
1834
1850
|
</scrollbox>
|
|
1851
|
+
</LineRoom.Provider>
|
|
1835
1852
|
)
|
|
1836
1853
|
}
|
|
1837
1854
|
// A long request gets room: the box grows with the text up to nearly half the screen, then scrolls.
|
|
@@ -1847,7 +1864,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1847
1864
|
<box style={{ flexDirection: "row", height: height - 1, flexShrink: 0, paddingTop: 1 }}>
|
|
1848
1865
|
<box onMouseDown={() => navRef.current.focus !== "chat" && setNav((n) => ({ ...n, focus: "chat" }))} style={{ flexDirection: "column", width: leftWidth, height: height - 2, paddingLeft: 2, paddingRight: 1 }}>
|
|
1849
1866
|
{infoPanels(leftWidth - 3)}
|
|
1850
|
-
{transcript(chatLines, {})}
|
|
1867
|
+
{transcript(chatLines, {}, leftWidth - 3)}
|
|
1851
1868
|
<box style={{ flexDirection: "column", flexShrink: 0 }}>{controls(leftWidth - 3)}</box>
|
|
1852
1869
|
</box>
|
|
1853
1870
|
<box style={{ flexDirection: "column", width: width - leftWidth, height: height - 2, paddingLeft: 1, paddingRight: 2 }}>
|
|
@@ -1883,7 +1900,7 @@ export function App({ options }: { options: TuiOptions }) {
|
|
|
1883
1900
|
}
|
|
1884
1901
|
return (
|
|
1885
1902
|
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
1886
|
-
{transcript(lines, { paddingLeft: 2, paddingRight: 2, paddingTop: 1 })}
|
|
1903
|
+
{transcript(lines, { paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, width - 4)}
|
|
1887
1904
|
<box style={{ flexDirection: "column", paddingLeft: 2, paddingRight: 2, flexShrink: 0 }}>{controls(promptWidth)}</box>
|
|
1888
1905
|
<Footer left={shortCwd()} right={footerRight} width={width} />
|
|
1889
1906
|
{paletteOverlay}
|
package/src/tui/components.tsx
CHANGED
|
@@ -18,14 +18,15 @@ export const BAR = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function Logo() {
|
|
21
|
-
|
|
21
|
+
// One wordmark in capitals, FORGE in the text colour and QE in the accent: one height, weight, and baseline.
|
|
22
|
+
const name = logoRows("FORGE")
|
|
22
23
|
const suffix = logoRows("QE")
|
|
23
24
|
return (
|
|
24
25
|
<box style={{ flexDirection: "column", alignItems: "flex-end", flexShrink: 0 }}>
|
|
25
26
|
{name.map((row, i) => (
|
|
26
27
|
<text key={i}>
|
|
27
|
-
<span fg={theme.
|
|
28
|
-
<span fg={theme.
|
|
28
|
+
<span fg={theme.text}>{row}</span>
|
|
29
|
+
<span fg={theme.accent}>{` ${suffix[i]}`}</span>
|
|
29
30
|
</text>
|
|
30
31
|
))}
|
|
31
32
|
<text fg={theme.muted}>by Capgemini</text>
|
package/src/tui/text.ts
CHANGED
|
@@ -31,3 +31,19 @@ export function activityFromLog(text: string): string {
|
|
|
31
31
|
if (text.startsWith("ForgeQE engine ·")) return "Starting the browser…"
|
|
32
32
|
return clip(text, 70)
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A link's text shortened to `max` columns, so it never wraps into what reads as two links. The start and the last
|
|
37
|
+
* part (the file, or the folder of a path ending in "/") are kept: ".forgeqe/reports/open-defect-rep…/report.html".
|
|
38
|
+
* The link itself keeps its whole target; only what is drawn is shorter.
|
|
39
|
+
*/
|
|
40
|
+
export function shortLink(text: string, max: number): string {
|
|
41
|
+
if (text.length <= max || max < 8) return text
|
|
42
|
+
const trimmed = text.replace(/\/$/, "")
|
|
43
|
+
const slash = trimmed.lastIndexOf("/")
|
|
44
|
+
const tail = slash > 0 ? text.slice(slash) : ""
|
|
45
|
+
// The last part, whole, when it leaves room for a useful start; otherwise both ends of the text.
|
|
46
|
+
if (tail && tail.length <= max - 10) return `${text.slice(0, max - tail.length - 1)}…${tail}`
|
|
47
|
+
const end = Math.floor((max - 1) / 2)
|
|
48
|
+
return `${text.slice(0, max - 1 - end)}…${text.slice(text.length - end)}`
|
|
49
|
+
}
|
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
|
@@ -136,6 +136,12 @@ export type CodeEvent =
|
|
|
136
136
|
| { type: "code"; kind: "usage"; usage: Usage; context?: number }
|
|
137
137
|
/** The agent's to-do list, each time it changes. */
|
|
138
138
|
| { type: "code"; kind: "plan"; items: PlanItem[] }
|
|
139
|
+
/** The reviewer's verdict on a test that passed: approved or not, why, and each issue with its file and fix. */
|
|
140
|
+
| { type: "code"; kind: "review"; attempt: number; max: number; approved: boolean; summary: string; issues: CodeReview["issues"] }
|
|
141
|
+
/** Scribe starts another attempt: why, and what it was asked to fix (the reviewer's issues, the test's error). */
|
|
142
|
+
| { type: "code"; kind: "attempt"; attempt: number; max: number; why: string; fixing: string[] }
|
|
143
|
+
/** What Scribe said it did in an attempt: its notes, and the files it changed. */
|
|
144
|
+
| { type: "code"; kind: "answer"; attempt: number; notes?: string; changed: string[] }
|
|
139
145
|
|
|
140
146
|
export type AgentEvent =
|
|
141
147
|
| { type: "log"; level: "info" | "warn" | "error"; text: string }
|