diff --git a/CHANGELOG.md b/CHANGELOG.md index 0172181..a11b6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,18 +31,14 @@ tree as paused, and resume requires the loop to actually be paused. - `read_artifact` now resolves symlinks before the confinement check, so a symlink inside the project cannot leak files from outside it. -- Added a live subagent dashboard: a TUI widget above the editor shows the - research run id, loop status, iteration, active agents with elapsed time, - and recently finished agents (updated every 1.5 s, hidden when idle). +- The live widget above the editor shows the research run id, loop status, + iteration, active agents with elapsed time, and recently finished agents + (updated every 1.5 s, hidden when idle). - Subagents no longer stall the main chat on serial model backends: new `agent_concurrency` (default 3) caps how many subagent sessions stream at once, and optional `subagent_model` routes subagents to a different model/backend (e.g. a local Ollama model) so they never contend with the main session. -- New `/agents` command opens an interactive overlay with the full agent tree: - tree glyphs (├─/└─/│), colored statuses and icons, live elapsed time, - keyboard navigation (↑/↓), and a detail pane (Enter) showing each agent's - task and result. The live widget header now hints at `/agents`. ## 0.1.1 — 2026-07-31 diff --git a/graphify-out/cache/ast/v0.8.44/8b8f8ebbd94cd9c50f0597f06a6cd68d53e6605d682f260a087c9fc7176f7a2b.json b/graphify-out/cache/ast/v0.8.44/8b8f8ebbd94cd9c50f0597f06a6cd68d53e6605d682f260a087c9fc7176f7a2b.json new file mode 100644 index 0000000..3833762 --- /dev/null +++ b/graphify-out/cache/ast/v0.8.44/8b8f8ebbd94cd9c50f0597f06a6cd68d53e6605d682f260a087c9fc7176f7a2b.json @@ -0,0 +1 @@ +{"nodes": [{"id": "home_emil_desktop_hypothesis_machine_changelog_md", "label": "CHANGELOG.md", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L1"}, {"id": "hypothesis_machine_changelog_changelog", "label": "Changelog", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L1"}, {"id": "hypothesis_machine_changelog_unreleased", "label": "Unreleased", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L3"}, {"id": "hypothesis_machine_changelog_0_1_1_2026_07_31", "label": "0.1.1 \u2014 2026-07-31", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L47"}, {"id": "hypothesis_machine_changelog_0_1_0_2026_07_31", "label": "0.1.0 \u2014 2026-07-31", "file_type": "document", "source_file": "CHANGELOG.md", "source_location": "L60"}], "edges": [{"source": "home_emil_desktop_hypothesis_machine_changelog_md", "target": "hypothesis_machine_changelog_changelog", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L1", "weight": 1.0}, {"source": "hypothesis_machine_changelog_changelog", "target": "hypothesis_machine_changelog_unreleased", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L3", "weight": 1.0}, {"source": "hypothesis_machine_changelog_changelog", "target": "hypothesis_machine_changelog_0_1_1_2026_07_31", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L47", "weight": 1.0}, {"source": "hypothesis_machine_changelog_changelog", "target": "hypothesis_machine_changelog_0_1_0_2026_07_31", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CHANGELOG.md", "source_location": "L60", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0} \ No newline at end of file diff --git a/graphify-out/cache/stat-index.json b/graphify-out/cache/stat-index.json index 4207857..a07702b 100644 --- a/graphify-out/cache/stat-index.json +++ b/graphify-out/cache/stat-index.json @@ -1 +1 @@ -{"/home/emil/Desktop/hypothesis-machine/package.json":{"size":1331,"mtime_ns":1785526465209535381,"hash":"f7514b7033a121f12b8960b05cad5a29bb4636b9641740561adc3b75352f95cb"},"/home/emil/Desktop/hypothesis-machine/tsconfig.build.json":{"size":171,"mtime_ns":1785523498961599333,"hash":"25868e1e77377761e5f756724739440927cdecf3d48c2800816407ef81b79222"},"/home/emil/Desktop/hypothesis-machine/tsconfig.json":{"size":430,"mtime_ns":1785523498961599333,"hash":"49863ef1ecd0b134189964c301f95090bf082241233c16055decd9aca6a9a314"},"/home/emil/Desktop/hypothesis-machine/.github/pull_request_template.md":{"size":534,"mtime_ns":1785526202969401851,"hash":"d76aeee1b42b19491e04604e657c0115b9b34df747f02b126e162df16538df1a"},"/home/emil/Desktop/hypothesis-machine/CHANGELOG.md":{"size":4559,"mtime_ns":1785531669243473488,"hash":"769d7b6bd162d14987cbe61d8f4563221c8450f44f201224c92c874efb2081c6"},"/home/emil/Desktop/hypothesis-machine/CONTRIBUTING.md":{"size":1522,"mtime_ns":1785526465210535393,"hash":"991a374ba1a32e74466f14e615736dec364ef2e22af71d3721a13e9f7779fd2c"},"/home/emil/Desktop/hypothesis-machine/README.md":{"size":6441,"mtime_ns":1785526465209535381,"hash":"d0ba5495074caf139af024082008ec136d4609995f58d431d716d24953bd7c07"},"/home/emil/Desktop/hypothesis-machine/SECURITY.md":{"size":974,"mtime_ns":1785526202966401815,"hash":"7870d4b34102270e45e1453c6052b25af09f1fe13bfdc4f244108b66a1e9a34b"},"/home/emil/Desktop/hypothesis-machine/docs/architecture.md":{"size":2002,"mtime_ns":1785524693984370854,"hash":"c04eb13e5a1728dc27f72e94bf88c179d26719c684e7f5f6129222910ae795cf"},"/home/emil/Desktop/hypothesis-machine/docs/development.md":{"size":1283,"mtime_ns":1785526465211535405,"hash":"f4af7af03f18576be7dbfec448e1443911d7fe43eabe9dc8ea0240052ff3383e"},"/home/emil/Desktop/hypothesis-machine/docs/pi-capabilities.md":{"size":4716,"mtime_ns":1785525665668981602,"hash":"32246de16c8fb36b826cb70b489f91ee0b3e5073bcbde729853ca04813fda9e4"},"/home/emil/Desktop/hypothesis-machine/docs/releasing.md":{"size":1269,"mtime_ns":1785526465213535429,"hash":"902bf6e8f541fbce500197fb303fb0ccfa2571998ee5bc57d7227e0f8d059b5a"},"/home/emil/Desktop/hypothesis-machine/docs/security.md":{"size":2817,"mtime_ns":1785526465213535429,"hash":"b8e428dd89560c6487d0177ae3f0b168dec24ef7727ad959e06f58dff206c5a1"},"/home/emil/Desktop/hypothesis-machine/prompts/agent-base.md":{"size":309,"mtime_ns":1785524192916593472,"hash":"6290b9307d35d28616e10d0043fd69150fac94ec4489b7a27a7fa1bbe830edf0"},"/home/emil/Desktop/hypothesis-machine/prompts/retrospective.md":{"size":244,"mtime_ns":1785524192917593484,"hash":"e16601b436e781f625c271fe5637d73c23a61780d3022ba0368503fc3b0516a9"},"/home/emil/Desktop/hypothesis-machine/prompts/supervisor.md":{"size":326,"mtime_ns":1785524192916593472,"hash":"78358e240ac6321433f93b14f138b6de917bba392078cb2e831f0276721968f3"},"/home/emil/Desktop/hypothesis-machine/prompts/synthesis.md":{"size":203,"mtime_ns":1785524192916593472,"hash":"33a91ff7d9ddc51110b59f673dea6bf954c48a6fc880f9a06b713dacb6c2c109"},"/home/emil/Desktop/hypothesis-machine/services/browser-worker/README.md":{"size":945,"mtime_ns":1785524192915593461,"hash":"eac3a08976ae79cb79300181be5b1b6cb8d0dd4a27aa8940e4e473a8429a15b8"}} \ No newline at end of file +{"/home/emil/Desktop/hypothesis-machine/package.json":{"size":1331,"mtime_ns":1785526465209535381,"hash":"f7514b7033a121f12b8960b05cad5a29bb4636b9641740561adc3b75352f95cb"},"/home/emil/Desktop/hypothesis-machine/tsconfig.build.json":{"size":171,"mtime_ns":1785523498961599333,"hash":"25868e1e77377761e5f756724739440927cdecf3d48c2800816407ef81b79222"},"/home/emil/Desktop/hypothesis-machine/tsconfig.json":{"size":430,"mtime_ns":1785523498961599333,"hash":"49863ef1ecd0b134189964c301f95090bf082241233c16055decd9aca6a9a314"},"/home/emil/Desktop/hypothesis-machine/.github/pull_request_template.md":{"size":534,"mtime_ns":1785526202969401851,"hash":"d76aeee1b42b19491e04604e657c0115b9b34df747f02b126e162df16538df1a"},"/home/emil/Desktop/hypothesis-machine/CHANGELOG.md":{"size":3998,"mtime_ns":1785531774587687095,"hash":"8b8f8ebbd94cd9c50f0597f06a6cd68d53e6605d682f260a087c9fc7176f7a2b"},"/home/emil/Desktop/hypothesis-machine/CONTRIBUTING.md":{"size":1522,"mtime_ns":1785526465210535393,"hash":"991a374ba1a32e74466f14e615736dec364ef2e22af71d3721a13e9f7779fd2c"},"/home/emil/Desktop/hypothesis-machine/README.md":{"size":6441,"mtime_ns":1785526465209535381,"hash":"d0ba5495074caf139af024082008ec136d4609995f58d431d716d24953bd7c07"},"/home/emil/Desktop/hypothesis-machine/SECURITY.md":{"size":974,"mtime_ns":1785526202966401815,"hash":"7870d4b34102270e45e1453c6052b25af09f1fe13bfdc4f244108b66a1e9a34b"},"/home/emil/Desktop/hypothesis-machine/docs/architecture.md":{"size":2002,"mtime_ns":1785524693984370854,"hash":"c04eb13e5a1728dc27f72e94bf88c179d26719c684e7f5f6129222910ae795cf"},"/home/emil/Desktop/hypothesis-machine/docs/development.md":{"size":1283,"mtime_ns":1785526465211535405,"hash":"f4af7af03f18576be7dbfec448e1443911d7fe43eabe9dc8ea0240052ff3383e"},"/home/emil/Desktop/hypothesis-machine/docs/pi-capabilities.md":{"size":4716,"mtime_ns":1785525665668981602,"hash":"32246de16c8fb36b826cb70b489f91ee0b3e5073bcbde729853ca04813fda9e4"},"/home/emil/Desktop/hypothesis-machine/docs/releasing.md":{"size":1269,"mtime_ns":1785526465213535429,"hash":"902bf6e8f541fbce500197fb303fb0ccfa2571998ee5bc57d7227e0f8d059b5a"},"/home/emil/Desktop/hypothesis-machine/docs/security.md":{"size":2817,"mtime_ns":1785526465213535429,"hash":"b8e428dd89560c6487d0177ae3f0b168dec24ef7727ad959e06f58dff206c5a1"},"/home/emil/Desktop/hypothesis-machine/prompts/agent-base.md":{"size":309,"mtime_ns":1785524192916593472,"hash":"6290b9307d35d28616e10d0043fd69150fac94ec4489b7a27a7fa1bbe830edf0"},"/home/emil/Desktop/hypothesis-machine/prompts/retrospective.md":{"size":244,"mtime_ns":1785524192917593484,"hash":"e16601b436e781f625c271fe5637d73c23a61780d3022ba0368503fc3b0516a9"},"/home/emil/Desktop/hypothesis-machine/prompts/supervisor.md":{"size":326,"mtime_ns":1785524192916593472,"hash":"78358e240ac6321433f93b14f138b6de917bba392078cb2e831f0276721968f3"},"/home/emil/Desktop/hypothesis-machine/prompts/synthesis.md":{"size":203,"mtime_ns":1785524192916593472,"hash":"33a91ff7d9ddc51110b59f673dea6bf954c48a6fc880f9a06b713dacb6c2c109"},"/home/emil/Desktop/hypothesis-machine/services/browser-worker/README.md":{"size":945,"mtime_ns":1785524192915593461,"hash":"eac3a08976ae79cb79300181be5b1b6cb8d0dd4a27aa8940e4e473a8429a15b8"}} \ No newline at end of file diff --git a/src/agent-tree-ui.ts b/src/agent-tree-ui.ts deleted file mode 100644 index 30c5f82..0000000 --- a/src/agent-tree-ui.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"; -import type { Theme } from "@earendil-works/pi-coding-agent"; -import type { AgentTree } from "./agent-tree.js"; -import type { AgentRecord } from "./types.js"; - -interface TreeRow { record: AgentRecord; prefix: string; depth: number } - -/** Flatten the agent tree into rows with tree glyphs (├─/└─/│). */ -function buildRows(tree: AgentTree): TreeRow[] { - const rows: TreeRow[] = []; - const root = tree.inspect(tree.rootId); - const walk = (record: AgentRecord, ancestors: boolean[], last: boolean, depth: number): void => { - const indent = ancestors.map((isLast) => (isLast ? " " : "│ ")).join(""); - const connector = ancestors.length === 0 ? "" : last ? "└─ " : "├─ "; - rows.push({ record, prefix: indent + connector, depth }); - record.children.forEach((childId, index) => walk(tree.inspect(childId), [...ancestors, last], index === record.children.length - 1, depth + 1)); - }; - walk(root, [], false, 0); - return rows; -} - -const STATUS_COLOR: Record string> = { - running: (t, s) => t.fg("accent", s), - waiting: (t, s) => t.fg("accent", s), - completed: (t, s) => t.fg("success", s), - failed: (t, s) => t.fg("error", s), - cancelled: (t, s) => t.fg("dim", s), - interrupted: (t, s) => t.fg("warning", s), - created: (t, s) => t.fg("text", s), - archived: (t, s) => t.fg("dim", s), -}; -const STATUS_ICON: Record = { - running: "▸", waiting: "⏸", completed: "✓", failed: "✖", cancelled: "·", interrupted: "⚠", created: "·", archived: "·", -}; - -function shortName(id: string): string { return id.replace(/-[a-f0-9]{8}$/, ""); } - -function elapsed(record: AgentRecord, now: number): string { - if (!record.startedAt) return ""; - const start = Date.parse(record.startedAt); - const end = record.finishedAt ? Date.parse(record.finishedAt) : (record.status === "running" || record.status === "waiting") ? now : undefined; - if (!end) return ""; - const span = Math.max(0, Math.round((end - start) / 1000)); - return `${String(Math.floor(span / 60)).padStart(2, "0")}:${String(span % 60).padStart(2, "0")}`; -} - -/** Interactive overlay showing the full agent tree with live statuses, keyboard navigation, and a detail pane. */ -export function createAgentTreeOverlay( - tree: AgentTree, - tui: { requestRender(): void }, - theme: Theme, - done: () => void, -): { render(width: number): string[]; invalidate(): void; handleInput(data: string): void; dispose(): void } { - let selected = 0; - let detail = false; - const timer = setInterval(() => tui.requestRender(), 1500); - return { - render(width) { - const rows = buildRows(tree); - if (selected >= rows.length) selected = Math.max(0, rows.length - 1); - const now = Date.now(); - const lines: string[] = [theme.fg("accent", `◆ Agent tree · ${tree.runId} · ${rows.length - 1} agents`), ""]; - for (let i = 0; i < rows.length; i++) { - const { record, prefix } = rows[i]!; - const marker = i === selected ? theme.fg("accent", ">") : " "; - const icon = STATUS_ICON[record.status]; - const status = STATUS_COLOR[record.status](theme, record.status); - const time = elapsed(record, now); - lines.push(`${marker} ${prefix}${theme.fg("text", icon)} ${theme.fg("text", shortName(record.id))} ${status}${time ? ` ${theme.fg("dim", time)}` : ""}`); - } - if (detail && rows[selected]) { - const { record } = rows[selected]!; - lines.push("", theme.fg("dim", "task:"), ...wrapTextWithAnsi(record.task, width - 2).slice(0, 4).map((line) => theme.fg("text", line))); - if (record.result?.summary) lines.push(theme.fg("dim", "result:"), ...wrapTextWithAnsi(record.result.summary, width - 2).slice(0, 5).map((line) => theme.fg("muted", line))); - if (record.error) lines.push(theme.fg("error", `error: ${truncateToWidth(record.error, width - 2)}`)); - } - lines.push("", theme.fg("dim", "↑↓ navigate · enter toggle details · esc close")); - return lines.map((line) => truncateToWidth(line, width)); - }, - invalidate() { /* rebuilt fresh on every render */ }, - handleInput(data) { - const rows = buildRows(tree); - if (matchesKey(data, Key.up)) { if (selected > 0) { selected--; tui.requestRender(); } } - else if (matchesKey(data, Key.down)) { if (selected < rows.length - 1) { selected++; tui.requestRender(); } } - else if (matchesKey(data, Key.enter)) { detail = !detail; tui.requestRender(); } - else if (matchesKey(data, Key.escape)) { done(); } - }, - dispose() { clearInterval(timer); }, - }; -} diff --git a/src/index.ts b/src/index.ts index f504ee2..546a057 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,6 @@ export default function hypothesisMachine(pi: ExtensionAPI): void { pi.on("session_shutdown", async () => { await supervisor.shutdown(); }); pi.registerCommand("team", { description: "Show the recursive research team", handler: async (_args, ctx) => { ctx.ui.notify(supervisor.team(), "info"); } }); - pi.registerCommand("agents", { description: "Show the interactive agent tree overlay", handler: async (_args, ctx) => { await supervisor.showAgentTree(ctx); } }); pi.registerCommand("research", { description: "Start a bounded research run", handler: async (args, ctx) => { const goal = args.trim(); if (!goal) { ctx.ui.notify("Usage: /research ", "warning"); return; } const state = supervisor.loop; if (!state || !supervisor.tree) throw new Error("Not initialized"); supervisor.tree.setGoal(goal); state.setGoal(goal); state.start(); const prompt = `Research goal: ${goal}\nUse research_control and the recursive agent tools. Create specialized children only when useful. Record each iteration and stop on the coded conditions. Report important progress without flooding the chat.`; if (ctx.isIdle()) pi.sendUserMessage(prompt); else pi.sendUserMessage(prompt, { deliverAs: "followUp" }); } }); pi.registerCommand("research-status", { description: "Show research loop state", handler: async (_args, ctx) => { ctx.ui.notify(JSON.stringify(supervisor.loop?.snapshot() ?? {}, null, 2), "info"); } }); pi.registerCommand("research-pause", { description: "Pause spawning and the loop", handler: async (_args, ctx) => { if (supervisor.loop?.snapshot().status !== "running") { ctx.ui.notify("Cannot pause a non-running loop", "warning"); return; } supervisor.loop?.pause(); supervisor.tree?.pause(); ctx.ui.notify("Research paused", "info"); } }); diff --git a/src/supervisor.ts b/src/supervisor.ts index 7c8dc49..3070b3b 100644 --- a/src/supervisor.ts +++ b/src/supervisor.ts @@ -1,7 +1,6 @@ import { resolve } from "node:path"; import { defineTool, ModelRuntime, type ExtensionAPI, type ExtensionContext, type ModelRegistry, type Theme, type ToolDefinition } from "@earendil-works/pi-coding-agent"; import { Text, truncateToWidth } from "@earendil-works/pi-tui"; -import { createAgentTreeOverlay } from "./agent-tree-ui.js"; import { Type } from "typebox"; import { StringEnum } from "@earendil-works/pi-ai"; import { AgentTree } from "./agent-tree.js"; @@ -64,15 +63,6 @@ export class SupervisorIntegration { team(): string { return this.required().tree.render(); } findings(kind?: string): unknown { return this.required().memory.list(kind); } - /** Open the interactive agent-tree overlay. */ - async showAgentTree(ctx: ExtensionContext): Promise { - const tree = this.required().tree; - await ctx.ui.custom((tui, theme, _kb, done) => createAgentTreeOverlay(tree, tui, theme, () => done(undefined)), { - overlay: true, - overlayOptions: { width: "55%", minWidth: 60, maxHeight: "80%", anchor: "top-right", margin: 1, visible: (termWidth) => termWidth >= 60 }, - }); - } - /** Live subagent dashboard widget above the editor, refreshed on a light timer. */ private installAgentWidget(ctx: ExtensionContext): void { ctx.ui.setWidget("hm-agents", (tui, theme) => { @@ -93,7 +83,7 @@ export class SupervisorIntegration { const active = agents.filter((agent) => agent.status === "running" || agent.status === "waiting"); if (active.length === 0 && state.status !== "running") return []; const now = Date.now(); - const lines: string[] = [theme.fg("accent", `◆ ${state.runId} · ${state.status} · iter ${state.iteration} · ${active.length} active`) + theme.fg("dim", " /agents")]; + const lines: string[] = [theme.fg("accent", `◆ ${state.runId} · ${state.status} · iter ${state.iteration} · ${active.length} active`)]; for (const agent of active) { const elapsed = agent.startedAt ? Math.max(0, Math.round((now - Date.parse(agent.startedAt)) / 1000)) : 0; const stamp = `${String(Math.floor(elapsed / 60)).padStart(2, "0")}:${String(elapsed % 60).padStart(2, "0")}`;